对于大规模乱序数组插入排序很慢,因为它只会交换相邻的元素,因此元素只能一点一点地从数组的一端移动到另一端。例如,如果主键最小的元素正好在数组的尽头,要将它挪到正确的位置就需要N-1次移动。希尔排序为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。
1.基本思想
希尔排序的思想是使数组中任意间隔为h的元素都是有序的。这样的数组被称为h有序数组。换句话说,一个h有序数组就是h个互相独立的有序数组编织在一起组成的一个数组(见下图)。在进行排序时,如果h很大,我们就能将元素移动到很远的地方,为实现更小的h有序创造方便。用这种方式,对于任意以1结尾的h序列,我们都能够将数组排序。这就是希尔排序。
希尔排序更高效的原因是它权衡了子数组的规模和有序性。排序之初,各个子数组都很短,排序之后子数组都是部分有序的,这两种情况都很适合插入排序。子数组部分有序的程度取决于递增序列的选择。透彻理解希尔排序的性能至今仍然是一项挑战。实际上,它是我们唯一无法准确描述其对于乱序的数组的性能特征的排序方法。
2.具体算法
/** * 希尔排序 * @author huazhou * */ public class Shell extends Model{ public void sort(Comparable[] a){ // System.out.println("Shell"); //将a[]按升序排列 int N = a.length; int h = 1; //1,4,13,40,121,364,1093,... while(h < N/3){ h = 3*h + 1; } //将数组变为h有序 while(h >= 1){ //将a[i]插入到a[i-h],a[i-2*h],a[i-3*h]...之中 for (int i = h; i < N; i++) { for (int j = i; j >= h && less(a[j],a[j-h]); j-=h) { exch(a, j, j-h); } } h = h/3; } } }
如果我们在插入排序中加入一个外循环来将h按照递增序列递减,我们就能得到这个简洁的希尔排序。增幅h的初始值是数组长度乘以一个常数因子,最小为1.
和选择排序以及插入排序形成对比的是,希尔排序也可以用于大型数组。它对任意排序(不一定是随机的)的数组表现也很好。实际上,对于一个给定的递增序列,构造一个使希尔排序运行缓慢的数组并不容易。下图是可视轨迹图:
3.算法分析
至于希尔排序算法的性能,目前最重要的结论是它的运行时间达不到平方级别。在实际应用中,使用算法中的递增序列基本就足够了。在最坏的情况下的比较次数和N3/2成正比。
命题:使用递增序列1,4,13,40,121,364...的希尔排序所需的比较次数不会超出N的若干倍乘以递增序列的长度。
证明:大量的实验证明平均每个增幅所带来的比较次数约为N1/5,但只有在N很大的时候这个增长幅度才会变得明显。
4.总结
通过SortCompare可以看到,希尔排序比插入排序和选择排序要快得多,并且数组越大,优势越大。希尔排序能够解决一些初级排序算法无能为力的问题。
有经验的程序员有时会选择希尔排序,因为对于中等大小的数组它的运行时间是可以接受的。它的代码量很小,且不需要使用额外的内存空间。在后面的学习中我们会看到更加高效的算法,但除了对于很大的N,它们可能只会比希尔排序快两倍(可能还达不到),而且更复杂。如果你需要解决一个排序问题而又没有系统排序函数可用(例如直接接触硬件或是运行于嵌入式系统中的代码),可以先用希尔排序,然后再考虑是否值得将它替换为更加复杂的排序算法。
【源码下载】