堆(优先队列)基础原理与题目说明

堆(优先队列)基础原理与题目说明

文章目录

🔗 查看完整专栏(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 的小顶堆"模板。

  1. 初始化空的小顶堆。
  2. 对每个数字推入堆中。关键规则:如果堆的大小超过 k,就把堆顶弹出去。
  3. 遍历结束,堆里只留了最大的 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) + 小顶堆 的方法解题。

  1. 先用哈希表统计数组中每个数字的出现频率。
  2. 维护一个大小固定为 k 的小顶堆,堆中存储元组 (频率, 数字)
  3. heapq 会自动根据元组的第一个元素(即频率)进行排序。频率超出限制时,弹出堆顶(当前频次最小的元素)。
  4. 最终堆中保留的就是频率最高的 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) 时间内返回当前的中位数。

解题思路

我们用两个堆把数据流劈成两半,实现动态的排序与中位数提取:

  1. 大顶堆(leftheap) :存放数据流中较小的一半 数字 → \rightarrow → 堆顶 = 左半部分的最大值(需用负号模拟)。
  2. 小顶堆(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()
相关推荐
another heaven2 小时前
【软考 对称加密与非对称加密】
服务器·网络
生万千欢喜心2 小时前
linux 安装 人大金仓数据库
linux·运维·数据库
王莎莎-MinerU2 小时前
MinerU 生态全景:Skills、RAG、MCP、Cursor Rules
人工智能·深度学习·计算机视觉·chatgpt·pdf·软件工程
QYR-分析2 小时前
全球轻量化新能源汽车市场分析:现状、机遇与发展展望
人工智能·机器人
m0_716430072 小时前
mysql数据库表名区分大小写吗_通过lower case table names配置
jvm·数据库·python
傻啦嘿哟2 小时前
Python多进程编程:用multiprocessing突破GIL限制
服务器·网络·数据库
mahtengdbb12 小时前
GDSAFusion全局-局部双尺度自适应融合改进YOLOv26多尺度特征表达能力
人工智能·深度学习·yolo
Finn Wang2 小时前
KeyPresser 一款自动化按键工具
运维·自动化
BitaHub20242 小时前
AI 也能按设计规范出图?Qwen-Image-2512 本地海报生成实战
人工智能·设计规范·bitahub