【算法题】堆

堆(优先队列)是一种基于完全二叉树的动态数据结构,核心特性是快速获取最值 (大根堆获取最大值,小根堆获取最小值),插入和删除操作的时间复杂度均为 O(log⁡n)O(\log n)O(logn)。它广泛应用于"动态维护最值""Top-K 问题""中位数维护"等场景,是处理动态数据的高效工具。本文通过4道经典题目,拆解堆在不同场景下的解题思路与代码实现。

一、最后一块石头的重量

题目描述:

有一堆石头,每回合选两块最重的石头粉碎:若重量相等则完全粉碎,否则剩下重量为两者差值的石头。返回最后剩下的石头重量(无剩余则返回0)。

示例

  • 输入:stones = [2,7,4,1,8,1],输出:1(粉碎过程:8-7=14-2=22-1=11-1=0→剩1)

解题思路:

用大根堆维护石头重量,每次取最大的两块处理:

  1. 将所有石头重量入大根堆。
  2. 当堆中元素数>1时,取出最大的两块 aba ≥ b):
    • a > b,将 a - b 入堆;
    • a == b,直接丢弃两块。
  3. 最终堆中若有元素则返回堆顶,否则返回0。

完整代码:

cpp 复制代码
class Solution {
public:
    int lastStoneWeight(vector<int>& stones) {
        priority_queue<int> heap; // 大根堆(默认)
        for(auto x : stones) heap.push(x);
        while(heap.size() > 1)
        {
            int a = heap.top(); heap.pop();
            int b = heap.top(); heap.pop();
            if(a > b) heap.push(a - b); 
        }

        return heap.size() ? heap.top() : 0;
    }
};

复杂度分析:

  • 时间复杂度:O(nlog⁡n)O(n\log n)O(nlogn),n 为石头数量,每次入堆/出堆操作时间为 O(log⁡n)O(\log n)O(logn),最多执行 nnn 次。
  • 空间复杂度:O(n)O(n)O(n),堆存储所有石头重量。

二、数据流中的第K大元素

题目描述:

设计一个类,动态维护数据流中的第K大元素(排序后的第K大,非第K个不同元素)。实现 KthLargest 类,包含初始化和添加元素后返回第K大的方法。

示例

  • 初始化:k=3, nums=[4,5,8,2],添加 3→返回4,添加 5→返回5,添加 10→返回5。

解题思路:

用小根堆维护"前K大的元素",堆顶即为第K大元素:

  1. 初始化时,将所有元素入堆,若堆大小超过K则弹出堆顶(保留前K大的元素)。
  2. 添加元素时,将新元素入堆,若堆大小超过K则弹出堆顶,堆顶即为当前第K大元素。

完整代码:

cpp 复制代码
class KthLargest {
    int _k;
    priority_queue<int, vector<int>, greater<int>> heap; // 小根堆

public:
    KthLargest(int k, vector<int>& nums) {
        _k = k;
        for(auto& x : nums)
        {
            heap.push(x);
            if(heap.size() > _k) heap.pop();
        }
    }
    
    int add(int val) {
        heap.push(val);
        if(heap.size() > _k) heap.pop();
        return heap.top();
    }
};

复杂度分析:

  • 初始化时间:O(nlog⁡K)O(n\log K)O(nlogK),n 为初始元素数,每个元素入堆/出堆时间为 O(log⁡K)O(\log K)O(logK)。
  • 添加元素时间:O(log⁡K)O(\log K)O(logK),每次入堆/出堆时间为 O(log⁡K)O(\log K)O(logK)。
  • 空间复杂度:O(K)O(K)O(K),堆最多存储K个元素。

三、前K个高频单词

题目描述:

给定单词列表 words 和整数 k,返回前K个出现次数最多的单词(频率相同按字典序升序排列)。

