1. 数据流中位数问题解析
1.1 问题定义与核心挑战
中位数计算看似简单,但在数据流场景下却暗藏玄机。传统的中位数计算需要对数据进行排序,这在静态数据集中没有问题,但当数据以流的形式不断到来时,每次都重新排序显然效率太低。假设数据流中有n个数字,每次排序的时间复杂度是O(nlogn),当n很大时(比如10^5量级),这样的操作完全不可接受。
这里的关键在于:我们需要一种数据结构,能够动态维护数据的有序性,同时支持快速查询中位数。这就是为什么对顶堆(Dual Heap)结构成为解决此类问题的黄金标准。
1.2 对顶堆的巧妙设计
对顶堆由两个堆组成:一个大根堆(small)和一个小根堆(big)。大根堆存储较小的一半数字,小根堆存储较大的一半数字。这种设计有以下几个精妙之处:
- 堆的性质保证了大根堆的堆顶是较小一半数字中的最大值,小根堆的堆顶是较大一半数字中的最小值
- 两个堆顶正好"相对",形成了数据的中位数边界
- 堆的插入和删除操作都是O(logn)时间复杂度,远优于排序的O(nlogn)
在实际实现时,我们需要特别注意保持两个堆的大小平衡。通常的做法是:
- 当总数为奇数时,让大根堆多存一个元素
- 当总数为偶数时,保持两个堆大小相等
这样设计的好处是:
- 奇数情况下,中位数直接就是大根堆的堆顶
- 偶数情况下,中位数是两个堆顶的平均值
1.3 实现细节与优化技巧
在LeetCode 295的实现中,我特别推荐使用第二种addNum方法(分类讨论法),原因如下:
- 代码更简洁:通过统一处理size相等和不等的情况,减少了条件判断
- 逻辑更清晰:无论num大小如何,都先放入一个堆,再通过pop和push调整
- 性能更稳定:避免了多次比较操作,减少了分支预测失败的可能性
这里有个重要的编程技巧:当两个堆size相等时,我们先将num放入小根堆,再把小根堆的顶部移到大根堆。这样做的效果是:
- 保证了新加入的数字一定会经过大小比较
- 自动维持了堆的大小关系
- 避免了直接比较num和堆顶的开销
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 滑动窗口中位数进阶问题
2.1 问题升级带来的挑战
滑动窗口中位数问题(LeetCode 480)在数据流中位数的基础上增加了两个难点:
- 窗口滑动时需要删除最旧的元
