C++篇(13)计算器实现
1、计算器实现思路
我们日常写的计算表达式都是中缀表达式,也就是运算符在中间,运算数在两边,但是直接读取无法马上进行计算,因为一个计算表达式还涉及运算符优先级的问题。比如:1-2*(3-4)+5 中遇到-和*都无法运算,因为后面还有括号,优先级更高。
所以其中一种实现思路就是把中缀表达式转换为后缀表达式,也就是分析计算表达式的优先级,将运算数放在前面,运算符放到运算数后面,然后我们依次读取后缀表达式,遇到运算符就可以进行计算了。后缀表达式也被称为逆波兰表达式(Reverse Polish Notation,RPN)
2、后缀表达式进行运算
https://leetcode.cn/problems/evaluate-reverse-polish-notation/submissions/671060134
后缀表达式因为已经确定好了优先级,运算方式非常简单,就是遇到运算符时,取前面两个运算数进行运算,因为经过中缀转后缀,优先级已经确立好了。
建立一个栈存储运算数,读取后缀表达式,遇到运算数入栈;遇到运算符,出栈顶的两个数据进行运算,运算后将结果作为一个运算数入栈,继续参与下一次运算。读取表达式结束之后,最后栈里面的值就是运算结果。
class Solution
{
public:int evalRPN(vector<string>& tokens){stack<int> st;for(auto& str : tokens){//str为数字if((str != "+") && (str != "-") && (str != "*") && (str != "/")){st.push(stoi(str));}else{//str为操作符int right = st.top();st.pop();int left = st.top();st.pop();switch(str[0]){case '+' : st.push(left + right);break;case '-' :st.push(left - right);break;case '*' :st.push(left * right);break;case '/' :st.push(left / right);break;}}}return st.top();}
};
3、中缀表达式转后缀表达式
依次读取计算表达式中的值,遇到运算数直接输出。
建立一个栈存储运算符,利用栈后进先出的性质,遇到后面的运算符,出栈里面存的前面运算符进行比较,确定优先级。
遇到运算符,如果栈为空 或者 栈不为空并且当前运算符比栈顶运算符优先级高,则当前运算符入栈。因为如果栈里面存储的是前一个运算符,当前运算符比前一个运算符优先级高,说明前一个不能运算,当前运算符也不能运算,因为后面可能还有更高优先级的运算符。
遇到运算符,如果栈不为空并且当前运算符比栈顶运算符优先级低,说明栈顶的运算符可以运算了,则输出栈顶运算符,当前运算符继续走前面遇到运算符的逻辑。
如果遇到(),则把括号的计算表达式当成一个子表达式,跟上面思路类似,进行递归处理子表达式,处理后转换出的后缀表达式加在前面表达式的后面即可。
计算表达式或者()中子表达式结束值,输出栈中所有运算符。




class Solution
{
public:map<char, int> operatorPrecedence = {{'+',1}, {'-',1}, {'*',2}, {'/',2}};//中缀转后缀void toRPN(const string& s, size_t& i, vector<string>& v){stack<char> st;while(i < s.size()){if(isdigit(s[i])){// 操作数直接输出string num;while(i < s.size() && isdigit(s[i])){num += s[i];++i;}v.push_back(num);}else if(s[i] == '('){// 子表达式,递归处理即可++i;toRPN(s, i, v);}else if(s[i] == ')'){// 子表达式结束// 输出栈里面的剩余运算符while(!st.empty()){v.push_back(string(1, st.top()));st.pop();}++i;return;}else {// 运算符if(st.empty() || (operatorPrecedence[st.top()] < operatorPrecedence[s[i]])){st.push(s[i]);++i;}else{char op = st.top();st.pop();v.push_back(string(1, op));}}}// 表达式结束// 输出栈里面的剩余运算符while(!st.empty()){v.push_back(string(1, st.top()));st.pop();}}
};
4、计算器的实现
https://leetcode.cn/problems/basic-calculator
有了上面两个部分,计算器OJ的大部分问题就解决了,但是这里还有一些问题需要处理。因为OJ中给的中缀表达式是字符串,且字符串中包含空格,需要去掉空格。
int calculate(string s)
{//去掉空格string news;for(auto& ch : s){if(ch != ' '){news += ch;}}size_t i = 0;vector<string> v;toRPN(news, i, v);return evalRPN(v);
}
那是不是这样就行了呢?提交代码之后,提示我们有测试样例未通过。
可以发现,这里还需要区分负数和减号。如果是减号的话,那么前面一个是操作数;如果是负号的话,前面一个是操作符。我们需要将所有的负数-x 转换为 0-x。
int calculate(string s)
{//去掉空格string news;for(auto& ch : s){if(ch != ' '){news += ch;}}//处理负数news.swap(s);news.clear();for(size_t i = 0;i < s.size();i++){if(s[i] == '-' && !isdigit(s[i - 1])){news += "0-";}else{news += s[i];}}size_t i = 0;vector<string> v;toRPN(news, i, v);return evalRPN(v);
}
提交之后发现仍然有测试样例不通过。
说明我们判断条件是不够的,还需要判断减号前面是不是右括号。
int calculate(string s)
{//去掉空格string news;for(auto& ch : s){if(ch != ' '){news += ch;}}//处理负数news.swap(s);news.clear();for(size_t i = 0;i < s.size();i++){if(s[i] == '-' && (!isdigit(s[i - 1]) && s[i - 1] != ')')){news += "0-";}else{news += s[i];}}size_t i = 0;vector<string> v;toRPN(news, i, v);return evalRPN(v);
}
再提交一下。。。
又有测试样例崩溃了,因为这里是s[0]的位置出现-号,因此s[i-1]就会越界。所以这个地方还要加一个条件。
完整题解代码如下:
class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> st;for(int i = 0;i < tokens.size();i++){string& str = tokens[i];if((str != "+") && (str != "-") && (str != "*") && (str != "/")){st.push(stoi(str));}else{int right = st.top();st.pop();int left = st.top();st.pop();switch(str[0]){case '+':st.push(left + right);break;case '-':st.push(left - right);break;case '*':st.push(left * right);break;case '/':st.push(left / right);break;}}}return st.top();}map<char, int> operatorPrecedence = { {'+',1},{'-',1},{'*',2},{'/',2} };//中缀转后缀void toRPN(const string& s, size_t& i, vector<string>& v){stack<char> st;while (i < s.size()){//操作数直接输出if (isdigit(s[i])){string num;while (i < s.size() && isdigit(s[i])){num += s[i];i++;}v.push_back(num);}else if (s[i] == '('){//子表达式,递归处理即可i++;toRPN(s, i, v);}else if (s[i] == ')'){//子表达式结束//输出栈里面的剩余运算符while (!st.empty()){v.push_back(string(1, st.top()));st.pop();}i++;return;}else{//运算符if (st.empty() || operatorPrecedence[s[i]] > operatorPrecedence[st.top()]){st.push(s[i]);i++;}else{char op = st.top();st.pop();v.push_back(string(1, op));}}}//表达式结束//输出栈里面的剩余运算符while (!st.empty()){v.push_back(string(1, st.top()));st.pop();}}int calculate(string s) {//去掉空格string news;for(auto& ch : s){if(ch != ' '){news += ch;}}//处理负数news.swap(s);news.clear();for(size_t i = 0;i < s.size();i++){if(s[i] == '-' && (i == 0 || (!isdigit(s[i - 1]) && s[i - 1] != ')'))){news += "0-";}else{news += s[i];}}size_t i = 0;vector<string> v;toRPN(news, i, v);return evalRPN(v);}
};
5、其他实现思路
https://leetcode.cn/problems/basic-calculator/solutions/646369/ji-ben-ji-suan-qi-by-leetcode-solution-jvir
其实一句话就可以解析:就是去括号的思路,比如1-(1+2)=1-1-2,有乘法和除法类似,比考虑中缀转后缀的思路简单。
class Solution {
public:int calculate(string s) {stack<int> ops;ops.push(1);int sign = 1;int ret = 0;int n = s.length();int i = 0;while (i < n) {if (s[i] == ' ') {i++;} else if (s[i] == '+') {sign = ops.top();i++;} else if (s[i] == '-') {sign = -ops.top();i++;} else if (s[i] == '(') {ops.push(sign);i++;} else if (s[i] == ')') {ops.pop();i++;} else {long num = 0;while (i < n && s[i] >= '0' && s[i] <= '9') {num = num * 10 + s[i] - '0';i++;}ret += sign * num;}}return ret;}
};
https://leetcode.cn/problems/basic-calculator-ii
https://leetcode.cn/problems/basic-calculator-ii/solutions/648647/ji-ben-ji-suan-qi-ii-by-leetcode-solutio-cm28
class Solution {
public:int calculate(string s) {vector<int> stk;char preSign = '+';int num = 0;int n = s.length();for (int i = 0; i < n; ++i) {if (isdigit(s[i])) {num = num * 10 + int(s[i] - '0');}if (!isdigit(s[i]) && s[i] != ' ' || i == n - 1) {switch (preSign) {case '+':stk.push_back(num);break;case '-':stk.push_back(-num);break;case '*':stk.back() *= num;break;default:stk.back() /= num;}preSign = s[i];num = 0;}}return accumulate(stk.begin(), stk.end(), 0);}
};