中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 例如
arr = [2,3,4]的中位数是3。- 例如
arr = [2,3]的中位数是(2 + 3) / 2 = 2.5。
解法1 暴力解法
思路
想要快速的找到中位数,最简单的办法就是每次插入都进行排序,最简单的排序就是插入排序。
具体思路就是每次插入找出该数字应该在的正确大小索引,找中位数则根据数组长度奇偶来返回。
代码
class MedianFinder {
private arr: number[];
constructor() {
this.arr = [];
}
addNum(num: number): void {
// 插入排序:找到插入位置
let left = 0, right = this.arr.length;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (this.arr[mid] < num) {
left = mid + 1;
} else {
right = mid;
}
}
this.arr.splice(left, 0, num); // 插入
}
findMedian(): number {
const n = this.arr.length;
if (n % 2 === 1) {
return this.arr[Math.floor(n / 2)];
} else {
const mid = n / 2;
return (this.arr[mid - 1] + this.arr[mid]) / 2;
}
}
}
时空复杂度
时间复杂度:初始化和 findMedian 都是 O(1),addNum 是 O(q),其中 q 是 addNum 的调用次数
空间复杂度:O(q)
解法2 大小堆
思路
我们利用堆顶存放数据的特点可以来优化一下。分别构建一个大根堆 A 和一个小根堆 B ,始终保持大根堆的元素个数大于等于小根堆的元素个数。
插入逻辑保持如下规则:
-
如果当前
A.size() !== B.size():说明 A 的元素比 B 多。为了维持平衡,我们应往 B 插入一个数。 但为了保证堆的有序性,我们:先把新数放入 A 然后把 A 的堆顶(最大值)弹出,插入 B。这样大的值“推”到了右边的小根堆中,保证中位数分布合理。 -
如果
A.size() === B.size():当前两边平衡,新数可以先进入右边(B)。然后从 B 弹出最小的值,插入 A(让左边继续保持比右边多一个)
代码
class MedianFinder {
private left;
private right;
constructor() {
this.left = new MaxPriorityQueue();
this.right = new MinPriorityQueue();
}
addNum(num: number): void {
if (this.left.size() === this.right.size()) {
this.right.enqueue(num);
this.left.enqueue(this.right.dequeue());
} else {
this.left.enqueue(num);
this.right.enqueue(this.left.dequeue());
}
}
findMedian(): number {
if (this.left.size() > this.right.size()) {
return this.left.front();
}
return (this.left.front() + this.right.front()) / 2;
}
}
时空复杂度
时间复杂度:初始化和 findMedian 都是 O(1),addNum 是 O(logq),其中 q 是 addNum 的调用次数。每次操作堆需要 O(logq) 的时间。
空间复杂度:O(logq)