【算法题】快排

快速排序是基于分治思想 的经典算法,核心逻辑是"选基准→分区→递归处理子区间":通过将数组切分为"小于基准""等于基准""大于基准"的三个部分,逐步缩小排序范围,平均时间复杂度为 O(nlog⁡n)O(n\log n)O(nlogn)。除了基础排序,快排的"分区+选择"思想还能高效解决"找第K大元素""取最小的N个元素"等问题。本文通过4道经典题目,拆解快排思想在不同场景下的应用。

一、颜色分类

题目描述:

给定含 0、1、2(对应红、白、蓝)的数组,原地排序使相同颜色相邻,且按 0→1→2 顺序排列(不能用内置排序函数)。

示例

  • 输入:nums = [2,0,2,1,1,0],输出:[0,0,1,1,2,2]

解题思路:

这是快排三向分区思想的典型应用(荷兰国旗问题):

  1. 定义三个指针:
    • left0 的右边界(初始 -1,表示暂无非零元素);
    • right2 的左边界(初始 n,表示暂无2元素);
    • i:遍历数组的指针(初始 0)。
  2. 遍历过程:
    • nums[i] == 0:与 left+1 交换,left++i++(0的区域扩大);
    • nums[i] == 2:与 right-1 交换,right--(2的区域扩大,i不递增,需重新检查交换后的元素);
    • nums[i] == 1:直接 i++(1的区域自然保留)。

完整代码:

cpp 复制代码
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++;
            else    
                swap(nums[--right], nums[i]);
        }
    }
};

复杂度分析:

  • 时间复杂度:O(n)O(n)O(n),仅遍历数组一次。
  • 空间复杂度:O(1)O(1)O(1),原地修改数组。

二、排序数组

题目描述:

实现快速排序,将整数数组升序排列(不能用内置排序函数,要求时间复杂度 O(nlog⁡n)O(n\log n)O(nlogn))。

示例

  • 输入:nums = [5,2,3,1],输出:[1,2,3,5]

解题思路:

实现带随机基准+三向切分的快速排序(避免有序数组的最坏情况,同时高效处理重复元素):

  1. 随机选基准 :从当前区间随机选一个元素作为基准(避免有序数组下时间复杂度退化到 O(n2)O(n^2)O(n2))。
  2. 三向切分 :将数组分为"小于基准""等于基准""大于基准"的三个部分(用 left 标记小于区右边界,right 标记大于区左边界)。
  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 = getRand(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]);
        } 

        qsort(nums, l, left);   // 排序小于基准的区间
        qsort(nums, right, r); // 排序大于基准的区间
    }

    int getRand(vector<int>& nums, int left, int right) {
        int r = rand();
        return nums[left + r % (right - left + 1)];
    }
};

复杂度分析:

  • 时间复杂度:平均 O(nlog⁡n)O(n\log n)O(nlogn),最坏 O(n2)O(n^2)O(n2)(随机选基准后概率极低)。
  • 空间复杂度:O(log⁡n)O(\log n)O(logn),递归栈的深度(平均情况)。

三、数组中的第K个最大元素

题目描述:

找到数组中第 K 个最大的元素(要求时间复杂度 O(n)O(n)O(n))。

示例

  • 输入:nums = [3,2,1,5,6,4], k = 2,输出:5

解题思路:

利用快排的快速选择算法(无需完全排序,仅需找到目标元素所在的分区):

  1. 随机选基准并三向切分数组,得到"小于基准""等于基准""大于基准"的三个区间。
  2. 计算大于基准的区间长度 c等于基准的区间长度 b
    • c >= k:第K大元素在"大于基准"的区间,递归处理该区间;
    • b + c >= k:第K大元素就是基准(等于基准的区间包含目标);
    • 否则:第K大元素在"小于基准"的区间,递归处理该区间(需调整K为 k - b - c)。

完整代码:

cpp 复制代码
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];

        int key = getRand(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]);
        }

        int c = r - right + 1; // 大于基准的元素个数
        int b = right - 1 - (left + 1) + 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);
    }

    int getRand(vector<int>& nums, int l, int r) {
        return nums[l + rand() % (r - l + 1)];
    }
};

复杂度分析:

  • 时间复杂度:平均 O(n)O(n)O(n),最坏 O(n2)O(n^2)O(n2)(随机选基准后概率极低)。
  • 空间复杂度:O(log⁡n)O(\log n)O(logn),递归栈的深度。

四、库存管理III(取最小的cnt个元素)

题目描述:

给定库存数组 stock,返回库存最少的 cnt 个元素(顺序不限)。

示例

  • 输入:stock = [2,5,7,4], cnt = 1,输出:[2]

解题思路:

同样基于快速选择思想,只需找到"最小的cnt个元素"所在的分区:

  1. 随机选基准并三向切分数组,得到"小于基准""等于基准""大于基准"的三个区间。
  2. 计算小于基准的区间长度 a等于基准的区间长度 b
    • a > cnt:最小的cnt个元素在"小于基准"的区间,递归处理该区间;
    • a + b >= cnt:当前区间已包含最小的cnt个元素,无需继续递归;
    • 否则:最小的cnt个元素还包含"大于基准"区间的部分,递归处理该区间(调整cnt为 cnt - a - b)。
  3. 最终取数组前 cnt 个元素即可。

完整代码:

cpp 复制代码
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>& nums, int l, int r, int k) {
        if(l >= r) return;

        int key = getRand(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]);
        }

        int a = left - l + 1; // 小于基准的元素个数
        int b = right - left - 1; // 等于基准的元素个数
        if(a > k) qsort(nums, l, left, k);
        else if(a + b >= k) return;
        else qsort(nums, right, r, k - a - b);
    }

    int getRand(vector<int>& nums, int l, int r) {
        return nums[l + rand() % (r - l + 1)];
    }
};

复杂度分析:

  • 时间复杂度:平均 O(n)O(n)O(n),最坏 O(n2)O(n^2)O(n2)。
  • 空间复杂度:O(log⁡n)O(\log n)O(logn),递归栈的深度。
相关推荐
一起努力啊~15 小时前
算法刷题--长度最小的子数组
开发语言·数据结构·算法·leetcode
rchmin15 小时前
限流算法:令牌桶与漏桶详解
算法·限流
leoufung15 小时前
LeetCode 221:Maximal Square 动态规划详解
算法·leetcode·动态规划
黑符石15 小时前
【论文研读】Madgwick 姿态滤波算法报告总结
人工智能·算法·机器学习·imu·惯性动捕·madgwick·姿态滤波
源代码•宸15 小时前
Leetcode—39. 组合总和【中等】
经验分享·算法·leetcode·golang·sort·slices
好易学·数据结构15 小时前
可视化图解算法77:零钱兑换(兑换零钱)
数据结构·算法·leetcode·动态规划·力扣·牛客网
AlenTech16 小时前
226. 翻转二叉树 - 力扣(LeetCode)
算法·leetcode·职场和发展
Tisfy16 小时前
LeetCode 1458.两个子序列的最大点积:动态规划
算法·leetcode·动态规划·题解·dp
求梦82016 小时前
【力扣hot100题】合并区间(9)
算法·leetcode·职场和发展