动画:一篇文章快速搞懂希尔排序
内容介绍
希尔排序简介
冒泡排序在1956年就已经被研究。但是排序速度是比较慢的,冒泡排序的时间复杂度是O(n^2),然而在后面的很长的时间里,尽管人们发明各种排序算法(比如前面选择排序和插入排序),但时间复杂度都是0(n^2),时间复杂度似乎没法超越0(n^2)。此时,计算机学术界充斥着“排序算法不可能突破O(n^2)的声音”。终于有一天,当一位科学家发布超越了0(n^2)新排序算法后,紧接着就出现了好几种可以超越0(n^2)的排序算法,并把内排序算法的时间复杂度提升到了0(nlogn)。“不可能超越0(n^2)”彻底成为了历史。
希尔排序按其设计者希尔(Donald Shell)的名字命名,该算法由1959年公布。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,在这之前排序算法的时间复杂度基本都是0(n^2)的,希尔排序算法是突破这个时间复杂度的第一批算法之一。
前面讲的插入排序,它的效率在数据近乎有序时候是很高的,只需要少量的插入操作,就可以完成整个排序工作,此时直接插入很高效。还有就是数据比较少时,直接插入的优势也比较明显。可问题在于,两个条件本身就过于苛刻,实际开发中数据少或者基本有序都属于特殊情况。
不过别急,条件不存在,我们创造条件也是可以去做的。希尔排序(Shell'sSort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。该方法因D.L.Shell于1959年提出而得名。虽然插入排序是O(n^2)的排序复杂度,但是如果已经是有序的,则可以提前结束,可以提高效率我们知道插入排序在近乎有序的数组中效率很高, 如何让待排序的记录数量少并且近乎有序呢?很容易想到的就是将原本有大量记录数的记录进行分组。分割成若干个子序列,此时每个子序列待排序的记录个数就比较少了,然后在这些子序列内分别进行直接插入排序,当整个序列都基本有序时,注意只是基本有序时,再对全体记录进行一次直接插入排序。
有同学会疑惑。这不对呀,比如我们现在有个序列是{9, 1, 5, 8, 3, 7, 4, 6, 2},现在将它分成三组:{9, 1, 5},{8, 3, 7},{4, 6, 2},哪怕将它们各自排序排好了,变成{1, 5, 9},{3, 7, 8},{2,4,6}再合并它们成{1, 5, 9, 3, 7, 8, 2, 4, 6},此时,这个序列还是杂乱无序,谈不上基本有序,要排序还是重来一遍直接插入有序,这样做没有用。需要强调一下,所谓的基本有序,就是小的关键字基本在前面,大的基本在后面,不大不小的基本在中间,像{2, 1, 3, 6, 4, 7, 5, 8, 9}这样 可以称为基本有序了。但像{1, 5, 9, 3, 7, 8, 2, 4, 6} 这样的9在第三位,2在倒数第三位就谈不上基本有序。问题其实也就在这里,我们分割待排序记录的目的是减少待排序记录的个数,并使整个序列向基本有序发展。而像上面这样分完组后就各自排序的方法达不到我们的要求。因此,我们需要采取跳跃分割的策略:将相距某个“增量” 的记录组成一一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
希尔排序的思想
希尔排序,通过增量(gap)将元素两两分组,对每组使用直接插入排序算法排序;增量(gap)逐渐减少,当增量(gap)减至1时,整个数据恰被分成一组,最后进行一次插入排序,整个数组就有序了。
希尔排序动画演示
一般没有特殊要求排序算法都是升序排序,小的在前,大的在后。
数组由{7, 3, 1, 9, 5, 4, 2, 8, 6} 这9个无序元素组成。
第一次:gap = 9/2 = 4 动画:
第二次:gap = 4/2 = 2 动画:
第三次:gap = 2/2 = 1 动画:
希尔排序分析
希尔排序,通过增量(gap)将元素分成n组,对每组使用直接插入排序算法排序。增量(gap)逐渐减少,当增量(gap)减至1时,整个数据恰被分成一组,最后进行一次插入排序。
第一次:gap = 9/2 = 4,每个元素和后面的元素之间间隔4,元素被分成4组。其中元素7, 5, 6分成一组,元素3,4分成一组,元素1, 2分成一组,元素9,8分成一组,效果如下图:
第二次:gap = 4/2 = 2,每个元素和后面的元素之间间隔2,元素被分成2组。
第三次:gap = 2/2 = 1,每个元素和后面的元素之间间隔1,元素被分成1组。
希尔排序代码编写
回顾一下刚才的分析,当我们有9个元素时,gap分别取值4, 2, 1。
元素数量不同gap取值也不同,需要一个循环控制gap的取值。当取好gap后需要使用插入排序对每组进行排序,需要使用嵌套循环。
在进行小范围插入排序时,要注意从哪个位置开始。我们先回顾一下之前学习的插入排序,第一个元素6
不需要动,从第二元素5
开始才需要进行比较和插入。
希尔排序拆分成多个小组后,每个小组使用插入排序,也是从每个小组的第二个数据开始比较插入,如下图所示:
从上图中可以看出,第一组从索引4开始比较,第二组从索引5开始比较,第三组从索引6开始比较,第四组从索引7开始比较。
希尔排序代码如下:
public class ShellSortTest2 {
public static void main(String[] args) {
int[] arr = new int[] {7, 3, 1, 9, 5, 4, 2, 8, 6};
shellSort(arr);
}
public static void shellSort(int[] arr) {
// 增量gap,通过gap来划分出不同的子序列,gap不断减少,划分的子序列更多
for (int gap = arr.length / 2; gap > 0; gap /= 2) { // 循环得到不同的gap,gap = 4, 2, 1
System.out.println("gap: " + gap);
for (int i = gap; i < arr.length; i++) { // 每组使用插入排序
for (int j = i; j-gap >= 0; j -= gap) {
if (arr[j-gap] > arr[j]) {
swap(arr, j-gap, j);
System.out.print("\t交换: " + arr[j-gap] + "和" + arr[j]);
System.out.println(Arrays.toString(arr));
}
}
}
}
}
public static void swap(int[] arr, int start, int end) {
int temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
}
}
执行效果:
gap: 4
交换: 5和7[5, 3, 1, 9, 7, 4, 2, 8, 6]
交换: 8和9[5, 3, 1, 8, 7, 4, 2, 9, 6]
交换: 6和7[5, 3, 1, 8, 6, 4, 2, 9, 7]
gap: 2
交换: 1和5[1, 3, 5, 8, 6, 4, 2, 9, 7]
交换: 4和8[1, 3, 5, 4, 6, 8, 2, 9, 7]
交换: 2和6[1, 3, 5, 4, 2, 8, 6, 9, 7]
交换: 2和5[1, 3, 2, 4, 5, 8, 6, 9, 7]
gap: 1
交换: 2和3[1, 2, 3, 4, 5, 8, 6, 9, 7]
交换: 6和8[1, 2, 3, 4, 5, 6, 8, 9, 7]
交换: 7和9[1, 2, 3, 4, 5, 6, 8, 7, 9]
交换: 7和8[1, 2, 3, 4, 5, 6, 7, 8, 9]
希尔排序的增量和复杂度
通过上面动画和代码,相信大家有些明白,希尔排序的关键并不是随便分组后各自排序,而是将相隔某个“增量”的记录组成一个子序列,实现跳跃式的移动,使得排序的效率提高。这里"增量”的选取就非常关键了。我们在代码中是用int gap = arr.length / 2;
的方式选取增量的,可究竟应该选取什么样的增量才是最好,目前还是一个数学难题,迄今为止还没有人找到一种最好的增量序列。
迄今为止,除了在一些特殊的情况下,还没有人能够从理论上分析希尔排序的效率。有各种各样基于试验的评估,估计它的时间复杂度从0(N^3/2)到0(N^7/6)。
希尔排序常见的增量序列:
Shell 增量序列,Shell 增量序列的递推公式为:
Hibbard 增量序列通项公式为:
Knuth 增量序列通项公式为:
Gonnet 增量序列递推公式为:
Sedgewick 增量序列通项公式为:
需要注意的是,增量序列的最后一个增量值必须等于1才行,最后一趟排序是一次普通的插入排序。希尔排序算法的发明,使得我们终于突破了慢速排序之后,更为高效的排序算法也就相继出现了。
总结
希尔排序,是直接插入排序算法的一种更高效的改进版本。通过增量(gap)将元素进行分组,对每组使用直接插入排序算法排序;增量(gap)逐渐减少,当增量(gap)减至1时,整个数据恰被分成一组,最后进行一次插入排序。
-------------------- END --------------------
历史推荐
观看更多精彩文章。