1.序
按照严吴版的《数据结构》里面对插入排序的介绍,实现其算法:
插入排序即通过插入方式完成排序,其中包括直接插入排序,二分插入排序,二路插入排序,表插入排序,希尔插入排序,分别给予实现
2.直接插入排序
《算法导论》:直接插入排序类似于打牌时,最先手中一张牌也没有,然后一张一张摸起来,分别插入到指定的位置,然后牌拉完了,手里的牌也是有序的了
代码如下:
/* * 对sq+1,sq+2,...,sq+length进行直接插入排序 */ void InsertSort(int *sq,int length){ for(int i=2;i<=length;i++){ sq[0]=sq[i]; int j; for(j=i-1;sq[0]<sq[j];j--){ sq[j+1]=sq[j]; } sq[j+1]=sq[0]; } }可见,直接插入排序时间复杂度为 n^2 ,当序列基本有序时,需要往前插入的次数会减少很多,效率还是较高的
直接插入排序中,前面的序列已经是有序的,需要找到当前需要插入的数在前面的数的位置,然后进行插入,直接插入排序采用从后往前依次找插入位置,如果前面的序列是有序表的话,可以用二分查找方法来找到要插入的位置。二分查找,设置low,high,当没有找到待插入元素时,>=high的元素都是大于当前待插入元素的,所以找到了插入位置,即high之前。
代码如下:
/* * 对sq+1,sq+2,...,sq+length进行二分直接插入排序 */ void BinaryInsertSort(int *sq,int length){ int low,mid,high; for(int i=2;i<=length;i++){ if(sq[i]<sq[i-1]){ sq[0]=sq[i]; low=1; high=i-1; while(low<=high){ mid=(low+high)/2; if(sq[0]<sq[mid]){ high=mid-1; } else if(sq[0]>sq[mid]){ low=mid+1; } else{ //有相同关键字 low=high=mid; break; } } printf("low=%d,high=%d\n",low,high); int j; for(j=i-1;j>=high+1;j--){ sq[j+1]=sq[j]; } sq[high+1]=sq[0]; } } }可见二分插入排序,只是减少了关键字之间的比较次数,并不能减少交换次数,只是在查找插入位置时提高了速度,而且必须是有序表的限制
5.表插入排序二路插入排序是在二分插入排序的基础上改进的方法,目的是为了减少排序过程中交换记录的次数,但为此需要额外的n个辅助记录空间。用一个记录(目前取第一个记录)作为枢纽点,然后把比该记录关键字大大记录插入当前空间,比该记录大的插入到辅助空间,最后合并两个数组,完成排序。
代码如下:
/* * 对sq+1,sq+2,...,sq+length进行二路插入排序 */ void DoubleRouterInsertSort(int *sq,int length){ int *sqadd=(int *)malloc((length+1)*sizeof(int)); *(sq+0)=*(sqadd+length)=*(sq+1); int insertpos; int pivotA=0; int pivotB=length; for(int i=2;i<=length;i++){ if( (*(sq+i)) > *((sq+0)) ){ //InsertB,插入到辅助空间 insertpos=pivotB; while( (insertpos<=length) && ((*(sq+i))>(*(sqadd+insertpos))) ){ *(sqadd+insertpos-1)=*(sqadd+insertpos); insertpos++; } *(sqadd+insertpos-1)=*(sq+i); pivotB--; } else{ //InsertA,插入到本身空间 if(pivotA==0){ *(sq+1)=*(sq+i); pivotA=1; } else{ insertpos=pivotA; while( (insertpos>=1) && ((*(sq+i))<(*(sq+insertpos))) ){ *(sq+insertpos+1)=*(sq+insertpos); insertpos--; } *(sq+insertpos+1)=*(sq+i); pivotA++; } } } //合并 AB for(;pivotA<length;pivotA++){ *(sq+pivotA+1)=*(sqadd+pivotB); pivotB++; } free(sqadd); }二路插入排序中,移动记录的次数约为 (n^2)/8 。因此,二路插入排序只能减少移动记录的次数,而不能绝对避免移动记录。并且当,*(sq+1) 为关键字最小/大的记录时,二路插入排序就完全失去其优越性。
6.希尔排序前面3中插入排序都无法完全避免记录之间的交换,只有改变数据结构才能解决这个问题,表插入排序通过修改next域来避免直接的记录交换。
代码如下:
/* * 表插入排序 */ typedef struct Node{ int key; int next; }Node; /* * MAX_LEN 是最大的表长,可以不被全部填充 * length 是表的实际长度 * sq[0].key=MAX_INT 为表头节点 */ typedef struct SList{ Node sq[MAX_LEN+1]; int length; }SList; void printSL(SList test){ int pos=test.sq[0].next; while(pos!=0){ printf("%d ",test.sq[pos].key); pos=test.sq[pos].next; } putchar('\n'); } void printSN(SList test){ for(int i=1;i<=test.length;i++){ printf("%d ",test.sq[i].key); } putchar('\n'); } /* * 用表插入法 对sl对应的表进行插入排序 * 生成一个静态有序循环链表 */ void TableInsertSort(SList &sl){ /* * 先让 sq[0],sq[1]构成循环链表 * 然后将sq[2],sq[3],...,sq[length]依次插入到循环链表 */ sl.sq[0].next=1; sl.sq[1].next=0; int insertpos; int preinsertpos; for(int i=2;i<=sl.length;i++){ insertpos=sl.sq[0].next; preinsertpos=0; while(sl.sq[i].key>sl.sq[insertpos].key){ preinsertpos=insertpos; insertpos=sl.sq[insertpos].next; } sl.sq[preinsertpos].next=i; sl.sq[i].next=insertpos; } } /* * 将静态循环链表整理成数组 * 即将链表中第i个节点移动到sq[i]处 */ void Arrange(SList &test){ int q,p=test.sq[0].next; Node tmp; for(int i=1;i<test.length;i++){ /* * 因为 对于数组sq后面的分量的next可能指向前面的分量,而此时前面的分量可能已经移动到后面 * 而要通过这个前面的找到的分量来找到这个已经移动到后面的分量 * 可更改前面分量的next域,使其指向和他对换分量的位置 */ while(p<i){ p=test.sq[p].next; } q=test.sq[p].next; if(p!=i){ //sq[p]<--->sq[i] tmp=test.sq[p]; test.sq[p]=test.sq[i]; test.sq[i]=tmp; test.sq[i].next=p; } p=q; } }可见,表插入排序基本操作依然是将一个记录插入到已排序的有序表中,和直接插入排序相比,不同之处是以修改 2n 次“指针”域代替了移动记录,排序过程中关键字之间的比较次数相同,因此表插入排序的时间复杂度依然是 n^2 。
希尔排序又称 “缩小增量排序”(diminishing increment sort),它也是一种插入排序类的方法,但在效率上有较大提高。
对直接插入排序的分析可知,其算法时间复杂度为 O(n^2) ,但是,若待排序记录序列为 “正序” 时,时间复杂度可提高到 O(n) 。由此可设想,若待排序序列按关键字基本有序,n也很小时,直接插入排序的效率还是很高的,希尔排序正式从这两点出发,对直接插入排序的一种改进排序算法。
代码如下:
/* * 按跳跃距离dk值,对序列sq进行一趟希尔插入排序 */ static void ShellInsert(int *sq,int length,int dk){ int tmp; for(int i=dk+1;i<=length;i++){ if( (*(sq+i))<(*(sq+i-dk)) ){ tmp=*(sq+i); /*找到 *(sq+i) 的位置并插入*/ int j; for(j=i-dk;(j>0)&&((*(sq+j))>tmp);j-=dk){ *(sq+j+dk)=*(sq+j); } *(sq+j+dk)=tmp; } } } /* * 用 increment[i]=2^(times-i)+1 创建增量序列,times指定增量序列的大小 */ static int* CreateIncrement(int times){ int *increment=(int *)malloc(times*sizeof(int)); if( NULL!=increment ){ for(int i=0;i<times-1;i++){ *(increment+i)=(int)(pow((double)(2),(double)(times-i-1))+1); } /* * 保证最后一次增量为1,即直接插入排序 */ *(increment+times-1)=1; } return increment; } /* * 对sq+1,sq+2,...,sq+length进行希尔排序 */ const int SHELL_TIMES=5; void ShellSort(int *sq,int length){ /* * 按增量序列 increment[0,1,2,...,length-1] 对顺序表sq作希尔排序 */ int *dk=CreateIncrement(SHELL_TIMES); if(NULL!=dk){ for(int i=0;i<SHELL_TIMES;i++){ ShellInsert(sq,length,*(dk+i)); } free(dk); dk=NULL; } }可见,希尔排序的特点是:子序列的构成不是简单地“逐段分割”,而是将相隔某个“增量”的记录组成一个子序列。因此,希尔排序中关键字小的记录不是一步一步向前挪动,而是跳跃式地向前移,从而使得最后一趟增量为1的插入排序时,序列已基本有序,这就是希尔排序效率提高的关键。
常见的两种增量序列:
....9,5,3,2,1 dlta[k]=2^(t-k)+1
....125,40,13,4,1 dlta[k]=(3^(t-k-1))/2
7.总结
老是嘴上说什么什么排序,只有自己亲手去实现了才懂其中的奥妙。