分治算法_快速排序专题总结-----分治

分治算法-快速排序专题总结:分治

刷题记录

  • 刷题周期: 1天(10.16)
  • 完成题量: 4题
  • 通过情况: 4/4 AC

目录


刷题过程

分治算法-快速排序专题集中刷题,从基础到进阶:

  • 基础应用:颜色分类(三指针法)
  • 核心算法:快速排序(分三块+随机基准)
  • 算法优化:快速选择(从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序列的最右边,初始化为-1
  • i:当前遍历指针,初始化为0
  • right:指向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不能++
        }
    }
};
关键点
  1. 循环条件不能带i++

    • for(int i = 0; i < right; i++)
    • for(int i = 0; i < right;)
    • 原因:遇到2时i不该动,但循环末尾的i++会强制让它移动
  2. 为什么遇到0时i可以++,遇到2时不能++?

    • 遇到0:left+1位置要么是1(已判断),要么是i自己,交换后一定正确
    • 遇到2:right-1位置是未判断过的,交换后必须重新判断
  3. 循环条件为什么是i < right

    • right表示2序列的左边界
    • 当i碰到right时,待处理区间为空,所有元素都已处理完毕

2. 快速排序 (LeetCode 912) ⭐⭐

难度: Medium
耗时: 30分钟(看题解 + 理解 + 敲代码)

题目描述

给你一个整数数组 nums,请你将该数组升序排列。

示例:

复制代码
输入:nums = [5,2,3,1]
输出:[1,2,3,5]
解题思路

使用快速排序的分治思想:

  1. 随机选择基准元素(避免最坏情况)
  2. 数组分三块:< key、== key、> key
  3. 递归排序左右两部分,中间相等部分不需要再排
代码实现
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];
    }
};
关键点
  1. srand(time(NULL)) 的作用

    • 设置随机数种子,让每次运行生成不同的随机数
    • 如果不设置,rand() 每次都会生成相同的序列
  2. 随机选择基准元素

    cpp 复制代码
    int r = rand();  // 生成随机数
    return nums[r % (right - left + 1) + left];  // 映射到区间
    • r % (right - left + 1):映射到 [0, right-left]
    • + left:偏移到 [left, right]
  3. 时间复杂度

    • 平均/最好:O(n log n)
    • 最坏:O(n²)(但随机基准使其概率极低)
  4. 递归出口的理解

    cpp 复制代码
    if(l >= r) return;
    • l == r:区间只有1个元素,已有序
    • l > r:区间为空(left初始化为l-1,可能保持不变)
  5. 为什么会出现 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
解题思路

使用快速选择算法,基于快速排序的分三段思想:

  1. 将数组分三块:[<key] [==key] [>key]
  2. 根据每段元素个数判断第k大落在哪段
  3. 只递归目标区间,不需要完全排序

关键转换: 从大到小看,右边区间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];
    }
};
关键点
  1. 快速选择 vs 快速排序

    • 快速排序:每次递归左右两边,O(n log n)
    • 快速选择:每次只递归一边,O(n)
  2. 判断逻辑(从大到小角度)

    复制代码
    从大到小看:
    右边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)大
  3. 时间复杂度分析

    复制代码
    第1次:处理n个元素
    第2次:处理n/2个元素
    第3次:处理n/4个元素
    ...
    总时间:n + n/2 + n/4 + ... ≈ 2n = O(n)
  4. 易错点

    • 递归调用忘记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);
    }
};
关键点
  1. 与"第K大元素"的区别

    题目 目标 返回类型 递归函数返回 最后操作
    第K大元素 找1个元素的值 int return key; 直接返回值
    最小K个数 找K个元素 vector<int> return; 取前k个
  2. 为什么递归函数是void?

    • 快速选择会原地修改数组
    • 把最小的k个数移到前k个位置
    • 不需要返回值,只需要调整位置
  3. 判断逻辑(从小到大角度)

    复制代码
    从小到大看:
    左边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)小
  4. 执行过程示例

    复制代码
    数组: [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-1right 初始化为 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道题,系统掌握了快速排序和快速选择算法:

  1. 基础:三指针法(荷兰国旗问题)
  2. 核心:快速排序(随机基准 + 分三块)
  3. 优化:快速选择(从O(n log n)到O(n))
  4. 应用:灵活处理不同返回类型的问题

分治算法的精髓在于分而治之,各个击破,将大问题分解为小问题,递归解决。快速排序和归并排序是分治算法的两大经典应用,掌握它们对理解分治思想至关重要。

下一步将继续学习归并排序及其应用(如逆序对问题),进一步深化对分治算法的理解。

相关推荐
前进之路93 小时前
Leetcode每日一练--35
算法·leetcode
董建光d3 小时前
【深度学习】目标检测全解析:定义、数据集、评估指标与主流算法
深度学习·算法·目标检测
赵杰伦cpp3 小时前
list的迭代器
开发语言·数据结构·c++·算法·链表·list
~~李木子~~4 小时前
机器学习集成算法实践:装袋法与提升法对比分析
人工智能·算法·机器学习
微笑尅乐4 小时前
三种思路彻底掌握 BST 判断(递归与迭代全解析)——力扣98.验证二叉搜索树
算法·leetcode·职场和发展
闻缺陷则喜何志丹4 小时前
【动态规划】数位DP的原理、模板(封装类)
c++·算法·动态规划·原理·模板·数位dp
豆沙沙包?4 小时前
2025年--Lc194-516. 最长回文子序列(动态规划在字符串的应用,需要二刷)--Java版
java·算法·动态规划
胖咕噜的稞达鸭4 小时前
二叉树搜索树插入,查找,删除,Key/Value二叉搜索树场景应用+源码实现
c语言·数据结构·c++·算法·gitee
showmethetime4 小时前
基于相空间重构的混沌时间序列预测MATLAB实现
算法