某大厂员工:强烈建议集团对P9及以上的领导进行智力筛查、记忆力筛查、老年痴呆筛查,特别是一些上了年纪,一心想要在公司退休的人。
真有员工把话挑明了:建议给P9以上领导做个智力、记忆力、老年痴呆筛查。听着像气话,其实一股子打工人憋久了的真情实感。
为啥会骂到这一步?大家也不是闲的,主要是有些老领导真就那套东西反复讲,会上拍脑袋,会后全忘,流程绕三圈,最后还得下面的人擦屁股。你说他不管吧,他处处要过问;你说他真懂吧,问两句又开始飘。
这话虽然毒,但职场里见过的人都懂。很多公司最大的问题不是没人干活,是上面有些人年纪、位置、惯性全叠满了,还坚信自己一句话能定生死。底下人改方案改到半夜,他第二天来一句“我昨天不是这个意思”。你说HR看完这类爆料,眼皮估计都得跳一下。
算法题:数据流的中位数
堆一把数字进来,要求你随时给中位数。这个题看着像“查一下数组中间那个数”就完了,真上手一写,很多人第一版就会栽在复杂度上。
最直觉的写法,是来一个数就塞进列表,然后排序,再取中间位置。代码不难看懂,但这玩意儿一旦数据流长一点,性能就开始发虚。你每插一次都全量排序,代价太高,这种代码我一般在面试里看到,第一眼就不太信。题目既然强调“数据流”,意思就不是让你离线算一次,而是边来边维护。这个地方要换思路。
这题的关键,不在“怎么找中位数”,而在“怎么让中间位置始终稳定”。
我一般会把数据拆成两半:
左边一堆,保存较小的一半; 右边一堆,保存较大的一半。
然后保证两个条件:
左边所有数都不大于右边所有数 两边数量差不超过 1
这样中位数就很好拿了。 如果总数是奇数,中位数就是元素更多那一边的堆顶。 如果总数是偶数,中位数就是两边堆顶的平均值。
Python 里 heapq 只提供小根堆,没有现成大根堆,这个也不是什么事,把左边的数取相反数塞进去就行。
代码直接看,别绕概念:
import heapq
classMedianFinder:
def__init__(self):
# left 用负数模拟大根堆,保存较小的一半
self.left = []
# right 是小根堆,保存较大的一半
self.right = []
defadd_num(self, num: int) -> None:
# 先往 left 塞,再把 left 最大的挪到 right
heapq.heappush(self.left, -num)
heapq.heappush(self.right, -heapq.heappop(self.left))
# 保证 left 的数量不少于 right
if len(self.right) > len(self.left):
heapq.heappush(self.left, -heapq.heappop(self.right))
deffind_median(self) -> float:
if len(self.left) > len(self.right):
return float(-self.left[0])
return (-self.left[0] + self.right[0]) / 2.0
这段代码短,但是味道是对的。
比如数据流依次进来:5, 2, 10, 4
过程大概是这样:
来 5:左边 [5],中位数 5来 2:左边 [2],右边[5],中位数(2+5)/2 = 3.5来 10:左边 [5,2],右边[10],中位数 5来 4:左边 [4,2],右边[5,10],中位数 4.5
可以顺手跑一下:
mf = MedianFinder()
for x in [5, 2, 10, 4]:
mf.add_num(x)
print(f"add {x}, median = {mf.find_median()}")
输出:
add 5, median = 5.0
add 2, median = 3.5
add 10, median = 5.0
add 4, median = 4.5
这个题真正该答出来的,不只是“双堆”三个字,而是你得知道为什么这么维护。
因为堆顶永远是当前这一半里最有代表性的那个数。左边堆顶是“小的一半里最大的”,右边堆顶是“大的一半里最小的”。中位数卡在中间,天然就落在这两个位置附近。这个结构一旦稳定下来,每次插入只需要做有限次堆调整,单次复杂度是 O(log n),取中位数是 O(1)。这就比“每次全排序”的 O(n log n)靠谱太多了。
再多说一句,这题还有个容易写歪的点:别一会儿比较 left,一会儿比较 right,塞来塞去把自己绕晕。上面这个写法我比较喜欢,顺序固定:
先丢 left, 再把 left 最大的挪到 right, 最后看数量要不要回调一次。
逻辑很顺,不容易漏边界。写算法题,代码短不是本事,短还不容易错,这才像能交上去的版本。