LeetCode 215: 数组中的第K个最大元素 (快速排序) —— 题解

👋 欢迎阅读

🔥愿旖旎 · 个人主页

📘学习专栏: 《算法专栏》《LangChain学习》《贪心算法》

🌄 钱塘江上潮信来,今日方知我是我

✨当前学习内容: 《快速排序》

一.题目

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

🎯 欢迎来到「数组中的第K个最大元素」题解之旅! 本文将带你从"在一堆数里找出第 K 大的那个"这一直观场景出发,深入理解快速选择(随机基准 + 三路划分) 的巧妙运用,并掌握如何按分区大小定位 k 且只递归一侧 来在平均 O(n) 内找到第 K 大元素。

在开始之前,建议你先:

  • 了解题目背景 :这是 LeetCode 215 题,给定整数数组 nums 和整数 k,返回第 k 大的元素 (按降序数第 k 个)。本质上,不必把整个数组排好序 ,只需确定 k 落在划分后的哪一段,问题转化为快速选择(Quick Select)。

  • 明确学习目标 :掌握三路划分(大于 / 等于 / 小于) 与随机基准防退化 ,理解 leftSize 与 midSize 的定位逻辑 ,并熟练处理重复元素 与右区 k 值换算等边界情况。

  • 准备好环境 :建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如 nums = [3,2,1,5,6,4],k = 2 输出 5)。

本文将从问题转化、三路划分、k 值定位、随机化 到代码实现,层层递进。即使你对快速选择 还不熟悉,我们也会从"大的扔左边、小的扔右边,看 k 落在哪一堆"这一直觉出发,让你轻松抓住核心思想 ------分三堆,k 在哪堆就只钻哪堆。现在,让我们一起随机选基准、三路划分,找到第 K 大的元素吧! 🎯🔍

二、算法分析

1. 问题分析(前置分析)

  • 题目要求:返回 nums 中第 k 大 的元素(降序第 k 个,k 从 1 开始计数)。
  • 关键约束:n 可达 10⁵;k 合法(1 ≤ k ≤ n);元素可含大量重复;要求比全排序更高效。
  • 核心思路:全排序是"把所有元素排好",找第 k 大只需知道它落在哪一段 ------快速选择每次划分后只递归 k 所在的那一侧,平均 O(n)。

📌 例子:为什么不必全排序

nums = [3,2,1,5,6,4],k = 2(第 2 大是 5):全排序需把 6 个数排成 [6,5,4,3,2,1],代价 O(n log n);而快速选择一次划分 就能确认"比 4 大的有 2 个",k = 2 正好落在左区,只钻这一侧 ,其余元素完全不用处理------"选择"省掉了"排序"中不需要的那部分工作。

2. 算法策略(随机基准 + 三路划分 + 单侧递归)

核心步骤:

  1. 随机选基准 :key = nums[rand() % 区间长度 + l],入口 srand(time(NULL)) 调用一次即可。
  2. 三路划分(降序) :left = l-1、right = r+1、i = l;nums[i] > key 换到左区、nums[i] == key 跳过、nums[i] < key 换到右区。
  3. 统计三区大小 :左区(全 > key)长度 leftSize = left - l + 1;中区(全 == key)长度 midSize = right - left - 1。
  4. 按 k 定位:
    • k <= leftSize → 目标在左区,递归 [l, left],k 不变;
    • k <= leftSize + midSize → 目标在中区(全是 key),直接返回 key;
    • 否则 → 目标在右区,递归 [right, r],k 减去 leftSize + midSize。
  5. 返回:递归单侧收敛到答案。

📊 示例 (nums = [3,2,1,5,6,4],k = 2,基准 key = 4):

步骤 i numsi 与 key=4 比较 操作 left right
初始 0 3 3 < 4 换到右区(--right = 5) -1 5
① 0 4 4 == 4 跳过(i++) -1 5
② 1 2 2 < 4 换到右区(--right = 4) -1 4
③ 1 6 6 > 4 换到左区(++left = 0,i++) 0 4
④ 2 1 1 < 4 换到右区(--right = 3) 0 3
⑤ 2 5 5 > 4 换到左区(++left = 1,i++) 1 3
结束 3 --- i == right 划分完成 1 3

划分后数组为 [6,5,4,1,2,3]:左区 [0,1] = [6,5](leftSize = 2)、中区 [2,2] = [4](midSize = 1)、右区 [3,5] = [1,2,3]。k = 2 ≤ leftSize = 2 → 递归左区 [0,1],最终得到第 2 大 = 5 ✅。

3. 正确性说明(简单版本)

  • 划分不变量 :循环每轮保持 [l, left] 全 > key、[left+1, right-1] 全 == key、[right, r] 全 < key,三个分支各做一次合法转移,不破坏不变量。
  • 定位正确 :左区有 leftSize 个元素都比 key 大,所以第 k 大若 k ≤ leftSize 必在左区 ;若 k 落在中区,说明第 k 大就是 key 本身(中区全是相等的 key),可直接返回 ;否则 k' = k - leftSize - midSize 才是它在右区内的排名。
  • k 换算正确 :右区元素都 < key,它们前面已有 leftSize + midSize 个更大的元素,故右区内找第 k - leftSize - midSize 大,换算不漏不错。
  • 随机化保证期望性能 :随机基准使划分期望对半,递归深度期望 O(log n)、总代价期望 O(n);极小概率退化为 O(n²)。

