本文是排序系列第 3 篇,聚焦希尔排序:它用分组插入 + 递减增量,把最坏 O(n^2) 降到接近 O(n log^2 n),是理解“局部有序→整体有序”思路的关键一站。

目标读者

  • 已掌握插入排序,想了解其高阶优化的学习者。
  • 需要在中等规模数据上用更小内存的工程师。
  • 想在算法分享或课程中讲解增量序列影响的人。

背景/动机

  • 插入排序在近乎有序时很快,但在随机数组上仍是 O(n^2)。
  • 希尔排序通过“分组 + 逐步减小增量”让元素快速移动到近似位置,再用小 gap 插入完成排序。
  • 增量序列的选择直接决定性能与实现复杂度,是本文重点。

A — Algorithm(题目与算法)

题目:对长度 n 的可比较序列进行排序,允许原地操作。

核心步骤(以 gap= n/2 开始)

  1. 选定初始 gap(增量),按 gap 将数组划分若干子序列。
  2. 对每个子序列做插入排序(步长为 gap)。
  3. 缩小 gap,重复步骤 2,直到 gap = 1(此时等同插入排序)。

基础示例 数组 [9, 8, 3, 7, 5, 6, 4, 1],gap 序列 4 → 2 → 1:

  • gap=4:子序列 (0,4),(1,5),(2,6),(3,7),分别插排,使元素大致到位。
  • gap=2:更细分组,再插排。
  • gap=1:最后一轮插排完成全局有序。

C — Concepts(核心思想)

关键概念说明
增量序列 (gap)典型有 n/2 递减、Knuth 序列 (1,4,13,40,…)、Sedgewick 序列等,影响比较次数上界。
分组插入在间隔为 gap 的子序列上执行插入排序,使远距离元素提前移动。
原地性仅使用常数额外空间。
稳定性传统实现不稳定(跨 gap 交换可能打乱相对顺序)。

复杂度范围

  • 最坏:取决于增量,简单的 n/2 递减最坏仍 O(n^2)。
  • 好的序列(如 Sedgewick)可达 O(n^(4/3)) 或 O(n log^2 n) 的上界,在实测中接近 O(n^{1.2~1.3})。
  • 空间:O(1)。

E — Engineering(工程应用)

场景 1:中等规模、内存敏感排序(C)

背景:嵌入式/后端中等规模数组(1e4~1e5),需要原地、无额外内存。 为何:希尔排序原地且常数低,优于纯插排;比堆排/快排在某些分布上更稳定性能。

void shell_sort(int *a, int n) {
    // Knuth 序列:1,4,13,40,... 直到 < n/3
    int gap = 1;
    while (gap < n/3) gap = gap * 3 + 1;
    for (; gap >= 1; gap /= 3) {
        for (int i = gap; i < n; ++i) {
            int temp = a[i], j = i;
            while (j >= gap && a[j-gap] > temp) {
                a[j] = a[j-gap];
                j -= gap;
            }
            a[j] = temp;
        }
    }
}

场景 2:几乎有序的小型业务列表(Python)

背景:列表每次追加少量尾部元素,但整体规模在 1e5 以内。 为何:用温和的 gap 序列让远端元素快速归位,最后 gap=1 插排收尾。

def shell_sort(arr):
    n = len(arr)
    gap = 1
    while gap < n // 3:
        gap = 3 * gap + 1  # Knuth
    while gap >= 1:
        for i in range(gap, n):
            temp = arr[i]
            j = i
            while j >= gap and arr[j - gap] > temp:
                arr[j] = arr[j - gap]
                j -= gap
            arr[j] = temp
        gap //= 3
    return arr

data = [9,8,3,7,5,6,4,1]
print(shell_sort(data))

场景 3:Go 服务端小型批处理

背景:单请求内排序长度 1e3~1e4,要求原地,减少 GC 压力。 为何:自定义希尔排序作为 sort.Interface 备选,避免额外分配。

package main

import "fmt"

func shellSort(a []int) {
    gap := 1
    for gap < len(a)/3 { gap = gap*3 + 1 }
    for gap >= 1 {
        for i := gap; i < len(a); i++ {
            tmp, j := a[i], i
            for j >= gap && a[j-gap] > tmp {
                a[j] = a[j-gap]
                j -= gap
            }
            a[j] = tmp
        }
        gap /= 3
    }
}

func main(){ arr := []int{9,8,3,7,5,6,4,1}; shellSort(arr); fmt.Println(arr) }

场景 4:前端大数组但需低内存(JavaScript)

背景:浏览器中处理几千条数据,避免频繁分配。 为何:原地、实现短,可直接用 Knuth 序列。

