227. 基本计算器 II
- 题目
- 算法设计:栈
- 扩展:后缀表达式
题目
传送门:https://leetcode.cn/problems/basic-calculator-ii/submissions/
算法设计:栈
一个功能完备的计算器功能,有很多功能,我们需要从最简单的功能迭代起来。
先宏观,再微分:
- 输入:字符串,需要把字符串转为数字
- 功能:加减、乘除优先、括号优优先
- 细节:读到空格跳过、不溢出整型最大值
计算的关键在于优先级处理,括号 > 乘除 > 加减。
第一步,字符串转整数。
int str_to_int( string s ){ // 字符串转整数
int num = 0;
for( int i=0; str.length(); i++ )
if( isdigit(str[i]) ) // 输入字符串为数字
num = 10 * num + (c - '0');
}
第二步,实现加减。
- 核心思路是把字符串分解成符号和数字的组合,如 2 - 3,变成 +2、-3。
int calculate(string s) {
stack<int> stk; // 记录算式中的数字
int num = 0; // 保存字符串转数字后的数字
char sign = '+'; // 记录每个数字之前的运算符,因为第一个数字没有符号的(3-2),设置为符号+(+3-2),如果第一个数是负数,那就是 +0
for (int i = 0; i < s.size(); i++) { // 遍历字符串
if (isdigit(s[i])) // 读到数字
num = 10 * num + (s[i] - '0'); // "123" -> 100 + 20 + 3 -> 123
if ( (!isdigit(c) && c != ' ') || i == s.size() - 1 ) { // s[i]是符号且不是空格(运算符) or 最后一个字符之后没有符号可读取了,所以也要入栈直接计算
switch (sign) { // 看数字前的 sign 来决定怎么处理 s[i] 前面的数
case '+':
stk.push(num); break; // 加法将之前的数字入栈
case '-':
stk.push(-num); break; // 加法将之前数字的相反数入栈
}
sign = s[i]; // 更新符号为当前符号
// 把 3-2 代入分析,初始sign为+,遍历到s[0]把'3'->3,再遍历s[1]到'-',后遍历到s[2]让+3入栈,更新sign为'-'。
num = 0; // 数字清零,计算下一个数字
}
}
// 将栈中所有结果求和就是答案
int res = 0;
while (!stk.empty()) {
res += stk.top();
stk.pop();
}
return res;
}
第三步,实现乘除。
- 核心思路依然是把字符串分解成符号和数字的组合,如 1*4,如 +1、*4。
乘除法优先于加减法体现在,乘除法可以和栈顶的数结合,而加减法只能把自己放入栈。
class Solution {
public:
int calculate(string s) {
stack<int> stk;
int num = 0;
char sign = '+';
for (int i = 0; i < s.size(); i++) {
if (isdigit(s[i]))
num = 10 * num + (s[i] - '0');
if ( (!isdigit(s[i]) && s[i] != ' ') || i == s.size() - 1 ) {
switch (sign) {
int pre;
case '+':
stk.push(num); break;
case '-':
stk.push(-num); break;
case '*':
pre = stk.top(); // 只要拿出前一个数字做对应运算即可
stk.pop();
stk.push(pre * num);
break;
case '/':
pre = stk.top(); // 只要拿出前一个数字做对应运算即可
stk.pop();
stk.push(pre / num);
break;
}
sign = s[i];
num = 0;
}
}
// 将栈中所有结果求和就是答案
int res = 0;
while (!stk.empty()) {
res += stk.top();
stk.pop();
}
return res;
}
};
第四步,处理括号。
括号具有递归性质。
calc(3 * (4 - 5/2) - 6)
= 3 * calc(4 - 5/2) - 6
= 3 * 2 - 6
= 0
无论多少层括号嵌套,通过 calc 函数递归调用自己,都可以将括号中的算式化简成一个数字。
括号包含的算式,直接视为一个数字就可。
- 遇到
(
开始递归 - 遇到
)
结束递归
class Solution:
def calculate(self, s: str) -> int:
def calc(s: List) -> int:
stack = []
sign = '+'
num = 0
while len(s) > 0:
c = s.popleft()
if c.isdigit():
num = 10 * num + int(c)
# 遇到左括号开始递归计算 num
if c == '(':
num = calc(s)
if (not c.isdigit() and c != ' ') or len(s) == 0:
if sign == '+':
stack.append(num)
elif sign == '-':
stack.append(-num)
elif sign == '*':
stack[-1] = stack[-1] * num
elif sign == '/':
# python 除法向 0 取整的写法
stack[-1] = int(stack[-1] / float(num))
num = 0
sign = c
# 遇到右括号返回递归结果
if c == ')': break
return sum(stack)
return calc(collections.deque(s))
你看,加了两三行代码,就可以处理括号了。
C++ 完整代码:
class Solution {
public:
int calculate(string str) {
int index = 0;
return calc(str, index);
}
int calc(string &s, int &index) {
stack<int> tokens;
int num = 0;
char sign = '+';
for (; index < s.size() + 1; ++index) {
char c = s[index];
if (isdigit(c))
num = num * 10 + (c - '0');
if (c == '(') {
index++;
num = calc(s, index);
continue;
}
if (!isdigit(c) && c != ' ') {
int temp;
switch (sign) {
case '+':
tokens.push(num);
break;
case '-':
tokens.push(-num);
break;
case '*':
temp = tokens.top();
tokens.pop();
tokens.push(temp * num);
break;
default:
temp = tokens.top();
tokens.pop();
tokens.push(temp / num);
}
num = 0;
sign = c;
}
if (c == ')')
break;
}
int result = 0;
while (!tokens.empty()) {
result += tokens.top();
tokens.pop();
}
return result;
}
};
至此,计算器的全部功能就实现了,通过对问题的层层拆解化整为零。
扩展:后缀表达式
计算思路:先把中缀表达式转化为后缀表达式,所有符号都在运算数字后面出现,可以不用括号了。
- 中缀表达式:9 + (3-1) * 3 + 10 / 2
- 后缀表达式:9 3 1 - 3 * + 10 2 / +
再从左到右遍历后缀表达式,遇到数字就进栈,遇到符号就将处于栈顶俩个数字出栈运算,运算结果入栈,一直重复,就可以计算出最终结果。
那怎么把中缀表达式转化为后缀表达式?
从左到右遍历中缀表达式的每个数字和符号,若是数字就加入后缀表达式。
引入一个栈存储符号:
- 若是左括号,入栈
- 若是右括号则一直出栈直到左括号出栈为止,出栈后加入后缀表达式
- 若此符号优先级高于栈顶符号(乘除 > 加减),入栈
- 若此符号优先级低于栈顶符号(加减 < 乘除),则栈顶元素全部出栈并加入后缀表达式,并将当前符号进栈
- 一直到最终的后缀表达式
举例,遍历中缀表达式 9 + (3-1) * 3 + 10 / 2。
第 1 个字符是数字 9,加入后缀表达式
- 后缀表达式:9
第二个字符是符号 +,入栈
第三个字符是符号 ‘(’,因为是左括号还没配对,入栈。
第四个字符是数字 3,加入后缀表达式
- 后缀表达式:9 3
第五个字符 ‘-’,入栈。
第六个数字 1,加入后缀表达式。
第七个字符 ‘)’,若是右括号则一直出栈直到左括号出栈为止,出栈后加入后缀表达式。
- 后缀表达式:9 3 1 -
第八个数字 3,加入后缀表达式。
- 后缀表达式:9 3 1 - 3
第九个符号 ‘*’,栈顶元素是 ‘+’,此符号大于栈顶元素,入栈。
第十个字符 ‘+’,栈顶元素是 ‘*’,此符号小于栈顶元素,栈中元素全部出栈(没有比 + 号更低的优先级,所以全部出栈),并加入后缀表达式,并将当前符号进栈。
- 后缀表达式:9 3 1 - 3 * +
第十一个数字 10,加入后缀表达式
- 后缀表达式:9 3 1 - 3 * + 10
第十二个字符 ‘/’,栈顶元素是 ‘+’,若此符号优先级高于栈顶符号(乘除 > 加减),入栈
第十三个数字 2,加入后缀表达式
- 后缀表达式:9 3 1 - 3 * + 10 2
因为是字符串的最后一个字符,所以栈中全部字符全部出栈并加入后缀表达式
- 后缀表达式:9 3 1 - 3 * + 10 2 / +