ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

数据结构表达式求值实验:栈、优先级与结合性避坑指南

数据结构表达式求值实验:栈、优先级与结合性避坑指南 简介这份《数据结构》课程设计实验报告以“表达式求值”为题面向计算机相关专业学生及正在学习栈应用的初学者帮助读者理解如何借助栈结构解决算术表达式求值中运算符优先级与括号嵌套的难题。压缩包内仅含1个doc文档约122KB内容为完整的课程设计报告书结构规范、层次清晰。报告从前言、概要设计、详细设计、软件测试到总结与附录逐层展开重点讲解运算符栈OPTR与操作数栈OPND的协同工作机制涵盖顺序栈的存储结构设计、算符优先关系表的构建、ADT Stack的接口定义以及Precede、Operate、EvalExpr等核心函数的实现思路并配有算法流程图与测试分析。目前已有461人学习下载适合需要撰写同类实验报告、准备课程设计答辩或巩固栈与表达式求值算法的读者参考借鉴可据此快速梳理设计脉络、理解算符优先算法的完整实现过程。1. 表达式求值实验为什么总在“栈溢出”和“优先级”上翻车很多人第一次拿到“数据结构表达式求值实验报告”这个题目以为就是写个计算器把35*2算成 13 就交差。真动手才发现输入(35)*2-10/5时结果对不上输入2^3^2时结果从 512 变成 64输入-32时程序直接崩掉。表达式求值在数据结构课程里属于栈结构的经典应用也是王道408、严蔚敏C语言版教材里反复强调的考点但教材给的是骨架实验报告要的是能跑、能测、能解释的完整方案。这个方向适合正在做课程设计的学生、准备408数据结构考研的复习者以及想用Java或C把中缀转后缀真正落地的一线开发者。核心就三件事栈怎么设计、优先级怎么定、边界怎么兜。把这三件事拆开揉碎实验报告自然有内容可写代码也能扛住老师的花式测试用例。2. 中缀转后缀两个栈的配合逻辑与手算验证2.1 为什么必须先把中缀转成后缀人习惯看中缀表达式比如12*3但计算机从左到右扫描时无法直接判断和*谁先算。后缀表达式逆波兰式把运算符放到操作数后面变成1 2 3 * 扫描时遇到运算符就弹出栈顶两个操作数计算不需要考虑括号和优先级。这个转换过程是表达式求值实验的核心也是数据结构课程里栈的典型应用场景。常见做法是用一个运算符栈暂存还没轮到计算的符号遇到数字直接输出到后缀序列遇到运算符则根据优先级决定是压栈还是弹出。严蔚敏版教材里给出的算符优先法就是这套逻辑王道408的代码题也反复考这个转换过程。转换规则可以拆成四条遇到操作数直接加入后缀序列遇到左括号直接压栈遇到右括号则不断弹出栈顶运算符加入后缀序列直到遇到左括号并将其弹出丢弃遇到普通运算符时如果栈顶运算符优先级不低于当前运算符就弹出栈顶加入后缀序列重复此过程直到栈顶优先级更低或栈为空然后把当前运算符压栈。扫描结束后把栈里剩余运算符依次弹出加入后缀序列。手算验证是实验报告里必须体现的一步。以35*(2-8)/4为例扫描过程如下3输出压栈5输出*因为栈顶优先级更低所以直接压栈(压栈2输出-压栈8输出)触发弹出-输出并丢弃(此时后缀序列为3 5 2 8 -栈内为 *。接着/到来栈顶*优先级不低于/弹出*输出此时栈顶变为优先级低于/停止弹出将/压栈。然后4输出。扫描结束依次弹出/、*、最终后缀表达式为3 5 2 8 - * 4 / 。这个手算过程写进实验报告比只贴代码更有说服力。2.2 用C语言实现转换结构体栈与优先级表下面是一段可以直接编译运行的C语言代码实现了中缀转后缀的核心逻辑。代码里用数组模拟栈定义了运算符优先级表处理了多位数和小数点的情况。#include stdio.h #include stdlib.h #include string.h #include ctype.h #define MAX 100 // 运算符栈 typedef struct { char data[MAX]; int top; } OpStack; void initOpStack(OpStack *s) { s-top -1; } int isOpStackEmpty(OpStack *s) { return s-top -1; } void pushOp(OpStack *s, char op) { if (s-top MAX - 1) { s-data[(s-top)] op; } } char popOp(OpStack *s) { if (!isOpStackEmpty(s)) { return s-data[(s-top)--]; } return \0; } char peekOp(OpStack *s) { if (!isOpStackEmpty(s)) { return s-data[s-top]; } return \0; } // 返回运算符优先级数字越大优先级越高 int priority(char op) { switch (op) { case : case -: return 1; case *: case /: return 2; case ^: return 3; case (: return 0; // 左括号在栈内优先级最低保证不弹出 default: return -1; } } // 中缀转后缀 void infixToPostfix(const char *infix, char *postfix) { OpStack s; initOpStack(s); int j 0; int i 0; while (infix[i] ! \0) { char c infix[i]; if (isdigit(c) || c .) { // 处理多位数和小数点 while (isdigit(infix[i]) || infix[i] .) { postfix[j] infix[i]; } postfix[j] ; // 用空格分隔操作数 } else if (c () { pushOp(s, c); i; } else if (c )) { while (!isOpStackEmpty(s) peekOp(s) ! () { postfix[j] popOp(s); postfix[j] ; } if (!isOpStackEmpty(s)) { popOp(s); // 弹出左括号 } i; } else if (c || c - || c * || c / || c ^) { // 注意^ 是右结合栈顶优先级大于当前才弹出 while (!isOpStackEmpty(s) priority(peekOp(s)) priority(c)) { postfix[j] popOp(s); postfix[j] ; } // 对于左结合运算符栈顶优先级等于当前也要弹出 if (c ! ^) { while (!isOpStackEmpty(s) priority(peekOp(s)) priority(c)) { postfix[j] popOp(s); postfix[j] ; } } pushOp(s, c); i; } else { i; // 跳过空格等无关字符 } } while (!isOpStackEmpty(s)) { postfix[j] popOp(s); postfix[j] ; } postfix[j] \0; } int main() { char infix[] 35*(2-8)/4; char postfix[MAX * 2]; infixToPostfix(infix, postfix); printf(中缀: %s\n, infix); printf(后缀: %s\n, postfix); return 0; }这段代码的关键点在于优先级表的定义和右结合运算符的处理。priority函数里左括号返回 0保证它不会被普通运算符弹出只有遇到右括号时才主动弹出。^运算符在数学上是右结合的2^3^2应该算成2^(3^2)512所以代码里对^单独处理栈顶优先级等于当前优先级时不弹出。其他运算符都是左结合栈顶优先级等于当前时也要弹出保证1-2-3算成(1-2)-3-4而不是1-(2-3)2。多位数处理用while循环连续读取数字和小数点并在每个操作数后加空格分隔方便后续后缀求值时切分。参数方面MAX定义了栈的最大容量实际实验里可以改成动态分配或者根据输入长度计算。postfix数组大小设为MAX*2是为了容纳空格分隔符如果输入表达式很长需要相应调整。priority函数里没有处理一元负号比如-32里的负号会被当成减号这是后面避坑章节要专门解决的问题。2.3 后缀求值操作数栈的压弹节奏拿到后缀表达式后求值过程比转换更直接。扫描后缀序列遇到数字就压入操作数栈遇到运算符就弹出两个操作数先弹出的是右操作数后弹出的是左操作数计算完把结果压回栈。扫描结束后栈里剩下的唯一元素就是最终结果。// 操作数栈 typedef struct { double data[MAX]; int top; } NumStack; void initNumStack(NumStack *s) { s-top -1; } void pushNum(NumStack *s, double val) { if (s-top MAX - 1) { s-data[(s-top)] val; } } double popNum(NumStack *s) { if (s-top 0) { return s-data[(s-top)--]; } return 0.0; } // 后缀表达式求值 double evalPostfix(const char *postfix) { NumStack s; initNumStack(s); int i 0; while (postfix[i] ! \0) { if (isdigit(postfix[i]) || postfix[i] .) { double num 0; int decimal 0; double fraction 0.1; while (isdigit(postfix[i]) || postfix[i] .) { if (postfix[i] .) { decimal 1; } else if (!decimal) { num num * 10 (postfix[i] - 0); } else { num (postfix[i] - 0) * fraction; fraction * 0.1; } i; } pushNum(s, num); } else if (postfix[i] || postfix[i] - || postfix[i] * || postfix[i] / || postfix[i] ^) { double right popNum(s); double left popNum(s); double result 0; switch (postfix[i]) { case : result left right; break; case -: result left - right; break; case *: result left * right; break; case /: result left / right; break; case ^: { result 1; for (int k 0; k (int)right; k) { result * left; } break; } } pushNum(s, result); i; } else { i; // 跳过空格 } } return popNum(s); }求值部分最容易翻车的地方是操作数弹出顺序。后缀表达式3 5 -对应中缀3-5扫描到-时先弹出的是5后弹出的是3所以left3, right5计算left-right得到-2。如果顺序写反结果就变成5-32这种错误在实验报告里如果没写清楚老师一眼就能看出来。^运算符这里用循环实现整数次幂实际实验里如果要求支持小数次幂需要引入math.h的pow函数但要注意链接时加-lm参数。3. 优先级与结合性三个必须写进实验报告的参数表3.1 运算符优先级表的设计与验证优先级表是表达式求值的“宪法”所有弹出和压栈决策都依赖它。下面这张表可以直接放进实验报告覆盖了常见运算符和括号。运算符栈内优先级栈外优先级结合性说明11左结合加减同级-11左结合加减同级*22左结合乘除同级/22左结合乘除同级^34右结合幂运算栈外优先级高于栈内(05—左括号栈内最低保证不被弹出)—0—右括号不压栈触发弹出这张表里^的栈内优先级是 3栈外优先级是 4这个差异是右结合的关键。当扫描到第二个^时栈顶也是^栈内优先级 3 小于栈外优先级 4所以不弹出直接压栈最终计算顺序从右往左。而的栈内和栈外都是 1扫描到第二个时栈顶也是栈内优先级等于栈外优先级左结合要求弹出栈顶所以先算左边的加法。这个细节在王道408的代码题里经常考实验报告里把这张表列出来并解释清楚能直接体现对栈结构的理解深度。3.2 结合性对结果的影响用2^3^2和1-2-3做对比结合性不是理论概念它直接改变计算结果。2^3^2如果按左结合算先算2^38再算8^264如果按右结合算先算3^29再算2^9512。数学上幂运算规定为右结合所以正确答案是 512。1-2-3如果按右结合算先算2-3-1再算1-(-1)2按左结合算先算1-2-1再算-1-3-4。减法是左结合正确答案是 -4。在代码里体现这个差异就是在弹出条件上加一个判断对于右结合运算符只有栈顶优先级严格大于当前优先级才弹出对于左结合运算符栈顶优先级大于等于当前优先级就弹出。这个逻辑在 2.2 节的代码里已经实现实验报告里可以单独列一小节用这两个表达式做测试用例把中间过程打印出来证明代码正确处理了结合性。3.3 括号匹配与非法表达式拦截括号处理是表达式求值里另一个高频翻车点。左括号在栈内优先级设为 0保证任何运算符都不会把它弹出去只有遇到右括号时才主动弹出直到左括号。如果扫描完整个表达式后栈里还有左括号说明括号不匹配需要报错。同样如果遇到右括号时栈已经空了或者栈顶不是左括号也说明括号不匹配。非法表达式拦截还包括运算符连续出现如32、操作数缺失如3、除数为零、小数点位置错误如3..5。这些检查不需要全部在转换阶段做可以在求值阶段捕获异常。实验报告里建议单独写一个校验函数在转换前先扫描一遍把明显非法的输入拦下来给出具体错误位置。这样比程序崩溃或者输出一个莫名其妙的结果要好得多也是实验报告里“测试与分析”部分的重要素材。4. 避坑与排查表达式求值实验里最常见的五个翻车现场4.1 现象-32算成-1而不是-1但3*-2直接崩溃原因一元负号没有被识别。在3*-2里*后面的-是一元负号但代码把它当成二元减号处理弹出操作数时栈里只有一个3另一个操作数不存在导致栈下溢。-32里开头的-也是一元负号但代码把它当成二元减号压栈后没有左操作数最终结果虽然碰巧对但逻辑是错的。解决在转换阶段判断-是一元还是二元。如果-出现在表达式开头或者出现在(后面或者出现在另一个运算符后面就是一元负号。一元负号可以特殊标记为#或者~优先级设为最高求值时取相反数。实验报告里可以把一元负号作为扩展功能写进去体现对边界情况的考虑。4.2 现象多位数123456算成12345621原因扫描时逐个字符处理没有把连续数字合并成一个操作数。123被拆成1、2、3三个操作数压栈求值时自然出错。解决在扫描到数字时用while循环连续读取后续所有数字和小数点拼成一个完整的数字字符串再用atof或手动转换。2.2 节的代码里已经用while (isdigit(infix[i]) || infix[i] .)处理了这个问题并在操作数后加空格分隔。如果实验要求支持科学计数法如1.5e3还需要额外处理e和符号。4.3 现象2^3^2输出 64 而不是 512原因^被当成左结合运算符处理栈顶优先级等于当前优先级时弹出了栈顶导致先算左边的2^3。解决在弹出条件里对^单独判断只有栈顶优先级严格大于当前优先级才弹出。2.2 节代码里if (c ! ^)那段就是处理这个问题的。实验报告里可以把2^3^2作为测试用例打印转换后的后缀表达式应该是2 3 2 ^ ^求值结果 512。4.4 现象输入(35)*2时程序输出3 5 2 *但求值结果不对原因后缀表达式转换正确但求值时操作数弹出顺序写反了。3 5 应该弹出5和3计算358如果写成53结果虽然一样但遇到3 5 -时就会算成5-32而不是3-5-2。解决求值时先弹出的赋值给right后弹出的赋值给left计算left op right。这个顺序在 2.3 节代码里已经体现。实验报告里建议用10-3-2做测试正确结果是 5如果顺序写反会得到 9 或别的值。4.5 现象除数为零时程序输出inf或直接崩溃原因浮点数除以零在C语言里不会报错但结果是inf或nan如果后续还有运算会传播。整数除以零会直接触发硬件异常导致程序崩溃。解决在求值阶段遇到除法时先判断除数是否为零。如果为零输出错误信息并终止求值或者返回一个特殊值。实验报告里可以把除零检查作为健壮性的一部分写进去同时测试1/0和0/0两种情况说明处理策略。5. 从实验报告到可复用代码把表达式求值封装成独立模块5.1 接口设计与错误码约定实验报告交完之后这套代码其实可以继续用。我一般会把表达式求值封装成一个独立模块对外只暴露两个函数一个负责校验和转换一个负责求值。接口设计如下// 错误码定义 typedef enum { EVAL_OK 0, EVAL_ERR_BRACKET 1, // 括号不匹配 EVAL_ERR_DIV_ZERO 2, // 除数为零 EVAL_ERR_INVALID_EXPR 3, // 非法表达式 EVAL_ERR_STACK_OVERFLOW 4 // 栈溢出 } EvalError; // 对外接口 EvalError evaluateExpression(const char *infix, double *result);这个接口把错误码和结果分开调用方先检查错误码再使用结果避免拿到一个无效值继续计算。错误码用枚举而不是数字可读性更好。实验报告里如果要求写“模块设计”章节这个接口定义可以直接放进去。5.2 用测试用例驱动验证从11到((23)*4-5)/6验证表达式求值模块最有效的方法是用测试用例驱动。下面这组用例覆盖了基本运算、优先级、括号、结合性、多位数、小数和错误处理可以直接写进实验报告的测试章节。用例编号输入表达式期望结果覆盖点1112最基本加法235*213乘法优先级高于加法3(35)*216括号改变优先级410-3-25减法左结合52^3^2512幂运算右结合6123456579多位数73.14*26.28小数8((23)*4-5)/62.5嵌套括号91/0错误码 2除零10(12错误码 1括号不匹配把这组用例跑通实验报告的“测试结果与分析”部分就有扎实的数据支撑。每个用例可以打印输入、输出和中间的后缀表达式方便定位问题。如果某个用例失败对照前面的避坑章节排查基本能覆盖90%以上的常见错误。5.3 性能边界与栈容量估算表达式求值的性能瓶颈在栈操作时间复杂度是 O(n)空间复杂度也是 O(n)n 是表达式长度。实际实验里输入表达式通常不会超过几百个字符用固定大小的数组栈完全够用。但如果要做成一个通用模块栈容量需要动态估算运算符栈的最大深度不会超过表达式长度操作数栈的最大深度也不会超过操作数个数。保守做法是把栈容量设为输入长度的两倍或者用动态数组在压栈时自动扩容。我自己的习惯是在实验报告里加一段“复杂度分析”把时间复杂度和空间复杂度写清楚再说明栈容量估算依据。这样老师能看到你不只是会写代码还理解背后的资源消耗。表达式求值这个方向从课程实验到408考研再到实际项目里的计算引擎核心逻辑都是一样的把栈、优先级、结合性这三块吃透后面遇到更复杂的语法分析也能触类旁通。希望帮到你。本文还有配套的精品资源点击获取
返回列表