function shellSort(a){
  let gap = 1;
  while (gap < a.length/3) gap = gap*3 + 1;
  while (gap >= 1){
    for (let i = gap; i < a.length; 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 = Math.floor(gap/3);
  }
  return a;
}
console.log(shellSort([9,8,3,7,5,6,4,1]));

R — Reflection(反思与深入)

  • 复杂度
    • 时间:取决于 gap 序列。Knuth 序列表现良好但最坏仍可达 O(n^2)。Sedgewick 序列可提升到 O(n^(4/3)) 上界。
    • 空间:O(1)。
  • 对比替代
    • vs 插入:希尔大幅减少远距离移动;gap=1 时回到插入。
    • vs 快排/堆:希尔更缓存友好,但无严格 O(n log n) 上界;快排/堆在大规模更稳健。
    • vs 归并:归并稳定但需要 O(n) 额外空间,希尔原地但不稳定。
  • 最优性解释
    • 当数据中存在长距离错位时,先用大 gap 可迅速把元素推向近似位置,后续插排成本小。
    • 选择合适的 gap 是关键:过大收益有限,过小难以降低逆序对。

S — Summary(总结)

  • 希尔排序 = 分组插排 + 递减增量,原地但不稳定,性能高度依赖 gap 序列。
  • Knuth 序列是实践友好的默认;追求更佳上界可研究 Sedgewick / Pratt 序列。
  • 适合中等规模、需原地、对稳定性无要求的场景;大规模或需稳定时考虑归并/TimSort。
  • 现实混合策略:在自定义排序中,可用希尔排序替代“≤ 某阈值的插排”作为中间层。
  • 评估时要基于真实数据分布做基准,而非仅看理论复杂度。

实践指南 / 步骤

  • 选择 gap:默认 Knuth;若追求更好上界,可尝试 Sedgewick 序列(1,5,19,41,109…)。
  • 设置切换条件:当 gap=1 后继续插排完成;在混合排序中,可在子数组规模小于阈值时用希尔。
  • 准备测试集:随机、近乎有序、逆序、大量重复,观察性能与稳定性。
  • 记录指标:比较/移动次数、耗时、缓存命中(可用 perf/pprof)。

可运行示例:多语言实现

Python

def shell_sort(a):
    n=len(a); gap=1
    while gap < n//3: gap = 3*gap + 1
    while gap>=1:
        for i in range(gap,n):
            tmp=a[i]; j=i
            while j>=gap and a[j-gap]>tmp:
                a[j]=a[j-gap]; j-=gap
            a[j]=tmp
        gap//=3
    return a
print(shell_sort([9,8,3,7,5,6,4,1]))

C

void shell_sort(int *a, int n){
    int gap=1; while(gap < n/3) gap = gap*3 + 1;
    for(; gap>=1; gap/=3){
        for(int i=gap;i<n;i++){
            int tmp=a[i], j=i;
            while(j>=gap && a[j-gap]>tmp){ a[j]=a[j-gap]; j-=gap; }
            a[j]=tmp;
        }
    }
}

C++

void shell(vector<int>& a){
    int n=a.size(), gap=1; while(gap<n/3) gap=gap*3+1;
    for(; gap>=1; gap/=3){
        for(int i=gap;i<n;i++){
            int tmp=a[i], j=i;
            while(j>=gap && a[j-gap]>tmp){ a[j]=a[j-gap]; j-=gap; }
            a[j]=tmp;
        }
    }
}

Go

func ShellSort(a []int) {
    gap := 1
    for gap < len(a)/3 { gap = gap*3 + 1 }
    for gap >= 1 {
        for i := gap; i < len(a); i++ {
            tmp, j := a[i], i
            for j >= gap && a[j-gap] > tmp {
                a[j] = a[j-gap]
                j -= gap
            }
            a[j] = tmp
        }
        gap /= 3
    }
}

Rust

pub fn shell_sort(a: &mut [i32]) {
    let mut gap = 1usize;
    while gap < a.len()/3 { gap = gap*3 + 1; }
    while gap >= 1 {
        for i in gap..a.len() {
            let tmp = a[i];
            let mut j = i;
            while j >= gap && a[j-gap] > tmp {
                a[j] = a[j-gap];
                j -= gap;
            }
            a[j] = tmp;
        }
        if gap == 1 { break; }
        gap /= 3;
    }
}

JavaScript

function shellSort(a){
  let gap=1; while(gap < a.length/3) gap = gap*3 + 1;
  while(gap>=1){
    for(let i=gap;i<a.length;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=Math.floor(gap/3);
  }
  return a;
}

常见问题与注意事项

  • 稳定性:希尔排序不稳定,若稳定性必需,选择归并/TimSort。
  • 增量选择:简单的 n/2 递减实现容易退化,建议至少用 Knuth 或 Sedgewick 序列。
  • 大小写:对极小数组直接用插排即可;对超大数组需评估是否改用 O(n log n) 算法。
  • 性能测试:不同 gap 在不同数据分布下差异大,务必实测。

最佳实践与建议

  • 默认用 Knuth 序列,代码短、性能好;需要理论上界可换 Sedgewick/Pratt。
  • 在混合排序中,将“子数组规模阈值”替换为希尔排序,观察是否好于纯插排。
  • 为教学准备可视化:展示 gap=4/2/1 的分组插排过程,帮助理解。
  • 记录“比较/移动”计数,作为评估不同 gap 序列的指标。

小结 / 结论

  • 希尔排序通过分组插排显著降低远距离逆序对,原地但不稳定,性能依赖增量序列。
  • Knuth 序列是实践优选;需要稳定或严格上界时改用归并/TimSort/堆排。
  • 在工程混合策略中,希尔排序可作为小规模优化层,弥合插排与快排/堆排间的性能差距。

参考与延伸阅读

  • D. L. Shell, “A High-Speed Sorting Procedure” (1959)
  • Robert Sedgewick, “Analysis of Shellsort and Related Algorithms” (1986)
  • CLRS《算法导论》希尔排序讨论

元信息

  • 阅读时长:约 15 分钟
  • SEO 关键词:希尔排序, Shell Sort, 增量序列, 原地排序, 不稳定排序
  • 元描述:排序专题第三篇,深入讲解希尔排序的增量序列、复杂度与工程实践,附多语言实现与选型建议。

行动号召(CTA)

  • 用你的真实数据分布,对比 Knuth 与 Sedgewick 序列的耗时差异。
  • 在现有快排实现中,把小分段插排改为希尔排序,测量性能变化。
  • 关注后续系列:归并、快排、堆排序、非比较排序、TimSort/Introsort 与选型实战。