每日算法 — 使用java实现希尔排序:增量序列设计与多轮插入优化

引言

希尔排序(Shell Sort)是插入排序的一种高效改进版本,由计算机科学家 Donald Shell 于 1959 年提出。它通过引入”增量序列”的概念,让相距一定间隔的元素先进行局部有序化,逐步缩小间隔直至为 1,最终完成全局排序。这种策略使得希尔排序在处理大规模数据时,时间复杂度远优于普通的 O(n²) 插入排序,最优可达 O(n log n)。本文将用 Java 完整实现希尔排序,并深入分析不同增量序列对性能的影响。

希尔排序的核心思想

普通插入排序的问题在于:当最小的元素位于数组末尾时,需要经过 n-1 次移动才能到达正确位置。希尔排序通过允许交换相距较远的元素,让小的元素可以”大步前进”,从而在前期快速改善数组的有序程度。

希尔排序的执行过程可以概括为:
– 选择一个增量序列 gap₁, gap₂, …, gapₖ,其中 gapₖ = 1
– 对于每个增量 gap,将数组分成 gap 个子序列,每个子序列由间隔为 gap 的元素组成
– 对每个子序列执行插入排序
– 当增量缩小到 1 时,数组已经”基本有序”,此时普通插入排序只需少量移动即可完成

经典增量序列

1. Shell 原始序列(gap = n/2, n/4, …)

最直观的增量序列,每次将增量减半。

2. Hibbard 序列(gap = 2^k – 1)

增量为 1, 3, 7, 15, 31, …,时间复杂度约为 O(n^1.5)。

3. Sedgewick 序列

结合了 9×4^k – 9×2^k + 1 和 4^k – 3×2^k + 1 的混合序列,时间复杂度约为 O(n^1.3)。

Java 实现

基础希尔排序(Shell 原始序列)

/**
 * 希尔排序基础实现
 * 使用 Shell 原始增量序列:n/2, n/4, n/8, ..., 1
 * 时间复杂度:最坏 O(n²),平均约为 O(n^1.3) ~ O(n^2)
 * 空间复杂度:O(1)
 * 稳定性:不稳定排序
 */
public class ShellSort {

    /**
     * 希尔排序主方法
     * @param arr 待排序的整型数组
     */
    public static void shellSort(int[] arr) {
        int n = arr.length;

        // 初始增量为数组长度的一半,逐步折半
        for (int gap = n / 2; gap > 0; gap /= 2) {
            // 对每个子序列执行插入排序
            // 从第 gap 个元素开始,逐个向所属子序列中插入
            for (int i = gap; i < n; i++) {
                int temp = arr[i];
                int j = i;

                // 在子序列中寻找插入位置
                // arr[j - gap] > temp 确保找到比 temp 小的元素为止
                while (j >= gap && arr[j - gap] > temp) {
                    arr[j] = arr[j - gap];  // 元素后移
                    j -= gap;               // 在同一子序列中向前比较
                }

                arr[j] = temp;  // 将 temp 放入正确位置
            }
        }
    }

    public static void main(String[] args) {
        int[] arr = {64, 34, 25, 12, 22, 11, 90, 5, 77, 30};
        System.out.println("排序前: " + java.util.Arrays.toString(arr));
        shellSort(arr);
        System.out.println("排序后: " + java.util.Arrays.toString(arr));
    }
}

Hibbard 增量序列实现

/**
 * 使用 Hibbard 增量序列的希尔排序
 * Hibbard 序列:1, 3, 7, 15, 31, ... (2^k - 1)
 * 理论时间复杂度约为 O(n^1.5)
 */
public static void shellSortHibbard(int[] arr) {
    int n = arr.length;

    // 预计算不超过 n 的最大 Hibbard 增量
    // 找到最大的 k,使得 2^k - 1 <= n
    int k = 1;
    while ((1 << (k + 1)) - 1 <= n) {
        k++;
    }

    // 从大到小遍历 Hibbard 增量
    for (; k >= 1; k--) {
        int gap = (1 << k) - 1;  // 2^k - 1

        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;
        }
    }
}

Sedgewick 增量序列实现

/**
 * 使用 Sedgewick 增量序列的希尔排序
 * 该序列混合了 9*4^k - 9*2^k + 1 和 4^k - 3*2^k + 1
 * 是目前已知表现最好的增量序列之一,时间复杂度约 O(n^1.3)
 */
