← 返回题目列表

希尔排序是什么?它是怎么改进插入排序的?

中等 第 25 / 26 题 更新于 2026/07/28
排序希尔排序插入排序

简化版

希尔排序是插入排序的改进版。插入排序每次只能交换相邻元素、移动很慢;希尔排序先用一个较大的间隔(gap) 把数组分成若干组,对每组做插入排序,然后逐步缩小 gap 直到 1(最后一轮就是普通插入排序)。大 gap 让元素能「大步跳跃」快速接近最终位置,等 gap 变小时数组已近乎有序,插入排序就很快。它突破了 O(n²),但不稳定

详细版

void shellSort(int[] a) {
    int n = a.length;
    for (int gap = n / 2; gap > 0; gap /= 2) {   // gap 逐步减半直到 1
        for (int i = gap; i < n; i++) {           // 对每个 gap 间隔的组做插入排序
            int cur = a[i], j = i - gap;
            while (j >= 0 && a[j] > cur) {
                a[j + gap] = a[j];                // 按 gap 后移
                j -= gap;
            }
            a[j + gap] = cur;
        }
    }
}
  • gap 序列:常见从 n/2 开始每次减半(希尔原始序列),直到 gap=1。
  • 每个 gap 下,对「间隔为 gap 的元素组」做插入排序(其实是把插入排序里的「1」换成「gap」)。
  • gap=1 的最后一轮就是标准插入排序,但此时数组已近乎有序,极快。

完整版教学

一、插入排序的瓶颈:只能挪一格

插入排序有个硬伤:它每次只能把元素和相邻元素交换/后移一格。如果一个很小的元素躺在数组最右边,要把它挪到最左边,得一格一格地移 n 步。当有很多这种「远距离逆序对」时,插入排序就慢(O(n²))。希尔排序的核心洞察是:能不能让元素先大步跳跃、快速接近目标位置,减少后面的小步移动?

二、希尔的思路:先粗排,再细排

希尔排序引入一个间隔 gap,把「相隔 gap 的元素」看成一组,对每组做插入排序:

  • gap 大时(如 n/2):每组只有很少几个元素,但它们在原数组里相距很远。对它们排序,能让元素一次跳跃 gap 那么远,快速把「大范围的乱序」消除掉。
  • gap 逐步缩小:每缩小一次,分组变粗、组内元素变多,但因为前面大 gap 已经让数组「大致有序」了,插入排序移动很少。
  • gap=1 时:就是普通插入排序,但此时数组已经近乎有序,插入排序发挥它「近乎有序时接近 O(n)」的优势,飞快收尾。

一句话:用大 gap 快速消除大范围乱序,用小 gap 精细收尾,避开了插入排序「只能挪一格」的瓶颈。

三、走一个直觉例子

数组: [8, 5, 3, 7, 1, 9, 2, 6]  n=8
gap=4: 分成 4 组 {8,1}{5,9}{3,2}{7,6},各组插入排序
       → [1, 5, 2, 6, 8, 9, 3, 7]   (大范围乱序被快速消除)
gap=2: 分成 2 组 {1,2,8,3}{5,6,9,7},各组插入排序
       → [1, 5, 2, 6, 3, 7, 8, 9]
gap=1: 普通插入排序,此时已近乎有序,少量移动即完成
       → [1, 2, 3, 5, 6, 7, 8, 9]

四、复杂度:取决于 gap 序列

希尔排序的复杂度和 gap 序列的选择密切相关,分析很复杂:

  • 希尔原始序列(n/2, n/4, …, 1):最坏 O(n²)
  • Hibbard 序列(1,3,7,…,2^k−1):最坏 O(n^1.5)。
  • Sedgewick 序列等:可到约 O(n^1.3)。

所以希尔排序平均大致在 O(n^1.3) ~ O(n^1.5),明显优于插入排序的 O(n²),但达不到 O(n log n)。它的实际表现和 gap 序列强相关,是个「用简单代码换来不错性能」的折中。

五、为什么希尔排序不稳定

插入排序是稳定的,但希尔排序不稳定。原因:希尔排序在不同的分组里移动元素,相等的两个元素可能被分到不同的 gap 组、各自移动,它们的相对顺序就可能被打乱。跨组的远距离移动破坏了稳定性。

