当前位置: 首页 > ds >正文

数据结构:希尔排序 (Shell Sort)

目录

从“改进插入排序”的思路开始

希尔排序的完整策略:

手动模拟全过程

代码的逐步完善

第一阶段:搭建外层循环框架(控制 gap)

第二阶段:实现“分组插入排序”(中间循环)

第三阶段:实现核心的“插入”逻辑(内层循环)

复杂度与特性分析

时间复杂度 (Time Complexity)

空间复杂度 (Space Complexity)

稳定性 (Stability)


我们来探讨一个非常巧妙的排序算法——希尔排序 (Shell Sort)。它也被称为缩小增量排序 (Diminishing Increment Sort)。

理解希尔排序的关键,在于先理解它要“改进”的那个算法的优缺点。

从“改进插入排序”的思路开始

我们先回顾一下插入排序 (Insertion Sort)

  • 优点:当一个数组几乎有序时,插入排序的效率极高,接近 O(n)。

  • 致命弱点:当一个非常小的元素处在数组的末尾时,插入排序的效率极低。因为它每次只能将元素移动一个位置,要把这个小元素挪到数组开头,需要进行大量的、一步一步的移动。

这就启发我们思考一个根本性的问题:有没有办法让元素可以进行“长距离”的移动?

如果我们可以先通过某种方式,将那个在末尾的小元素,一次性地“跳”到数组的前半部分,那么后续的排序就会轻松很多。

💡唐纳德·希尔 (Donald Shell) 的天才构想:

