排序算法总结
戎马一生 2021-7-31 撰稿
排序的基本概念
排序就是按照表中元素关键字的大小关系,对元素进行重新排列的过程。
内部排序
一、插入排序
基本思想:
每次将一个待排序的元素按其关键字的大小插入到前边已经排序好的子序列中,直到所有的元素都插入完成。
1、直接插入排序
代码实现:
#临时变量的插入排序算法
void InsertSort(ElemType A[],int n)
{
int i,j,temp;
for(i=1;i<=n;i++)
{
if(A[i]<A[i-1]){
temp=A[i];//复制给临时变量
for(j=i-1;temp<A[j]&&j>=0;--j){//从后往前查找待插入位置
A[j+1]=A[j];//向后挪位
}
A[j+1]=temp;//复制到插入位置
}
}
}
#带有哨兵的插入排序算法
void InsertSort(ElemType A[],int n)
{
int i,j;
for(i=2;i<=n;i++)
{
if(A[i]<A[i-1]){
A[0]=A[i];//复制为哨兵,A[0]不存放元素
for(j=i-1;A[0]<A[j];--j){//从后往前查找待插入位置
A[j+1]=A[j];//向后挪位
}
A[j+1]=A[0];//复制到插入位置
}
}
}
效率分析:
空间效率:仅使用了常数个辅助单元,空间复杂度为O(1).
时间效率:在排序过程中,向有序子表中插入元素进行了n-1次,每次操作都是比较关键字和移动元素,因此时间复杂度取决于待排序表的初始状态。最好的情况:已有序,只需比较一次O(1)。最坏的情况:逆序。比较次数和移动次数约为n^2/4。综合:直接插入排序的算法时间复杂度为O(n2)
稳定性:每次是从后往前比较,所以相对位置不发生变化。是稳定的排序算法。
适用性:适用于顺序存储和链式存储的线性表。链式(从前往后)。
2、折半插入排序
基本思想:
由于查找待插入位置于复制元素两个步骤是分离的,对于基于顺序表实现的插入排序,可以将查找步骤按照折半查找来实现,在确定插入位置后,再统一向后移动元素。
代码实现:
void InsertSort(ElemType A[],int n)
{
int i,j,low,high,mid;
for(i=2;i<=n;i++){
A[0]=A[i];//哨兵暂存
low=1;high=i-1;//折半查找的范围
while(low<=high){
mid=(low+high)/2;
if(A[mid]>A[0])
high=mid-1;
else
low=mid+1;//按照之前的插入排序,当A[mid]=A[0]时,应该停止查找。
//此处为保证插入排序算法的稳定性,A[mid]=A[0]时,
//继续向右查找,则low+1跳过此值,保证了原来的位置不发生改变。
}
//找到位置应该是high之后,low之前即low=high+1
for(j=i-1;j>=low;--j)//此时将[low,i-1]元素后移
{
A[j+1]=A[j];
}
A[low]=A[0];//A[low]位置赋值
}
}
效率分析:
空间复杂度O(1).
**时间复杂度O(n2).**比较的次数为O(nlog2(n)).但是移动次数不变。
稳定性:稳定的排序算法
3、希尔排序
基本思想:
由于插入排序更加适合于基本有序的排序表和数量不大的排序表。希尔排序是基于这两点改进而来:即缩小增量排序,先追求部分有序,然后逼近全局有序。
先将待排序表按照某个增量d划分为若干个子序列,对每个子表内部分别进行直接插入排序。最后对全体记录进行依次插入排序。
常用的增量为d1=n/2,di+1=Ldi/2….最后一个为d=1.
代码实现:
//插入排序之希尔排序
void InserSort(ElemType A[],int n)
{
int temp;//
for(int dk=n/2;dk>=1;dk++)//步长变化
{
for(int i=dk+1;i<=n;i++){//1、i指向子序列的下一个元素
//2、当dk不是对半分的时候,i++,则会切换子表进行插入排序
if(A[i]<A[i-dk]){//如果需要将A[i]调整
temp=A[i];
for(int j=i-dk;j>0&&temp<A[j];j-=dk)//后移,直到j<0或者temp<A[j]
A[j+dk]=A[j];
A[j+dk]=temp;//插入元素
}
}
}
}
效率分析:
空间效率:O(1)
时间效率:O(n1.3)<O(n2)
稳定性:不稳定,当相同的关键字划分到不同的子表中,相对位置会发生改变。
适用性:仅仅适用于线性表为顺序存储的情况。
二、交换排序
所谓交换排序就是根据两个元素关键字的大小来对换这两个记录在序列中的位置。
1、冒泡排序
基本思想:从前往后两两比较相邻元素的值,若为逆序即(A[i-1]>A[i]),则交换他们swap(),直到序列比较完。**此为一趟冒泡排序,结果是将最大的一个元素放到正确的位置。**最多需要n-1趟冒泡排序则可以排好序。
代码实现:
//交换排序之冒泡排序
void BubbleSort(ElemType A[],int n){
for(int i=0;i<n;i++)
{
bool flag=false;//标志本次冒泡是否发生了交换
for(int j=n-1;j>i;j--){//从后往前上浮,将最小的元素放置第一个位置
//也可以写成for(int j=i+1;j<n;j++) 从前往后
if(A[j-1]>A[j]){
swap(A[j-1],A[j]);//交换元素
flag=true;
}
}
if(flag=false)//本次遍历没有元素交换说明已经有序
return;
}
}
效率分析:
空间效率:O(1)
时间效率:最好情况是有序序列,只需比较n-1次。为O(n),最坏的情况是逆序,n-1趟排序,第i趟排序进行n-i次比较,每次的比较进行3次的移动,这种情况下时间复杂度为O(n2)平均时间复杂度为O(n2)。
稳定性:由于j>i,且A[j]>A[i]时,不发生交换,所以是稳定的排序算法。
适用性:适用于顺序表和链表。
2、快速排序
**基本思想:**基于分治法的,枢轴元素pivot将待排序表划分为两个子列,pivot左边的元素都小于它,右边的都大于它。这称为一次的快速排序。对两边的子列进行上述的过程,直至每部分内只有一个元素或空为止。
代码实现:
//快速排序
void QuickSort(ElemType A[],int low,int high)
{
if(low<high)
{
int pivotpos=Partition(A,low,high);//划分
QuickSort(A,low,pivotpos-1);//递归左半部分
QuickSort(A,pivotpos+1,high);
}
}
int Partition(ElemType A[],int low,int high){
ElemType pivot=A[low];//通常以第一个元素为枢轴元素
while(low<high){
while(low<high&&A[high]>=pivot) --high;
A[low]=A[high];//将比枢轴元素小的移动到左端
while(low<high&&A[low]<=pivot) ++low;
A[high]=A[low];//将比枢轴元素大的移动到右端
}
A[low]=pivot;//枢轴元素放置最终位置
return low;
}
效率分析:
空间效率:递归的栈。最好的情况O(log2(n)),最坏的情况下O(n).平均为O(log2(n))。
时间效率:如果序列基本有序或者逆序的情况下,最坏为O(n2)。最理想的情况下:划分为均衡划分,则时间复杂度为O(nlog2n)。快速排序算法是内部排序算法中的算法平均性能最好的算法。
稳定性:不稳定的排序算法。
注意:一次快排并不会产生有序序列,但是它会将枢轴元素放到最终的位置上。
三、选择排序
基本思想:每一次在后面的n-i+1个元素中选择关键字最小的元素,作为有序子列的第i 个元素,直到n-1趟做完,待排元素只剩下一个就不用再选择了。
1、简单选择排序
**基本思想:**最基本的选择排序,如上面的思想一样,每一次只需从待排子列中找出最小的元素于位置i处的元素交换位置即可。每一次即可确定一个元素的最终位置。
代码实现:
//选择排序之简单选择排序
void SelectSort(ElemType A[],int n)
{
for(int i=0;i<n-1;i++){//一共需要进行i-1趟
min=i;//记录最小元素的位置
for(int j=i+1;j<n;j++){//在A[i...n-1]中选择最小元素
if(A[j]<A[i]){
min=j;//更新最小元素的位置
}
}
if(min!=i) swap(A[i],A[min]);//交换最小元素
}
}
效率分析:
空间效率:O(1)
时间效率:元素移动:最好的情况是已经有序,移动0次。最坏不会超过3(n-1)次。元素比较:始终是n(n-1)/2次。因此时间复杂度为O(n2)。
稳定性:不稳定。
2、堆排序(重点)
堆的ADT:一棵符合以下性质的二叉树,是一棵完全树,满足堆次序。基本操做:创建空堆、检查堆是否为空、插入一个项、提取最大元素、最小元素。
1、堆的插入:插入的新元素先放入树底,然后对该元素不断**上升,**直到不能上升为止。(小根堆为例)
2、堆的删除:用堆底元素代替被删除的元素,然后对该元素不断下坠,直至不能下坠为止。(小根堆为例)
**堆排序的基本思想:**首先将一维数组建成初始堆,以大根堆为例,顶端是最大值,交换堆顶与堆底的元素,此时大根堆的性质破坏,则需要堆顶下调算法以维持大根堆形状。,再输出堆顶,反复如此,即可得到序列的从小到大的排序。核心:1构建初始堆;2、调整堆算法。注意:大根堆是正序排序,小根堆是逆序排序
代码实现:
//选择排序之堆排序
//建立大根堆
void BuidMaxHep(ElemType A[],int len)
{
for(int i=len/2;i>0;i--)//从i=[n/2]~1反复调整堆,因为1-len/2的节点是分支节点
HeadAdjust(A,i,len);
}
//将元素k为根的子树进行调整
void HeadAdjust(ElemType A[],int k,int len)
{
A[0]=A[k];//暂存根节点
for(int i=2*k;i<=len;i*=2){
if(i<len&&A[i]<A[i+1])//如果存在右孩子,且更大
i++;//则指向右孩子
if(A[0]>=A[i]) break;//此时不用调正
else{ //此时需要调整
A[k]=A[i];
k=i;//修改k的值,继续向下调整
}
}
A[k]=A[0]; //根放入最终位置
}
//堆排序
void HeapSort(ElemType A[],int len)
{
BuildMaxHeap(A,len);//创建初始堆
for(int i=len;i>1;i--)//n-1次交换和调整堆的过程
{
Swap(A[i],A[1]);//通常将堆顶与堆底互换
HeapAdjust(A,1,i-1);//使用下调算法调正最大堆 ;每次传入的长度减少1,因为最大的值已经放在最后位置
}
}
效率分析:
空间效率:O(1),交换的临时空间
时间效率:初始堆为O(n),n-1次的下调工作,每次下调的时间复杂度与树高有关为O(log2n)。综上堆排序的时间复杂度为O(nlog2(n))。
稳定性:不稳定的。
适用性:关键字比较多的情况。最好用顺序存储,也可链式存储。
四、归并排序
**基本思想:**归并的含义是将两个或两个以上的有序表组合成一个新的有序表。假定待排序表有n个元素,可将其视为n个有序子表,然后两两归并,一之递归下去……合成一个长度为n的有序表为止。
代码实现:
//二路归并排序
ElemType *B=(ElemType * )malloc((n+1)*sizeof(ElemType));//辅助数组
void Merge(ElemType A[],int low,int mid,int high)
{//表A的两段A[low..mid]和A[mid+1,high]各自有序,将他们进行合并
for(int k=low;k<=high;k++){
B[k]=A[k];//暂存辅助数组中
}
for(int i=low,j=mid+1,k=i;i<=mid&&j<high;k++)
{
if(B[i]<=B[j]){//比较B中左右两段的元素
A[k]=B[i];
i++;
}
//将值较小的赋值到A中
else{
A[k]=B[j];
j++;
}
}
while(i<=mid){//如果第一段未检测完,复制到后面
A[k]=B[i];
k++;
i++;
}
while(j<=high){//如果第二段未检测完,复制到后面
A[k]=B[j];
k++;
j++;
}
}
void MergeSort(ElemType A[],int low,int high)
{
if(low<high){
int mid=(low+high)/2;//对半划分
MergerSort(A,low,mid);//对左半部分递归排序
MergerSort(A,mid+1,high);//对右半部分递归排序
Merge(A,low,mid,high);//归并最终结果
}
}
效率分析:
空间效率:merger操作需要n个辅助单元,因此为O(n)。
时间效率:每次归并的时间复杂度为O(n),一共需要【log2n】次,所以为O(nlog2n).
稳定性:稳定的。
五、基数排序
**基本思想:**基于关键字的各个数位的大小进行排序,适用于多为数排序。通常有两种思路:1、最高位优先MSD;2、最低位优先LSD。
1、**分配:**按照最低位数字构造0-9共9个队列,将待排序数字分配到9个队列中去。
2、**收集:**将0-9队列里的数字串联。此时已经按照最低位大小进行排序。
3、**循环:**按照待排序数字的最大位数,循环1、2.注意:需要将低位数高位补0以变成位数相同的数字
效率分析:
空间效率:一趟分配需要r个队列,但以后会重复使用。因此位O®。
时间效率:需要进行d趟分配和收集,一次分配需要O(n),收集需要O®。所以时间复杂度为O(d(n+r))。
稳定性:稳定的。
六、内部排序算法的对比分析
算法种类 | 时间效率 | 空间复杂度 | 是否稳定 | ||
---|---|---|---|---|---|
最好情况 | 平均情况 | 平均情况 | |||
直接插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 是 |
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 是 |
简单选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 否 |
希尔排序 | O(n^1.3) | O(1) | 否 | ||
快速排序 | O(nlog2n) | O(nlog2n) | O(n^2) | O(log2n) | 否 |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 否 |
2路归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 是 |
基数排序 | O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O® | 是 |
七、排序算法的选择
1、若n较小,采用直接插入排序或简单选择排序。
2、若初始状态已经基本有序,采用直接插入排序或者冒泡排序为宜。
3、若n较大,宜采用O(nlog2n)的排序算法,快排是目前公认最好的排序算法。堆排序所需的辅助空间少。但是这两个算法都是不稳定的,若要求稳定,则采用二路归并算法为宜。(最佳搭配:直接插入排序和归并进行结合)
4、当记录关键字本身信息量较大时,为避免消耗大量的移动,因此采用链表存储为宜。
外部排序
对于大文件进行排序,无法将整个文件一次性读入内存,因此需将外存中的文件一部分一部分调入内存排序。
1、**划分:**根据内存缓冲区的大小将外存上的文件划分为若干的长度为l的子文件,一次读入内存进行排序,将排好序的子文件一次写回外存,这些子文件称为归并段或顺串。
2、**归并:**对着些归并段进行逐趟归并,使归并段由小到大,直至整个有序文件为止。
注意:增大归并的路数,可以减少归并的次数,进而减少总的磁盘I/O次数。
多路平衡归并—-败者树
**败者树用于增大归并路数:**是一颗完全二叉树,叶子节点存放参加比赛的选手,中间节点记录失败者的标号。每一层相当于一轮比赛。
因为k路归并的败者树深度为log2k,因此k个记录中关键字最小的记录最多需要log2k次比较。因此总的比较次数为(n-1)log2r.
置换选择排序算法增大归并段长度来减少归并段个数