力扣hot100—系列5-堆

堆的特点是能以 O(log⁡N)O(\log N)O(logN) 的时间复杂度完成插入和删除,并以 O(1)O(1)O(1) 的时间获取最大值或最小值。


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

核心思想:维护一个大小为 KKK 的小顶堆。

  • 直观思路:
    • 如果我们想找第 KKK 大的数,最简单的是全排序,但代价太高。
    • 想象一个容器,它只装前 KKK 个最大的数。 当新来一个数时,如果它比容器里最小的那个还要大,就把容器里最小的踢出去,把它放进去。
    • 这个"容器"就是小顶堆 。堆顶永远是这 KKK 个数里最小的那一个,也就是全局第 KKK 大的数。
  • 复杂度: 时间 O(Nlog⁡K)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 的小顶堆。

  • 直观思路:
    1. 先用字典(哈希表)统计每个数字出现的频率
    2. 和上一题类似,我们找"频率最高"的 KKK 个。
    3. 建立一个小顶堆,但堆里存的是 (频率, 元素)。堆根据频率排序,保持堆的大小为 KKK。
    4. 最后堆里剩下的 KKK 个元素就是结果。
  • 复杂度: 时间 O(Nlog⁡K)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)。
    • 规则:
      1. 让大顶堆的数量 ≥\ge≥ 小顶堆的数量(最多多 1 个)。
      2. 如果有奇数个数,中位数就是大顶堆的堆顶。
      3. 如果有偶数个数,中位数就是两个堆顶的平均值。
  • 难点(如何添加数): 为了保证平衡,新数先加进大顶堆,再把大顶堆最大的那个挪到小顶堆,如果小顶堆人多了,再挪回大顶堆。

代码实现 (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

刷题建议:

  1. 为什么用小顶堆求"最大 K 个"?
    因为你需要不断淘汰掉当前"前 K 名"里最弱的(最小的),所以要把最小的放在堆顶方便踢出去。
  2. Python 的 heapq 默认是小顶堆。
    如果你需要大顶堆,最简单的办法是给所有数字乘以 -1,取出时再乘回来。
  3. 对顶堆是处理动态中位数的"大杀器"。
    它的本质是用两个堆模拟了一个有序数组的中间分界点。
相关推荐
ZoeJoy81 分钟前
算法筑基(二):搜索算法——从线性查找到图搜索,精准定位数据
算法·哈希算法·图搜索算法
Alicx.6 分钟前
dfs由易到难
算法·蓝桥杯·宽度优先
_日拱一卒18 分钟前
LeetCode:找到字符串中的所有字母异位词
算法·leetcode
云泽8081 小时前
深入 AVL 树:原理剖析、旋转算法与性能评估
数据结构·c++·算法
心软小念1 小时前
金三银四,全网最详细的软件测试面试题总结
软件测试·面试·职场和发展
Wilber的技术分享2 小时前
【LeetCode高频手撕题 2】面试中常见的手撕算法题(小红书)
笔记·算法·leetcode·面试
邪神与厨二病2 小时前
Problem L. ZZUPC
c++·数学·算法·前缀和
软件测试媛3 小时前
软件测试常见的面试题(46道)
功能测试·面试·职场和发展
梯度下降中3 小时前
LoRA原理精讲
人工智能·算法·机器学习
IronMurphy3 小时前
【算法三十一】46. 全排列
算法·leetcode·职场和发展