在上一篇介绍插入排序的文章末尾,我们说通过插入排序算法可以引申出另一个重要的排序算法-希尔排序(shell sort),这篇文章我们就来说说希尔排序。
希尔排序也称为递减增量排序算法。希尔排序的名称是以美国计算机科学家 Donald Lewis Shell 的名字来命名,本文就选了他本人作为封面,1959年7月他在ACM通讯(Communications of the ACM,CACM)上发表了希尔排序算法。
希尔排序思想
在上一篇插入排序的文章中提到两点:
在数组近乎有序的情况下,插入排序的性能非常高,甚至可以达到线性的排序效率;
在插入排序中,每次都和之前的一个元素进行比较。
希尔排序就是基于这两点提出了改进方法。首先明确,希尔排序有个“步长”的概念,我们记为 step。接下来看看希尔排序是怎么改进的:
通过 step 将数组中的数据分组,同组之间的数据进行独立排序(这里排序的方法用的其实就是插入排序),这样可以让一个元素一次性地移动 step 个位置,朝它的最终位置前进一大步;
将 step 逐渐缩小并进行排序,这样一步一步的将一个完全无序的数组变成一个近乎有序的数组;
最后当 step = 1 时,这时算法其实就变成了普通的插入排序,而数组此时也变成了近乎有序的数组,我们知道这种情况用插入排序会非常快。
可以发现,希尔排序的整体思路其实就是插入排序的延伸,而插入排序其实就是 step = 1 的希尔排序。
接下来就用个简单的demo来演示一下整个过程吧。假设初始“步长”为数组长度,“步长”的计算方式为:step = 前一次step / 2,待排序数组如下:
2 | 5 | 4 | 8 | 7 | 9 | 1 | 6 | 3 |
第一轮比较和交换的详细过程为:
step = 4,数据分组情况如下(相同颜色为一组):[2, 7, 3],[5, 9],[4, 1],[8, 6]
2 | 5 | 4 | 8 | 7 | 9 | 1 | 6 | 3 |
2 | 5 | 1 | 8 | 7 | 9 | 4 | 6 | 3 |
2 | 5 | 1 | 6 | 7 | 9 | 4 | 8 | 3 |
2 | 5 | 1 | 6 | 3 | 9 | 4 | 8 | 7 |
接下来每次新的一轮都要先更新 step = 前一次step / 2,具体的比较和交换过程和上面类似,就不赘述了。这里直接给出所有轮次的 step 和初始分组情况以及结果如下:
step = 4;[2, 7, 3],[5, 9],[4, 1],[8, 6]
2 | 5 | 1 | 6 | 3 | 9 | 4 | 8 | 7 |
1 | 5 | 2 | 6 | 3 | 8 | 4 | 9 | 7 |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
希尔排序代码
1 | /** |
希尔排序优化
在某些极端情况下,希尔排序是比插入排序还要慢的。比如下面这种情况,仍然假设初始“步长”为数组长度,“步长”的计算方式为:step = 前一次step / 2,待排序数组如下:
2 | 1 | 4 | 3 | 6 | 5 | 8 | 7 |
按照之前的思路来分析,会发现,第一轮 step = 4 和第二轮 step = 2 时,每个单独的分组内部的元素都没有进行任何交换,一直到 step = 1 时,数组才按照插入排序的方式进行排序。对于这样的数组,希尔排序反而白白增加了分组的成本。
造成这种情况的主要原因是:step 选择的不合适,因为我们使用的计算 step 的算法得到的“步长”是个等比序列,会出现了增量的盲区。所以,为希尔排序选择更有效的“步长”成为了优化的关键。
其实只要最终的步长为 1,那么任何步长序列都可以工作,因为当步长为 1 时,算法会变为普通插入排序,这就保证了数据一定会被排序。那如何选择更有效的“步长”呢?
其实这个问题我们不用太纠结,因为许多计算机科学家们提出了很多步长序列,其背后都有着复杂的推导过程。
这里介绍其中两种:
目前已知的最好步长序列。由Sedgewick提出的(1, 5, 19, 41, 109, …),该序列的项来自 9 * 4 ^ i - 9 * 2 ^ i + 1 和 2 ^ (i + 2) * (2 ^ (i + 2) - 3) + 1 这两个算式。用这样步长序列的希尔排序比插入排序要快,甚至在小数组中比快速排序和堆排序还快;
在大数组中表现优异的步长序列。由斐波那契数列除去0和1将剩余的数以黄金分割比的两倍的幂进行运算得到的数列(1, 9, 34, 182, 836, 4025, 19001, 90358, 428481, 2034035, 9651787, 45806244, 217378076, 1031612713,…)
当然大家只需要了解即可,有兴趣的同学可以深究一下。
复杂度和稳定性分析
- 时间复杂度
最优时间复杂度:O(n)
- 类似于插入排序的最优时间复杂度分析。
最坏时间复杂度:O(n^2)
平均时间复杂度:根据步长序列的不同而不同。O(n * (logn)^2)
- 空间复杂度:O(1)
- 整个过程都是原地排序,并没有用到其它辅助的空间。
- 希尔排序是一种不稳定排序。
- 假如待排序数组为 [4, 4, 3, 5, 2]。其实也很好理解,虽然一次插入排序是稳定的,但在不同步长的插入排序过程中,相同的元素可能在各自的插入排序中被移动,就可能导致其稳定性被打乱。