示例

  • 输入:words = ["i","love","leetcode","i","love","coding"], k=2,输出:["i","love"](频率均为2,字典序 i < love

解题思路:

哈希表统计频率 + 小根堆维护前K个高频单词:

  1. 用哈希表统计每个单词的出现频率。
  2. 定义小根堆的比较规则:
    • 频率不同时,频率小的优先出堆;
    • 频率相同时,字典序大的优先出堆(保证堆顶是"频率最小/字典序最大"的候选,弹出后保留前K个)。
  3. 遍历哈希表,将"单词-频率"入堆,若堆大小超过K则弹出堆顶。
  4. 逆序收集堆中元素(因小根堆弹出的是较小的元素,需反转得到从大到小的顺序)。

完整代码:

cpp 复制代码
class Solution {
    typedef pair<string, int> PSI;
    struct cmp
    {
        bool operator()(const PSI a, const PSI b)
        {
            if(a.second == b.second)
                return a.first < b.first; // 频率相同,字典序大的优先出堆
            else 
                return a.second > b.second; // 频率小的优先出堆
        }
    };
public:
    vector<string> topKFrequent(vector<string>& words, int k) {
        unordered_map<string, int> hash;
        for(auto& x : words) 
            hash[x]++;

        priority_queue<PSI, vector<PSI>, cmp> heap;

        for(auto& psi : hash)
        {
            heap.push(psi);
            if(heap.size() > k) heap.pop();
        }

        vector<string> ret(k);
        for(int i = heap.size() - 1; i >= 0; i--)
        {
            ret[i] = heap.top().first;
            heap.pop();
        }
        return ret;
    }
};

复杂度分析:

  • 时间复杂度:O(mlog⁡k)O(m\log k)O(mlogk),m 为不同单词的数量,每个单词入堆/出堆时间为 O(log⁡k)O(\log k)O(logk)。
  • 空间复杂度:O(m+k)O(m + k)O(m+k),哈希表存储所有单词频率,堆存储K个单词。

四、数据流的中位数

题目描述:

设计一个类,动态维护数据流的中位数(奇数个元素取中间值,偶数个取中间两个的平均值)。实现 MedianFinder 类,包含添加元素和获取中位数的方法。

示例

  • 添加 1→添加 2→中位数 1.5→添加 3→中位数 2.0

解题思路:

用两个堆维护数据流的左右两部分:

  1. 大根堆 left:存储左半部分元素(≤中位数),堆顶为左半部分最大值;
  2. 小根堆 right:存储右半部分元素(≥中位数),堆顶为右半部分最小值;
  3. 保持平衡规则:
    • 总元素数为偶数时,left.size() == right.size()
    • 总元素数为奇数时,left.size() = right.size() + 1(中位数为 left.top());
  4. 添加元素时,根据元素与堆顶的大小关系选择入堆,并调整堆的大小以保持平衡。

完整代码:

cpp 复制代码
class MedianFinder {
    priority_queue<int> left; // 大根堆(左半部分)
    priority_queue<int, vector<int>, greater<int>> right; // 小根堆(右半部分)
public:
    MedianFinder() {
        
    }
    
    void addNum(int num) {
        if(left.size() == right.size())
        {
            if(left.empty() || num < left.top())
            {
                left.push(num);
            }
            else 
            {
                right.push(num);
                left.push(right.top());
                right.pop();
            }
        }
        else
        {
            if(num < left.top())
            {
                left.push(num);
                right.push(left.top());
                left.pop();
            }
            else
            {
                right.push(num);
            }
        }
    }
    
    double findMedian() {
        if(left.size() == right.size())
            return (left.top() + right.top()) / 2.0;
        else
            return left.top();
    }
};

复杂度分析:

  • 添加元素时间:O(log⁡n)O(\log n)O(logn),每次入堆/出堆时间为 O(log⁡n)O(\log n)O(logn)。
  • 获取中位数时间:O(1)O(1)O(1),直接取堆顶计算。
  • 空间复杂度:O(n)O(n)O(n),两个堆存储所有元素。
相关推荐
Kk.080229 分钟前
数据结构|排序算法(二) 希尔排序
数据结构·算法·排序算法
AI医影跨模态组学38 分钟前
NPJ Precis Oncol(IF=8)复旦大学肿瘤医院等团队:基于生境CT放射组学解析可切除非小细胞肺癌时空异质性预测新辅助化疗免疫治疗病理反应
大数据·人工智能·算法·医学·医学影像
Book思议-1 小时前
二叉树的递归遍历详解:前序、中序与后序
数据结构·算法·二叉树的递归遍历-前中后序
Demon--hx1 小时前
[LeetCode]100 链表-专题
算法·leetcode·链表
Omics Pro1 小时前
首款多模态生物推理大语言模型
人工智能·算法·语言模型·自然语言处理·数据挖掘·数据分析·aigc
国产化创客1 小时前
基于ESP32+Wi‑Fi CSI的开源项目ESPectre
物联网·算法·信息与通信
_深海凉_2 小时前
LeetCode热题100-LRU 缓存
算法·leetcode·缓存
Via_Neo2 小时前
今天是周六,两天后是周几?
java·数据结构·算法
伟大的车尔尼2 小时前
广度优先搜索和深度优先搜索的概念
数据结构·算法·并查集·深度优先搜索·广度优先搜索