分治算法-快速排序专题总结:分治
刷题记录
- 刷题周期: 1天(10.16)
- 完成题量: 4题
- 通过情况: 4/4 AC

目录
- 刷题过程
- 分治算法核心概念
- 快速排序核心思想
- 题目详解
- [1. 颜色分类 (LeetCode 75)](#1. 颜色分类 (LeetCode 75))
- [2. 快速排序 (LeetCode 912) ⭐⭐](#2. 快速排序 (LeetCode 912) ⭐⭐)
- [3. 数组中的第K个最大元素 (LeetCode 215) ⭐⭐⭐](#3. 数组中的第K个最大元素 (LeetCode 215) ⭐⭐⭐)
- [4. 最小的K个数 (剑指Offer 40) ⭐⭐](#4. 最小的K个数 (剑指Offer 40) ⭐⭐)
- 核心收获
- 易错点总结
刷题过程
分治算法-快速排序专题集中刷题,从基础到进阶:
- 基础应用:颜色分类(三指针法)
- 核心算法:快速排序(分三块+随机基准)
- 算法优化:快速选择(从O(n log n)到O(n))
- 灵活应用:最小K个数(理解返回值类型的差异)
重点突破了递归出口的理解和快速选择算法的优化思想。
分治算法核心概念
什么是分治算法?
分治(Divide and Conquer) 是一种算法设计思想,将原问题分解为若干个规模较小的子问题,递归地解决这些子问题,然后将子问题的解合并为原问题的解。
分治算法三要素
1. 分(Divide):将原问题分解为若干个子问题
2. 治(Conquer):递归地解决这些子问题
3. 合(Combine):将子问题的解合并为原问题的解
分治算法的适用条件
- 问题可以分解为若干个子问题
- 子问题与原问题性质相同,只是规模变小
- 子问题的解可以合并为原问题的解
- 子问题之间相互独立
分治算法的经典应用
算法 | 分 | 治 | 合 | 时间复杂度 |
---|---|---|---|---|
快速排序 | 选基准,分三块 | 递归排序左右 | 无需合并 | O(n log n) |
归并排序 | 从中间分两半 | 递归排序左右 | 合并有序数组 | O(n log n) |
二分查找 | 从中间分两半 | 只递归一边 | 无需合并 | O(log n) |
快速排序核心思想
传统快排 vs 优化快排
传统快排(数组分两块)
cpp
void qsort(vector<int>& nums, int l, int r) {
if(l >= r) return;
int key = nums[l]; // 固定选第一个元素
int i = l, j = r;
while(i < j) {
while(i < j && nums[j] >= key) j--;
nums[i] = nums[j];
while(i < j && nums[i] <= key) i++;
nums[j] = nums[i];
}
nums[i] = key;
qsort(nums, l, i - 1);
qsort(nums, i + 1, r);
}
问题:
- 固定选第一个元素,遇到有序数组退化为O(n²)
- 大量重复元素时,相等元素也要递归处理
优化快排(数组分三块 + 随机基准)
cpp
void qsort(vector<int>& nums, int l, int r) {
if(l >= r) return;
// 1. 随机选择基准
int key = getRandom(nums, l, r);
// 2. 数组分三块:[<key] [==key] [>key]
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]);
}
// 3. 只递归 < key 和 > key 的部分
qsort(nums, l, left);
qsort(nums, right, r);
}
优势:
- 随机基准使最坏情况概率降到几乎为0
- 三块划分跳过相等元素,处理重复元素效率高
三指针法(荷兰国旗问题)
核心思想: 用三个指针将数组分成四段
[0, left] 已确定的小于key的元素
[left+1, i-1] 已确定的等于key的元素
[i, right-1] 待处理的元素
[right, n-1] 已确定的大于key的元素
关键点:
left
初始化为l-1
(小于key的区间初始为空)right
初始化为r+1
(大于key的区间初始为空)- 遇到小于key的元素:
swap(nums[++left], nums[i++])
,i可以++ - 遇到等于key的元素:
i++
- 遇到大于key的元素:
swap(nums[--right], nums[i])
,i不动
为什么遇到大于key时i不动?
- 因为从right换过来的元素是未判断过的
- 必须在下一轮循环中重新判断
快速选择算法
核心思想: 不需要完全排序,只找目标元素
cpp
// 快速排序:递归左右两边
qsort(nums, l, left);
qsort(nums, right, r);
// 快速选择:只递归一边
if(target在左边) quickSelect(nums, l, left, target);
else if(target在中间) return key;
else quickSelect(nums, right, r, target);
时间复杂度优化:
快速排序:n + n + n/2 + n/2 + n/4 + n/4 + ... = O(n log n)
快速选择:n + n/2 + n/4 + n/8 + ... ≈ 2n = O(n)
题目详解
1. 颜色分类 (LeetCode 75)
难度: Medium
耗时: 25分钟(看视频15分钟 + 自己写10分钟)
题目描述
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0、1 和 2 分别表示红色、白色和蓝色。
示例:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
解题思路
使用三指针法将数组分成四段:
left
:指向0序列的最右边,初始化为-1i
:当前遍历指针,初始化为0right
:指向2序列的最左边,初始化为n
代码实现
cpp
class Solution {
public:
void sortColors(vector<int>& nums) {
int n = nums.size();
int left = -1, right = n;
for(int i = 0; i < right;) { // 注意:循环条件里没有i++
if(nums[i] == 0) swap(nums[++left], nums[i++]);
else if(nums[i] == 1) i++;
else swap(nums[--right], nums[i]); // 这里i不能++
}
}
};
关键点
-
循环条件不能带i++
- ❌
for(int i = 0; i < right; i++)
- ✅
for(int i = 0; i < right;)
- 原因:遇到2时i不该动,但循环末尾的i++会强制让它移动
- ❌
-
为什么遇到0时i可以++,遇到2时不能++?
- 遇到0:
left+1
位置要么是1(已判断),要么是i自己,交换后一定正确 - 遇到2:
right-1
位置是未判断过的,交换后必须重新判断
- 遇到0:
-
循环条件为什么是
i < right
?- right表示2序列的左边界
- 当i碰到right时,待处理区间为空,所有元素都已处理完毕
2. 快速排序 (LeetCode 912) ⭐⭐
难度: Medium
耗时: 30分钟(看题解 + 理解 + 敲代码)
题目描述
给你一个整数数组 nums,请你将该数组升序排列。
示例:
输入:nums = [5,2,3,1]
输出:[1,2,3,5]
解题思路
使用快速排序的分治思想:
- 随机选择基准元素(避免最坏情况)
- 数组分三块:< key、== key、> key
- 递归排序左右两部分,中间相等部分不需要再排
代码实现
cpp
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
srand(time(NULL)); // 种下随机数种子
qsort(nums, 0, nums.size() - 1);
return nums;
}
void qsort(vector<int>& 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]);
}
// [l, left], [left+1, right-1], [right, r]
qsort(nums, l, left); // 递归排序左边
qsort(nums, right, r); // 递归排序右边
}
int getRandom(vector<int>& nums, int left, int right) {
int r = rand();
return nums[r % (right - left + 1) + left];
}
};
关键点
-
srand(time(NULL)) 的作用
- 设置随机数种子,让每次运行生成不同的随机数
- 如果不设置,
rand()
每次都会生成相同的序列
-
随机选择基准元素
cppint r = rand(); // 生成随机数 return nums[r % (right - left + 1) + left]; // 映射到区间
r % (right - left + 1)
:映射到[0, right-left]
+ left
:偏移到[left, right]
-
时间复杂度
- 平均/最好:O(n log n)
- 最坏:O(n²)(但随机基准使其概率极低)
-
递归出口的理解
cppif(l >= r) return;
l == r
:区间只有1个元素,已有序l > r
:区间为空(left初始化为l-1,可能保持不变)
-
为什么会出现 l > r?
left
初始化为l-1
,表示"小于key的区间"初始为空- 如果所有元素都 >= key,left保持为 l-1
- 递归调用
qsort(nums, l, l-1)
就是 l > r
3. 数组中的第K个最大元素 (LeetCode 215) ⭐⭐⭐
难度: Medium
耗时: 40分钟(理解算法 + 调试)
题目描述
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
示例:
输入: [3,2,1,5,6,4], k = 2
输出: 5
解题思路
使用快速选择算法,基于快速排序的分三段思想:
- 将数组分三块:
[<key]
[==key]
[>key]
- 根据每段元素个数判断第k大落在哪段
- 只递归目标区间,不需要完全排序
关键转换: 从大到小看,右边区间c是最大的c个元素
代码实现
cpp
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
srand(time(NULL));
return qsortselk(nums, 0, nums.size() - 1, k);
}
int qsortselk(vector<int>& nums, int l, int r, int k) {
if(l >= r) return nums[l];
// 数组分三块
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]);
}
// 三个区间:[l, left], [left+1, right-1], [right, r]
int a = left - l + 1; // 左边区间(小于key)
int b = (right - 1) - (left + 1) + 1; // 中间区间(等于key)
int c = r - right + 1; // 右边区间(大于key)
if(c >= k) return qsortselk(nums, right, r, k);
else if((b + c) >= k) return key;
else return qsortselk(nums, l, left, k - b - c);
}
int getRandom(vector<int>& nums, int left, int right) {
int r = rand();
return nums[r % (right - left + 1) + left];
}
};
关键点
-
快速选择 vs 快速排序
- 快速排序:每次递归左右两边,O(n log n)
- 快速选择:每次只递归一边,O(n)
-
判断逻辑(从大到小角度)
从大到小看: 右边c个:最大的c个元素 中间b个:等于key 左边a个:最小的a个元素 判断第k大: 1. c >= k: 第k大在右边 2. c < k <= c+b: 第k大就是key 3. k > c+b: 第k大在左边的第(k-b-c)大
-
时间复杂度分析
第1次:处理n个元素 第2次:处理n/2个元素 第3次:处理n/4个元素 ... 总时间:n + n/2 + n/4 + ... ≈ 2n = O(n)
-
易错点
- 递归调用忘记return
- 函数参数不用引用(会超时)
- 右边区间个数计算错误
4. 最小的K个数 (剑指Offer 40) ⭐⭐
难度: Medium
耗时: 35分钟(理解题意 + 调试)
题目描述
设计一个算法,找出数组中最小的k个数,以任意顺序返回这k个数均可。
示例:
输入:arr = [1,3,5,7,2,4,6,8], k = 4
输出:[1,2,3,4](顺序可以不同)
解题思路
使用快速选择算法,和上一题类似,但目标不同:
- 上一题:找第K大的元素(1个值)
- 这一题:找最小的K个数(K个值)
核心:用partition将最小的k个数移到前k个位置,最后取前k个。
代码实现
cpp
class Solution {
public:
vector<int> smallestK(vector<int>& arr, int k) {
if(k == 0) return {};
srand(time(NULL));
quickSelect(arr, 0, arr.size() - 1, k);
// 取前k个元素(已经是最小的k个了)
vector<int> ret(arr.begin(), arr.begin() + k);
return ret;
}
void quickSelect(vector<int>& arr, int l, int r, int k) {
if(l >= r) return;
// 数组分三块
int key = arr[getRandom(arr, l, r)];
int i = l, left = l - 1, right = r + 1;
while(i < right) {
if(arr[i] < key) swap(arr[++left], arr[i++]);
else if(arr[i] == key) i++;
else swap(arr[--right], arr[i]);
}
// 判断第k小在哪个区间
int a = left - l + 1; // 左边区间(小于key)
int b = right - left - 1; // 中间区间(等于key)
if(k <= a) quickSelect(arr, l, left, k);
else if(k <= a + b) return;
else quickSelect(arr, right, r, k - a - b);
}
int getRandom(vector<int>& arr, int left, int right) {
int r = rand();
return left + r % (right - left + 1);
}
};
关键点
-
与"第K大元素"的区别
题目 目标 返回类型 递归函数返回 最后操作 第K大元素 找1个元素的值 int
return key;
直接返回值 最小K个数 找K个元素 vector<int>
return;
取前k个 -
为什么递归函数是void?
- 快速选择会原地修改数组
- 把最小的k个数移到前k个位置
- 不需要返回值,只需要调整位置
-
判断逻辑(从小到大角度)
从小到大看: 左边a个:最小的a个元素 中间b个:等于key 右边c个:最大的c个元素 判断第k小: 1. k <= a: 第k小在左边 2. a < k <= a+b: 第k小是key,前k个已经是最小的k个 3. k > a+b: 第k小在右边的第(k-a-b)小
-
执行过程示例
数组: [3,2,1,5,6,4], k=4 选key=4,划分: [3,2,1] [4] [5,6] a=3 b=1 c=2 判断:k=4, a+b=4,前4个已经是最小的4个 取前4个 → [3,2,1,4] ✅
核心收获
1. 掌握快速排序的核心思想
三指针法(荷兰国旗):
- 将数组分成
[<key]
[==key]
[>key]
三块 left
初始化为l-1
,right
初始化为r+1
- 遇到小于key:
swap(nums[++left], nums[i++])
- 遇到等于key:
i++
- 遇到大于key:
swap(nums[--right], nums[i])
,i不动
随机选择基准:
- 避免有序数组导致的O(n²)退化
- 使最坏情况概率降到几乎为0
递归结构:
cpp
qsort(nums, l, left); // 递归左边
qsort(nums, right, r); // 递归右边
// 中间相等部分不需要递归
2. 理解快速选择算法
核心优化:
- 不需要完全排序,只找目标元素
- 每次只递归一边,而不是两边都递归
- 时间复杂度从O(n log n)优化到O(n)
时间复杂度分析:
快速排序:n + n + n/2 + n/2 + ... = O(n log n)
快速选择:n + n/2 + n/4 + ... ≈ 2n = O(n)
3. 区分两类问题
第K大元素(返回1个值):
cpp
int qsortselk(...) {
if(c >= k) return qsortselk(...); // 返回递归结果
else if((b+c) >= k) return key; // 返回找到的值
else return qsortselk(...);
}
最小K个数(返回K个值):
cpp
void quickSelect(...) {
if(k <= a) quickSelect(...); // 只是递归调用
else if(k <= a+b) return; // 直接结束
else quickSelect(...);
}
// 最后取前k个:vector<int> ret(arr.begin(), arr.begin() + k);
4. 递归出口的重要性
为什么用 >=
而不是 ==
?
cpp
if(l >= r) return;
l == r
:区间只有1个元素,已有序l > r
:区间为空(left初始化为l-1,可能保持不变)
为什么会出现 l > r?
left
初始化为l-1
,表示"小于key的区间"初始为空- 如果所有元素都 >= key,left保持为 l-1
- 递归调用
qsort(nums, l, l-1)
就是 l > r
5. 分治算法的精髓
分而治之,各个击破:
- 将大问题分解为小问题
- 递归解决小问题
- 合并(或不需要合并)得到答案
快排的分治:
- 分:选基准,数组分三块
- 治:递归排序左右
- 合:无需合并(原地排序)
易错点总结
1. 循环条件带i++导致错误
错误写法:
cpp
for(int i = 0; i < right; i++) { // ❌
if(nums[i] == 0) swap(nums[++left], nums[i++]);
else if(nums[i] == 1) i++;
else swap(nums[--right], nums[i]);
}
正确写法:
cpp
for(int i = 0; i < right;) { // ✅
if(nums[i] == 0) swap(nums[++left], nums[i++]);
else if(nums[i] == 1) i++;
else swap(nums[--right], nums[i]); // i不动
}
原因: 遇到2时i不该动,但循环末尾的i++会强制让它移动。
2. 递归调用忘记return
错误写法:
cpp
if(c >= k) qsortselk(nums, right, r, k); // ❌ 缺少return
正确写法:
cpp
if(c >= k) return qsortselk(nums, right, r, k); // ✅
原因: 函数返回类型是int,必须返回值。
3. 函数参数不用引用
错误写法:
cpp
int qsortselk(vector<int> nums, int l, int r, int k) // ❌
正确写法:
cpp
int qsortselk(vector<int>& nums, int l, int r, int k) // ✅
原因: 值传递会每次递归都复制数组,导致超时。
4. 区间大小计算错误
错误写法:
cpp
int b = right - left; // ❌ 多算了1个
int c = (r - right + 1) + 1; // ❌ 多加了一个+1
正确写法:
cpp
int b = right - left - 1; // ✅ 中间区间:[left+1, right-1]
int c = r - right + 1; // ✅ 右边区间:[right, r]
5. 忘记边界情况
错误写法:
cpp
vector<int> smallestK(vector<int>& arr, int k) {
quickSelect(arr, 0, arr.size() - 1, k); // ❌ k=0时出错
...
}
正确写法:
cpp
vector<int> smallestK(vector<int>& arr, int k) {
if(k == 0) return {}; // ✅ 处理k=0
quickSelect(arr, 0, arr.size() - 1, k);
...
}
6. 混淆返回类型
第K大元素(返回int):
cpp
int qsortselk(...) {
if(c >= k) return qsortselk(...); // 需要返回值
else if((b+c) >= k) return key; // 返回具体的值
else return qsortselk(...);
}
最小K个数(返回void):
cpp
void quickSelect(...) {
if(k <= a) quickSelect(...); // 不需要返回值
else if(k <= a+b) return; // 只是结束递归
else quickSelect(...);
}
总结
通过这4道题,系统掌握了快速排序和快速选择算法:
- 基础:三指针法(荷兰国旗问题)
- 核心:快速排序(随机基准 + 分三块)
- 优化:快速选择(从O(n log n)到O(n))
- 应用:灵活处理不同返回类型的问题
分治算法的精髓在于分而治之,各个击破,将大问题分解为小问题,递归解决。快速排序和归并排序是分治算法的两大经典应用,掌握它们对理解分治思想至关重要。
下一步将继续学习归并排序及其应用(如逆序对问题),进一步深化对分治算法的理解。