【LeetCode 150】逆波兰表达式求值:为什么栈是它的最佳拍档?
逆波兰表达式 (Reverse Polish Notation, RPN),也称为后缀表达式,初看之下可能觉得反直觉:运算符竟然跟在数字后面?比如 2 + 1 写成了 2 1 +。
但这种表达方式对计算机来说却极其友好:它不需要括号来规定优先级,也不需要复杂的递归,只需要从左到右扫描一遍即可算出结果。
解决这个问题的神器,就是数据结构中的------栈(Stack)。
1. 核心逻辑:数字入栈,符号吃栈
逆波兰表达式的计算规则非常简单,可以概括为一句话:遇到数字就存起来,遇到符号就拿最近的两个数字计算。
栈的"后进先出"(LIFO)特性完美契合了这个需求。我们可以把栈想象成一个暂存箱:
- 遍历数组 :从左到右依次读取字符串数组
tokens。 - 遇到数字 :直接压入栈中(Push),等待被后面的运算符"临幸"。
- 遇到运算符 :
- 从栈中弹出(Pop)两个数字。
- 根据运算符进行计算。
- 将计算结果重新压入栈中(Push),作为下一轮计算的操作数。
- 结束:遍历完成后,栈里剩下的最后一个元素就是最终结果。
2. 关键细节:左右操作数的顺序
在代码实现中,有一个非常容易出错的细节:出栈顺序与运算顺序的关系。
当遇到减法 - 或除法 / 时,顺序至关重要。
栈顶的元素是最后 放进去的,所以在原表达式中,它是运算符右边的那个数。
例如表达式 4 13 5 / +:
- 栈中有
[4, 13, 5](5在栈顶)。 - 遇到
/。 - 第一次 Pop 拿到
5(这是除数/右操作数val2)。 - 第二次 Pop 拿到
13(这是被除数/左操作数val1)。 - 计算
13 / 5,而不是5 / 13。
更简单的说,第一个弹出栈的就是操作符右边的数,第二个弹出来的就是操作符左边的数
3. 代码深度解析
让我们结合 Java 代码来看看这个过程是如何落地的。
3.1 辅助判断工具
首先,为了代码简洁,封装一个判断当前字符串是否为运算符的方法:
java
private boolean isOperator(String s) {
// 只要是 + - * / 中的一种,就是运算符
if(s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/")) {
return true;
}
return false;
}
3.2 主逻辑流程
java
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for(int i = 0; i < tokens.length; i++) {
// 情况一:如果是数字
if(!isOperator(tokens[i])) {
// 将字符串转为整数,直接入栈
int num = Integer.parseInt(tokens[i]);
stack.push(num);
}
// 情况二:如果是运算符
else {
// 注意弹出顺序!
// 先出来的是右操作数 (val2)
int val2 = stack.pop();
// 后出来的是左操作数 (val1)
int val1 = stack.pop();
// 根据符号进行计算
switch(tokens[i]) {
case "+":
stack.push(val1 + val2);
break;
case "-":
stack.push(val1 - val2); // 注意是 val1 - val2
break;
case "*":
stack.push(val1 * val2);
break;
case "/":
stack.push(val1 / val2); // 注意是 val1 / val2
break;
}
}
}
// 最终栈里只剩下一个结果
return stack.pop();
}
4. 复杂度分析
- 时间复杂度 :O(N)O(N)O(N)。我们需要遍历整个
tokens数组一次,每个元素的操作(入栈、出栈、计算)都是常数时间的。 - 空间复杂度 :O(N)O(N)O(N)。在最坏的情况下(例如
1 2 3 4 + + +),栈中需要存储接近 N 个数字。
5. 总结
LeetCode 150 是一道经典的栈应用题。它展示了栈在计算机科学中最本质的用途之一:处理具有嵌套结构或顺序依赖的运算。
通过这道题,我们不仅学会了如何解析逆波兰表达式,更深刻理解了:
- 栈的暂存能力:保存暂时不用的数据。
- 操作数的顺序性:在非交换律运算(减、除)中,谁先出栈,谁是右操作数。
掌握了这个模式,以后遇到"后缀表达式求值"或者"基本计算器"类的题目,都能迎刃而解。