引言:
前面详解了如何优化冒泡排序?,图解选择排序与插入排序,这些简单排序算法平均时间复杂度都是O(n^2)。希尔排序是第一批打破二次时间屏障的算法之一。下面我们来分析为什么希尔排序可以打破二次时间复杂度。
一、分析简单排序算法的下界
逆序:具有性质i < j但 a[i] > a[j]的序偶(a[i],a[j])。如序列34,8,64,51,32,21有9个逆序,即(34,8)、(34,32)、(34,31)、(64,51)、(64,32)、(64,21)、(51,32)、(51,21)以及(32,21)。注意,这正好是需要(隐含)执行的交换次数。事实总是这样,因为交换两个不按顺序排序的相邻元素恰好消除一个逆序,而一个排过序的数组没有逆序。由于算法中还有O(N)量的工作,假设 I 是原数组中的逆序数,所以插入排序的运行时间是O(I + N)。所以,如果逆序数是O(N),则插入排序以线性时间运行;冒泡排序通过加标志位也可以在有序的条件下达到最优O(N)。
所以我们可以通过计算原始序列中的平均逆序数得出简单排序的平均时间精确的界。
这里假设元素互异(如果允许重复,那我们连重复的平均次数都无法知道)。
证明:N个互异数的序列L与逆序列Lr中,所有序偶为N(N-1)/2(很容易理解:相当于从N个互异数中选两个元素,这两个元素有顺序,即:A= N(N-1)/2)。那平均的逆序表列该有一半:即 N(N-1)/4个逆序。
证明:因为初始的平均逆序数为N(N-1)/4,而每次交换只减少一个逆序数,因此需要
结论:这个下界告诉我们,为了使一个排序算法以亚二次或O(N^2)时间运行,必须执行一些比较,特别是要对相距较远的元素进行交换。一个排序算法通过删除逆序得以向前进行,而为了有效地进行它必须使每次交换删除不止一个逆序。下面我们来看希尔排序怎么打破二次时间界。
二、希尔排序(ShellSort)
希尔排序按其设计者希尔(Donald Shell)的名字命名。希尔排序通过多次插入排序来实现。它通过比较相距一定间隔的元素来工作;各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。所以希尔排序也叫缩减增量排序。
算法思想:使用一个序列h,h,….h,叫做增量序列。只要h=1,任何增量序列都是可行的。在使用增量h排序后,对于每一个i 我们都有a[i]<=a[i+h];所有相隔h的元素都被排序,这称为h排序。只要最后h=1(这时就是最普通的插入排序),希尔排序都可完成工作。一趟h排序就是对h个子数组进行插入排序。
排序过程
- 选择一个增量序列h,h,….h,h=1。
- 根据增量序列个数,即循环t次进行排序,每次排序结束后更换为h的增量。
- 把原数组分为h个子数组,对每个子数组进行插入排序。
下面我们使用增量序列1、3、5对序列 {3, 7, 1, 13, 9, 11, 5, 8, 2, 4, 12, 6, 10}进行希尔排序的图解。
每种颜色代表一个子数组,很直观看到每一趟排序过程及结果。
- java实现冒泡排序:代码中我们使用希尔建议的增量序列(但效率不高)。h=N/2和h=h/2。理解了插入排序流程,代码实现很简单。
private static <T extends Comparable<? super T>> void shellsort(T[] a) {
int j;
T tmp = null;
for (int gap = a.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < a.length; i++) {
tmp = a[i];
for (j = i; j >= gap &&
tmp.compareTo(a[j - gap]) < 0; j -= gap) {
a[j] = a[j - gap];
}
a[j] = tmp;
}
}
}
- 时间、空间复杂度及稳定性分析:
- 希尔排序的运行时间依赖于增量序列的选择,而证明很复杂【有兴趣可查看其他资料】。
- 使用希尔增量时希尔排序最坏时间复杂度是:O(n^2)。
- 使用Hibbard增量的希尔排序最坏时间复杂度是:O(n);最优时间复杂度是O(n)。
- 使用Sedgewick 增量序列,排序最坏时间复杂度是:O(n);平均时间复杂度是O(n)。最好的序列是{1,5,19,41,109……}。该序列中的项或者是9 * 4 - 9 * 2 +1的形式,或者是4 - 3* 2+1)的形式。
- 空间复杂度:只用到一个临时变量,所以空间复杂度为O(1);
- 稳定性:不稳定排序。因为每一趟的步长不一样,所以步长长的插入排序可能会把后面的元素插入到前面。
三、总结
本篇说明了想要打破二次时间界,必须比较相距较远的元素来进行交换,这样可以一次交换删除多个逆序,以达到突破二次时间界。希尔排序通过使用增量序列来将原始序列分为多个子序列,对每个子序列进行插入排序。只要最终增量序列h=1,希尔排序都可正常工作。希尔排序时间严重依赖于增量序列的选择,我们可以直接先将好的增量序列“打表”存在数组中,这样不用每次排序都去计算。
声明:画图码字都辛苦,如有转载,请注明出处。文中引用来自《数据结构与算法java语言描述(原书第三版)》