LeetCode 703. 数据流中的第 K 大元素
本节目标
将固定容量小根堆改造成可持续更新的数据结构,在每次插入后立即回答第 k 大。
这道题是堆与优先队列解题框架的第三道母题。它与“数组中的第 K 个最大元素”使用相同的不变量,但数据不再一次性给出,而是持续加入,并要求每次加入后立刻回答。
题意与约束
设计一个类:
- 构造时接收整数
k和初始数组nums; - 每次调用
add(val),把val加入数据流; - 返回当前全部元素中的第
k大元素。
题目保证查询时数据流中至少有 k 个元素。重复值参与排名,例如 [5, 5, 4] 的第 2 大仍是 5。
为什么不能每次重新排序
若每次 add 后都把全部历史数据重新排序,随着数据流增长,一次查询的成本也会不断增长,而且此前完成的排序工作被反复重做。
我们真正需要长期保存的仍然只是最大的 k 个元素。其余元素一旦低于当前淘汰线,将来也不会因为更大的新元素到来而重新进入前 k 名,因此可以永久舍弃。
在线维护同一个不变量
对象内部保存容量为 k 的小根堆。无论处理构造函数中的初始数组,还是后来调用 add 加入新值,都执行同一套更新:
把 val 加入小根堆
如果堆大小超过 k:
弹出堆顶
返回堆顶
每次更新结束后:
- 堆中保存截至当前最大的
k个元素; - 堆顶是这些元素中最小的一个;
- 因而堆顶就是当前第
k大。
构造过程也必须使用相同更新逻辑。若初始数组很长却全部保留,就破坏了空间只与 k 有关的目标。
一个持续更新的例子
设 k = 3,初始数组为 [4, 5, 8, 2],初始化后堆中保留 {4, 5, 8},堆顶为 4。
| 操作 | 调整后保留的三个最大元素 | 返回值 |
|---|---|---|
add(3) | {4, 5, 8} | 4 |
add(5) | {5, 5, 8} | 5 |
add(10) | {5, 8, 10} | 5 |
add(9) | {8, 9, 10} | 8 |
被淘汰的元素无需保留,因为以后只会加入更多元素,它们不可能重新成为第 3 大。
代码实现
C++ 和 Python 都把“小根堆 + 容量上限”封装在 KthLargest 类中。构造阶段逐个加入初始元素,add 方法恢复不变量并返回堆顶。
- C++
- Python
C++17
#include <functional>
#include <queue>
#include <vector>
using namespace std;
class KthLargest {
private:
int k;
priority_queue<int, vector<int>, greater<int>> top;
void addToHeap(int value) {
top.push(value);
if (static_cast<int>(top.size()) > k) {
top.pop();
}
}
public:
KthLargest(int k, vector<int>& nums) : k(k) {
for (int value : nums) {
addToHeap(value);
}
}
int add(int val) {
addToHeap(val);
return top.top();
}
};
Python 3
import heapq
class KthLargest:
def __init__(self, k, nums):
self.k = k
self.top = []
for value in nums:
self._add_to_heap(value)
def _add_to_heap(self, value):
heapq.heappush(self.top, value)
if len(self.top) > self.k:
heapq.heappop(self.top)
def add(self, val):
self._add_to_heap(val)
return self.top[0]
复杂度分析
- 构造时间复杂度:
O(n log k),其中n是初始数组长度。 - 单次
add时间复杂度:O(log k)。 - 空间复杂度:
O(k),与数据流累计长度无关。
易错点
- 保存全部历史数据,失去在线算法应有的空间优势。
- 只在
add中限制堆大小,却没有同样处理初始数组。 - 返回新加入的值或堆中最大值;正确答案始终是小根堆堆顶。
- 把第
k大理解为第k个不同值,错误去重。 - 认为小于当前堆顶的旧元素以后可能重新成为答案。随着新元素加入,前
k名的门槛只会保持或升高,不会降低。
模式迁移
离线问题变成数据流问题时,关键是寻找可以在一次更新后快速恢复的不变量。固定容量堆适合持续维护 Top K;如果要持续维护中位数,仅保留一侧的 k 个候选已经不够,需要两个堆共同维护数据的两半。