堆的特点是能以 O(logN)O(\log N)O(logN) 的时间复杂度完成插入和删除,并以 O(1)O(1)O(1) 的时间获取最大值或最小值。
1. 数组中的第 K 个最大元素
核心思想:维护一个大小为 KKK 的小顶堆。
- 直观思路:
- 如果我们想找第 KKK 大的数,最简单的是全排序,但代价太高。
- 想象一个容器,它只装前 KKK 个最大的数。 当新来一个数时,如果它比容器里最小的那个还要大,就把容器里最小的踢出去,把它放进去。
- 这个"容器"就是小顶堆 。堆顶永远是这 KKK 个数里最小的那一个,也就是全局第 KKK 大的数。
- 复杂度: 时间 O(NlogK)O(N \log K)O(NlogK),空间 O(K)O(K)O(K)。
代码实现 (Python):
python
import heapq
def findKthLargest(nums, k):
# 初始化一个空堆
min_heap = []
for num in nums:
# 将当前数字压入堆中
heapq.heappush(min_heap, num)
# 如果堆的大小超过了 k,弹出堆顶(最小的那个)
if len(min_heap) > k:
heapq.heappop(min_heap)
# 遍历结束后,堆中剩下的就是前 k 个最大的数
# 堆顶就是这 k 个数中最小的,即第 k 个最大的数
return min_heap[0]
2. 前 K 个高频元素
核心思想:哈希表计数 + 维护大小为 KKK 的小顶堆。
- 直观思路:
- 先用字典(哈希表)统计每个数字出现的频率。
- 和上一题类似,我们找"频率最高"的 KKK 个。
- 建立一个小顶堆,但堆里存的是
(频率, 元素)。堆根据频率排序,保持堆的大小为 KKK。 - 最后堆里剩下的 KKK 个元素就是结果。
- 复杂度: 时间 O(NlogK)O(N \log K)O(NlogK),空间 O(N)O(N)O(N)。
代码实现 (Python):
python
import heapq
from collections import Counter
def topKFrequent(nums, k):
# 1. 统计频率 {数字: 出现次数}
count = Counter(nums)
# 2. 维护大小为 k 的小顶堆
min_heap = []
for num, freq in count.items():
# 注意:heapq 默认根据元组的第一个值(freq)排序
heapq.heappush(min_heap, (freq, num))
if len(min_heap) > k:
heapq.heappop(min_heap)
# 3. 提取结果
return [item[1] for item in min_heap]
3. 数据流的中位数
核心思想:对顶堆(大顶堆 + 小顶堆)。
- 直观思路:
- 中位数把有序序列分成左右两半:左半部分都小于中位数,右半部分都大于中位数。
- 左半部分: 我们需要左边最大 的那个数,用 大顶堆(SmallHalf)。
- 右半部分: 我们需要右边最小 的那个数,用 小顶堆(LargeHalf)。
- 规则:
- 让大顶堆的数量 ≥\ge≥ 小顶堆的数量(最多多 1 个)。
- 如果有奇数个数,中位数就是大顶堆的堆顶。
- 如果有偶数个数,中位数就是两个堆顶的平均值。
- 难点(如何添加数): 为了保证平衡,新数先加进大顶堆,再把大顶堆最大的那个挪到小顶堆,如果小顶堆人多了,再挪回大顶堆。
代码实现 (Python):
python
import heapq
class MedianFinder:
def __init__(self):
# Python 只有小顶堆,实现大顶堆需将数值取负
self.small_half = [] # 大顶堆 (存较小的一半)
self.large_half = [] # 小顶堆 (存较大的一半)
def addNum(self, num: int) -> None:
# 1. 先把数放进大顶堆(取负存入)
heapq.heappush(self.small_half, -num)
# 2. 确保左边最大的数搬运到右边
# 将大顶堆的堆顶弹出,放入小顶堆
heapq.heappush(self.large_half, -heapq.heappop(self.small_half))
# 3. 平衡数量:如果小顶堆人多,搬回大顶堆
if len(self.large_half) > len(self.small_half):
heapq.heappush(self.small_half, -heapq.heappop(self.large_half))
def findMedian(self) -> float:
if len(self.small_half) > len(self.large_half):
return -self.small_half[0]
else:
return (-self.small_half[0] + self.large_half[0]) / 2.0
刷题建议:
- 为什么用小顶堆求"最大 K 个"?
因为你需要不断淘汰掉当前"前 K 名"里最弱的(最小的),所以要把最小的放在堆顶方便踢出去。 - Python 的
heapq默认是小顶堆。
如果你需要大顶堆,最简单的办法是给所有数字乘以-1,取出时再乘回来。 - 对顶堆是处理动态中位数的"大杀器"。
它的本质是用两个堆模拟了一个有序数组的中间分界点。