Appearance
希尔排序
插入排序的增量优化
直接插入排序在基本有序时很快,希尔排序就利用了这一点:先按大增量分组排序让序列"基本有序",再逐步缩小增量,最后增量为 1 时做一次插入排序收尾。这样整体效率突破了 O(n^2) 的瓶颈。
408 考研中,希尔排序是插入排序的重要改进,也是唯一一个时间复杂度与增量序列相关的排序算法。
核心思想
希尔排序的核心特点:
- 缩小增量:选定一个增量序列 d₁ > d₂ > … > dₖ = 1,每趟按当前增量 dᵢ 将序列划分为若干子序列
- 分组插入:对每个子序列执行直接插入排序,使元素能跨越较大距离移动到接近最终位置
- 逐步逼近有序:增量不断缩小,子序列越来越长、序列越来越有序,最后一趟 d=1 等价于对"几乎有序"的序列做直接插入排序
工作原理示意(以 d=4, 2, 1 为例):
原始序列: [8, 5, 2, 9, 3, 7, 1, 6]
d=4: 分组 {8,3} {5,7} {2,1} {9,6} → 各组插入排序
[3, 5, 1, 6, 8, 7, 2, 9]
d=2: 分组 {3,1,8,2} {5,6,7,9} → 各组插入排序
[1, 5, 2, 6, 3, 7, 8, 9]
d=1: 整体插入排序(此时序列已基本有序,移动次数很少)
[1, 2, 3, 5, 6, 7, 8, 9]交互可视化
通过下方的交互动画,你可以逐步观察希尔排序的执行过程:
操作详解
算法思路
- 选择一个增量序列,初始增量 d = n/2(或其他方案)
- 按增量 d 将序列分为 d 组:下标为 {0, d, 2d, …}、{1, 1+d, 1+2d, …} 等
- 对每组执行直接插入排序
- 缩小增量 d = d/2,重复步骤 2-3
- 直到 d = 1,完成最后一趟排序
c
void ShellSort(int A[], int n) {
// d 为增量,每次减半
for (int d = n / 2; d >= 1; d /= 2) {
// 从第 d 个元素开始,对每个子序列做插入排序
for (int i = d; i < n; i++) {
int temp = A[i];
int j = i - d;
// 在当前子序列中找到插入位置
while (j >= 0 && A[j] > temp) {
A[j + d] = A[j];
j -= d;
}
A[j + d] = temp;
}
}
}注意:代码中并没有显式地"分组再分别排序",而是用一个循环交替处理各组元素,效果等价但写法更简洁。
增量序列
增量序列的选择直接影响希尔排序的时间复杂度,408 常见的有以下几种:
| 增量序列 | 公式 | 示例(n=16) | 最坏时间复杂度 |
|---|---|---|---|
| Shell 原始序列 | dᵢ = n/2ⁱ | 8, 4, 2, 1 | O(n²) |
| Hibbard 序列 | dᵢ = 2ⁱ - 1 | 15, 7, 3, 1 | O(n^1.5) |
| Knuth 序列 | dᵢ = (3ⁱ - 1)/2 | 13, 4, 1 | O(n^1.5) |
⭐ 考试默认使用 Shell 原始序列(d = n/2, n/4, …, 1),除非题目另有说明。
⭐ 增量序列必须满足的条件:最后一个增量必须为 1(否则无法保证排序完成)。
排序过程详解
以序列 [49, 38, 65, 97, 76, 13, 27, 49*](n=8)为例,使用 Shell 原始增量序列。其中 49* 用于标注第二个 49 以观察稳定性。
第一趟 d=4:分为 4 组
组1: A[0]=49, A[4]=76 → 49, 76(无交换)
组2: A[1]=38, A[5]=13 → 13, 38
组3: A[2]=65, A[6]=27 → 27, 65
组4: A[3]=97, A[7]=49* → 49*, 97结果:[49, 13, 27, 49*, 76, 38, 65, 97]
第二趟 d=2:分为 2 组
组1: A[0]=49, A[2]=27, A[4]=76, A[6]=65
→ 插入排序后: 27, 49, 65, 76
组2: A[1]=13, A[3]=49*, A[5]=38, A[7]=97
→ 插入排序后: 13, 38, 49*, 97结果:[27, 13, 49, 38, 65, 49*, 76, 97]
第三趟 d=1:整体插入排序
结果:[13, 27, 38, 49, 49*, 65, 76, 97]
⭐ 观察两个 49 的最终位置:原始序列中 49 在 49* 前面,排序后 49 仍在 49* 前面——看似稳定,但这只是巧合。在 d=4 那趟中,49* 被移动到了 49 前面(下标 3 < 下标 0 的后续位置),后续又被调回。不同增量序列或不同数据下,相同元素的相对顺序可能被打乱。 因此希尔排序是不稳定的排序算法。
复杂度分析
| 指标 | 结果 | 说明 |
|---|---|---|
| 最好时间复杂度 | O(n^1.3) | 经验值,取决于增量序列 |
| 最坏时间复杂度 | O(n²) | 使用 Shell 原始增量序列时 |
| 平均时间复杂度 | 约 O(n^1.3) | 数学上未完全解决,考试记此值 |
| 空间复杂度 | O(1) | 仅需常数级辅助变量 |
| 稳定性 | 不稳定 | 跨子序列移动可能改变相同元素的相对顺序 |
| 适用性 | 仅适用于顺序表 | 需要按下标随机访问,链表无法使用 |
为什么不稳定? 相同关键字的元素可能被分到不同子序列中,各子序列独立排序时,无法感知其他子序列中相同元素的位置,从而可能打乱它们的相对顺序。
易错:希尔排序的时间复杂度取决于增量序列的选择,没有统一的精确公式。408 一般不要求精确分析,但要记住:希尔排序是不稳定排序;最后一趟增量必须为 1;最坏情况优于 O(n^2)。
易错:希尔排序不稳定的反例:相同的元素可能被分到不同的组中,各组独立排序后相对顺序可能改变。
与直接插入排序的对比:
| 对比项 | 直接插入排序 | 希尔排序 |
|---|---|---|
| 时间复杂度 | O(n²) | 约 O(n^1.3) |
| 稳定性 | 稳定 | 不稳定 |
| 适用场景 | 基本有序或 n 较小 | 中等规模数据 |
| 核心区别 | 每次移动一个位置 | 元素可跨越多个位置移动 |
考研高频考点
- ⭐ 给定序列和增量,写出每趟排序的结果(大题/选择题高频)
- ⭐ 希尔排序的稳定性判断及原因说明(选择题/简答题必考)
- ⭐ 增量序列的最后一个值必须为 1(判断题)
- ⭐ 时间复杂度与增量序列的关系:Shell 原始序列最坏 O(n²),考试中平均记 O(n^1.3)
- ⭐ 希尔排序 vs 直接插入排序的联系与区别(简答题)
- 希尔排序不能用于链表(需要随机访问)
- 空间复杂度 O(1),属于原地排序
相关知识
- 直接插入排序——希尔排序是插入排序的推广,理解插入排序是前提
- 排序算法对比——各排序算法的时间/空间/稳定性横向对比
- 希尔排序是唯一一个时间复杂度与增量序列相关的排序算法
- 希尔排序只能用于顺序表,链表无法按下标随机访问