某东员工吐槽:干了一年,几乎天天10点下班,年底给了个B+,以为会涨薪,结果一分没有,从此以后早点走。。
刚刷到一个某东员工的吐槽,干了一整年,基本每天晚上十点才下班,饭点像假的,生活像外包给公司了。到年底一看绩效,B+。
他估计也琢磨着,辛苦一年,怎么也该有点表示。结果涨薪一看,零。不是少,是一分没有。
这就很魔幻了。你让人天天熬到十点,最后告诉他“表现还可以”,但钱不动。那这个B+到底是奖励,还是安慰奖啊。
最真实的是后面,他也不闹了,也不画饼了,直接开始早点走。以前十点,现在到点就撤。领导要是问,可能也没啥好说的。
公司用钱投票,员工也用时间投票。你给我什么反馈,我就调整什么投入。挺公平的,甚至有点过于文明了。
算法题:最大栈
订单回滚的时候,最大值取错了。
不是算法题里那种取错,是线上补偿脚本里真取错。栈里压进去一批价格变更记录,业务要撤掉“当前最大的那条调整”,结果代码用 max(stack) 扫了一遍,再 remove()。数据少的时候看不出来,数据一多,脚本跑得跟没睡醒一样。
这个地方我第一眼就不太信 remove()。
因为最大栈不是普通栈。
普通栈只关心一件事:最后进来的先出去。
最大栈还得多管一件事:随时知道当前最大值,甚至把这个最大值弹出去。
如果只是写个最简单版本,很多人会这么写:
classBadMaxStack:
def__init__(self):
self.items = []
defpush(self, value):
self.items.append(value)
defpop(self):
return self.items.pop()
defmax(self):
return max(self.items)
这代码不是不能用。
但问题也很明显,max() 每次都是全表扫描。你压了 10 万条数据,每次取最大值都扫一遍,后面不用看,CPU 肯定有意见。
更麻烦的是,如果业务还要 pop_max(),也就是弹出当前最大值,这种写法就开始别扭了。
value = max(stack)
stack.remove(value)
这行我一般不会放过。
remove() 删除的是第一个匹配值,不一定是离栈顶最近的那个最大值。最大值重复的时候,这里很容易悄悄错。
比如:
[5, 1, 5]
从栈的角度看,pop_max() 应该删掉后面的那个 5,因为它更靠近栈顶。
但 remove(5) 删除的是前面的那个 5。
这种 bug 很恶心,因为日志里看起来都叫 5,业务同学只会说“最大值是对的啊”,但顺序已经乱了。
我一般会先写一个能撑住现场的版本:主栈负责正常 push/pop,堆负责找最大值,再用一个 deleted 集合处理两边不同步的问题。
Python 的 heapq 是小根堆,所以最大值要取负数。重复值也要处理,所以每次入栈带一个自增编号。
代码如下:
import heapq
classMaxStack:
def__init__(self):
self.stack = [] # (value, seq)
self.heap = [] # (-value, -seq)
self.deleted = set()
self.seq = 0
defpush(self, value: int) -> None:
self.seq += 1
item = (value, self.seq)
self.stack.append(item)
heapq.heappush(self.heap, (-value, -self.seq))
def_clean_stack(self) -> None:
while self.stack and self.stack[-1][1] in self.deleted:
self.stack.pop()
def_clean_heap(self) -> None:
while self.heap:
_, neg_seq = self.heap[0]
seq = -neg_seq
if seq notin self.deleted:
break
heapq.heappop(self.heap)
defpop(self) -> int:
self._clean_stack()
ifnot self.stack:
raise IndexError("max stack is empty")
value, seq = self.stack.pop()
self.deleted.add(seq)
return value
deftop(self) -> int:
self._clean_stack()
ifnot self.stack:
raise IndexError("max stack is empty")
return self.stack[-1][0]
defpeek_max(self) -> int:
self._clean_heap()
ifnot self.heap:
raise IndexError("max stack is empty")
return -self.heap[0][0]
defpop_max(self) -> int:
self._clean_heap()
ifnot self.heap:
raise IndexError("max stack is empty")
neg_value, neg_seq = heapq.heappop(self.heap)
seq = -neg_seq
self.deleted.add(seq)
return -neg_value
这里有个细节,别忽略。
堆里存的是:
(-value, -seq)
为什么不是只存 -value?
因为最大值可能重复。
当两个值一样时,要弹出后进来的那个,也就是 seq 更大的那个。存成 -seq 之后,Python 小根堆会优先拿到更小的 -seq,也就是更大的 seq。
跑一下:
s = MaxStack()
for x in [5, 1, 5, 3]:
s.push(x)
print("top =", s.top())
print("max =", s.peek_max())
print("pop_max =", s.pop_max())
print("top =", s.top())
print("pop =", s.pop())
print("max =", s.peek_max())
输出应该是:
top = 3
max = 5
pop_max = 5
top = 3
pop = 3
max = 5
注意,pop_max() 弹掉的是第二个 5,不是第一个 5。
这才是栈语义里比较自然的处理。
这个实现里,push 是 O(log n),peek_max 和 pop_max 主要看堆,正常也是 O(log n)。pop 表面是 O(1),但因为有懒删除,偶尔会清理一些已经删除的元素。摊开看问题不大。
有些场景不需要 pop_max(),只需要随时知道最大值,那就别上堆,太重了。
维护两个栈就够:
classSimpleMaxStack:
def__init__(self):
self.data = []
self.max_data = []
defpush(self, value: int) -> None:
self.data.append(value)
ifnot self.max_data:
self.max_data.append(value)
return
self.max_data.append(max(value, self.max_data[-1]))
defpop(self) -> int:
ifnot self.data:
raise IndexError("stack is empty")
self.max_data.pop()
return self.data.pop()
defmax(self) -> int:
ifnot self.max_data:
raise IndexError("stack is empty")
return self.max_data[-1]
这个版本更干净。
每压入一个值,同时把“当前这一刻的最大值”也压进去。弹出时两个栈一起弹。它适合做指标窗口、括号解析、单调检查前的临时栈。
但它不能高效删除最大值。
所以我一般这么选:
只要 push/pop/max,用双栈。
只要出现 pop_max,别犹豫,用主栈加堆,懒删除处理一致性。
别小看这个数据结构,很多线上脚本慢,不是慢在数据库,不是慢在接口,就是慢在这种看起来“就一行 max()”的地方。
那一行扫起来,可一点都不客气。