求解一个四则运算表达式是我们在学习《数据结构(栈)》时的经典问题。而现在,我们尝试脱离栈的使用,用同样优美的代码实现一个四则运算。
下面,将循序渐进,从最简单加减计算器,加上乘除,加上括号,最终呈现一个完整的四则运算计算器。让我们开始吧 >_< !!!
基本计算器1——只有加减
input:“2+3+11-6”
output:10
第一题就涉及到本类问题的一个重要思想:四则运算都可以看成加法运算。
看图很好理解:
现在思考一个极其重要的问题,上图中分割加减运算为加法运算的依据是什么?换句话讲,当遍历到什么字符时,我们就知道此时应该分割出一个完整加法项了?
是数字吗?当我们遍历到+
,就记录一下这个符号;接着遍历到3
,便在最终结果上+3。看似是个正确的思路,但是当遍历到1
时,程序误认为此次加法分割结束,便在最终结果上+1,可实际上应该+11才对。
分割的依据是加号/减号。理解这点后,本题的思路呼之欲出:我们设置一个ops
缓存所谓上一个符号,设置一个num
记录当前的数字,设置一个res
作为最终结果;当c
遍历到数字时便记录下来;当c
遍历到加号/减号时便将上一个加法项(由ops
和num
组成)追加到最终结果上;同时c
所指向的加号/减号缓存入ops
,成为上一个符号,c
继续移动。
另外注意,最后一个加法项是不是没被分割出来?因为它的后面没有符号。因此,当遍历到最后一个字符时(len - 1
),也作为分割出加法项的依据。
还是看图理解:
下面给出代码(另外,此代码中对于多位数(11
)的处理非常优雅,注意体会):
class Solution {
public int calculate(String s) {
int num = 0; // 记录的数字(看看它在上图中的位置)
char ops = '+'; // 缓存的符号(看看它在上图中的位置)
int res = 0; // 最终结果
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 如果是数字
if (Character.isDigit(c)) {
num = num * 10 + c - '0';
}
// 如果是加减
if (c == '+' || c == '-' || i == s.length() - 1) {
switch (ops) {
case '+': res += num; break;
case '-': res -= num; break;
}
num = 0;
ops = c;
}
}
return res;
}
}
基本计算器2——加减乘除
input:“2-3+4*6+2”
output:25
你应该能立马意识到,这比加减运算多出了什么?优先级。
我们暂时不谈优先级,先想想加减乘除运算是否可以分割加法项。不仅可以,从下面的分割中,你更能理解为什么分割的依据是不是数字,不是乘号/除号,而是加号/减号。
还是看图:
下面解决优先级问题。不难发现,上面的+4
不能急着追加到res
当中,因此我们还需要一个缓存结果tmp
;遍历到加/减/乘/除时,便在tmp
运算上一个符号,并缓存当前符号;只有遍历到加号/减号时,才将缓存讲过tmp
追加到最终结果res
上。
还是看图:
下面给出代码:
class Solution {
public int calculate(String s) {
int num = 0; // 记录的数字
char ops = '+'; // 缓存的符号
int tmp = 0; // 缓存结果
int res = 0; // 最终结果
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 如果是数字
if (Character.isDigit(c)) {
num = num * 10 + c - '0';
}
// 如果是符号
if (c == '+' || c == '-' || c == '*' || c == '/' || i == s.length() - 1) {
switch (ops) {
case '+': tmp += num; break;
case '-': tmp -= num; break;
case '*': tmp *= num; break;
case '/': tmp /= num; break;
}
if (c == '+' || c == '-' || i == s.length() - 1) {
res += tmp;
tmp = 0;
}
num = 0;
ops = c;
}
}
return res;
}
}
基本计算器3——带括号的加减
input:“2-(4-(7+1))”
output:6
这不也是优先级问题吗,括号可以将任何运算上升到最高优先级?
不是。括号可以嵌套。所以这是一个递归问题。
不妨暴力一点,直接切割字符串。当遍历到括号时,开启cnt
计数用于括号匹配(遇到左括号,计数加一;遇到右括号,计数减一);当cnt
回归到0时,将最大括号内部的内容作为一个新的表达式进行递归,递归返回的计算结果赋值给num
。
依旧是看图:
下面给出代码(在基本计算器1代码基础上,多加了对括号的处理而已):
class Solution {
public int calculate(String s) {
int num = 0; // 记录的数字
char ops = '+'; // 缓存的符号
int res = 0; // 最终结果
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 如果是括号
if (c == '(') {
int cnt = 0;
int t = i;
for (; i < s.length(); i++) {
if (s.charAt(i) == '(') cnt++;
if (s.charAt(i) == ')') cnt--;
if (cnt == 0) {
break;
}
}
num = calculate(s.substring(t + 1, i));
}
// 如果是数字
if (Character.isDigit(c)) {
num = num * 10 + c - '0';
}
// 如果是加减
if (c == '+' || c == '-' || i == s.length() - 1) {
switch (ops) {
case '+': res += num; break;
case '-': res -= num; break;
}
num = 0;
ops = c;
}
}
return res;
}
}
基本计算器4——四则运算(带括号的加减乘除)
综合了上面所有的思想:
1) 四则运算可以看成多个加法项
2)分割加法项的依据不是数字,不是乘号/除号,不是括号,而是加号/减号
3)遍历到数字,无脑记录;遍历到符号,计算上一个符号
3)乘除带来的优先级问题用缓存结果解决
4)括号带来的优先级问题用递归字符串解决
以及一些细节让代码更优雅和健壮:
1)最后一个字符也作为切割加法项的标志
2)多位数字的处理
3)表达式中空字符的处理
代码如下:
class Solution {
public int calculate(String s) {
int num = 0; // 当前的数字
char ops = '+'; // 当前的数字之前的符号
int tmp = 0; // 临时结果
int res = 0; // 最终结果
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 如果是括号
if (c == '(') {
int cnt = 0;
int t = i;
for (; i < s.length(); i++) {
if (s.charAt(i) == '(') cnt++;
if (s.charAt(i) == ')') cnt--;
if (cnt == 0) {
break;
}
}
num = calculate(s.substring(t + 1, i));
}
// 如果是数字
if (Character.isDigit(c)) {
num = num * 10 + c - '0';
}
// 如果是符号
if (c == '+' || c == '-' || c == '*' || c == '/' || i == s.length() - 1) {
switch (ops) {
case '+': tmp += num; break;
case '-': tmp -= num; break;
case '*': tmp *= num; break;
case '/': tmp /= num; break;
}
if (c == '+' || c == '-' || i == s.length() - 1) {
res += tmp;
tmp = 0;
}
num = 0;
ops = c;
}
}
return res;
}
}
L o l i Loli Loli i s is is t h e the the B E S T BEST BEST a n d and and P R E C I O U S PRECIOUS PRECIOUS g i f t gift gift i n in in t h e the the w o r l d . world. world.