从LeetCode‘逆波兰表达式求值’题出发,复盘C++栈操作的三个易错点(附避坑代码)

张开发
2026/4/14 18:17:30 15 分钟阅读

分享文章

从LeetCode‘逆波兰表达式求值’题出发,复盘C++栈操作的三个易错点(附避坑代码)
从LeetCode‘逆波兰表达式求值’题出发复盘C栈操作的三个易错点附避坑代码在技术面试中逆波兰表达式后缀表达式求值是一个经典问题它考察了候选人对栈数据结构的理解以及C标准库的熟练程度。许多求职者能够快速套用栈模板通过LeetCode第150题的测试用例但在实际工程中这样的代码往往隐藏着一些容易被忽视的陷阱。本文将从一个存在隐患的初版实现出发通过代码Review的方式深入分析三个常见易错点并提供经过实战检验的改进方案。1. 初版实现与潜在问题让我们先看一个典型的能通过测试但不完美的实现#include iostream #include string #include stack #include vector #include sstream using namespace std; int evalRPN(vectorstring tokens) { stackint st; for (auto token : tokens) { if (token || token - || token * || token /) { int b st.top(); st.pop(); int a st.top(); st.pop(); if (token ) st.push(a b); else if (token -) st.push(a - b); else if (token *) st.push(a * b); else st.push(a / b); } else { st.push(stoi(token)); } } return st.top(); }这个实现看似简洁但实际上存在几个潜在问题整数除法问题C中整数除法会截断小数部分这与数学期望可能不符操作数顺序问题减法/除法时操作数顺序容易搞错异常处理缺失对非法输入没有防御性处理扩展性差添加新运算符需要修改多处代码2. 栈操作的三个关键易错点2.1stack.top()与stack.pop()的调用顺序与返回值在初版代码中我们这样处理操作数int b st.top(); st.pop(); int a st.top(); st.pop();这种写法虽然常见但存在两个潜在问题操作顺序依赖必须确保先取b再取a否则运算顺序会出错异常安全性如果栈为空top()和pop()都会导致未定义行为更健壮的写法应该是auto safePop [](stackint st) { if (st.empty()) throw runtime_error(Invalid RPN expression); int val st.top(); st.pop(); return val; }; int b safePop(st); int a safePop(st);2.2 字符串转换与输入处理初版代码使用stoi进行字符串转换这存在几个问题不支持浮点数stoi会丢弃小数部分错误处理不足非法字符串会导致异常空格处理如果输入是单个字符串而非token数组需要额外处理改进方案double parseNumber(const string s) { try { return stod(s); } catch (...) { throw runtime_error(Invalid number: s); } } vectorstring tokenize(const string expr) { vectorstring tokens; istringstream iss(expr); string token; while (iss token) { tokens.push_back(token); } return tokens; }2.3 运算符扩展的优雅实现初版代码使用一连串的if-else判断运算符当需要添加新运算符时会变得难以维护。更优雅的方式是使用映射表unordered_mapstring, functiondouble(double, double) ops { {, [](double a, double b) { return a b; }}, {-, [](double a, double b) { return a - b; }}, {*, [](double a, double b) { return a * b; }}, {/, [](double a, double b) { if (b 0) throw runtime_error(Division by zero); return a / b; }}, {^, [](double a, double b) { return pow(a, b); }} }; // 使用方式 if (ops.count(token)) { double b safePop(st); double a safePop(st); st.push(ops[token](a, b)); }3. 完整健壮实现结合上述改进点我们得到如下健壮实现#include iostream #include string #include stack #include vector #include sstream #include unordered_map #include functional #include cmath using namespace std; class RPNCalculator { unordered_mapstring, functiondouble(double, double) ops; stackdouble st; double safePop() { if (st.empty()) throw runtime_error(Invalid RPN expression); double val st.top(); st.pop(); return val; } double parseNumber(const string s) { try { return stod(s); } catch (...) { throw runtime_error(Invalid number: s); } } public: RPNCalculator() { ops { {, [](double a, double b) { return a b; }}, {-, [](double a, double b) { return a - b; }}, {*, [](double a, double b) { return a * b; }}, {/, [](double a, double b) { if (b 0) throw runtime_error(Division by zero); return a / b; }}, {^, [](double a, double b) { return pow(a, b); }} }; } double evaluate(const vectorstring tokens) { for (const auto token : tokens) { if (ops.count(token)) { double b safePop(); double a safePop(); st.push(ops[token](a, b)); } else { st.push(parseNumber(token)); } } if (st.size() ! 1) { throw runtime_error(Invalid RPN expression); } return st.top(); } vectorstring tokenize(const string expr) { vectorstring tokens; istringstream iss(expr); string token; while (iss token) { tokens.push_back(token); } return tokens; } };这个实现具有以下优点异常安全对所有可能的错误输入都有检查支持浮点数使用double而非int避免精度损失易于扩展添加新运算符只需在ops映射中添加一项清晰的错误信息不同错误类型有明确的错误提示4. 实际应用与面试技巧在实际工程中逆波兰表达式的应用场景包括科学计算器许多计算器内部使用后缀表达式进行计算表达式解析引擎编译器/解释器常用技术数据处理管道某些ETL工具使用类似原理在面试中当被问到这个问题时可以展示以下进阶点防御性编程如何处理非法输入扩展性设计如何优雅地添加新运算符性能考量分析时间/空间复杂度边界条件空输入、单个数字、连续运算符等情况一个常见的面试follow-up问题是如何将中缀表达式转换为后缀表达式这时可以讨论调度场算法(Shunting-yard algorithm)的思路初始化一个空栈用于运算符一个空列表用于输出从左到右扫描中缀表达式遇到操作数直接加入输出列表遇到运算符时与栈顶运算符比较优先级遇到左括号压栈右括号则弹出栈顶元素直到遇到左括号最后弹出栈中所有剩余运算符在实际项目中我曾遇到过需要支持自定义函数和变量的表达式求值需求。基于这个RPN计算器我们可以进一步扩展// 添加变量支持 unordered_mapstring, double variables; // 添加函数支持 unordered_mapstring, functiondouble(vectordouble) functions; // 在parseNumber中添加变量查找 double parseNumber(const string s) { if (variables.count(s)) return variables[s]; // 原有实现... }这种设计模式体现了开闭原则对扩展开放对修改关闭是面试官通常欣赏的代码质量。

更多文章