215. 数组中的第K个最大元素(Kth Largest Element in an Array)

难度:中等 | 主题:堆、快速选择、分治

题目

给定整数数组 nums 和整数 k,返回数组中第 k 个最大的元素。注意要找的是排序后的第 k 个最大元素,不是第 k 个不同的元素。

示例

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

思路

先讲个故事:成绩单上的第 K 名

期末考试后,老师让你找出全班第 K 高的分数。你会怎么做?

最直接的想法:把全班的分数从高到低排个序,取第 K 个。但更聪明的方式是——只关心分数最高的 K 个人,用一个”小本子”记下他们,其他人不用管。

第 K 大 = 升序排序后倒数第 K 个 = 索引 n-k 的元素。

引导式推导:三层递进


graph LR

    subgraph 三层递进

        A["排序<br/>O(n log n)"]

        B["最小堆<br/>O(n log k)"]

        C["Quickselect<br/>O(n) 平均"]

    end

    A -->|"k << n 时<br/>全排序浪费"| B

    B -->|"追求最优时间"| C

第 1 层:排序

sort() 后取 nums[n-k]。简单,但不是最优。

第 2 层:最小堆(推荐)

维护一个大小为 k 的小顶堆,遍历数组:

  • 堆没满 → 直接入堆

  • 堆满了且当前元素 > 堆顶 → 弹出堆顶,入堆当前元素

堆里始终存着”当前遇到的最大的 k 个数”,堆顶是这 k 个里的最小值——就是第 k 大。

 
nums = [3,2,1,5,6,4], k = 2
 
堆操作:
 
① 3 入堆 → [3]
 
② 2 入堆 → [2,3](堆大小=k ✓)
 
③ 1 < 堆顶 2 → 跳过
 
④ 5 > 堆顶 2 → pop(2), push(5) → [3,5]
 
⑤ 6 > 堆顶 3 → pop(3), push(6) → [5,6]
 
⑥ 4 < 堆顶 5 → 跳过
 
结果:堆顶=5 ✓
 

第 3 层:Quickselect(最优时间)

基于快排的 partition,每次选一个 pivot,根据其位置决定递归哪一侧——只递归包含目标的那一半。

 
找第 2 大(n-k=4):
 
[3,2,1,5,6,4] → pivot=4 → [3,2,1,|4|,5,6]
 
  pivot 在索引 3 < 目标 4,递归右边
 
[5,6] → pivot=6 → [5,|6|]
 
  pivot 在索引 5 > 目标 4,递归左边
 
[5] → 只剩索引 4 → 返回 5 ✓
 

代码

 
import heapq
 
# 方法1:最小堆(稳妥写法)
 
def findKthLargest(self, nums, k):
 
    min_heap = []
 
    for num in nums:
 
        heapq.heappush(min_heap, num)
 
        if len(min_heap) > k:
 
            heapq.heappop(min_heap)
 
    return min_heap[0]
 
# 快速选择(Quickselect)
 
def findKthLargest(self, nums, k):
 
    return self.quickselect(nums, 0, len(nums) - 1, len(nums) - k)
 
def quickselect(self, nums, left, right, k):
 
    if left == right:
 
        return nums[left]
 
    pivot_idx = self.partition(nums, left, right)
 
    if pivot_idx == k:
 
        return nums[pivot_idx]
 
    elif pivot_idx > k:
 
        return self.quickselect(nums, left, pivot_idx - 1, k)
 
    else:
 
        return self.quickselect(nums, pivot_idx + 1, right, k)
 
def partition(self, nums, left, right):
 
    pivot = nums[right]
 
    i = left
 
    for j in range(left, right):
 
        if nums[j] <= pivot:
 
            nums[i], nums[j] = nums[j], nums[i]
 
            i += 1
 
    nums[i], nums[right] = nums[right], nums[i]
 
    return i
 

复杂度

指标解释
最小堆时间O(n log k)每个元素入堆出堆一次
最小堆空间O(k)堆大小不超过 k
QuickselectO(n) 平均,O(n²) 最坏只递归一半,概率保证

实战考量

频率分析

出现在:常见核心题,字节/阿里/美团等几乎必考。两种写法都要会——先写堆再提 Quickselect 会显著加分。约 50% 常会从这道题延伸讨论大数据场景。

延伸思考

Q:Quickselect 最坏为什么是 O(n²)?怎么优化?

A:每次 pivot 选到最大/最小,退化为单边递归。优化:随机选 pivot 或三数取中法。

Q:如果 K 很大(接近 n),哪种方法更好?

A:堆变成 O(n log n),Quickselect 仍是 O(n),Quickselect 更优。

Q:如果数据流不断进来,找动态第 K 大呢?

A:维护大小为 K 的最小堆,新数据比堆顶大就替换。这就是 295/703 题的思路。

Q:如果找第 K 小呢?

A:维护大小为 K 的最大堆,或 Quickselect 找索引 k-1。

Q:最小堆里存的是什么?

A:存的是”最大的 k 个数”,堆顶是这 k 个里的最小值——即第 k 大。

易错点

  • 最小堆堆顶是第 k 大,不是最大堆

  • Quickselect 的 k 是升序索引 n-k,不是第 k 大

  • 堆超过 k 时要 pop,不要忘记


生活类比

第 K 大 → 用一个小本本记下最大的 K 个人

你不需要知道全班每个人的分数——只需要一个能记下 K 个人的小本子,每当发现一个新分数比本子上最低的高,就把最低的划掉,写上新的。

Quickselect 更聪明:像一个面试官,每次拿一份简历当基准(pivot),说”比这个好的站右边,比这个差的站左边”,然后只去人多的那边继续找。


相关题目

题目关系
347前K个高频元素堆 + 哈希表,Top K 模式
912排序数组(手撕快排)Quickselect 基于 partition
295数据流的中位数双堆设计

→ 返回题单:LeetCode学习路线图 > 六、堆

速记卡(面试闪卡)

Q1:一句话讲清「215. 数组中的第K个最大元素(Kth Largest Element in an Array)」到底是什么?

A:在数组中找出排序后第 k 个最大的元素。

Q2:思路 —— 怎么理解?

A:像老师找全班第 k 高的分:最笨是排个序取第 k 个;聪明是只记最大的 k 人。推荐 Min-heap(最小堆)存最大 k 个,堆顶即第 k 大。

Q3:代码 —— 怎么理解?

A:最小堆法:遍历数组,堆未满就入,满了且当前>堆顶就弹顶入新;堆顶即答案。Quickselect 用 partition 只递归含目标那半。

Q4:复杂度 —— 怎么理解?

A:最小堆时间 O(n log k)、空间 O(k);Quickselect 平均 O(n)、最坏 O(n²)(pivot 总选到极值)。

Q5:实战考量 —— 怎么理解?

A:字节/阿里/美团近必考,先写堆再提 Quickselect 加分。易错:堆顶是第 k 大不是最大;Quickselect 的 k 是升序索引 n-k。

Q6:核心速记主线有哪些?

  • 最小堆存最大 k 个,堆顶即第 k 大

  • 堆满且当前>堆顶才替换,保持 O(k) 大小

  • 最小堆 O(n log k),Quickselect 平均 O(n)

  • 易错:用最小堆非最大堆,k 取 n-k 索引

口诀

A:第k大怎么找

小顶堆里瞄

最大k个存

堆顶就是宝

相关链接