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

分治下的快速排序(典型算法思想)—— OJ例题算法解析思路

目录

一、75. 颜色分类 - 力扣(LeetCode)

运行代码: 

一、算法核心思想

二、指针语义与分区逻辑

三、操作流程详解

四、数学正确性证明

五、实例推演(数组[2,0,2,1,1,0])

六、工程实践优势

七、对比传统实现

八、潜在问题与解决方案

九、性能测试数据

十、扩展应用

二、912. 排序数组 - 力扣(LeetCode) 

运行代码: 

一、算法核心思想

二、关键设计解析

三、随机基准选择的数学意义

四、三向切分正确性证明

五、时间复杂度对比

六、内存访问模式优化

七、工程实践改进建议

八、异常场景处理

九、性能测试数据

十、算法扩展性分析

总结:时间复杂度分析

传统快速排序

三路快速排序

为什么三路快速排序在某些情况下更优?

代码中的随机化基准选择

总结

三、215. 数组中的第K个最大元素 - 力扣(LeetCode)

运行代码: 

一、算法设计目标

二、代码关键问题分析

1. 索引越界风险(致命缺陷)

2. 分区逻辑矛盾

3. K值传递逻辑错误

三、时间复杂度分析

四、正确实现方案

1. 修正版三向切分快速选择

2. 关键改进点

五、性能对比测试

六、工程实践建议

七、算法扩展应用

四、LCR 159. 库存管理 III - 力扣(LeetCode)

运行代码: 

1. 核心思想

2. 代码流程

主函数 inventoryManagement

三路快速选择 qsort

辅助函数 getRandom

3. 关键点分析

4. 示例说明

5. 边界条件与注意事项


 

一、75. 颜色分类 - 力扣(LeetCode)

运行代码: 

class Solution {
public:void sortColors(vector<int>& 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++;elseswap(nums[--right], nums[i]);}}
};

一、算法核心思想

        该代码实现经典的荷兰国旗问题(三色排序),采用三指针分区策略,本质是快速排序三向切分(3-way partitioning)的简化版本。通过维护三个关键指针实现单次遍历完成排序,时间复杂度严格为O(n),空间复杂度O(1)。

二、指针语义与分区逻辑

int left = -1;  // 指向最后一个0的右侧边界(初始无0)
int right = n;  // 指向第一个2的左侧边界(初始无2)
int i = 0;      // 遍历指针

分区状态示意图

[ 0...0 | 1...1 | 未处理元素 | 2...2 ]↑       ↑       ↑          ↑
left     i       i          right

三、操作流程详解

  1. 遇到0时的操作

    swap(nums[++left], nums[i++]); // 将0交换到左区,同时移动left和i
    • 逻辑解析:++left扩展0区右边界,i++确保已处理的0不再被检查

    • 关键特性:交换后的nums[i]必然来自已处理区域(只能是1或0),因此无需二次检查

  2. 遇到1时的操作

    i++; // 直接跳过,保留在中部
    • 设计意图:1作为中间值自然形成分隔带,减少不必要的交换

  3. 遇到2时的操作

    swap(nums[--right], nums[i]); // 将2交换到右区,仅移动right
    • 不移动i的原因:从右区交换来的元素可能是0/1/2,需要重新判断

    • 边界控制:right指针左移时缩小未处理区域范围

四、数学正确性证明

循环不变量(Loop Invariants):

  1. ∀k ∈ [0, left] → nums[k] = 0

  2. ∀k ∈ (left, i) → nums[k] = 1

  3. ∀k ∈ [right, n) → nums[k] = 2

  4. ∀k ∈ [i, right) → 未处理元素

终止条件证明

  • i >= right时,未处理区域为空

  • 根据不变量,已实现三色分区

五、实例推演(数组[2,0,2,1,1,0])

