20. 有效的括号
题目描述
给定一个只包括 '('、')'、'{'、'}'、'['、']' 的字符串 s,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例:
- 输入:
s = "()"→ 输出:true - 输入:
s = "()[]{}"→ 输出:true - 输入:
s = "(]"→ 输出:false - 输入:
s = "([)]"→ 输出:false - 输入:
s = "{[]}"→ 输出:true
解题思路
第一步:理解问题本质
括号匹配有一个关键性质:最近遇到的未闭合左括号,必须最先被闭合。
用一个具体的例子来说明:{[()]}
- 遇到
{,它等待被}闭合。 - 遇到
[,它比{更晚出现,必须先于{被闭合。 - 遇到
(,它比[更晚出现,必须先于[被闭合。 - 遇到
),正好闭合最近的(,合法。 - 遇到
],正好闭合最近的[,合法。 - 遇到
},正好闭合最近的{,合法。
这种"后进先出"的结构,正是栈的天然应用场景。
第二步:暴力解法
反复扫描字符串,每次找到相邻的合法括号对(如 ()、[]、{})并删除,直到字符串为空(合法)或无法继续删除(非法)。
class Solution:
def isValid(self, s: str) -> bool:
while "()" in s or "[]" in s or "{}" in s:
s = s.replace("()", "").replace("[]", "").replace("{}", "")
return s == ""
时间复杂度:O(n²),每次替换最多删除一对,最多做 n/2 次,每次字符串操作 O(n)。
为何不够好:字符串替换操作开销大,而且每轮只能处理最内层的括号对,效率低。使用栈可以一次遍历完成,达到 O(n)。
第三步:最优解法(栈)
核心思路:
- 遇到左括号(
(、[、{):将其压入栈,等待对应的右括号来闭合它。 - 遇到右括号(
)、]、}):检查栈顶是否是对应的左括号。- 若栈为空:没有待匹配的左括号,非法,返回
False。 - 若栈顶不匹配:类型不对应,非法,返回
False。 - 若栈顶匹配:弹出栈顶,继续处理下一个字符。
- 若栈为空:没有待匹配的左括号,非法,返回
- 遍历结束后:若栈为空,说明所有左括号都已被正确闭合,返回
True;若栈非空,说明有未闭合的左括号,返回False。
哈希表的作用:用 pair_map = {')': '(', ']': '[', '}': '{'} 将每个右括号映射到其对应的左括号,使得匹配判断只需一行代码,避免三个 if-else 分支。
class Solution:
def isValid(self, s: str) -> bool:
stack = []
pair_map = {')': '(', ']': '[', '}': '{'}
for char in s:
if char in '([{':
stack.append(char)
else:
if not stack or stack.pop() != pair_map[char]:
return False
return len(stack) == 0
完整代码实现
class Solution:
def isValid(self, s: str) -> bool:
stack = []
pair_map = {')': '(', ']': '[', '}': '{'}
for char in s:
if char in '([{': # 左括号,压栈
stack.append(char)
else: # 右括号,检查栈顶
if not stack or stack.pop() != pair_map[char]:
return False # 栈空或类型不匹配
return len(stack) == 0 # 栈为空才说明全部合法闭合
示例推演
以 s = "({[]})" 为例,期望输出 True。
| 步骤 | 字符 | 操作 | 栈状态(栈底→栈顶) |
|---|---|---|---|
| 1 | ( | 左括号,压栈 | ['('] |
| 2 | { | 左括号,压栈 | ['(', '{'] |
| 3 | [ | 左括号,压栈 | ['(', '{', '['] |
| 4 | ] | 右括号,pair_map[']']='[',弹出栈顶 '[',匹配 | ['(', '{'] |
| 5 | } | 右括号,pair_map['}']='{',弹出栈顶 '{',匹配 | ['('] |
| 6 | ) | 右括号,pair_map[')']='(',弹出栈顶 '(',匹配 | [] |
遍历结束,栈为空,返回 True。
再以 s = "([)]" 为例,期望输出 False。
| 步骤 | 字符 | 操作 | 栈状态 |
|---|---|---|---|
| 1 | ( | 压栈 | ['('] |
| 2 | [ | 压栈 | ['(', '['] |
| 3 | ) | pair_map[')']='(',弹出栈顶 '[','[' != '(',不匹配! | — |
立刻返回 False,正确。
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 暴力(反复替换) | O(n²) | O(n) | 每轮替换 O(n),最多 n/2 轮 |
| 最优(栈) | O(n) | O(n) | 每个字符最多入栈出栈各一次 |
栈的空间最坏情况下(全是左括号时)存储 n 个元素,空间复杂度为 O(n)。
易错点总结
- 先判断栈空再弹出:遇到右括号时,必须先检查
not stack,再执行stack.pop()。若先 pop 后比较,空栈会抛出IndexError。代码中用not stack or stack.pop() != pair_map[char]利用短路求值,not stack为真时不会执行后面的 pop。 - 遍历结束后必须检查栈是否为空:即使遍历中没有触发
return False,也可能存在未闭合的左括号留在栈中(如s = "(("这种情况),必须用return len(stack) == 0来最终判断。 - 奇数长度字符串必然非法:可以在开头加
if len(s) % 2 == 1: return False提前剪枝,但题目约束下非必须。
扩展思考
- 括号生成(LeetCode 22):反向问题,给定 n 对括号,生成所有合法组合,同样用栈(或递归)的思路来确保括号合法性。
- 最长有效括号(LeetCode 32):在括号序列中找最长的合法子串,也是栈的经典应用,难度升级为困难。
- 栈的本质:栈处理的是"对称嵌套"结构——任何具有"后出现的需要先结束"特性的问题,都是栈的适用场景。括号匹配是最典型的例子,其他如函数调用栈、HTML 标签匹配等都遵循相同原理。