希尔排序核心是分组插入,即按gap间隔将数组分为多个子序列,每轮对各子序列独立执行插入排序;gap序列推荐Knuth序列(1,4,13,…),需预生成并确保末轮gap=1;避免在循环中动态计算gap或滥用std::swap。

希尔排序的核心是“分组插入”,不是改插入排序的逻辑
希尔排序本质是把插入排序泛化成多轮、带间隔的版本,重点不在“改进插入排序”,而在用 gap 控制比较距离。很多人写错,是因为硬套插入排序模板,却忘了每轮要对多个子序列分别做插入——不是整个数组扫一遍就完事。
常见错误现象:std::out_of_range 或结果完全无序,往往因为 gap 更新后没重置内层循环起始位置,或子序列遍历时越界。
-
gap初始值必须小于数组长度,推荐用n/2或 Knuth 序列(gap = gap * 3 + 1倒推) - 每轮中,对每个子序列(从
gap开始,步长为gap)单独执行插入逻辑 - 内层比较时,下标是
j - gap,不是j - 1;否则就退化成普通插入排序,失去希尔的意义
Knuth 序列比 n/2 更稳,尤其对小数组或部分有序数据
用 gap = n / 2 看似简单,但容易在某些输入下跳过关键比较,比如数组长度为 10,gap 序列为 5→2→1,中间缺了 3 这一层,可能让相距 3 位的逆序对漏检。Knuth 序列(1, 4, 13, 40…)增长更平缓,实测平均比较次数少 10%–15%。
使用场景:当你要在嵌入式或教学代码里兼顾可读性和稳定性时,Knuth 比 Sedgewick 或 Hibbard 更易手写且不易出错。
立即学习“C++免费学习笔记(深入)”;
- 生成方式:先让
gap = 1,循环gap = gap * 3 + 1直到超过n,再倒序取值 - 注意边界:最后一轮
gap必须为 1,否则排序不完整 - C++ 中建议用
vector<int></int>存 gap 序列,避免整数溢出(尤其n > 1e6)
别在 for 循环里反复算 gap,性能差还易错
有人写成 for (int gap = n/2; gap > 0; gap /= 2),看似简洁,但每次迭代都要除法,而且 gap 序列不可控。更糟的是,如果某次 gap 变成 0(比如 n=1),循环直接崩。
性能影响:现代 CPU 除法延迟高,gap /= 2 在小数组上差异不明显,但一旦进循环体上百次,分支预测失败+除法瓶颈就拖慢整体 5%–8%。
- 预生成 gap 序列,用索引控制轮次,比如
for (size_t k = gaps.size(); k > 0; --k) - 用
gaps[k-1]当前 gap,避免重复计算和整数截断风险 - 如果确定只用 n/2 序列,至少写成
gap >>= 1,位移比除法快,也明确表达意图
swap 不如直接赋值,C++ 里别迷信 std::swap
很多示例用 std::swap(arr[j], arr[j-gap]),看起来干净,但实际多一次拷贝——尤其 arr 是 std::vector<:string></:string> 或自定义类时,构造+析构开销明显。希尔排序本身移动频繁,这里省一点是一点。
容易踩的坑:用 std::swap 后忘记更新 j 的值,导致后续比较还是旧元素;或者 swap 后没继续往前比(该做的插入逻辑断了)。
- 推荐写法:
T tmp = arr[j]; arr[j] = arr[j - gap]; j -= gap; arr[j] = tmp; - 如果类型支持移动语义(C++11+),确保
T有移动构造,否则仍走拷贝 - 对 POD 类型(
int、double),编译器通常能优化掉临时变量,不用过度担心
事情说清了就结束。最常被忽略的是:每轮子序列的起始点不是固定 0,而是从 gap 到 n-1,且每个起始点要独立跑完一次插入过程——这一步漏了,希尔排序就只剩个壳。









