在上一篇博文中咱们提到:要令排序算法的时间复杂度低于O(n2),必须令算法执行“远距离的元素交换”,使得平均每次交换减小不止1逆序数。算法
而希尔排序就是“简单地”将这个道理应用到了插入排序中,将插入排序小小的升级了一下。那么,希尔排序是怎么将这个道理应用于插入排序的呢?咱们先来回顾一下插入排序的代码:编程
void InsertionSort(int *a, unsigned int size) {//StartPos表示执行插入操做的元素开始插入时的下标 //令StartPos从1递增至size-1,对于每一个a[StartPos],咱们执行向前插入的操做 for (int StartPos = 1;StartPos < size;++StartPos) for (int CurPos = StartPos;CurPos != 0;--CurPos) if(a[CurPos - 1] > a[CurPos]) swap(&a[CurPos],&a[CurPos-1]); //令当前元素与前一元素交换 }
不难看出,在插入排序中,对于每个元素,咱们都令其执行“向前插入”操做,直至到达顺序位置。可是,在“向前插入”这个操做中,每一次“当前元素”都是与前一元素进行比较,而这也是插入排序时间复杂度没能低于O(n2)的缘由。数组
因此,希尔排序与插入排序之间的区别就是:希尔排序在“向前插入”时,“当前元素”老是与前k元素(若当前元素下标为n,则前k元素即下标为n-k的元素)进行比较,而且第一个开始“插队”的元素再也不是[1],而是[k]。从代码角度来讲,即是将插入排序的循环改成:函数
//StartPos表示执行插入操做的元素开始插入时的下标 //令StartPos从k递增至size-1,对于每一个a[StartPos],咱们执行向前插入的操做 for (int StartPos = k;StartPos < size;++StartPos) for (int CurPos = StartPos;CurPos >= k;CurPos-=k) if(a[CurPos - k] > a[CurPos]) swap(&a[CurPos],&a[CurPos-k]); //令当前元素与前k元素交换
不难看出,插入排序就是k=1的状况。通过上述代码处理后,数据能够保证以下属性:性能
a[n],a[n+k],a[n+2k]……a[n+x*k]有序,其中0=<n<size且n+x*k<size,也就是说:全部相隔距离为k的元素组成的数列都有序(当k为1时即全体有序)学习
举个实例来看看,假设数组以下,间距为3的元素用同色标注:spa
35,30,32,28,12,41,75,15,96,58,81,94,95code
令k=3,进行k=3的“插入排序”后,间距为3的元素互相有序:对象
28,12,32,35,15,41,58,30,94,75,81,96,95blog
分析上例能够看出,当k>1时,间距为k的k-插入排序的交换能够实现“远距离交换元素”,上例中,3-的插入排序交换了5次元素,逆序数减小了9,平均一次交换减小了1.8逆序数。
同时能够看出,上述属性,只有在k为1时才能保证整个数组有序,也即普通插入排序的状况,而k>1时则不能。也就是说,要想“远距离交换元素”,就要令k>1,而k>1却又不能保证数组最后有序,那该怎么办呢?
万幸的是,咱们有这么一个定理:
若数组已经进行过间距为k的k-插入排序,即已经肯定间距为k的元素互相有序,则对数组进行间距为(k-1)的(k-1)-插入排序后,数组依然保持“间距为k的元素互相有序”
用大白话来讲,就是:虽然k>1的k-插入排序不能保证数组彻底有序,但能够保证不增长数组的逆序数。
因而,希尔排序的发明者唐纳德·希尔想出了这么一个办法,也就是希尔排序:先进行k比较大的“插入排序”,而后逐步减少k的值,直至k=1。这样一来,希尔排序就能保证最后数组有序。
接下来的问题就是,k的初始值该如何选?k又该如何减少至1?这一点相当重要,其重要性相似于哈希函数对于哈希表的意义。咱们称k从初始值kn减少至1的各值:kn,kn-1,kn-2……1组成的序列称为“增量序列”,即“增量”(Increment,意指k的大小)组成的序列。希尔本人推荐的增量序列是初始值为size/2,任一kn-1=kn/2。这样一来,使用希尔增量序列的希尔排序完整算法以下:
void ShellSort(int *a, unsigned int size) { unsigned int CurPos, Increment; //CurPos表示执行插入的元素当下所处的下标,Increment即增量k int temp; //用于暂存当前执行插入操做的元素值,能够减小交换操做 //Increment从size/2开始,按Increment/=Increment的方式递减至1 for (Increment = size / 2;Increment > 0;Increment /= 2) //下方代码与插入排序几乎相同,只是比较对象由[CurPos-1]变为[CurPos-Increment] for (unsigned int StartPos = Increment;StartPos < size;++StartPos) { temp = a[StartPos]; for (CurPos = StartPos;CurPos >= Increment&&a[CurPos - Increment] > temp;CurPos -= Increment) a[CurPos] = a[CurPos - Increment]; a[CurPos] = temp; } }
接下来,咱们以希尔增量序列为例,说明为何增量序列的设定对于希尔排序性能相当重要:
设数据为:1,9,2,10,3,11,4,12,则对应增量序列为4,2,1
4-插入排序后:1,9,2,10,3,11,4,12
2-插入排序后:1,9,2,10,3,11,4,12
1-插入排序后:1,2,3,4,9,10,11,12
不难发现,这个例子中的增量序列很很差,4-排序和2-排序都没有任何的有效操做。这个例子告诉咱们两件事:
1.增量序列对于希尔排序的性能很是重要,差的增量序列会减小须要本能够执行的“远距离交换”
2.希尔推荐的增量序列编程实现简单,但实际应用中表现并很差,缘由在于其增量序列不互素。
而且能够肯定的是,若需排序的数组a大小n为2的幂,任一x为偶数的a[x]均大于x为奇数的a[x],且a[x]>a[x-2],则希尔的增量序列只有在进行1-排序时才有交换操做。
举例来讲:9,1,10,2,11,3,12,4,13,5,14,6,15,7,16,8。
其增量序列为8,4,2,1,可是8-排序、4-排序与2-排序都没有交换元素。
此外,若某元素排序前位于下标奇数处,排序后所在位置为i,则进行1-排序前,其位置在2*i+1处(如例中元素4,其下标为奇数,其有序位置应为3,1-排序前位置为7),而将其从位置2*i+1移动至i须要执行i+1次交换,这样的元素(下标奇数)共有n/2个,因此将这些元素移动至正确位置就须要(0+1)+(1+1)+(2+1)+……+(N/2+1)共N2/8-N/4,时间复杂度为O(n2)。可见,使用希尔增量序列希尔排序的最坏状况是O(n2)
那么,希尔排序的增量序列该如何选择呢?本文给出两个序列,它们都比希尔增量序列要好:
1.Hibbard序列:{1,3,7……2k-1},k为大于0的天然数,使用Hibbard序列的希尔排序平均运行时间为θ(n5/4),最坏情形为O(n3/2)。
2.Sedgewick序列:令i为天然数,将9*4-9*2+1的全部结果与4-3*2+1的全部结果进行并集运算,所得数列{1,5,19,41,109……}。使用此序列的希尔排序最坏情形为O(n4/3),平均情形为O(n7/6)
如何实现这两个序列的希尔排序并非难事,Hibbard序列能够直接经过计算得出初始值(小于数组大小便可),然后每次令Increment=(Increment-1)/2便可。Sedgewick序列则稍稍麻烦点,须要先将序列计算出足够项(最后一项小于数组大小),然后存于某个数组,再不断从中取出元素做为增量。
希尔排序的性能(使用Sedgewick序列)在数据量较大时依然是不错的。若是说插入排序是咱们的“初级排序”,用于较少数据或趋于有序数据的状况,那么希尔排序就是咱们的“中级排序”,用于数据量偏多的状况。固然,当数据量极大时,咱们将用上咱们的“高级排序”——快速排序。至于怎么样算数据量偏多,这个就须要因情境而异了,数据的存储形式等都是须要考虑的问题,通常来讲数据量为万级时咱们使用希尔排序,数据量为十万、百万级时使用快速排序,而数据量为百、千级时插入排序和希尔排序均可以考虑。而且须要再次说明的是,数据越趋于有序,则插入排序越快。从这个角度来讲,插入排序也不失为一个“高级排序”。
那么,咱们学习堆时提到的用堆进行排序的想法,明明有着很好的时间界O(N*logN),为何不在考虑之列呢?咱们下一篇博文就简单地分析分析。