表达式求值,通常的方法是人为创建两个栈来解决问题,但其实也是可以用递归的方法来解决的。使用递归和双栈(两个栈)的原理和本质都是一样的。递归其实是使用了系统栈,和人为写栈来解决问题是同理的,故两种方法本质是没有区别的。
下面打算先用递归的方式解决表达式求值的问题。
先理解个概念,表达式树:以运算符作为根节点,以相关的操作数作为子节点。
3*(4+5)是一个乘法表达式,4+5这个加法只是在解决乘法问题前的一个子问题。
那么这里其实就是乘法问题里面包含了一个加法的子问题。
根节点代表了3*(4+5)是一个乘法表达式。两棵子树代表是3乘(4+5)。其中右子树4+5又是一颗树。这就是问题的嵌套,适合使用递归或者栈来解决此类问题。
如何判断出来这是一个乘法表达式?
因为3*(4+5)这整个式子,乘号是最后一个被计算的运算符。
最后被计算的运算符,又是整个表达式中优先级最低的那个运算符。
于是我们能得到递归解决表达式求值的方法论:
step1:把整个表达式看成一个函数calculate(),找到其中优先级最低的那个运算符。
step2:以找到的那个优先级最低的运算符为界,将整个表达式一分为二。分成了3和(4+5)
step3:在分别把这两部分(3和4+5)看成一个函数calculate(),然后分别递归求解。
Q:如何找到表达式中优先级最低的那个运算符?
A:设定运算符+和-的优先级是1,*和/的优先级是2。在括号里面的运算符,都额外加100。
以表达式3*(4+5)为例,*优先级是2,+优先级是101。
3*(4*(5+6)),第一个*优先级2,第二个*优先级102,+优先级201。
代码实现:
int calculate (char*s, int l, int r) {
//参数一是待计算的表达式(字符串),参数二和三是字符串的坐标,代表待计算的范围。
//step1:找到优先级最低的那个运算符。
int op = -1; //op是最低优先级的运算符的坐标,先初始化为-1。
int pri = 10000-1; //目前最小的优先级是多少。
int cur_pri; //当前运算符的优先级。
int temp = 0; //额外由括号增加的优先级。(一层括号加100,两层括号加200)
for(i = l; i <= r; i++) { //for循环遍历表达式,找出最低优先级的运算符。
cur_pri = 10000; //假设最开始,当前运算符的优先级是最大。
switch (s[i]) {
case '+':
case '-': cur_pri = 1 + temp; break;
case '*':
case '/': cur_pri = 2 + temp; break;
case '(': temp += 100; break;
case ')': temp -= 100; break;
}
if(cur_pri <= pri) { //当前运算符的优先级和最小的优先级做比较。
pri = cur_pri;
op = i;
}
}
if(op == -1) { //递归终止条件
//找不到最低优先级的运算符,证明当前表达式里面没有运算符,只有数字和括号。
int num = 0;
for(int i = l; i <= r; i++) { //将只包含数字和括号的表达式(字符串)转换成纯数字(int整数)。
if(s[i] < '0' || s[i] > '9') continue; //筛掉表达式里的括号(字符),只保留数字(字符)。
else num = num*10 + s[i]-'0'; //将数字字符转换成真正的数字,并将其加入到num里。
}
return num;
}
printf("calculate find: %c\n", s[op]); //输出,看一下这一轮找到的优先级最低的运算符是啥。
//step2:递归运算
int a = calculate(s, l, op-1);
//以op为分界点,将表达式分成两半,来进行递归运算。(不包括op本身)
int b = calculate(s, op+1, r);
//这里就已经得到了运算符op的左表达式的值a和右表达式的值b。
switch (s[op]) { //根据op具体是什么运算符,返回对a和b计算得到的表达式的值。
case '+': return a+b;
case '-': return a-b;
case '*': return a*b;
case '/': return a/b;
}
return 0;
}
int main() {
char s[100]; //字符串存储表达式
while (~scanf("%[^\n]s", s)) { //读入一个表达式(字符串)
gethchar();
printf("%s = %d\n", s, calculate(s, 0, strlen() - 1));
}
return 0;
}
递归代码解析:
33 //step2:递归运算
34 int a = calculate(s, l, op-1);
36 int b = calculate(s, op+1, r);最低优先级的运算符op将表达式一分为二,然后将左子式(L~op-1)和右子式(op+1~R)都看成一整个表达式,再分别递归求值(参照下面图解)。
递归代码图解:把递归想成表达式树即可。
![]()
以表达式3*(4+(5-2))为例,其递归过程如下面图解。
表达式会一直递归,直到分解出来的子表达式里面没有运算符。(递归终止条件)
第一轮递归,把表达式3*(4+(5-2))拆分成了运算符*,左子式3,右子式4+(5-2)。
第二轮递归,左子式3不需要再拆分,把右子式拆分成4和5-2。
第三轮递归,左子式4不需要拆分,右子式5-2拆分成5和2。
表达式已经没有可以再拆分的了,满足了递归终止条件,递归完毕。
特殊情况:若表达式里面有负数(如:5+-3)该怎么计算?
思路:在表达式里面的负数(-3)前面加上一个0。表达式5+-3变成5+0-3,再正常计算即可。
上面用递归的方式实现了表达式求值,下面要用栈的方式来实现此功能。