3.2 快速排序:二十世纪最伟大的算法之一

历史背景

快速排序由英国计算机科学家 Tony Hoare 于 1959 年在莫斯科国立大学访问期间发明,并于 1961 年正式发表。Hoare 当时正在研究机器翻译问题,需要一种高效的排序算法来处理词典。快速排序因其卓越的平均性能,被《Computing in Science & Engineering》期刊评为 20 世纪十大算法之一。Java 的 Arrays.sort() 对基本类型数组使用的就是双轴快速排序(Dual-Pivot Quicksort)。

算法原理

快速排序采用分治策略:

  1. 选择基准:从数组中选择一个元素作为基准(pivot)
  2. 分区:重新排列数组,使所有小于基准的元素位于基准左侧,所有大于基准的元素位于基准右侧
  3. 递归:对左右两个子数组递归执行上述步骤

分区操作是快速排序的核心。常见的分区方案有 Lomuto 分区和 Hoare 分区。

Java 实现

java
复制代码
public class QuickSort {
    
    /**
     * 快速排序(入口方法)
     */
    public static void sort(int[] arr) {
        quickSort(arr, 0, arr.length - 1);
    }
    
    /**
     * 快速排序递归实现(Lomuto 分区方案)
     */
    private static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            int pivotIndex = lomutoPartition(arr, low, high);
            quickSort(arr, low, pivotIndex - 1);
            quickSort(arr, pivotIndex + 1, high);
        }
    }
    
    /**
     * Lomuto 分区方案
     * 选择最右侧元素作为基准
     */
    private static int lomutoPartition(int[] arr, int low, int high) {
        int pivot = arr[high];
        int i = low - 1;  // i 指向最后一个小于 pivot 的元素
        
        for (int j = low; j < high; j++) {
            if (arr[j] <= pivot) {
                i++;
                swap(arr, i, j);
            }
        }
        swap(arr, i + 1, high);
        return i + 1;
    }
    
    /**
     * Hoare 分区方案(通常更高效,交换次数更少)
     */
    private static int hoarePartition(int[] arr, int low, int high) {
        int pivot = arr[low];
        int i = low - 1;
        int j = high + 1;
        
        while (true) {
            // 从左向右找到第一个大于等于 pivot 的元素
            do {
                i++;
            } while (arr[i] < pivot);
            
            // 从右向左找到第一个小于等于 pivot 的元素
            do {
                j--;
            } while (arr[j] > pivot);
            
            if (i >= j) {
                return j;
            }
            swap(arr, i, j);
        }
    }
    
    /**
     * 快速排序(三数取中法优化)
     * 使用三数取中法选择基准,避免最坏情况
     */
    public static void sortWithMedianOfThree(int[] arr) {
        quickSortMedian(arr, 0, arr.length - 1);
    }
    
    private static void quickSortMedian(int[] arr, int low, int high) {
        if (low < high) {
            // 使用三数取中法选择基准
            int mid = low + (high - low) / 2;
            // 将三个数的中位数放到 low 位置
            if (arr[mid] < arr[low]) swap(arr, low, mid);
            if (arr[high] < arr[low]) swap(arr, low, high);
            if (arr[high] < arr[mid]) swap(arr, mid, high);
            swap(arr, low, mid);  // 将中位数放到 low 位置
            
            int pivotIndex = hoarePartition(arr, low, high);
            quickSortMedian(arr, low, pivotIndex);
            quickSortMedian(arr, pivotIndex + 1, high);
        }
    }
    
    /**
     * 快速排序(三向切分,适用于大量重复元素)
     * Dijkstra 三向切分方案
     */
    public static void sortThreeWay(int[] arr) {
        quickSortThreeWay(arr, 0, arr.length - 1);
    }
    
    private static void quickSortThreeWay(int[] arr, int low, int high) {
        if (low >= high) return;
        
        int pivot = arr[low];
        int lt = low;      // arr[low..lt-1] < pivot
        int gt = high;     // arr[gt+1..high] > pivot
        int i = low + 1;   // arr[lt..i-1] == pivot
        
        while (i <= gt) {
            if (arr[i] < pivot) {
                swap(arr, lt, i);
                lt++;
                i++;
            } else if (arr[i] > pivot) {
                swap(arr, i, gt);
                gt--;
            } else {
                i++;
            }
        }
        
        quickSortThreeWay(arr, low, lt - 1);
        quickSortThreeWay(arr, gt + 1, high);
    }
    
    /**
     * 快速排序(泛型版本)
     */
    public static <T extends Comparable<T>> void sortGeneric(T[] arr) {
        quickSortGeneric(arr, 0, arr.length - 1);
    }
    
    private static <T extends Comparable<T>> void quickSortGeneric(T[] arr, int low, int high) {
        if (low < high) {
            int pivotIndex = partitionGeneric(arr, low, high);
            quickSortGeneric(arr, low, pivotIndex - 1);
            quickSortGeneric(arr, pivotIndex + 1, high);
        }
    }
    
    private static <T extends Comparable<T>> int partitionGeneric(T[] arr, int low, int high) {
        T pivot = arr[high];
        int i = low - 1;
        for (int j = low; j < high; j++) {
            if (arr[j].compareTo(pivot) <= 0) {
                i++;
                T temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
        T temp = arr[i + 1];
        arr[i + 1] = arr[high];
        arr[high] = temp;
        return i + 1;
    }
    
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

效率分析

指标 最好情况 最坏情况 平均情况
时间复杂度 O(n log n) O(n²) O(n log n)
空间复杂度 O(log n) O(n) O(log n)
比较次数 ≈ n log n n(n-1)/2 ≈ 2n ln n
交换次数 ≈ n log n / 6 n(n-1)/2 ≈ n log n / 3

快速排序是不稳定的排序算法(分区过程中相等元素的相对顺序可能改变)。虽然最坏情况是 O(n²),但通过随机化或三数取中等策略,最坏情况几乎不会发生。实际上,快速排序的平均性能是所有比较排序中最好的之一,这得益于其优秀的缓存局部性。

应用场景

  • 大规模数据排序(默认选择)
  • Java Arrays.sort() 对基本类型数组的默认算法(双轴快速排序)
  • 需要高性能通用排序的场景
  • 数据随机分布的场景
  • 排序可以放入内存的大数据集