通过后缀表达式求表达式的值
通过后缀表达式求表达式的值
知识点:树的前中后序遍历(可以参考AK宝典),后缀表达式(逆波兰式),中缀转后缀,后缀表达式求值
引言:
对于一个数学表达式,比如说 1-(2+3/4)*5=?可以很容易地人工计算出结果。然而如果想要用计算机求这样表达式的值似乎有一点麻烦,因为计算机不太方便处理运算符的优先级,尤其是括号对优先级的影响。因此,想要用计算机求表达式的值,最好要找到一种方法可以排除括号,运算符优先级的干扰。
表达式与树:
事实上,任何一个数学表达式都可唯一转化为一棵树,比如说:1-(2+3/4)*5=?可以转化为如下一棵二叉树:
树的特性:
可以看出,这棵二叉树的每一个叶子节点都是一个数,而每两个节点的父节点都是一个运算符,比如最右边子树3,4,/,它代表3/4
如果能拿到这样的一颗二叉树,求上述表达式的值就很简单了,我们只需要先算出子树3/4=0.75,再计算子树2+0.75=2.75,再计算子树5*2.75=13.75,最后计算根节点处1-13.75=-12.75即可求出表达式的值
不难发现,这颗二叉树的中序遍历结果为:1-2+3/4*5
,也就是原表达式去掉括号的样子。因此,原数学表达式也被称为中缀表达式,对这棵树后序遍历得到的结果称为后缀表达式
树的求法:
而这样的树的求法也很很简单,我们只需要找出原表达式中最先计算的运算( 3/4
),把它作为一颗子树,然后我们就可以把这个运算(3/4
)视为一个整体,找出原式中其次优先的运算(2+3/4
)和之前的子树组成一颗更大的树,直至把所有运算都处理完,就可以得出一整棵树
不过,对于计算机而言,无论是求树的过程,还是通过树求值的过程都还是有些复杂,需要进一步简化
使用后缀表达式求值:
首先,这颗树对应的后缀表达式为 1 2 3 4 / + 5 * -,不难看出,后缀表达式不含括号,数字的排列顺序与原式相同,原式中优先级越高的运算符在该后缀表达式中越先出现。
因此,一定存在某种方案能够使得计算机在处理一个后缀表达式时能够先把运算优先级高的运算计算出来,使其能够用于后续的低优先级运算。
而对于中缀表达式,由于运算级高的运算不一定排在前面,因此计算机必须反复扫描中缀表达式,先算优先级高的,再算优先级低的运算,这就是后缀表达式更加易于求值的原因。
具体而言:
我们依次处理后缀表达式中的每一个数字或符号a[i]:
- 如果a[i]是一个数字,就直接将其压入栈s中
- 如果a[i]是一个运算符,则将栈s顶端的第一个元素a,第二个元素b分别从栈中取出并清除,计算 c=b运算a (比如
c=b-a
),然后再将c压入栈中,进行后续操作
对于后缀表达式1 2 3 4 / + 5 * -,运算过程如下:
后缀表达式 | 栈(从底到顶) | 运算过程 |
1 2 3 4 / + 5 * - | 1 | 1入栈 |
1 2 3 4 / + 5 * - | 1 2 | 2入栈 |
1 2 3 4 / + 5 * - | 1 2 3 | 3入栈 |
1 2 3 4 / + 5 * - | 1 2 3 4 | 3入栈 |
1 2 3 4 / + 5 * - | 1 2 0.75 | 3,4出栈 3/4=0.75入栈 |
1 2 3 4 / + 5 * - | 1 2.75 | 2,0.75出栈 2+0.75=2.75入栈 |
1 2 3 4 / + 5 * - | 1 2.75 5 | 5入栈 |
1 2 3 4 / + 5 * - | 1 13.75 | 2.75,5出栈 2.75*5=13.75入栈 |
1 2 3 4 / + 5 * - | -12.75(答案) | 1 13.75出栈 1-13.75=-12.75入栈 |
不难看出,使用后缀表达式求值的过程其实本质上与通过子树逐步求值的过程相同
那么,要如何求后缀表达式呢?
中缀表达式转后缀表达式:
中缀表达式转后缀表达式可以借助两个栈来完成,设两个栈分别为s1,s2,我们从左到右遍历原本的中缀表达式,设当前遍历到的数字或符号为a[i]
- 如果a[i]为
数字
,那么直接压入s2中 - 如果a[i]为
运算符
,那么需比较a[i]和s1栈顶符号ch的优先级- 如果s1为空或ch为'(',则直接将a[i]压入栈s1中
- 如果a[i]优先级 > ch优先级,则直接将a[i]压入栈s1中
- 如果a[i]优先级 < ch优先级,则将ch压入s2中,并从s1中删除,再比较新的s1栈顶元素ch'与a[i]的优先级
- 如果a[i]优先级 = ch优先级,若ch与a[i]为左结合,则认为ch优先级 > a[i]优先级;若ch与a[i]为右结合,则认为ch优先级 < a[i]优先级
- 如果a[i]为左括号
'('
,那么直接压入s1中 - 如果a[i]为右括号
')'
,那么将s1中第一个左括号'('前的所有运算符依次压到到s2中,并从s1中删除,最后将s1中的这个左括号'('删除 - 如果a[i]为结束符
'='
,那么s1中剩余的全部运算符依次移到s2中,转换结束
完成后s2所存结果就是原式的后缀表达式,但是由于栈式先进后出的,所以s2栈顶元素依次输出所得其实式逆序的后缀表达式,需要进行反向处理
注:典型的左结合运算符有+-*/
,特点为a+b+c=(a+b)+c
典型的右结合运算符有^
(幂运算),特点为a^b^c=a^(b^c)
对于1-(2+3/4)*5=,运算过程如下:
原式 | 栈s1 | 栈s2 | 运算过程 |
1-(2+3/4)*5= | 1 | 1 入栈 | |
1-(2+3/4)*5= | - | 1 | - 入栈 |
1-(2+3/4)*5= | - ( | 1 | ( 入栈 |
1-(2+3/4)*5= | - ( | 1 2 | 2 入栈 |
1-(2+3/4)*5= | - ( + | 1 2 | + 入栈 |
1-(2+3/4)*5= | - ( + | 1 2 3 | 3 入栈 |
1-(2+3/4)*5= | - ( + / | 1 2 3 | / 入栈 |
1-(2+3/4)*5= | - ( + / | 1 2 3 4 | 4 入栈 |
1-(2+3/4)*5= | - | 1 2 3 4 / + | / + 移入s2,( 清除 |
1-(2+3/4)*5= | - * | 1 2 3 4 / + | * 入栈 |
1-(2+3/4)*5= | - * | 1 2 3 4 / + 5 | 5 入栈 |
1-(2+3/4)*5= | 1 2 3 4 / + 5 * - | * - 依次移入s2,结束 |
依次输出s2栈顶元素 -*5+/4321 ,是后缀表达式的逆序
完整代码如下:
#include <stdio.h> //使用后缀表达式(逆波兰式)求表达式的值,表达式只含+-*/()与整数 //样例 : 1-(2+3/4)*5= double s3[5005]; //栈s3,结果为浮点数 int size; // s3中的元素数 struct stack //栈 { int size; //栈中元素数量 int flag[5005]; // flag[i]=0表示s[i]是数字, flag[i]=1表示s[i]是符号 int s[5005]; //既可以存数字,也可以存运算符 } s1, s2; //栈s1,s2 typedef struct stack stack; int top(stack *a) //取出栈顶元素 { return a->s[a->size]; } int flg(stack *a) //判断栈顶元素的类型(字符或数字) { return a->flag[a->size]; } void push(stack *a, int flag, int x) //向栈顶压入一个元素 { a->size++; a->flag[a->size] = flag; //标记元素的类型(运算符或数字) a->s[a->size] = x; //存入元素 } void pop(stack *a) //删除栈顶元素 { a->size--; } int pr(char op) //求判断运算符优先级 { if (op == '*' || op == '/') return 1; else if (op == '+' || op == '-') return 0; else //此处视括号优先级最低 return -1; } void read() //读取中缀表达式 { char ch = getchar(); while (1) { if (ch == '=') //若为 = ,将s1中所有元素移入s2中,并结束函数 { while (s1.size > 0) { int tmp = top(&s1); pop(&s1); push(&s2, 1, tmp); } return; } else if (ch >= '0' && ch <= '9') //若为数字,求出整个数,并放入s2中 { int x = 0; while (ch >= '0' && ch <= '9') //思想与快读相同 { x = x * 10 + ch - '0'; ch = getchar(); } push(&s2, 0, x); continue; } else if (ch == '+' || ch == '-' || ch == '/' || ch == '*') //若为运算符 { while (s1.size > 0 && pr(top(&s1)) >= pr(ch)) //当ch的优先级<s1栈顶元素优先级时 { int tmp = top(&s1); //将s1栈顶元素移到s2并清除 pop(&s1); //清除该元素 push(&s2, 1, tmp); } push(&s1, 1, ch); //将运算符ch压入栈s1 } else if (ch == '(') //左括号直接压入s1 { push(&s1, 1, ch); } else if (ch == ')') //右括号 { while (top(&s1) != '(') //将左右括号之间的运算符全部移到s2 { int tmp = top(&s1); pop(&s1); push(&s2, 1, tmp); } pop(&s1); //清除s1中左括号 } ch = getchar(); } } void cal() { while (s1.size > 0) //后缀表达式求值 { if (flg(&s1) == 0) //如果栈顶为数字,直接压入栈s3中 { int tmp = top(&s1); s3[++size] = tmp; } else if (flg(&s1) == 1) //如果栈顶为运算符 { double a = s3[size--]; //取出s3的两个栈顶元素相运算 double b = s3[size--]; if (top(&s1) == '+') { s3[++size] = b + a; } else if (top(&s1) == '-') { s3[++size] = b - a; //后减前,并将结果压入栈s3 } else if (top(&s1) == '*') { s3[++size] = b * a; } else if (top(&s1) == '/') { s3[++size] = b / a; //后者除以前者,并将结果压入栈s3 } } pop(&s1); //将s1栈顶元素清除 } } int main() { read(); while (s2.size > 0) // s2中结果依次输出为一个逆序的后缀表达式 { int tmp = top(&s2); int flag = flg(&s2); pop(&s2); push(&s1, flag, tmp); } //需将其移入s1中使其反向为一个正序的后缀表达式 // while(s1.size>0)//调试,输出后缀表达式 // { // if(s1.flag[s1.size]) // printf("%c ",top(&s1)); // else printf("%d ",top(&s1)); // pop(&s1); // } cal(); printf("%.2lf\n", s3[size]); //结果为s3中剩下的一个元素 return 0; }
本文来自博客园,作者:STEllIAF0X,转载请注明原文链接:https://www.cnblogs.com/STEllIAF0X/p/16166578.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!