作为最早突破
本文尽可能通俗的解释了希尔排序为什么快,以及打破二次时间屏障的关键一击是什么。
复杂度证明放最后了,有点烧脑,但不影响上文阅读。
希尔排序的思想:
希尔排序是插入排序的改进版,通过比较一定间隔的元素进行插入排序,并且不断缩小间隔,直到比较相邻元素。我们管这个间隔叫增量,增量为h的排序我们成为h-排序。
如果我们使用插入排序的话,最后一步将元素1从最后一个位置移动到第一个位置,需要把其余元素依次往后挪动一位,这种开销十分巨大。
而使用希尔排序时,我们通过较大的间隔(增量),将元素1快速送到第一个位置。
第一趟排序:
根据图2,增量为4时,我们只对间隔为4的元素进行插入排序。将[8,4]进行插入排序,得到[4,8]。将[7,3]进行插入排序,得到[3,7]。将[6,2]进行插入排序,得到[2,6]。将[5,1]进行插入排序,得到[1,5]。
第一趟排序后,通过每隔四个元素进行插入排序,花费较少的比较次数,算法将小元素快速的送到前面,同理大元素被挤到后面。
第二趟排序:
根据图3,增量为2时,我们对间隔为2的元素进行插入排序。将[4,2,8,6]进行插入排序,得到[2,4,6,8]。将[3,1,7,5]进行插入排序,得到[1,3,5,7]。
第二趟排序后,通过每隔二个元素进行插入排序,小元素继续被快速的送到前面,同理大元素被挤到后面。
经过两趟大增量的排序,数据在宏观上是比较有序的。此时只需要最后执行一次增量为1的排序,也就是普通的插入排序,将现在的数据进行微调,就能得到有序的结果。
对[2, 1, 4, 3, 6, 5, 8, 7]这个半成品进行插入排序,显然比对[8, 7, 6, 5, 4, 3, 2, 1]进行插入排序容易得多。
/*希尔排序c代码,来自《数据结构与算法分析》*/
void ShellSort(ElementType A[], int N) {
int i, j, increment;
ElementType tmp;
// 每一趟排序增量折半,当然也可以使用其他增量
/* 1*/ for (increment = N / 2; increment > 0; increment /= 2) {
/* 2*/ for (i = increment; i < N; ++i) {
/* 3*/ tmp = A[i];
// 对A[i],A[i-increment],A[i-2*increment]...进行插入排序
/* 4*/ for (j = i; j > increment; j -= increment) {
/* 5*/ if (tmp < A[j - increment]) {
/* 6*/ A[j] = A[j - increment];
/* 7*/ }
/* 8*/ else {
/* 9*/ break;
/*10*/ }
/*11*/ }
/*12*/ A[j] = tmp;
/*13*/ }
/*14*/ }
}
通俗的讲,希尔排序能够以较大的步伐将小元素往前送,这样大大的减少了需要比较的次数,从而提高了速度。
使用1, 2, 4, ...,
比如图4:
因此Hibbard提出了著名的Hibbard增量:1, 3, 7, ...,
通俗来说,能打破二次时间界的核心原因是:
在执行
所以4-11行代码循环次数并没有想象中的那么多。
证明:使用Hibbard增量的希尔排序最坏运行时间是
假设我们已经进行到
所以对于位置 P和位置 P-d上的两个元素,如果d是
同样的,当d是
比如d=1*3+2*7,那么根据7-排序结果,A[17]≥A[10]≥A[3],根据3-排序结果,A[3]≥A[0],所以A[17]≥A[0]。
因为
所以4-11行for循环代码只需要检查和当前tmp左侧且距离不超过
又因为增量是
因为代码第二行的for循环执行
但是当
所以我们约定,当
所以假设增量个数为t。总复杂度为:
有了这个上界,排序的速度就得到了极大的保障。举个例子,对10000个元素进行排序,如果普通插入排序最坏情形不超过100秒,那么Hibbard增量的希尔排序最坏情况不会超过1秒。
ps:最后证明过程来自Weiss的《数据结构与算法分析》,但是总复杂度证明那里把