力扣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. 对顶堆是处理动态中位数的"大杀器"。
    它的本质是用两个堆模拟了一个有序数组的中间分界点。
相关推荐
郝学胜-神的一滴18 分钟前
AI 编程智能体 16:从零基础搭建到精细化优化
人工智能·python·算法·语言模型·langchain·pip
喜欢打篮球的普通人27 分钟前
AI 编译器内存分配算法:从静态/动态划分到内存复用
人工智能·算法
mjhcsp1 小时前
Code date creation v8.0 竞赛数据生成与对拍实战指南
算法·ai·深度优先·cdc
小郎君6582 小时前
keep-alive 页面怎么“真刷新”?别改外层 key——一个 refreshVersion 的完整实现
vue.js·算法
瑶池酒剑仙2 小时前
C++变量初始化五法完全指南:默认/拷贝/直接/列表/值初始化与窄化转换陷阱(青云之志)
c语言·开发语言·数据结构·c++·算法·visual studio
用户238003099452 小时前
从零开始实现扫雷游戏(C 语言控制台版)
算法
国科安芯2 小时前
星上计算的第一道防线:低轨遥感卫星载荷处理链中的AS32S601型抗辐射MCU—— 面向高分辨率成像与在轨智能处理的场景化综述
大数据·网络·人工智能·单片机·算法·低轨卫星·抗辐射
thusloop3 小时前
零门槛接入 GPT-6-astra 与 claude-opus-5-5,三大场景实战指南
python·gpt·算法·agent·claude·codex
瓦瓦gogo3 小时前
学回溯算法时,我先弄明白了怎么“退回来”
算法
Lzg_na3 小时前
堆和堆排序
算法