Python技术迷

某东员工吐槽:干了一年,几乎天天10点下班,年底给了个B+,以为会涨薪,结果一分没有,从此以后早点走。。

刚刷到一个某东员工的吐槽,干了一整年,基本每天晚上十点才下班,饭点像假的,生活像外包给公司了。到年底一看绩效,B+。

他估计也琢磨着,辛苦一年,怎么也该有点表示。结果涨薪一看,零。不是少,是一分没有。

Image

这就很魔幻了。你让人天天熬到十点,最后告诉他“表现还可以”,但钱不动。那这个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()”的地方。

那一行扫起来,可一点都不客气。