排序---快速排序(Quick Sort)

一、算法核心概念

快速排序是一种分治法(Divide and Conquer) 思想的排序算法,由计算机科学家Tony Hoare于1960年提出。其核心思想是:通过选择一个"基准元素"(Pivot),将数组划分为两个子数组,其中左子数组的元素均小于等于基准,右子数组的元素均大于等于基准;然后递归地对两个子数组执行相同操作,最终使整个数组有序。

快速排序的高效性源于分区操作(Partition)------每次分区能将一个元素(基准)放到最终位置,同时将问题规模缩小一半(理想情况),从而实现接近O(nlogn)的时间复杂度。

二、基本工作原理

快速排序的工作流程可概括为三步:选择基准→分区→递归排序,具体如下:

  1. 选择基准(Pivot Selection)

    从数组中选择一个元素作为基准(如第一个、最后一个、中间元素或随机元素)。基准的选择直接影响算法效率(后续优化部分详细说明)。

  2. 分区(Partition)

    重新排列数组,使所有小于基准的元素移到基准左侧,所有大于基准的元素移到基准右侧(等于基准的元素可放任意侧)。分区后,基准元素的位置即为其在最终有序数组中的位置。

  3. 递归排序

    对基准左侧的子数组和右侧的子数组分别重复上述步骤(选择基准→分区),直至子数组长度为0或1(天然有序)。

三、分区策略(核心步骤详解)

分区是快速排序的核心操作,常用的分区方法有双边循环法单边循环法,以下以双边循环法为例详细说明:

双边循环法步骤(升序排序):
  • 初始状态:设数组为arr,左边界left,右边界right,选择arr[left]为基准pivot
  • 左指针ileft开始向右移动,寻找第一个大于pivot的元素;
  • 右指针jright开始向左移动,寻找第一个小于pivot的元素;
  • i < j,交换arr[i]arr[j],重复上述步骤;
  • i >= j时,交换arr[left](基准)和arr[j],此时j为基准的最终位置,分区完成。
四、实例演示(完整排序过程)

以数组[4, 7, 6, 5, 3, 2, 8, 1]为例,演示快速排序过程:

  1. 第一次分区

    • 选择基准pivot = 4(第一个元素),left=0right=7
    • 左指针i右移,找到第一个大于4的元素7(索引1);
    • 右指针j左移,找到第一个小于4的元素1(索引7);
    • 交换71→数组变为[4, 1, 6, 5, 3, 2, 8, 7]
    • 继续移动指针:i找到6(索引2),j找到2(索引5),交换→[4, 1, 2, 5, 3, 6, 8, 7]
    • 继续移动:i找到5(索引3),j找到3(索引4),交换→[4, 1, 2, 3, 5, 6, 8, 7]
    • 此时i=4j=4i >= j),交换基准4arr[j][3, 1, 2, 4, 5, 6, 8, 7]
    • 分区完成,基准4的位置为索引3,左子数组[3,1,2],右子数组[5,6,8,7]
  2. 递归处理左子数组[3,1,2]

    • 选择基准3,分区后得到[2,1,3],基准3在索引2,左子数组[2,1]
    • 递归处理[2,1],分区后得到[1,2]
  3. 递归处理右子数组[5,6,8,7]

    • 选择基准5,分区后5在索引0,右子数组[6,8,7]
    • 处理[6,8,7],选择基准6,分区后6在索引0,右子数组[8,7]
    • 处理[8,7],分区后得到[7,8]
  4. 最终有序数组[1, 2, 3, 4, 5, 6, 7, 8]

五、时间复杂度与空间复杂度
  • 时间复杂度

    • 理想情况(基准每次将数组分为等长两部分):每轮分区处理n个元素,递归深度为logn,总操作次数为O(nlogn);
    • 最坏情况(基准为数组的最值,如已排序数组选第一个元素):每次分区仅减少1个元素,递归深度为n,总操作次数为O(n²);
    • 平均情况:通过合理选择基准(如随机选择),可避免最坏情况,平均时间复杂度为O(nlogn)
  • 空间复杂度

    快速排序为原地排序(仅需常数空间存储临时变量),但递归调用会产生栈空间开销:

    • 理想情况:递归深度为logn,空间复杂度O(logn)
    • 最坏情况:递归深度为n,空间复杂度O(n)
六、稳定性分析

快速排序是不稳定的排序算法

  • 原因:分区过程中,相等元素的相对顺序可能被破坏。例如,数组[2, 2, 1]中,若选择第一个2为基准,分区时会将1与第一个2交换,导致两个2的相对顺序颠倒(变为[1, 2, 2],虽然结果有序,但原数组中第一个2在第二个2之前,排序后位置交换)。
七、优化策略(避免最坏情况)

