题目解析
先附上青训营中的题目链接 52. 简单四则运算解析器
这道题是非常经典的一道算法题了,它的核心是实现一个支持加减乘除及括号运算的简单表达式解析器,考察的是栈的应用以及中缀表达式的计算规则。由于不能使用内置的 eval 函数,必须手动解析字符串,并实现操作符优先级处理、括号匹配及运算操作。
算法的核心思路是通过两个栈(一个存储操作数,另一个存储操作符)模拟表达式的解析与计算过程。这种方式适用于绝大多数简单的数学表达式解析任务。
解题思路
1. 表达式的解析规则
表达式的计算顺序受到操作符优先级和括号嵌套的限制,因此我们需要遵循以下规则:
• 优先级处理:乘法和除法优先于加法和减法执行;
• 括号嵌套:括号内的表达式优先计算;
• 顺序计算:在优先级和括号影响下,从左至右逐步解析并计算。
解析时,我们需要维护一个运算环境,使得每个操作符都能按优先级正确处理。
2. 数据结构设计
为了实现上述解析规则,使用两个栈:
-
数字栈(numberStack) :用于存储操作数;
-
操作符栈(operatorStack) :用于存储操作符和括号。
栈的特点是后进先出(LIFO),非常适合处理括号嵌套和优先级问题。当遇到右括号或者优先级较低的操作符时,可以通过栈顶操作实现当前解析范围的子表达式计算。
3. 运算符优先级
为了正确处理操作符,我们通过一个哈希表定义优先级:
• + 和 - 的优先级为 0;
• * 和 / 的优先级为 1。
优先级的作用是在栈顶存在高优先级操作符时,要求先进行栈顶操作计算,确保计算顺序正确。
4. 解析过程
解析过程分为以下几步:
-
逐字符扫描:逐一读取表达式的字符,根据其类型(数字、操作符、括号)采取不同操作。
-
处理数字:如果是数字,直接转换为整数并入 numberStack。
-
处理操作符:判断当前操作符与栈顶操作符优先级关系:
• 若栈顶操作符优先级高,则出栈进行运算;
• 否则直接将当前操作符入栈。
- 处理括号:
• 遇到左括号时,直接入 operatorStack;
• 遇到右括号时,不断弹出操作符栈并计算,直到匹配到对应的左括号。
- 处理剩余操作符:扫描完成后,依次从栈中取出操作符和操作数完成计算。
5. 处理剩余操作符
在扫描完成后,栈中可能仍然存在未计算的操作符。需要将 operatorStack 逐步清空,依次从 numberStack 中取出两个数字进行运算。
代码实现
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
public class Main {
private static int applyOperation(int a, int b, char op) {
switch (op) {
case '+':
return a + b;
case '-':
return a - b;
case '*':
return a * b;
case '/':
return a / b;
default:
throw new IllegalArgumentException("Invalid operator: " + op);
}
}
public static int solution(String expression) {
// Please write your code here
Stack<Integer> numberStack = new Stack<>();
Stack<Character> operatorStack = new Stack<>();
// 操作符优先级
Map<Character, Integer> map = new HashMap<>() {
{
put('+', 0);
put('-', 0);
put('*', 1);
put('/', 1);
}
};
for (char ch : expression.toCharArray()) {
if (ch >= '0' && ch <= '9')
numberStack.push(ch - '0'); // 数字直接入栈
else if (ch == '+' || ch == '-' || ch == '*' || ch == '/') {
while (!operatorStack.isEmpty() && operatorStack.peek() != '(' && map.get(ch) <= map.get(operatorStack.peek()))
{
// 操作栈不为空,栈顶不为左括号且优先级比外面低,就压入
int b = numberStack.pop();
int a = numberStack.pop();
char op = operatorStack.pop();
numberStack.push(applyOperation(a, b, op));
}
// 否则操作符入栈
operatorStack.push(ch);
} else if (ch == '(') {
operatorStack.push(ch);
} else if (ch == ')')// 遇到右括号,弹出栈顶操作符进行运算,直到遇到左括号并弹出
{
while (operatorStack.peek() != '(') {
char op = operatorStack.pop();
int b = numberStack.pop();
int a = numberStack.pop();
numberStack.push(applyOperation(a, b, op));
}
operatorStack.pop();
}
}
// 扫描完毕,继续运算
while (!operatorStack.empty()) {
char op = operatorStack.pop();
int b = numberStack.pop();
int a = numberStack.pop();
numberStack.push(applyOperation(a, b, op));
}
return numberStack.peek();
}
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);
}
}
一些Tips & 个人思考
这道题目表面上看是实现一个简单的计算器,但其核心包含了许多计算逻辑与数据结构知识的结合,尤其是在表达式解析、栈的应用以及算法优化等方面。完成这道题后,我有以下几点心得与收获:
1. 栈的灵活性和重要性
栈是表达式解析中不可或缺的数据结构,它的后进先出的特点完美契合了括号匹配和操作符优先级的需求。在这道题中,我们通过两个栈分别管理操作数和操作符,解决了从中缀表达式直接计算结果的问题。
在解决过程中,我对栈的理解更进一步,尤其是通过动态调整栈的内容解决不同优先级的操作。栈的这种灵活性让我意识到,它不仅能用于简单的括号匹配,还可以很好地解决复杂问题。未来在涉及嵌套结构或递归问题时,栈无疑是一个重要工具。
2. 动态优先级处理的设计
这道题的关键之一是如何正确处理操作符的优先级。加减法和乘除法的优先级不同,需要在解析时动态调整计算顺序。通过设计优先级的哈希表,并结合操作符栈的内容判断是否需要出栈运算,我发现这种方法非常高效且易于扩展。
这一思路让我意识到,解决问题时,动态调整优先级不仅限于计算问题,还可以应用在其他需要排序或优先处理的任务中。例如,在操作系统的任务调度中,动态优先级也可以根据当前的状态进行调整,这种思想是一种通用的解决方案。
3. 对括号嵌套的处理
括号是表达式解析中最复杂的部分之一,它不仅改变了运算的顺序,还需要正确地匹配左右括号。在实现过程中,我采用了简单但有效的方案:左括号直接入栈,右括号则触发括号内子表达式的计算,直到匹配到对应的左括号为止。
这一设计让我明白了解决复杂问题时,分而治之是非常有效的策略。将嵌套问题拆解为独立的小问题,逐步解决,既能保证逻辑清晰,也能减少错误发生的可能性。
4. 边界条件的思考
在实现过程中,我还特别关注了一些边界情况,例如:
• 空字符串应该返回0;
• 操作符和数字的连续性是否合法;
• 除法时如何处理除以零的问题(题目假设输入有效,不需特殊处理)。
虽然这些边界条件在题目描述中没有明确要求,但在实际开发中,处理边界情况是写好健壮代码的关键。这次练习让我更加重视边界条件对程序正确性的影响。
5. 算法的可扩展性
当前实现的解析器只支持简单的整数加减乘除运算,但这一框架具有很强的可扩展性。例如:
• 支持多位数和小数:通过引入一个缓冲区(StringBuilder)来处理连续的数字字符,支持多位数解析。
• 支持更多操作符:通过扩展优先级表,增加如幂运算(^)、取模运算(%)等高级运算。
• 支持函数调用:通过将函数解析为嵌套表达式,扩展解析器的功能,如支持 sin(), cos(), log() 等。
• 支持变量和动态输入:结合符号表解析变量名,或通过动态赋值支持用户自定义表达式。
这道题让我感受到,解决问题不仅仅是完成当前任务,还要思考解决方案在更广泛场景中的适用性。一个设计良好的框架能够承载更多的功能扩展,而不需要从头再写。
6. 表达式解析的实际意义
表达式解析是编译器、脚本引擎和数据处理工具中的核心功能。例如,在编译器中,解析器需要将复杂的代码表达式转化为语法树;在数据库中,查询解析器需要将 SQL 表达式翻译为可执行的操作计划。通过这道题,我对表达式解析的基本实现有了直观的感受,也更理解实际开发中解析任务的重要性。