【C++】神秘-希尔排序

发布时间:2026/8/1 9:58:30

【C++】神秘-希尔排序 【C】神秘-希尔排序你是否曾经在面试中被问到“除了冒泡排序、快速排序你还知道哪些排序算法”然后大脑突然一片空白或者在处理一些中等规模数据时发现 O(n²) 的简单排序太慢而 O(n log n) 的复杂排序又难以手写今天我要带你认识一个“既简单又神秘”的排序算法——希尔排序Shell Sort。它像是插入排序的“进化版”但又藏着一些不为人知的细节。本文将用最通俗的语言配合可运行的 C 代码彻底揭开它的面纱。### 希尔排序是什么—— 从“插入排序”的痛点说起想象一下你手里有一摞乱序的扑克牌你通常会用“插入排序”来整理每次取一张牌插到前面已排序序列的合适位置。但插入排序有个致命弱点如果最小的牌在最后面它要一步一步地挪到最前面效率极低。比如序列[9, 8, 7, 6, 5, 1]数字 1 需要和前面 5 个元素比较并移动时间复杂度接近 O(n²)。希尔排序的“神秘”之处在于它先让数据“宏观有序”再“微观调整”。具体做法是——将相隔一定“增量gap”的元素组成一个子序列分别进行插入排序。然后逐步缩小增量直到增量为 1此时整个序列基本有序再做一次标准插入排序就能高效完成。举个直观例子假设有数组[9, 8, 7, 6, 5, 4, 3, 2, 1]初始增量设为 4那么下标 0,4,8 是一组1,5 是一组2,6 一组3,7 一组。对每组分别排序后数组会变成[1, 2, 3, 4, 5, 6, 7, 8, 9]不不会那么快但你会发现较小的元素很快就跳到了前面这就是希尔排序高效的关键。### 代码示例 1基础版希尔排序C 实现下面是一个最简单、最经典的希尔排序实现增量序列采用gap n/2并且每次减半也叫“希尔德增量”。虽然它不是最优的增量序列但足以说明原理。cpp#include iostream#include vectorusing namespace std;// 希尔排序函数void shellSort(vectorint arr) { int n arr.size(); // 外层循环控制增量每次减半直到增量为 1 for (int gap n / 2; gap 0; gap / 2) { // 内层循环对每个子序列执行插入排序 for (int i gap; i n; i) { int temp arr[i]; // 保存当前元素 int j i; // 在同一子序列中向前比较并移动元素 while (j gap arr[j - gap] temp) { arr[j] arr[j - gap]; j - gap; } arr[j] temp; // 插入到正确位置 } }}int main() { vectorint data {9, 8, 7, 6, 5, 4, 3, 2, 1}; cout 排序前: ; for (int x : data) cout x ; cout endl; shellSort(data); cout 排序后: ; for (int x : data) cout x ; cout endl; return 0;}运行结果排序前: 9 8 7 6 5 4 3 2 1 排序后: 1 2 3 4 5 6 7 8 9代码解读-gap是“间隔”一开始为n/2每次循环后除以 2直到 0。- 内层的for循环从gap开始对每个元素在其所在的“子序列”相隔 gap 的元素中做插入排序。- 关键点arr[j - gap]是当前元素在子序列中的前一个元素通过 while 循环找到合适插入位置。- 这种“跨步”移动让数据快速接近有序。### 为什么希尔排序“神秘”—— 谈谈增量序列的玄学如果你觉得上面的代码太简单那你就低估了希尔排序的深度。它的时间复杂度并不固定而是取决于你选择的增量序列。最坏情况下使用n/2减半的增量时间复杂度是 O(n²)和插入排序一样差。但如果你选择一个“神秘”的增量序列性能会大幅提升。比如Hibbard 增量序列1, 3, 7, 15, ...时间复杂度可达到 O(n^(3/2))Sedgewick 增量序列1, 5, 19, 41, ...甚至可以达到 O(n^(4/3))。这背后的数学证明非常复杂所以我说它“神秘”——简单代码背后藏着深奥的复杂度分析。那增量序列的选择有什么规律吗目前没有绝对最优解但有一个经验法则增量应尽量互质这样每一轮排序时不同子序列的元素能交叉混合避免重复比较。### 代码示例 2改进版希尔排序使用 Hibbard 增量下面我们用 Hibbard 增量序列2^k - 1来升级代码你会发现性能在数据量较大时明显优于基础版。cpp#include iostream#include vectorusing namespace std;// 生成 Hibbard 增量序列并保存到 vectorvectorint getHibbardGaps(int n) { vectorint gaps; int gap 1; while (gap n) { gaps.push_back(gap); gap gap * 2 1; // 即 2^k - 1 序列 } // 由于我们想从大到小使用增量所以反转 reverse(gaps.begin(), gaps.end()); return gaps;}// 希尔排序使用 Hibbard 增量void shellSortHibbard(vectorint arr) { int n arr.size(); vectorint gaps getHibbardGaps(n); for (int gap : gaps) { for (int i gap; i n; i) { int temp arr[i]; int j i; while (j gap arr[j - gap] temp) { arr[j] arr[j - gap]; j - gap; } arr[j] temp; } }}int main() { vectorint data {12, 34, 54, 2, 3, 7, 19, 45, 67, 89, 1}; cout 排序前: ; for (int x : data) cout x ; cout endl; shellSortHibbard(data); cout 排序后: ; for (int x : data) cout x ; cout endl; return 0;}运行结果排序前: 12 34 54 2 3 7 19 45 67 89 1 排序后: 1 2 3 7 12 19 34 45 54 67 89代码解读-getHibbardGaps函数生成增量序列例如n11时序列为[7, 3, 1]。- 从大到小使用增量保证最后一轮gap1彻底排序。- 注意这里我们用了reverse函数因为生成的是从小到大而我们需要从大到小。### 希尔排序 vs 其他排序——何时选择它你可能想问既然有快速排序、归并排序为什么还要学希尔排序原因有三1.实现简单代码量少且不需要递归或额外数组适合嵌入式或内存受限场景。2.对中等规模数据几百到几千性能优秀比 O(n²) 算法快且常数因子比快速排序小。3.不稳定但可预测对于部分有序数据表现极佳。但要注意希尔排序不稳定相同元素的相对顺序可能改变而且对于超大规模数据百万级不如快速排序。所以它像是“性价比之王”但不是“全能冠军”。### 总结希尔排序就像一个“神秘的魔术师”——它用简单的“分组插入”技巧突破了 O(n²) 的壁垒。虽然它不如快速排序那样名声显赫但在特定场景下却非常实用。通过本文你学会了两种实现基础版gap 减半和进阶版Hibbard 增量。下次面试时如果你能侃侃而谈增量序列对复杂度的影响一定能让人刮目相看。记住关键点- 核心思想先宏观分组排序再微观整体排序。- 时间复杂度取决于增量序列从 O(n²) 到 O(n^(3/2)) 不等。- 稳定性不稳定。- 适用场景中等规模数据、内存受限系统。现在不妨自己动手修改增量序列看看性能差异吧排序算法的世界永远比你想象的更神秘。

相关新闻