不要直接对整个数组进行插入排序。我们可以先设定一个比较大的步长(或称增量/间隔,gap,比如 gap = 4。然后,我们不把数组看作一个整体,而是看作4个独立的“子数组”:

  1. arr[0], arr[4], arr[8], ... 构成的子数组。

  2. arr[1], arr[5], arr[9], ... 构成的子数组。

  3. arr[2], arr[6], arr[10], ... 构成的子数组。

  4. arr[3], arr[7], arr[11], ... 构成的子数组。

然后,我们对这4个子数组分别进行插入排序。

这样做有什么好处?

在对 {arr[0], arr[4], arr[8]} 进行排序时,arr[8] 的元素如果比 arr[0] 小,它们就可以一次性交换!这实现了一次跨度为8的“长距离移动”。

这个过程完成后,数组并没有完全排好序,但它变得比原来“更有序”了。那些非常小的元素,通过这种大步长的交换,被快速地移动到了数组的前端。

希尔排序的完整策略:

  1. 选择一个大的 gap 值,对所有“gap-子数组”进行插入排序。

  2. 缩小 gap 的值,例如减半,重复第1步。

  3. 不断缩小 gap,直到最后 gap = 1

gap = 1 时,我们做的是什么?就是对整个数组进行一次普通的插入排序。 但这里的“魔法”在于:

经过前面几轮大 gap 的宏观调控,当 gap 最终为1时,这个数组已经几乎是有序的了!而我们知道,对一个几乎有序的数组进行插入排序,效率是极高的。

这个“先宏观调控,再逐步细化,最后精准完成”的思路,就是希尔排序的第一性原理。

下面我们以一个数组为例,详细展示希尔排序的全过程。


手动模拟全过程

假设我们有以下数组: [9, 1, 5, 2, 8, 3, 7, 4, 6, 0]

为了简化演示,我们使用 Hibbard 增量序列:2^k − 1。 假设初始增量为 2^3 − 1=7。

第1轮排序:增量 gap = 4

我们以4为间隔将数组分为4个子序列:

  • 子序列1:[9, 8, 6]

  • 子序列2:[1, 3, 0]

  • 子序列3:[5, 7]

  • 子序列4:[2, 4]

对每个子序列进行插入排序

  • 子序列1:[9, 8, 6] 排序后为 [6, 8, 9]

  • 子序列2:[1, 3, 0] 排序后为 [0, 1, 3]

  • 子序列3:[5, 7] 排序后为 [5, 7]

  • 子序列4:[2, 4] 排序后为 [2, 4]

将排序后的子序列重新放回原数组,得到:

[6, 0, 5, 2, 8, 1, 7, 4, 9, 3]


第2轮排序:增量 gap = 2

现在,我们将增量减小为2。我们将数组分为两个子序列:

  • 子序列1:[6, 5, 8, 7, 9]

  • 子序列2:[0, 2, 1, 4, 3]

对每个子序列进行插入排序:

  • 子序列1:[6, 5, 8, 7, 9] 排序后为 [5, 6, 7, 8, 9]

  • 子序列2:[0, 2, 1, 4, 3] 排序后为 [0, 1, 2, 3, 4]

将排序后的子序列重新放回原数组,得到:

[5, 0, 6, 1, 7, 2, 8, 3, 9, 4]


第3轮排序:增量 gap = 1

最后,我们将增量减小为1。此时,希尔排序就退化为标准的插入排序。我们对整个数组进行排序,因为之前的排序已经让数组变得“大致有序”,所以这一次的插入排序效率会非常高。

对数组 [5, 0, 6, 1, 7, 2, 8, 3, 9, 4] 进行插入排序。

  1. 0 插入到 5 前面:[0, 5, 6, 1, 7, 2, 8, 3, 9, 4]

  2. 1 插入到 5 前面:[0, 1, 5, 6, 7, 2, 8, 3, 9, 4]

  3. 2 插入到 6 前面:[0, 1, 2, 5, 6, 7, 8, 3, 9, 4]

  4. 3 插入到 8 前面:[0, 1, 2, 3, 5, 6, 7, 8, 9, 4]

  5. 4 插入到 9 前面:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

经过这一轮排序,数组最终变为完全有序:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

总结一下,希尔排序通过 “先宏观,后微观” 的方式,先将数组进行大跨度分组排序,使其接近有序状态,然后再逐步减小增量,最后以小增量(1)进行精细排序,从而大大提高了效率。


代码的逐步完善

第一阶段:搭建外层循环框架(控制 gap

希尔排序的“总指挥”是一个控制 gap 变化的循环。最简单的 gap 序列就是 希尔本人提出的 n/2, n/4, ..., 1

#include <iostream>void shellSort(int arr[], int n) {// 这个外层循环控制着步长 gap 的变化// 从 n/2 开始,每次减半,直到 gap 为 1for (int gap = n / 2; gap > 0; gap /= 2) {// 在这里,我们将对当前 gap 值下的所有子数组进行插入排序}
}

第二阶段:实现“分组插入排序”(中间循环)

对于一个固定的 gap,我们如何对所有子数组(例如 {arr[0], arr[4], ...}{arr[1], arr[5], ...})进行排序呢?

我们不需要真的把它们拆分出来。我们可以用一个统一的循环来处理。这个循环从第 gap 个元素开始,一直到数组末尾。arr[i] 就是我们每次要进行插入操作的“新牌”。

void shellSort(int arr[], int n) {for (int gap = n / 2; gap > 0; gap /= 2) {// 这个中间循环,从 gap 开始,遍历整个数组// 它隐式地处理了所有交织在一起的子数组for (int i = gap; i < n; i++) {// 对于 arr[i],我们需要将它插入到它所属的// 那个 gapped 子数组的正确位置// 例如,当 gap=4, i=5 时,// 我们要把 arr[5] 插入到 {arr[1], arr[5], ...} 这个子数组的正确位置}}
}

第三阶段:实现核心的“插入”逻辑(内层循环)

这一步几乎就是标准插入排序的翻版,但有一个关键区别:所有元素的移动和比较,都是以 gap 为步长,而不是以 1 为步长。

void shellSort(int arr[], int n) {for (int gap = n / 2; gap > 0; gap /= 2) {for (int i = gap; i < n; i++) {// 1. 暂存待插入的元素 arr[i]int temp = arr[i];// 2. j 指向 arr[i] 的前一个同组元素int j;// 3. 在当前子数组中,为 temp 寻找插入位置//    比较和移动都是以 gap 为步长for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {// 如果前面的元素更大,就把它向后移动 gap 个位置arr[j] = arr[j - gap];}// 4. 将 temp 插入到找到的正确位置arr[j] = temp;}}
}

将这三层循环组合在一起,我们就得到了一个完整的、逻辑清晰的希尔排序实现。


复杂度与特性分析

希尔排序的分析是所有基础排序算法中最复杂的,因为它和 gap 序列的选择密切相关。

时间复杂度 (Time Complexity)

  • 希尔排序的时间复杂度至今没有一个公认的、精确的数学公式,它取决于所使用的 gap 序列。

  • 最坏情况

    • 使用希尔原始的 n/2 序列,最坏情况时间复杂度是 O(n^2)。在某些特殊构造的数据下,它会退化。

    • 使用更优的 gap 序列(如 Hibbard 序列: 2^k−1 或 Sedgewick 序列),可以获得更好的最坏情况性能,例如 O(n^3/2) 或 O(n^4/3)。

  • 平均情况

    • 平均性能也和 gap 序列有关,但通常远好于 O(n^2)。可以认为是介于 O(nlogn) 和 O(n^2) 之间的一个“亚二次方”级别。

📌关键点:我们无法像归并排序那样给出一个稳定的 O(nlogn) 保证,但它的实际表现通常比 O(n^2) 的插入、选择排序要快得多。


空间复杂度 (Space Complexity)

  • 观察我们的代码,排序过程是在原数组上直接进行的。我们只使用了几个额外的变量(gap, i, j, temp)来辅助操作。

  • 这些变量的数量是固定的,不随输入数组的大小 n 变化。

  • 因此,希尔排序的空间复杂度是:O(1)

  • 它是一个原地排序 (In-place Sort) 算法。


稳定性 (Stability)

  • 希尔排序的核心操作涉及到长距离的元素交换。

  • 例如,在数组 [5a, 5b, 1] 中,gap=2

    • 第一个子数组是 {5a, 1}。为了排序,5a1 需要交换。

    • 数组变为 [1, 5b, 5a]

  • 在这个过程中,5a “跳过”了 5b,跑到了它的后面。两个相等元素 5a5b 的原始相对顺序被破坏了。

  • 因此,希尔排序是不稳定的排序算法 (Unstable Sort)

希尔排序是算法发展史上的一个重要里程碑。它展示了如何通过突破“相邻交换”的思维定势来大幅提升简单排序算法的性能。虽然在绝对速度上它不如快速排序或归并排序,但它的实现相对简单,并且是原地排序,这使它在某些资源受限的场景或作为其他算法的子程序时,仍有其用武之地。

http://www.xdnf.cn/news/19789.html

相关文章:

  • 【51单片机】【protues仿真】基于51单片机呼叫系统
  • 基于Force-closure评估的抓取计算流程
  • 生成知识图谱与技能树的工具指南:PlantUML、Mermaid 和 D3.js
  • 【AI报表】JimuReport 积木报表 v2.1.3 版本发布,免费可视化报表和大屏
  • 【leetcode】222. 完全二叉树的节点个数
  • Altium Designer中的Net-Tie:解决多网络合并与电气隔离的利器
  • CPTS-Vintage 票据,基于资源的约束委派 (RBCD),DPAPI密钥
  • 自制扫地机器人(二) Arduino 机器人避障设计——东方仙盟
  • Veo Videos Generation API 对接说明
  • 鸿蒙NEXT表单选择组件详解:Radio与Checkbox的使用指南
  • 开源 C++ QT Widget 开发(十)IPC进程间通信--共享内存
  • 零跑汽车8月交付57066台,同比增长超88%
  • amd cpu是x86架构吗
  • 【Audio】静音或振动模式下重复来电响铃
  • stdexcept介绍与使用指南
  • 【LeetCode】3670. 没有公共位的整数最大乘积 (SOSDP)
  • Day19_【机器学习—线性回归 (3)—回归模型评估方法】
  • Docker一键快速部署压测工具,高效测试 API 接口性能
  • ES6手录01-let与const
  • 学习日记-spring-day47-9.1
  • PyCharm 2025版本中新建python工程文件自动创建.venv的意义和作用
  • 教育 AI 的下半场:个性化学习路径生成背后,技术如何平衡效率与教育本质?
  • 第二十八天-DAC数模转换实验
  • “便农惠农”智慧社区系统(代码+数据库+LW)
  • 【深度学习基础】深度学习中的早停法:从理论到实践的全面解析
  • OpenCV C++ 入门实战:从基础操作到类封装全解析
  • UART控制器——ZYNQ学习笔记14
  • QT中的HTTP
  • GSM8K 原理全解析:从数学推理基准到大模型对齐的试金石
  • 五、练习2:Git分支操作