某大厂员工爆料:强烈建议集团对P9及以上的领导进行智力筛查、记忆力筛查、老年痴呆筛查,特别一心想要在公司退休的人。
现在有些大厂高管给人的感觉,真不是“管理经验丰富”,员工都忍不住开喷,说建议给P9以上做个智力、记忆力筛查,重点看看那种年纪上来了、还死守位置等退休的领导。话糙,但你别说,评论区一堆人秒懂。
有人说这帮人开会永远三件套:重复老话、听不进人话、最后拍个离地的板。还有人更损,说他们不是没经验,是经验太旧,旧到还活在上个版本,公司早更新了,他们脑子还没打补丁。
最烦的其实不是年纪大,是明明跟不上,还特别爱指挥,张嘴战略,落地全靠下面的人擦屁股。年轻人加班改方案,他们一句“我记得以前不是这么做的”,HR看完估计都头皮发麻。
面试题:数据流的中位数
堆里这玩意,看着像模板题,真放到线上就不是那么回事了。
比如实时取监控指标中位数、直播间礼物价格中位数、撮合系统滑动统计,数据不是一次性给你一坨数组让你排完序再算,而是一条一条往里灌。你每来一个数都 sort 一遍,代码是能写,性能也是真的难看。
这题我一般不会先想“怎么求中位数”,而是先想两件事: 第一,数据流是持续进入的,之前排好的结果最好别动太多。 第二,中位数只关心中间,不关心两头那些离谱的大数小数。
所以这题的手感,基本就是两个堆。
一个大顶堆,装较小的一半数据。 一个小顶堆,装较大的一半数据。
这样一来:
大顶堆堆顶,就是“小的一半里最大的那个” 小顶堆堆顶,就是“大的一半里最小的那个”
中位数刚好就在这两个位置附近。
再加一条规矩,整个结构就稳了:两个堆的元素个数差不能超过 1,而且让大顶堆的元素个数始终大于等于小顶堆。
那中位数怎么取就很顺了:
总数是奇数,中位数就是大顶堆堆顶 总数是偶数,中位数就是两个堆顶平均值
代码直接看,别整太虚的。
import java.util.Collections;
import java.util.PriorityQueue;
publicclassMedianFinder{
// 存较小的一半,堆顶最大
privatefinal PriorityQueue<Integer> left =
new PriorityQueue<>(Collections.reverseOrder());
// 存较大的一半,堆顶最小
privatefinal PriorityQueue<Integer> right =
new PriorityQueue<>();
publicvoidaddNum(int num){
// 先放左边,再把左边最大的挪到右边
left.offer(num);
right.offer(left.poll());
// 保证 left 的数量 >= right
if (left.size() < right.size()) {
left.offer(right.poll());
}
}
publicdoublefindMedian(){
if (left.isEmpty()) {
return0.0;
}
if (left.size() > right.size()) {
return left.peek();
}
return (left.peek() + right.peek()) / 2.0;
}
publicstaticvoidmain(String[] args){
MedianFinder finder = new MedianFinder();
finder.addNum(5);
finder.addNum(2);
finder.addNum(8);
finder.addNum(3);
System.out.println(finder.findMedian()); // 4.0
}
}
这段写法我比较喜欢,原因不是“短”,而是稳定。 很多人会写一堆 if (num <= left.peek()) 这种分支,逻辑也对,但边界容易绕晕。尤其是空堆、重复值、负数一起进来的时候,调半天。
上面这个写法的思路很直接: 不管新来的数是谁,先丢左边; 再把左边最大的拨到右边; 最后如果右边比左边多了,就再拨回来。
这样天然保证:
左边所有元素 <= 右边所有元素 两边数量平衡
时间复杂度也合适:
addNum()是O(log n)findMedian()是O(1)
这题真正该记住的,不是“双堆”这三个字,而是这种拆问题的习惯: 你不是在维护完整有序数组,你只是在维护“中间那条线”两边的平衡。
题不难,难的是第一次别往 ArrayList + 插入排序 那个方向拐。那种代码一开始能跑,数据一上来就开始发热了。