1.问题描述
小F面临一个编程挑战:实现一个基本的计算器来计算简单的字符串表达式的值。该字符串表达式有效,并可能包含数字(0-9)、运算符+
、-
及括号()
。注意,字符串中不包含空格。除法运算应只保留整数结果。请实现一个解析器计算这些表达式的值,且不使用任何内置的eval
函数。
示例1
输入:
expression = "1+1"
输出:2
示例2
输入:
expression = "3+4*5/(3+2)"
输出:7
示例3
输入:
expression = "4+2*5-2/1"
输出:12
示例4
输入:
expression = "(1+(4+5+2)-3)+(6+8)"
输出:23
示例5
输入:
expression = "2*(5+5*2)/3+(6+8*3)"
输出:40
难度等级
中等
题目链接
2.解题思路
这道题目要我们自己来写一个简单的加减乘除的计算器,这道题看起来简单,但做起来没有想象中的那么轻松,让我们一起来看一下吧。
首先,这道题有几个需要考虑的地方,怎么解决四则运算的先后顺序,怎么处理有多个小括号甚至是嵌套的小括号的情况,怎么处理负数的情况,怎么处理两位数及以上的情况。
需要考虑的问题很多,但没关系,我们先慢慢来看。上面最难考虑的就是多个小括号嵌套的情况,所以我们可以暂时将它排除在外,我们先看看没有小括号要如何来解决,如果我们能解决没有小括号的情况,那么多个小括号甚至小括号嵌套,无非就是把每一个小括号内的式子单独计算,将得到的结果替换掉小括号即可。
这么一想,只要我们把没有小括号的情况解决了,有小括号的情况也能轻松解决。
考虑到四则运算是有计算的先后顺序的,所以我们得依次把每一个数字和符号单独拿出来,这样才能进行根据计算顺序进行计算,这里我选择了双端队列作为存储数字和符号的数据结构,原因是它既能从队列头取元素,也能从队列尾取元素,非常灵活。
Deque<Integer> queue = new LinkedList<>();
选择好数据结构之后,我们就可以开始遍历字符串了。遍历字符串,我们可能遇到五种情况,+ - * / 和数字。我们一一来看。
遇到数字的情况。因为我们是一个字符一个字符的遍历字符串,所以如果我们遍历的字符是一个数字,我们还不能直接将它加入队列。因为字符表示数字只能表示0-9,而字符串中的数字可以是两位数、三位数甚至更多位数,所以我们要用一个循环在当前字符的基础上继续遍历,直到遇到一个四则运算符为止,记录下数字尾的索引,然后根据数字第一个字符的索引和数字尾的索引,将数字从字符串中截取出来,再将数字加入队列。
//将数字放入队列中
int p = i;
while(i < expression.length() && expression.charAt(i)!= '+' && expression.charAt(i)!= '-' && expression.charAt(i)!= '*' && expression.charAt(i)!= '/'){
i++;
}
queue.add(Integer.parseInt(expression.substring(p, i)));
i--;
遇到+号的情况。因为我们的队列存储的是整数类型,而将+号存入队列中,+号会被自动转成43(ASCII码),这样+号可能更我们的数字冲突,所以我们不能直接将+号存入队列中。我们可以将int的最大值作为+号的标记存入队列中,因为int的最大值几乎不可能出现在我们的四则运算中。
//遇到+号
if(expression.charAt(i) == '+'){
queue.add(Integer.MAX_VALUE);
continue;
}
如果遇到-号的情况。和+号是同样的道理,- 号如果直接存入队列,也会被转成45(ASCII码),我们可以将int的最小值作为-号的标记存入队列中。但是,在将 - 号存入队列中之前,我们还要判断队列为的元素是符号还是数字,如果是数字,则直接正常存入;如果是+号,我们则要将队列尾的+号取出,再将 - 号存入;如果是 - 号,我们则要将队列尾的 - 号取出,存入+号。
//遇到-号
if(expression.charAt(i) == '-'){
//遇到-号,如果队列中前一个元素也是-号,则将-号弹出,将-号替换为+号
if(queue.getLast() == Integer.MIN_VALUE){
queue.removeLast();
queue.add(Integer.MAX_VALUE);
continue;
}
//遇到-号,如果队列中前一个元素是+号,则将+号弹出,将-号放入队列中
if(queue.getLast() == Integer.MAX_VALUE){
queue.removeLast();
}
queue.add(Integer.MIN_VALUE);
continue;
}
如果遇到 * 或者 / 号的情况,我们要将队列尾的数字取出,然后再将* 或者 /号后的数字从字符串中也取出来(方法和遇到数字时取数字的方法类似),直接进行运算,再将运算后的结果存入队列当中。即我们在遍历字符串时将乘除这两种优先级比较高的运算一并解决了。
//先算乘除
for(int i = 0;i < expression.length();i++){
//遇到乘号
if(expression.charAt(i) == '*'){
//从队列末尾拿出第一个乘数
Integer num1 = queue.removeLast();
//从字符串中取出第二个乘数
int p = ++i;
while(i < expression.length() && expression.charAt(i)!= '+' && expression.charAt(i)!= '-' && expression.charAt(i)!= '*' && expression.charAt(i)!= '/'){
i++;
}
Integer num2 = Integer.parseInt(expression.substring(p, i));
i--;
//将相乘后的结果放入队列中
queue.add(num1*num2);
continue;
}
//遇到除号
if(expression.charAt(i) == '/'){
//取出被除数
Integer num1 = queue.removeLast();
//从字符串中获取除数
int p = ++i;
while(i < expression.length() && expression.charAt(i)!= '+' && expression.charAt(i)!= '-' && expression.charAt(i)!= '*' && expression.charAt(i)!= '/'){
i++;
}
Integer num2 = Integer.parseInt(expression.substring(p, i));
i--;
//将商放入队列中
queue.add(num1/num2);
continue;
}
遍历完字符串后,我们队列中的元素就只有数字已经+号和-号,这两种符合的优先级一样,计算顺序是从左往右,所以我们只需要依次从队列头将它们取出来进行计算即可,并将最后的计算结果以字符串的形式保留下来。(因为我们还需要考虑小括号的情况)
//再算加减
int result = queue.pop();
while(!queue.isEmpty()){
//弹出
Integer pop = queue.pop();
//如果是+号,则将队列中下一个元素加到result上
if(pop == Integer.MAX_VALUE){
result += queue.pop();
}
//如果是-号,则将队列中下一个元素减到result上
if(pop == Integer.MIN_VALUE){
result -= queue.pop();
}
}
上述的思路,是在没有考虑小括号的情况下进行的四则运算,我们可以将上面的过程,封装成一个方法,用于小括号情况的计算。
有了上面的方法,我们只需要将所有的小括号看成一个一个的式子,计算出各个式子的值,用这些值来代替小括号,最终带有小括号的式子也会被转变成一个没有小括号的式子,然后再调用一次上面的方法,就可以计算出结果了。
我们可以使用两个指针,用来记录左右括号的索引,当遇到左括号时,更新左括号指针的索引,当遇到第一个右括号时,更新右括号指针的索引,此时两个指针之间的式子,就是最内层的小括号里的式子(不再包含小括号),我们调用前面已经封装的方法对这个式子进行计算,将得到的结果替换掉原来的小括号。
// 左括号索引
int leftBracket = -1;
//右括号索引
int rightBracket = -1;
int p = 0;
//消除所有括号
while(expression.contains("(")){
//遇到左括号
if(expression.charAt(p) == '('){
//更新最新左括号索引
leftBracket = p;
}
//遇到第一个右括号
if(expression.charAt(p) == ')'){
//更新第一个右括号索引
rightBracket = p;
//计算括号内的值
String calculate = calculate(expression.substring(leftBracket + 1, rightBracket));
//用括号内的值替换掉括号
expression = expression.replace(expression.substring(leftBracket, rightBracket + 1), calculate);
//重置指针重新寻找括号
p = -1;
}
p++;
}
然后重置左右括号的指针以及遍历指针,继续寻找小括号,直到式子中没有小括号为止。
当我们消除了所有小括号之后,再调用一次封装好的方法进行最后一次计算,就得到了最终的答案。
//计算最终结果
return Integer.parseInt(calculate(expression));
3.代码展示
import java.util.Deque;
import java.util.LinkedList;
public class Main {
public static int solution(String expression) {
// Please write your code here
// 左括号索引
int leftBracket = -1;
//右括号索引
int rightBracket = -1;
int p = 0;
//消除所有括号
while(expression.contains("(")){
//遇到左括号
if(expression.charAt(p) == '('){
//更新最新左括号索引
leftBracket = p;
}
//遇到第一个右括号
if(expression.charAt(p) == ')'){
//更新第一个右括号索引
rightBracket = p;
//计算括号内的值
String calculate = calculate(expression.substring(leftBracket + 1, rightBracket));
//用括号内的值替换掉括号
expression = expression.replace(expression.substring(leftBracket, rightBracket + 1), calculate);
//重置指针重新寻找括号
p = -1;
}
p++;
}
//计算最终结果
return Integer.parseInt(calculate(expression));
}
public static String calculate(String expression){
Deque<Integer> queue = new LinkedList<>();
//先算乘除
for(int i = 0;i < expression.length();i++){
//遇到乘号
if(expression.charAt(i) == '*'){
//从队列末尾拿出第一个乘数
Integer num1 = queue.removeLast();
//从字符串中取出第二个乘数
int p = ++i;
while(i < expression.length() && expression.charAt(i)!= '+' && expression.charAt(i)!= '-' && expression.charAt(i)!= '*' && expression.charAt(i)!= '/'){
i++;
}
Integer num2 = Integer.parseInt(expression.substring(p, i));
i--;
//将相乘后的结果放入队列中
queue.add(num1*num2);
continue;
}
//遇到除号
if(expression.charAt(i) == '/'){
//取出被除数
Integer num1 = queue.removeLast();
//从字符串中获取除数
int p = ++i;
while(i < expression.length() && expression.charAt(i)!= '+' && expression.charAt(i)!= '-' && expression.charAt(i)!= '*' && expression.charAt(i)!= '/'){
i++;
}
Integer num2 = Integer.parseInt(expression.substring(p, i));
i--;
//将商放入队列中
queue.add(num1/num2);
continue;
}
//遇到+号
if(expression.charAt(i) == '+'){
queue.add(Integer.MAX_VALUE);
continue;
}
//遇到-号
if(expression.charAt(i) == '-'){
//遇到-号,如果队列中前一个元素也是-号,则将-号弹出,将-号替换为+号
if(queue.getLast() == Integer.MIN_VALUE){
queue.removeLast();
queue.add(Integer.MAX_VALUE);
continue;
}
//遇到-号,如果队列中前一个元素是+号,则将+号弹出,将-号放入队列中
if(queue.getLast() == Integer.MAX_VALUE){
queue.removeLast();
}
queue.add(Integer.MIN_VALUE);
continue;
}
//将数字放入队列中
int p = i;
while(i < expression.length() && expression.charAt(i)!= '+' && expression.charAt(i)!= '-' && expression.charAt(i)!= '*' && expression.charAt(i)!= '/'){
i++;
}
queue.add(Integer.parseInt(expression.substring(p, i)));
i--;
}
//再算加减
int result = queue.pop();
while(!queue.isEmpty()){
//弹出
Integer pop = queue.pop();
//如果是+号,则将队列中下一个元素加到result上
if(pop == Integer.MAX_VALUE){
result += queue.pop();
}
//如果是-号,则将队列中下一个元素减到result上
if(pop == Integer.MIN_VALUE){
result -= queue.pop();
}
}
//返回结果
return String.valueOf(result);
}
public static void main(String[] args) {
// You can add more test cases here
System.out.println(solution("1+1") == 2);
System.out.println(solution("3+4*5/(3+2)") == 7);
System.out.println(solution("4+2*5-2/1") == 12);
System.out.println(solution("(1+(4+5+2)-3)+(6+8)") == 23);
}
}
4.总结
这道题看似简单,但小坑还是挺多的,内嵌小括号、四则运算的先后顺序、数字的位数以及负数的情况,但是只要想要解决起来并不困难,有点耐心就可以了。好了,我就不多说废话了,这篇写得有点小长,也不知道我讲没讲清楚。祝大家刷题愉快~