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

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

刷题记录

  • 刷题周期: 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-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道题,系统掌握了快速排序和快速选择算法:

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

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

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

相关推荐
倒头就睡的小比特5 天前
算法竞赛C++常用的STL
c++·算法
小羊没烦恼!5 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
猎头南楼5 天前
知识社区推荐系统实践:新用户冷启动与长短期兴趣建模的挑战 资深推荐算法工程师
人工智能·深度学习·算法·机器学习
旖旎夜光5 天前
力控面试题 01.01: 判定字符是否唯一(位运算) —— 题解
c++·学习·算法·leetcode·力控
wzdark5 天前
大规模并行计算中的负载均衡算法研究4
算法
Because_of_Her15 天前
并查集-听课笔记
笔记·算法·并查集
码流子5 天前
高速公路安全监测实践:碰撞监测预警+物联网底座,从感知到处置的闭环
大数据·人工智能·物联网·算法·架构
another heaven5 天前
【算法/C++ MD5算法能否逆解码?原理、C++实现与同类哈希算法对比】
c++·算法·哈希算法
wzdark5 天前
从算法设计模式看编程思维的抽象能力4
算法
2601_962218615 天前
万象生鲜系统称重自动多退少补算法解决生鲜非标品痛点
大数据·数据库·人工智能·python·算法