首页 > Java > java教程 > 正文

深入理解快速排序:一种高效的就地分区实现

心靈之曲
发布: 2025-09-30 12:19:00
原创
253人浏览过

深入理解快速排序:一种高效的就地分区实现

本文详细阐述了快速排序算法的一种常见实现,重点介绍其核心的“就地分区”策略。通过选择一个基准值,并利用双指针技术将数组划分为小于、等于和大于基准值的区域,最终将基准值放置到其最终排序位置。随后,算法递归地对左右子数组进行排序,从而实现高效的整体排序。

引言:快速排序概述

快速排序(quicksort)是一种高效的、基于比较的排序算法,由c.a.r. hoare在1960年提出。它采用了“分而治之”(divide and conquer)的思想,其核心在于通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据要小,然后按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

快速排序的主要步骤包括:

  1. 选择基准值(Pivot Selection):从数组中选择一个元素作为基准值。
  2. 分区(Partitioning):重新排列数组,将所有小于基准值的元素移到基准值的左边,所有大于基准值的元素移到基准值的右边。在这个过程中,基准值会移动到其最终的排序位置。
  3. 递归排序(Recursive Sort):递归地对基准值左右两边的子数组进行快速排序。

核心机制:就地分区(In-Place Partitioning)

分区操作是快速排序算法的灵魂。其目标是选择一个基准值,并通过一系列交换操作,将数组分为两部分:一部分包含所有小于基准值的元素,另一部分包含所有大于基准值的元素,并且将基准值放置到它在最终有序数组中的正确位置。本文将介绍一种常见的就地分区策略,该策略利用双指针技术实现。

分区函数详解:getPivotIndex

getPivotIndex 函数负责执行实际的分区操作,并返回基准值在排序后子数组中的最终索引。

  1. 基准值选择:在本实现中,我们简单地选择当前子数组的第一个元素 arg[startIndex] 作为基准值 (pivotVal)。虽然这种选择方式简单,但在某些情况下(如数组已部分有序或逆序),可能会导致性能下降。
  2. 双指针初始化
    • 左指针 i 初始化为 startIndex。
    • 右指针 j 初始化为 endIndex(注意:endIndex 是不包含的边界)。
  3. 遍历与交换逻辑: while (i < j) 循环是分区过程的核心。
    • 右指针 j 移动:while (i < j && (arg[--j] >= pivotVal));
      • 右指针 j 从右向左移动(先自减),寻找第一个小于 pivotVal 的元素。
      • 如果找到一个小于 pivotVal 的元素,并且 i < j 仍然成立,则将该元素移动到 arg[i] 的位置。这相当于将一个“小”元素从右侧移动到了左侧的空位。
    • 左指针 i 移动:while (i < j && (arg[++i] <= pivotVal));
      • 左指针 i 从左向右移动(先自增),寻找第一个大于 pivotVal 的元素。
      • 如果找到一个大于 pivotVal 的元素,并且 i < j 仍然成立,则将该元素移动到 arg[j] 的位置。这相当于将一个“大”元素从左侧移动到了右侧的空位。
    • 这个过程持续进行,直到 i 和 j 相遇或交叉。当它们相遇时,i 和 j 指向的将是基准值最终的正确位置。
  4. 基准值归位:循环结束后,i 和 j 相遇的位置就是基准值最终的正确位置。我们将最初选定的 pivotVal 放置于 arg[j]。
  5. 返回基准值索引:函数返回 j 作为基准值的最终索引。此时,arg[j] 左侧的所有元素都小于或等于 pivotVal,右侧的所有元素都大于或等于 pivotVal。

主排序函数:quickSort

quickSort 函数是快速排序的入口点,它负责处理递归调用。

简篇AI排版
简篇AI排版

AI排版工具,上传图文素材,秒出专业效果!

简篇AI排版 554
查看详情 简篇AI排版
  1. 递归基线条件:if (endIndex - startIndex < 2)
    • 当子数组的长度小于2时(即子数组只包含0个或1个元素),表示子数组已经有序,无法再进行分区。这是递归的终止条件,直接返回。
  2. 分区调用:int pivotIndex = getPivotIndex(arg, startIndex, endIndex);
    • 调用 getPivotIndex 函数,对当前子数组进行分区操作,并获取基准值在数组中的最终位置索引。
  3. 递归调用
    • quickSort(arg, startIndex, pivotIndex);:对基准值左侧的子数组(从 startIndex 到 pivotIndex,不包含 pivotIndex 处的基准值)进行递归排序。
    • quickSort(arg, pivotIndex + 1, endIndex);:对基准值右侧的子数组(从 pivotIndex + 1 到 endIndex,不包含 endIndex)进行递归排序。

完整示例代码 (Java)

以下是上述快速排序算法的完整 Java 实现:

public class QuickSort_Impl {

