C语言表达式求值:栈实现中缀转后缀与逆波兰计算详解
发布时间:2026/10/14 4:09:59 锦皓数字建站

很多学数据结构的人第一次被表达式求值劝退不是因为栈的原理多难懂而是课本上的伪代码和人话之间隔着一道墙。伪代码里写如果当前字符是运算符则与栈顶比较优先级听起来轻飘飘的真到自己动手写C语言的时候问题全来了数字怎么从字符串里抠出来优先级比较到底谁出栈括号怎么处理才不会乱后缀表达式求值的时候减法和除法的操作数顺序为什么容易搞反这篇就是把我自己完整跑通的代码实现过程拆开揉碎了讲一遍从设计思路到每个函数的细节再到测试时踩过的坑尽量让看完的人能直接照着写出一个能跑的版本而不是停留在好像懂了的阶段。1. 为什么绕一圈用后缀表达式计算机的读取习惯和人类完全不同先别急着写代码。表达式求值这件事难点从来不是计算本身而是顺序。我们人类看中缀表达式2 3 * 4一眼就懂因为眼睛会自动抓住优先级。但计算机用栈处理中缀的时候必须反复回看、比较优先级、临时保存操作数整个流程绕来绕去。而后缀表达式也叫逆波兰表达式就不一样了它把运算符全部放到操作数的后面比如上面这个式子写成后缀就是2 3 4 * 。计算机从左往右扫描一遍遇到数字就压栈遇到运算符就弹出两个数字算完再压回去没有任何优先级和括号的概念干净利落。有人可能会问那为什么不直接在前缀/后缀表达式里写式子干脆别用中缀了原因很简单中缀表达符合人类直觉是输入端的习惯而后缀表达符合机器执行逻辑是运算端的习惯。所以完整方案必然是中缀转后缀加后缀求值两个步骤。这也是为什么你看到的几乎所有教程都把代码拆成infixToSuffix和calcSuffix两个函数而不是一个函数从头算到尾。转换的核心机制其实就一句话扫描中缀表达式的时候用一个栈暂时保存那些还没轮到的运算符。遇到运算符时如果它优先级比栈顶低或相等说明栈顶那个运算符的操作数已经齐了可以出栈输出了。括号则是特殊的逻辑开关左括号直接进栈右括号负责把栈里最近的左括号之上的所有运算符全部弹出。这个过程理解透彻了后面写代码就是翻译的问题。2. C语言实现的完整代码结构栈的定义、优先级表、核心函数逐一拆解我不喜欢写一堆抽象定义直接上代码反而更快。整个程序我拆成四个部分栈的基本操作、优先级判断与转换函数、后缀表达式求值函数、主函数。下面是完整代码每个函数后面我会详细解释设计时的想法。#include stdio.h #include stdlib.h #include string.h #include ctype.h #define MAX_SIZE 100 // ---------- 1. 栈结构定义与基本操作 ---------- // 这里统一用了顺序栈因为表达式求值的栈深度不会太大没必要上链栈 typedef struct { char data[MAX_SIZE]; int top; } CharStack; typedef struct { double data[MAX_SIZE]; int top; } DoubleStack; void initCharStack(CharStack *s) { s-top -1; } int isCharStackEmpty(CharStack *s) { return s-top -1; } int pushChar(CharStack *s, char c) { if (s-top MAX_SIZE - 1) return 0; s-data[(s-top)] c; return 1; } int popChar(CharStack *s, char *c) { if (isCharStackEmpty(s)) return 0; *c s-data[(s-top)--]; return 1; } int peekChar(CharStack *s, char *c) { if (isCharStackEmpty(s)) return 0; *c s-data[s-top]; return 1; } void initDoubleStack(DoubleStack *s) { s-top -1; } int isDoubleStackEmpty(DoubleStack *s) { return s-top -1; } int pushDouble(DoubleStack *s, double val) { if (s-top MAX_SIZE - 1) return 0; s-data[(s-top)] val; return 1; } int popDouble(DoubleStack *s, double *val) { if (isDoubleStackEmpty(s)) return 0; *val s-data[(s-top)--]; return 1; } // ---------- 2. 运算符优先级判断 ---------- // 返回1表示op1优先级高于op2返回0表示低于或等于 int isHigher(char op1, char op2) { if (op1 * || op1 /) { if (op2 * || op2 /) return 0; return 1; } return 0; // op1是或-永远不会比op2高除非op2是左括号但左括号不会进这个比较流程 } // ---------- 3. 中缀转后缀 ---------- void infixToSuffix(const char *infix, char *suffix) { CharStack s; initCharStack(s); int i 0, j 0; int len strlen(infix); while (i len) { char c infix[i]; // 如果是数字或者小数点连续读入并直接输出 if (isdigit(c) || c .) { while (i len (isdigit(infix[i]) || infix[i] .)) { suffix[j] infix[i]; } suffix[j] ; // 加空格分隔操作数方便后面求值 continue; } // 跳过空格 if (c ) { i; continue; } // 左括号直接入栈 if (c () { pushChar(s, c); i; continue; } // 右括号把栈顶元素弹出并输出直到遇到左括号或栈空 if (c )) { char topChar; while (popChar(s, topChar)) { if (topChar () break; suffix[j] topChar; suffix[j] ; } i; continue; } // 处理运算符 if (c || c - || c * || c /) { char topChar; // 注意这里是循环判断不是if判断 // 因为栈顶可能有多个比当前运算符优先级高的运算符 while (peekChar(s, topChar)) { if (topChar () break; // 左括号在栈里时视为优先级最低 if (isHigher(topChar, c)) { popChar(s, topChar); suffix[j] topChar; suffix[j] ; } else { break; } } pushChar(s, c); i; continue; } // 理论上到这里应该都处理完了万一有奇怪字符就直接跳过 i; } // 扫描结束把栈里剩下的运算符全部弹出 char topChar; while (popChar(s, topChar)) { if (topChar ! () { suffix[j] topChar; suffix[j] ; } } suffix[j] \0; } // ---------- 4. 后缀表达式求值 ---------- double calcSuffix(const char *suffix) { DoubleStack s; initDoubleStack(s); int i 0; int len strlen(suffix); while (i len) { char c suffix[i]; if (isdigit(c) || c .) { // 从字符串中解析数字 char numStr[20]; int k 0; while (i len (isdigit(suffix[i]) || suffix[i] .)) { numStr[k] suffix[i]; } numStr[k] \0; pushDouble(s, atof(numStr)); continue; } if (c ) { i; continue; } // 运算符弹出两个操作数注意顺序 if (c || c - || c * || c /) { double a, b; popDouble(s, b); // 第一个弹出的是右操作数 popDouble(s, a); // 第二个弹出的是左操作数 switch (c) { case : pushDouble(s, a b); break; case -: pushDouble(s, a - b); break; case *: pushDouble(s, a * b); break; case /: if (b 0) { printf(错误除数不能为0\n); return 0; } pushDouble(s, a / b); break; } i; continue; } i; } double result; popDouble(s, result); return result; } // ---------- 5. 主函数 ---------- int main() { char infix[MAX_SIZE]; char suffix[MAX_SIZE]; printf(请输入中缀表达式支持 - * / 和小括号操作数可为整数或小数: \n); // 用fgets而不是gets安全性更好 fgets(infix, sizeof(infix), stdin); // 去掉字符串末尾的换行符 infix[strcspn(infix, \n)] \0; infixToSuffix(infix, suffix); printf(后缀表达式: %s\n, suffix); double result calcSuffix(suffix); printf(计算结果: %.2f\n, result); return 0; }3. 代码里那些看上去不起眼但一改就错的关键细节这段代码已经不是第一次改写了早期版本经常在某些细节上莫名其妙出错。有些问题当时排查了很久最后发现是设计上的疏忽这里单独拿出来讲都是血泪教训。3.1 数字和运算符号之间为什么要加空格中缀转后缀输出的时候我在每个数字后面都追加了一个空格。这不是随意写的而是为了后缀求值阶段能够正确区分连续的独立数字。举个例子中缀12 3如果后缀写成123那后面的解析器会把123当成一个整体数字直接算成 123 而不是 15。加上空格变成12 3 解析器才能明确知道12和3分别是两个操作数。栈领域有一句经验后缀表达式里的分隔符和操作数同等重要。空格就是这里的分隔符。你在课本里看到有些实现会把#或当分隔符原理一样用空格最直观。3.2 弹出顺序为什么是b先出、a再出后缀求值时遇到运算符要弹出两个操作数这行代码是无数人写反的经典位置popDouble(s, b); // 先弹出的是右操作数 popDouble(s, a); // 后弹出的是左操作数为什么必须这样因为栈是后进先出。后缀表达式的顺序是a b op也就是说a先入栈b后入栈。执行减法时按照人类的阅读习惯我们希望计算的是a - b所以后弹出的是a先弹出的是b。如果写反成a 先弹出的那减法就全反了变成b - a。书上的伪代码通常不强调这一点只写pop(opnd1)、pop(opnd2)很多人想当然认为第一个弹出的就是左操作数结果调试半天发现结果永远差个符号。小技巧不确定的时候就用个简单例子心算一遍比如3 5 -正确结果是 -2如果你算出来 2那一定是弹栈顺序反了。3.3 运算符出栈的循环判断而不是if判断中缀转后缀的运算符处理部分我用的是while循环出栈这个细节也是从错误里学来的。第一次写的时候用的是if只判断一次栈顶。对于1 2 * 3这种简单表达式没问题但一旦碰到1 * 2 3问题就来了。处理的时候栈顶是**优先级更高必须弹出。弹出来之后栈里可能还有一个*或者/比如表达式是1 * 2 * 3 4遇到第二个*的时候栈顶已经是*了这时候你只弹一次栈里还剩一个*最终后缀顺序就错了。正确逻辑是只要栈顶运算符优先级 当前运算符就一直弹直到遇到(或栈空或者优先级低的运算符。这样一次性把该算的先算完保证后缀表达式的顺序严格还原中缀表达式的优先级结构。3.4 括号在栈里的优先级处理栈里的左括号优先级要特殊对待。我的写法是遇到左括号直接入栈遇到右括号时疯狂弹出运算符直到遇见左括号。这里有个表面简单但容易忽略的点判断栈顶运算符是否应该弹出的时候必须先判断topChar (再 break。如果不加这个判断直接调用isHigher函数比较*和(的优先级你的优先级表就必须额外给括号定义优先级。很多教材会单独给(设一个栈内优先级栈内 1栈外 5就是用来解决这个问题的。我的代码选择了更直白的方案换优先级函数之外单独用if (topChar () break;拦截。本质效果一样只是代码更短更直观。理解这一点以后看到别人用等级表实现时就不会觉得奇怪了。4. 实测几种典型输入从正确结果到边界异常代码写完之后不可能直接拿去交差必须跑测试用例。我整理了几组自己实际跑过的输入从最简单的到刁钻的每一组都说明了预期原因。你能看到整个程序的能力边界在哪里。输入表达式转换出的后缀计算结果说明12*31 2 3 * 7.00验证优先级处理乘号优先于加号(12)*31 2 3 *9.00验证括号改变优先级102.5*410 2.5 4 * 20.00验证多位数字和小数点解析((10-2)3)*(4/2)10 2 - 3 4 2 / *22.00验证括号嵌套100/5/2100 5 / 2 /10.00验证同优先级左结合3/053 0 / 5 错误除数不能为0验证运行时错误捕获重点说两组(12)*3这组专门测括号。转换时遇到(入栈遇到1直接输出遇到入栈遇到)把弹出然后遇到*入栈。最后表达式结束弹出*。你仔细看后缀1 2 3 *计算顺序恰好是先加后乘与中缀含义完全一致。100/5/2这组测的是同优先级左结合。100/5/2在数学上是(100/5)/2 10不是100/(5/2) 40。我的isHigher函数在op1和op2都为乘除时返回 0也就是说当前运算符遇到栈顶同优先级运算符时会先弹出栈顶这样第一个/就会被第二个/弹出后缀变成100 5 / 2 /实际求值时先除一次再除一次符合左结合规则。如果你遇到过2^3^2这种右结合的数学表达式这个左结合规则就需要单独处理但四则运算里不需要考虑。5. 评论区经常出现的问题一元负号、单栈直接求值、可视化演示每次贴出这段代码都会有同学追问几个进阶问题这里统一说下我的看法和实践。5.1 负数怎么处理一元负号和二元减号怎么区分这是表达式求值最经典的扩展问题。我的代码目前默认输入是正规中缀表达式像-3 5这种以负号开头的式子转换时会把开头的-当成二元运算符导致后缀表达式变成3 5 -栈里就一个操作数弹不出两个算法直接崩掉。常规处理办法有两种。一种是在输入阶段做预处理区分一元负号和二元减号。判断方法不复杂——如果-出现在表达式开头或者它的前一个字符是运算符或左括号那它就是一元负号。对于一元负号可以用一个特殊符号替代比如~转换时遇到~就直接把后面的操作数取负。另一种更省事的办法规定输入格式要求负数的字面量必须带括号写成(-3)或2*(-3)。这种方案代码改动量最小缺点是约束了用户输入的自由度。我自己的课程设计里两个方案都做过如果只是为了交作业建议用第二种逻辑清晰不容易出错。想挑战自己就实现第一种的预处理函数代码量大概多三十行。5.2 能不能不用后缀表达式一个栈直接从左到右算可以双栈法或者叫算子栈算符栈法。原理是中缀扫描时同时维护两个栈一个存操作数一个存运算符。遇到数字压入操作数栈遇到运算符先比较它和运算符栈栈顶的优先级如果当前优先级高就入栈否则从运算符栈弹出一个运算符同时从操作数栈弹出两个操作数算完结果压回操作数栈然后继续比较。我来回折腾过这个方案最终在写教学博客时选择了中缀转后缀再求值的两阶段法原因只有一个代码可读性好、分段排查bug容易。第一次写完整个程序如果结果不对我只需要分别在转换函数和求值函数里各加一条打印语句立刻就能找出哪一步出了问题。双栈法把两件事揉在一个循环里逻辑压缩得很紧适合熟练的人写。好这篇讲到这里。上面的代码实现基本按照课程设计的标准要求来做可以随提随交。如果你打算继续深入可以试着把容器从顺序栈改成链栈再把上面说的一元负号处理函数补上整个程序会硬气很多。以后遇到求值相关的问题但凡能用后缀表达式栈解决的别犹豫这个模式是真的好用。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。