public static void shellSortSedgewick(int[] arr) {
    int n = arr.length;

    // 预计算 Sedgewick 增量序列
    java.util.ArrayList<Integer> gaps = new java.util.ArrayList<>();
    int k = 0;
    int gap;
    while (true) {
        if (k % 2 == 0) {
            gap = 9 * (1 << (2 * k)) - 9 * (1 << k) + 1;
        } else {
            gap = (1 << (2 * k + 2)) - 3 * (1 << (k + 1)) + 1;
        }
        if (gap > n) break;
        gaps.add(gap);
        k++;
    }

    // 从大到小应用增量
    for (int idx = gaps.size() - 1; idx >= 0; idx--) {
        int g = gaps.get(idx);
        for (int i = g; i < n; i++) {
            int temp = arr[i];
            int j = i;
            while (j >= g && arr[j - g] > temp) {
                arr[j] = arr[j - g];
                j -= g;
            }
            arr[j] = temp;
        }
    }
}

排序过程可视化

以数组 [64, 34, 25, 12, 22, 11, 90, 5] 为例,使用 Shell 原始序列(gap = 4, 2, 1):

gap = 4 时:
– 子序列 1(索引 0, 4):[64, 22] → 排序后 [22, 64]
– 子序列 2(索引 1, 5):[34, 11] → 排序后 [11, 34]
– 子序列 3(索引 2, 6):[25, 90] → 已有序
– 子序列 4(索引 3, 7):[12, 5] → 排序后 [5, 12]
– 数组变为:[22, 11, 25, 5, 64, 34, 90, 12]

gap = 2 时:
– 子序列 1(索引 0, 2, 4, 6):[22, 25, 64, 90] → 已有序
– 子序列 2(索引 1, 3, 5, 7):[11, 5, 34, 12] → 排序后 [5, 11, 12, 34]
– 数组变为:[22, 5, 25, 11, 64, 12, 90, 34]

gap = 1 时(普通插入排序):
– 数组已基本有序,只需少量移动即可得到最终结果

不同增量序列性能对比

增量序列 最坏时间复杂度 平均时间复杂度 额外空间
Shell (n/2^k) O(n²) O(n log n) ~ O(n²) O(1)
Hibbard (2^k-1) O(n^1.5) O(n^1.5) O(1)
Sedgewick O(n^1.3) O(n^1.3) O(1)
Knuth (3k+1) O(n^1.5) O(n^1.5) O(1)

完整测试与性能评测

import java.util.Arrays;
import java.util.Random;

public class ShellSortBenchmark {

    public static void main(String[] args) {
        int[] sizes = {1000, 10000, 100000};
        Random rand = new Random(42);

        for (int size : sizes) {
            int[] arr1 = new int[size];
            int[] arr2 = new int[size];
            int[] arr3 = new int[size];

            for (int i = 0; i < size; i++) {
                int val = rand.nextInt(size * 10);
                arr1[i] = val;
                arr2[i] = val;
                arr3[i] = val;
            }

            System.out.println("\n数组大小: " + size);

            long t1 = System.currentTimeMillis();
            ShellSort.shellSort(arr1);
            System.out.println("Shell 原始序列: " + (System.currentTimeMillis() - t1) + "ms");

            long t2 = System.currentTimeMillis();
            ShellSort.shellSortHibbard(arr2);
            System.out.println("Hibbard 序列: " + (System.currentTimeMillis() - t2) + "ms");

            long t3 = System.currentTimeMillis();
            ShellSort.shellSortSedgewick(arr3);
            System.out.println("Sedgewick 序列: " + (System.currentTimeMillis() - t3) + "ms");
        }
    }
}

总结

希尔排序是对插入排序的精妙改进,通过增量序列让元素能够”大步前进”,有效解决了普通插入排序中逆序对过多导致的效率问题。虽然希尔排序不是稳定排序,且最坏时间复杂度取决于增量序列的选择,但其实现简单、原地排序、无需递归栈空间的特点,使其在嵌入式系统和内存受限环境中仍有广泛应用。Sedgewick 增量序列是实际应用中表现最优的选择之一,值得在工程实践中优先考虑。