ARTICLE DETAIL

资讯详情

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

数据结构表达式求值:双栈与算符优先算法详解

数据结构表达式求值:双栈与算符优先算法详解 简介这份《数据结构》课程设计实验报告面向计算机专业学生与数据结构初学者聚焦栈在算术表达式求值中的经典应用帮助读者理解运算符优先级与括号嵌套的处理思路。报告以顺序栈为核心分别构建运算符栈OPTR与操作数栈OPND采用算符优先算法完成识别与计算输入限定为正整数与、-、*、/四则运算以#作为结束标志。内容涵盖前言、概要设计、ADT描述、功能模块分析、详细设计、软件测试与总结等完整章节并给出Precede优先级判断表、栈结构定义及主要算法代码便于对照复现与课程答辩准备。资源包共1个doc文档约122KB结构完整、篇幅精炼适合作为课程设计模板或实验参考。目前已有461人学习可帮助读者快速掌握栈的典型应用与表达式求值实现要点。1. 表达式求值实验报告从栈结构到可运行代码的完整拆解很多人第一次拿到「数据结构表达式求值实验报告.doc」这类资源时会以为它只是一份交完就忘的课程作业。但如果你正在准备数据结构课程设计、考研 408 的代码手写题或者想搞懂编译器里表达式解析的底层逻辑这份文档的价值就完全不一样了。它完整覆盖了从 ADT 定义、算符优先关系表、双栈结构设计到 C 语言完整源码和测试用例的全链路内容。操作数限定为正整数运算符为 、-、*、/以 # 作为表达式结束符——这个简化模型恰好是理解算符优先分析法的经典入口。适合正在做课程设计的学生、需要手写代码应对 408 数据结构代码题的考研人以及想从零实现一个表达式解析器的开发者。2. 双栈结构设计为什么运算符栈和操作数栈必须分开2.1 顺序栈的存储结构选择这份实验报告在数据结构设计上做了一个关键决策用两个独立的顺序栈分别存储运算符和操作数。原因很直接——运算符是 char 类型操作数是 int 类型如果强行用一个栈遇到两位数以上的整数就没法处理了。文档里定义了两套结构体/* 定义字符类型栈 —— 存运算符 */ typedef struct { int stacksize; // 栈容量 char *base; // 栈底指针始终指向栈底 char *top; // 栈顶指针插入时增1删除时减1 } Stack; /* 定义整型栈 —— 存操作数 */ typedef struct { int stacksize; // 栈容量 int *base; // 栈底指针 int *top; // 栈顶指针 } Stack2;这里用的是动态分配的顺序栈初始容量STACK_INIT_SIZE为 100扩容增量STACKINCREMENT为 20。base始终指向栈底top随插入删除移动top base就是栈空标记。常见做法是初始化时用malloc分配连续内存如果分配失败返回ERROR。注意文档中的 Push 操作没有做栈满判断和扩容处理实际使用时如果表达式很长存在溢出风险。我一般会在 Push 里加一个if(s-top - s-base s-stacksize)的检查触发realloc扩容。2.2 算符优先关系表的实现方式整个算法的核心在于Precede(c1, c2)函数——它决定了当前读到的运算符和栈顶运算符之间谁先算。文档用了一个一维数组array[49]来存储 7×7 的优先关系矩阵通过array[7*ij]定位。7 个运算符分别是、-、*、/、(、)、#。char Precede(char c1, char c2) { int i 0, j 0; static char array[49] { , , , , , , , // 行 , , , , , , , // - 行 , , , , , , , // * 行 , , , , , , , // / 行 , , , , , , !, // ( 行 , , , , !, , , // ) 行 , , , , , !, // # 行 }; switch(c1) { case : i 0; break; case -: i 1; break; case *: i 2; break; case /: i 3; break; case (: i 4; break; case ): i 5; break; case #: i 6; break; } switch(c2) { case : j 0; break; case -: j 1; break; case *: j 2; break; case /: j 3; break; case (: j 4; break; case ): j 5; break; case #: j 6; break; } return array[7 * i j]; }返回值含义表示栈顶运算符优先级高先弹出计算表示当前运算符优先级高压栈表示脱括号左右括号相遇!表示非法组合如#)或(#正常表达式不会出现。参数说明c1是运算符栈顶元素c2是当前扫描到的字符。矩阵中*和/对和-全部返回因为乘除优先级高于加减(对除)以外的所有运算符返回保证括号内的运算先被处理。2.3 操作数识别与多位数处理文档中EvalExpr()函数处理操作数的逻辑值得单独拎出来说。它用atoi(ptr)从字符串当前位置提取整数再用num(m)计算这个整数占了多少字符位然后ptr n跳过这些字符。num()函数内部用itoa把整数转回字符串再取strlen这个做法虽然绕了一步但逻辑是对的。int num(int n) { // 返回操作数的字符长度 char p[10]; itoa(n, p, 10); // 整数转字符串 n strlen(p); // 求字符长度 return n; }常见做法是直接用sprintf替代itoa因为itoa不是标准 C 函数在 GCC 下可能编译不过。替换方案int num(int n) { char p[10]; sprintf(p, %d, n); // 标准库函数可移植性更好 return strlen(p); }3. EvalExpr 主循环算符优先算法的完整执行流程3.1 算法主循环的四个分支EvalExpr()是整个程序的中枢。它的主循环条件是c ! # || GetTop(OPTR) ! #意思是当前字符和栈顶运算符不同时为#时继续循环。循环体内分两大分支int EvalExpr() { char c, theta, x; int n, m, a, b; c *ptr; while (c ! # || GetTop(OPTR) ! #) { if (!In(c)) { // 不是运算符即操作数 if (!In(*(ptr - 1))) ptr ptr - 1; m atoi(ptr); // 提取整数值 n num(m); // 计算字符长度 Push2(OPND, m); // 操作数入栈 ptr ptr n; // 指针跳过该操作数 c *ptr; } else { switch (Precede(GetTop(OPTR), c)) { case : // 栈顶优先级低当前运算符入栈 Push(OPTR, c); c *ptr; break; case : // 脱括号 x Pop(OPTR); c *ptr; break; case : // 栈顶优先级高弹出计算 theta Pop(OPTR); b Pop2(OPND); a Pop2(OPND); Push2(OPND, Operate(a, theta, b)); break; } } } return GetTop2(OPND); }四个关键点第一In(c)判断当前字符是否为运算符不是就按操作数处理第二ptr - 1回退是因为c *ptr已经让指针前进了一位第三case 里弹出两个操作数时先弹出的是右操作数b后弹出的是左操作数a顺序不能反第四case 只在左右括号相遇时触发弹出左括号即可。3.2 以 3*(7-2) 为例的逐步追踪文档给出了一个完整的执行过程表我把它重新整理成更直观的形式步骤OPTR 栈OPND 栈当前字符操作1#3*操作数 3 入 OPND2#3*Precede(#,)入 OPTR3#*3(Precede(*,()( 入 OPTR4#*(37操作数 7 入 OPND5#*(3,7-Precede(,-)- 入 OPTR6#*(-3,72操作数 2 入 OPND7#*(-3,7,2)Precede(-,))计算 7-258#*(3,5)Precede((,))脱括号9#*3,5#Precede(,#)计算 351510#15#栈顶和当前字符均为 #返回 15这个追踪过程把算符优先算法的每一步都摊开了。新手最容易卡住的地方是第 7 步遇到)时先和栈顶的-比较得到所以弹出-并计算7-2结果 5 压回 OPND。然后继续比较(和)得到弹出(继续读下一个字符。3.3 Operate 函数与除法边界int Operate(int a, char op, int b) { switch (op) { case : return a b; case -: return a - b; case *: return a * b; case /: return a / b; // 整数除法需注意除零 } return 0; }参数说明a是左操作数b是右操作数op是运算符。这里用的是整数除法7/2结果是 3 而不是 3.5。如果表达式里出现除零程序会直接崩溃。常见做法是在case /里加一个判断if (b 0) { printf(除数不能为0\n); exit(1); }。4. 编译运行与测试从源码到可执行文件的踩坑记录4.1 编译环境的适配问题文档中的源码是在 Windows 环境下用 VC 6.0 或类似 IDE 编写的直接拿到 GCC 或 Clang 下编译会遇到几个问题。最典型的是itoa函数——它不是 ANSI C 标准库函数Linux 下 GCC 不提供。另外gets()函数在 C11 标准中已被移除新版编译器会报 warning 甚至 error。# Linux 下编译需要先替换 itoa 和 gets gcc -o expr_eval expr_eval.c -stdc99 -Wall # 如果报错 undefined reference to itoa替换为 sprintf # 如果报错 gets is dangerous替换为 fgets替换gets的方案// 原代码 do { gets(expr); } while (!*expr); // 替换为 do { fgets(expr, sizeof(expr), stdin); expr[strcspn(expr, \n)] \0; // 去掉换行符 } while (!*expr);fgets比gets安全因为它限制了读取长度不会造成缓冲区溢出。strcspn用来找到换行符位置并替换为字符串结束符。4.2 测试用例设计与预期结果文档的软件测试部分只给了截图描述没有具体的测试数据。我补充一组可以直接用的测试用例输入表达式预期输出测试目的3*(7-2)#15括号优先级1020*3#70多位数与乘加优先级100/5-8#12多位数除法与减法(46)*(37)#100多重括号234*5#120连续同级运算12345#15长表达式栈操作运行方式编译后在终端输入表达式以#结尾回车即可。./expr_eval 请输入正确的表达式以#结尾:3*(7-2)# 表达式结果为:154.3 指针回退逻辑的隐患EvalExpr()里有一行if (!In(*(ptr - 1))) ptr ptr - 1;这个回退操作是为了处理c *ptr已经让指针前进但当前字符是操作数首位的情况。但这个逻辑在表达式以操作数开头时是正确的如果表达式以运算符开头比如-35#ptr - 1指向的是字符串起始位置之前属于未定义行为。常见做法是在表达式前面补一个0把-35变成0-35或者单独处理一元负号。5. 避坑与排查这份实验报告代码里最容易翻车的五个地方5.1 现象输入两位数结果只算了第一位原因num()函数用itoa把整数转字符串求长度但部分编译器对itoa的支持不一致导致返回长度错误。或者ptr指针在提取操作数后没有正确跳过所有字符位。解决把itoa替换为sprintf并在Push2之后打印ptr当前指向的字符做调试。确认ptr ptr n中的n等于实际数字位数。5.2 现象表达式(35)#计算结果为 0 或崩溃原因脱括号分支case 中x Pop(OPTR)弹出的是左括号但后续没有正确处理 OPND 栈中的数据。如果 OPND 栈为空时调用GetTop2会读到非法内存。解决在case 中确认 OPND 栈不为空并在脱括号后检查GetTop(OPTR)是否仍为#。如果表达式以(开头且没有匹配的)循环条件会提前退出。5.3 现象连续同级运算2*3*4#结果错误原因Precede(*, *)返回的是意味着栈顶的*先算。但如果Operate函数中参数顺序搞反a和b互换减法和除法会得到错误结果。解决确认case 中先b Pop2(OPND)再a Pop2(OPND)因为栈是后进先出先弹出的是右操作数。加法和乘法交换顺序不影响结果但减法和除法必须严格区分。5.4 现象程序在输入空表达式或只有#时崩溃原因main函数中do { gets(expr); } while (!*expr);只检查了字符串是否为空但如果用户直接输入#EvalExpr中GetTop2(OPND)会在空栈上操作。解决在EvalExpr返回前加一个判断if (OPND.top OPND.base) return 0;。或者在main中检查表达式长度只有#时直接输出 0。5.5 现象GCC 编译报错undefined reference to itoa原因itoa是 Windows 特有函数Linux 下的 glibc 不提供。解决用sprintf替代或者自己写一个简单的整数转字符串函数int num(int n) { char p[10]; int len 0; if (n 0) return 1; while (n 0) { p[len] 0 n % 10; n / 10; } return len; }6. 从实验报告到工程代码三个可以直接落地的改进技巧6.1 用哈希表替代 7×7 数组做优先级判断文档中的Precede函数用一维数组模拟二维矩阵可读性一般。如果换成链式结构或直接用二维数组char precede[7][7]代码会更直观。更进一步可以用一个简单的哈希函数把运算符映射到 0-6 的索引int op_index(char op) { switch (op) { case : return 0; case -: return 1; case *: return 2; case /: return 3; case (: return 4; case ): return 5; case #: return 6; default: return -1; } } char precede_table[7][7] { {,,,,,,}, {,,,,,,}, {,,,,,,}, {,,,,,,}, {,,,,,,!}, {,,,,!,,}, {,,,,,!,} }; char Precede(char c1, char c2) { int i op_index(c1); int j op_index(c2); if (i -1 || j -1) return !; return precede_table[i][j]; }这样改的好处是新增运算符时只需要扩展op_index和表格不需要手动计算7*ij的偏移量。二维数组的视觉结构也和文档里的优先关系表完全对应调试时一眼就能看出哪个组合出了问题。6.2 增加表达式合法性校验文档的代码假设输入永远合法但实际使用中用户可能输入35#、3*(#这类非法表达式。我一般会在EvalExpr开头加一个预扫描函数int validate_expr(char *expr) { int paren_count 0; int prev_is_op 1; // 标记前一个字符是否为运算符 while (*expr *expr ! #) { if (*expr () { paren_count; prev_is_op 1; } else if (*expr )) { paren_count--; if (paren_count 0) return 0; // 右括号多于左括号 prev_is_op 0; } else if (In(*expr)) { if (prev_is_op *expr ! () return 0; // 连续运算符 prev_is_op 1; } else if (*expr 0 *expr 9) { prev_is_op 0; } else { return 0; // 非法字符 } expr; } return paren_count 0 !prev_is_op; }这个校验函数在main中调用不合法就提示用户重新输入。参数说明paren_count跟踪括号配对prev_is_op防止连续运算符。返回 1 表示合法0 表示非法。6.3 用栈深度监控辅助调试算符优先算法出问题时最有效的调试手段是打印两个栈的实时状态。我在EvalExpr循环末尾加了一段条件编译的调试输出#ifdef DEBUG printf(OPTR: ); for (char *p OPTR.base; p OPTR.top; p) printf(%c , *p); printf(| OPND: ); for (int *p OPND.base; p OPND.top; p) printf(%d , *p); printf(| 当前: %c\n, c); #endif编译时加-DDEBUG就能看到每一步的栈变化对照第 3 章的执行过程表哪一步偏了一眼就能定位。从那以后我每次实现栈类算法都强制走一遍「先打印栈状态再单步跟踪」的流程比盯着代码空想快得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表