1. 布尔表达式解析问题背景
LeetCode 1106题"解析布尔表达式"是一个典型的字符串处理与递归解析问题。题目要求我们实现一个能够解析特定格式布尔表达式的函数,这种表达式由字符 't'(true)、'f'(false)、'&'(与)、'|'(或)、'!'(非)以及括号组成。
在实际开发中,布尔表达式的解析器有着广泛的应用场景。比如在规则引擎配置、动态条件判断、查询条件构建等场景下,我们经常需要将字符串形式的逻辑表达式转换为可执行的布尔逻辑。这道题目正是这类实际问题的简化版本。
题目给出的表达式遵循特定的语法规则:
- 单个字符 't' 表示 true
- 单个字符 'f' 表示 false
- !(expr) 表示逻辑非运算
- &(expr1,expr2,...) 表示逻辑与运算
- |(expr1,expr2,...) 表示逻辑或运算
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题分析与解法思路
2.1 输入输出分析
函数签名为:
java复制public boolean parseBoolExpr(String expression)
输入是一个字符串,表示布尔表达式,输出是解析后的布尔值。例如:
- 输入:"!(f)",输出:true
- 输入:"&(t,f)",输出:false
- 输入:"|(&(t,f,t),!(t))",输出:false
2.2 递归下降解析法
这类表达式解析问题通常可以采用递归下降的方法来解决。基本思路是:
- 遇到操作符(&,|,!)时,记录当前操作类型
- 遇到'('时,开始解析子表达式
- 遇到')'时,结束当前层级的解析
- 遇到't'或'f'时,根据当前操作类型进行相应的布尔运算
这种方法天然适合处理嵌套的表达式结构,因为每个括号内的子表达式都可以看作一个独立的解析问题。
2.3 栈辅助迭代法
除了递归方法,我们也可以使用栈来迭代地解析表达式。栈可以帮助我们保存当前的操作符和中间结果,特别适合处理多层嵌套的表达式。
栈方法的优势在于避免了递归可能导致的栈溢出问题(虽然在这个问题中不太可能发生),并且有时在性能上会有轻微优势。
3. 递归解法实现详解
3.1 基础递归实现
java复制public boolean parseBoolExpr(String expression) {
return parse(expression, 0, expression.length());
}
private boolean parse(String s, int start, int end) {
char c = s.charAt(start);
if (c == 't') return true;
if (c == 'f') return false;
if (c == '!') return !parse(s, start + 2, end - 1);
boolean result = (c == '&'); // &初始为true,|初始为false
int level = 0;
int prev = start + 2;
for (int i = start + 1; i < end; i++) {
char ch = s.charAt(i);
if (ch == '(') level++;
if (ch == ')') level--;
if ((ch == ',' && level == 1) || (ch == ')' && level == 0)) {
boolean sub = parse(s, prev, i);
prev = i + 1;
if (c == '&') {
result &= sub;
if (!result) return false; // 短路优化
} else {
result |= sub;
if (result) return true; // 短路优化
}
}
}
return result;
}
3.2 递归实现解析
- 基础情况处理:直接返回't'或'f'对应的布尔值
- 非运算处理:递归解析括号内的表达式,然后取反
- 与/或运算处理:
- 初始化结果:与运算初始为true,或运算初始为false
- 使用level变量跟踪括号嵌套层级
- 遇到逗号且在最外层时,分割子表达式
- 递归解析每个子表达式并应用相应运算
- 实现短路优化:一旦结果确定即可提前返回
3.3 递归解法复杂度分析
- 时间复杂度:O(n),每个字符最多被处理一次
- 空间复杂度:O(n),递归调用栈的深度与表达式嵌套深度相关
4. 栈解法实现详解
4.1 双栈实现
java复制public boolean parseBoolExpr(String expression) {
Deque<Character> ops = new ArrayDeque<>();
Deque<Boolean> vals = new ArrayDeque<>();
for (int i = 0; i < expression.length(); i++) {
char c = expression.charAt(i);
if (c == ',') continue;
if (c == 't' || c == 'f') {
vals.push(c == 't');
} else if (c == '!' || c == '&' || c == '|') {
ops.push(c);
} else if (c == '(') {
vals.push(null); // 标记括号开始
} else if (c == ')') {
char op = ops.pop();
boolean res = op == '&'; // 初始值
while (vals.peek() != null) {
boolean top = vals.pop();
if (op == '&') res &= top;
else if (op == '|') res |= top;
else res = !top; // !运算只有一个操作数
}
vals.pop(); // 弹出null标记
vals.push(op == '!' ? res : res);
}
}
return vals.pop();
}
4.2 栈解法解析
-
双栈设计:
- ops栈:存储操作符(&,|,!)
- vals栈:存储操作数和中间结果
-
处理流程:
- 遇到操作符:压入ops栈
- 遇到't'/'f':转换为布尔值压入vals栈
- 遇到'(':压入null作为标记
- 遇到')':弹出操作符,处理直到遇到null标记的所有值
-
运算处理:
- 与/或运算:遍历所有操作数进行累积运算
- 非运算:只处理一个操作数
4.3 栈解法复杂度分析
- 时间复杂度:O(n),每个字符处理一次
- 空间复杂度:O(n),最坏情况下需要存储所有操作符和操作数
5. 边界条件与测试用例
5.1 常见边界情况
- 单值表达式:"t", "f"
- 单层非运算:"!(t)", "!(f)"
- 单层与/或运算:"&(t,f)", "|(t,f)"
- 多层嵌套运算:"|(&(t,f,t),!(t))"
- 复杂嵌套:"!(&(|(f,t),!(t)))"
5.2 测试用例设计
java复制@Test
public void testParseBoolExpr() {
Solution solution = new Solution();
// 基础测试
assertTrue(solution.parseBoolExpr("t"));
assertFalse(solution.parseBoolExpr("f"));
// 单层运算
assertFalse(solution.parseBoolExpr("!(t)"));
assertTrue(solution.parseBoolExpr("!(f)"));
assertFalse(solution.parseBoolExpr("&(t,f)"));
assertTrue(solution.parseBoolExpr("|(t,f)"));
// 多层嵌套
assertFalse(solution.parseBoolExpr("|(&(t,f,t),!(t))"));
assertTrue(solution.parseBoolExpr("!(&(|(f,t),!(t)))"));
// 复杂情况
assertFalse(solution.parseBoolExpr("&(t,f,&(t,t,f))"));
assertTrue(solution.parseBoolExpr("|(f,|(f,|(f,|(f,t))))"));
}
6. 性能优化与工程实践
6.1 短路优化
在布尔运算中,与运算一旦遇到false就可以提前返回,或运算一旦遇到true也可以提前返回。这在递归和栈解法中都可以实现:
java复制// 在递归解法中的短路优化
if (c == '&') {
result &= sub;
if (!result) return false; // 短路优化
} else {
result |= sub;
if (result) return true; // 短路优化
}
6.2 表达式验证
在实际工程应用中,我们可能还需要添加表达式合法性验证:
java复制private boolean isValidExpression(String expr) {
// 检查括号匹配
int balance = 0;
for (char c : expr.toCharArray()) {
if (c == '(') balance++;
if (c == ')') balance--;
if (balance < 0) return false;
}
if (balance != 0) return false;
// 检查操作符后必须有'('
for (int i = 0; i < expr.length(); i++) {
char c = expr.charAt(i);
if (c == '!' || c == '&' || c == '|') {
if (i + 1 >= expr.length() || expr.charAt(i + 1) != '(') {
return false;
}
}
}
return true;
}
6.3 扩展思考
在实际项目中,布尔表达式解析器可以进一步扩展:
- 支持更多操作符:如异或(^)、等于(=)等
- 支持变量替换:如"&(var1,var2)",从上下文中获取变量值
- 支持关系运算:如">", "<", "=="等
- 性能优化:预编译表达式为AST,支持重复计算
7. 常见问题与调试技巧
7.1 常见错误
-
括号匹配错误:在处理嵌套表达式时,容易忽略括号的层级关系
- 解决方法:使用level变量跟踪当前括号层级
-
操作符作用范围错误:特别是非运算(!)只需要作用于一个表达式
- 解决方法:明确区分一元和二元运算符的处理逻辑
-
短路逻辑实现错误:过早或过晚应用短路优化
- 解决方法:确保在每次子表达式计算后立即检查短路条件
7.2 调试技巧
-
打印递归/栈状态:在处理每个字符时打印当前状态
java复制System.out.println("Processing char: " + c + ", ops: " + ops + ", vals: " + vals); -
单元测试辅助:为每个边界情况编写小测试
java复制@Test public void testNestedNot() { assertFalse(solution.parseBoolExpr("!(!(f))")); } -
可视化解析过程:对于复杂表达式,可以画出解析树帮助理解
code复制|(&(t,f),!(t)) ├── &(t,f) │ ├── t │ └── f └── !(t) └── t
8. 算法选择与比较
8.1 递归 vs 迭代
| 特性 | 递归解法 | 栈解法 |
|---|---|---|
| 代码简洁性 | 更简洁 | 稍复杂 |
| 空间复杂度 | O(嵌套深度) | O(n) |
| 可读性 | 更直观 | 需要理解栈操作 |
| 扩展性 | 较难支持高级特性 | 更容易扩展 |
8.2 选择建议
- 对于简单需求或面试场景,递归解法通常足够且更易实现
- 对于工程应用或需要处理极深嵌套的情况,栈解法更可靠
- 如果需要支持表达式验证、变量替换等高级功能,基于栈/状态机的解法更合适
9. 实际应用场景
9.1 规则引擎
许多规则引擎的核心就是布尔表达式解析器。例如:
java复制// 规则配置
String rule = "&(>=(age,18),|(==(gender,'male'),==(married,false)))";
// 使用时
Map<String, Object> context = new HashMap<>();
context.put("age", 20);
context.put("gender", "male");
context.put("married", true);
boolean result = RuleEngine.evaluate(rule, context);
9.2 动态查询构建
在数据库查询构建中,动态布尔表达式很有用:
java复制String filter = "|(&(==(status,'active'),>=(score,80)),!(==(banned,true)))";
Query query = new Query();
query.addCriteria(parseBoolExpression(filter));
List<User> users = userRepository.find(query);
9.3 配置化业务逻辑
将业务逻辑配置为布尔表达式,实现动态调整:
java复制// 配置
String discountEligible = "&(>=(purchaseAmount,100),|(==(memberLevel,'gold'),==(isFirstOrder,true)))";
// 运行时
if (parseBoolExpr(discountEligible, context)) {
applyDiscount(10);
}
10. 扩展学习与相关题目
10.1 LeetCode相关题目
- 基本计算器(224题):处理加减和括号
- 基本计算器II(227题):处理加减乘除
- 基本计算器III(772题):处理加减乘除和括号
- 原子的数量(726题):化学式解析
10.2 解析器设计模式
在实际开发中,解析器模式是处理这类问题的经典设计模式:
java复制interface Expr {
boolean interpret();
}
class Constant implements Expr {
private boolean value;
// 实现interpret方法
}
class NotExpr implements Expr {
private Expr expr;
// 实现interpret方法
}
class AndExpr implements Expr {
private List<Expr> exprs;
// 实现interpret方法
}
10.3 编译原理基础
这个问题本质上是编译原理中词法分析和语法分析的简化版。深入学习可以了解:
- 词法分析:将字符流转换为token流
- 语法分析:根据文法规则构建语法树
- 递归下降解析:手写解析器的常用技术
- 运算符优先级处理:如何正确处理不同优先级的运算符
11. 个人实践建议
在实际编码练习中,我建议:
- 从简单到复杂:先实现单层解析,再处理嵌套
- 测试驱动开发:先写测试用例,再实现功能
- 多种解法对比:尝试递归和迭代两种方法
- 性能分析:使用JMH等工具对比不同解法的性能
- 扩展实践:尝试添加新操作符或支持变量
对于面试准备,这道题考察了几个关键点:
- 字符串处理能力
- 递归思维或栈的应用
- 边界条件处理
- 代码组织和可读性
在实现时,清晰的代码结构和充分的注释往往比一味追求简洁更重要。面试官通常更看重解题思路和代码的可维护性。
