news 2026/9/24 22:38:40

Hot 100堆题全攻略:优先队列、TopK与面试实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Hot 100堆题全攻略:优先队列、TopK与面试实战

1. 说在前面:hot100里的“堆”到底是什么

这两年铺天盖地的LeetCode Hot 100刷题清单,很多人一上来就按顺序从两数之和开刷,刷到树和图就开始崩溃,然后跳过一堆题目。说实话,Hot 100里跟堆(Heap)相关的题其实不算多,标签里明确带“堆”的也就七八道左右,但它恰恰是面试中被问得最频繁的数据结构之一,尤其是大厂一面、二面的手撕代码环节,TopK、中位数、多路归并这几个经典场景,十次有八次会出现在白板上。

先说清一个概念:这里讨论的“堆”,是数据结构中的二叉堆(Binary Heap),不是操作系统或者JVM里那个“内存堆”。虽然热词里有人搜“编译器的堆空间不足”“堆外内存”,那是另一个维度的概念,指的是动态内存分配区域。两者同名但完全不同,刷题的时候千万别混。

Hot 100里的堆题,核心就一句话:利用大小根堆的堆顶特性,以O(log n)的代价维护一个有序的窗口或集合。你不需要会手写复杂平衡树,也不需要会Treap、Splay,能把优先队列(Priority Queue)用顺,再明白它背后是怎么用数组模拟完全二叉树的,就足够应付绝大多数题目。

这篇文章把我的刷题笔记、面试现场踩过的坑、以及对每道hot100堆题的理解全部整理出来,包含可直接抄的模板代码、复杂度分析、以及面试追问时的应对思路。不管你是刚开始刷题的新手,还是准备冲刺大厂的老兵,这套内容应该都能帮上忙。

2. 堆的底层原理与hot100堆题的题型分布

2.1 堆的本质:用数组模拟的完全二叉树

堆在逻辑上是一棵完全二叉树,但在物理存储上就是一个数组。对于一个下标从0开始的数组,节点i的左孩子是2i+1,右孩子是2i+2,父节点是(i-1)/2。之所以能用数组存,正是因为完全二叉树每一层都是从左到右紧密排列的,中间不会出现空洞。

大根堆的性质是每个节点的值都大于等于它的左右孩子,所以堆顶就是这个堆里的最大值;小根堆正好相反,堆顶是最小值。注意,堆只保证父子之间的有序性,不保证兄弟节点之间的顺序,这是堆和二叉搜索树的本质区别。很多人刚开始会把堆和排序树搞混,记住一点:二叉搜索树可以中序遍历得到有序序列,堆做不到,堆只能保证堆顶是极值。

这个“只保证堆顶有序”的特性,决定了堆适合解决“我只关心最大/最小的那一个或那一批”的问题。如果你需要全局有序,应该用排序;如果只需要动态维护极值、局部有序,堆就是成本最低的选择。

2.2 手写堆的两个核心操作:上浮与下沉

虽然日常刷题可以直接用语言内置的优先队列,但面试官有时候会追问“优先队列是怎么实现的”,甚至会要求手写一个堆。这时候如果你只知道调API,场面会很尴尬。

手写堆就两个核心操作:

  • 上浮(swim / shift-up):新元素插入到数组末尾,然后不断和父节点比较,如果不满足堆性质就交换,直到满足为止。用于插入操作。
  • 下沉(sink / shift-down):把某个节点和它的左右孩子中较大的那个(大根堆)比较,如果父节点小就交换,然后继续下沉。用于删除堆顶或者堆排序。

插入一个元素,先放到末尾,然后上浮;删除堆顶,把末尾元素放到堆顶,然后下沉。两个操作的时间复杂度都是O(log n),因为完全二叉树的高度是log n。

下面给一份大根堆手写模板,我建议每个准备面试的人都默写一遍:

class MaxHeap { private: vector<int> a; void swim(int i) { while (i > 0 && a[(i - 1) / 2] < a[i]) { swap(a[(i - 1) / 2], a[i]); i = (i - 1) / 2; } } void sink(int i) { int n = a.size(); while (2 * i + 1 < n) { int j = 2 * i + 1; if (j + 1 < n && a[j + 1] > a[j]) j++; if (a[i] >= a[j]) break; swap(a[i], a[j]); i = j; } } public: void push(int x) { a.push_back(x); swim(a.size() - 1); } int top() { return a[0]; } void pop() { a[0] = a.back(); a.pop_back(); sink(0); } };

