排序(Sort),就是重新排列表中的元素,使表中的元素满⾜按关键字有序的过程
每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中, 直到全部记录插入完成。
顺序查找找到插入的位置, 适用于顺序表、链表
代码实现:
//直接插入排序:
//法一:(无哨兵)
void InsertSort(int A[],int n)
{
int temp;//临时存放待插入的数据,用于比较(因为若小于前面一个数,则要后移覆盖待插入数据的位置)
//将各元素插入已排好序的序列中,A[i]之前的已排好序
for(int i = 1; i < n; i++)
{
if(A[i] < A[i-1])
{
temp = A[i]; //temp暂存A[i],用于比较和比较后的插入
//1.检查所有前面已排好序的元素,下标为0~i-1,所有大于temp的元素都要依次往后挪一位
for(int j = i-1; j >= 0 && A[j] > temp; j--)
{
A[j+1] = A[j];
}
//2.找到下标为j的元素小于等于temp时,插入到下标为j+1的位置(若没找到,退出循环时j=-1,则插入到下标为0的位置)
A[j+1] = temp;
}
}
}
//法二:(带哨兵,数组下标为0的位置存放哨兵,数据存放在下标为1到n的地方,数组大小为n+1)可以每次少比较j>=0
void InsertSort(int A[],int n)
{
//将各元素插入已排好序的序列中,A[i]之前的已排好序
for(int i = 2; i < n+1; i++)
{
if(A[i] < A[i-1])
{
A[0] = A[i]; //复制为哨兵存放在A[0]
//1.检查所有前面已排好序的元素,所有大于哨兵的元素都要依次往后挪一位
for(int j = i-1; A[0] < A[j]; j--)
{
A[j+1] = A[j];
}
//2.找到下标为j的元素小于等于哨兵时,插入到下标为j+1的位置(若没找到,找到哨兵即退出循环j=0,则插入到下标为1的位置)
A[j+1] = A[0];
}
}
}
算法效率分析:
最好情况复杂度:O(n)——原本就有序,共n-1趟处理,每⼀趟只需要对⽐关键字1次,不⽤移动元素
最坏情况复杂度:O(n^2^)——原本为逆序,第i趟需对⽐关键字 i+1次,移动元素 i+2 次
先用折半查找找到应插入的位置, 仅适用于顺序表
过程:
一直到low > high 时才停止折半查找,应将 [low, i-1] 内的元素全部右移,并将 A[0] 复制到 low 所指位置。
注意: 当mid所指元素等于当前元素时,应继续令low = mid+1 , 以保证“ 稳定性”
代码实现:
void InsertSort(int A[],int n)
{
int temp;//临时存放待插入的数据,用于比较(因为若小于前面一个数,则要后移覆盖待插入数据的位置)
int low,high,mid; //折半所用到的指针
//将各元素插入已排好序的序列中,A[i]之前的已排好序
for(int i = 1; i < n; i++)
{
//每次插入都要初始化
low = 0; high = i-1;temp = A[i];
//1.找到要插入的位置low
while(low <= high)
{
mid = (low+high)/2;
if (temp < A[mid])
{
high = mid - 1;
}
else
{
low = mid + 1;
}
}
//2.右移[low, i-1] 内的元素
for(int j = i-1; j >= low; j--)
{
A[j+1] = A[j];
}
//3.插入temp
A[low] = temp;
}
}
算法效率分析:
仅适⽤于顺序表,不适⽤于链表
希尔排序:先将待排序表分割成若⼲形如 L[i, i + d, i + 2d,…, i + kd] 的“特殊”子表,对各个子表分别进行直接插入排序。然后缩小增量d重复上述过程,直到d=1为⽌。(先追求表中元素部分有序,再逐渐逼近全局有序)
增量d也等于分组的数量
增量d的取值可以任意
代码实现:
//希尔排序,哨兵实现(即0存放哨兵,n个数据存到下标为1~n的数组中,数组大小为n+1)
void ShellSort(int A[],int n)
{
int d;
//步长/增量d变化,进行分组
for(d = n/2; d >= 1; d = d/2)
{
//将每组各元素插入已排好序的序列中,A[i]之前的已排好序
for(int i = 1+d; i <= n; i++) //从第二个元素(下标为1+d)开始往前比较,有点像并发,每一次循环会跳转到下一分组进行排序
{
//和同一分组的上一个进行比较
if(A[i] < A[i-d])
{
A[0] = A[i]; //复制为哨兵存放在A[0]
//1.检查所有前面已排好序的元素,所有大于哨兵的元素都要依次往后挪一位
for(int j = i-d; j > 0 && A[0] < A[j]; j = j-d )//注意j>0条件
{
A[j+d] = A[j];
}
//2.找到下标为j的元素小于等于哨兵时,插入到下标为j+d的位置(若没找到,找到哨兵即退出循环j=0,则插入到下标为1的位置)
A[j+d] = A[0];
}
}
}
}
交换排序:根据序列中两个元素关键字的比较结果来交换这两个记录在序列中的位置,分为冒泡排序和快速排序
适⽤于顺序表、链表
从后往前(或从前往后)两两⽐较相邻元素的值,若为逆序(即A[i-1]>A[i]),则交换它们,直到序列⽐较完。称这样过程为⼀趟冒泡排序。
代码实现:
//顺序表从后往前冒泡的实现
void BubbleSort(int A[],int n)
{
int temp;
//一、i表示已排好i个元素,当排好n-1个即可退出循环(总共轮数为i-1躺)
for(int i = 0; i < n-1; i++)
{
bool flag = false; //表示本躺冒泡是否发生交换的标志位
//二、表示每一趟需要比较的次数(i=0时,j取值范围为1~n-1)
for(int j = n-1; j > i; j--)
{
if(A[j-1] > A[j]) //若为逆序则交换,算法是稳定的
{
temp = A[j];
A[j] = A[j-1];
A[j-1] = temp;
flag = true;
}
}
if(flag==false) //若某一趟没有发生交换,说明表已经有序
{
return;
}
}
}
//链表从前往后冒泡的实现
void BubbleSort(int A[],int n)
注意:每次交换都需要移动元素3次
仅适⽤于顺序表,不适⽤于链表
①在待排序表L[1…n]中任取⼀个元素pivot作为枢轴(即基准),通过⼀次划分将待排序表划分为独立的两部分L[1…k-1]和L[k+1…n],使得L[1…k-1]中的所有元素小于pivot,L[k+1…n]中的所有元素大于等于pivot,则pivot放在了其最终位置L(k)上,这个过程称为⼀次“划分”。
②然后分别递归地对两个子表重复上述过程,直⾄每部分内只有⼀个元素或空为⽌,则所有元素放在了其最终位置上。
注意:⼀次“划分”≠⼀趟排序。⼀次划分可以确定⼀个元素的最终位置,⽽⼀趟排序也许可以确定多个元素的最终位置。(对所有尚未确定最终位置的所有元素进⾏⼀遍处理称为⼀趟排序,下图一层QuickSort对应一趟排序)
算法优化思路:
尽量选择可以把数据中分的枢轴元素(枢轴默认取首元素):
①选头、中、尾三个位置的元素,取中间值作为枢轴元素;
②随机选⼀个元素作为枢轴元素
代码实现:
//快速排序(递归调用)
void QuickSort(int A[], int low, int high)
{
if(low < high) //递归跳出的条件
{
int pivotpos = Partition(A , low, high); //确定枢轴位置并划分
QuickSort(A, low, pivot-1); //划分为左子表
QuickSort(A, pivot+1, high); //划分为右子表
}
}
//辅助函数:用数组首元素将序列划分为左右两部分,并返回枢轴位置
int Partition(int A[], int low, int high)
{
//1.确定基准/枢轴
int pivot = A[low];
//2,用low、high搜索枢轴的最终位置,并将数组划分为比枢轴小的左半部分和比枢轴大的右半部分
while(low<high)
{
//用high找到比枢轴小的元素,放到此时A[low]的位置
while(pivot <= A[high] && low<high) //如果没有low
{
high--;
}
A[low] = A[high];
//用low找到比枢轴大的元素,放到此时A[high]的位置
while(pivot >= A[low] && low<high)
{
low++;
}
A[high] = A[low];
}
//当low=high时,表示已经⽤第⼀个元素(枢轴)把待排序序列“划分”为左边更⼩,右边更⼤的两个部分。
//3.此时将基准放入此时low/high所指位置
A[low] = pivot;
return low; //返回枢轴位置
}
把n个元素组织成⼆叉树,⼆叉树的层数就是递归调⽤的层数
(n个结点的⼆叉树最⼩⾼度 =[log2n]向下取整+1,最⼤⾼度 = n)
每⼀层的QuickSort 只需要处理剩余的待排序元素,时间复杂度不超过O(n)
算法优化思路:
若每⼀次选中的“枢轴”将待排序序列划分为很不均匀的两个部分,则会导致递归深度增加,算法效率变低。故若初始序列有序或逆序,则快速排序的性能最差(因为每次选择的都是最靠边的元素)
尽量选择可以把数据中分的枢轴元素:
①选头、中、尾三个位置的元素,取中间值作为枢轴元素;
②随机选⼀个元素作为枢轴元素
选择排序:每⼀趟在待排序元素中选取关键字最⼩(或最⼤)的元素加⼊有序⼦序列
适⽤性:既可以⽤于顺序表,也可⽤于链表
每⼀趟在待排序元素中选取关键字最⼩的元素加⼊有序⼦序列,n个元素的简单选择排序需要 n-1 趟处理(⽆论有序、逆序、还是乱序)
//选择排序:从待排序元素中比较全部,选择最小的加入有序子序列,而不是每次找到更小的值就交换(频繁交换增加复杂度)
void SelectSort(int A[], int n)
{
//共排序n-1躺
for(int i = 0; i < n-1; i++)
{
int min = i; //记录最小元素下标
for(int j = i+1; j < n; j++)
{
if(A[min] > A[j])
{
min = j; //更新最小元素下标
}
}
//若更新了最小元素下标,则需交换(需移动元素三次)
if(min != i)
{
swap(A[i],A[min]);
}
}
}
//辅助函数
void swap(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
归并:把两个或多个已经有序的序列合并成⼀个
(m路归并,每选出⼀个元素需要对⽐关键字 m-1 次)
归并排序:利用归并的思想,把数组内的两个最小的有序序列(在内部排序中⼀般采⽤2路归并,则最小的有序序列只有一个元素)归并为⼀个大的有序序列,再将归并好的有序序列与其他归并好的有序序列依次归并,直至数组有序。(核⼼操作:把数组内的两个有序序列归并为⼀个,即归并)
//创建一个临时数组B[]存放所有A[]的元素(为避免每一次调用都创建一个数组,在函数体外面创建)
int *B = new int[sizeof(A)/sizeof(A[0])]; //数组非空
//归并排序
void MergeSort(int A[], int low, int high)
{
//递归划分到最小,先归并最小的两个有序数组,将左右两个⼦序列分别进⾏归并排序(最小时每个⼦序列只含有1个元素)
if(low < high)
{
int mid = (low+high)/2; //从中间划分
MergeSort(A, low, mid);
MergeSort(A, mid+1, high);
Merge(A,low,mid,high);
}
}
//辅助函数:A[low...mid]和A[mid+1...high]各自有序,将两个部分归并
void Merge(int A[], int low, int mid, int high)
{
//1.将所有A[low]~A[high]的元素复制到数组B[]中
for(int k = low; k <= high; k++)
{
B[i] = A[i];
}
//2.i指向B[]中左边有序数组的待归并元素,j指向B[]中右边有序数组的待归并元素,k指向A[]中归并插入的位置
int i = low;
int j = mid + 1;
for(int k = i; i <= mid && j <= high; k++)
{
//归并,将较小值复制到A中
if(B[i]<=B[j]) //保证稳定
{
A[k] = B[i++];
}
else
{
A[k] = B[j++];
}
}
while(i <= mid) //若B[]中某一个有序数组已插入完,将另一个的有序数组剩下的插入到A[]
{
A[k++] = B[i++];
}
while(j <= high)
{
A[k++] = B[j++];
}
}
2路归并的“归并树”——形态上就是⼀棵倒⽴的⼆叉树
高度为h的二叉树至多有 (2h-1)个结点,n个元素进⾏2路归并排序,应满足n ≤ 2h-1,归并趟数为h-1 = [log2n]向上取整
结论:n个元素进⾏2路归并排序,归并趟数= [log2n]向上取整
基数排序不是基于“比较”的排序算法
基数排序通常基于链式存储实现
假设长度为n的线性表中每个结点aj的关键字由d元组 (kjd−1,kjd−2,…kj0 )组成。其中,0 ≤ kji ≤ r - 1(0 ≤ j<n, 0 ≤ i ≤d - 1,r 称为“基数”)————kjd−1称为最⾼位关键字(最主位关键字),kj0 称为最低位关键字(最次位关键字)
如358结点,由三元组(百、十、个)组成,元组中每个取值范围为0~9
按照各个关键字位权重递增的次序做“分配”和“收集”,可使队列内部有序(如要获得递减的序列,则当以百位进行分配时,十位已经从大到小排好序,进入队列时是十位大的优先入队)
代码实现:
//单链表结点和链表结构
typedef struct LinkNode{
ElemType data;
struct LinkNode*next;
}LinkNode, *LinkList;
//链式队列结构
typedef struct{
LinkNode *front,*rear;//队列的队头和队尾指针
}LinkQueue;
//收集队列内元素步骤:(故收集⼀个队列只需O(1)时间)
p->next = Q[6].front;
Q[6].front = NULL ;
Q[6].rear = NULL ;
p = ...;
(把关键字拆为d个部分,每个部分可能取得 r 个值)
基数排序擅长解决的问题:
①数据元素的关键字可以⽅便地拆分为 d 组,且 d 较⼩
②每组关键字的取值范围不⼤,即 r 较⼩
③数据元素个数 n 较⼤