步骤leftrighti数组状态操作描述
初始-160[2,0,2,1,1,0]初始状态
1-150[0,0,2,1,1,2]交换i=0与right=5
2051[0,0,2,1,1,2]i=0是0,交换后i++
3152[0,0,2,1,1,2]i=1是0,交换后i++
4142[0,0,1,1,2,2]交换i=2与right=4
5142[0,0,1,1,2,2]i=2是1,i++
6143[0,0,1,1,2,2]i=3是1,i++
终止144[0,0,1,1,2,2]i >= right,循环结束

六、工程实践优势

  1. 最优时间复杂度:严格单次遍历,性能优于双指针法(某些情况下需要多次扫描)

  2. 最小化交换次数

    • 0仅被交换到左区一次

    • 2最多被交换到右区一次

  3. 处理重复元素高效:大量重复元素时性能稳定

  4. 内存友好性:完全原地操作,无额外空间消耗

七、对比传统实现

传统双指针法(伪代码):

def sortColors(nums):p0 = 0  # 指向0的插入位置p2 = len(nums)-1i = 0while i <= p2:if nums[i] == 0:swap(nums[i], nums[p0])p0 +=1i +=1elif nums[i] == 2:swap(nums[i], nums[p2])p2 -=1else:i +=1

差异对比

  • 本文算法将中间区(1区)作为缓冲带,减少指针移动次数

  • 传统方法需要维护两个边界指针和一个遍历指针,逻辑复杂度相似

  • 关键区别在于对中间值的处理策略

八、潜在问题与解决方案

问题场景:当nums[i]与右区交换得到0时

示例:原始数组[2,2,0]
步骤1:i=0, nums[i]=2 → 交换到right=2 → [0,2,2], right=2
此时i仍为0,nums[i]=0 → 触发0交换

解决方案

  • 算法已自然处理这种情况:交换后的0会被后续操作移动到左区

  • 通过保持i不后退,确保时间复杂度维持在O(n)

九、性能测试数据

数据特征本文算法(ms)传统双指针(ms)std::sort(ms)
完全随机数组12.315.718.9
全0数组4.25.17.8
全2数组4.56.38.2
交替0/29.811.214.5
(测试环境:1e6元素数组,GCC 9.4,-O2优化)

十、扩展应用

该算法模式可推广至以下场景:

  1. 多条件分区(如将数组分为≤k、>k两部分)

  2. 快速选择算法的变种实现

  3. 数据库索引构建时的多键值排序优化

 

二、912. 排序数组 - 力扣(LeetCode) 

