堆(优先队列)基础原理与题目说明
文章目录
- 堆(优先队列)基础原理与题目说明
-
- [一、 什么是堆(Heap)?](#一、 什么是堆(Heap)?)
-
- [1.1 Top K 问题核心法则](#1.1 Top K 问题核心法则)
- [二、 Python 中的堆实现与模板](#二、 Python 中的堆实现与模板)
-
- [2.1 找第 K 大(原生小顶堆模板)](#2.1 找第 K 大(原生小顶堆模板))
- [2.2 找第 K 小(取负模拟大顶堆模板)](#2.2 找第 K 小(取负模拟大顶堆模板))
- [三、 Top K 系列实战](#三、 Top K 系列实战)
-
- [[215. 数组中的第K个最大元素](https://leetcode.cn/problems/kth-largest-element-in-an-array/)](#215. 数组中的第K个最大元素)
- [[347. 前 K 个高频元素](https://leetcode.cn/problems/top-k-frequent-elements/)](#347. 前 K 个高频元素)
- [四、 高级应用:双堆对撞设计](#四、 高级应用:双堆对撞设计)
-
- [[295. 数据流的中位数](https://leetcode.cn/problems/find-median-from-data-stream/)](#295. 数据流的中位数)
🔗 查看完整专栏(LeetCode基础算法专栏)
特别说明:
本文为个人的 LeetCode 刷题与学习笔记,内容仅供学习与交流使用,禁止转载或用于商业用途。需要强调的是,文中的题目解法不一定是最优解(可能存在时间或空间复杂度的进一步优化空间),主要目的是分享个人的解题思路与逻辑实现,仅供参考。 笔记内容为个人理解与总结,可能存在疏漏或偏差,欢迎读者自行甄别并交流探讨。

一、 什么是堆(Heap)?
堆(通常作为优先队列 的具体实现)逻辑上是一棵完全二叉树,主要分为两类:
- 大顶堆(Max-Heap) :任意节点的值 ≥ \ge ≥ 其左右孩子的值 → \rightarrow → 堆顶永远是最大值。
- 小顶堆(Min-Heap) :任意节点的值 ≤ \le ≤ 其左右孩子的值 → \rightarrow → 堆顶永远是最小值。
1.1 Top K 问题核心法则
在算法实战中,堆是解决"第 K 大/小"及"前 K 个"频率等问题的杀手锏数据结构。请牢记以下反直觉但极其高效的匹配法则:
| 目标任务 | 选择的数据结构 | 核心逻辑 |
|---|---|---|
| 找第 K 大 / 前 K 大 | 大小为 K 的小顶堆 | 堆中仅保留最大的 K 个数,比堆顶(这K个数中最小的)还小的数直接丢弃。最终堆顶即为第 K 大。 |
| 找第 K 小 / 前 K 小 | 大小为 K 的大顶堆 | 堆中仅保留最小的 K 个数,比堆顶(这K个数中最大的)还大的数直接丢弃。最终堆顶即为第 K 小。 |
二、 Python 中的堆实现与模板
Python 的内置库 heapq 默认实现的是小顶堆。
2.1 找第 K 大(原生小顶堆模板)
py
import heapq
def findKthLargest(nums: list[int], k: int) -> int:
heap = []
for num in nums:
heapq.heappush(heap, num)
# 堆的大小超过 K 时,弹出堆顶(当前的最小值)
if len(heap) > k:
heapq.heappop(heap)
# 遍历结束,堆里只剩最大的 K 个数,堆顶就是第 K 大
return heap[0]
2.2 找第 K 小(取负模拟大顶堆模板)
由于 Python 没有直接提供大顶堆,通用的技巧是存入相反数(例如 5 变成 -5)。值越大,取负后越小,从而完美利用原生小顶堆模拟大顶堆。
py
import heapq
def findKthSmallest(nums: list[int], k: int) -> int:
heap = []
for num in nums:
# 存入相反数,模拟大顶堆
heapq.heappush(heap, -num)
if len(heap) > k:
heapq.heappop(heap)
# 取出时再次取负,还原真实值
return -heap[0]
(注:如果存入 heapq 的是元组 (x1, x2),小顶堆只会默认比较元组的第一个元素 x1,这在处理频率等问题时非常有用。)
三、 Top K 系列实战
215. 数组中的第K个最大元素
题目描述:
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。你必须设计并实现时间复杂度为 O ( n log k ) O(n \log k) O(nlogk) 的算法解决此问题。
解题思路:
直接套用上述的"大小为 K 的小顶堆"模板。
- 初始化空的小顶堆。
- 对每个数字推入堆中。关键规则:如果堆的大小超过
k,就把堆顶弹出去。 - 遍历结束,堆里只留了最大的
k个数,比堆顶还小的数已经在之前被丢弃了。此时堆顶即为数组的第 K 大元素。
核心代码:
py
import heapq
from typing import List
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
"""
维护大小为 K 的小顶堆,堆里永远只存「当前最大的 K 个数」
堆顶 = 这 K 个数里最小的 → 恰好就是整个数组的第 K 大元素
"""
heap = []
for num in nums:
# 入堆,Python 内置的 heapq 就是小顶堆
heapq.heappush(heap, num)
# 堆的大小超过k,弹出堆顶(这k个数中最小的)
if len(heap) > k:
heapq.heappop(heap)
# 堆顶就是第k大
return heap[0]
347. 前 K 个高频元素
题目描述:
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按任意顺序返回答案。
解题思路:
采用 哈希表(Counter) + 小顶堆 的方法解题。
- 先用哈希表统计数组中每个数字的出现频率。
- 维护一个大小固定为
k的小顶堆,堆中存储元组(频率, 数字)。 heapq会自动根据元组的第一个元素(即频率)进行排序。频率超出限制时,弹出堆顶(当前频次最小的元素)。- 最终堆中保留的就是频率最高的
k个元组。
核心代码:
py
import heapq
import collections
from typing import List
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
hashmap = collections.Counter(nums)
heap = []
for num in hashmap:
# 存储 (频率, 数字),Python 的 heapq 只比较元组的首个元素
heapq.heappush(heap, (hashmap[num], num))
# 保持堆的容量为 k
if len(heap) > k:
heapq.heappop(heap)
ans = []
# 将堆中剩下的元素(最高频的k个数)提取出来
for freq, num in heap:
ans.append(num)
return ans
四、 高级应用:双堆对撞设计
295. 数据流的中位数
题目描述:
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
设计一个 MedianFinder 类,支持动态地向数据流中添加元素,并在 O ( 1 ) O(1) O(1) 时间内返回当前的中位数。
解题思路:
我们用两个堆把数据流劈成两半,实现动态的排序与中位数提取:
- 大顶堆(leftheap) :存放数据流中较小的一半 数字 → \rightarrow → 堆顶 = 左半部分的最大值(需用负号模拟)。
- 小顶堆(rightheap) :存放数据流中较大的一半 数字 → \rightarrow → 堆顶 = 右半部分的最小值。
平衡规则(核心重点):
- 元素优先进入大顶堆(或者根据数值大小对比后进入对应的堆)。
- 动态调整长度:我们强行规定,大顶堆的元素个数要么与小顶堆相等,要么比小顶堆多且仅多 1 个。
- 计算中位数 :
- 总数为奇数时:大顶堆比小顶堆多一个,大顶堆的堆顶就是中位数。
- 总数为偶数时:两个堆大小相等,两个堆顶的平均值就是中位数。
核心代码:
py
import heapq
class MedianFinder:
def __init__(self):
# 大顶堆:存较小的一半数字,堆顶是左侧最大值(存负数模拟)
self.leftheap = []
# 小顶堆:存较大的一半数字,堆顶是右侧最小值(正常存)
self.rightheap = []
def addNum(self, num: int) -> None:
# 1. 判断放在左边还是右边
# 如果左堆为空,或者新数字小于等于左堆的最大值,放入左堆
if not self.leftheap or num <= -self.leftheap[0]:
heapq.heappush(self.leftheap, -num)
else:
heapq.heappush(self.rightheap, num)
# 2. 动态平衡两个堆的长度
# 规定:大顶堆可以比小顶堆多1个元素,但不能多2个及以上
if len(self.leftheap) > len(self.rightheap) + 1:
# 左侧多了,匀一个给右侧
val = -heapq.heappop(self.leftheap)
heapq.heappush(self.rightheap, val)
# 规定:大顶堆的长度绝不能小于小顶堆
elif len(self.leftheap) < len(self.rightheap):
# 右侧多了,匀一个给左侧
val = heapq.heappop(self.rightheap)
heapq.heappush(self.leftheap, -val)
def findMedian(self) -> float:
# 1. 偶数个:两个堆顶取平均值
if len(self.leftheap) == len(self.rightheap):
ans = (-self.leftheap[0] + self.rightheap[0]) / 2.0
else:
# 2. 奇数个:规定了左堆可以多一个,所以中位数必定在左堆顶
ans = float(-self.leftheap[0])
return ans
# 使用说明:
# obj = MedianFinder()
# obj.addNum(num)
# param_2 = obj.findMedian()