尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

华为OD机试:双栈法解析火星文计算,实现中缀表达式求值器

华为OD机试:双栈法解析火星文计算,实现中缀表达式求值器 1. 项目概述从“火星文”到“中缀表达式”最近在准备华为OD机试的朋友估计不少人都刷到了这道“火星文计算”的题目。乍一看标题什么“火星文”又是“#”又是“$”的感觉花里胡哨好像很复杂。但如果你静下心来把题目描述仔细读两遍就会发现它的核心本质其实非常经典实现一个支持自定义运算符的中缀表达式求值器。所谓的“火星文”不过是题目为了增加趣味性和辨识度给两个自定义运算符“#”和“$”起的名字并赋予了它们特殊的运算规则。题目给出的公式是x # y 2*x 3*y 4x $ y 3*x y 2这里的“#”和“$”其地位就等同于我们熟悉的“”和“*”。所以当我们拿到一个像7#6$5#12这样的字符串时任务就是按照正确的运算顺序本题通常隐含运算符优先级相同从左到右计算解析出数字和运算符然后应用对应的公式进行计算。这本质上就是编译原理前端处理、表达式解析的简化版也是面试中考察候选人字符串处理、栈应用、逻辑抽象能力的绝佳题目。用C来实现既能考察你对基础数据结构的掌握std::stack,std::string也能看出你代码的健壮性和边界处理能力。接下来我就结合自己多次模拟和实战的经验拆解一下实现一个高通过率方案的完整思路、关键细节和避坑指南。2. 核心思路拆解双栈法与状态机面对表达式求值尤其是可能涉及优先级的问题一个经过千锤百炼的经典算法就是“双栈法”。一个栈存放操作数num_stack一个栈存放运算符op_stack。算法的核心流程是一个状态机顺序扫描表达式字符串根据当前字符是数字还是运算符来决定是累积数字入栈还是进行栈顶运算符的运算。2.1 双栈算法流程精讲对于本题由于运算符“#”和“$”的优先级被设定为相同通常题目会说明若无说明则按相同处理需仔细审题且没有括号流程可以大大简化但理解完整双栈流程对应对变种题目至关重要。基本状态机逻辑如下初始化创建空的操作数栈和运算符栈。为了简化边界判断我习惯先在运算符栈里压入一个优先级最低的哨兵运算符比如‘’。扫描字符串遇到数字进入数字累积状态。因为数字可能不止一位如12需要用while循环将连续的数字字符转换为整数然后压入操作数栈。遇到运算符‘#’ 或 ‘$’这是关键步骤。在将当前运算符op入栈前需要检查只要运算符栈顶的运算符优先级 当前运算符op的优先级就立即进行一次计算。计算时从操作数栈弹出两个数注意顺序先弹出的是右操作数b再弹出的是左操作数a从运算符栈弹出栈顶运算符根据该运算符调用对应的计算函数将结果压回操作数栈。重复此过程直到栈顶运算符优先级低于op再将op压入运算符栈。这个“延迟计算”的策略保证了高优先级或同优先级但先出现的运算符先被计算。扫描结束表达式字符串处理完毕后运算符栈中可能还有未计算的运算符。此时需要清空栈只要运算符栈不为空且栈顶不是哨兵就重复步骤2中的计算过程。得到结果最后操作数栈的栈顶元素就是整个表达式的计算结果。对于本题优先级相同的情况第2步中的“优先级比较”条件始终成立所以实际上每遇到一个新运算符都会立即将前面的运算完成效果等同于从左到右依次计算。但实现时依然建议保留完整的优先级比较框架代码更具通用性。2.2 运算符优先级与计算函数的抽象即使本题优先级相同良好的设计也应将运算符的优先级和计算逻辑抽象出来这样代码清晰易于扩展。// 定义运算符到优先级的映射 unordered_mapchar, int op_priority { {#, 1}, // 优先级数值相同即可 {$, 1} }; // 定义运算符到计算函数的映射 int calculate(char op, int a, int b) { switch(op) { case #: return 2 * a 3 * b 4; // 注意公式是 x#ya对应xb对应y case $: return 3 * a b 2; default: return 0; // 不应该发生 } }这里有一个极易出错的关键点注意计算函数中a和b的顺序。当我们从栈中弹出两个数时先弹出的是第二个操作数b后弹出的是第一个操作数a。所以calculate(‘#’, a, b)实现的必须是x#y的语义即a是xb是y。顺序弄反会导致结果完全错误。实操心得在编写calculate函数时我习惯在旁边用注释明确写出公式x op y ...并标明a对应xb对应y。这是一个简单的习惯但能避免很多调试时的头疼事。3. 实现细节与代码逐行解析理解了算法我们来动手实现。下面我将给出一个完整、健壮且注释详细的C实现并逐一解释关键代码段。3.1 完整代码实现#include iostream #include string #include stack #include unordered_map #include cctype // 用于 isdigit 函数 using namespace std; class MartianCalculator { private: // 运算符优先级表 unordered_mapchar, int priority { {#, 1}, {$, 1} // 如果未来扩展可以在这里添加 ‘’ ‘-’ ‘*’ ‘/’ 等 }; // 计算核心函数 int compute(char op, int left, int right) { switch(op) { case #: // x # y 2*x 3*y 4 return 2 * left 3 * right 4; case $: // x $ y 3*x y 2 return 3 * left right 2; default: // 防御性编程理论上不会执行到这里 return 0; } } // 执行一次栈顶运算 void performTopOperation(stackint nums, stackchar ops) { // 检查栈内元素是否足够 if (nums.size() 2 || ops.empty()) { // 实际上在正确调用此函数前我们应已确保条件满足 return; } int right nums.top(); nums.pop(); // 第二个操作数 (y) int left nums.top(); nums.pop(); // 第一个操作数 (x) char op ops.top(); ops.pop(); int result compute(op, left, right); nums.push(result); } public: int calculate(const string s) { stackint numStack; stackchar opStack; // 为了方便处理可以在运算符栈底放入一个优先级最低的哨兵 // opStack.push(); // 哨兵优先级设为0 int len s.length(); int i 0; while (i len) { char c s[i]; // 情况1当前字符是数字 if (isdigit(c)) { int num 0; // 处理多位数字 while (i len isdigit(s[i])) { num num * 10 (s[i] - 0); // 经典字符转数字方法 i; } numStack.push(num); // 注意此处i已经指向数字后的第一个字符循环末尾不再i continue; } // 情况2当前字符是运算符 (# 或 $) else if (c # || c $) { // 关键当栈顶运算符优先级 当前运算符优先级时先计算栈顶 // 对于本题优先级相同所以会一直计算到栈空或遇到哨兵 while (!opStack.empty() priority[opStack.top()] priority[c]) { performTopOperation(numStack, opStack); } // 当前运算符入栈 opStack.push(c); i; } // 情况3理论上题目输入只包含数字和#、$但可留出处理空格或其他字符的接口 // else if (c ) { i; continue; } // else { // 非法字符根据题目要求处理可能抛出异常或返回错误码 } } // 表达式扫描完毕清空运算符栈 while (!opStack.empty()) { performTopOperation(numStack, opStack); } // 最终结果应在操作数栈顶 return numStack.top(); } }; int main() { MartianCalculator calculator; string expression; // 示例输入7#6$5#12 // 计算过程(7#6) 2*73*641418436 // (36$5) 3*365210852115 // (115#12)2*1153*124230364270 while (getline(cin, expression)) { // 实际机试中可能需要处理多组输入或去除首尾空格 // 这里简单处理假设输入是单行无空格的有效表达式 int result calculator.calculate(expression); cout result endl; } return 0; }3.2 关键代码段深度解析数字解析 (while (i len isdigit(s[i])))这是处理多位数的标准做法。isdigit()函数来自cctype判断字符是否为十进制数字。num num * 10 (s[i] - ‘0’)是核心累加公式将字符‘0’-‘9’转换为整数0-9。循环结束后i指向了数字串后面的第一个非数字字符所以我们在continue跳过本次循环末尾的i防止跳过这个字符。运算符处理与优先级比较 (while (!opStack.empty() priority[opStack.top()] priority[c]))这是双栈法的灵魂。这个循环条件决定了“何时进行计算”。priority[opStack.top()]获取栈顶运算符的优先级priority[c]是当前扫描到的运算符的优先级。“”确保了相同优先级时先出现的运算符栈顶先计算实现了从左到右的结合性。这个循环会一直进行直到栈顶运算符优先级低于当前运算符从而保证了高优先级运算的优先执行。清空栈 (while (!opStack.empty()))在扫描完整个字符串后运算符栈里可能还有剩余的运算符例如表达式最后一个运算符之后的运算。这个循环确保所有挂起的运算都被执行完毕。performTopOperation函数我将一次“弹出两个数和一个运算符计算后结果入栈”的操作封装成了函数。这使主逻辑更清晰。特别注意操作数弹出顺序先弹出的是右操作数 (right)后弹出的是左操作数 (left)。这个顺序与计算函数compute的参数顺序必须严格对应。4. 边界条件与常见“坑点”排查即使算法正确忽略边界条件也会导致功亏一篑。下面是我在调试和模拟中总结的几个高频“坑点”。4.1 输入处理与假设验证机试平台的输入通常来自标准输入 (std::cin)。你需要明确输入格式题目是单行一个表达式还是多组测试数据示例7#6$5#12暗示是单行。但稳妥起见代码应能处理getline循环读取直到文件结束 (EOF)。字符串内容题目明确说“x、y是无符号整数”且运算符只有“#”和“$”。但输入中是否可能包含空格我的建议是在数字解析和运算符判断前可以增加一个过滤空格的步骤这样代码更健壮。while (i len s[i] ) i; // 跳过空格 if (i len) break; // 跳过空格后可能到末尾4.2 数字溢出问题题目虽未明确说明数字范围但用int类型在大多数情况下是足够的。然而如果表达式非常长中间结果可能会超出int的表示范围-2^31 ~ 2^31-1。一个更稳妥的做法是使用long long来存储操作数和中间结果。在performTopOperation和compute函数中将int改为long long是简单的防御性策略。long long compute(char op, long long left, long long right) { ... } stacklong long numStack;4.3 运算符栈的初始状态在双栈算法中为了统一处理而不必每次都判断栈是否为空可以在运算符栈初始化时压入一个“哨兵”运算符并赋予其最低优先级如0。这样在while (!opStack.empty() ...)的判断中当栈里只有哨兵时循环条件自然不成立。这是一个让代码更简洁优雅的小技巧。opStack.push(); // 在priority映射中给‘’定义优先级为04.4 错误处理与鲁棒性虽然机试用例通常正确但养成错误处理习惯是优秀工程师的素养。操作数不足在performTopOperation中如果numStack.size() 2却尝试计算说明表达式不合法如“#5”。未知运算符如果扫描到既不是数字也不是‘#’或‘$’的字符应根据题目要求返回错误或忽略如空格。最终栈状态计算结束后numStack应该恰好剩下一个元素结果opStack应为空或只剩哨兵。否则表达式可能不完整如“7#”。可以在函数返回前添加断言或检查if (numStack.size() ! 1 || (!opStack.empty() opStack.top() ! )) { // 抛出异常或返回特定错误值 cerr Invalid expression! endl; return -1; // 或用其他方式标识错误 } return numStack.top();5. 测试用例设计与调试技巧自己构造全面的测试用例是保证代码通过率100%的关键。5.1 必须覆盖的测试场景基础功能7#6$5#12手算验证结果应为270。单运算符5#10(25310444)5$10(3*510227)。连续相同运算符1#2#3。计算顺序应为((1#2)#3)。1#2 2*13*241212#3 2*123*34249437大数测试输入包含较大数字如1000$2000#500检查是否溢出。边界数字0#0(4)0$0(2)。长表达式压力测试自动生成一个很长的表达式如1#2$3#4$5...确保循环和栈操作正确。非法/特殊输入如果允许空字符串应如何处理只有数字如“123”应直接返回123。开头或结尾是运算符如“#123”或“123#”这通常是非法的你的程序应能检测到。5.2 高效的调试方法在本地或在线IDE调试时打印中间状态在performTopOperation函数内部打印left, op, right, result。这能让你清晰看到每一步计算是否符合预期。cout “Performing: “ left op right “” result endl;可视化栈内容编写辅助函数打印两个栈的当前状态在每次入栈、出栈或循环前后调用非常直观。单元测试将calculate函数和测试用例封装起来使用断言 (assert) 进行验证。void test() { MartianCalculator calc; assert(calc.calculate(“7#6$5#12”) 270); assert(calc.calculate(“5#10”) 44); assert(calc.calculate(“1#2#3”) 37); assert(calc.calculate(“0#0”) 4); cout “All tests passed!” endl; }6. 性能分析与优化空间对于机试题目通常时间复杂度在 O(N) 即可通过N为表达式长度。我们的双栈法每个字符处理一次每个运算符入栈出栈一次正是 O(N) 复杂度。空间复杂度也是 O(N)最坏情况下所有数字和运算符都入栈。如果追求极致本题由于优先级相同可以不用栈直接一次扫描完成计算空间复杂度可降至 O(1)。思路是维护当前累积的结果curr和下一个待读取的数字next以及上一个运算符prev_op。遇到新运算符时根据prev_op将curr和next合并然后更新prev_op和next。这种写法更简洁但通用性不如双栈法。在机试中清晰、健壮、易维护的代码比微小的性能优化更重要除非题目有明确的性能限制。最后代码的整洁度也很关键。将优先级映射、计算逻辑、栈操作封装成清晰的函数和类使用有意义的变量名添加必要的注释这些都能给阅卷系统或面试官留下好印象。把上面的代码理解透彻自己动手敲几遍遇到类似的表达式计算题目你就能从容应对了。
返回列表