希尔排序的核心是分组插入,关键在间隔序列选择和组内有序性传递;需用gap步长跳跃比较交换,而非简单套用插入排序;knuth等优化序列可避免o(n²)退化,且算法不稳定。

希尔排序的核心是分组插入,不是“改进插入排序”的字面理解
很多人误以为希尔排序只是把 insertion_sort 套个外层循环就完事了,其实关键在「间隔序列」的选择和「组内有序性」的传递。直接套用普通插入排序逻辑会导致每组内部排完后,跨组无序性被忽略,最终结果错误。
正确做法是:对每个间隔 gap,以该步长为单位,对所有偏移量为 0,1,...,gap-1 的子序列分别做插入排序——但不是独立排序,而是用同一轮 for 循环驱动,靠内层 while 向前跳跃 gap 位置比较交换。
- 常见错误:用
for (int i = gap; i 然后只对 <code>i所在子序列做一次插入,漏掉其他起始位置(如i=0,1,...,gap-1) - 正确结构:外层控制
gap缩减,中层i从gap到n-1,内层j从i开始,每次减gap,直到j 或 <code>arr[j] >= arr[j-gap] - 性能影响:
gap序列决定最坏时间复杂度。用gap = gap / 2(Knuth 序列更优)比暴力除 2 更稳定;避免使用gap = 1作为唯一终态前的最后一步,否则退化成纯插入排序
如何选 gap 序列?别用简单的 n/2, n/4, …
原始 Shell 提出的 gap = n/2, n/4, ..., 1 在某些数据下会退化到 O(n²),比如已按奇偶分块有序的数组。现代实践中推荐 Knuth 序列:gap = 3 * gap + 1 生成后倒序使用,或 Sedgewick 序列(4^k + 3×2^(k−1) + 1),它们能更好打破局部有序性。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 预生成 gap 序列时,先从 1 开始正向生成直到 >
n,再逆序遍历(确保首项 ≤n-1) - 避免在循环中实时计算
gap /= 2,容易因整数截断过早变成 0(如gap=1时1/2==0),导致死循环或越界 - c++ 中可用
std::vector<int></int>存 gap,或用 while 预算最大 gap 再递减,比递归生成更可控
原地排序下,swap 操作必须支持 gap 步长移动
标准插入排序中,swap(arr[j], arr[j-1]) 是相邻交换;希尔排序里必须改成 swap(arr[j], arr[j-gap]),且 j 的更新也要是 j -= gap。漏掉这个细节,算法就变成多个独立插入排序,无法实现“远距离元素提前靠近”的效果。
示例片段(关键部分):
for (int gap = max_gap; 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,不是 j-1 j -= gap; // 步长是 gap,不是 1 } arr[j] = temp; } }
- 注意:
arr[j] = temp不在 while 内,避免覆盖未比较位置 - 比较条件用
arr[j - gap] > temp而非arr[j] > arr[j-gap],这是标准插入式写法,减少重复读取 - 如果元素类型不支持
operator>(如自定义结构体),需传入比较函数对象,且要确保它适用于任意间隔下的元素对
稳定性?别指望希尔排序是稳定的
希尔排序本质不稳定——相同值的元素可能因不同 gap 轮次被跨距交换,相对位置不可逆。例如数组 [5a, 2, 5b, 1](a/b 表示相同值但不同实例),在 gap=2 时 5a 和 5b 不比较,但在 gap=1 时 5b 可能被移到 5a 前面。
如果你需要稳定排序且又想利用分组思想,应改用归并或带索引的计数排序,而不是强行改造希尔排序。
实际工程中,除非明确要求原地、平均 O(n^1.3) 且数据规模中等(几万以内)、对稳定性无感,否则直接用 std::sort 更稳妥——它底层是 introsort,兼顾最坏性能与常数因子。