Skip to content

间隔序列:决定性能的关键

基于通用算法套路 · 核于 2026-07

速查

  • gap 序列决定复杂度:希尔排序的时间复杂度完全取决于 gap 序列的选择——同一个「分组插入」框架,换一套 gap 序列,最坏复杂度可能从 O(n²) 降到 O(n^1.3)。这是希尔排序最核心、也最独特的特性。
  • Shell 原版序列n/2, n/2/2, …, 1(即 gap = gap >> 1,每次减半)。最简单直观,但最坏仍 O(n²)——存在精心构造的输入让它在某些 gap 上的子序列恰好全逆序。
  • Knuth 序列1, 4, 13, 40, 121, …(递推 h = 3h + 1,从 1 倒推到不超过 n)。最坏 O(n^1.5),且工程实现简洁,是教科书和库实现的常用选择。
  • Sedgewick 序列1, 5, 19, 41, 109, …(由 9×4^k − 9×2^k + 14^k − 3×2^k + 1 交错合并)。平均约 O(n^(4/3)),是目前实际表现最好的序列之一。
  • 为何 gap 必须互质/有特定结构:若 gap 序列之间有公因子(如都是偶数),元素会反复在固定的少数几个位置间移动,无法充分「打散」;好的序列让各轮的分组互相错开,覆盖所有位置。
  • gap 必须缩到 1:gap=1 是正确性的兜底(普通插入排序消除所有相邻逆序);任何 gap 序列的最后一项都必须是 1。
  • 统一代码框架:外层遍历 gap 序列,内层是「把 gap 当步长的插入排序」——换序列只改外层 gap 的取值,内层逻辑完全不变。
  • 无「最优」序列:希尔排序 gap 序列的最坏复杂度至今是数学上的开放问题,没有严格证明的下界;O(n^1.3) 等多为经验值。
  • 进阶顺序性能分析参考

一、为何 gap 序列决定复杂度

希尔排序的框架是固定的:多轮插入排序,gap 从大到小。但「取哪些 gap 值」直接影响每一轮的工作量和总效果。考虑两个极端:

  • 差的序列(如 gap 之间有公因子):各轮分组高度重叠,元素在固定位置间反复横跳,总比较次数退化到 O(n²)。
  • 好的序列(如互质或有精巧递推):各轮分组互相错开,每轮都在前一轮基础上有效推进,总比较次数降到 O(n^1.3) 左右。

这正是 Shell 原版(n/2 减半)最坏仍是 O(n²)、而 Sedgewick 序列能到 O(n^1.3) 的原因——算法骨架相同,性能差异完全来自 gap 取值。这是希尔排序区别于其他排序(如快排的复杂度由 pivot 决定)的独特之处:它的「调参」就是选 gap 序列。

二、Shell 原版序列:最简单的 O(n²)

Donald Shell 最初提出的序列就是每次减半

gap = n/2, n/4, n/8, …, 1   (gap = gap >> 1,直到 gap = 0)
js
for (let gap = n >> 1; gap >= 1; gap >>= 1) {
  for (let i = gap; i < n; i++) {
    const tmp = a[i];
    let j = i;
    while (j >= gap && a[j - gap] > tmp) { a[j] = a[j - gap]; j -= gap; }
    a[j] = tmp;
  }
}
  • 优点:实现极简(一行 gap >>= 1),易记易写。
  • 缺点最坏 O(n²)。原因是 gap 取值都是 2 的幂,它们之间有公因子 2——存在精心构造的输入(让某些子序列在多个 gap 下都全逆序),导致总比较次数退化。
  • 实际表现:对随机数据仍比插入排序快得多(接近 O(n^1.3) 的实际体验),但最坏情况不保证。

三、Knuth 序列:最坏 O(n^1.5) 的工程选择

Knuth 提出用 h = 3h + 1 递推生成序列,让相邻 gap 互质(3 倍关系),避免公因子问题:

