中缀表达式的计算(一个很长的递归函数)

        输入一个数学表达式(假定表达式输入格式合法),计算表达式结果并输出。数学表达式由单个数字和运算符“+”、“-”、“*”、“/”、“(、) ”构成,例如 2 + 3 * ( 4 + 5 ) - 6 / 4。变量、输出采用整数,只舍不入。

        栈的某一应用就是计算逆波兰表达式(后缀表达式)。因此需要先将中缀表达式转为后缀进行计算。但是否有方法直接计算中缀表达式?这里给出一种途径。

        思路:当一个表达式中没有括号和乘除号,仅还有加减号时,这个式子是很容易计算的,我们不需要考虑优先级的问题。仅仅需要将式子从左往右按照给出的加号减号运算一遍即可。因此首先需要做的,就是把一个含有括号和乘除号的中缀表达式转换为仅含有加减号的中缀表达式

        回想平时我们自己计算一个中缀表达式的过程。例如计算1+2*(3+4)时,我们会如何计算?我们会在看到1+x后立即计算出1+x的值吗?显然不会,我们会继续向后看,看这个x是否会和其他的子表达式(或可能就是一个数)进行优先级更高运算,如果有这样的表达式,我们会优先计算后者。在这个例子中*的优先级显然比+要高。因此,在编码时,我们在拿到例如+、-这样的符号时,无法立即将后面的操作数立即和前面的操作数进行运算。同理,拿到×或÷这样的符号时,需要考虑到括号的存在。在这个例子中,我们一路往下看,看到括号,优先计算我们第一个看到的括号,在这个括号中的表达式,仍然遵循以上的规则。幸运的是,括号内的表达式很容易计算。但我们很可能遇到括号的平行结构或嵌套结构这样更复杂的结构。但运算规则始终是不变的。我们将这个公共的逻辑抽取出来,就得到了递归。即,计算一个表达式中,若在其中有括号的子表达式,我们优先计算这个括号,直到子表达式没有括号,只有+,-,×,÷符号为止。随后回溯结果,一直到最外层的括号。

        上一步我们解决了括号的问题。现在我们需要解决×和÷的优先级问题。上面已经提到了,在没有括号的式子中,乘除的优先级是最高的。因此,我们遇到乘号或除号时,可以直接取其后的操作数(可能是个括号子表达式,递归计算其值),与前面的操作数进行运算。然后保留得到的操作数。

        通过上面两步,括号通过递归计算解决,乘除优先运算,这样整个表达式中仅剩下加减操作数和操作数。这样的式子的计算无疑难度降低了很多。我们可以通过栈这个数据结构来解决。每次取两个操作数和一个操作符直至操作符栈为空(这时没有需要运算的东西了),运算后压栈,把最后的结果从操作数栈中取出,即为最后结果。

        下面是整个思路的图解。以((3+1)*2-(1-2))+1/8为例。

        

        图解:将操作数和操作符分别置于两个栈中。每次判断当前遇到的是操作符还是操作数。是操作数直接存入操作数栈中。如果是操作符,分情况讨论。第一种是+,-。优先级很低,不知道计算次序,先入操作符栈等待最后计算。如果是后续是左括号,找到其匹配的右括号,进入下一层递归,计算这个括号内的子表达式的值,直到子表达式没有括号位置,带着结果返回上一层的递归。如果是×或者÷,优先级在没有括号时最高。恰巧,我们的括号都在下一层递归算出来了,原本的括号表达式变成了一个操作数,那就直接拿出来这个操作数和操作符之前的一个操作数,进行运算。结果压栈。最终 ,计算剩余的仅有加减符号的表达式。

        随后是代码。用的C++。

        

