算法 --快速排序

发布时间:2026/10/5 3:42:18

算法 --快速排序 什么是快速排序快速排序是一种分治法Divide and Conquer的排序算法。它的核心思想非常简单选基准Pivot从数组中选择一个元素作为基准值。划分Partition将数组重新排列使得所有小于基准值的元素放在基准前面所有大于基准值的元素放在基准后面等于基准的放哪边都可以。这个过程结束后基准值就找到了它在最终排序数组中的正确位置。递归Recurse递归地对基准值左边和右边的子数组进行同样的操作。生活类比就像整理扑克牌你先随便抽一张牌基准然后把比它小的放左边比它大的放右边。接着对左边那堆牌和右边那堆牌分别重复这个动作直到所有牌都有序。基础版本的快速排序最经典的实现是Hoare 划分法双指针法。代码逻辑伪代码void quickSort(vectorint nums, int l, int r) { if (l r) return; // 递归终止条件 // 1. 选基准通常选最左边或最右边但这是有隐患的 int pivot nums[l]; int i l, j r; // 2. 划分 while (i j) { // 从右往左找第一个小于 pivot 的数 while (i j nums[j] pivot) j--; // 从左往右找第一个大于 pivot 的数 while (i j nums[i] pivot) i; // 交换这两个数 if (i j) swap(nums[i], nums[j]); } // 将基准值放到正确位置 swap(nums[l], nums[i]); // 3. 递归处理左右两边 quickSort(nums, l, i - 1); quickSort(nums, i 1, r); }快速排序的性能分析时间复杂度平均情况O(nlog⁡n)。每次划分都能将数组大致分为两半递归深度为 log⁡n每层处理 n 个元素。最坏情况O(n2)。当数组已经有序或逆序且每次选的都是最大/最小值作为基准时划分极度不平衡递归深度退化为 n。空间复杂度O(log⁡n) ~ O(n)。主要是递归调用栈的深度。平均为 O(log⁡n)最坏为 O(n)。稳定性不稳定。在交换过程中相同元素的相对顺序可能会被打乱。题目一颜色分类class Solution { public: void sortColors(vectorint nums) { int n nums.size(); int left -1, right n, i 0; while (i right) { if (nums[i] 0) swap(nums[left], nums[i]); else if (nums[i] 1) i; else swap(nums[--right], nums[i]); } } };代码逻辑详解初始化指针left -1指向 0 区域的右边界初始时 0 区域为空。right n指向 2 区域的左边界初始时 2 区域为空。i 0当前遍历的指针。循环条件while(i right)当i遇到right时停止因为right及其后面的元素都已经确认是 2 了。分支判断核心逻辑情况一nums[i] 0说明当前元素属于红色区域。执行swap(nums[left], nums[i])。先将left向右移一位然后将当前元素i与left位置的元素交换。因为交换过来的元素一定是 1或者就是它自己所以i也向右移动。情况二nums[i] 1说明当前元素属于白色区域位置正确。执行i。直接跳过继续检查下一个元素。情况三nums[i] 2说明当前元素属于蓝色区域应该放到最右边。执行swap(nums[--right], nums[i])。注意这里i没有自增。先将right向左移一位然后交换。由于交换过来的元素原本在right位置的元素是未知的可能是 0、1 或 2所以需要在下一次循环中继续检查当前位置i的新值因此i不能加 1。题目二排序数组class Solution { public: vectorint sortArray(vectorint nums) { srand(time(NULL)); // 种下⼀个随机数种⼦ quickSort(nums, 0, nums.size() - 1); return nums; } int getRandom(vectorint nums, int left, int right) { int r rand(); return nums[r % (right - left 1) left]; } void quickSort(vectorint nums, int l, int r) { if (l r) return; int key getRandom(nums, l, r); int i l, left l - 1, right r 1; while (i right) { if (nums[i] key) swap(nums[left], nums[i]); else if (nums[i] key) i; else swap(nums[--right], nums[i]); } quickSort(nums, l, left); quickSort(nums, right, r); } };代码详细拆解int key getRandom(nums, l, r);随机选取一个基准值。int i l, left l - 1, right r 1;定义三个指针。left指向小于区域的最右侧初始为l-1。right指向大于区域的最左侧初始为r1。i当前遍历的指针。while(i right)遍历数组。if(nums[i] key) swap(nums[left], nums[i]);当前元素小于基准将其交换到左侧区域left和i同时右移。else if(nums[i] key) i;当前元素等于基准跳过i右移。else swap(nums[--right], nums[i]);当前元素大于基准将其交换到右侧区域right左移。注意此时i不自增因为从右侧交换过来的元素还未被检查。递归处理qsort(nums, l, left);递归排序小于基准的区域。qsort(nums, right, r);递归排序大于基准的区域。中间等于基准的区域[left1, right-1]已经就位无需递归。优化策略随机化基准值int getRandom(vectorint nums, int left, int right) { int r rand(); return nums[r % (right - left 1) left]; }在sortArray函数开头调用srand(time(NULL))初始化随机数种子。在getRandom中通过取模运算r % (right - left 1) left生成一个在[left, right]范围内的随机索引并返回该索引对应的值作为基准值key。为什么需要随机化如果每次都固定选择最左边或最右边的元素作为基准当数组已经有序如[1,2,3,4,5]时快速排序会退化成冒泡排序时间复杂度变为 O(n2)。随机选取基准值可以有效避免最坏情况的发生使得期望时间复杂度稳定在 O(nlog⁡n)。题目三数组中第k个最大元素class Solution { public: int findKthLargest(vectorint nums, int k) { srand(time(NULL)); return qsort(nums, 0, nums.size() - 1, k); } int getRandom(vectorint nums, int left, int right) { return nums[rand() % (right - left 1) left]; } int qsort(vectorint nums, int l, int r, int k) { if (l r) return nums[l]; int key getRandom(nums, l, r); int left l - 1, right r 1, i l; while (i right) { if (nums[i] key) swap(nums[left], nums[i]); else if (nums[i] key) i; else swap(nums[--right], nums[i]); } int c r - right 1, b right - left - 1; if (c k) return qsort(nums, right, r, k); else if (b c k) return key; else return qsort(nums, l, left, k - b - c); } };核心思想快速选择 (Quick Select)快速选择算法是快速排序的变种。它的核心思想是我们不需要对整个数组进行完全排序只需要找到第 k 大的元素即可。在快速排序中我们通过基准值pivot将数组分为三部分后会递归处理左右两边。但在快速选择中我们根据 k 的大小每次只需要递归处理其中一部分。这样平均时间复杂度就从O(nlog⁡n)降到了 O(n)。代码详细拆解函数签名int qsort(vectorint nums, int l, int r, int k)这里的k代表我们需要在当前[l, r]区间内寻找第 kk 大的元素。第一步递归终止条件if(l r) return nums[l];当区间内只剩下一个元素时这个元素必然就是我们要找的第 kk 大元素直接返回。第二步随机化基准值与三路划分int key getRandom(nums, l, r); int left l - 1, right r 1, i l; while(i right) { if(nums[i] key) swap(nums[left], nums[i]); else if(nums[i] key) i; else swap(nums[--right], nums[i]); }这部分逻辑与上一题排序数组完全一致。经过这轮循环后数组在[l, r]范围内被划分成了三部分[l, left]小于key的元素左区间[left 1, right - 1]等于key的元素中区间[right, r]大于key的元素右区间第三步分情况讨论核心优化点int c r - right 1, b right - left - 1; if(c k) return qsort(nums, right, r, k); else if(b c k) return key; else return qsort(nums, l, left, k - b - c);这里定义了三个关键变量c大于key的元素个数即右区间的长度。b等于key的元素个数即中区间的长度。分支逻辑分析if(c k)如果右区间大于key的元素的数量c已经大于等于k说明第 kk 大的元素一定在右区间里。此时我们只递归右区间qsort(nums, right, r, k)。注意这里的k不变因为我们仍然是在找整个区间内第 kk 大的元素。else if(b c k)如果c k且b c k说明第 kk 大的元素既不在右区间也不在左区间而是正好落在中区间等于key的部分。因为中区间的所有元素都等于key所以第 kk 大的元素就是key。直接返回key。else如果b c k说明第 kk 大的元素在左区间。此时我们只递归左区间qsort(nums, l, left, k - b - c)。注意这里的k变成了k - b - c因为我们排除了右区间和中区间共b c个元素所以在左区间中我们需要找的是第k - b - c大的元素。题目四库存管理IIIclass Solution { public: vectorint inventoryManagement(vectorint stock, int cnt) { srand(time(NULL)); quickSort(stock, 0, stock.size() - 1, cnt); return {stock.begin(), stock.begin() cnt}; } void quickSort(vectorint stock, int l, int r, int cnt) { if (l r) return; int key getRandom(stock, l, r); int left l - 1, right r 1, i l; while (i right) { if (stock[i] key) swap(stock[left], stock[i]); else if (stock[i] key) i; else swap(stock[--right], stock[i]); } int a left - l 1, b right - left - 1; if (a cnt) quickSort(stock, l, left, cnt); else if (a b cnt) return; else quickSort(stock, right, r, cnt - a - b); } int getRandom(vectorint stock, int l, int r) { return stock[rand() % (r - l 1) l]; } };核心思想Top K 问题的变种这道题的本质是Top K 问题找出最小的 K 个元素。最简单的方法是直接对整个数组排序然后取前cnt个时间复杂度为 O(nlog⁡n)。代码利用了快速选择算法在期望 O(n)的时间复杂度内将最小的cnt个元素移动到数组的最左侧而不需要保证它们内部是有序的题目也说了“返回顺序不限”。代码详细拆解第一部分主入口inventoryManagementvectorint inventoryManagement(vectorint stock, int cnt) { srand(time(NULL)); quickSort(stock, 0, stock.size() - 1, cnt); return {stock.begin(), stock.begin() cnt}; }srand(time(NULL))初始化随机数种子保证基准值选择的随机性。quickSort(...)调用核心逻辑。注意这里传入的cnt代表我们需要在数组中找到最小的cnt个元素。return {stock.begin(), stock.begin() cnt};由于quickSort执行完毕后数组的前cnt个位置已经存放了最小的cnt个元素虽然顺序是乱的直接截取并返回即可。第二部分核心逻辑quickSortvoid quickSort(vectorint stock, int l, int r, int cnt) { if (l r) return; // 递归终止条件 int key getRandom(stock, l, r); int left l - 1, right r 1, i l; while (i right) { if (stock[i] key) swap(stock[left], stock[i]); else if (stock[i] key) i; else swap(stock[--right], stock[i]); } int a left - l 1, b right - left - 1; if (a cnt) quickSort(stock, l, left, cnt); else if (a b cnt) return; else quickSort(stock, right, r, cnt - a - b); }三路划分与之前的题目完全一致。经过while循环后数组在[l, r]范围内被分为三部分[l, left]小于key的元素左区间。[left 1, right - 1]等于key的元素中区间。[right, r]大于key的元素右区间。分情况讨论核心剪枝逻辑这里定义了a和ba左区间小于key的元素个数。b中区间等于key的元素个数。我们的目标是确保最小的cnt个元素都落在数组的前cnt个位置。if (a cnt)如果左区间的元素个数a已经大于cnt了说明最小的cnt个元素全部在左区间里。此时我们只需要递归处理左区间quickSort(stock, l, left, cnt)。中区间和右区间都不用管了。else if (a b cnt)如果a不够cnt但是a b左区间 中区间够cnt了。说明最小的cnt个元素正好由左区间的所有元素和部分中区间元素组成。由于中区间的元素都等于key所以此时数组的前cnt个位置已经全部是符合要求的最小元素了。直接return不需要再做任何递归。else如果a b cnt说明左区间和中区间的元素加起来都不够cnt个。说明最小的cnt个元素分布在左区间、中区间以及部分右区间中。此时我们需要递归处理右区间quickSort(stock, right, r, cnt - a - b)。注意cnt的变化因为我们已经在左区间和中区间找到了a b个最小元素所以还需要在右区间中找cnt - (a b)个最小元素。第三部分随机化工具getRandomint getRandom(vectorint stock, int l, int r) { return stock[rand() % (r - l 1) l]; }生成[l, r]范围内的随机索引返回该索引对应的值作为基准值。快速排序 核心总结1. 核心思想分治法选基准从数组中选一个元素作为基准。划分把小于基准的放左边大于基准的放右边等于基准的放中间。递归 (Recurse)对左右两边子数组重复上述过程。2. 性能指标时间复杂度平均 O(nlog⁡n)最坏 O(n2)可通过随机化避免。空间复杂度O(log⁡n) ~ O(n)取决于递归栈深度。稳定性不稳定交换操作会打乱相同元素的相对顺序。3. 三大核心优化现代工业级写法标配随机化基准值目的避免在数组已经有序时退化成 O(n2)。做法用rand()随机选一个元素与最左边交换再作为基准。三路划分目的解决数组中存在大量重复元素导致的性能退化问题。做法将数组分为 key、 key、 key三部分。等于key的中间部分不再参与递归直接留在原地。经典实现荷兰国旗问题使用left,right,i三个指针。小区间优化目的减少递归调用开销。做法当子数组长度小于某个阈值时改用插入排序。4. 快排的变种快速选择 —— 解决 Top K 问题的利器核心区别快排递归处理左右两边快速选择根据目标 kk只递归处理其中一边。时间复杂度期望 O(n)。应用场景找第 K 大/小的元素。找最小/最大的 K个元素。剪枝逻辑结合三路划分如果右边大于基准的数量c k只递归右边。如果b c k说明第 k 大就在中间等于基准的区域直接返回key。否则只递归左边且目标 k 变为k - b - c。5. C STL 的std::sort是什么不是纯快排而是内省排序 (Introsort)。结合了快排主体 堆排防止最坏情况 插入排序小区间优化。识别信号1. 题目要求“原地排序”且“不允许使用内置函数”触发词“你必须在不使用任何内置函数的情况下解决问题”、“原地对它们进行排序”、“空间复杂度尽可能小”。场景比如前面看的LeetCode 912. 排序数组或者75. 颜色分类。原因快排是原地排序空间复杂度 O(log⁡n)不需要像归并排序那样开辟 O(n)的额外数组空间非常适合内存受限的场景。2. 题目要求 O(n) 时间复杂度解决 Top K 问题触发词“第 K 个最大/最小的元素”、“找出最小的 K 个数”、“库存管理”。场景比如前面看的LeetCode 215. 数组中的第K个最大元素或者LCR 159. 库存管理 III。原因这是快速选择Quick Select的绝佳场景。它不需要对整个数组排序每次划分后只需要根据 K 的大小只递归处理其中一边从而将平均时间复杂度从 O(nlog⁡n) 降到了O(n)。这是解决 Top K 问题的最优解之一另一种是堆排序时间复杂度 O(nlog⁡k。3. 数据量巨大且包含大量重复元素触发词“数组中有大量重复元素”、“注意nums 的值不一定唯一”。场景比如75. 颜色分类只有 0, 1, 2 三种元素或者数组中存在大量相同的数值。原因此时必须使用三路划分的快排。传统的快排遇到大量重复元素会退化成 O(n2)而三路划分可以将等于基准值的元素直接固定在中间不参与后续递归完美解决重复元素问题。4. 题目要求“不稳定排序”且对缓存友好触发词通常不会直接说但如果你需要极高的实际运行速度。原因快排的局部性访问非常好连续内存对 CPU 缓存友好在实际工程中通常比归并排序和堆排序跑得快。C 的std::sort底层就是快排内省排序。
延伸阅读

更多相关文章

2026/10/5 3:37:17

高分遥感语义分割实战:PyTorch从数据到推理的工程链路

简介:这份资源面向遥感图像处理方向的研究者、工程师及具备一定深度学习基础的学习者,提供基于Pytorch实现高分辨率遥感图像语义分割的完整教程与配套数据集,帮助解决地物信息提取中从数据预处理到模型训练、评估的全流程问题。压缩包共1029个…

2026/10/5 3:37:17

AI编程超能力:四大本地化开发工具链实战指南

1. “Superpowers”不是功能开关,而是开发者工作流的范式迁移最近在多个技术社区和开发工具讨论区里,“superpowers”这个词高频出现,但它既不是某个具体软件的官方功能名,也不是某家公司的注册商标。它本质上是一群一线开发者自发…

2026/10/5 3:37:17

MySQL索引失效的8大场景与排查实战:从B+树原理到EXPLAIN

1. 先搞清楚为什么索引会失效:B树和回表聊索引失效之前,得先把底层原理捋一遍。很多人排查慢查询时只看表面,什么"函数导致失效""隐式转换导致失效",背了一堆口诀,但一到真正复杂的SQL还是懵。原因…

2026/10/5 4:32:20

儿童近视防控全攻略:从眼轴监测到OK镜与离焦镜选型

1. 近视防控这件事,先想明白比先动手更重要最近几年,家长群里聊孩子近视的话题越来越多,焦虑感也越来越重。今天你得了个“远视储备告急”的诊断,明天同事说她家孩子已经“真性近视100度”,后天又在短视频里刷到各种“…

2026/10/5 4:32:20

洛谷P1144最短路计数:BFS原理、链式前向星与避坑指南

洛谷P1144,标准的题目名叫“最短路计数”,是我刷图论入门题单时绕不开的一道题。题目本身不复杂:给你一张可能有重边和自环的无向无权图,从点1出发,问到达每个点的最短路径一共有多少条,结果对100003取模。…

2026/10/5 4:32:20

企业微信外部群自动化推送:Webhook对接、监控告警与风控实战

在私域运营和企业协作里,“企业微信外部群自动化消息推送”是近期被问得最多的一类需求。团队想把监控告警、业务通知、运营内容自动推到客户群或者合作方群里,但又怕频率太高、行为太像机器人,反而被封号。这篇就是聊聊我实际做过的方案&…

2026/10/5 4:32:20

基于SpringBoot的行李寄存管理系统:从部署到答辩完整拆解

大概每一两周就会收到一次私信,问"行李寄存管理系统"这类基于SpringBoot的项目怎么跑起来、代码怎么读、答辩怎么讲。这类项目在课程设计和毕业设计里出现频率极高,原因很简单:业务场景足够真实,技术栈足够主流&#xf…

2026/10/5 4:32:20

Soap:专为GGUF模型设计的轻量级LoRA微调工具

1. Soap不是协议,是微调界的“傻瓜相机”——为什么它突然火了? Soap!一键微调大模型!4G显存可调8B模型!——看到这个标题,我第一反应不是点开,而是把手机横过来截图发给三个做AI落地的朋友。不…

2026/10/5 4:27:19

律所发票批量录入实操指南:从手工逐条到Excel导入提效

1. 为什么律所发票录入这么慢,问题到底出在哪办工桌前一坐就是一下午,就为了把几十张发票一张张敲进系统。这种事在律所行政、财务和内勤岗位上太常见了。我自己也干过这事,第一次处理月度票据归档时,对着业务管理系统逐条手工录发…

2026/10/4 0:01:02

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/4 0:01:02

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/4 1:01:05

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