    public static void main(String[] args) {
        int[] unsortedArray = {12, 3, 45, 23, 6, -4, -6, 10, 1, 8};

        System.out.println("原始数组:");
        for (int i : unsortedArray)
            System.out.print(i + " ");
        System.out.println();

        // 调用快速排序,对整个数组进行排序
        // endIndex 使用数组长度,因为它是排他性的
        quickSort(unsortedArray, 0, unsortedArray.length);

        System.out.println("排序后数组:");
        for (int i : unsortedArray)
            System.out.print(i + " ");
        System.out.println();
    }

    /**
     * 快速排序主函数,采用分治策略对数组进行排序。
     * @param arg 待排序数组
     * @param startIndex 子数组的起始索引(包含)
     * @param endIndex 子数组的结束索引(不包含)
     */
    static void quickSort(int[] arg, int startIndex, int endIndex) {
        // 递归基线条件:如果子数组长度小于2,则无需进一步分区,直接返回。
        // 例如,只剩一个元素或没有元素。
        if (endIndex - startIndex < 2)
            return;

        // 调用分区函数,找到基准值的最终位置,并完成一次分区。
        int pivotIndex = getPivotIndex(arg, startIndex, endIndex);

        // 递归排序基准值左侧的子数组。
        // 范围从 startIndex 到 pivotIndex (不包含 pivotIndex)。
        quickSort(arg, startIndex, pivotIndex);

        // 递归排序基准值右侧的子数组。
        // 范围从 pivotIndex + 1 到 endIndex (不包含 endIndex)。
        quickSort(arg, pivotIndex + 1, endIndex);
    }

    /**
     * 执行分区操作,并将基准值放置到其最终的排序位置。
     * @param arg 待分区数组
     * @param startIndex 子数组的起始索引(包含)
     * @param endIndex 子数组的结束索引(不包含)
     * @return 基准值最终位置的索引
     */
    private static int getPivotIndex(int[] arg, int startIndex, int endIndex) {
        // 选择子数组的第一个元素作为基准值。
        // 这是常见的选择方式,但并非总是最优。
        int pivotVal = arg[startIndex];
        int i = startIndex; // 左指针
        int j = endIndex;   // 右指针

        // 当左指针 i 小于右指针 j 时,持续进行分区操作。
        while (i < j) {
            // 右指针 j 从右向左移动,寻找第一个小于基准值的元素。
            // 注意:j 先自减,确保 j 指向有效元素。
            while (i < j && (arg[--j] >= pivotVal));
            // 如果找到,将该元素移动到 i 指向的位置。
            if (i < j)
                arg[i] = arg[j];

            // 左指针 i 从左向右移动,寻找第一个大于基准值的元素。
            // 注意:i 先自增,确保 i 指向有效元素。
            while (i < j && (arg[++i] <= pivotVal));
            // 如果找到,将该元素移动到 j 指向的位置。
            if (i < j)
                arg[j] = arg[i];

        } // End Outer while

        // 循环结束后,i 和 j 相遇,该位置就是基准值最终的排序位置。
        // 将基准值放置到这个位置。
        arg[j] = pivotVal;

        // 返回基准值的最终索引。
        return j;
    }
}
登录后复制

性能分析与注意事项

  1. 时间复杂度
    • 平均情况:O(n log n)。这是快速排序最常见的性能表现,非常高效。
    • 最坏情况:O(n^2)。当基准值选择不当,导致每次分区都产生一个空子数组(例如,每次都选择最大或最小元素作为基准值,而数组已经有序或逆序),此时快速排序的性能会退化为类似于冒泡排序
  2. 空间复杂度
    • 平均情况:O(log n)。这主要取决于递归调用的深度。
    • 最坏情况:O(n)。在最坏情况下,递归深度可能达到 n。
  3. 基准值选择
    • 基准值的选择对快速排序的性能至关重要。本文示例中选择第一个元素作为基准值,这在某些特定输入下可能导致最坏情况。
    • 优化策略包括:
      • 随机选择:随机选择一个元素作为基准值,可以有效避免最坏情况的发生。
      • 三数取中法:选择子数组的第一个、中间和最后一个元素,取它们的中位数作为基准值,这通常能更好地选择一个“中间”的基准值。
  4. 稳定性:快速排序通常不是稳定排序算法。这意味着具有相同值的元素的相对顺序在排序后可能发生改变。
  5. 与Lomuto/Hoare分区对比
    • 本文实现的这种分区策略是快速排序中常见且高效的一种,其核心是将基准值放置到最终位置。它与Lomuto分区法有异曲同工之处,都是在单次遍历中完成分区并确定基准位置。
    • Hoare分区法是另一种经典的分区方案,它通常被认为在实践中比Lomuto分区法更高效,因为它交换的次数更少。Hoare分区法不保证基准值最终位于其精确的排序位置,但它保证基准值左侧的元素都小于或等于基准值,右侧的元素都大于或等于基准值。

总结

快速排序是一种功能强大且广泛应用的排序

以上就是深入理解快速排序:一种高效的就地分区实现的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号