Back to Journal
02 / Entry· 2 min read

排序算法系列 05:希尔排序——给插入排序装个加速器

希尔排序的原理与实现,通过分组大步调整让插入排序突破O(n²)的瓶颈。

🔊 系统朗读

排序算法系列 05:希尔排序——给插入排序装个加速器

上一篇说了,插入排序在数据基本有序时非常快。那问题来了:如果数据很乱呢?

比如最小的数在数组最后面,插入排序得一步一步把它挪到最前面,中间所有元素都要移动一遍。太慢了。

希尔排序的想法很朴素:既然插入排序擅长处理基本有序的数据,那我先想办法让数据变得"大致有序",再让插入排序收尾。

怎么让数据大致有序?答案是:先用大步长做分组插入排序,再逐步缩小步长。

核心思想

不一上来就相邻比较,而是先隔开一定间距分组,每组内部做插入排序。然后缩小间距,再排。最后间距缩到 1,就是普通插入排序——但这时候数据已经基本有序了。

举个直觉上的例子:一个很小的数在数组末尾,普通插入排序要挪 n 步才能到前面。但如果步长是 4,它一次就能跳 4 格,几步就到了大致正确的位置。

执行过程

[8, 5, 3, 1, 6, 9, 2, 7, 4] 跑一遍,数组长度 9。

第一趟,gap = 4:

按间隔 4 分组:

text
下标 0, 4, 8 → 元素 8, 6, 4
下标 1, 5    → 元素 5, 9
下标 2, 6    → 元素 3, 2
下标 3, 7    → 元素 1, 7

每组内部做插入排序,大元素和小元素之间的距离被快速拉近。

第二趟,gap = 2:

间距缩小,继续分组排序。数据更接近有序了。

第三趟,gap = 1:

就是普通插入排序。但因为前面两趟已经把数据调得差不多了,这一趟基本上只需要微调,跑得很快。

代码实现

javascript
function shellSort(arr) {
  const result = [...arr];
  let gap = Math.floor(result.length / 2);

  while (gap > 0) {
    for (let i = gap; i < result.length; i++) {
      const current = result[i];
      let j = i;

      while (j >= gap && result[j - gap] > current) {
        result[j] = result[j - gap];
        j -= gap;
      }

      result[j] = current;
    }

    gap = Math.floor(gap / 2);
  }

  return result;
}

console.log(shellSort([8, 5, 3, 1, 6, 9, 2, 7, 4]));
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

仔细看这段代码——它本质上就是插入排序,只不过把原来的 j - 1 换成了 j - gap。当 gap = 1 时,它和插入排序一模一样。

间隔序列怎么选?

上面用的是最简单的折半序列:n/2, n/4, n/8, ..., 1

但这不是最优的。学术界研究了很多不同的间隔序列,比如 Knuth 序列(1, 4, 13, 40, 121, ...),不同序列会影响时间复杂度的上界。

对于入门来说,折半就够了。知道"间隔序列会影响性能"这个结论就行。

复杂度

情况时间复杂度
最好接近 O(n log n)
平均通常好于 O(n²),取决于间隔序列
最坏折半序列下可能 O(n²)

空间复杂度 O(1)

稳定性:不稳定。大步长交换可能让相等元素的相对顺序被打乱。

希尔排序的定位

说实话,在工程实践中,希尔排序用得不多。快速排序、归并排序在大多数场景下更优。

但希尔排序的价值在于这个思想:先粗调再细调。这种"先用大粒度处理,再逐步精细化"的策略在很多地方都能看到——比如图像处理的多分辨率金字塔、搜索引擎的分层索引。

另外它也是理解"为什么插入排序在基本有序时快"的最佳注脚——希尔排序就是在利用这个性质。

下一篇进入高级排序的领域:归并排序。从这里开始,时间复杂度会从 O(n²) 跳到 O(n log n)


系列导航

上一篇:排序算法系列 04:插入排序——打牌的人都会

下一篇:排序算法系列 06:归并排序——分治思想的教科书级应用

分享
← 返回博客列表
🎁 有邀请福利哦,点击查看
🎁