希尔排序
-
算法描述
-
希尔排序是简单插入排序的改进,它基于以下事实。
-
简单插入排序对排序程度较高的序列有较高的效率。假设初始序列已完全排序,则每一轮均只需比较一次,将得到 O(n)O(n) 的线性复杂度,冒泡排序和选择排序做不到这一点,均仍需 O(n^2)O(n 2) 次比较(冒泡排序在应用提前结束优化后可以做到)。
-
简单插入排序每次比较最多将数字移动一位,效率较低。
-
Donald Shell在1959年发表的论文中,针对第二点,提出如下方法。对原待排序列中相隔 gap 的数字执行简单插入排序,然后缩小 gap,对新的 gap 间隔的数字再次执行简单插入排序。以一种规则减少 gap 的大小,当 gap 为1时即简单插入排序,因此希尔排序也称作 增量递减排序。希尔在论文中提出的增量序列为{1, 2, 4, 8,…},即2^k,k = 1, 2, 3, …,在讨论希尔排序时,可将其称为 希尔增量。
-
程序开始时 gap 较大,待排元素较少,因此排序速度较快。当 gap 较小时,基于第一点,此时待排序列已大致有序,排序效率接近线性复杂度。因此能够期待希尔排序复杂度将优于 O(n^2)。详细见「复杂度分析」。
-
稳定性:不稳定。
-
gap > 1时,跨越gap的插入可能会改变两个相同值元素的位置关系。例如{0, 1, 4, 3, 3, 5, 6},当gap = 2时,对{0, 4, 3, 6}简单插入排序后得到{0, 1, 3, 3, 4, 5, 6},原数组中的两个3的位置互换了。
-
复杂度分析
-
时间复杂度:希尔排序的时间复杂度与增量序列的选择有关。最优复杂度增量序列尚未明确。
Shell增量(Shell, 1959): {1, 2, 4, 8,…},即 2^k2 k,k = 1, 2, 3, …,最坏时间复杂度 Θ(n^2)Θ(n 2)。
Hibbard增量(Hibbard, 1963):{1, 3, 7, 15,…},即 2^k - 12 k −1,k = 1, 2, 3, …,最坏时间复杂度 Θ(n^\frac{3}{2})Θ(n 23)。
Knuth增量(Knuth, 1971):{1, 4, 13, 40,…},即 (3^k - 1) / 2(3 k −1)/2,k = 1, 2, 3, …,最坏时间复杂度 Θ(n^\frac{3}{2})Θ(n 23 )。
Sedgewick增量(Sedgewick, 1982): {1, 8, 23, 77, 281},即 4^k + 3*2^(k-1) + 14 k +3∗2 ( k−1)+1 (最小增量1直接给出),k = 1, 2, 3, …,最坏时间复杂度 Θ(n^\frac{4}{3})Θ(n 34 )。
复杂度的证明需要借助数论和组合数学,略 (我不会)。
-
空间复杂度:算法中只有常数项变量,O(1)O(1)。
-
逆序数
希尔排序是较早出现的 突破二次复杂度 的排序算法,下面从 逆序数 的角度来直观地证明为何希尔排序能够突破二次复杂度。
在一个排列中,如果任意一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个 逆序,一个排列中逆序的总数就称为这个排列的 逆序数。排序的过程就是不断减少逆序数 直到逆序数为 0 的过程。
回顾冒泡排序和简单插入排序,算法的每一次交换,都只交换相邻元素(简单插入排序中元素每次右移也看作交换),因此每次交换只能减少一个逆序。冒泡排序和简单插入排序的元素平均交换次数均为 O(n^2)O(n 2), 也即逆序数(或逆序数减少次数)为 O(n^2)O(n 2 )。 如果能跨越多个数字进行交换,则可能一次减少多个逆序。在选择排序中,每轮选到最小元素后的交换即是跨越多个元素的,交换次数(减少逆序数的操作)为 O(n)O(n),要少于冒泡和简单插入排序,只是因为比较次数仍是 O(n^2)O(n 2 ), 所以整体复杂度为 O(n^2)O(n 2 )。
现在来分析跨越多个元素的交换如何减少逆序数,假设 arr[i] > arr[j], i < j。对于任意的 arr[k] (i < k < j):
若 arr[k] < arr[j],交换 arr[i] 和 arr[j] 后,三者的逆序数从2变为1
若 arr[k] > arr[i],交换 arr[i] 和 arr[j] 后,三者的逆序数从2变为1
若 arr[i] > arr[k] > arr[j],交换 arr[i] 和 arr[j] 后,三者的逆序数从3变为0
arr[k] = arr[i] 或 arr[k] = arr[j] 的情况一样,都使得三者逆序数从2变为1,下图省略。
对 arr[i] 和 arr[j] 的逆序消除,使得逆序 至少 减少一次,并 有机会减少大于一次的逆序 (情况3),因此能够以比 n^2n 2 低阶的次数消除所有逆序。
实际上归并排序,快速排序,堆排序均实现了 长距离交换元素,使得复杂度优于 O(n^2)。
代码
// 希尔排序: 采用Knuth增量
public int[] shellSort(int[] arr) {
if (arr.length < 2) return arr;
int gap = 1; // 初始化gap
while (gap < arr.length / 3) { // Knuth增量序列
gap = gap * 3 + 1;
}
// 不断缩小gap直到1,对每一个gap值执行一次插入排序
while (gap > 0) {
for (int i = gap; i < arr.length; i += gap) { // 注意步长增量是gap
int target = arr[i], j = i - gap;
for (; j >= 0; j -= gap) {
if (target < arr[j]) arr[j + gap] = arr[j];
else break;
}
if (j != i - gap) arr[j + gap] = target; // 插入
}
gap /= 3; // 缩小gap值
}
return arr;
}