共计 2996 个字符,预计需要花费 8 分钟才能阅读完成。
题目形容
这是 LeetCode 上的 295. 数据流的中位数 ,难度为 艰难。
Tag :「优先队列(堆)」
中位数是有序列表两头的数。如果列表长度是偶数,中位数则是两头两个数的平均值。
例如,
[2,3,4]
的中位数是 $3$[2,3]
的中位数是 $(2 + 3) / 2 = 2.5$
设计一个反对以下两种操作的数据结构:
void addNum(int num)
– 从数据流中增加一个整数到数据结构中。double findMedian()
– 返回目前所有元素的中位数。
示例:
addNum(1)
addNum(2)
findMedian() -> 1.5
addNum(3)
findMedian() -> 2
进阶:
- 如果数据流中所有整数都在 $0$ 到 $100$ 范畴内,你将如何优化你的算法?
- 如果数据流中
99%
的整数都在 $0$ 到 $100$ 范畴内,你将如何优化你的算法?
优先队列(堆)
这是一道经典的数据结构使用题。
具体的,咱们能够应用两个优先队列(堆)来保护整个数据流数据,令保护数据流左半边数据的优先队列(堆)为 l
,保护数据流右半边数据的优先队列(堆)为 r
。
显然,为了能够在 $O(1)$ 的复杂度内获得以后中位数,咱们该当令 l
为大根堆,r
为小根堆,并人为固定 l
和 r
之前存在如下的大小关系:
- 当数据流元素数量为偶数:
l
和r
大小雷同,此时动静中位数为两者堆顶元素的平均值; - 当数据流元素数量为奇数:
l
比r
多一,此时动静中位数为l
的堆顶原数。
为了满足上述说的奇偶性堆大小关系,在进行 addNum
时,咱们该当分状况解决:
-
插入前两者大小雷同,阐明插入前数据流元素个数为偶数,插入后变为奇数。咱们冀望操作完达到「
l
的数量为r
多一,同时双堆维持有序」,进一步分状况探讨:- 如果
r
为空,阐明以后插入的是首个元素,间接增加到l
即可; - 如果
r
不为空,且num <= r.peek()
,阐明num
的插入地位不会在后半局部(不会在r
中),间接加到l
即可; - 如果
r
不为空,且num > r.peek()
,阐明num
的插入地位在后半局部,此时将r
的堆顶元素放到l
中,再把num
放到r
(相当于从r
中置换一位进去放到l
中)。
- 如果
-
插入前两者大小不同,阐明前数据流元素个数为奇数,插入后变为偶数。咱们冀望操作完达到「
l
和r
数量相等,同时双堆维持有序」,进一步分状况探讨(此时l
必然比r
元素多一):- 如果
num >= l.peek()
,阐明num
的插入地位不会在前半部分(不会在l
中),间接增加到r
即可。 - 如果
num < l.peek()
,阐明num
的插入地位在前半部分,此时将l
的堆顶元素放到r
中,再把num
放入l
中(相等于从l
中替换一位进去当到r
中)。
- 如果
代码:
class MedianFinder {PriorityQueue<Integer> l = new PriorityQueue<>((a,b)->b-a);
PriorityQueue<Integer> r = new PriorityQueue<>((a,b)->a-b);
public void addNum(int num) {int s1 = l.size(), s2 = r.size();
if (s1 == s2) {if (r.isEmpty() || num <= r.peek()) {l.add(num);
} else {l.add(r.poll());
r.add(num);
}
} else {if (l.peek() <= num) {r.add(num);
} else {r.add(l.poll());
l.add(num);
}
}
}
public double findMedian() {int s1 = l.size(), s2 = r.size();
if (s1 == s2) {return (l.peek() + r.peek()) / 2.0;
} else {return l.peek();
}
}
}
- 工夫复杂度:
addNum
函数的复杂度为 $O(\log{n})$;findMedian
函数的复杂度为 $O(1)$ - 空间复杂度:$O(n)$
进阶
- 如果数据流中所有整数都在 $0$ 到 $100$ 范畴内,你将如何优化你的算法?
能够应用建设长度为 $101$ 的桶,每个桶别离统计每个数的呈现次数,同时记录数据流中总的元素数量,每次查找中位数时,先计算出中位数是第几位,从前往后扫描所有的桶失去答案。
这种做法相比于对顶堆做法,计算量上没有劣势,更多的是空间上的优化。
对顶堆解法两个操作中耗时操作复杂度为 $O(\log{n})$,$\log$ 操作常数不会超过 $3$,在极限数据 $10^7$ 状况下计算量依然低于耗时操作复杂度为 $O(C)$($C$ 固定为 $101$)桶计数解法。
- 如果数据流中
99%
的整数都在 $0$ 到 $100$ 范畴内,你将如何优化你的算法?
和上一问解法相似,对于 $1$% 采纳哨兵机制进行解决即可,在惯例的最小桶和最大桶两侧别离保护一个有序序列,即建设一个代表负无穷和正无穷的桶。
上述两个进阶问题的代码如下,但留神因为实在样例的数据分布不是进阶所形容的那样(不是绝大多数都在 $[0,100]$ 范畴内),所以会 TLE
。
代码:
class MedianFinder {TreeMap<Integer, Integer> head = new TreeMap<>(), tail = new TreeMap<>();
int[] arr = new int[101];
int a, b, c;
public void addNum(int num) {if (num >= 0 && num <= 100) {arr[num]++;
b++;
} else if (num < 0) {head.put(num, head.getOrDefault(num, 0) + 1);
a++;
} else if (num > 100) {tail.put(num, tail.getOrDefault(num, 0) + 1);
c++;
}
}
public double findMedian() {
int size = a + b + c;
if (size % 2 == 0) return (find(size / 2) + find(size / 2 + 1)) / 2.0;
return find(size / 2 + 1);
}
int find(int n) {if (n <= a) {for (int num : head.keySet()) {n -= head.get(num);
if (n <= 0) return num;
}
} else if (n <= a + b) {
n -= a;
for (int i = 0; i <= 100; i++) {n -= arr[i];
if (n <= 0) return i;
}
} else {
n -= a + b;
for (int num : tail.keySet()) {n -= tail.get(num);
if (n <= 0) return num;
}
}
return -1; // never
}
}
最初
这是咱们「刷穿 LeetCode」系列文章的第 No.295
篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,局部是有锁题,咱们将先把所有不带锁的题目刷完。
在这个系列文章外面,除了解说解题思路以外,还会尽可能给出最为简洁的代码。如果波及通解还会相应的代码模板。
为了不便各位同学可能电脑上进行调试和提交代码,我建设了相干的仓库:https://github.com/SharingSou…。
在仓库地址里,你能够看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其余优选题解。
更多更全更热门的「口试 / 面试」相干材料可拜访排版精美的 合集新基地 🎉🎉
本文由 mdnice 多平台公布