一、题目描述
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 例如
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
二、解题思路
为了解决这个问题,我们可以使用两个堆(优先队列)来维护数据流中的中位数。具体来说,我们使用一个大顶堆(最大堆)来存储较小的一半元素,以及一个小顶堆(最小堆)来存储较大的一半元素。这样做的目的是使得大顶堆的堆顶元素和小顶堆的堆顶元素分别代表当前所有元素中的较小和较大的中间值。
以下是具体的实现步骤:
-
初始化两个堆:一个大顶堆(记为
maxHeap
)用于存储较小的一半元素,一个小顶堆(记为minHeap
)用于存储较大的一半元素。 -
当添加一个数字时:
- 如果
maxHeap
为空或者数字小于等于maxHeap
的堆顶元素,我们将数字添加到maxHeap
中。 - 否则,我们将数字添加到
minHeap
中。 - 为了保持两个堆的大小平衡,如果
maxHeap
的大小比minHeap
的大小大2,我们需要将maxHeap
的堆顶元素移动到minHeap
中;反之亦然。
- 如果
-
当查询中位数时:
- 如果
maxHeap
和minHeap
的大小相同,中位数是两个堆顶元素的平均值。 - 如果它们的大小不同,中位数是大小较大的那个堆的堆顶元素。
- 如果
三、具体代码
import java.util.PriorityQueue;
import java.util.Collections;
public class MedianFinder {
private PriorityQueue<Integer> maxHeap; // 大顶堆,存储较小的一半元素
private PriorityQueue<Integer> minHeap; // 小顶堆,存储较大的一半元素
public MedianFinder() {
maxHeap = new PriorityQueue<>(Collections.reverseOrder()); // 创建大顶堆
minHeap = new PriorityQueue<>(); // 创建小顶堆
}
public void addNum(int num) {
if (maxHeap.isEmpty() || num <= maxHeap.peek()) {
maxHeap.offer(num);
} else {
minHeap.offer(num);
}
// 平衡两个堆的大小
if (maxHeap.size() > minHeap.size() + 1) {
minHeap.offer(maxHeap.poll());
} else if (minHeap.size() > maxHeap.size()) {
maxHeap.offer(minHeap.poll());
}
}
public double findMedian() {
if (maxHeap.size() == minHeap.size()) {
return (maxHeap.peek() + minHeap.peek()) / 2.0;
} else {
return maxHeap.peek();
}
}
}
// 使用示例
// MedianFinder medianFinder = new MedianFinder();
// medianFinder.addNum(1);
// medianFinder.addNum(2);
// System.out.println(medianFinder.findMedian()); // 输出 1.5
// medianFinder.addNum(3);
// System.out.println(medianFinder.findMedian()); // 输出 2.0
四、时间复杂度和空间复杂度
1. 时间复杂度
-
addNum(int num) 方法:
offer
方法将一个元素添加到优先队列中,其时间复杂度是 O(logN),其中 N 是优先队列中的元素数量。- 在
addNum
方法中,我们最多调用两次offer
方法,一次是添加元素到maxHeap
或minHeap
,另一次是在两个堆之间调整元素以达到平衡。因此,addNum
方法的时间复杂度是 O(logN)。
-
findMedian() 方法:
peek
方法获取优先队列的堆顶元素,其时间复杂度是 O(1)。- 在
findMedian
方法中,我们最多调用两次peek
方法(当两个堆的大小相等时)。由于peek
是常数时间操作,所以findMedian
方法的时间复杂度是 O(1)。
2. 空间复杂度
-
整体空间复杂度:
- 两个优先队列
maxHeap
和minHeap
用来存储所有输入的数字。在最坏的情况下,所有的数字都存储在这两个堆中,因此空间复杂度是 O(N),其中 N 是输入数字的总数。
- 两个优先队列
-
addNum(int num) 方法:
- 该方法没有使用额外的空间,除了存储输入数字所需的堆空间。因此,
addNum
方法的空间复杂度是 O(1)。
- 该方法没有使用额外的空间,除了存储输入数字所需的堆空间。因此,
-
findMedian() 方法:
- 该方法没有使用额外的空间,只是访问了两个堆的堆顶元素。因此,
findMedian
方法的空间复杂度是 O(1)。
- 该方法没有使用额外的空间,只是访问了两个堆的堆顶元素。因此,
五、总结知识点
-
类定义(Class Definition):
- 代码定义了一个名为
MedianFinder
的类,该类用于找到数据流中的中位数。
- 代码定义了一个名为
-
成员变量(Member Variables):
- 类中有两个成员变量
maxHeap
和minHeap
,它们都是PriorityQueue
类型,分别用于存储较小的一半和较大的一半元素。
- 类中有两个成员变量
-
构造函数(Constructor):
- 类的构造函数
MedianFinder()
负责初始化两个优先队列。maxHeap
使用Collections.reverseOrder()
来创建一个大顶堆,而minHeap
默认是一个小顶堆。
- 类的构造函数
-
优先队列(Priority Queue):
PriorityQueue
是一个基于优先级堆的无界优先队列,它提供了 O(logN) 时间复杂度的插入和删除操作。
-
堆操作(Heap Operations):
offer()
方法用于将元素插入到优先队列中。peek()
方法用于获取队列的头部元素,但不从队列中删除它。poll()
方法用于获取并移除队列的头部元素。
-
条件语句(Conditional Statements):
if-else
语句用于在addNum
方法中决定元素应该添加到哪个堆中,以及在两个堆之间进行平衡。
-
逻辑运算符(Logical Operators):
||
(逻辑或) 和&&
(逻辑与) 运算符用于addNum
方法中的条件判断。
-
算术运算(Arithmetic Operations):
- 在
findMedian
方法中,使用加法(+
)和除法(/
)来计算中位数。
- 在
-
类型转换(Type Conversion):
- 在计算中位数时,使用了一个整数除以 2.0 来确保结果是一个浮点数。
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。