Skip to content

希尔排序

为什么需要希尔排序

直接插入排序在序列基本有序时效率很高,可以达到 O(n);但当序列逆序或随机分布时,每次插入都需要大量移动元素,退化为 O(n²)。

希尔排序(Shell Sort)的思路是:先让序列宏观上趋于有序,再对整体做一次插入排序。通过将序列按一定增量分组,对每组分别进行插入排序,随着增量逐步缩小,整个序列越来越接近有序,最终增量为 1 时完成排序。这就是"缩小增量排序"名称的由来。

核心思想

希尔排序的核心特点:

  • 缩小增量:选定一个增量序列 d₁ > d₂ > … > dₖ = 1,每趟按当前增量 dᵢ 将序列划分为若干子序列
  • 分组插入:对每个子序列执行直接插入排序,使元素能跨越较大距离移动到接近最终位置
  • 逐步逼近有序:增量不断缩小,子序列越来越长、序列越来越有序,最后一趟 d=1 等价于对"几乎有序"的序列做直接插入排序

工作原理示意(以 d=4, 2, 1 为例):

原始序列:  [8, 5, 2, 9, 3, 7, 1, 6]

d=4:  分组 {8,3} {5,7} {2,1} {9,6}  →  各组插入排序
      [3, 5, 1, 6, 8, 7, 2, 9]

d=2:  分组 {3,1,8,2} {5,6,7,9}      →  各组插入排序
      [1, 5, 2, 6, 3, 7, 8, 9]

d=1:  整体插入排序(此时序列已基本有序,移动次数很少)
      [1, 2, 3, 5, 6, 7, 8, 9]

交互可视化

通过下方的交互动画,你可以逐步观察希尔排序的执行过程:

加载可视化中...

操作详解

算法思路

  1. 选择一个增量序列,初始增量 d = n/2(或其他方案)
  2. 按增量 d 将序列分为 d 组:下标为 {0, d, 2d, …}、{1, 1+d, 1+2d, …} 等
  3. 对每组执行直接插入排序
  4. 缩小增量 d = d/2,重复步骤 2-3
  5. 直到 d = 1,完成最后一趟排序
cpp
void ShellSort(int A[], int n) {
    // d 为增量,每次减半
    for (int d = n / 2; d >= 1; d /= 2) {
        // 从第 d 个元素开始,对每个子序列做插入排序
        for (int i = d; i < n; i++) {
            int temp = A[i];
            int j = i - d;
            // 在当前子序列中找到插入位置
            while (j >= 0 && A[j] > temp) {
                A[j + d] = A[j];
                j -= d;
            }
            A[j + d] = temp;
        }
    }
}

注意:代码中并没有显式地"分组再分别排序",而是用一个循环交替处理各组元素,效果等价但写法更简洁。

增量序列

增量序列的选择直接影响希尔排序的时间复杂度,408 常见的有以下几种:

增量序列公式示例(n=16)最坏时间复杂度
Shell 原始序列dᵢ = n/2ⁱ8, 4, 2, 1O(n²)
Hibbard 序列dᵢ = 2ⁱ - 115, 7, 3, 1O(n^1.5)
Knuth 序列dᵢ = (3ⁱ - 1)/213, 4, 1O(n^1.5)

考试默认使用 Shell 原始序列(d = n/2, n/4, …, 1),除非题目另有说明。

⭐ 增量序列必须满足的条件:最后一个增量必须为 1(否则无法保证排序完成)。

排序过程详解

以序列 [49, 38, 65, 97, 76, 13, 27, 49*](n=8)为例,使用 Shell 原始增量序列。其中 49* 用于标注第二个 49 以观察稳定性。

第一趟 d=4:分为 4 组

组1: A[0]=49, A[4]=76  →  49, 76(无交换)
组2: A[1]=38, A[5]=13  →  13, 38
组3: A[2]=65, A[6]=27  →  27, 65
组4: A[3]=97, A[7]=49* →  49*, 97

结果:[49, 13, 27, 49*, 76, 38, 65, 97]

第二趟 d=2:分为 2 组

组1: A[0]=49, A[2]=27, A[4]=76, A[6]=65
     → 插入排序后: 27, 49, 65, 76
组2: A[1]=13, A[3]=49*, A[5]=38, A[7]=97
     → 插入排序后: 13, 38, 49*, 97

结果:[27, 13, 49, 38, 65, 49*, 76, 97]

第三趟 d=1:整体插入排序

结果:[13, 27, 38, 49, 49*, 65, 76, 97]

⭐ 观察两个 49 的最终位置:原始序列中 4949* 前面,排序后 49 仍在 49* 前面——看似稳定,但这只是巧合。在 d=4 那趟中,49* 被移动到了 49 前面(下标 3 < 下标 0 的后续位置),后续又被调回。不同增量序列或不同数据下,相同元素的相对顺序可能被打乱。 因此希尔排序是不稳定的排序算法。

复杂度分析

指标结果说明
最好时间复杂度O(n^1.3)经验值,取决于增量序列
最坏时间复杂度O(n²)使用 Shell 原始增量序列时
平均时间复杂度约 O(n^1.3)数学上未完全解决,考试记此值
空间复杂度O(1)仅需常数级辅助变量
稳定性不稳定跨子序列移动可能改变相同元素的相对顺序
适用性仅适用于顺序表需要按下标随机访问,链表无法使用

为什么不稳定? 相同关键字的元素可能被分到不同子序列中,各子序列独立排序时,无法感知其他子序列中相同元素的位置,从而可能打乱它们的相对顺序。

与直接插入排序的对比

对比项直接插入排序希尔排序
时间复杂度O(n²)约 O(n^1.3)
稳定性稳定不稳定
适用场景基本有序或 n 较小中等规模数据
核心区别每次移动一个位置元素可跨越多个位置移动

考研高频考点

  • ⭐ 给定序列和增量,写出每趟排序的结果(大题/选择题高频)
  • ⭐ 希尔排序的稳定性判断及原因说明(选择题/简答题必考)
  • ⭐ 增量序列的最后一个值必须为 1(判断题)
  • ⭐ 时间复杂度与增量序列的关系:Shell 原始序列最坏 O(n²),考试中平均记 O(n^1.3)
  • ⭐ 希尔排序 vs 直接插入排序的联系与区别(简答题)
  • 希尔排序不能用于链表(需要随机访问)
  • 空间复杂度 O(1),属于原地排序