【Leetcode hot 100】295.数据流的中位数
问题链接
295.数据流的中位数
问题描述
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
例如 arr = [2,3,4] 的中位数是 3 。
例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。
实现 MedianFinder 类:
MedianFinder() 初始化 MedianFinder 对象。
void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。
示例 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(); // return 2.0
提示:
-10^5 <= num <= 10^5- 在调用
findMedian之前,数据结构中至少有一个元素 - 最多
5 * 10^4次调用addNum和findMedian
问题解答
题目分析
数据流的中位数问题核心挑战在于:数据流是动态添加的,需要频繁维护有序状态以快速计算中位数。若使用普通数组/列表,每次添加元素后排序的时间复杂度为 O(n log n),在最多 5*10^4 次调用的场景下会超时。
最优解决方案是使用 对顶堆(大顶堆 + 小顶堆),通过以下规则维护数据,确保 addNum 和 findMedian 操作的时间复杂度均为 O(log n):
- 大顶堆(maxHeap):存储数据流中较小的一半元素,堆顶为这部分的最大值(即中位数的候选之一)。
- 小顶堆(minHeap):存储数据流中较大的一半元素,堆顶为这部分的最小值(即中位数的候选之一)。
- 平衡规则:
- 元素总数为奇数时,确保大顶堆元素个数比小顶堆多 1(此时大顶堆堆顶即为中位数)。
- 元素总数为偶数时,确保两个堆元素个数相等(此时中位数为两堆顶平均值)。
- 始终保证大顶堆的堆顶 ≤ 小顶堆的堆顶(确保两堆元素有序划分)。
完整代码实现
import java.util.Collections;
import java.util.PriorityQueue;class MedianFinder {// 大顶堆:存储较小的一半元素(Java 默认是小顶堆,需用 Collections.reverseOrder() 反转)private final PriorityQueue<Integer> maxHeap;// 小顶堆:存储较大的一半元素private final PriorityQueue<Integer> minHeap;/*** 初始化 MedianFinder 对象* - maxHeap 初始化为大顶堆* - minHeap 初始化为默认小顶堆*/public MedianFinder() {maxHeap = new PriorityQueue<>(Collections.reverseOrder());minHeap = new PriorityQueue<>();}/*** 向数据流中添加元素 num* 核心逻辑:先按大小分配到对应堆,再调整堆的平衡*/public void addNum(int num) {// 1. 分配元素:若 num ≤ 大顶堆堆顶(或大顶堆为空),加入大顶堆;否则加入小顶堆if (maxHeap.isEmpty() || num <= maxHeap.peek()) {maxHeap.offer(num);} else {minHeap.offer(num);}// 2. 调整平衡:确保两堆元素个数差不超过 1,且大顶堆不小于小顶堆// 情况1:大顶堆元素过多(比小顶堆多 2 个),需转移堆顶到小顶堆if (maxHeap.size() > minHeap.size() + 1) {minHeap.offer(maxHeap.poll());}// 情况2:小顶堆元素过多(比大顶堆多 1 个),需转移堆顶到大顶堆else if (minHeap.size() > maxHeap.size()) {maxHeap.offer(minHeap.poll());}}/*** 计算并返回当前数据流的中位数*/public double findMedian() {// 元素总数为奇数:大顶堆元素多 1 个,堆顶即为中位数if (maxHeap.size() > minHeap.size()) {return maxHeap.peek();}// 元素总数为偶数:两堆顶平均值即为中位数(需转 double 避免整数除法)else {return (maxHeap.peek() + minHeap.peek()) / 2.0;}}// 测试代码(可直接在 LeetCode 中忽略,本地调试用)public static void main(String[] args) {MedianFinder medianFinder = new MedianFinder();medianFinder.addNum(1); // 数据流:[1]medianFinder.addNum(2); // 数据流:[1, 2]System.out.println(medianFinder.findMedian()); // 输出 1.5medianFinder.addNum(3); // 数据流:[1, 2, 3]System.out.println(medianFinder.findMedian()); // 输出 2.0}
}/*** Your MedianFinder object will be instantiated and called as such:* MedianFinder obj = new MedianFinder();* obj.addNum(num);* double param_2 = obj.findMedian();*/
关键细节解释
-
堆的初始化:
- Java 的
PriorityQueue默认是小顶堆,因此大顶堆需要通过Collections.reverseOrder()反转比较器。 - 初始化时两堆均为空,后续添加元素时会自动建立堆结构。
- Java 的
-
元素分配逻辑:
- 优先将元素加入大顶堆的条件是“大顶堆为空”或“num ≤ 大顶堆堆顶”,确保大顶堆始终存储较小的一半元素。
- 若 num 大于大顶堆堆顶,则加入小顶堆,确保小顶堆存储较大的一半元素。
-
平衡调整逻辑:
- 仅在添加元素后可能出现失衡(两堆大小差超过 1),此时通过“弹出多的堆的堆顶,加入另一堆”来修复平衡。
- 调整后始终满足
maxHeap.size() == minHeap.size()或maxHeap.size() == minHeap.size() + 1,保证中位数计算的正确性。
-
中位数计算:
- 奇数个元素:大顶堆多 1 个元素,堆顶就是排序后的中间元素。
- 偶数个元素:两堆顶分别是排序后的中间两个元素,平均值即为中位数(除以
2.0而非2,避免整数除法导致的精度丢失)。
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
MedianFinder() 初始化 | O(1) | O(1) |
addNum(int num) | O(log n) | O(n) |
findMedian() | O(1) | O(1) |
- 时间复杂度:堆的
offer()和poll()操作均为O(log n)(n 为当前数据流元素总数),peek()操作为O(1),因此整体效率极高。 - 空间复杂度:两堆共存储所有数据流元素,因此为
O(n)。