📌 例子:为什么"中区可以直接返回 key"

nums = [3,3,2,1],k = 2,取 key = 3:划分后左区为空(leftSize = 0)、中区 [3,3](midSize = 2)、右区 [2,1]。k = 2 ≤ 0 + 2,说明第 2 大正好是 key = 3(降序为 [3,3,2,1],第 2 个就是 3)✅------中区可能包含多个相同值,k 落在这里时答案就是 key,无需继续递归,这也是三路划分能高效处理重复元素的原因。

4. 实现细节(边界防护)

  • 初始化:srand(time(NULL)) 在 findKthLargest 入口调用一次 ;qsort(nums, 0, n-1, k),k 为 1 起始的排名。
  • 边界防护:left = l-1、right = r+1 为虚拟边界 (++left / --right 首次落在 l / r);getRandom 中 rand() % (r-l+1) + l 保证基准落在 [l, r];l == r 时区间长度 1,取模后仍落在该位置,不会越界 ;k 始终满足 1 ≤ k ≤ 当前区间长度,故递归不会进入空区间。
  • 复杂度:平均 O(n) 、最坏 O(n²)(随机化后概率极低);空间为期望 O(log n) 的递归栈(最坏 O(n))。
  • 关键操作 :int key = getRandom(nums, l, r);(随机基准)、swap(nums[++left], nums[i++]) / swap(nums[--right], nums[i])(左右分区)、k - leftSize - midSize(右区排名换算)。

📌 例子:虚拟边界与单元素区间

nums = [1]、k = 1:l = r = 0,key = nums[0] = 1;left = -1、right = 1;nums[0] == key → i = 1,此时 i < right(1 < 1)不成立,循环结束。leftSize = -1 - 0 + 1 = 0、midSize = 1 - (-1) - 1 = 1,k = 1 ≤ 0 + 1 → 返回 key = 1 ✅。虚拟边界让"先自增/自减再使用"永远落在合法下标,单元素区间也无需特判。

5. 返回值(目标映射)

  • 返回 第 k 大的元素值 :若落在中区直接返回 key,否则递归收敛后返回该值,对应题目"返回第 k 大的元素"。

三.代码

cpp 复制代码
class Solution
{
public:
    int findKthLargest(vector<int>& nums, int k)
    {
        int n = nums.size();
        srand(time(NULL));                  // 随机种子只需设置一次
        return qsort(nums, 0, n - 1, k);    // k 从 1 开始计数
    }

    // 快速选择:在 [l, r] 内找第 k 大
    int qsort(vector<int>& nums, int l, int r, int k)
    {
        // 1. 随机选基准,避免有序输入退化为 O(n²)
        int key = getRandom(nums, l, r);

        // 2. 三路划分(降序):大于 key 归左区、等于留中区、小于归右区
        int i = l;
        int left = l - 1;                   // 左区右边界(虚拟位置)
        int 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. 划分结果:[l, left] 为左区、[left+1, right-1] 为中区、[right, r] 为右区
        int leftSize = left - l + 1;        // 比 key 大的元素个数
        int midSize = right - left - 1;     // 等于 key 的元素个数

        // 4. 按 k 落在哪一区决定去留
        if (k <= leftSize)
        {
            return qsort(nums, l, left, k);                     // 左区找第 k 大
        }
        else if (k <= leftSize + midSize)
        {
            return key;                                         // 落在中区:答案就是 key
        }
        else
        {
            return qsort(nums, right, r, k - leftSize - midSize);   // 右区:换算新的排名
        }
    }

    // 随机取 [l, r] 内的一个元素值作为基准
    int getRandom(vector<int>& nums, int l, int r)
    {
        int q = rand();
        return nums[q % (r - l + 1) + l];
    }
};

四、易错点分析

难点1:三路划分的方向必须与"第 k 大"对应

cpp 复制代码
if (nums[i] > key)
{
    swap(nums[++left], nums[i++]);     // 大于 key 放左边
}
else
{
    swap(nums[--right], nums[i]);      // 小于 key 放右边
}

本题要第 k 大 ,所以数组应按降序 分区:> key 的元素必须放左区 (更大在前)。若沿用升序模板(< key 放左),leftSize 就变成"比 key 小的个数 ",k <= leftSize 的判定含义完全不同,定位会全部反掉 (可能返回第 k 小而不是第 k 大)。方向与目标语义必须严格对齐,这是本题最容易犯的方向性错误。

难点2:< key 分支中 i 不能前进

cpp 复制代码
else
{
    swap(nums[--right], nums[i]);    // 注意:没有 i++
}

