☰
算数表达式求值实验全攻略:中缀转后缀与栈的应用
2026/10/6 4:43:58 网站建设 项目流程

简介:这是一份面向数据结构课程设计的大一下学期实验报告,围绕“算数表达式求值”完整呈现利用栈与“算符优先法”解析包含加减乘除、括号及#边界符的表达式的实现过程。报告从问题描述、运行环境、算法思想与流程图、核心代码到性能分析层层展开,并额外实现了动态扩容、非法符号与除零检测、括号匹配检查等功能,适合学习栈应用、表达式求值算法或准备课程设计的读者参考。压缩包内共1个docx文件,约2.29MB,包含报告正文、代码说明及运行截图,可直接用于文档结构对照与算法思路梳理。资源已有3620人学习,对理解中缀表达式求值、双栈协作与运算符优先级处理有较实用价值。

1. 算数表达式求值实验到底在做什么:从“人读算式”到“机读算式”

算数表达式求值是数据结构课程里出现频率最高的实验报告题目之一,很多学校的课程设计、数据结构期末复习和考研机试都会拿它做原型。这个实验不涉及多复杂的算法,但它把栈、运算符优先级、括号匹配、连续数字扫描这些知识点一次串起来,很多人上课觉得听懂了,真让程序算3+4*2和(3+4)*2时才发现结果对不上。这篇笔记按我实际写过、也帮人改过这类实验报告的经验来写,讲清楚实现路径、关键代码和最容易翻车的地方。适合正在写实验报告的人,也适合想用最短时间把这块知识补扎实的考研党。

2. 为什么数据结构实验都选“中缀转后缀”:优先级表、栈与选型理由

2.1 为什么不是直接求值而是先转后缀:一种适合实验报告的拆法

人写的算式叫中缀表达式,运算符夹在两个操作数中间,比如3+4*2。机器直接读中缀很别扭,因为+和*谁先算不是由位置决定的,而是由优先级表决定的,再加上括号,扫描一遍根本算不干净。后缀表达式则完全不同,它把运算符放到操作数后面,3+4*2写成3 4 2 * +,这种形式没有括号、没有优先级,只有“数字、数字、运算符”的线性序列。计算后缀表达式只需要一个栈,遇到数字压栈,遇到运算符弹两个数运算再压回去,一遍扫完结果就在栈顶。

为什么不直接用“双栈一次扫描”的算法?双栈法确实能在一个循环里同时处理操作数和运算符,但转换和计算耦合在一起,中间状态只有两个栈,出错了很难定位是优先级判断错还是求值错。递归下降法能力更强,能处理一元负号、函数调用、赋值语句,但对一份数据结构实验报告来说设计偏重,而且递归的栈帧和“用数据结构栈”不是同一个考点。最稳妥的拆法是“中缀转后缀 + 后缀求值”两步走,每一步都能打印中间结果单独验证,报告里也能把“逆波兰式”这个考点一起覆盖。实验报告交上去,老师问你为什么这么设计,你可以直接说:第一步负责消解优先级和括号,第二步负责纯计算,职责分离。

这个拆法还有一层好处:中缀转后缀的过程本身就是对栈“后进先出”语义最直观的展示。运算符压在栈里等待比自己优先级更高的运算符先输出,括号用来强制改变等待顺序,这比单纯用栈做括号匹配要深一层。报告里能把“栈在这里到底存了什么”讲清楚,比贴一大段代码更有说服力。

2.2 优先级表与转换四规则:报告里这张表比代码更值钱

中缀转后缀的规则可以浓缩成一张表。我的习惯是在报告里先画优先级表,再写代码,因为代码只是这张表的翻译:

运算符优先级数值说明
+-1二元加减,左结合
*/2二元乘除,左结合
(0压栈后视作最低,保证括号内运算符先处理

转换流程从左到右扫描输入串,四句话就能说完:

  1. 数字连续读入,直接输出到后缀串;
  2. 左括号无条件入栈;
  3. 右括号不断弹栈输出,直到栈顶是左括号,再把左括号弹出丢弃;
  4. 运算符与栈顶运算符比较优先级:栈顶不低于当前运算符,就弹栈输出,重复这一步直到栈顶优先级更低或遇到左括号,然后把当前运算符入栈。

注意第4条用的是“不低于”,也就是>=。原因是加减乘除都是左结合,遇到同优先级运算符时要先让前一个出去。比如3-2-1,如果栈顶是-,再遇到一个-,必须把栈里的-弹出来,后缀才是3 2 - 1 -,结果是0。写成>会导致后缀变成3 2 1 - -,计算时变成3-(2-1)=2,整份报告就错了。

我在报告里还会补一句:转换过程天然能检测括号匹配。如果遇到右括号时栈已经为空,或者弹到底都没遇到左括号,说明输入里括号不配对。这个细节写在“异常处理”小节里,老师会认为你想过输入不合法的情况。调试时我的习惯是每处理完一个字符就把当前后缀串和栈内容打出来,对照纸上手推的结果。转后缀这段逻辑一旦写错,后面求值代码再对也白搭。

2.3 后缀求值的弹出顺序:一个栈够用,但左右操作数不能反

后缀求值的流程比转换更简单,用一个例子就能讲明白。3 4 + 2 *:读到3压栈,读到4压栈,读到+弹出4和3,算3+4=7压回去,读到2压栈,读到*弹出2和7,算7*2=14,结束。整个过程只有一个操作数栈,运算符只是“信号”,告诉程序现在该消费最近的两个操作数了。这就是选栈的核心理由:中间结果被临时记住,且消费顺序正好是最晚压栈的最先被用。

这里有个隐蔽的考点,几乎每个第一次写的人都会踩。弹出两个操作数的时候,先弹出来的是右操作数。后缀表达式的顺序是a b op,压栈时a先进b后进,弹出时先拿到b,再拿到a,所以代码要写成b = pop(); a = pop();然后执行a op b。如果顺手写成b op a,3-2会算出-1,减法和除法全反。排查这个问题有个笨办法:把后缀串和计算结果对着看,3 2 -期望是1,出来-1那一定就是弹出顺序反了。

3. 用 C 语言手写算数表达式求值:完整代码与参数设计

3.1 完整可运行代码:转换与求值两个函数一次实现

下面这份代码是我给学生讲实验时常用的版本。它不追求一行代码写三个功能,而是把每个分支单独列出来,方便对照上一章的规则看。输入约定为:操作数是整数,运算符只支持+ - * /和小括号,不支持空格、小数和一元负号。计算结果用double保存,避免整数除法截断。

#include <stdio.h> #include <string.h> #include <ctype.h> #include <stdlib.h> #define MAX 100 // 表达式最大长度 // 运算符优先级:数字越大越优先 int priority(char op) { switch (op) { case '+': case '-': return 1; case '*': case '/': return 2; case '(': return 0; // 左括号压栈后优先级最低,保证括号内运算符先出栈 default: return -1; // 非法字符 } } // 中缀转后缀:结果写入 postfix,数字之间用空格分隔 void to_postfix(const char *expr, char *postfix) { char stack[MAX]; int top = -1; int i, j = 0; for (i = 0; expr[i] != '\0'; i++) { char c = expr[i]; if (isdigit(c)) { // 连续数字组成一个完整操作数,整体输出 while (isdigit(expr[i])) { postfix[j++] = expr[i++]; } postfix[j++] = ' '; // 操作数输出后加空格分隔 i--; // 回退到非数字字符,for 循环的 i++ 会重新落到它 } else if (c == '(') { stack[++top] = c; } else if (c == ')') { // 弹栈到左括号,弹出来的运算符都进后缀串 while (top != -1 && stack[top] != '(') { postfix[j++] = stack[top--]; postfix[j++] = ' '; } if (top != -1) top--; // 弹出左括号本身,括号不出现在后缀串里 } else if (priority(c) > 0) { // 栈顶优先级不低于当前运算符时先弹栈,保证左结合 while (top != -1 && stack[top] != '(' && priority(stack[top]) >= priority(c)) { postfix[j++] = stack[top--]; postfix[j++] = ' '; } stack[++top] = c; } } // 扫描结束,栈里剩下的运算符依次输出 while (top != -1) { postfix[j++] = stack[top--]; postfix[j++] = ' '; } postfix[j] = '\0'; } // 后缀求值:返回最终结果 double eval_postfix(const char *postfix) { double stack[MAX]; int top = -1; int i = 0; while (postfix[i] != '\0') { if (postfix[i] == ' ') { i++; continue; } if (isdigit(postfix[i])) { double num = 0; while (isdigit(postfix[i])) { num = num * 10 + (postfix[i] - '0'); i++; } stack[++top] = num; } else { double b = stack[top--]; // 先弹出的是右操作数 double a = stack[top--]; // 后弹出的是左操作数 switch (postfix[i]) { case '+': stack[++top] = a + b; break; case '-': stack[++top] = a - b; break; case '*': stack[++top] = a * b; break; case '/': if (b == 0) { printf("除零错误\n"); exit(1); } stack[++top] = a / b; break; } i++; } } return stack[top]; } int main() { char expr[MAX]; char postfix[MAX * 2]; printf("请输入表达式(整数、+ - * / 和小括号,不支持空格): "); scanf("%s", expr); to_postfix(expr, postfix); printf("后缀表达式: %s\n", postfix); printf("计算结果: %.6g\n", eval_postfix(postfix)); return 0; }

逻辑说明:to_postfix里四个分支和上一章的四条规则一一对应,最容易看混的是数字分支里的i--。当while (isdigit(expr[i]))退出时,i已经停在了非数字字符上,i--回退一位,交给for循环的i++再前进,最终刚好重新指向这个非数字字符。这个写法省一个辅助变量,但注释一定要写清楚。eval_postfix的操作数栈是double数组,运算符分支先弹b再弹a,减法和除法必须按a op b计算。除零检查放在b == 0时直接退出,实验场景下比返回特殊值更直观。

3.2 参数怎么调、输入怎么扩展:栈容量、数值类型、格式串三个必改点

下面这几个参数是实验变体里最常改的地方,也是报告里“设计说明”一节可以写的内容:

参数/配置本代码取值实验扩展时的改法
表达式最大长度MAX=100改成strlen(expr)+1动态分配栈,或读入后先算长度再定数组
后缀串数组大小MAX*2MAX*2足够容纳运算符和空格;如果支持一元负号,建议再加 50%
操作数类型double输入仍按整数读,运算用 double,想支持小数需在数字扫描里加小数点判断
输出格式%.6g自动去掉多余尾零,5/2输出2.5

如果实验要求支持带空格的表达式,把scanf("%s", expr)换成fgets(expr, MAX, stdin),然后在to_postfix的循环开头跳过空白字符即可。注意fgets会把换行符也读进数组,处理时要去掉末尾的\n。这个改动很小,报告里可以单独写一小段“输入预处理”,属于典型的白给步骤分。

还有一个容易忽略的运行细节:如果你改造成命令行传参,比如./calc "(1+2)*3",那括号前后必须加双引号,否则 shell 会把括号当成语法吞掉。从标准输入scanf读则没有这个问题。我在帮人调试时见过好几次“程序没问题,是 shell 把参数拆了”的情况。

4. 算数表达式求值的避坑清单:从一元负号到测试用例的五个翻车点

4.1 一元负号没处理:-3+5直接把程序打崩溃

现象:输入-3+5或者2*-3,程序要么输出错得离谱,要么求值阶段直接算出负数加法的奇怪结果。

原因:一元负号和中缀减法在字符上都是-,优先级表里只定义了二元减法,没有定义“这个减号是负号”。转换阶段会把开头的-当成普通运算符压栈,数字扫描又识别不了负号,整个后缀串的顺序就是乱的。

解决:最简单的是在报告里明确写清“本实验只支持二元运算符”,这不算偷懒,很多教材版本就是二元起步。想做得完善,常见做法是在词法层加一个判断:如果-出现在表达式开头,或者前一个字符是运算符或左括号,就把它视为一元负号,处理成“0 - 操作数”。比如-3+5在预处理阶段改写成(0-3)+5,后面逻辑完全不用动。这一步扩展量不大,写在报告的“扩展实现”里很加分。

4.2 右括号弹栈后忘了 pop 左括号:后缀串里混进括号

现象:(1+2)*3正确结果应该是1 2 + 3 *,实际输出变成1 2 + ( 3 *,或者计算时报错。

原因:右括号分支的while循环条件写成了“遇到左括号就停”,循环结束后没有把左括号从栈里弹出。左括号留在栈里,后续运算符弹栈时就会被它挡住,甚至直接输出到后缀串。

解决:循环结束后补一句if (top != -1) top--;,把左括号丢弃。一个很实用的自检方法:后缀串里一旦出现(或),说明右括号处理逻辑有 bug,因为后缀表达式永远不该有括号。

4.3 除法被 int 截断:5/2算出2,printf 打出垃圾值

现象:5/2期望2.5,程序输出2。另一种更隐蔽的情况是结果算对了,但printf用%d输出double,屏幕上出现一串莫名其妙的数字。

原因:求值栈用的是int,5/2在 C 里整除截断成2。后面一种则纯粹是格式化输出把 double 的位模式按整数解读,属于未定义行为。

解决:操作数栈从int改成double,所有运算按浮点走。printf 用%.6g,它能把2.5输出成2.5,把2.0输出成2,最贴近人看的习惯。在报告的设计说明里写一句“运算全程使用 double 以避免整数除法精度丢失”,老师一眼就能看到你考虑过数值边界。

4.4 弹栈条件写成>:3-2-1被算成2

现象:100-50-20期望30,程序算出70。3-2-1期望0,算出2。

原因:弹栈条件写了>而不是>=。遇到连续的两个同优先级运算符时,前一个不弹栈,导致后缀串变成3 2 1 - -。计算时对应3-(2-1)=2,减法和除法这类左结合运算符的语义被整体反转。

解决:把条件改成priority(stack[top]) >= priority(c)。这个坑的特征是:加法和乘法没问题,减法和除法一出错就是系统性的。测试用例里一定要放一条连续减法表达式,比如100-50-20,这条过了,说明结合性基本正确。

4.5 测试用例只有教科书样例:加一张边界用例表,报告立刻立体起来

现象:拿1+2*3和(1+2)*3测完就算“验证通过”,交上去被老师指出嵌套括号、除法精度、连续减法都没测。

解决:构造一个覆盖边界的最小测试矩阵,直接作为报告里的“测试与结果”表:

输入期望结果覆盖点
1+2*37优先级基本规则
(1+2)*39括号改变优先级
100-50-2030左结合,同优先级连续计算
((1+2)*(3+4))-5/218.5嵌套括号与浮点除法
5/22.5整数输入、浮点结果
8/0除零错误异常分支

这张表放进报告的“测试”章节,比写三大段“测试充分”都有用。老师看报告时最关心的不是代码能不能跑,而是你有没有想过边界条件。这张表本身就证明你想过了。

5. 让实验报告再多拿 10 分:用后缀表达式建树做自动对拍

有精力的同学可以多做一步:把后缀表达式进一步建成表达式树,再用中序遍历把树还原成带括号的中缀表达式,跟原始输入做对比。这相当于给求值器加了一个“自我检查”,也是区分“能跑”和“可验证”的分水岭。

做法不复杂。表达式树的后缀构建和后缀求值如出一辙:遇到数字建叶子节点压栈,遇到运算符弹出两颗子树,组成新节点再压栈。我用数组模拟树的节点,避免动态内存管理,实验报告里更好解释:

typedef struct { double value; // 叶子存数值 int is_op; // 1 表示运算符节点 char op; // 运算符 int left, right; // 左右孩子在 nodes 数组中的下标 } TreeNode; int build_tree(const char *postfix, TreeNode nodes[], int *cnt) { int stack[MAX], top = -1, i = 0; while (postfix[i]) { if (postfix[i] == ' ') { i++; continue; } if (isdigit(postfix[i])) { double num = 0; while (isdigit(postfix[i])) { num = num * 10 + postfix[i++] - '0'; } nodes[*cnt] = (TreeNode){num, 0, 0, -1, -1}; stack[++top] = (*cnt)++; } else { int r = stack[top--]; // 先弹出右子树 int l = stack[top--]; nodes[*cnt] = (TreeNode){0, 1, postfix[i], l, r}; stack[++top] = (*cnt)++; i++; } } return stack[top]; // 树根在 nodes 中的下标 }

建完树之后,中序遍历并在恰当位置加括号,就能还原出标准中缀表达式。把这个字符串和原始输入一比对,转换和求值任何一步出错都会暴露出来。有余力还可以写一个小脚本随机生成合法表达式批量喂给程序,把输出结果和期望值对拍。我当年写这个实验时只验证了三五个样例就交了,后来被同学拿100-50-20一测就露馅,那次教训之后,我写任何表达式求值器都会先做一次树对拍。多花这 30 分钟,报告的质量完全不在一个层次上。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询