老读者都知道,代码随想录算法训练营到第11天,已经过了数组、链表、哈希表这些基础关,开始往“栈和队列”这个更抽象的方向走了。这几道题——150. 逆波兰表达式求值、239. 滑动窗口最大值、347. 前 K 个高频元素——表面上看是三道题,实际上对应的是三种基础数据结构在算法题里的典型用法:栈的应用、队列的变形、堆的应用。说实话,我在刷这三题之前,栈和队列就停留在“会用API”的程度,但刷完这一天的内容,我开始真正理解“为什么需要这三种结构”,也理解了为什么很多面试官喜欢围绕这几题往下挖。
今天的题量不大,但信息密度很高。150 逆波兰表达式是栈最经典的场景;239 滑动窗口最大值是“单调队列”从概念到实战的敲门砖;347 前 K 个高频元素则是“小顶堆”这种数据结构的第一次正式登场。如果你刚过完基础内容、正准备进入中等难度的题目,这一天值得多花点时间吃透。
1. 150. 逆波兰表达式求值:栈的“消除”能力到底强在哪
1.1 为什么会有逆波兰表达式这玩意儿
先问一个问题:你平时写 (1 + 2) * 3 这种算式,计算机是怎么算的?人眼扫一眼就知道先算括号里的 1 + 2,再乘以 3。但计算机从左往右读字符串时,得先拆出数字和运算符,还要处理运算符优先级和括号的嵌套关系,这就很麻烦。逆波兰表达式(Reverse Polish Notation,RPN)也被称为后缀表达式,它把运算符写在操作数后面,比如 (1 + 2) * 3 写成 1 2 + 3 *,括号直接不需要了,优先级也不需要了。
这就是计算机友好的表达方式:从左到右扫描,遇数字就压栈,遇运算符就弹出最近的两个数做运算,再把结果压回去。它把“表达式求值”这种需要处理优先级的人为规则,变成了“无脑按顺序执行”的机械规则。编译器在做表达式翻译、计算器程序做表达式解析时,用的就是这套思路。
所以 150 题表面上是在考“你能不能用一个栈模拟计算过程”,实际上是在训练你把“中缀表达式怎么求值”这个复杂问题拆成“后缀表达式 + 栈”的简单模型。训练营把这题放在这里,就是为了让你把栈当成一个“有记忆的临时存储区”来用,而不是只会用 push 和 pop。
1.2 核心思路与完整推演
逆波兰表达式求值的规则就四条:
- 遇到数字,压入栈。
- 遇到运算符,弹出栈顶两个数字。
- 先弹出的数字放在运算符右侧,后弹出的放在左侧,做对应运算。
- 把运算结果压回栈。
最后栈里剩下的唯一数字,就是整个表达式的值。
我拿题目里的例子 ["2", "1", "+", "3", "*"] 走一遍完整过程:
- 读入
2,栈变为[2] - 读入
1,栈变为[2, 1] - 读入
+,弹出1和2,计算2 + 1 = 3,栈变为[3] - 读入
3,栈变为[3, 3] - 读入
*,弹出3和3,计算3 * 3 = 9,栈变为[9]
结果就是 9,和 (2 + 1) * 3 完全一致,但没用到任何括号优先级逻辑。
再看一个更长的例子:["4", "13", "5", "/", "+"],对应中缀是 4 + 13 / 5,但因为都是整数除法,结果是 4 + 2 = 6。
这里有一个细节必须注意:弹出两个数时,谁是被减数、谁是被除数。假设栈从栈底到栈顶依次是 a, b,弹出时先拿到的是 b,后拿到的是 a。做减法必须用 a - b,而不是 b - a;做除法必须用 a / b,而不是 b / a。很多人第一次写的时候会在这个地方写反,结果整个表达式结果全错了,而且错误非常隐蔽,不容易一眼看出来。
1.3 代码实现(Python)
python复制from typing import List
def evalRPN(tokens: List[str]) -> int:
stack = []
operators = {"+", "-", "*", "/"}
for token in tokens:
if token not in operators: # 数字直接入栈
stack.append(int(token))
else:
right = stack.pop() # 先弹出的是右操作数
left = stack.pop() # 后弹出的是左操作数
if token == "+":
stack.append(left + right)
elif token == "-":
stack.append(left - right)
elif token == "*":
stack.append(left * right)
else: # "/" 整数除法,需要向0取整
# Python 的 //
