Article / 文章
LeetCode 第295题:数据流的中位数
中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。 设计一个支持以下两种操作的数据结构: - void addNum(int num) - 从数据流中添加一个整数到数据结构中。 - double findMedian() - 返回目前所有元素的中位数。
📖 文章摘要
本文详细解析LeetCode第295题“数据流的中位数”,这是一道考察堆(优先队列)和数据流处理的困难难度题目。文章提供了双堆和有序数组两种实现方案,包含C#、Python、C++三种语言实现,配有详细的算法分析和性能对比。适合学习堆和数据流处理的读者。
核心知识点: 堆、优先队列、数据流处理
难度等级: 困难
推荐人群: 具备基础算法知识,想要提升数据结构应用能力的开发者
题目描述
中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。
设计一个支持以下两种操作的数据结构:
void addNum(int num)- 从数据流中添加一个整数到数据结构中。double findMedian()- 返回目前所有元素的中位数。
示例
示例 1:
输入:
["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"]
[[], [1], [2], [], [3], []]
输出:
[null, null, null, 1.5, null, 2.0]
解释:
MedianFinder medianFinder = new MedianFinder();
medianFinder.addNum(1); // arr = [1]
medianFinder.addNum(2); // arr = [1, 2]
medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3); // arr[1, 2, 3]
medianFinder.findMedian(); // 返回 2.0
提示
-105 <= num <= 105- 在调用
findMedian之前,数据结构中至少有一个元素 - 最多调用
addNum和findMedian方法5 * 104次
解题思路
本题可以使用两种方法来实现:
-
双堆法:
- 使用一个大顶堆存储较小的一半数字
- 使用一个小顶堆存储较大的一半数字
- 保持两个堆的大小平衡(相等或相差1)
- 中位数可以通过堆顶元素快速计算
-
有序数组法:
- 使用有序数组存储所有数字
- 使用二分查找确定插入位置
- 中位数可以通过下标直接获取
- 适用于数据量较小的情况
图解思路
双堆结构分析表
| 操作 | 大顶堆 | 小顶堆 | 中位数 |
|---|---|---|---|
| 初始状态 | [] | [] | - |
| addNum(1) | [1] | [] | 1.0 |
| addNum(2) | [1] | [2] | 1.5 |
| addNum(3) | [1,2] | [3] | 2.0 |
| addNum(4) | [1,2] | [3,4] | 2.5 |
平衡维护步骤表
| 步骤 | 操作 | 目的 |
|---|---|---|
| 1 | 比较与堆顶元素 | 决定加入哪个堆 |
| 2 | 插入对应的堆 | 保持有序性 |
| 3 | 检查大小差异 | 确保平衡 |
| 4 | 必要时移动元素 | 调整平衡 |
代码实现
C# 实现
public class MedianFinder {
private PriorityQueue<int, int> maxHeap; // 大顶堆,存储较小的一半
private PriorityQueue<int, int> minHeap; // 小顶堆,存储较大的一半
public MedianFinder() {
maxHeap = new PriorityQueue<int, int>();
minHeap = new PriorityQueue<int, int>();
}
public void AddNum(int num) {
if (maxHeap.Count == 0 || num < maxHeap.Peek()) {
maxHeap.Enqueue(num, -num);
} else {
minHeap.Enqueue(num, num);
}
// 平衡两个堆
if (maxHeap.Count > minHeap.Count + 1) {
var val = maxHeap.Dequeue();
minHeap.Enqueue(val, val);
} else if (minHeap.Count > maxHeap.Count) {
var val = minHeap.Dequeue();
maxHeap.Enqueue(val, -val);
}
}
public double FindMedian() {
if (maxHeap.Count > minHeap.Count) {
return maxHeap.Peek();
}
return (maxHeap.Peek() + minHeap.Peek()) / 2.0;
}
}
Python 实现
from heapq import *
class MedianFinder:
def __init__(self):
self.max_heap = [] # 存储较小的一半(取负数实现大顶堆)
self.min_heap = [] # 存储较大的一半
def addNum(self, num: int) -> None:
if len(self.max_heap) == 0 or num < -self.max_heap[0]:
heappush(self.max_heap, -num)
else:
heappush(self.min_heap, num)
# 平衡两个堆
if len(self.max_heap) > len(self.min_heap) + 1:
heappush(self.min_heap, -heappop(self.max_heap))
elif len(self.min_heap) > len(self.max_heap):
heappush(self.max_heap, -heappop(self.min_heap))
def findMedian(self) -> float:
if len(self.max_heap) > len(self.min_heap):
return -self.max_heap[0]
return (-self.max_heap[0] + self.min_heap[0]) / 2
C++ 实现
class MedianFinder {
private:
priority_queue<int> maxHeap; // 大顶堆,存储较小的一半
priority_queue<int, vector<int>, greater<int>> minHeap; // 小顶堆,存储较大的一半
public:
MedianFinder() {}
void addNum(int num) {
if (maxHeap.empty() || num < maxHeap.top()) {
maxHeap.push(num);
} else {
minHeap.push(num);
}
// 平衡两个堆
if (maxHeap.size() > minHeap.size() + 1) {
minHeap.push(maxHeap.top());
maxHeap.pop();
} else if (minHeap.size() > maxHeap.size()) {
maxHeap.push(minHeap.top());
minHeap.pop();
}
}
double findMedian() {
if (maxHeap.size() > minHeap.size()) {
return maxHeap.top();
}
return (maxHeap.top() + minHeap.top()) / 2.0;
}
};
执行结果
C# 实现
- 执行用时:408 ms
- 内存消耗:98.2 MB
Python 实现
- 执行用时:368 ms
- 内存消耗:35.8 MB
C++ 实现
- 执行用时:296 ms
- 内存消耗:116.8 MB
性能对比
| 语言 | 执行用时 | 内存消耗 | 特点 |
|---|---|---|---|
| C# | 408 ms | 98.2 MB | 代码结构清晰,性能适中 |
| Python | 368 ms | 35.8 MB | 实现简洁,内存占用小 |
| C++ | 296 ms | 116.8 MB | 性能最优,内存占用较大 |
代码亮点
- 🎯 使用双堆结构实现高效查找
- 💡 巧妙的平衡维护策略
- 🔍 边界条件处理完善
- 🎨 代码结构清晰易懂
常见错误分析
- 🚫 未正确维护堆的平衡
- 🚫 插入顺序错误
- 🚫 中位数计算错误
- 🚫 整数除法未转换为浮点数
解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 双堆法 | O(log n) | O(n) | 插入和查找都很快 | 需要额外空间 |
| 有序数组法 | O(n) | O(n) | 实现简单 | 插入较慢 |
相关题目
📖 系列导航
🔥 算法专题合集 - 查看完整合集
📢 关注合集更新:点击上方合集链接,关注获取最新题解!目前已更新第295题。
💬 互动交流
感谢大家耐心阅读到这里!希望这篇题解能够帮助你更好地理解和掌握这道算法题。
如果这篇文章对你有帮助,请:
- 👍 点个赞,让更多人看到这篇文章
- 📁 收藏文章,方便后续查阅复习
- 🔔 关注作者,获取更多高质量算法题解
- 💭 评论区留言,分享你的解题思路或提出疑问
你的支持是我持续分享的动力!
💡 一起进步:算法学习路上不孤单,欢迎一起交流学习!