--right 处换到 i 位置的是尚未检查 的元素,可能大于 key、可能等于,因此必须原地重查 ;若写成 swap(nums[--right], nums[i++]),这个换入元素会被直接跳过 ,若它大于 key 就会滞留在右区,分区错误 。这与 75 题"2 分支不前进"是同一个道理------"右区换入未检查,必须停留"。

难点3:右区递归的 k 必须减去左区与中区

cpp 复制代码
return qsort(nums, right, r, k - leftSize - midSize);

右区元素全都 < key,它们前面已有 leftSize + midSize 个更大的元素;因此在右区内要找的是"全局第 k 大在右区内的排名 ",即 k - (leftSize + midSize)。若漏减(直接传 k),递归会重复计算前面那些更大的元素 ,导致定位偏移、答案错误。三个分支中,只有右区需要换算 k。

难点4:leftSize / midSize 的公式与区间衔接

cpp 复制代码
int leftSize = left - l + 1;
int midSize = right - left - 1;

left 是左区最右元素 的下标(左区为 [l, left]),故 leftSize = left - l + 1;中区为 [left+1, right-1],故 midSize = (right-1) - (left+1) + 1 = right - left - 1。若把 midSize 误写成 right - left,就会把 right 处的元素也算进中区 (它实际属于右区),导致 k 的定位差一,答案偏移。

难点5:随机基准------srand 只调一次,且返回的是"值"

cpp 复制代码
int getRandom(vector<int>& nums, int l, int r)
{
    int q = rand();
    return nums[q % (r - l + 1) + l];   // 返回元素"值",不是下标
}

getRandom 返回的是基准值 key (用于比较),而不是基准的下标 ------后续划分与 key 比较、中区直接 return key,都依赖它是值。若误改成返回下标,if (nums[i] > key) 就变成"拿值比下标",逻辑全错。另外 srand(time(NULL)) 应在入口调用一次 (每次递归都调会用同一秒的种子,失去随机性);q % (r-l+1) + l 映射到 [l, r],l == r 时区间长 1 仍安全,但 rand() % range 存在极轻微取模偏差,对本题无实际影响。

五、流程图

🎯 闭幕

🎉 恭喜你完成了「数组中的第K个最大元素」问题的学习!

为了巩固知识并进一步拓展,建议你:

🚀 动手实践 在 LeetCode 上提交代码,尝试不同的测试用例。

💡 深入思考

  • 本题采用三路划分 + 单侧递归 。请问为什么定位到 k 所在分区后只需递归一侧?与全排序的本质区别是什么?
  • 中区(全等于 key)可以直接返回 key 。请问为什么不用继续递归 ?用 nums = [3,3,2,1]、k = 2 说明。
  • 右区递归传的是 k - leftSize - midSize 。请问为什么必须减去这两个数量?漏减会得到什么?

如果你觉得本文对你有所帮助,欢迎:

👍 点赞 / 收藏 👤 关注作者,获取更多题解 💬 留言交流你的疑问或优化思路

📌 深入思考答案

  • 划分后三区大小已知:左区有 leftSize 个更大的、中区有 midSize 个相等的。k 落在哪一区就能确定答案所在区间 ,其余两区不可能包含第 k 大,故只需递归一侧;全排序则要把两侧都排好,代价 O(n log n),而这里期望 O(n)。
  • 中区内所有元素都等于 key ,它们的"大排名"连续且都等于同一个值------若 k 落在中区,说明第 k 大就是 key 本身,[3,3,2,1] 中 k=2 时答案正是 3,无需再细分;这也让重复元素不会导致退化。
  • 右区元素都小于 key,全局排名要跳过"左区 + 中区"共 leftSize + midSize 个更靠前的元素;故右区内应找第 k - leftSize - midSize 大。漏减会把前面那些更大的元素重复计入,定位偏移。

祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨

相关推荐
技术胖2 小时前
奎爷巡演 AIGC 视频教程 文档
算法
德彪稳坐倒骑驴3 小时前
Leetcode力扣刷题记录与题解
算法·leetcode·职场和发展
lzhdim9 小时前
AI的分类
人工智能·算法·机器学习·分类·数据挖掘
凤凰院凶涛QAQ12 小时前
《Java版数据结构 & 集合类剖析》Map & Set & 反射:“从哈希碰撞到红黑树转换,反射帮你扒开 HashMap 的桶数组看个究竟”
java·数据结构·算法
w_zero_one13 小时前
算法--模拟
java·数据结构·算法
外收内放14 小时前
面向对象高级(实战案例)
开发语言·python·学习
秋天的怀念_David14 小时前
算法一千零一夜 · 第十三夜 幻方,武侠世界里的数学迷宫
java·程序人生·算法
·present·14 小时前
CSAPP(lecture 4_01)
c语言·c++
FPGA小迷弟14 小时前
用 Claude Code 重构遗留系统:从评估到落地的完整实践指南
c++·python·ai·plotly·django·ai编程·claude