当前位置: 首页 > article >正文

快速排序不啦不啦


一、快速排序基本思想

想象你有一堆杂乱的书需要按编号排列。快速排序的做法是:
1️⃣ 随便选一本书作为"基准书"
2️⃣ 把比它小的放左边,比它大的放右边
3️⃣ 对左右两堆书重复这个过程


二、代码逐行解析

1. 随机数生成
int getRand(int min, int max) {
    return (rand() % (max - min + 1)) + min;
}

作用:生成[min,max]范围内的随机数
类比:闭着眼睛从书堆里随机摸一本书


2. 核心排序逻辑
void quick_sort(int arr[], int low, int high) {
    if(low >= high) return; // 终止条件:只剩一本书时不需要整理

    // 🎯 随机选基准书
    int t_id = getRand(low, high);
    int tmp = arr[t_id];
    
    // 📌 初始化两个"整理员"
    int i = low-1;  // 左整理员(从最左边开始往右走)
    int j = high+1; // 右整理员(从最右边开始往左走)

    while(i < j) {
        // 👉 右整理员找小书
        while(arr[--j] > tmp); // 一直往左走,直到找到比基准小的书
        
        // 👈 左整理员找大书
        while(arr[++i] < tmp); // 一直往右走,直到找到比基准大的书

        if(i < j) swap(arr[i], arr[j]); // 交换两本放错位置的书
    }

    // ✂️ 分割成左右两堆继续整理
    quick_sort(arr, low, j);    // 整理左边小书堆
    quick_sort(arr, j+1, high); // 整理右边大书堆
}

三、动态演示

假设要排序数组 [3,1,4,2,5],随机选中3作为基准:

步骤1️⃣:整理员开始工作

初始状态:i=-1, j=5
[3,1,4,2,5]
 ↑       ↑

右整理员j找到2(比3小) → j=3
左整理员i找到4(比3大) → i=2
交换后:[3,1,2,4,5]

继续循环:
右整理员j移动到1 → j=1
左整理员i移动到4 → i=3
此时i>j,循环结束

步骤2️⃣:分割区间

分割点j=1 → 左区间[0,1],右区间[2,4]
继续递归处理左右区间

四、关键点解释

为什么要随机选基准?

如果总是选第一个元素,遇到已排序数组时效率会退化成O(n²)。随机选择能避免最坏情况。

i和j的移动顺序
  • --j 先移动再判断:避免死循环
  • ++i 先移动再判断:保证每次循环都有进展
循环终止条件

当i和j相遇或交叉时(i >= j),说明分区完成


五、存在的问题

当所有元素相同时(如全2),每次只能确定一个元素的位置,时间复杂度退化为O(n²)。解决方法:

+ 使用三路快排,将数组分为 <、=、> 三部分
+ 遇到重复元素时直接跳过中间区域

六、性能对比

情况平均时间复杂度最坏时间复杂度
普通快排O(nlogn)O(n²)
三路快排O(nlogn)O(nlogn)

逐步解释:

普通的快速排序在最坏情况下(例如所有元素完全相同)仍然会出现 O(n²) 的时间复杂度。以下是详细分析:


一、问题根源

当数组中所有元素相同时,代码的分区逻辑会失效:

  1. 每次分区只能确定一个元素的位置
    假设数组为 [2,2,2,2,2]

    • 随机选择基准后,ij 会在首尾相遇
    • 分区后左区间为整个数组,右区间为空
    • 每次递归只能排除一个元素(基准本身)
  2. 递归深度变为 O(n)
    导致总操作次数为:

    n + (n-1) + (n-2) + ... + 1 = n(n+1)/2 → O(n²)
    

二、代码验证

以全为 2 的数组 [2,2,2,2,2] 为例:

  1. 第一次分区

    • ilow-1=-1 开始右移,直到 i=4(越界)
    • jhigh+1=5 开始左移,直到 j=0
    • 分区后:左区间 [0,0],右区间 [1,4]
  2. 递归右区间 [1,4]
    重复上述过程,每次只能排除一个元素。


三、解决方法

使用 三路快速排序 优化重复元素处理:

  1. 将数组分为三部分
    • < pivot= pivot> pivot
  2. 跳过重复元素区域
    递归时只需处理 <> 的部分。

四、三路快排核心代码
void quick_sort(int arr[], int low, int high) {
    if (low >= high) return;

    // 随机选择基准并交换到low位置
    int t_id = getRand(low, high);
    swap(arr[low], arr[t_id]);
    int pivot = arr[low];

    // 三路分割初始化
    int lt = low;      // 最后一个小于pivot的位置 (less than)
    int gt = high;     // 第一个大于pivot的位置 (greater than)
    int i = low + 1;   // 当前遍历指针

    while (i <= gt) {
        if (arr[i] < pivot) {
            swap(arr[i], arr[lt]);
            lt++;
            i++;
        } else if (arr[i] > pivot) {
            swap(arr[i], arr[gt]);
            gt--;
        } else {
            i++;
        }
    }

    // 递归处理左右区域
    quick_sort(arr, low, lt - 1);  // 处理 < 部分
    quick_sort(arr, gt + 1, high); // 处理 > 部分
}

五、复杂度对比
情况原代码(双路快排)三路快排
普通数组O(n log n)O(n log n)
全相同元素O(n²)O(n)

六、总结
  • 原代码问题:随机化基准无法解决全相同元素的极端情况,时间复杂度仍可能为 O(n²)。
  • 终极解决方案:通过三路快排跳过重复元素区域,将最坏情况复杂度优化至 O(n)。

http://www.kler.cn/a/612793.html

相关文章:

  • 工作记录 2017-03-10
  • Success is the sum of small efforts repeated day in and day out.
  • VLAN 聚合
  • skynet.socket.limit 使用详解
  • Ubuntu里安装Jenkins
  • 整理和记录前端常见问题
  • 科技快讯 | 谷歌发布新一代推理模型;我国成功发射天链二号04星;Manus:将举行线下活动 正努力让更多人用上Manus
  • 如何查看 SQL Server 的兼容性级别
  • MySQL 表连接(内连接与外连接)
  • HCIA-Datacom高阶:基础的单区域 OSPF 与多区域 OSPF的配置
  • OpenGL —— 流媒体播放器 - ffmpeg解码rtsp流,opengl渲染yuv视频(附源码,glfw+glad)
  • Docker安装MySql 8.0
  • Modbus主站EtherNet/IP转ModbusRTU/ASCII工业EIP网关串口服务器
  • 企业签名app部分用户能安装,部分用户不能安装
  • Java基础——面向对象
  • vue3 数据监听(watch、watchEffect)
  • Go常用的设计模式
  • STM32F103_LL库+寄存器学习笔记11 - 串口收发的中断优先级梳理
  • HTML跑酷
  • 活动预告丨CCF开源发展委员会“开源高校行”第三十三期—湖南师范大学站