//传入表达式,直接整个传入,然后规定括号范围
int	calculate(string str,int start,int end) {
	//为表达式创建操作数栈和操作符栈
	stack<int> number;
	stack<char> operation;
	//遍历传入的字符串
	for (int i = start; i <= end; i++) {
		char c = str.at(i);
		if (c >= 48) {
			//如果是操作数,直接入栈
			number.push(c - 48);
		}

        递归方法中,我们要确定计算的子表达式的范围(即括号的范围),随后在该层递归中为子表达式的操作数和操作符分别建立一个栈。遍历子表达式,如果是操作数,那可以直接存入栈中。

        

//操作符,每一个操作符都要判断后续的符号是不是左括号
		else {
			//没有考虑到上来就是括号
			if (((i+1<=end)&&str.at(i + 1) == '(')) {
				//如果是左括号 找到和它匹配的右括号,递归计算这个括号的值 然后把i跳到右括号上去
				//此外,如果是有嵌套结构的括号,我们应该为较大的括号创建操作数和操作符栈,否则无法计算括号内式子的值
				int j = i + 1;
				stack<char> bracket;
				for (; j <= end; j++) {
					//括号匹配算法 为左括号直接入栈
					if (str.at(j) == '(') {
						bracket.push(str.at(j));
					}
					//由于一定合法,不用验证栈是否为空
					if (str.at(j) == ')') {
						bracket.pop();
						if (bracket.empty()) {
							//如果栈已经空了,那么说明找到了匹配的右括号,索引即为j
							break;
						}
					}
				}
				int newNumber = calculate(str, i + 1, j);

        对于+,-,×,÷而言,后面都可能接一个左括号。那么我们每碰到一个操作符,都要判断后续是不是括号,如果是括号,先计算完括号里的子表达式。返回结果。

        这里进下一层递归需要直到子表达式范围,即左右括号位置。用到了一个括号匹配算法,也是基于栈解决的。我们遍历字符串,如果是左括号则入栈,右括号则弹栈。由于输入表达式默认合法,不用担心多出来的右括号。当首次栈为空时,代表着匹配左括号的右括号已经找到了。跳出遍历。

        

int newNumber = calculate(str, i + 1, j);
				//解决开头嵌套括号问题
				if (str.at(i) == '(') { number.push(newNumber); }
				//如果是加或者减
				if (str.at(i) == 43 || str.at(i) == 45) {
					//不着急,直接把计算括号新得到的数推进去就行
					number.push(newNumber);
					//还要把符号推进去
					operation.push(str.at(i));
				}
				//如果是乘或除
				else if(str.at(i)=='*'||str.at(i)=='/') {
					//立即推出一个操作数进行运算
					int number1 = number.top();
					number.pop();
					int newNumber2;
					if (str.at(i) == 42) {
						newNumber2 = number1 * newNumber;
					}
					else {
						newNumber2 = number1 / newNumber;
					}
					number.push(newNumber2);
				}
				//这一步是跳过括号,因此可以不用担心会出现else里拿到括号的情况
				i = j;
			}

        计算完括号后,从start位置到当前的子表达式中,只有加减乘除四个符号了。我们按照上述规则计算。如果是乘除,因为没有括号了,可以直接计算;如果是加减,只能把得到的子表达式结果和当前位置上的操作符分别推入对应的栈中,留待后续运算。

        随后很关键的一步,我们已经计算完后续的括号了,无需在遍历字符串时再算一遍了,因此我们要移动指针的位置,跳过这个括号。j是匹配当前左括号的右括号索引,将指针移动到这个位置,循环结束模拟指针自增,会自动跳到这个右括号的下一个位置上去。        

else {
				//如果后面一个不是左括号,说明是一个操作数
				if (str.at(i) == 43 || str.at(i) == 45) {
					//老样子,加减不急,先入栈等乘除括号
					operation.push(str.at(i));
				}
				//乘除的话,即便后面也括号,也先算的乘除,不用担心优先级没括号高
				else if(str.at(i)=='*'||str.at(i)=='/') {
					//拿到后面的操作数
					int number2 = str.at(i + 1) - 48;
					int newNumber;
					int number1 = number.top();
					number.pop();
					if (str.at(i) == '*') {
						newNumber = number1 * number2;
					}
					else if(str.at(i)=='/') {
						newNumber = number1 / number2;
					}
					//乘除计算得到的数入栈
					number.push(newNumber);
					//因为已经计算了下一个数字,应该将指针后移
					i = i + 1;
				}
			}

               

         如果后续没有括号了,我们就可以按照仅有加减乘除符号的规则计算。拿到加减,入栈等待。拿到乘除,立即计算,结果压栈。注意乘除时我们取出了下一个操作数,运算完后指针后移,跳过这个运算过的数。

//没考虑一上来就括号
			else if (str.at(0)=='('&&i==0) {
				//如果是左括号 找到和它匹配的右括号,递归计算这个括号的值 然后把i跳到右括号上去
				//此外,如果是有嵌套结构的括号,我们应该为较大的括号创建操作数和操作符栈,否则无法计算括号内式子的值
				int j = i;
				stack<char> bracket;
				for (; j <= end; j++) {
					//括号匹配算法 为左括号直接入栈
					if (str.at(j) == '(') {
						bracket.push(str.at(j));
					}
					//由于一定合法,不用验证栈是否为空
					if (str.at(j) == ')') {
						bracket.pop();
						if (bracket.empty()) {
							//如果栈已经空了,那么说明找到了匹配的右括号,索引即为j
							break;
						}
					}
				}
				int newNumber = calculate(str, i+1, j);
				//上来就是括号,说明后续可能的乘或除时没有前面的元素了,要先存进去。
				number.push(newNumber);
				//如果是加或者减
				if (str.at(i) == 43 || str.at(i) == 45) {
					//不着急,直接把计算括号新得到的数推进去就行
					//还要把符号推进去
					operation.push(str.at(i));
				}
				//如果是乘或除
				else if(str.at(i)=='*'||str.at(i)=='/') {
					//立即推出一个操作数进行运算
					int number1 = number.top();
					number.pop();
					int newNumber2;
					if (str.at(i) == 42) {
						newNumber2 = number1 * newNumber;
					}
					else if(str.at(i)=='/') {
						newNumber2 = number1 / newNumber;
					}
					number.push(newNumber2);
				}
				//这一步是跳过括号,因此可以不用担心会出现else里拿到括号的情况
				i = j;
			}

        随后是这个思路的一个硬伤,我没有考虑到整个表达式一上来就是括号怎么办。就想图示中的那样。后来只能做了个特判,判断是否表达式头部就是左括号,如果是,仿照上述后续符号是左括号的例子,计算这个括号的内容。由于特殊性,这里一些参数都改了。比方说递归方法不能再从左括号位置开始,否则会因为始终头部是左括号而进入死循环,需要从后一个元素开始递归。而且,如果括号在头部,我们在后续运算时前一个元素就是空的,无论后续是否是乘除我们都应该将这个数推入操作数栈中,否则取数时栈内无元素。

//由于我的严重失误,我们需要把两个栈的数据倒过来,因为计算是从前往后算的
	stack<int> reverseNumber;
	stack<char> reverseOperation;
	while (!number.empty()) {
		reverseNumber.push(number.top());
		number.pop();
	}
	while (!operation.empty()) {
		reverseOperation.push(operation.top());
		operation.pop();
	}

	//计算栈中剩下的所有内容的结果,此时操作数仅有+,-
	while (!reverseOperation.empty()) {
		int number1 = reverseNumber.top();
		reverseNumber.pop();
		int number2 = reverseNumber.top();
		reverseNumber.pop();
		char op = reverseOperation.top();
		reverseOperation.pop();
		if (op == 43) {
			reverseNumber.push(number1 + number2);
		}
		else {
			reverseNumber.push(number1-number2);
		}
	}
	//获取结果
	int result = reverseNumber.top();
	reverseNumber.pop();
	return result;
			
}

                在子表达式仅剩加减和操作数时,我们就可以按上述流程进行计算了。拿出两个操作数和一个操作符,运算后结果压栈,直到操作符栈中为空即可。注意,表达式计算顺序是从左往右。操作数符栈中顺序反的,要倒过来算。解决方法不止上图中的那个。比如一次取一个操作数和操作符,因为仅有加减,定义一个结果变量,对这个结果变量进行运算即可。

图示结果测试如下

完整代码如下

#include<stack>
#include<string>
#include<iostream>
using namespace std;


//传入表达式,直接整个传入,然后规定括号范围
int	calculate(string str,int start,int end) {
	//为表达式创建操作数栈和操作符栈
	stack<int> number;
	stack<char> operation;
	//遍历传入的字符串
	for (int i = start; i <= end; i++) {
		char c = str.at(i);
		if (c >= 48) {
			//如果是操作数,直接入栈
			number.push(c - 48);
		}
		//操作符,每一个操作符都要判断后续的符号是不是左括号
		else {
			//没有考虑到上来就是括号
			if (((i+1<=end)&&str.at(i + 1) == '(')) {
				//如果是左括号 找到和它匹配的右括号,递归计算这个括号的值 然后把i跳到右括号上去
				//此外,如果是有嵌套结构的括号,我们应该为较大的括号创建操作数和操作符栈,否则无法计算括号内式子的值
				int j = i + 1;
				stack<char> bracket;
				for (; j <= end; j++) {
					//括号匹配算法 为左括号直接入栈
					if (str.at(j) == '(') {
						bracket.push(str.at(j));
					}
					//由于一定合法,不用验证栈是否为空
					if (str.at(j) == ')') {
						bracket.pop();
						if (bracket.empty()) {
							//如果栈已经空了,那么说明找到了匹配的右括号,索引即为j
							break;
						}
					}
				}
				int newNumber = calculate(str, i + 1, j);
				if (str.at(i) == '(') { number.push(newNumber); }
				//如果是加或者减
				if (str.at(i) == 43 || str.at(i) == 45) {
					//不着急,直接把计算括号新得到的数推进去就行
					number.push(newNumber);
					//还要把符号推进去
					operation.push(str.at(i));
				}
				//如果是乘或除
				else if(str.at(i)=='*'||str.at(i)=='/') {
					//立即推出一个操作数进行运算
					int number1 = number.top();
					number.pop();
					int newNumber2;
					if (str.at(i) == 42) {
						newNumber2 = number1 * newNumber;
					}
					else {
						newNumber2 = number1 / newNumber;
					}
					number.push(newNumber2);
				}
				//这一步是跳过括号,因此可以不用担心会出现else里拿到括号的情况
				i = j;
			}
			//没考虑一上来就括号
			else if (str.at(0)=='('&&i==0) {
				//如果是左括号 找到和它匹配的右括号,递归计算这个括号的值 然后把i跳到右括号上去
				//此外,如果是有嵌套结构的括号,我们应该为较大的括号创建操作数和操作符栈,否则无法计算括号内式子的值
				int j = i;
				stack<char> bracket;
				for (; j <= end; j++) {
					//括号匹配算法 为左括号直接入栈
					if (str.at(j) == '(') {
						bracket.push(str.at(j));
					}
					//由于一定合法,不用验证栈是否为空
					if (str.at(j) == ')') {
						bracket.pop();
						if (bracket.empty()) {
							//如果栈已经空了,那么说明找到了匹配的右括号,索引即为j
							break;
						}
					}
				}
				int newNumber = calculate(str, i+1, j);
				//上来就是括号,说明后续可能的乘或除时没有前面的元素了,要先存进去。
				number.push(newNumber);
				//如果是加或者减
				if (str.at(i) == 43 || str.at(i) == 45) {
					//不着急,直接把计算括号新得到的数推进去就行
					//还要把符号推进去
					operation.push(str.at(i));
				}
				//如果是乘或除
				else if(str.at(i)=='*'||str.at(i)=='/') {
					//立即推出一个操作数进行运算
					int number1 = number.top();
					number.pop();
					int newNumber2;
					if (str.at(i) == 42) {
						newNumber2 = number1 * newNumber;
					}
					else if(str.at(i)=='/') {
						newNumber2 = number1 / newNumber;
					}
					number.push(newNumber2);
				}
				//这一步是跳过括号,因此可以不用担心会出现else里拿到括号的情况
				i = j;
			}
			else {
				//如果后面一个不是左括号,说明是一个操作数
				if (str.at(i) == 43 || str.at(i) == 45) {
					//老样子,加减不急,先入栈等乘除括号
					operation.push(str.at(i));
				}
				//乘除的话,即便后面也括号,也先算的乘除,不用担心优先级没括号高
				else if(str.at(i)=='*'||str.at(i)=='/') {
					//拿到后面的操作数
					int number2 = str.at(i + 1) - 48;
					int newNumber;
					int number1 = number.top();
					number.pop();
					if (str.at(i) == '*') {
						newNumber = number1 * number2;
					}
					else if(str.at(i)=='/') {
						newNumber = number1 / number2;
					}
					//乘除计算得到的数入栈
					number.push(newNumber);
					//因为已经计算了下一个数字,应该将指针后移
					i = i + 1;
				}
			}
		}
	}

	//由于我的严重失误,我们需要把两个栈的数据倒过来,因为计算是从前往后算的
	stack<int> reverseNumber;
	stack<char> reverseOperation;
	while (!number.empty()) {
		reverseNumber.push(number.top());
		number.pop();
	}
	while (!operation.empty()) {
		reverseOperation.push(operation.top());
		operation.pop();
	}

	//计算栈中剩下的所有内容的结果,此时操作数仅有+,-
	while (!reverseOperation.empty()) {
		int number1 = reverseNumber.top();
		reverseNumber.pop();
		int number2 = reverseNumber.top();
		reverseNumber.pop();
		char op = reverseOperation.top();
		reverseOperation.pop();
		if (op == 43) {
			reverseNumber.push(number1 + number2);
		}
		else {
			reverseNumber.push(number1-number2);
		}
	}
	//获取结果
	int result = reverseNumber.top();
	reverseNumber.pop();
	return result;
			
}

int main() {
	cout << "Input" << endl;
	string str;
	cin >> str;
	int result = calculate(str, 0, str.length() - 1);
	cout << "Output" << endl;
	cout << result << endl;
	cout << "End";
	return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值