快速排序

92 阅读4分钟

快速排序是一种基于分治法的高效排序算法,其平均时间复杂度为 O(n log n)。

🔍 快速排序的核心思想与步骤

快速排序的核心思想是分而治之,通过一趟排序将待排序列分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据小,然后再按此方法对这两部分数据分别进行快速排序,整个过程可以递归进行。

其具体步骤如下:

  1. 选择基准值:从数组中选择一个元素作为“基准”(pivot)。常见的策略有选择第一个元素、最后一个元素、中间元素,或者使用更复杂的如“三数取中法”。
  2. 分区操作:这是快速排序的核心。重新排列数组,使得所有比基准值小的元素都放在基准前面,所有比基准值大的元素都放在基准后面。操作结束后,基准值就处于其最终的正确位置上。
  3. 递归排序:递归地将小于基准值的子数组和大于基准值的子数组进行快速排序。

递归的基本情况是子数组的大小为0或1,此时数组已经被视为有序。

快排2.gif

💻 快速排序的Java实现

以下是一个使用最后一个元素作为基准值的快速排序Java实现,代码中包含了详细的注释以帮助你理解。

public class QuickSort {

    // 快速排序的公开入口方法
    public static void quickSort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }
        quickSort(arr, 0, arr.length - 1);
    }

    // 递归排序的主方法
    private static void quickSort(int[] arr, int low, int high) {
        // 递归终止条件:当子数组只有一个元素或没有元素时
        if (low < high) {
            // partition方法对数组进行分区,并返回基准值的正确位置索引
            int pivotIndex = partition(arr, low, high);
            
            // 递归排序基准值左边的子数组
            quickSort(arr, low, pivotIndex - 1);
            // 递归排序基准值右边的子数组
            quickSort(arr, pivotIndex + 1, high);
        }
    }

    // 分区方法:将数组分为两部分,并返回基准值的最终位置
    private static int partition(int[] arr, int low, int high) {
        // 选择最右边的元素作为基准值(pivot)
        int pivot = arr[high];
        
        // 初始化一个指针`i`,指向比基准值小的区域的最后一个元素
        int i = low - 1;

        // 遍历从`low`到`high-1`的所有元素
        for (int j = low; j < high; j++) {
            // 如果当前元素小于或等于基准值
            if (arr[j] <= pivot) {
                // 将指针`i`向右移动一位,扩大小于基准值的区域
                i++;
                // 交换arr[i]和arr[j],将当前小的元素挪到“小区域”
                swap(arr, i, j);
            }
        }

        // 将基准值(arr[high])与`i+1`位置的元素交换,将其放到正确的位置
        // 此时,基准值左边的元素都小于等于它,右边的元素都大于它
        swap(arr, i + 1, high);
        
        // 返回基准值的最终位置索引
        return i + 1;
    }

    // 交换数组中两个元素的方法
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    // 测试代码
    public static void main(String[] args) {
        int[] arr = {10, 7, 8, 9, 1, 5};
        System.out.println("排序前:" + java.util.Arrays.toString(arr));
        quickSort(arr);
        System.out.println("排序后:" + java.util.Arrays.toString(arr));
    }
}

🔄 代码执行示例

以数组 [10, 7, 8, 9, 1, 5]为例,详细说明第一轮分区过程:

  • 基准值:选择最后一个元素 5作为基准。

  • 分区过程

    • j=0:10 > 5,不交换,i不动。
    • j=1:7 > 5,不交换,i不动。
    • j=2:8 > 5,不交换,i不动。
    • j=3:9 > 5,不交换,i不动。
    • j=4:1 <= 5,i自增为0,交换arr0和arr4。数组变为 [1, 7, 8, 9, 10, 5]
  • 基准归位:遍历结束,交换arr[i+1](即arr[1]=7)和基准arr[5]=5。数组变为 [1, 5, 8, 9, 10, 7]。此时基准值5已经位于其正确的位置(索引1)。

  • 接着,递归排序左子数组 [1]和右子数组 [8, 9, 10, 7]

⚙️ 算法特性与优化

  • 时间复杂度

    • 平均情况:O(n log n),高效是其被广泛使用的原因。
    • 最坏情况:当数组已完全有序或逆序,且基准选择不当时,会退化为 O(n²)。
  • 空间复杂度:主要取决于递归调用栈的深度,平均为 O(log n),最坏为 O(n)。

  • 稳定性:快速排序是不稳定的排序算法。在分区过程中,相等元素的相对位置可能会改变。

为了避免最坏情况的发生,常见的优化策略有:

  1. 随机化基准:随机选择一个元素作为基准。
  2. 三数取中法:取数组头、尾、中间三个元素的中位数作为基准。
  3. 小数组优化:当子数组规模较小(如小于10)时,切换使用插入排序。

💎 核心要点回顾

快速排序的核心在于分区操作,通过双指针(如上述代码中的 ij)将数组划分为三个部分:小于基准区、待处理区、大于基准区。掌握好分区过程,就掌握了快速排序的精髓。