数据流的中位数:用双堆优雅处理动态数据的中位数计算!
大家好,我是忍者算法。今天我们来聊一个非常有趣的题目 - 数据流中的中位数。这个问题看似简单,实则暗藏玄机,需要我们巧妙运用堆这个数据结构来解决。别担心!我会用最直观的方式,带你理解这个精妙的解决方案。
📚 生活中的中位数
想象你是一个体育老师,正在记录学生们的跑步成绩。每当有新的同学完成跑步,你都需要立即知道到目前为止所有成绩的中位数。这就是我们今天要解决的核心问题!
💡 问题是什么
用大白话说:我们要设计一个数据结构,支持两个操作:
- 添加一个新的数字到我们的数据集中
- 随时可以获取当前所有数字的中位数
比如说:
java
// 假设数据流是:[4,5,8,2,3]
addNum(4) // [4],中位数是4
addNum(5) // [4,5],中位数是4.5
addNum(8) // [4,5,8],中位数是5
addNum(2) // [2,4,5,8],中位数是4.5
addNum(3) // [2,3,4,5,8],中位数是4🤔 为什么这个问题有趣?
这个问题的挑战在于:
- 数据是流式输入的,我们不能预先知道所有数字
- 每次添加新数字后都要能快速得到中位数
- 数据规模可能很大,我们需要高效的解决方案
🚀 优雅的解决方案
我们可以用两个堆来解决这个问题:
- 一个大顶堆存储较小的一半数字
- 一个小顶堆存储较大的一半数字
java
class MedianFinder {
// 大顶堆,存储较小的一半数字
private PriorityQueue<Integer> smallerHalf;
// 小顶堆,存储较大的一半数字
private PriorityQueue<Integer> largerHalf;
public MedianFinder() {
// 初始化两个堆
smallerHalf = new PriorityQueue<>((a, b) -> b - a); // 大顶堆
largerHalf = new PriorityQueue<>(); // 小顶堆
}
public void addNum(int num) {
// 先把新数字放入大顶堆(较小的一半)
smallerHalf.offer(num);
// 确保smallerHalf中的最大数不超过largerHalf中的最小数
if (!smallerHalf.isEmpty() && !largerHalf.isEmpty() &&
smallerHalf.peek() > largerHalf.peek()) {
largerHalf.offer(smallerHalf.poll());
}
// 保持两个堆的大小平衡
// 允许smallerHalf比largerHalf最多多一个元素
if (smallerHalf.size() > largerHalf.size() + 1) {
largerHalf.offer(smallerHalf.poll());
}
if (largerHalf.size() > smallerHalf.size()) {
smallerHalf.offer(largerHalf.poll());
}
}
public double findMedian() {
if (smallerHalf.size() > largerHalf.size()) {
// 奇数个元素时,中位数就是smallerHalf的堆顶
return smallerHalf.peek();
} else {
// 偶数个元素时,中位数是两个堆顶的平均值
return (smallerHalf.peek() + largerHalf.peek()) / 2.0;
}
}
}📝 代码是怎么工作的?
让我们用一个具体的例子来看看这个结构是如何工作的:
假设数据流是:[4,5,8,2,3]
添加4:
- smallerHalf: [4]
- largerHalf: []
- 中位数:4
添加5:
- smallerHalf: [4]
- largerHalf: [5]
- 中位数:(4+5)/2 = 4.5
添加8:
- smallerHalf: [4]
- largerHalf: [5,8]
- 平衡后:
- smallerHalf: [4,5]
- largerHalf: [8]
- 中位数:5
依此类推...就像是在玩跷跷板,我们始终保持两边平衡,中位数自然就在中间!
🎯 要注意的关键点
堆的选择
- 为什么用大顶堆和小顶堆?
- 这样可以方便地获取中间的数字
平衡的维护
- 两个堆的大小差不超过1
- 较小堆的最大值不超过较大堆的最小值
处理偶数和奇数的情况
- 奇数个数时:中位数是较小堆的堆顶
- 偶数个数时:中位数是两个堆顶的平均值
💡 生活中的类似场景
这种设计思路在生活中很常见:
考试成绩统计
- 实时计算班级的中等成绩
- 新的成绩提交后快速更新
房价中位数
- 房地产市场的价格中位数
- 新房源上市后的即时统计
工资中位数
- 公司薪资水平的中位数
- 新员工入职后的即时更新
🎨 解决方案的可视化
svg
<svg viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg">
<!-- 背景 -->
<rect width="600" height="400" fill="#f8f9fa"/>
<!-- 标题 -->
<text x="300" y="40" font-size="20" fill="#1976d2" text-anchor="middle">双堆结构示意图</text>
<!-- 左边的大顶堆 -->
<g transform="translate(150,100)">
<text x="0" y="-20" text-anchor="middle" fill="#2196f3">较小的一半(大顶堆)</text>
<path d="M 0 0 L -60 60 L 60 60 Z" fill="none" stroke="#2196f3" stroke-width="2"/>
<circle cx="0" cy="10" r="20" fill="#2196f3"/>
<circle cx="-40" cy="70" r="20" fill="#2196f3"/>
<circle cx="40" cy="70" r="20" fill="#2196f3"/>
<text x="0" cy="15" fill="white" text-anchor="middle">5</text>
<text x="-40" y="75" fill="white" text-anchor="middle">3</text>
<text x="40" y="75" fill="white" text-anchor="middle">4</text>
</g>
<!-- 右边的小顶堆 -->
<g transform="translate(450,100)">
<text x="0" y="-20" text-anchor="middle" fill="#4caf50">较大的一半(小顶堆)</text>
<path d="M 0 0 L -60 60 L 60 60 Z" fill="none" stroke="#4caf50" stroke-width="2"/>
<circle cx="0" cy="10" r="20" fill="#4caf50"/>
<circle cx="-40" cy="70" r="20" fill="#4caf50"/>
<circle cx="40" cy="70" r="20" fill="#4caf50"/>
<text x="0" y="15" fill="white" text-anchor="middle">6</text>
<text x="-40" y="75" fill="white" text-anchor="middle">8</text>
<text x="40" y="75" fill="white" text-anchor="middle">7</text>
</g>
<!-- 中位数指示 -->
<g transform="translate(300,250)">
<text x="0" y="0" text-anchor="middle" font-size="16">中位数 = (5 + 6) / 2 = 5.5</text>
<path d="M -150 -130 L 0 -20 L 150 -130" fill="none" stroke="#ff5722" stroke-width="2" stroke-dasharray="5,5"/>
</g>
</svg>🌟 面试时怎么说?
开场白
- "我们可以用两个堆来维护数据流的中位数"
- "一个堆存较小的一半,一个堆存较大的一半"
解释优势
- 时间复杂度:添加数字O(logn),查找中位数O(1)
- 空间复杂度:O(n)存储所有数字
补充说明
- 解决方案的可扩展性
- 处理大数据流的能力
🎩 扩展思考
这个解决方案的思路还可以用在其他场景:
滑动窗口中位数
java// 在固定大小的窗口中维护中位数 class SlidingWindowMedian { // 类似的双堆结构,加上删除操作 }数据流的百分位数
java// 扩展到计算任意百分位的数值 class PercentileFinder { // 可以调整两个堆的比例 }
这个双堆的设计非常优雅,它告诉我们:有时候把数据分成两半处理,反而能得到更简单的解决方案!
作者:忍者算法 公众号:忍者算法 🎁 回复【刷题清单】获取LeetCode高频面试题合集 🧑💻 回复【代码】获取多语言完整题解 💡 回复【加群】加入算法交流群,一起进步 #算法面试 #LeetCode #堆 #数据结构
如果你对这个话题还有任何疑问,欢迎在评论区讨论!理解了这个问题,你会发现很多看似复杂的数据流问题都能用类似的思路来解决。