一、排序算法的稳定性
排序就是使无序的序列排列成有序的序列,针对两个元素,其对应的关键字相同,若待排序的元素中有两个元素A和B,未排序前A的位置在B的前面,若经排序算法后,A仍在B的前面,则说明这个排序算法是稳定的,即经过排序后能使关键字相同的元素保持原本顺序中的相对位置不变,则称这个算法是稳定的,反之则不稳定。另外,算法的稳定性并不代表该排序算法的优劣。
二、排序算法的分类
1、根据所要排序的元素是否完全在内存中进行排序,可分为内部排序和外部排序:
排序 | 特点 |
内部排序(In-place) | 排序的元素完全在内存中 |
外部排序(Out-place) | 在排序过程中不断在内、外存之间交换 |
2、通常将排序算法分为以下:
三、插入排序
(一)直接插入排序
直接插入排序是将要排序的序列按照关键字的大小插入至已排好序的子序列中,一直进行直到整个序列有序,代码如下:
/*直接插入排序(由小到大)*/ void InsertSort(int r[],int n) { int i,j,temp; for(i=1; i<n; ++i) { temp=r[i]; //将要插入的元素暂存在temp中 for(j>=0,j=i-1;temp<r[j];--j) r[j+1]=r[j]; //向后挪一位 r[j+1]=temp; //找到插入位置并插入 } }
分析:
(1)进行直接插入排序的元素个数为n,排序过程中,向有序子表中逐次地插入元素执行了n-1趟操作,其中比较次数和移动次数取决于排序表初始状态;
例如,序列{21,32,46,40,80,69,90,94},其比较次数为:第一趟,插入32,比较1次;第二趟,插入46,比较1次;第三趟,插入40,由于40<46且40>32,即比较2次;第四趟,插入80,比较1次;第五趟,插入69,由于69<80且69>40,比较2次;第六趟,插入90,比较1次;第七趟,比较1次,所以一共比较次数为9次。
(2)空间复杂度:直接插入排序的空间复杂度为O(1);
(3)时间复杂度:最好情况下,即元素都有序,此时只需比较元素而不需移动元素,最好时间复杂度为O(n),而最坏情况下,即排好的序列刚好与初始序列相反,呈逆序排列,则此时比较次数和移动次数都到达最大值,最坏时间复杂度为O(n2),而考虑平均情况下,总的比较次数和移动次数约为n2/4,故直接插入排序的时间复杂度为O(n2);
(4)稳定性:由于每次插入元素时总是从后向前比较后再移动,所以不会出现相同元素相对位置发生变化的情况,所以直接插入排序是稳定的;
(5)对于一个含有n个元素的顺序表,对其采用直接插入排序,最好情况下的比较次数为n-1,最坏情况下的比较次数为n(n-1)/2;
(6)适用性:直接插入排序可适用于顺序存储和链式存储的线性表,当链式存储时,可以从前向后进行比较查找插入位置;
(7)排序方式:直接插入排序是一种内部排序(In-place)。
例如,对于一个序列{2,1,0,5,3}进行直接插入排序,代码如下:
#include<stdio.h> #define MAXSIZE 100 /*创建函数*/ void Create(int r[],int n) { for(int i=0; i<n; i++) { printf("输入第%d个元素:",i+1); scanf("%d",&r[i]); } } /*输出函数*/ void Display(int r[],int n) { for(int i=0; i<n; i++) printf("%d ",r[i]); } /*直接插入排序(由小到大)*/ void InsertSort(int r[],int n) { int i,j,temp; for(i=1; i<n; ++i) { temp=r[i]; //将要插入的元素暂存在temp中 for(j>=0,j=i-1;temp<r[j];--j) r[j+1]=r[j]; //向后挪一位 /*j=i-1; while(j>=0&&temp<r[j]) { //若大于待排元素,则后移一位 r[j+1]=r[j]; //向后挪一位 --j; }*/ r[j+1]=temp; //找到插入位置并插入 } } /*主函数*/ int main() { int n; int r[MAXSIZE]; printf("请输入排序表的长度:"); scanf("%d",&n); Create(r,n); printf("已建立的序列为:\n"); Display(r,n); InsertSort(r,n); printf("\n"); printf("排序后的序列为:\n"); Display(r,n); }
运行结果如下:
具体的执行步骤如下:
1、首先对于序列{2,1,0,5,3}中的2,它是有序的;
2、由于1<2,所以插入1。2向后移动一个位置,1插入到2的原本的位置,此次排序后的序列为:{1,2,0,5,3};
3、由于0<2,所以插入0。2向后移动一个位置,0插入到1的原本的位置,另外由于0<1,所以继续插入,1向后移动一个位置,即0应该插入至最前面,此次排序后的序列为:{0,1,2,5,3};
4、由于5>2,所以不需要移动,5理应在2的后面,此次排序后的序列为:{0,1,2,5,3};
5、由于3<5,所以插入3。5向后移动一个位置,3插入到5的原本的位置,从后向前逐个比较,位置合理,直接插入排序过程结束,最后的排序结果为此次排序后的序列为:{0,1,2,3,5}。
(二)折半插入排序
折半插入排序是针对直接插入排序的改进,主要改变了查找插入位置的方法,即它是采用折半查找法(二分查找)来查找的。
折半查找搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。
先折半查找元素的插入位置,然后移动插入位置之后的所有元素。
/*折半插入排序*/ void Binary_InsertSort(int r[],int n) { int i,j,temp,low,high,mid; for(i=1; i<=n; i++) { temp=r[i]; //将要插入的元素暂存在temp中 low=0; high=i-1; //low和high为折半查找的范围 while(low<=high) { mid=(low+high)/2; //mid取中间点 if(r[mid]>temp) //查找左半子表 high=mid-1; else //查找右半子表 low=mid+1; } for(j=i-1; j>=high+1; j--) //先后移动元素,空出位置留给插入元素 r[j+1]=r[j]; r[j+1]=temp; //找到插入位置并插入 } }
分析:
(1)空间复杂度:与直接插入排序一样,空间复杂度仍为O(1);
(2)时间复杂度:折半插入排序减少了比较元素的次数,其移动次数与直接插入排序是一样的,所以其时间复杂度与其是一样的。最好时间复杂度为O(nlog2n),而最坏情况下,最坏时间复杂度为O(n2),而考虑平均情况下,折半插入排序的时间复杂度仍为O(n2);
(3)稳定性:折半插入排序也是一种稳定的排序算法;
(4)适用性:折半插入排序只适用于顺序表,不适用于链表;
(5)排序方式:折半插入排序是一种内部排序(In-place)。
例如,对于一个序列{1,0,2,5,3,4}进行直接插入排序,代码如下:
#include<stdio.h> #define MAXSIZE 100 /*创建函数*/ void Create(int r[],int n) { for(int i=0; i<n; i++) { printf("输入第%d个元素:",i+1); scanf("%d",&r[i]); } } /*输出函数*/ void Display(int r[],int n) { for(int i=0; i<n; i++) printf("%d ",r[i]); } /*折半插入排序*/ void Binary_InsertSort(int r[],int n) { int i,j,temp,low,high,mid; for(i=1; i<=n; i++) { temp=r[i]; //将要插入的元素暂存在temp中 low=0; high=i-1; //low和high为折半查找的范围 while(low<=high) { mid=(low+high)/2; //mid取中间点 if(r[mid]>temp) //查找左半子表 high=mid-1; else //查找右半子表 low=mid+1; } for(j=i-1; j>=high+1; j--) //先后移动元素,空出位置留给插入元素 r[j+1]=r[j]; r[j+1]=temp; //找到插入位置并插入 } } /*主函数*/ int main() { int n; int r[MAXSIZE]; printf("请输入排序表的长度:"); scanf("%d",&n); Create(r,n); printf("已建立的序列为:\n"); Display(r,n); Binary_InsertSort(r,n); printf("\n"); printf("排序后的序列为:\n"); Display(r,n); }
运行结果如下:
(三)希尔排序
希尔排序也称为缩小增量排序,从这一点可看出它是通过选取一定的增量来排序的,其本质还是插入排序,通过增量将序列分为几个子序列,然后对每个子序列进行直接插入排序,代码如下(下例代码中每次增量除以2向下取整,实际情况可以按需求取不同增量):
/*希尔排序*/ void ShellSort(int r[],int n) { int i,j,temp,dk; dk=floor(n/2); //初始增量为序列长度的一半然后向下取整 while(dk>0) { for(i=dk; i<n; i++) { temp=r[i]; //将要插入的元素暂存在temp中 j=i-dk; while(j>=0&&temp<r[j]) { r[j+dk]=r[j]; //记录后移,查找插入的位置 j=j-dk; } r[j+dk]=temp; //找到插入位置并插入 j=j-dk; } dk=floor(dk/2); //步长变化,除以2继续向下取整 } }
分析:
2、空间复杂度:希尔排序的空间复杂度为O(1);
3、时间复杂度:希尔排序的时间复杂度依赖于增量的选取(设n为序列长度)。
(1)若每次所取增量除以2向下取整,即⌊ n/2 ⌋、⌊ n/4 ⌋、……、⌊ n/2k ⌋、……、2、1,此时的时间复杂度为O(n2)。
⌈ ⌉表示向上取整,取比自己大的最小整数,⌊ ⌋表示向下取整,取比自己小的最大整数)。总结一下c/c++里面如何使用取整函数,使用前需要加上<math.h>头文件,通过使用floor()函数,返回的是小于或等于x的最大整数【向下取整】;通过使用ceil()函数,返回的是大于x的最小整数【向上取整】。
(2)若每次所取增量为2k+1(小于序列长度n,2k+1<n)依次递减,即2k+1、……、9、5、3、1(k为大于或等于1的整数),此时的时间复杂度为O(n1.5)。
希尔排序增量选取中,增量序列的最后一个值一定取1;增量序列中的值应尽量没有除1之外的公因子。
4、稳定性:由于分为不同子序列后,可能会出现改变其相对位置情况,所以希尔排序是不稳定的;
5、适用性:希尔排序只适用于以顺序存储结构存储的线性表;
6、排序方式:希尔排序是一种内部排序(In-place)。
例如,对于一个序列{4,-1,2,1,8}进行直接插入排序,每次所取增量除以2向下取整,代码如下(floor()函数需在开头加<math.h>头文件):
#include<stdio.h> #include<math.h> #define MAXSIZE 100 /*创建函数*/ void Create(int r[],int n) { for(int i=0; i<n; i++) { printf("输入第%d个元素:",i+1); scanf("%d",&r[i]); } } /*输出函数*/ void Display(int r[],int n) { for(int i=0; i<n; i++) printf("%d ",r[i]); } /*希尔排序*/ void ShellSort(int r[],int n) { int i,j,temp,dk; dk=floor(n/2); //初始增量为序列长度的一半然后向下取整 while(dk>0) { for(i=dk; i<n; i++) { temp=r[i]; //将要插入的元素暂存在temp中 j=i-dk; while(j>=0&&temp<r[j]) { r[j+dk]=r[j]; //记录后移,查找插入的位置 j=j-dk; } r[j+dk]=temp; //找到插入位置并插入 j=j-dk; } dk=floor(dk/2); //步长变化,除以2继续向下取整 } } /*主函数*/ int main() { int n; int r[MAXSIZE]; printf("请输入排序表的长度:"); scanf("%d",&n); Create(r,n); printf("已建立的序列为:\n"); Display(r,n); ShellSort(r,n); printf("\n"); printf("排序后的序列为:\n"); Display(r,n); }
运行结果如下:
四、总结
三种插入排序的总结如下表:
排序算法 | 空间复杂度 | 平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 排序方式 | 稳定性 | 适用性 |
直接插入排序 | O(1) | O(n2) | O(n) | O(n2) | 内部排序(In-place) | √ | 顺序存储和链式存储 |
折半插入排序 | O(1) | O(n2) | O(nlog2n) | O(n2) | 内部排序(In-place) | √ | 顺序存储 |
希尔排序 | O(1) | 依赖于增量序列 | 依赖于增量序列 | 依赖于增量序列 | 内部排序(In-place) | × | 顺序存储 |