1. 问题背景与核心挑战
LeetCode上"删除无效的括号"是一道经典的字符串处理题目,编号为301。这道题要求我们从一个包含括号和其他字符的字符串中,删除最少数量的无效括号,使得剩余的字符串成为有效的括号组合。乍看简单,实际处理时需要同时考虑多种边界情况。
我第一次遇到这个问题时,以为只需要简单统计左右括号数量就能解决。实际编写代码后发现,像"()())()"这样的输入,存在多个可能的有效结果(["()()()", "(())()"]),简单的计数法无法处理这种歧义情况。这促使我深入研究更可靠的解法。
2. 解法思路分析与选择
2.1 暴力回溯法的可行性
最直观的解法是生成所有可能的子字符串,然后检查每个子串是否有效。对于一个长度为n的字符串,每个字符有保留或删除两种选择,时间复杂度是O(2^n)。当n>20时,这种解法在LeetCode上会超时。
python复制def removeInvalidParentheses(s):
def isValid(sub):
balance = 0
for ch in sub:
if ch == '(': balance += 1
elif ch == ')': balance -= 1
if balance < 0: return False
return balance == 0
level = {s}
while True:
valid = list(filter(isValid, level))
if valid: return valid
level = {s[:i] + s[i+1:] for s in level for i in range(len(s))}
2.2 优化思路:有限步数删除
观察到题目要求"最少删除",这提示我们可以使用BFS逐层处理。每一层尝试删除一个括号,直到找到有效结果。这种方法在最坏情况下时间复杂度仍是O(2^n),但平均情况会好很多。
关键优化点:使用队列实现BFS时,需要记录已经处理过的字符串避免重复计算。这在测试用例包含大量重复字符时特别重要。
3. 完整Python实现与解析
3.1 BFS标准实现
python复制from collections import deque
def removeInvalidParentheses(s):
def is_valid(string):
cnt = 0
for ch in string:
if ch == '(': cnt += 1
elif ch == ')': cnt -= 1
if cnt < 0: return False
return cnt == 0
queue = deque([s])
visited = set([s])
found = False
res = []
while queue:
current = queue.popleft()
if is_valid(current):
res.append(current)
found = True
if found: continue # 确保只处理当前层
for i in range(len(current)):
if current[i] not in '()': continue
next_str = current[:i] + current[i+1:]
if next_str not in visited:
visited.add(next_str)
queue.append(next_str)
return res if res else [""]
3.2 关键步骤解析
- 初始化队列:从原始字符串开始BFS遍历
- 有效性检查:检查当前字符串是否括号有效
- 层级控制:使用found标志确保只处理当前层
- 生成子串:尝试删除每一个括号生成新字符串
- 去重处理:使用visited集合避免重复计算
3.3 时间复杂度分析
最坏情况下(如全为")"的字符串),需要检查所有可能的子串组合:
- 每层处理时间:O(n × 2^n)
- 空间复杂度:O(2^n)
实际LeetCode测试中,由于存在剪枝和去重,性能比纯暴力法好很多。
4. 进阶优化:DFS与剪枝策略
4.1 先计算需要删除的括号数
我们可以预先扫描字符串,计算出需要删除的多余左括号和右括号数量:
python复制def calculate_remove(s):
l_remove = r_remove = 0
for ch in s:
if ch == '(':
l_remove += 1
elif ch == ')':
if l_remove == 0:
r_remove += 1
else:
l_remove -= 1
return l_remove, r_remove
4.2 带剪枝的DFS实现
python复制def removeInvalidParentheses(s):
l_remove, r_remove = calculate_remove(s)
res = set()
def dfs(index, left, right, l_remain, r_remain, path):
if index == len(s):
if left == right and l_remain == 0 and r_remain == 0:
res.add(''.join(path))
return
ch = s[index]
# 尝试删除当前字符(如果是括号)
if ch == '(' and l_remain > 0:
dfs(index+1, left, right, l_remain-1, r_remain, path)
elif ch == ')' and r_remain > 0:
dfs(index+1, left, right, l_remain, r_remain-1, path)
# 保留当前字符
path.append(ch)
if ch not in '()':
dfs(index+1, left, right, l_remain, r_remain, path)
elif ch == '(':
dfs(index+1, left+1, right, l_remain, r_remain, path)
elif ch == ')' and left > right:
dfs(index+1, left, right+1, l_remain, r_remain, path)
path.pop()
dfs(0, 0, 0, l_remove, r_remove, [])
return list(res) if res else [""]
4.3 剪枝策略解析
- 提前计算删除数量:减少不必要的递归路径
- 有效性剪枝:
- 右括号数量不能超过左括号
- 剩余删除配额必须非负
- 路径记录:使用列表保存当前路径,最后转换为字符串
5. 边界情况处理与测试用例
5.1 常见边界情况
- 空字符串输入:""
- 无括号字符串:"abc"
- 全无效括号:"))(("
- 多重解情况:"()())()"
- 连续相同括号:"((())"
5.2 测试用例设计
python复制test_cases = [
("()())()", ["(())()", "()()()"]),
("(a)())()", ["(a())()", "(a)()()"]),
(")(", [""]),
("n", ["n"]),
("((()", ["()"])
]
for s, expected in test_cases:
result = removeInvalidParentheses(s)
assert sorted(result) == sorted(expected), f"Failed: {s}"
6. 性能对比与选择建议
6.1 方法对比
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| BFS | O(n×2^n) | O(2^n) | 字符串较短时 |
| DFS+剪枝 | O(2^n) | O(n^2) | 字符串较长时 |
6.2 选择建议
- 面试场景:推荐实现BFS版本,逻辑清晰易于解释
- 竞赛场景:使用DFS+剪枝,处理更大输入
- 学习目的:建议两种方法都实现,理解不同思路
7. 常见错误与调试技巧
7.1 典型错误
- 忘记去重:输入"())"可能生成多个相同的"()"
- 层级控制错误:BFS中过早返回会遗漏同层其他解
- 有效性检查不完整:只检查括号数量不检查顺序
7.2 调试技巧
- 打印中间状态:在BFS每层处理时打印队列内容
- 小规模测试:先用简单用例验证基本逻辑
- 边界测试:专门测试全括号、无括号等情况
调试时可以限制递归深度或队列大小,逐步扩大输入规模定位问题。
8. 扩展思考与变种问题
8.1 变种问题
- 只返回一个解:只需找到第一个有效解即可返回
- 统计删除次数:返回最少删除次数而非具体解
- 其他括号类型:处理{}、[]等其他括号组合
8.2 实际应用场景
- 代码编辑器中的括号匹配检查
- 配置文件语法验证
- 自然语言处理中的结构分析
这道题看似简单,但涵盖了DFS/BFS、剪枝、字符串处理等多个重要概念。我在实际面试中多次遇到它的各种变种,深入理解其解法对提升算法能力很有帮助。建议在理解基础解法后,尝试自己实现几个变种问题,这对培养举一反三的能力很有帮助。