1. 项目概述
今天我们来拆解LeetCode第1106题"解析布尔表达式"这道中等难度的算法题。这道题要求我们解析一个由字符't'(true)、'f'(false)、'&'(与)、'|'(或)、'!'(非)组成的复杂布尔表达式,并返回最终的布尔值结果。
在实际开发中,我们经常会遇到需要解析复杂逻辑表达式的场景,比如规则引擎、条件过滤、权限判断等。这道题很好地模拟了这类需求,考察了我们对栈数据结构的运用能力以及对布尔运算的理解深度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题分析与解题思路
2.1 题目详细说明
给定一个字符串表达式expression,它由以下字符组成:
- 't':表示true
- 'f':表示false
- '&':表示逻辑与
- '|':表示逻辑或
- '!':表示逻辑非
- '('和')':用于分组
表达式可以嵌套,例如"!(f)"、"|(t,f)"、"&(t,|(f,t))"都是有效的输入。
2.2 解题思路分析
这道题的核心在于如何处理嵌套的布尔表达式。我们可以观察到:
- 每当遇到运算符('&','|','!')时,后面必定跟着一个括号包裹的子表达式
- 子表达式中可能包含更深的嵌套
- 我们需要从最内层的表达式开始计算,逐步向外层计算
这种"后进先出"的特性提示我们可以使用栈(stack)这种数据结构来解决问题。具体思路是:
- 使用两个栈:一个存储运算符,一个存储操作数
- 遇到运算符时压入运算符栈
- 遇到't'或'f'时压入操作数栈
- 遇到')'时弹出运算符和操作数进行计算
- 最终栈中剩下的就是结果
3. 详细实现步骤
3.1 数据结构设计
我们使用两个栈:
java复制Deque<Character> operators = new ArrayDeque<>(); // 存储运算符
Deque<Character> operands = new ArrayDeque<>(); // 存储操作数
3.2 核心算法流程
- 遍历表达式中的每个字符
- 如果当前字符是','则跳过
- 如果当前字符是运算符('&','|','!'),压入运算符栈
- 如果当前字符是't'或'f',压入操作数栈
- 如果当前字符是'(',压入操作数栈作为分隔符
- 如果当前字符是')',开始计算:
- 弹出运算符栈顶元素
- 不断弹出操作数栈顶元素直到遇到'('
- 根据运算符计算结果
- 将结果压回操作数栈
3.3 完整Java实现代码
java复制public boolean parseBoolExpr(String expression) {
Deque<Character> operators = new ArrayDeque<>();
Deque<Character> operands = new ArrayDeque<>();
for (char c : expression.toCharArray()) {
if (c == ',') continue;
if (c == '&' || c == '|' || c == '!') {
operators.push(c);
} else if (c == 't' || c == 'f') {
operands.push(c);
} else if (c == '(') {
operands.push(c);
} else if (c == ')') {
char operator = operators.pop();
List<Character> subExpr = new ArrayList<>();
while (operands.peek() != '(') {
subExpr.add(operands.pop());
}
operands.pop(); // 弹出'('
char result = calculate(operator, subExpr);
operands.push(result);
}
}
return operands.pop() == 't';
}
private char calculate(char operator, List<Character> subExpr) {
if (operator == '!') {
return subExpr.get(0) == 't' ? 'f' : 't';
}
boolean result = operator == '&'; // 初始值:对于&是true,对于|是false
for (char c : subExpr) {
boolean b = c == 't';
if (operator == '&') {
result &= b;
} else {
result |= b;
}
}
return result ? 't' : 'f';
}
4. 复杂度分析与优化
4.1 时间复杂度分析
- 遍历表达式:O(n)
- 每个字符最多入栈和出栈一次:O(n)
- 计算子表达式:最坏情况下需要遍历所有操作数
- 总体时间复杂度:O(n)
4.2 空间复杂度分析
- 两个栈的空间消耗:O(n)
- 临时存储子表达式的列表:O(n)
- 总体空间复杂度:O(n)
4.3 可能的优化方向
- 可以合并两个栈为一个栈,通过标记区分运算符和操作数
- 对于大型表达式,可以考虑迭代替代递归实现以避免栈溢出
- 可以预处理表达式,去除所有逗号简化处理逻辑
5. 常见问题与调试技巧
5.1 常见错误类型
- 括号不匹配:忘记处理'('或')'
- 运算符优先级混淆:特别是'!'运算符的处理
- 初始值设置错误:'&'和'|'的初始值不同
- 类型混淆:将字符't'/'f'直接当作布尔值使用
5.2 调试技巧
- 打印栈状态:在处理每个字符后打印两个栈的内容
- 单元测试:针对不同运算符和嵌套级别编写测试用例
- 边界测试:测试空表达式、单操作数、深度嵌套等情况
提示:在处理'!'运算符时,特别注意它只需要一个操作数,而其他运算符需要多个操作数。
6. 实际应用场景
这种布尔表达式解析器在实际开发中有广泛应用:
- 规则引擎:解析用户定义的条件规则
- 权限系统:组合多个权限条件
- 搜索过滤:构建复杂的过滤条件
- 配置系统:解析配置中的条件表达式
例如,一个电商系统可能需要解析这样的过滤条件:
"&(category=electronics,|(price<1000,on_sale=t))"
7. 扩展思考
7.1 支持更多运算符
可以扩展支持:
- '^':异或运算
- '>'/'<':比较运算
- '=':等于运算
7.2 支持变量和常量
允许表达式包含变量名和常量值,如"&(age>18,country=US)"
7.3 性能优化
对于频繁计算的表达式,可以:
- 预编译表达式为语法树
- 缓存计算结果
- 支持惰性求值
8. 个人实现心得
在实际实现这道题时,我最初尝试了递归下降的解析方法,但发现对于嵌套较深的表达式容易导致栈溢出。改用栈结构后,不仅代码更简洁,性能也更稳定。关键点在于:
- 明确区分运算符和操作数的处理时机
- 使用'('作为子表达式的开始标记
- 在计算子表达式时正确处理不同运算符的语义
- 特别注意'!'运算符只需要一个操作数
另一个值得注意的地方是'&'和'|'运算符的初始值设置。'&'需要初始为true,因为true & x = x;而'|'需要初始为false,因为false | x = x。