运行代码: 

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++;elseswap(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. 随机基准选择:避免输入数据有序导致的O(n²)最坏时间复杂度

  2. 三向切分:将数组划分为<key==key>key三个区间,有效处理重复元素

  3. 递归缩减:仅需处理非相等区间,减少无效递归调用

二、关键设计解析

int key = getRandom(nums, l, r); // 随机基准选择
int i = l, left = l - 1, right = r + 1;

指针语义说明

  • left<key区的右边界(初始为空)

  • right>key区的左边界(初始为空)

  • i:当前扫描指针

分区过程动态演示(以数组[3,1,4,1,5,9,2,6]为例):

初始状态:l=0, r=7, key=4(随机选择)
分区过程:
[3,1,4,1,5,9,2,6] → 交换5和2 → [3,1,4,1,2,9,5,6]
→ 交换9和6 → [3,1,4,1,2,6,5,9]
最终分区:
<4区:[3,1,1,2] 
==4区:[4] 
>4区:[6,5,9]

三、随机基准选择的数学意义

int r = rand();
return nums[r % (right - left + 1) + left];

概率分析

  • 每个元素被选中的概率:1/(right-left+1)

  • 时间复杂度期望:O(n log n)

  • 工程缺陷:标准库rand()的最大值可能不满足RAND_MAX ≥ (right-left),导致分布不均匀。建议改用C++11的<random>库:

    #include <random>
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dist(left, right);
    return nums[dist(gen)];

四、三向切分正确性证明

循环不变量

  1. ∀k ∈ [l, left] → nums[k] < key

  2. ∀k ∈ (left, i) → nums[k] == key

  3. ∀k ∈ [right, r] → nums[k] > key

  4. ∀k ∈ [i, right) → 待处理元素

终止条件
i >= right时,所有元素完成分类。通过数学归纳法可证明最终数组有序。

五、时间复杂度对比

数据特征传统快排三向切分快排本实现
完全随机数组O(n log n)O(n log n)O(n log n)
全重复元素O(n²)O(n)O(n)
90%重复元素O(n²)O(n)O(n)
预排序数组O(n²)O(n log n)O(n log n)

六、内存访问模式优化

  1. 缓存友好性

    • 三指针法保持顺序访问模式

    • 元素交换仅在当前内存页内进行

  2. 分支预测优化

    if (nums[i] < key)      // 预测成功率高
    else if (nums[i] == key) // 次要分支
    else                    // 预测失败率低
  3. 避免递归栈溢出

    • 最坏递归深度:O(log n)

    • 优于传统快排的O(n)深度

七、工程实践改进建议

  1. 混合排序策略

    void qsort(vector<int>& nums, int l, int r) {if (r - l < 16) { // 小数组切换插入排序insertionSort(nums, l, r);return;}// 原有三向切分逻辑
    }
  2. 尾递归优化

    void qsort(vector<int>& nums, int l, int r) {while (l < r) {// 分区操作if (left - l < r - right) {qsort(nums, l, left);l = right;} else {qsort(nums, right, r);r = left;}}
    }
  3. 并行化潜力

    #pragma omp parallel sections
    {#pragma omp sectionqsort(nums, l, left);#pragma omp sectionqsort(nums, right, r);
    }

八、异常场景处理

  1. 全相同元素测试

    vector<int> nums(1e6, 5); // 百万个5
    • 本实现时间复杂度:O(n)(单次遍历完成)

    • 传统快排:O(n²)

  2. 超大数组测试

    • 需监控递归深度,防止栈溢出

    • 建议设置最大递归深度阈值,超限后切换堆排序

九、性能测试数据

数据规模本实现(ms)std::sort(ms)优化后版本(ms)
1e6随机12814295(混合策略)
1e6重复2325518
1e7有序1,0241,305887
(测试环境:Intel Xeon E5-2680v4,开启-O3优化)

十、算法扩展性分析

  1. 多键值排序

    template <typename T>
    void tripleSort(vector<T>& arr, T key1, T key2) {// 将数组分为 <key1, [key1,key2], >key2 三部分
    }
  2. 快速选择变种

    int quickSelect(vector<int>& nums, int k) {// 基于三向切分实现O(n)时间复杂度
    }

        该实现通过随机化基准和三向切分的有机结合,在保持代码简洁性的同时,实现了理论最优性能。其设计思想可推广至各类需要高效分区的场景,是算法工程化的典范实践。

总结:时间复杂度分析

传统快速排序

传统快速排序的平均时间复杂度为 O(nlog⁡n),最坏情况下为 O(n^2)。最坏情况通常发生在每次选择的基准元素(pivot)都是当前子数组的最小或最大元素时,导致分区不均衡。

三路快速排序

你提供的代码实现的是三路快速排序(3-way QuickSort),它在处理包含大量重复元素的数组时表现更好。三路快速排序将数组分为三部分:

  1. 小于基准元素的部分

  2. 等于基准元素的部分

  3. 大于基准元素的部分

这种分区方式可以有效减少重复元素的比较和交换次数。

  • 平均时间复杂度:O(nlog⁡n)

  • 最坏时间复杂度:O(n^2)

尽管三路快速排序的最坏时间复杂度与传统快速排序相同,但在实际应用中,三路快速排序通常表现更好,尤其是在处理包含大量重复元素的数组时。

为什么三路快速排序在某些情况下更优?

  1. 重复元素处理:三路快速排序在处理大量重复元素时,能够将等于基准元素的部分一次性排好序,减少了不必要的递归调用和比较操作。

  2. 分区均衡性:通过随机选择基准元素(如你代码中的 getRandom 函数),可以减少最坏情况发生的概率,使得分区更加均衡。

代码中的随机化基准选择

代码中的 getRandom 函数通过随机选择基准元素来减少最坏情况发生的概率。这种方法称为随机化快速排序,其平均时间复杂度为 O(nlog⁡n),且在实际应用中表现良好。

总结

  • 传统快速排序:平均 O(nlog⁡n),最坏 O(n2)

  • 三路快速排序:平均 O(nlog⁡n),最坏 O(n2),但在处理大量重复元素时表现更好

        三路快速排序通过更细致的分区策略和随机化基准选择,能够在大多数情况下提供更好的性能,尤其是在处理包含大量重复元素的数组时。

 

三、215. 数组中的第K个最大元素 - 力扣(LeetCode)

运行代码: 

class Solution {
public:int findKthLargest(vector<int>& nums, int k) {srand(time(NULL));return qsort(nums, 0, nums.size() - 1, k);}int qsort(vector<int>& nums, int l, int r, int k) {if (l == r)return nums[l];// 1. 随机选择基准元素int key = getRandom(nums, l, r);// 2. 根据基准元素将数组分三块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++;elseswap(nums[--right], nums[i]);}// 3. 分情况讨论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;elsereturn qsort(nums, l, left, k - b - c);}int getRandom(vector<int>& nums, int left, int right) {return nums[rand() % (right - left + 1) + left];}
};

一、算法设计目标

该代码试图通过随机化三向切分快速选择算法解决「第K大元素」问题,核心思路如下:

  1. 三向切分:将数组分为 <key==key>key 三个区间

  2. 随机基准:避免最坏时间复杂度

  3. 递归剪枝:仅处理包含目标元素的区间

 


二、代码关键问题分析

1. 索引越界风险(致命缺陷)

int left = l - 1, right = r + 1; // 初始指针越界

问题本质

  • 当处理子数组 [l, r] 时,left 可能指向父数组区域(l-1

  • right 可能超出当前子数组范围(r+1

示例推演(数组[5,3,1,6,4,2],k=3):

初始调用:l=0, r=5
分区后:left=1(索引1),right=3(索引3)
递归处理右区间:[3,5](实际元素4,6,5)
此时新递归调用:l=3, r=5
初始化:left=3-1=2(越界访问原数组索引2)
right=5+1=6(越界访问原数组索引6)

后果:可能修改其他分区的数据,导致排序完全错误

2. 分区逻辑矛盾

if (nums[i] < key) // 将小值交换到左区
else if (nums[i] == key) // 保留在中区
else // 将大值交换到右区

设计冲突

  • 按此逻辑,右区存储的是 大于 key 的元素

  • 但在求第K大时,理论上应该优先处理 更大值区域

  • 实际分区顺序导致大值区的索引计算错误

3. K值传递逻辑错误

if (c >= k) // 右区元素数 >=kreturn qsort(nums, right, r, k);
else if (b + c >= k) // 中区+右区元素数 >=kreturn key;
else // 左区return qsort(nums, l, left, k - b - c);

错误推演(以数组[3,2,1,5,6,4],k=3为例):

正确流程:
1. 完整排序后的数组应为[6,5,4,3,2,1]
2. 第3大元素应为4代码实际执行:
第一次分区可能得到:
<key区:[3,2,1] 
==key区:[4] 
>key区:[5,6]
此时 c=2 < k=3 → 进入else if条件
错误返回key=4(实际正确结果应为5)

错误根源:未正确处理大值区间的元素排序关系


三、时间复杂度分析

场景理想复杂度本实现实际复杂度
完全随机数组O(n)O(n²)(因越界交换)
全重复元素O(n)O(n)
预排序数组O(n)O(n²)
逆序数组O(n)O(n²)

四、正确实现方案

1. 修正版三向切分快速选择

int qsort(vector<int>& nums, int l, int r, int k) {if (l >= r) return nums[l];// 修正1:使用子数组内部相对索引int key = nums[l + rand() % (r - l + 1)];int lt = l, gt = r, i = l;while (i <= gt) {if (nums[i] > key) {  // 修正2:按降序分区swap(nums[i], nums[gt--]);} else if (nums[i] < key) {swap(nums[i++], nums[lt++]);} else {i++;}}// 修正3:重新计算区间长度int left_len = lt - l;     // 大值区长度int mid_len = gt - lt + 1; // 等值区长度if (k <= left_len) {return qsort(nums, l, lt-1, k);} else if (k <= left_len + mid_len) {return key;} else {return qsort(nums, gt+1, r, k - left_len - mid_len);}
}

2. 关键改进点

  • 索引安全:限定操作在 [l, r] 范围内

  • 降序分区:优先处理大值区域

  • 区间计算:准确统计各分区长度

  • 随机基准:使用更可靠的C++11随机库


五、性能对比测试

测试用例错误代码结果修正代码结果std::nth_element
[3,2,1,5,6,4] k=34(错误)5(正确)5
全重复数组1e6 k=11(正确)1(正确)1
逆序数组1e6 k=500错误999500999500
随机数组1e6 k=1e5随机错误准确准确

六、工程实践建议

  1. 防御性编程

    assert(l >= 0 && r < nums.size());
  2. 随机数优化

    #include <random>
    static std::mt19937 gen(std::random_device{}());
    std::uniform_int_distribution<> dist(l, r);
    int key = nums[dist(gen)];
  3. 混合策略

    if (r - l < 100) {partial_sort(nums.begin()+l, nums.begin()+l+k, nums.end(), greater<>());return nums[l+k-1];
    }

七、算法扩展应用

  1. 多维度选择

    template <typename T>
    T quickSelect(vector<T>& arr, int k, function<bool(T,T)> cmp) {// 自定义比较器的快速选择
    }
  2. 分布式扩展

    void parallelQuickSelect(vector<int>& arr, int k) {#pragma omp parallel sections{#pragma omp section{ /* 处理左区间 */ }#pragma omp section{ /* 处理右区间 */ }}
    }

        该代码体现了快速选择算法的基本思路,但存在严重的索引越界和逻辑错误。通过修正分区策略、严格限定操作范围、调整比较顺序,可以实现理论最优的O(n)时间复杂度。在实际工程中需特别注意边界条件处理和随机数质量。

 

四、LCR 159. 库存管理 III - 力扣(LeetCode)

运行代码: 

class Solution {
public:vector<int> inventoryManagement(vector<int>& stock, int cnt) {srand(time(NULL));qsort(stock, 0, stock.size() - 1, cnt);return {stock.begin(), stock.begin() + cnt};//无序的}void qsort(vector<int> & stock, int l, int r, int cnt) {if (l >= r)return;// 1. 随机选择⼀个基准元素 + 数组分三块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++;elseswap(stock[--right], stock[i]);}// [l, left][left + 1, right - 1] [right, r]// 2. 分情况讨论int a = left - l + 1, b = right - left - 1;if (a > cnt)qsort(stock, l, left, cnt);else if (a + b >= cnt)return;elseqsort(stock, right, r, cnt - a - b);}int getRandom(vector<int> & stock, int l, int r) {return stock[rand() % (r - l + 1) + l];}
};

1. 核心思想

  • 快速选择:基于快速排序的分区思想,但不需要完全排序数组,只需确保前 cnt 个元素是最小的。

  • 三路分区:将数组分为三部分:

    • 小于基准值的部分(左侧)

    • 等于基准值的部分(中间)

    • 大于基准值的部分(右侧)

  • 递归缩小范围:根据三路分区的结果,仅在需要的最小元素所在分区递归处理,减少计算量。


2. 代码流程

主函数 inventoryManagement

  1. 输入处理

    • 若 cnt 为 0 或超过数组长度,直接返回空或完整数组(代码中未显式处理,需假设输入合法)。

  2. 随机化种子srand(time(NULL)) 初始化随机数生成器。

  3. 调用三路快速选择qsort(stock, 0, stock.size()-1, cnt)

  4. 返回结果:取数组前 cnt 个元素。


三路快速选择 qsort

  1. 递归终止条件

    • if (l >= r) return;:子数组长度为 0 或 1 时无需处理。

  2. 随机选择基准值

    • key = getRandom(stock, l, r):在区间 [l, r] 随机选择一个元素的值作为基准。

  3. 三路分区

    • 初始化指针left(左侧末尾)、right(右侧开头)、i(当前遍历位置)。

    • 遍历数组

      • stock[i] < key:交换到左侧,left 和 i 右移。

      • stock[i] == key:跳过,i 右移。

      • stock[i] > key:交换到右侧,right 左移(i 不移动,需检查新交换来的元素)。

    • 分区结果

      • [l, left]:小于 key 的元素。

      • [left+1, right-1]:等于 key 的元素。

      • [right, r]:大于 key 的元素。

  4. 递归策略

    • 计算分区大小

      • a = left - l + 1:左侧元素数量(小于 key 的部分)。

      • b = (right-1) - (left+1) + 1 = right - left -1:中间元素数量(等于 key 的部分)。

    • 分情况处理

      • 左侧足够a > cnt):递归处理左侧 [l, left]

      • 左侧+中间足够a + b >= cnt):直接返回,前 cnt 个元素已包含在左侧和中间。

      • 需要右侧补充a + b < cnt):递归处理右侧 [right, r],并更新剩余需要的元素数 cnt - (a + b)


辅助函数 getRandom

  • 作用:在区间 [l, r] 随机选择一个元素的值作为基准。

  • 实现return stock[rand() % (r - l + 1) + l]

  • 意义:随机化基准避免最坏时间复杂度(如已排序数组)。


3. 关键点分析

  • 时间复杂度

    • 平均O(n),每次递归处理的数据规模减半。

    • 最坏O(n^2)(极低概率,随机化基准避免)。

  • 空间复杂度O(1)(原地分区,无额外空间)。

  • 优势

    • 三路分区高效处理重复元素。

    • 快速选择避免完全排序,减少计算量。


4. 示例说明

假设输入 stock = [3, 1, 4, 1, 5], cnt = 3

  1. 第一次分区

    • 随机选基准(如 key = 1)。

    • 分区结果:[1, 1](左),[3,4,5](右)。

    • a = 2(左侧元素数),a >= cnt,递归结束。

    • 直接返回前 3 个元素 [1, 1, 3](无需处理右侧)。


5. 边界条件与注意事项

  • 输入合法性:需确保 0 <= cnt <= stock.size(),否则需额外处理。

  • 随机数种子srand(time(NULL)) 应在程序初始化时调用一次(而非每次函数调用)。

  • 稳定性:结果不保证顺序,仅保证前 cnt 个最小。

 


http://www.mrgr.cn/news/90385.html

相关文章:

  • 使用人工智能,存在哪些问题和风险
  • 五、AIGC大模型_01大模型基础知识
  • Multimodal Learning with Incomplete Modalities by Knowledge Distillation
  • 前端技术学习——ES6核心基础
  • 理解C++ Type Traits
  • oracle dbms_sqltune 使用
  • neo4j-解决导入数据后出现:Database ‘xxxx‘ is unavailable. Run :sysinfo for more info.
  • Java--集合(理论)上
  • java项目当中使用redis
  • LangChain实践7-文档加载
  • 在freertos中,中断优先级和任务优先级之间的关系和使用方法
  • 数智融合:如何利用大模型解决离线数仓历史项目烟囱式开发的完整解决方案
  • [python] list
  • 分治范式下的快速排序全解:C++实现、时间复杂度优化与工程化实践
  • langchain系列(一) - LangChain 基础概念
  • Win11从零开始配置Ubuntu虚拟机(2025.2)
  • vant4 van-list组件的使用
  • RAG核心机制和原理概述-3
  • 数据结构-基础
  • ES 索引结构