六、希尔排序的定位

  • 优点:代码简单(就是带 gap 的插入排序)、原地 O(1) 空间、突破了 O(n²)、对中等规模数据表现不错。
  • 缺点:不稳定;复杂度分析困难、依赖 gap 序列;比 O(n log n) 的快排/归并慢。
  • 定位:是「简单排序」到「高效排序」之间的过渡,历史意义大(第一个突破 O(n²) 的排序),实际工程用得不多(有快排/归并),但面试常考它「如何改进插入排序」的思想。

七、把不变量、推演与工程边界落到代码上

算法正确性的核心不是记住某个 while,而是始终维护这个不变量:每个 gap 阶段结束后,所有相距 gap 的子序列都分别有序。

对应的状态推进是:对每个 gap 做分组插入排序,逐步缩小 gap,最后必须取 gap=1。每次推进前都应能解释“为什么被排除的部分不可能再包含答案”,否则只是碰巧通过样例。

初始化边界与状态
while 尚未结束:
    根据当前状态作出唯一可证明安全的选择
    更新边界、计数或局部结构
    断言不变量仍然成立
返回不变量在终止状态下推出的答案

复杂度不能只背一个符号。复杂度依赖增量序列;常见折半序列最坏可达 O(n²),不能笼统写 O(n log n)。

带数字走一遍:[9,1,8,2,7,3] 用 gap=3 可先跨距离移动 9 和 2,减少最终逐格搬移。手算时要同时记录下标、区间含义和本轮排除的候选,才能暴露等号与边界错误。

核对项结论
前提适合中等规模、内存受限且不要求稳定的内部排序
时间复杂度取决于 gap 序列,常见最好优于插入、最坏可 O(n²)
额外空间O(1)
关键边界gap 序列必须最终到 1;跨组移动会破坏相等元素顺序
替代方案现代通用排序通常采用经过充分验证的混合算法

易错点:模板只有在前提和不变量一致时才成立;换了区间定义、重复值规则或输出语义,等号与推进方式就必须重新推导。

实现完成后至少检查五类用例:

  • 空输入或题目允许的最小规模,验证初始化不会越界。
  • 单元素与两个元素,验证循环条件和最后一次推进。
  • 大量重复值,验证相等分支、稳定性或去重语义。
  • 已满足目标性质与极端逆序/偏斜分布,观察最好和退化路径。
  • 上面的数字样例逐轮手算,并与程序记录的状态轨迹逐项对照。

八、常见误区与追问

  • 误区:只要记住模板就适用于所有输入。 本题成立的前提是“适合中等规模、内存受限且不要求稳定的内部排序”,前提被破坏后必须换算法或重新证明。
  • 误区:复杂度只写 取决于 gap 序列,常见最好优于插入、最坏可 O(n²) 就算完整。 还要说明输入参数、预处理、递归栈以及最坏退化条件;本题的推导是“复杂度依赖增量序列;常见折半序列最坏可达 O(n²),不能笼统写 O(n log n)”。
  • 误区:重复值和边界值不会改变代码。 gap 序列必须最终到 1;跨组移动会破坏相等元素顺序。
  • 追问:为什么每次推进不会漏掉答案? 因为始终维护“每个 gap 阶段结束后,所有相距 gap 的子序列都分别有序”,被舍弃区域已由顺序或状态关系证明不可能更优。
  • 追问:用一个数字例子怎么讲? 可以从“[9,1,8,2,7,3] 用 gap=3 可先跨距离移动 9 和 2,减少最终逐格搬移”开始,逐轮写出状态与被排除区间。
  • 追问:什么时候不该使用这个方案? 当前提不满足或退化代价不可接受时,应考虑“现代通用排序通常采用经过充分验证的混合算法”。
  • 追问:如何设计自测避免只过样例? 同时覆盖最小规模、重复值、极端分布、无解/边界解,并对关键状态加断言。

九、加强记忆

希尔排序是插入排序的改进:用间隔 gap 分组、对每组插入排序,gap 逐步缩小到 1(最后一轮即普通插入排序)。核心思想:大 gap 让元素大步跳跃、快速消除大范围乱序,小 gap 时数组已近乎有序、插入排序飞快收尾,避开了插入排序「只能挪一格」的瓶颈。突破 O(n²)(约 O(n^1.3~1.5),依赖 gap 序列)、原地 O(1)、但不稳定(跨组远距离移动打乱相等元素)。