3.4 希尔排序:插入排序的进化

历史背景

希尔排序由 Donald Shell 于 1959 年发明,是第一个突破 O(n²) 时间复杂度的基于比较的排序算法。Shell 在他 1959 年的论文中提出了"增量递减"的思想,即先用较大的间隔进行粗排序,再逐渐缩小间隔进行精细排序。希尔排序是最早证明在某些情况下可以打破 O(n²) 壁垒的排序算法之一,对后续排序算法的发展产生了深远影响。

算法原理

希尔排序是插入排序的推广。基本思想是:先将整个待排序序列分割成若干个子序列,分别进行插入排序;然后逐渐减小子序列之间的间隔,最终当间隔为 1 时,整个序列已经基本有序,此时进行一次普通的插入排序即可完成排序。

间隔序列的选择对希尔排序的性能影响很大。常见的间隔序列有:

  • Shell 原始序列:n/2, n/4, ..., 1
  • Hibbard 序列:1, 3, 7, ..., 2ᵏ-1
  • Sedgewick 序列:1, 5, 19, 41, 109, ...

Java 实现

java
复制代码
public class ShellSort {
    
    /**
     * 希尔排序(使用 Shell 原始间隔序列)
     */
    public static void sort(int[] arr) {
        int n = arr.length;
        
        // 初始间隔为 n/2,每次减半
        for (int gap = n / 2; gap > 0; gap /= 2) {
            // 对每个子序列进行插入排序
            for (int i = gap; i < n; i++) {
                int key = arr[i];
                int j = i;
                
                // 插入排序,间隔为 gap
                while (j >= gap && arr[j - gap] > key) {
                    arr[j] = arr[j - gap];
                    j -= gap;
                }
                arr[j] = key;
            }
        }
    }
    
    /**
     * 希尔排序(使用 Hibbard 间隔序列)
     * Hibbard 序列:1, 3, 7, 15, 31, ...
     * 最坏情况时间复杂度为 O(n^(3/2))
     */
    public static void sortHibbard(int[] arr) {
        int n = arr.length;
        
        // 计算最大的 Hibbard 间隔
        int gap = 1;
        while (gap < n / 3) {
            gap = gap * 3 + 1;  // 1, 4, 13, 40, 121, ...
        }
        
        while (gap > 0) {
            for (int i = gap; i < n; i++) {
                int key = arr[i];
                int j = i;
                while (j >= gap && arr[j - gap] > key) {
                    arr[j] = arr[j - gap];
                    j -= gap;
                }
                arr[j] = key;
            }
            gap = (gap - 1) / 3;
        }
    }
    
    /**
     * 希尔排序(使用 Sedgewick 间隔序列)
     * Sedgewick 序列:1, 5, 19, 41, 109, 209, 505, 929, ...
     * 实际性能通常优于 Hibbard 序列
     */
    public static void sortSedgewick(int[] arr) {
        int n = arr.length;
        
        // Sedgewick 间隔序列
        int[] gaps = {1, 5, 19, 41, 109, 209, 505, 929, 2161, 3905, 
                      8929, 16001, 36289, 64769, 146305, 260609};
        
        // 找到最大的可用间隔
        int gapIndex = gaps.length - 1;
        while (gapIndex >= 0 && gaps[gapIndex] >= n) {
            gapIndex--;
        }
        
        while (gapIndex >= 0) {
            int gap = gaps[gapIndex];
            for (int i = gap; i < n; i++) {
                int key = arr[i];
                int j = i;
                while (j >= gap && arr[j - gap] > key) {
                    arr[j] = arr[j - gap];
                    j -= gap;
                }
                arr[j] = key;
            }
            gapIndex--;
        }
    }
    
    /**
     * 希尔排序(泛型版本)
     */
    public static <T extends Comparable<T>> void sortGeneric(T[] arr) {
        int n = arr.length;
        for (int gap = n / 2; gap > 0; gap /= 2) {
            for (int i = gap; i < n; i++) {
                T key = arr[i];
                int j = i;
                while (j >= gap && arr[j - gap].compareTo(key) > 0) {
                    arr[j] = arr[j - gap];
                    j -= gap;
                }
                arr[j] = key;
            }
        }
    }
}

效率分析

间隔序列 最好情况 最坏情况 平均情况
Shell 原始 (n/2ᵏ) O(n log n) O(n²) O(n^(3/2))
Hibbard (2ᵏ-1) O(n log n) O(n^(3/2)) O(n^(5/4))
Sedgewick O(n log n) O(n^(4/3)) ≈ O(n^(7/6))

希尔排序是不稳定的排序算法(因为间隔分组导致相等元素可能被交换到不同位置),但它是原地排序。希尔排序的实际性能取决于间隔序列的选择,好的间隔序列可以使其在中等规模数据上接近 O(n log n) 的性能。

应用场景

  • 中等规模数据排序
  • 嵌入式系统(代码简单且不需要额外空间)
  • 数据基本有序但不确定的场景
  • 作为快速排序的预处理步骤
  • 实时系统(因为最坏情况可预测且不需要递归)