序列:1, 4, 13, 40, 121, 364, …   (h_{k+1} = 3·h_k + 1)
取法:从最大的「不超过 n/3」的 h 开始,逐步除以 3(向下取整)缩到 1
js
// 先生成不超过 n 的最大 h
let h = 1;
while (h < n / 3) h = 3 * h + 1;   // 1, 4, 13, 40, … 取最大的 < n/3
for (; h >= 1; h = Math.floor(h / 3)) {
  for (let i = h; i < n; i++) {
    const tmp = a[i];
    let j = i;
    while (j >= h && a[j - h] > tmp) { a[j] = a[j - h]; j -= h; }
    a[j] = tmp;
  }
}
  • 最坏复杂度 O(n^1.5):Knuth 证明了用这个序列,最坏情况是 O(n^1.5)——比 Shell 原版的 O(n²) 严格更优。
  • 工程友好:递推简单(3h+1),只需一个 while 预算最大 h,内层与 Shell 原版完全一致。是许多教科书和库的默认选择。
  • 平均约 O(n^1.25):经验上对随机数据表现良好。

四、Sedgewick 序列:实际最快的 O(n^1.3)

Sedgewick 通过大量实验和数论分析,提出一个由两个公式交错合并的序列,是目前实际表现最好的之一:

序列:1, 5, 19, 41, 109, 209, 505, 929, …
生成:偶数项 9×4^k − 9×2^k + 1(k=0,1,2,…)= 1, 19, 109, 505, …
      奇数项 4^(k+2) − 3×2^(k+2) + 1(k=0,1,2,…)= 5, 41, 209, 929, …
      交错合并并去重排序
js
// 预先生成不超过 n 的 Sedgewick 序列(降序使用)
const gaps = [];
let k = 0;
while (true) {
  const a = 9 * (1 << (2 * k)) - 9 * (1 << k) + 1;          // 9·4^k − 9·2^k + 1
  const b = (1 << (2 * k + 4)) - 3 * (1 << (k + 2)) + 1;    // 4^(k+2) − 3·2^(k+2) + 1
  if (a <= n) gaps.push(a);
  if (b <= n) gaps.push(b);
  if (a > n && b > n) break;
  k++;
}
gaps.sort((x, y) => y - x);   // 降序,从大到小用
for (const gap of gaps) {
  for (let i = gap; i < n; i++) {
    const tmp = a[i];
    let j = i;
    while (j >= gap && a[j - gap] > tmp) { a[j] = a[j - gap]; j -= gap; }
    a[j] = tmp;
  }
}
  • 平均约 O(n^(4/3)) ≈ O(n^1.33):实验和部分理论分析显示,这是目前序列中平均表现最好的之一。
  • 最坏 O(n^(4/3)):已证明的最坏上界也优于 Knuth 的 O(n^1.5)。
  • 代价:序列生成稍复杂(两个公式交错),但性能收益明显,适合对速度有要求的场景。

五、各 gap 序列性能对比

gap 序列典型取值最坏复杂度平均(经验)特点
Shell 原版n/2, n/4, …, 1O(n²)~O(n^1.3)最简单,最坏会退化
Hibbard2^k − 1O(n^1.5)~O(n^1.25)相邻互质
Knuth3h+1(1,4,13,40,…)O(n^1.5)~O(n^1.25)教科书常用,工程友好
Sedgewick9·4^k−9·2^k+1 等O(n^(4/3))~O(n^(4/3))实际最快之一
Pratt2^p·3^qO(nlog²n)O(nlog²n)理论好但项数多、常数大

选型建议:教学/快速实现用 Shell 原版(够直观);工程默认用 Knuth 序列(平衡);追求极致性能用 Sedgewick 序列

六、为何 gap 之间需要「互质」

如果 gap 序列的所有值都有公因子(比如 Shell 原版全是 2 的幂),那么各轮的分组在数学上是高度相关的——某些下标组合永远不会被同一轮处理,导致这些位置的逆序对要等到 gap=1 才能消除,退化成插入排序的 O(n²)。

好的 gap 序列让相邻两个 gap 互质(如 Knuth 的 3 倍关系、Hibbard 的 2^k−1),这样每一轮的分组都和前一轮错开,能持续处理新的逆序对,避免「原地踏步」。这是 Knuth/Sedgewick 序列优于 Shell 原版的本质原因——不是 gap 的数值大小,而是它们之间的数论关系决定性能。

交互演示

下一步

理解了 gap 序列如何决定复杂度后,下一步是深入性能分析——为何跨距离交换能突破 O(n²)、平均 O(n^1.3) 从何而来、以及与各排序的横向对比,见性能分析