希尔排序是对直接插入排序的一种高效改进,它通过引入“增量”的概念,对元素进行分组和预处理,巧妙地提升了排序效率。
下面我将通过一个例子、一个表格、一段代码,帮你彻底掌握它。
📃 核心思想与执行步骤
希尔排序的核心思想是:先将整个待排序的序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
具体步骤如下:
- 选择增量:首先确定一个初始增量(
gap),通常为数组长度的一半。 - 分组排序:将序列中间隔为
gap的元素分成一组,对每一组进行直接插入排序。 - 缩小增量:减小
gap的值(例如,gap = gap / 2),重复第2步,直到gap为 1。 - 最终排序:当
gap为 1 时,整个序列作为一组进行最后一次直接插入排序,此时由于序列已经基本有序,这次排序会非常高效。
🧮 增量序列的选择
增量的选择方案对希尔排序的性能至关重要。下表对比了两种常见的增量序列取法:
| 增量序列类型 | 计算方式 | 特点说明 |
|---|---|---|
| Shell 原始序列 | 初始增量:gap = n/2 后续增量:gap = gap / 2 | 实现简单,易于理解,但效率并非最优。 |
| Knuth 序列 | 初始增量:h = 1 后续计算:h = 3*h + 1,直到 h > n/3,然后增量反向计算:gap = h / 3 | 通常能获得更好的排序效率,是实践中常用的高效序列之一。 |
💻 Java 代码实现
以下是使用 Knuth 序列实现的希尔排序(升序)示例代码,关键步骤配有详细注释:
public class ShellSort {
public static void shellSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
int n = arr.length;
// 1. 计算初始增量(Knuth序列)
int h = 1;
while (h <= n / 3) {
h = 3 * h + 1; // 1, 4, 13, 40, 121, ...
}
// 2. 开始排序,增量逐步缩小
while (h >= 1) {
// 3. 对每个子序列进行插入排序,从第h个元素开始
for (int i = h; i < n; i++) {
int temp = arr[i]; // 当前待插入元素
int j = i;
// 4. 在子序列中寻找插入位置
while (j >= h && arr[j - h] > temp) {
arr[j] = arr[j - h]; // 元素后移
j -= h;
}
arr[j] = temp; // 插入到正确位置
}
// 5. 缩小增量
h = h / 3;
}
}
public static void main(String[] args) {
int[] arr = {9, 1, 5, 8, 3, 4, 6, 10, 2, 12};
System.out.println("排序前: " + java.util.Arrays.toString(arr));
shellSort(arr);
System.out.println("排序后: " + java.util.Arrays.toString(arr));
}
}
⚙ 算法特性分析
- 时间复杂度:希尔排序的时间复杂度是增量序列的函数,并非一个定值。使用 Knuth 序列时,平均时间复杂度约为 O(n^1.5) ,优于普通插入排序的 O(n²)。最好情况下可接近 O(n log n),最坏情况下为 O(n²)。
- 空间复杂度:排序过程仅需常数个额外空间(如变量
temp,h等),因此空间复杂度为 O(1) 。 - 稳定性:由于相等元素可能在不同的增量分组排序过程中被移动,导致相对顺序改变,因此希尔排序是不稳定的排序算法。
💎 总结与适用场景
希尔排序是对直接插入排序的重要改进,它通过前期的大步长调整使元素大幅跨距离移动,快速趋于有序,减少了后期小步长排序的工作量。
它特别适用于中等规模数据的排序,在数据量不是特别巨大时,其性能表现往往优于更复杂的排序算法。同时,由于其代码量适中且是原地排序(空间复杂度 O(1)),在一些内存空间有限的嵌入式系统或特定场景下也很有价值。