1、序言
这是《漫谈经典排序算法系列》第二篇,解析了各种插入排序算法。主要包括:直接插入排序、折半插入排序、表插入排序、希尔插入排序。每一种算法的开头都叙述了引出该算法的原因,然后给出代码,最后分析算法效率及和其他插入排序相比,优劣在哪里。
各种排序算法的解析请参考如下:
《漫谈经典排序算法:五、线性时间排序(计数、基数、桶排序)》
注:为了叙述方便,本文以及源代码中均不考虑A[0],默认下标从1开始。
2、直接插入排序
2.1 引出
给定待排序序列A[ 1…..n ],现假设A[1…i]已经有序,那么我们取出A[i+1]插入到序列A[1…i].这样有序序列记录数就增加了1.如此重复上述操作,不断取出记录插入有序序列,直到A[n]插入到有序序列,排序完成。
2.2 代码
//直接插入排序 void straightInsertSort(int *a,int n) { int i,j; int temp; //逐个记录插入有序序列 for(i=2;i<=n;i++){ temp=a[i]; //把a[i]插入有序序列 for(j=i-1;j>=1;j--){ if(temp<a[j]){ a[j+1]=a[j]; }else break; } a[j+1]=temp; } }
2.3 效率分析
容易看出,要插入的记录个数为n-1,其中关键字的比较次数和记录移动次数是依赖于给出的待排序序列是否基本有序。在最佳情况下(待排序序列有序),比较次数和移动次数时间为o(1),所以时间复杂度为o(n).在最坏情况下(待排序序列逆序)和平均时间均为o(n^2).从上述分析中可以看出,直接插入排序适合记录数比较少、给定序列基本有序的情况。熟悉了排序过程我们发现,直接插入排序是一种稳定的原地排序算法。
3、折半插入排序
3.1 引出
在直接插入排序过程中,我们是把一个记录插入到有序序列中,至于要插入到有序序列中的哪个位置呢?采用的是顺序查找确定插入的位置。显然对于有序序列,折半查找的效率要高,所以在寻找插入位置时可以用折半查找。折半查找主要分为三步:1、查找要插入的位置 2、移位 3、把记录插入相应的位置。
3.2 代码
//折半查找 int binarySearch(int *a,int low,int high,int key) { int mid=(low+high)/2; if(low>high) return low; if(a[mid]==key) return mid; else if(key<a[mid]) return binarySearch(a,low,mid-1,key); else return binarySearch(a,mid+1,high,key); } //折半插入排序 void binaryInsertSort(int *a,int n) { int i,j,site,temp; for(i=2;i<=n;i++){ //1.折半查找要插入的位置 site=binarySearch(a,1,i,a[i]); temp=a[i]; //2.移位 for(j=i;j>site;j--) a[j]=a[j-1]; //3.插入a[i] a[site]=temp; } }
3.3 效率分析
折半插入排序是对直接插入排序的一种改进,这种改进只考虑了关键字比较次数,并没有减少移位次数,所以平均时间和最坏情况下(待排序序列逆序)时间复杂度o(n^2),如果记录数量很大的话,这两种情况下是优于直接插入排序。再来看一下最佳情况(待排序序列有序),此时关键字比较次数并不为o(1),时间复杂度为o(n*log2n)。(其中折半查找时间复杂度o(log2n),这个在以后写查找的时候再分析,这里不做详细讲解。)。所以在记录数较小、待排序序列基本有序情况下直接插入排序优于折半插入排序。此外,折半插入排序是不稳定的原地排序,实现起来也较复杂。
4、表插入排序
4.1 引出
折半插入排序相对于直接插入排序来说减少了比较次数。那么我们可不可以减少移动次数呢,答案是可以的。于是就有了表插入排序,用一个静态链表来存储待排序序列,其他操作和直接插入排序很像。主要步骤:1、初始化链表 2、取出要插入的记录 3、遍历链表寻找插入位置 4、记录插入链表中。
4.2 代码
//静态链表 typedef struct { int key;//关键字 int next;//指向下一个关键字的下标 }Node,*PNode; //表插入排序 void tableInsertSort(PNode list,int n) { int p,head; int i; //初始化 list[0].next=1; list[1].next=0; //逐个插入 for(i=2;i<=n;i++){ head=0; p=list[0].next; //遍历链表,寻找插入位置 while(p!=0 && list[p].key<=list[i].key){ head=p; p=list[p].next; } if(p==0){//插入的值是最大值 list[i].next=p; list[head].next=i; }else{ list[i].next=p; list[head].next=i; } } }
4.3 效率分析
表插入排序也是对直接插入排序的一种改进,这种改进只减少了移动次数,并没有减少关键字比较次数,所以平均时间和最坏情况下(待排序序列逆序)时间复杂度o(n^2),如果记录数量很大的话,这两种情况下是优于直接插入排序。再来看一下最佳情况(待排序序列有序),关键字比较次数并为o(1),时间复杂度为o(n)。此时和直接插入排序时间复杂度一样。此外,表插入排序改变了记录的存储结构,无法顺序访问,是一种稳定的排序算法,实现起来也较复杂。
5、希尔插入排序
5.1 引出
上述两种排序都是只考虑减少关键字比较次数或者只考虑减少关键字移动次数。有没有别的改进办法呢?我们注意到,直接插入排序适合于记录数较少、基本有序的情况。于是我们可以先将整个待排序序列分割成若干子序列分别进行直接插入排序,整个序列基本有序时,再对序列进行一次直接插入排序。这就是希尔排序。
5.2 代码
//一趟增量为dk的希尔插入排序 void shellInsert(int *a,int n,int dk) { int i,j,temp; for(i=dk+1;i<=n;i+=dk){ temp=a[i]; for(j=i-dk;j>=0;j-=dk) if(a[j]>temp) a[j+dk]=a[j]; else break; a[j+dk]=temp; } } //希尔排序 void shellSort(int *a,int n) { int i; int dk[]={5,4,3,2,1}; for(i=0;i<5;i++) shellInsert(a,6,dk[i]); }
5.3 效率分析
当给定序列记录量较大时,希尔排序性能优于直接插入排序。再希尔排序的过程中,关键字是跳跃式移动的,这样就减少了移动次数。希尔排序性能的分析是一个复杂的问题,时间与所取的增量有关。增量选取的不好可能会大大降低排序效率。
6、附录
附录一、参考书籍
《数据结构》严蔚敏版
附录二、所有源代码
#include<stdio.h> //直接插入排序 void straightInsertSort(int *a,int n) { int i,j; int temp; //逐个记录插入有序序列 for(i=2;i<=n;i++){ temp=a[i]; //把a[i]插入有序序列 for(j=i-1;j>=1;j--){ if(temp<a[j]){ a[j+1]=a[j]; }else break; } a[j+1]=temp; } } void main() { int i; int a[7]={0,3,5,8,9,1,2};//不考虑a[0] straightInsertSort(a,6); for(i=1;i<=6;i++) printf("%-4d",a[i]); printf("\n"); }
#include<stdio.h> //折半查找 int binarySearch(int *a,int low,int high,int key) { int mid=(low+high)/2; if(low>high) return low; if(a[mid]==key) return mid; else if(key<a[mid]) return binarySearch(a,low,mid-1,key); else return binarySearch(a,mid+1,high,key); } //折半插入排序 void binaryInsertSort(int *a,int n) { int i,j,site,temp; for(i=2;i<=n;i++){ //1.折半查找要插入的位置 site=binarySearch(a,1,i,a[i]); temp=a[i]; //2.移位 for(j=i;j>site;j--) a[j]=a[j-1]; //3.插入a[i] a[site]=temp; } } void main() { int i; int a[7]={0,3,5,8,9,1,2};//不考虑a[0] binaryInsertSort(a,6); for(i=1;i<=6;i++) printf("%-4d",a[i]); printf("\n"); }
#include<stdio.h> #define MAX 10000 //静态链表 typedef struct { int key;//关键字 int next;//指向下一个关键字的下标 }Node,*PNode; //表插入排序 void tableInsertSort(PNode list,int n) { int p,head; int i; //初始化 list[0].next=1; list[1].next=0; //逐个插入 for(i=2;i<=n;i++){ head=0; p=list[0].next; //遍历链表,寻找插入位置 while(p!=0 && list[p].key<=list[i].key){ head=p; p=list[p].next; } if(p==0){//插入的值是最大值 list[i].next=p; list[head].next=i; }else{ list[i].next=p; list[head].next=i; } } } void main() { int p; Node list[7]={MAX,0,3,0,5,0,8,0,9,0,1,0,2,0}; tableInsertSort(list,6); p=list[0].next; while(p!=0){ printf("%-4d",list[p].key); p=list[p].next; } printf("\n"); }
#include<stdio.h> //一趟增量为dk的希尔插入排序 void shellInsert(int *a,int n,int dk) { int i,j,temp; for(i=dk+1;i<=n;i+=dk){ temp=a[i]; for(j=i-dk;j>=0;j-=dk) if(a[j]>temp) a[j+dk]=a[j]; else break; a[j+dk]=temp; } } //希尔排序 void shellSort(int *a,int n) { int i; int dk[]={5,4,3,2,1}; for(i=0;i<5;i++) shellInsert(a,6,dk[i]); } void main() { int i; int a[7]={0,3,5,8,9,1,2};//不考虑a[0] shellSort(a,6); for(i=1;i<=6;i++) printf("%-4d",a[i]); printf("\n"); }