这份代码面试时非常加分。它能让你在白板上跟面试官聊清楚“为什么优先队列的时间复杂度是O(log n)”“堆排序为什么不稳定”这类追问。堆排序不稳定,是因为堆调整过程中会跳过间隔元素,相同值的相对顺序无法保证。

2.3 hot100堆题的三大模式

把hot100里的堆题全部过一遍,你会发现它们本质上是三类问题:

第一类:TopK问题。核心套路:维护一个大小为k的堆,求第K大用最小堆,求第K小用最大堆。这类的代表是215. 数组中的第K个最大元素、347. 前K个高频元素。为什么求第K大要用小根堆?因为小根堆的堆顶是堆里最小的元素,当堆的大小超过k时,堆顶就是“目前这k+1个元素里最不该保留的”,把它弹掉,剩下k个就是目前最大的k个。遍历完整个数组后,堆顶就是第K大的那个。

第二类:多路归并问题。多个有序序列要合并成一个有序序列,每次从所有序列的当前指针中取最小的那个。用一个大小为路数的小根堆,堆顶就是当前最小的元素。代表是23. 合并K个升序链表。这类题的关键是堆里存的不只是值,还需要记录这个值来自哪一路、以及当前走到哪了。

第三类:动态数据流问题。数据不断插入,随时需要查极值或中位数。代表是295. 数据流的中位数,用一个最大堆和一个最小堆相互配合。这类题的关键是堆的“动态调整”,每插入一个数就要维护两个堆的平衡。

明白了这三类模式,hot100里所有堆题都有了解题框架。下面我按题目逐个拆解。

3. hot100堆题逐个拆解:思路、代码与易错点

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

这是堆题里最经典的一道,也是TopK问题的原型。题目给你一个无序数组,要求返回数组中第K个最大的元素,注意不是第K个不同元素。

用堆的解法非常直观:

