文章目录
- [1. 计算器通解](#1. 计算器通解)
-
- [1.1 逆波兰表达式](#1.1 逆波兰表达式)
- [1.2 逆波兰表达式计算](#1.2 逆波兰表达式计算)
- [1.3 合并双栈写法](#1.3 合并双栈写法)
- [2. LeetCode 题目优化](#2. LeetCode 题目优化)
-
- [2.1 基本计算器](#2.1 基本计算器)
- [2.2 基本计算器 II](#2.2 基本计算器 II)
1. 计算器通解
1.1 逆波兰表达式
计算器实现,关键在于处理各类运算符之间的优先级关系 ,而传统的数学表达式,是中缀表达式,并不能很好表达这种优先级关系。
逆波兰表达式 ,即后缀表达式,该表达式本身已经将运算符优先关系处理好,运算是非常简单的。
因此,计算器通解 ,或者说解决最复杂的计算器问题,本质都是两步:逆波兰表达式转换 + 计算。
逆波兰表达式核心处理逻辑:
- 用一个栈存储运算符。
- 遇到运算符时,如果栈为空,或者当前运算符优先级大于等于栈顶运算符,那么入栈。
- 遇到运算符时,如果当前运算符优先级低于栈顶运算符,那么需要出栈顶运算符加入最终的逆波兰表达式中,然后重复该逻辑,直至栈为空,或当前运算符优先级大于等于栈顶运算符,即情况2。
- 遇到数字,将数字直接加入逆波兰表达式中。
- 整个循环结束,将栈中剩余的运算符依次弹出,加入最终表达式中,
- 括号情况。括号内的表达式本质是相对独立的,同样需要独立考虑计算。本质就是以左括号为开始,右括号为结束,递归上述过程。
非常重要的一点,上述逆波兰表达式处理,必须针对合法的表达式,因此需要预处理原中缀表达式,去掉空格,同时保证一个运算符,一定严格有两个运算数与之匹配,即+X或-X情况,必须处理为0+X或0-X情况。
以下,以加减乘除为例的逆波兰表达式转换代码。
cpp
int Hash(char ch) {
switch(ch) {
case '+':
return 1;
case '-':
return 1;
case '*':
return 2;
case '/':
return 2;
}
return -1;
}
void myToRPN(const string& s, size_t& i, vector<string>& v) {
stack<char> st;
int sz = s.size();
while (i < sz) {
if (isdigit(s[i])) {
string num;
while (isdigit(s[i])) {
num += s[i++];
}
v.push_back(num);
}
else if (s[i] == '(') {
i++;
myToRPN(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() ||
Hash(s[i]) > Hash(st.top())) {
st.push(s[i++]);
} else {
char ch = st.top();
st.pop();
v.push_back(string(1, ch));
}
}
}
while (!st.empty()) {
char ch = st.top();
st.pop();
v.push_back(string(1, ch));
}
return;
}
1.2 逆波兰表达式计算
得到逆波兰表达式后,计算逻辑相对而言是简单的。
具体规则:
- 如果是数字,那么放入栈中。
- 如果运算符,那么出栈中两个元素,进行运算。需注意,先出栈的数字,为运算符右侧元素,后出栈的数字,为运算符左侧元素。 运算后的结果,重新加入栈中。
- 遍历完整个逆波兰表达式后,若为合法逆波兰表达式,那么此时栈中仅会剩下一个元素,即为最终结果,返回栈顶元素即可。
以下同样是加减乘除版本的计算代码示例:
cpp
int evalRPN(vector<string>& tokens) {
stack<int> st;
int sz = tokens.size();
for (int i = 0; i < sz; i++) {
const string& tmp = tokens[i];
if (tmp == "+" || tmp == "-" || tmp == "*" || tmp == "/") {
int val2 = st.top();
st.pop();
int val1 = st.top();
st.pop();
switch (tmp[0]) {
case '+':
st.push(val1 + val2);
break;
case '-':
st.push(val1 - val2);
break;
case '*':
st.push(val1 * val2);
break;
case '/':
st.push(val1 / val2);
break;
}
} else {
st.push(stoi(tmp));
}
}
return st.top();
}
1.3 合并双栈写法
在上述逆波兰表达式转换 + 计算 两个过程中,各自用到了一个栈结构 ,因此我们可以使用双栈结构,将两个过程合并为一个过程,即一边进行逆波兰表达式转换,一边进行计算,以此简化整体代码。
加减乘除版本的示例代码如下:
cpp
#include <cctype>
class Solution {
public:
int Hash(char ch) {
if(ch == '+')
return 1;
else if(ch == '-')
return 1;
else if(ch == '*')
return 2;
return -1;
}
long long cal(stack<char>& op,stack<long long>& st) {
char ch = op.top();
op.pop();
long long n1 = st.top();
st.pop();
long long n2 = st.top();
st.pop();
switch(ch) {
case '+' :
return n1 + n2;
case '-':
return n2 - n1;
case '*':
return n1 * n2;
}
return -1;
}
int ToRPN(const string& str,stack<long long>& st,int& i) {
stack<char> op;
int sz = str.size();
while(i < sz) {
char ch = str[i];
if(isdigit(ch)) {
int j = i + 1;
long long num = ch - '0';
while(j < sz && isdigit(str[j]))
num = num * 10 + str[j++] - '0';
i = j;
st.push(num);
}
else if(ch == '+' || ch == '-' || ch == '*') {
if(op.empty() || Hash(op.top()) < Hash(ch)) {
op.push(ch);
i++;
}
else {
// 此处需要循环比较,直至进入上面的逻辑
st.push(cal(op,st));
}
}
else {
if(ch == '(')
ToRPN(str,st,++i);
else {
i++;
while(!op.empty())
st.push(cal(op,st));
return st.top();
}
}
}
while(!op.empty())
st.push(cal(op,st));
return st.top();
}
int solve(string s) {
string init;
int n = s.size();
for(int i = 0; i < n; i++) {
if(isspace(s[i]))
continue;
else if(s[i] == '-' && (i == 0 || s[i - 1] == '('))
init += "0-";
else
init += s[i];
}
int i = 0;
stack<long long> st; // 记录数字
int ret = ToRPN(init,st,i);
return ret;
}
};
2. LeetCode 题目优化
上述逆波兰表达式转换 + 计算的解法 ,本质是应对最复杂计算器情况的通解,代码也是最复杂的,但实际上,针对具体的计算器题目,由于具体要求不同,可能不需要使用上述做法,而使用更贴近题目本身的优化解法。
2.1 基本计算器
这道计算器题目特殊在:没有乘除运算符,只有加减运算和括号。
对于只包含加减运算的表达式,可以通过去括号,使得表达式最终仅仅为简单的加减运算。而去括号逻辑本质就是正负符号的叠加运算,这一点,我们可以借助栈实现。
具体思路:
- 维护一个栈和一个变量。栈用来记录一对括号之前的符号情况,变量用于记录前一个符号情况。
- 栈的维护。遍历到左括号,就将变量记录的符号与栈顶符号相乘,然后结果入栈;遍历到右括号,就将栈顶元素出栈。
- 变量维护。每遇到一个符号,动态更新符号情况即可。
- 栈和变量初始化。为了不对栈空和变量初始情况做额外判断,统一初始化为正,不影响结果,同时简化代码 。特别注意,维护栈时,遍历到左括号,变量与栈顶符号相乘,结果入栈后,需重新将变量初始化为1,避免括号内第一个数字为正数,却不带正号的特殊情况。
代码示例如下:
cpp
class Solution {
public:
int calculate(string s) {
stack<int> st;
st.push(1);
int pre = 1;
int i = 0, n = s.size();
long long ret = 0;
while(i < n) {
if(isspace(s[i]));
else if(s[i] == '+')
pre = 1;
else if(s[i] == '-')
pre = -1;
else if(isdigit(s[i])) {
int j = i + 1;
long long sum = s[i] - '0';
while(j < n && isdigit(s[j]))
sum = sum * 10 + s[j++] - '0';
ret += st.top() * pre * sum;
i = j;
continue;
}
else {
if(s[i] == '(') {
st.push(st.top() * pre);
pre = 1; // reset
}
else {
st.pop();
}
}
i++;
}
return ret;
}
};
2.2 基本计算器 II
这道计算器题目,特殊之处在于:有加减乘除四种运算符,但是没有括号,所以优先级的处理是明确的------一定先处理乘除,最后处理加减。
那么根据上述思路,可得如下算法规则:
- 维护三个变量,
ret记录结果,pre记录前一个符号,pre_num记录前一个数字。 pre维护。遇到符号就更新。pre_num和ret维护。遇到数字,判断pre符号。如果是加减符号,ret += pre_num,pre_num 动态更新成当前数字(如果是减号,那么更新成相反数);如果是乘除符号,那么pre_num *= 当前数或pre_num /= 当签数。- 细节问题。
ret和pre_num初始化为 0 ,pre初始化为正号情况。
具体代码如下:
cpp
class Solution {
public:
int calculate(string s) {
long long ret = 0;
long long pre_num = 0;
int i = 0, n = s.size();
char pre = '+';
while(i < n) {
if(isspace(s[i]))
i++;
else if(isdigit(s[i])) {
int j = i + 1;
long long sum = s[i] - '0';
while(j < n && isdigit(s[j]))
sum = sum * 10 + s[j++] - '0';
switch(pre) {
case '+':
ret += pre_num;
pre_num = sum;
break;
case '-':
ret += pre_num;
pre_num = -sum;
break;
case '*':
pre_num *= sum;
break;
case '/':
pre_num /= sum;
break;
}
i = j;
}
else
pre = s[i++];
}
ret += pre_num;
return ret;
}
};