快速排序的性能极大依赖基准选择,以下是常见优化手段:

  1. 随机选择基准

    每次从当前区间随机选择一个元素作为基准,避免在有序数组中总是选择最值作为基准(概率上降低最坏情况发生的可能性)。

  2. 三数取中法

    从区间的首、尾、中间三个位置选择中间值作为基准(如median(arr[left], arr[mid], arr[right])),适用于已知数据可能接近有序的场景。

  3. 小规模子数组改用插入排序

    当子数组长度小于阈值(如10)时,改用插入排序(插入排序在小规模数据上效率高于快速排序,减少递归开销)。

  4. 尾递归优化

    对较大的子数组采用递归,较小的子数组采用循环,减少递归栈深度(避免栈溢出)。

八、C++实现代码(含优化)

以下实现采用随机选择基准双边循环分区,并添加基本边界处理:

cpp 复制代码
#include <iostream>
#include <vector>
#include <cstdlib>  // 用于rand()和srand()
#include <ctime>    // 用于time()初始化随机数种子
using namespace std;

// 交换两个元素
void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

// 随机选择基准并分区(双边循环法)
int partition(vector<int>& arr, int left, int right) {
    // 随机选择基准索引(避免最坏情况)
    int pivotIndex = left + rand() % (right - left + 1);
    // 将基准交换到区间左侧(方便分区逻辑)
    swap(arr[left], arr[pivotIndex]);
    int pivot = arr[left];  // 基准值

    int i = left;   // 左指针(从left开始)
    int j = right;  // 右指针(从right开始)

    while (i < j) {
        // 右指针左移:找到第一个小于pivot的元素
        while (i < j && arr[j] >= pivot) {
            j--;
        }
        // 左指针右移:找到第一个大于pivot的元素
        while (i < j && arr[i] <= pivot) {
            i++;
        }
        // 交换左右指针指向的元素
        if (i < j) {
            swap(arr[i], arr[j]);
        }
    }

    // 将基准放到最终位置(i=j处)
    swap(arr[left], arr[i]);
    return i;  // 返回基准索引
}

// 快速排序主函数
void quickSort(vector<int>& arr, int left, int right) {
    // 递归终止条件:区间长度 <= 1
    if (left >= right) {
        return;
    }

    // 分区:得到基准的最终位置
    int pivotPos = partition(arr, left, right);
    // 递归排序左子数组
    quickSort(arr, left, pivotPos - 1);
    // 递归排序右子数组
    quickSort(arr, pivotPos + 1, right);
}

int main() {
    // 初始化随机数种子(确保每次运行随机基准不同)
    srand(time(nullptr));

    vector<int> arr = {4, 7, 6, 5, 3, 2, 8, 1};
    cout << "排序前:";
    for (int num : arr) {
        cout << num << " ";
    }

    quickSort(arr, 0, arr.size() - 1);

    cout << "\n排序后:";
    for (int num : arr) {
        cout << num << " ";
    }
    // 输出:排序前:4 7 6 5 3 2 8 1 
    //      排序后:1 2 3 4 5 6 7 8 
    return 0;
}
九、适用场景与优缺点
适用场景:
  1. 大规模数据排序:平均时间复杂度O(nlogn),实际性能优于归并排序和堆排序(缓存友好,局部性好);
  2. 内存受限场景:原地排序(除递归栈外无需额外空间),空间效率高;
  3. 通用排序需求 :C++标准库的std::sort、Java的Arrays.sort(针对基本类型)等均采用快速排序的变种(如 introsort)。
优点:
  • 平均性能优异,实际应用中排序速度快;
  • 原地排序,空间复杂度低(O(logn));
  • 对缓存友好(局部访问数据,符合CPU缓存机制)。
缺点:
  • 不稳定,不适合要求保持相等元素相对顺序的场景;
  • 最坏情况时间复杂度为O(n²)(需通过基准选择优化避免);
  • 递归实现可能导致栈溢出(可通过尾递归优化解决)。

快速排序是分治法的经典应用,通过"选择基准→分区→递归"三步实现高效排序。其核心优势在于平均O(nlogn)的时间复杂度和原地排序特性,使其成为实际应用中最常用的排序算法之一。尽管存在不稳定性和最坏情况风险,但通过随机基准、三数取中等优化策略,可有效规避缺陷。理解快速排序的分区逻辑和优化思想,不仅能掌握一种高效排序方法,更能深化对分治法的理解,为解决复杂算法问题提供思路。

相关推荐
刘梓谦4 小时前
如何在Qt中使用周立功USB转CAN卡
开发语言·qt·zlg·周立功
小蒜学长4 小时前
旅行社旅游管理系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·旅游
Kevinyu_4 小时前
RabbitMQ
java·rabbitmq·java-rabbitmq
江公望4 小时前
Qt QML实现无边框窗口
开发语言·qt
TT哇4 小时前
【多线程案例】:单例模式
java·单例模式·面试
秦禹辰4 小时前
宝塔面板安装MySQL数据库并通过内网穿透工具实现公网远程访问
开发语言·后端·golang
黄焖鸡能干四碗4 小时前
智慧教育,智慧校园,智慧安防学校建设解决方案(PPT+WORD)
java·大数据·开发语言·数据库·人工智能