前言:直接插入排序实际上是把待排数列中的每一个逆序对都进行了一次交换。
- 对于下标i<j,如果A[i] >A[j],则称(i,j)是一对逆序对。
如{5,2,1,4,3}中(5,2)、(5,1)、(5,4)、(5,3)、(2,1)、(4,3)共计6个逆序对,那么直接插入排序就会进行6次交换。但是如果我们先让{5,1}和{2,4}作为子序列进行直接插入排序,那么序列变为{1,2,5,4,3},这个序列的逆序对数是(5,4)(5,3)(4,3),然后再进行直接插入排序交换3次就行,加上(5,1)的交换共计4次交换,算法的效率提升了。希尔排序就是这样对直接插入排序进行优化。
- 希尔排序的思想是:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
这里的子序列不是简单的把整个序列逐段分割,这样做没有意义。而是选择一个增量,将相隔某个“增量”的位置上的记录组成一个个子序列。如增量为3那么就分3个子序列{a[0],a[3],a[6]}、{a[1],a[4],a[7]}、{a[2],a[5],a[8]} ,然后将这些子序列进行直接插入排序。如果还嫌这样操作后整个序列不算“基本有序”,我们还可以让增量为2再进行一次,最后一定是增量为1进行一次直接插入排序。那么我们通常如何选择增量呢,通常让增量为n/2,然后再进行增量为n/2/2,直到增量为1。代码实现如下:
void Shell_Sort(ElementType A[],int N)//N为关键字数目
{ int p,i;
for(D=N/2;D>0;D/=2)//D是希尔增量序列
{ for(P=D;P<=N-1;P++)//从A[0+D]开始,希尔排序这些增量子序列是同时进行直接插入排序的
{
for(i=p;i>=D&&A[i]<A[i-D];i-=D)//判断不能越界,i>=D要成立,想一下增量为1时i>=1
swap(A[i],A[i-D]); //要插入的数比前D个数小所以交换
}
}
}
void swap(int &a, int &b)//引用,形参修饰实参
{
int tem = a;
a = b;
b = tem;
}
也可以增量为n/3+1,然后为(n/3+1)/3+1...最后为1。不写成n/3是为了避免最后出现n=2使增量直接为零,而直接错过了增量为1这个最关键的步骤。所以无论你选择什么增量序列,都要保证最后一个增量为1.
那到底我们选择增量序列为多少呢?目前最常见的增量序列,也就是上述代码所使用的希尔增量,它的最坏时间复杂度为0()。其他增量序列目前由于涉及到数学上未解决的难题,并未被完全证实,只是局部结论。所以希尔排序的时间复杂度(默认都是最坏时间复杂度)根据排序所用到的增量序列不同而不同。下表为使用不同的步长序列时希尔排序相应的时间复杂度:
使用的增量序列 | (最坏)时间复杂度 | 最好时间复杂度 | 空间复杂度 |
---|---|---|---|
希尔增量序列 | O() | O(nlogn) | O(1) |
Hibbard增量序列 | O() | O(nlogn) | O(1) |
Sedgewick增量序列 | O() | O(nlogn) | O(1) |
算法分析
我们知道一次直接插入排序是稳定的,但是在不同的插入排序过程中,相同的记录可能在各自的子序列中移动,最后稳定性就会被打乱,所以希尔排序是不稳定排序。
按复杂度分析希尔排序属于改进算法。
希尔排序按按不同视角被分到:
- 不稳定排序
- 改进排序
- 内排序
- 插入排序
希尔排序的时间性能优于直接插入排序的原因:
①当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。直接插入排序胜。
②当n值较小时,n和的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0( )差别不大。直接插入排序也差不多。
③在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量D逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按D作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。
第③点才是最重要的,因此,希尔排序在效率上较直接插入排序有较大的改进。直接插入排序的最好时间复杂度为0(n),希尔排序排序的最好时间复杂度为O(nlogn),但是希尔排序更有可能靠近这个最好的情况,所以说希尔排序算法的发明,我们终于突破了慢速排序的时代。