面试知识库

215. 数组中的第K个最大元素#

今天我们来解决一道堆的经典题目——LeetCode 215「数组中的第K个最大元素」。这道题在面试里出现频率非常高,因为它既能考察你对堆的理解,也能顺便看出你是否真的明白“Top K”类问题的本质。

📚 生活中的场景#

想象你在组织一场长跑比赛。比赛结束后,你不需要把所有选手从第一名到最后一名都完整排出来,你只想知道:

  • 第1名是谁
  • 第2名是谁
  • 第K名是谁

如果为了找第K名,把所有人成绩都完整排序一遍,当然可以做,但有点“大炮打蚊子”。

更聪明的做法是:

我们只维护当前前 K 名。

一旦有人成绩不够好,就直接淘汰;只有足够优秀的人,才能留在“前 K 名候选区”里。

这就是小顶堆的思维。

问题描述#

题目目标#

给定整数数组 nums 和整数 k ,请返回数组中第 k 个最大的元素。这里指的是排序后的第 k 个最大元素,而不是第 k 个不同的元素。

示例 1#

输入: nums = [3,2,1,5,6,4], k = 2 输出: 5 说明: 把输入和输出放在一起看,会更容易直接抓住题目要求的变化。

💡 问题定义#

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

注意:

  • 这里找的是“排序后的第 k 个最大元素”
  • 不是“第 k 个不同的元素”

例如:

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

🤔 最直接的思路#

最容易想到的方法就是:

  1. 先把数组整体排序
  2. 然后取排序后倒数第 k 个元素

代码很简单,但时间复杂度是 O(nlogn)。

这当然能过,但如果面试官继续追问:

有没有比完整排序更高效的方法? 这时就该轮到堆登场了。

🚀 解法一:小顶堆(面试最稳)#

核心思路#

我们维护一个大小为 k 的小顶堆:

  • 堆里始终保存“当前最大的 k 个元素”
  • 堆顶就是这 k 个元素里最小的那个
  • 也就是“当前第 k 大的元素”

遍历数组时:

  • 如果堆还没满,直接加入
  • 如果堆已满,且当前数字比堆顶大,说明它有资格进入前 k
    • 先弹出堆顶
    • 再加入当前数字
  • 如果当前数字不比堆顶大,直接忽略

遍历结束后,堆顶就是答案。

Java 代码#

📝 用例子走一遍#

以 nums = [3,2,1,5,6,4], k = 2 为例。

我们维护一个大小为 2 的小顶堆:

  1. 插入 3
    • 堆:[3]
  2. 插入 2
    • 堆:[2,3]
    • 此时前 2 大候选是 2 和 3
  3. 遇到 1
    • 1 <= 2,没资格进入前 2
    • 堆不变:[2,3]
  4. 遇到 5
    • 5 > 2,可以挤掉当前最小的 2
    • 堆变成:[3,5]
  5. 遇到 6
    • 6 > 3
    • 堆变成:[5,6]
  6. 遇到 4
    • 4 <= 5,进不了前 2
    • 堆保持:[5,6]

最终堆顶是 5,也就是第 2 大元素。

🎯 为什么小顶堆是对的?#

关键在于两个不变量:

  1. 堆的大小始终不超过 k
  2. 堆中始终保存当前扫描范围内最大的 k 个元素

既然堆里保存的是最大的 k 个元素,那么:

  • 堆顶是这 k 个元素中最小的
  • 它刚好就是“第 k 大”

这个思路在很多 Top K 问题中都通用,比如:

  • 前 K 个高频元素
  • 数据流中的第 K 大元素
  • 海量数据中的 Top K

⏱️ 复杂度分析#

小顶堆解法#

  • 时间复杂度:O(nlogk)
  • 空间复杂度:O(k)

为什么不是 O(nlogn)?

因为堆的大小始终只有 k,每次入堆/出堆的代价是 logk,而不是 logn。

当 k 远小于 n 时,这种做法会比完整排序更划算。

⚡ 解法二:快速选择(平均更快)#

如果面试官继续追问“还能再优化吗”,你可以补充 快速选择。

它和快速排序很像,但每次只递归进入一边,因此平均时间复杂度可以做到 O(n)。

Java 代码#

📝 两种方法怎么选?#

  1. 排序法

    • 时间复杂度:O(nlogn)
    • 优点:简单直接
    • 缺点:做了很多没必要的工作
  2. 小顶堆法

    • 时间复杂度:O(nlogk)
    • 优点:稳定、好写、面试友好
    • 缺点:不是理论上的最优平均复杂度
  3. 快速选择法

    • 平均时间复杂度:O(n)
    • 最坏时间复杂度:O(n^2)
    • 优点:平均更快
    • 缺点:代码复杂度更高,边界更容易写错

如果是面试,我建议:

  • 先写小顶堆,稳
  • 有余力再补充快速选择,体现深度

⚠️ 易错点提醒#

  1. 第K大不是第K个不同元素

    • 重复元素也要参与排序
    • 例如 [5,5,4] 中第 2 大是 5
  2. 堆要用小顶堆,不是大顶堆

    • 我们要维护“前 k 大”
    • 所以要让最弱的那个站在门口,方便随时淘汰
  3. 快速选择里的目标下标别写错

    • 第 k 大对应升序后的下标是 n - k
    • 不是 k - 1
  4. 不要每次都先入堆再出堆

    • 可以像上面的写法一样先比较,再决定是否替换
    • 逻辑更清晰

🎨 数据结构图解#

graph TB
    A[遍历数组] --> B{堆大小 < k?}
    B -->|是| C[直接入堆]
    B -->|否| D{当前元素 > 堆顶?}
    D -->|否| E[忽略当前元素]
    D -->|是| F[弹出堆顶后入堆]
    C --> G[继续遍历]
    E --> G
    F --> G
    G --> H[遍历结束]
    H --> I[堆顶即第K大元素]

🌟 面试时怎么说#

  1. 先定义问题本质

    • “这题本质上是一个 Top K 问题。”
  2. 给出核心策略

    • “我用一个大小为 k 的小顶堆,维护当前最大的 k 个元素。”
  3. 解释为什么是小顶堆

    • “堆顶就是当前前 k 大里最小的那个,也就是第 k 大。”
  4. 补充进阶方案

    • “如果追求更优平均复杂度,还可以用快速选择。”

🎩 延伸应用#

这道题的思想可以直接迁移到很多经典问题:

  1. 前 K 个高频元素

    • 先统计频率,再维护大小为 k 的堆
  2. 数据流中的第 K 大元素

    • 每来一个新数字,就更新堆
  3. 海量日志 Top K

    • 不可能把所有数据都排序,只能维护一个候选堆

这道题的关键不是“会不会排序”,而是你能不能意识到:我们真正关心的,只有前 k 名。抓住这个本质,堆的用法就自然了。