📝 LeetCode 做题笔记(二):678. 有效的括号字符串
题目链接 :678. 有效的括号字符串
难度 :中等 | 语言 :Java | 标签:栈、贪心、字符串、动态规划
🔍 题目理解
给定字符串 s,包含 '('、')'、'*' 三种字符,其中 '*' 可视为:
- 左括号
'(' - 右括号
')' - 空字符串
""
判断是否存在一种替换方式,使字符串成为有效括号序列。
📌 有效括号规则
- 左右括号数量匹配
- 任意前缀中,左括号数 ≥ 右括号数
💡 解题思路:贪心 + 区间维护(最优解)
核心洞察
不枚举
*的所有可能,而是维护未匹配左括号数量的可能范围[low, high]
状态定义:
low:当前最少可能的未匹配左括号数(尽量把*当)用)high:当前最多可能的未匹配左括号数(尽量把*当(用)
遍历规则:
| 字符 | low 变化 | high 变化 | 说明 |
|---|---|---|---|
'(' |
+1 | +1 | 必定增加未匹配左括号 |
')' |
-1 | -1 | 必定消耗一个左括号 |
'*' |
-1 | +1 | 三种可能:)/(/"" |
关键剪枝:
java
// high < 0:即使把*全当(用,右括号还是太多 → 无效
// low = Math.max(low, 0):未匹配左括号数不能为负(前缀合法性)
🧩 代码实现(贪心法 - Java)
java
class Solution {
public boolean checkValidString(String s) {
int low = 0, high = 0;
for (char ch : s.toCharArray()) {
if (ch == '(') {
low++;
high++;
} else if (ch == ')') {
low = Math.max(low - 1, 0); // 关键:low不能为负
high--;
} else { // ch == '*'
low = Math.max(low - 1, 0); // * 当作 )
high++; // * 当作 (
}
if (high < 0) { // 右括号太多,无法匹配
return false;
}
}
return low == 0; // 最终能否完全匹配
}
}
🔄 其他解法对比
方法二:双栈法(直观但空间略高)
- 用两个栈分别记录
'('和'*'的位置 - 遇到
')'优先匹配'(',没有再匹配'*' - 最后检查剩余
'('能否被右侧'*'匹配
java
class Solution {
public boolean checkValidString(String s) {
Deque<Integer> leftStack = new ArrayDeque<>();
Deque<Integer> starStack = new ArrayDeque<>();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (ch == '(') {
leftStack.push(i);
} else if (ch == '*') {
starStack.push(i);
} else { // ')'
if (!leftStack.isEmpty()) {
leftStack.pop();
} else if (!starStack.isEmpty()) {
starStack.pop();
} else {
return false;
}
}
}
// 匹配剩余的 '(',确保 '(' 在 '*' 左侧
while (!leftStack.isEmpty() && !starStack.isEmpty()) {
if (leftStack.peek() < starStack.peek()) {
leftStack.pop();
starStack.pop();
} else {
break; // '(' 在 '*' 右侧,无法匹配
}
}
return leftStack.isEmpty();
}
}
方法三:动态规划(通用但复杂)
dp[i][j]表示s[0:i]是否能形成j个未匹配左括号- 时间复杂度 O(n²),适合理解但不推荐面试使用
java
// 简略版思路,完整实现略长
boolean[][] dp = new boolean[n + 1][n + 1];
dp[0][0] = true;
// 状态转移略...
📊 复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 贪心区间 | O(n) | O(1) | ✅ 首选 |
| 双栈法 | O(n) | O(n) | 思路直观 |
| 动态规划 | O(n²) | O(n²) | 理解状态转移 |
🎯 总结 & 易错点
✅ 贪心法精髓:
- 用区间
[low, high]表示所有可能状态,避免指数级枚举 low = Math.max(low, 0)是保证前缀合法的关键
❌ 常见错误:
- 忘记
low不能为负(前缀中右括号不能超过左括号) - 最终判断写成
high == 0(应该是low == 0,只要有一种可能完全匹配即可) *当空字符串的情况被忽略(体现在low-1和high+1的区间扩展)
💡 举一反三:
- 类似题目:20. 有效的括号、32. 最长有效括号
- 贪心区间思想可迁移到其他"不确定性选择"问题
🔧 Java 特性小贴士
java
// 1. TreeSet 自动排序 + 去重
TreeSet<Integer> set = new TreeSet<>();
set.add(3); set.add(1); set.add(2);
// 迭代顺序: 1, 2, 3 (升序)
set.pollFirst(); // 移除并返回最小值
set.pollLast(); // 移除并返回最大值
// 2. Deque 作为栈使用(比 Stack 更高效)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // 入栈
int top = stack.pop(); // 出栈
boolean empty = stack.isEmpty();
// 3. 字符串遍历推荐方式
for (char ch : s.toCharArray()) { // 简洁
// ...
}
// 或需要索引时:
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
// ...
}
🌟 写在最后 :
两道题代表两类典型思维:
- 1878:几何枚举 + 边界处理 → 锻炼坐标变换能力
- 678:贪心区间 + 状态压缩 → 培养抽象建模思维
Java 选手注意:
TreeSet是升序,转结果数组时要反转Math.max()处理边界比三元表达式更清晰- 优先用
ArrayDeque替代Stack类刷题不在多,而在每道题都吃透核心思想。共勉!🚀
如果这篇笔记对你有帮助,欢迎点赞收藏~ 有其他题想看我写笔记,评论区告诉我!