在 LeetCode 中,逆波兰表达式求值(150 题)是栈数据结构最经典、最基础的应用题之一。它不仅考察对逆波兰表示法的理解,更考验对栈操作的熟练度,以及边界场景的处理能力。今天就带大家一步步拆解这道题,从题目理解到代码实现,再到易错点排查,彻底吃透这道高频题。
一、题目解读(清晰易懂版)
题目核心:给一个字符串数组 tokens,里面装的是「逆波兰表达式」的元素,要求我们计算这个表达式的结果,返回一个整数。
先搞懂:什么是逆波兰表达式?
逆波兰表达式(Reverse Polish Notation,简称 RPN),也叫后缀表达式。我们平时写的数学表达式是中缀表达式(比如 3 + 4),而逆波兰表达式是把运算符放在两个操作数后面(比如 3 4 +)。
举个例子:中缀表达式 10 + 6 * (9 + 3) / (-11) + 17 + 5,对应的逆波兰表达式就是题目里的测试用例 ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]。
这种表达式的优势是:不需要括号来表示运算优先级,只要用栈就能按顺序计算,效率很高。
题目关键约束(必看,避坑重点)
-
运算符只有 4 种:
+、-、*、/,没有其他符号; -
操作数可以是整数(正数、负数,比如
"-123"),也可以是另一个表达式的结果(但我们不用管这个,栈会帮我们处理); -
重点坑:两个整数除法 必须向零截断(比如 6 / -132 = -0.045,结果取 0;-1 / 2 = -0.5,结果取 0);
-
无需处理除零异常,题目保证输入合法;
-
结果和中间计算值,都能用 32 位整数表示(不用考虑溢出问题)。
二、解题核心思路(栈是关键!)
逆波兰表达式的计算逻辑,天生适配「栈」的先进后出特性,核心思路只有 3 步,非常简单:
-
初始化一个空栈,用来存储操作数(数字);
-
遍历字符串数组 tokens 中的每一个元素(token):
-
如果当前 token 是 数字(包括正数、负数),就把它转换成数字,压入栈中;
-
如果当前 token 是 运算符(+、-、*、/),就从栈中弹出两个操作数(注意顺序!先弹出的是「右操作数」,后弹出的是「左操作数」);
-
-
用当前运算符,对两个操作数进行计算,把计算结果压入栈中;
-
遍历结束后,栈中只会剩下一个元素,这个元素就是逆波兰表达式的最终计算结果。
思路验证(结合经典测试用例)
测试用例:tokens = ["10","6","9","3","+","-11"," * ","/","*","17","+","5","+"]
分步计算过程(跟着栈的变化走,一眼看懂):
-
token = "10"(数字)→ 栈:[10]
-
token = "6"(数字)→ 栈:[10, 6]
-
token = "9"(数字)→ 栈:[10, 6, 9]
-
token = "3"(数字)→ 栈:[10, 6, 9, 3]
-
token = "+"(运算符)→ 弹出 3(右)、9(左),计算 9+3=12 → 栈:[10, 6, 12]
-
token = "-11"(数字)→ 栈:[10, 6, 12, -11]
-
token = " * "(运算符)→ 弹出 -11(右)、12(左),计算 12*(-11)=-132 → 栈:[10, 6, -132]
-
token = "/"(运算符)→ 弹出 -132(右)、6(左),计算 6/(-132)=0(向零取整)→ 栈:[10, 0]
-
token = " * "(运算符)→ 弹出 0(右)、10(左),计算 10*0=0 → 栈:[0]
-
token = "17"(数字)→ 栈:[0, 17]
-
token = "+"(运算符)→ 弹出 17(右)、0(左),计算 0+17=17 → 栈:[17]
-
token = "5"(数字)→ 栈:[17, 5]
-
token = "+"(运算符)→ 弹出 5(右)、17(左),计算 17+5=22 → 栈:[22]
遍历结束,栈中只剩 22,就是最终结果,和预期完全一致。
三、完整代码实现(TypeScript)
结合上面的思路,我们来实现代码。代码中会加入详细的注释,同时处理好所有边界场景(比如负数、非法表达式校验),直接复制到 LeetCode 就能通过。
function evalRPN(tokens: string[]): number {
// 1. 初始化栈,用于存储操作数(数字)
const stack: number[] = [];
// 2. 定义运算符集合,方便快速判断当前token是否是运算符(比isNaN更高效)
const operators = new Set(['+', '-', '*', '/']);
// 3. 遍历每一个token
for (const token of tokens) {
// 判断当前token是否是运算符
if (operators.has(token)) {
// 弹出右操作数(先弹出的是栈顶元素,作为右操作数)
const right = stack.pop();
// 弹出左操作数(后弹出的是下一个栈顶元素,作为左操作数)
const left = stack.pop();
// 校验操作数(题目保证输入合法,此处仅做兜底,避免异常)
if (left === undefined || right === undefined) {
throw new Error(`Invalid RPN expression: ${tokens.join(' ')}`);
}
// 定义计算结果
let result: number;
// 根据不同运算符计算
switch (token) {
case '+':
result = left + right;
break;
case '-':
result = left - right; // 注意顺序:左 - 右
break;
case '*':
result = left * right;
break;
case '/':
// 核心重点:向零取整,用Math.trunc直接去除小数部分(兼容正负)
result = Math.trunc(left / right);
break;
default:
throw new Error(`Unsupported operator: ${token}`);
}
// 将计算结果压入栈中
stack.push(result);
} else {
// 不是运算符,就是数字(兼容正负数字,如"-123")
stack.push(Number(token));
}
}
// 遍历结束,栈中唯一元素就是结果
return stack[0];
};
四、代码逐行解析(新手也能看懂)
我们逐行拆解代码,重点讲关键细节和避坑点,避免只抄代码不懂原理。
1. 初始化栈和运算符集合
const stack: number[] = [];
const operators = new Set(['+', '-', '*', '/']);
-
stack:空数组,专门存数字,类型标注为 number[],符合 TypeScript 规范;
-
operators:用 Set 存储运算符,好处是
has()方法判断是否为运算符的效率是 O(1),比用if (token === '+' || token === '-')更简洁、高效,也避免了冗余的数字转换。
2. 遍历 token 并判断类型
for (const token of tokens) {
if (operators.has(token)) {
// 运算符逻辑
} else {
// 数字逻辑
stack.push(Number(token));
}
}
-
遍历 tokens 数组,每个元素都是 token;
-
若 operators 包含当前 token,说明是运算符,执行运算逻辑;
-
否则是数字,用 Number(token) 转换为数字(兼容 "-123" 这类负数字符串),压入栈中。
3. 运算符核心逻辑(重点!)
const right = stack.pop();
const left = stack.pop();
if (left === undefined || right === undefined) {
throw new Error(`Invalid RPN expression: ${tokens.join(' ')}`);
}
-
重点顺序:
pop()方法会弹出栈顶元素,所以 先弹出的是右操作数,后弹出的是左操作数!比如计算 6 / -132,left 是 6,right 是 -132,不能搞反(搞反会得到 -132/6 = -22,结果错误); -
兜底校验:题目保证输入合法,这里的 if 判断是为了避免极端情况(比如输入是 ["+"]),抛出明确的错误信息,便于调试。
4. 除法向零取整(最容易踩坑的地方)
case '/':
result = Math.trunc(left / right);
break;
为什么不用 Math.floor?因为 Math.floor 是「向下取整」,会导致负数除法结果错误:
-
比如 6 / -132 = -0.045,Math.floor(-0.045) = -1(不符合向零取整),而 Math.trunc(-0.045) = 0(正确);
-
比如 -1 / 2 = -0.5,Math.floor(-0.5) = -1(错误),Math.trunc(-0.5) = 0(正确);
Math.trunc 的作用是「直接去除数字的小数部分」,完美符合题目「向零截断」的要求。如果环境不支持 ES6+ 的 Math.trunc,也可以用兼容写法:
result = left / right > 0 ? Math.floor(left / right) : Math.ceil(left / right);
5. 返回结果
return stack[0];
遍历结束后,栈中只会有一个元素,就是最终结果,直接返回即可。
五、易错点总结(避坑必看)
这道题看似简单,但很多人提交会出错,核心就是踩了以下几个坑,记住就能少走弯路:
-
操作数顺序搞反:必须是「先弹右操作数,后弹左操作数」,尤其是减法和除法(左 - 右、左 / 右);
-
除法取整错误:误用 Math.floor,一定要用 Math.trunc 或兼容写法,确保向零取整;
-
负数处理:忘记兼容 "-123" 这类负数字符串,直接用 Number(token) 转换即可,无需额外判断;
六、解题总结
LeetCode 150 题的核心,就是「栈的应用」—— 逆波兰表达式的特性和栈的先进后出完美契合,记住「数字入栈,运算符出栈计算」的逻辑,就能轻松解决。
这道题的难度不算高,但非常考验细节:操作数顺序、除法取整、负数处理,这些细节直接决定代码是否能通过所有测试用例。
另外,这道题也是面试高频题,面试官常用来考察候选人对栈的理解和代码细节处理能力。掌握了这道题,你对栈的应用会更熟练,后续遇到类似的栈应用题(比如括号匹配)也能举一反三。