int findKthLargest(vector<int>& nums, int k) { priority_queue<int, vector<int>, greater<int>> pq; // 小根堆 for (int x : nums) { pq.push(x); if (pq.size() > k) pq.pop(); } return pq.top(); }

时间复杂度O(n log k),空间复杂度O(k)。当k远小于n时,这个复杂度优于直接排序的O(n log n)。

面试官几乎一定会追问的替代方案是快速选择(Quick Select),基于快排的partition思想,平均O(n),最坏O(n^2)。我个人的建议是:面试时优先答堆,因为代码短、思路清晰;如果面试官追问“能不能更快”,再提快速选择,并且要能说明它的平均复杂度和随机化改进。

注意一个细节:C++的priority_queue<int, vector<int>, greater<int>>是小根堆,如果你用的是Java的PriorityQueue<Integer>,默认就是小根堆;Python的heapq默认也是小根堆。真正容易坑的是C++,因为priority_queue默认是大根堆,想用小根堆必须带全三个模板参数。很多初学者在这里写错,结果求第K大变成求第K小。

3.2 347. 前K个高频元素

这道题是哈希表 + 堆的经典组合。先统计每个数字的出现次数,然后用一个大小为k的小根堆维护“当前出现频率最高的k个元素”,堆顶是这k个里频率最低的。新元素频率比堆顶高时,替换堆顶。

vector<int> topKFrequent(vector<int>& nums, int k) { unordered_map<int, int> freq; for (int x : nums) freq[x]++; // 小根堆,pair<频率, 元素值> auto cmp = [](pair<int, int>& a, pair<int, int>& b) { return a.first > b.first; }; priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> pq(cmp); for (auto& [val, f] : freq) { pq.push({f, val}); if (pq.size() > k) pq.pop(); } vector<int> res; while (!pq.empty()) { res.push_back(pq.top().second); pq.pop(); } return res; }

这里有个容易踩的坑:C++用lambda做比较器时,返回true表示第一个参数的优先级低于第二个参数。很多人从sort的习惯过来,sort的lambda返回true表示第一个参数排在前面,但priority_queue的规则是反直觉的。比如return a.first > b.first,意思是频率大的在堆底、频率小的在堆顶,形成小根堆。如果你照搬sort的写法return a.first < b.first,你会得到一个最大堆,堆顶是频率最高的,弹出它的时候恰恰会把正确答案丢掉。

这也是我建议面试时多用语言内置API的原因之一,但你必须清楚API底层的行为。想验证自己有没有理解错,就在本地跑一段几行的小代码,打印堆顶输出看一眼。

这道题的时间复杂度是O(n log k),因为哈希遍历是O(n),堆操作是O(log k)。如果要追求极致,还可以用桶排序思想做到O(n),把频率作为数组下标,从高到低收集元素。面试中能说出这个优化会很加分。

3.3 295. 数据流的中位数

这道题的难度在hot100里属于中上,核心技巧是“双堆”:一个最大堆存较小的一半,一个最小堆存较大的一半。中位数就是两个堆顶之一或二者平均。

class MedianFinder { private: priority_queue<int> left; // 大根堆,存较小的一半,堆顶是较小一半的最大值 priority_queue<int, vector<int>, greater<int>> right; // 小根堆,存较大的一半,堆顶是较大一半的最小值 public: void addNum(int num) { // 先插入左侧大根堆 if (left.empty() || num <= left.top()) left.push(num); else right.push(num); // 平衡:左侧最多比右侧多1个 if (left.size() > right.size() + 1) { right.push(left.top()); left.pop(); } if (right.size() > left.size()) { left.push(right.top()); right.pop(); } } double findMedian() { if (left.size() > right.size()) return left.top(); return (left.top() + right.top()) / 2.0; } };

为什么左侧用大根堆、右侧用小根堆?因为中位数恰好是“较小一半的最大值”和“较大一半的最小值”之间的分界。只要左侧堆顶小于等于右侧堆顶,中位数就能从两个堆顶取得。

这道题面试官会连环追问:

  • 如果不要求实时查询,只是给一个数组求中位数,直接sort是O(n log n),双堆的优势在于每插入一个数O(log n)、查询O(1)。
  • 如果数据范围有限,比如0到100之间的整数,可以用计数数组,插入O(1)、查询O(1),比双堆还快。
  • 如果数据量极大放不下内存,可以分桶、先用外部排序等方案。

双堆题的易错点是平衡逻辑。很多人喜欢先无脑插入,再“一边倒”地调整,思路没问题,但平衡条件要写对。我用的约定是:左侧数量要么等于右侧,要么比右侧多1。这个约定让findMedian只需要判断一种情况,代码最简洁。面试时先跟面试官说明这个约定,再写代码,逻辑清晰很多。

3.4 23. 合并K个升序链表

这是多路归并的代表题。K个升序链表,最简单的做法是每次比较K个头结点,取最小的,时间复杂度O(nK),n是总节点数。用堆优化后,每次从堆顶取最小节点O(log K),整体O(n log K),在K很大时优势明显。

ListNode* mergeKLists(vector<ListNode*>& lists) { auto cmp = [](ListNode* a, ListNode* b) { return a->val > b->val; }; priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> pq(cmp); for (auto* head : lists) { if (head) pq.push(head); } ListNode* dummy = new ListNode(0); ListNode* tail = dummy; while (!pq.empty()) { auto cur = pq.top(); pq.pop(); tail->next = cur; tail = cur; if (cur->next) pq.push(cur->next); } return dummy->next; }

这个解法的一个精妙之处在于:堆里只保存每个链表当前的头节点,而不是保存所有节点。取走某个节点后,再把它的next压入堆中。这样堆的大小始终不超过K,空间复杂度O(K)。

坑点有两个。第一个是空链表的处理,初始化时就要把空链表跳过,不然后面pq.top()会拿到空指针。第二个是比较器不要比较地址,一定要比较a->val。我见过有人图省事直接把指针放进去,默认比较指针大小,运行结果完全错误,还排查了半天。

这道题的变体是“合并K个排序数组”“合并K个升序序列”,思路完全一样。再往深层说,这就是外部排序的核心思想:当数据量大到内存放不下时,把大文件拆成多个有序小文件,再用多路归并合成一个有序大文件。面试如果聊到大数据排序,你就可以拿这个例子来承接,画风立刻不一样。

3.5 1046. 最后一块石头的重量

这道题属于堆的入门题,但hot100里包含了它,说明堆的标签在hot100里其实包含了从入门到进阶的跨度。题目:一堆石头,每次取两块最重的相互粉碎,如果重量相同全碎,否则剩余差值的石头放回,求最后石头的重量。

解法就是大根堆模拟:

int lastStoneWeight(vector<int>& stones) { priority_queue<int> pq(stones.begin(), stones.end()); while (pq.size() > 1) { int a = pq.top(); pq.pop(); int b = pq.top(); pq.pop(); if (a != b) pq.push(a - b); } return pq.empty() ? 0 : pq.top(); }

思路一句话:每次取两个最大值,模拟粉碎过程。这题的难点完全不在算法,而在理解“为什么用堆最合适”——因为题目每次需要动态获取最大值,并且取走最大值后还会有新元素插入,这种“边取边加”的场景恰好是堆的使用场景。如果用贪心排序,每次重新排序的成本太高。

顺带一提,这题也是一个很好的“读题能力”考察点。面试官期待的是你从题目里提取出“动态取最大”这个关键需求。你可以练习着把这类描述都转译成数据结构需求的句式,比如“每次取最大/最小且伴随插入”就等于“堆”。

3.6 378. 有序矩阵中第K小的元素

这道题的矩阵每一行、每一列都非严格递增,让你找第K小的元素。两种主流解法:一是多路归并+堆,二是二分答案。

堆的做法类似合并K个有序序列,把每一行的第一个元素预先放入堆里,每次弹出最小,再将该行下一列的元素入堆,弹出K次即可:

int kthSmallest(vector<vector<int>>& matrix, int k) { int n = matrix.size(); auto cmp = [&](const pair<int, int>& a, const pair<int, int>& b) { return matrix[a.first][a.second] > matrix[b.first][b.second]; }; priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> pq(cmp); for (int i = 0; i < n; i++) { pq.push({i, 0}); } int res = 0; while (k-- > 0) { auto [r, c] = pq.top(); pq.pop(); res = matrix[r][c]; if (c + 1 < n) pq.push({r, c + 1}); } return res; }

时间复杂度O(k log n),当k接近n^2时会退化为O(n^2 log n),但实际题目k一般不大。另一种二分答案法是O(n log(max-min)),在k很大的时候更稳定。两种方法面试都值得掌握,我个人的建议是优先掌握堆方法,因为和多路归并是同一个套路,好记;如果时间充裕,再学二分解法作为“进阶方案”备用。

4. 面试现场的经验心得:关于堆的本质认知

4.1 堆和栈的误区:别再被“堆栈”两个字绕晕

热词里有人搜“堆和栈”,这其实是两个层次的混淆。在算法题里,堆和栈是两种完全不同的数据结构,一个是完全二叉树,一个是线性表;在程序运行内存里,堆是动态内存分配区,栈是函数调用栈。两个维度千万不要混在一起。

很多初学者问:栈不也能O(1)取最大值吗?不是,普通栈O(1)只能取尾元素,如果你维护一个单调栈,确实可以在O(1)取某个方向上的极值,但单调栈只能解决“一次性扫描”的问题,插入一个新元素时单调栈需要弹出大量元素,摊还分析下很多场景仍然O(n)。而堆的核心优势是每次插入、删除都是严格的O(log n),适合反复动态变化的场景。

一句话总结:如果你需要在一堆不断增删的数据里反复取极值,用堆;如果只需要在静态序列上扫描一次维护一个单调的窗口,用单调栈/单调队列更合适。这个区分在面试里非常常见,务必想清楚。

4.2 TopK问题:堆和快速选择的取舍

TopK是堆最重要的应用场景,但堆不是唯一解法。我在面试中会先给出堆解法,然后跟面试官讨论三种方案:

方案时间复杂度空间复杂度适用场景
排序O(n log n)O(1)数据量小,需要全部有序
O(n log K)O(K)数据量大,K远小于n,适合流式数据
快速选择平均O(n)O(1)数据一次性给定,不需要动态插入

这里有个容易被忽略的点:堆特别适合流式数据。如果数据是一个一个到来的,你没法一次性排序,也没法做快速选择,只能用堆维护当前TopK。这是堆的不可替代性,也是面试官最想听到的理解。

4.3 C++优先队列的自定义比较器:方向陷阱详解

这一段是很多人的痛点,我必须展开写。C++的priority_queue比较器语义和sort完全不同,这导致无数人翻车。

reduce到一句话:priority_queue的比较器返回true时,表示第一个元素排在第二个元素后面(优先级更低),所以它实际上定义的是弱序中的“优先级关系”,而不是“排序关系”。

  • priority_queue<int>默认是大根堆,因为默认比较器less<int>a < b返回true时表示a排在后面,所以较大的数优先级更高。
  • priority_queue<int, vector<int>, greater<int>>是小根堆,因为a > b返回true时表示a排在后面,所以较小的数优先级更高。

如果你用lambda自定义,想创建小根堆,需要写:

auto cmp = [](int a, int b) { return a > b; // 注意是 >,不是 < }; priority_queue<int, vector<int>, decltype(cmp)> pq(cmp);

a > b返回true,表示a位于b之后,a的优先级更低,所以小的数在堆顶。这个写法看起来像反的,但实际上完全正确。最好的自检方法是:往堆里push三个数,打印堆顶,看是不是你期望的极值。

Python用户会舒服很多,heapq默认就是小根堆,计算第K大时只需要用-x入堆技巧即可。Java的PriorityQueue默认也是小根堆。只有C++需要额外注意这个坑。

4.4 堆和快排结合:找前K个高频单词的进阶思考

hot100里没有收录“前K个高频单词”,但它是347的一个重要变体,面试中经常出现。它要求频率降序、同频率按字典序升序。用堆的时候,比较器会变得很微妙,你需要在堆里实现“频率少优先弹出、同频率字典序大的优先弹出”,这样堆里剩下的才是正确答案。

用C++自定义比较器:

auto cmp = [](pair<string, int>& a, pair<string, int>& b) { if (a.second != b.second) return a.second > b.second; return a.first < b.first; };

这里的逻辑是:频率小的优先级低,同等频率下字典序大的优先级低,这样留在堆里的就是频率大且字典序小的单词。换句话说,你要反过来想——堆在弹出时淘汰谁,剩下的就是你要的答案。这个思考方式对TopK变体题特别管用。

5. 工程场景里的“堆”:不只是刷题

5.1 从TopK到海量数据:堆在外排序中的角色

很多人觉得堆只是面试用的,实际工作中用不到。其实堆在工程里的应用相当广泛,最常见的场景就是海量数据的排序与归并。

举个例子:你有一份几百GB的日志文件,机器的内存只有几GB,怎么排序?标准做法是外部排序:把大文件切分成多个可以载入内存的小块,每一块内部用快速排序排好,然后维护一个大小为“文件路数”的小根堆,每次从堆顶取最小元素写入输出文件,再从对应的块中补充下一个元素。

这就是堆的多路归并在工程中最为典型的一个落地。它的核心价值在于:内存里只需要保存每路当前的最小值,不需要把整个数据载入内存。理解了这个场景,再回头看23题合并K个升序链表,你会觉得它不是一个孤立的题目,而是在模拟真实世界的数据处理过程。

再比如常见的求日活Top10榜单、热卖商品Top100,这类场景的本质也是流式TopK。数据一条一条地来,内存有限,不能存全量数据,那你只能维护一个大小为K的小根堆。这就是堆算法在海量数据场景下最直接的应用。

5.2 一个容易被忽略的细节:优先队列与“懒删除”

在实际编码中,优先队列删除任意一个元素是很麻烦的,因为堆只支持删除堆顶。那如果业务上有需求要删除一个不在堆顶的元素怎么办?比如在一个动态榜单里,某个商品的分数变了,需要在堆里更新它的位置。

常见的做法是懒删除:你并不立刻在堆中删除该元素,而是再插入一个新元素表示新状态,被淘汰的旧状态后面弹出堆顶时再检查是否为垃圾数据,是的话直接跳过。这个技巧在不支持任意删除的语言环境中非常实用。配合一个unordered_map记录每个元素当前的有效状态即可。

刷题虽然不需要频繁用懒删除,但理解它有助于你看懂很多开源的定时器实现、任务调度器实现。堆+懒删除的组合,在工程里出现频率极高。

5.3 提醒:编译器的堆空间不足与内存维度的“堆”

热词里出现了“编译器的堆空间不足”,这跟算法题里讨论的数据结构堆不是一个东西,但在面试的角落里可能会交汇。内存维度的堆是动态分配内存的区域,使用new/malloc分配的内存都放在这里,如果不释放,长期运行的程序就会遇到“内存溢出”“堆空间不足”。

在刷题场景下,偶尔也会遇到类似问题:递归太深导致栈溢出,或者一次性把超大数组塞进内存导致堆溢出。后者通常不是因为程序写错了,而是算法空间复杂度太高。举个例子,如果你用排序解决第K大问题,空间是O(n),用堆解决则是O(k)。当n是千万级别时,O(k)的方案可能内存占用只有几MB,O(n)的方案可能直接爆掉。

所以“为什么这个题要用堆”,很多时候不只是为了时间复杂度,更是为了空间复杂度。这个角度面试中值得主动提出来,因为它展现了你对资源消耗的敏感度。

6. 常见问题与排查技巧实录

6.1 堆题调试三板斧:边界、比较器、空指针

堆题的bug其实高度集中,我把踩过的坑总结成一份排查清单,每次WA或者RE,按顺序检查这三样:

第一,边界条件。第K大问题里,K是否等于1或等于n?堆的大小变化是否符合预期?数据流中位数里,第一次插入时两个堆都为空,代码会不会崩溃?合并K个链表时,所有链表都是空的,dummy节点能不能正确返回空?

第二,比较器方向。C++自定义比较器的方向对不对?你是否用完sort的习惯误写了<?堆里存的是pair,比较的是first还是second?很多TopK变体题,pair的两个字段一个要升序一个要降序,细节极多。

第三,空指针与空容器。弹出堆顶之前检查空了吗?链表题的dummy节点处理了吗?矩阵题的行索引、列索引会不会越界?这些错误有个共同特征:编译不报错、逻辑跑起来偶尔错,特别难排查。

6.2 数据流中位数:双堆平衡条件的调试心得

双堆题是我见过初学者最容易写乱的一道。常见错误是:插入逻辑和平衡逻辑混在一起,最后两个堆的大小要么差太多,要么堆顶关系错误(左侧堆顶大于右侧堆顶)。

我的调试建议是写一个辅助函数来验证堆的性质:

bool isValid() { if (left.size() < right.size()) return false; if (left.size() > right.size() + 1) return false; if (left.empty() || right.empty()) return true; return left.top() <= right.top(); }

每次addNum之后调用它,返回false就说明平衡逻辑写错了。这个辅助函数同样适用于面试现场:它可以让面试官立刻明白你对自己代码的正确性是有把握的,而不是写完就草草了事。

6.3 堆排序手写题:面试官到底想看什么

有时候面试官不问你堆题,而是直接让你手写堆排序。这其实是对堆理解的终极考验。我建议按照“建堆 + 反复交换堆顶与末尾”这个框架来写:

void heapSort(vector<int>& a) { int n = a.size(); // 建堆:从最后一个非叶子节点开始下沉 for (int i = n / 2 - 1; i >= 0; i--) { down(a, i, n); } // 依次取出堆顶放到末尾 for (int i = n - 1; i > 0; i--) { swap(a[0], a[i]); down(a, 0, i); } }

这里最容易被问的是“建堆为什么从n/2-1开始”,因为n/2-1是最后一个非叶子节点的下标,叶子节点没有孩子,不需要下沉。整个建堆的时间复杂度是O(n),不是O(n log n),这一点很多人会算错。推导思路是各层节点数乘以它的高度求和,最后收敛为一个常数倍的n。

堆排序不是稳定的排序算法,这是一个高频考点。原因是堆调整过程中,值相同的元素可能在数组中做跨越式移动,打破原有顺序。面试答到这里,基本可以收尾了。

7. 刷题顺序建议与小技巧分享

如果你正准备刷hot100的堆题,我建议按下面的顺序来,难度阶梯比较合理:

    1. 最后一块石头的重量——堆的入门,熟悉API
    1. 数组中的第K个最大元素——掌握TopK套路
    1. 前K个高频元素——哈希+堆配合,理解pair比较器
    1. 合并K个升序链表——多路归并,队列中存指针
    1. 数据流的中位数——双堆,进阶动态维护
    1. 有序矩阵中第K小的元素——多路归并变体,顺便学二分

每做完一道,我建议你追问自己三个问题:这道题如果不用堆,还能怎么做?复杂度差在哪里?如果数据是不断流入的(流式),哪种方案仍然可用?

把这三个问题的答案写在这道题旁边,会形成你自己的“堆题方法论”。我到后来回顾,发现自己面试时能快速反应出最佳方案,靠的就是这种整理,而不是盲目刷得多。

另一个小技巧是:把每一道堆题的“比较器”单独抽出来练习。因为堆题一半的难度在比较器上,而比较器又能通用到其他题里。比如把347的pair比较器改一改,就是前K个高频单词的解法;把23的节点比较器改成数组下标比较,就是合并K个排序数组的解法。练熟比较器,等于一次掌握五六道题。

我个人还有一个习惯:一题三写。同一道题,用C++的priority_queue写一遍,用Python的heapq写一遍,再手写一份数组堆模拟。前两种帮你熟悉工业级API,后一种帮你理解堆的底层。三种写过之后,这道题的理解深度完全不一样。

最后再分享一个小技巧:在笔试或者白板面试那种紧张环境下,堆题最忌讳的就是一上来就写代码。先花几秒钟大声说出你选的堆类型、性质、以及堆中元素代表什么含义。比如“我用一个小根堆维护当前最大的K个元素,堆顶就是第K大的”。这句话说完,你的思路已经固定,代码基本不会跑偏。这个“先说后写”的习惯,尤其适合堆这种边界条件多、比较器容易写反的题型,能帮你少踩至少一半的坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 22:38:38

SSM框架实战:衡水特产展销系统开发全流程解析

做衡水特产相关的系统开发&#xff0c;其实是个挺有意思的选题。地方特产市场这几年一直在往线上走&#xff0c;但真正接地气的平台并不多。SSM262的衡水特产展销系统&#xff0c;从名字就能看出技术栈——SSM框架&#xff0c;也就是Spring、SpringMVC、MyBatis这三件套&#x…

作者头像 李华
网站建设 2026/9/24 22:38:18

TensorFlow 2.x实战指南:从环境配置到Transformer回归与PyTorch对比

关于新项目到底选TensorFlow还是PyTorch&#xff0c;这个问题我几乎每周都会被人问起。尤其是一些刚入行或者准备转AI方向的朋友&#xff0c;似乎总觉得选错框架就会“输在起跑线”。但认真聊下来我发现&#xff0c;大多数人纠结的点其实都跑偏了——他们把选择框架等同于选择了…

作者头像 李华
网站建设 2026/9/24 22:38:16

Source Insight实战指南:从主题配置到性能优化

很多新入行的朋友第一次看到我还在用Source Insight时&#xff0c;第一反应都是&#xff1a;这老古董怎么还活着&#xff1f;确实&#xff0c;和那种装几十个插件、启动时疯狂加载的现代编辑器相比&#xff0c;Source Insight的界面就像上个世纪的产物。但你真要扎进一个几十万…

作者头像 李华
网站建设 2026/9/24 22:37:26

基于Python的电影数据可视化分析系统:从爬虫采集到Web展示全流程

简介&#xff1a;面向毕业设计场景的电影数据可视化分析系统源码包&#xff0c;适合计算机相关专业学生用于课程设计、毕设参考或数据分析实践。项目将豆瓣电影数据爬取后存入SQLite&#xff0c;基于Flask搭建后端&#xff0c;结合ECharts、Bootstrap、WordCloud完成前端可视化…

作者头像 李华
网站建设 2026/9/24 22:37:17

C++枚举类完全指南:类型安全、位掩码与状态机实战

写这篇的时候&#xff0c;我脑子里最先浮出来的是前两年维护过的一个老项目。角色状态全部用 int 常量表示&#xff0c;0 是待机&#xff0c;1 是跑步&#xff0c;2 是攻击&#xff0c;后来要加浮空、硬直、受击后退&#xff0c;结果某天有人把两个状态的数值写重了&#xff0c…

作者头像 李华
网站建设 2026/9/24 22:37:07

Python日志记录实战:从入门到生产级配置体系

我干了这么多年Python&#xff0c;有个体会越来越深&#xff1a;日志记录&#xff08;Logging&#xff09;就是程序的“黑匣子”。飞机不能没有黑匣子&#xff0c;生产环境里跑的服务也不能没有像样的日志。能用好Python自带的logging模块&#xff0c;跟只会print("xxx&qu…

作者头像 李华