☰
栈解“有效的括号”:从LeetCode刷题到面试手写
2026/10/17 14:55:03 网站建设 项目流程

做LeetCode刷题的人应该都遇到过“有效的括号”这道题,它排在栈专题的前几名,看起来简单到像是送分题,但你真拿笔在纸上一步步推演、或者面试时突然被要求手写,很容易暴露出很多隐藏问题。它要求我们判断一个只包含(、)、{、}、[、]的字符串是否满足两个条件:左括号必须用相同类型的右括号闭合,并且闭合顺序要正确。()[]{}是有效的,([)]看起来左右数量对得上,却是无效的。我先说结论:这道题的标准解法是用栈,遍历字符串,左括号入栈,遇到右括号就弹出栈顶元素比对类型,最后栈空才有效。但真正把这道题吃透,至少还要搞清楚四个问题:为什么是栈、边界条件有哪些、复杂度能不能优化、同类题怎么延伸。这篇文章我会按照自己实际刷题的思路完整拆一遍,代码用 Python,但逻辑不绑定语言。

1. 先拆题:有效括号到底在验证什么

1.1 题目约束与有效规则的本质

题目给出的字符串长度有限,通常认为最大长度在 10^4 级别,所以一个 O(n) 的解法是肯定可以的。有效规则其实可以拆成两条:

  • 每个右括号都要找到一个“最近的、还没被匹配的”同类型左括号。
  • 所有左括号都要在字符串结束前被匹配完。

这两条规则本质上是“后进先出”的顺序。举个例子,{[]}这个字符串从左往右看,遇到{后遇到[,然后遇到],此时能匹配的是最近的左括号[,而不是远处的{;接着遇到},再匹配{。这个顺序就像叠盘子,先放上去的盘子最后才能取走。如果改成{[}],遇到第一个}时,栈顶却是[,类型对不上,整个字符串就无效。

1.2 为什么栈是天然适配的数据结构

很多初学者会问:能不能用计数器?如果只有一种括号,确实可以,遇到左括号加一,遇到右括号减一,最后看是不是零。但题目给了三种括号,单靠计数无法区分([)]这种交错情况的错误,因为它的左括号数量和右括号数量相等,计数结果为零,但顺序是错的。

栈恰好保存了左括号出现的顺序,每次只关心最近一次出现的左括号。这种“最近匹配”逻辑和栈的 LIFO(后进先出)完全一致。编译器在解析表达式、IDE 在做括号高亮时,底层也是同样的思路。你可以把栈想象成一个只能从顶部进出的篮子,左括号放进去,遇到右括号时就从篮子里取出最上面的那个左括号比对。取出的如果类型不匹配,或者篮子已经空了,说明这个右括号多余或顺序错误。

1.3 常见的错误理解:只数数量不辨顺序

我见过有人拿到题第一反应是统计六种字符的数量,然后判断左括号总数是否等于右括号总数,再分别判断每种括号的数量是否相等。这个思路作为第一层过滤没问题,但它绝对不是充分条件。[(])每种括号数量都相等,却无效。还有人对“闭合顺序”理解成“只要两种括号不相交叉就行”,但(([]))是嵌套,([)]是交叉,交叉一定无效,嵌套可以。栈解法不用刻意区分嵌套和交叉,它每次都检查栈顶,交叉场景必然会在某个右括号处发现栈顶类型不匹配。

2. 栈解法的完整代码与逐行拆解

2.1 最直接的实现方式

先写一版入门级代码,只依赖列表的append和pop:

def isValid(s: str) -> bool: stack = [] for ch in s: if ch in "({[": stack.append(ch) else: if not stack: return False left = stack.pop() if (left == "(" and ch != ")") or \ (left == "{" and ch != "}") or \ (left == "[" and ch != "]"): return False return len(stack) == 0

这段代码逻辑很直白:遇到左括号压栈,遇到右括号先检查栈是否为空,为空说明右括号多余;不为空就弹出栈顶左括号,用if判断三个配对条件。遍历结束后,如果栈里还有残留左括号,说明这些左括号没有对应的右括号,同样无效。

2.2 用哈希表替代一堆 if 判断

三个if不算多,但如果括号类型增加到四五种,代码会变得很啰嗦。更工程化的写法是用字典维护配对关系:

def isValid(s: str) -> bool: pair = {")": "(", "}": "{", "]": "["} stack = [] for ch in s: if ch in pair: if not stack or stack[-1] != pair[ch]: return False stack.pop() else: stack.append(ch) return not stack

这里pair的 key 是右括号,value 是对应的左括号。遍历时如果当前字符是右括号,就检查栈顶是否为对应的左括号。注意stack[-1]在栈空时会抛异常,所以先用not stack判断。把ch in pair作为判断条件,左括号自然全部走else分支入栈,代码可读性比连续if好很多。

2.3 手动走一遍执行流程

拿s = "([{}])"演示:

  1. 读入(,是左括号,入栈。栈:['(']
  2. 读入[,入栈。栈:['(', '[']
  3. 读入{,入栈。栈:['(', '[', '{']
  4. 读入},是右括号,栈顶是{,匹配,弹出。栈:['(', '[']
  5. 读入],栈顶是[,匹配,弹出。栈:['(']
  6. 读入),栈顶是(,匹配,弹出。栈:[]
  7. 遍历结束,栈空,返回 True。

再来一个错误示例s = "(]":

  1. 读入(,入栈。
  2. 读入],是右括号,栈顶是(,但pair[']']是[,不等于(,返回 False。

2.4 刷题之外的工程化思考

如果你在公司里写代码,遇到这种括号匹配需求,可能还会考虑:字符串里混有普通字符怎么办、嵌套层级过深导致递归爆栈怎么办、用栈能不能做到流式判断。流式判断就是边读边处理,不需要一次性载入整个字符串,这在解析大文件时很有用。栈解法天然支持流式处理,这也是它比“先统计所有字符再处理”更实用的原因之一。

3. 边界条件与易错点排查:刷题翻车实录

3.1 空字符串算不算有效

按 LeetCode 的定义,空字符串被认为是有效的括号字符串。很多人在一开始会漏掉这个,直接把return len(stack) == 0写上,其实已经处理了空串,因为空串时栈本来就是空的,返回 True。如果你在一开始写了if len(s) == 0: return False,那就错了。我见过不少同学因为题目没明说,就被这题摆了一道。

3.2 第一个字符就是右括号

")("这种字符串,第一轮遍历遇到)时,栈是空的,直接返回 False。这个逻辑在代码里已经体现:if not stack or stack[-1] != pair[ch]。很多人容易踩的坑是先写stack[-1]再检查栈空,导致IndexError。正确的顺序永远是“先判空,再取栈顶”。

3.3 左右数量相同但括号交错

([)]的完整判断过程是这样的:

  1. 读入(,入栈。
  2. 读入[,入栈。
  3. 读入),栈顶是[,pair[')']是(,两者不匹配,返回 False。

即使后面还有(和],算法已经提前终止。这说明栈解法不仅能判断“完整性”,也能判断“顺序性”。提前终止是性能优势,也符合短路判断的直觉。

3.4 遍历结束后栈里还有残留

"(()"这种字符串,读到最后一个)时,栈顶是(,匹配弹出,此时栈里还剩一个(,遍历结束栈不为空,返回 False。这个场景最容易被遗漏,因为不少人只关注遍历过程中的匹配关系,忘记最终收尾检查。

3.5 题面没提但实际可能出现的字符

LeetCode 这题已经限定只有六种括号,但如果你把代码拿到生产环境去改,很可能遇到空格、字母、中文括号甚至换行符。建议先过滤无用字符,或者明确约定输入格式。我曾经在某个解析器里复用这道题的逻辑,结果用户输入了全角括号(,直接被当作普通字符忽略,导致校验失灵。所以真实项目里要先做白名单校验。

4. 复杂度分析:为什么标准解法就是最优解

4.1 时间和空间复杂度

标准栈解法的时间复杂度是 O(n),因为每个字符恰好入栈一次、出栈一次(左括号入栈,右括号触发弹栈),主要动作都是 O(1) 的列表尾部操作。空间复杂度最坏是 O(n),当字符串全是左括号时,所有字符都要进栈。

4.2 能不能把空间压到 O(1)

一个常见的贪心问题是:只用三个计数器分别记录三种括号的出现次数,能不能判断?答案是不能,因为计数器丢失了顺序信息。我用一个反例:([)],三种括号各自数量相等,计数器全部归零,但它无效。所以对于三种括号并存的一般情况,O(n) 空间是必要的,这不是解法不够好,而是问题本身需要记录历史顺序。

那如果只考虑一种括号呢?确实可以用一个整数变量代替栈,遇到左括号加一,遇到右括号减一,中途小于零直接失败,最后等于零成功。这在面试里可以作为“问题简化”的讨论点:当括号类型从三种变成一种,数据结构可以从栈退化成计数器。但题目明确是三种,所以标准答案还是栈。

4.3 如果试图用字符串替换做这件事

有些新手会想到循环把"()"、"[]"、"{}"替换成空字符串,最后看结果是否为空。这个思路在概念上没问题,但最坏时间复杂度很高,比如"(((((((((())))))))))"这种深度嵌套,每轮替换只能消掉最中间的一对,总共要执行 O(n) 轮替换,每次替换又是 O(n) 的字符串复制,整体 O(n^2)。LeetCode 上字符串长度如果达到 10^4,这个做法会超时或非常慢。栈解法则是标准的 O(n),没有这个问题。

4.4 什么时候需要额外思考内存优化

如果括号字符串是几百万字符的长文本,栈的最大深度可能接近字符串长度,内存占用不容忽视。但可以换个角度:很多情况下括号嵌套深度是有限的,栈的深度不会超过最大嵌套深度而不是字符串长度。你可以在入栈前检查栈深度是否超过某个业务阈值,超过就认为非法,这样既能防止内存暴涨,也能提前拦截明显恶意的输入。

5. 从这道题延伸开的同类问题

5.1 括号生成:从验证到构造

LeetCode 第 22 题要求生成所有有效的括号组合,输入是n对括号,输出所有合法序列。这道题从“验证”变成了“构造”,但核心判断逻辑几乎一样。通常用回溯法,在递归过程中维护左括号剩余数量和右括号剩余数量,只有当前右括号剩余数大于左括号剩余数时才允许放右括号,这样生成的序列一定有效。你做完“有效的括号”后,应该立刻去试这题,感受从“检查器”到“生成器”的思路迁移。

5.2 最长有效括号:从合法性到连续性

第 32 题是高频难题:求一个只包含左右括号的字符串中最长有效括号子串的长度。比如")()())"的最长有效长度是 4。这道题用栈时,栈里存的不是括号本身,而是下标,并且初始要把-1放进去作为基准点。遇到)时弹出栈顶,然后计算当前下标和栈顶下标的差值,这个差就是当前连续有效的长度。它和“有效的括号”题的关系在于:判断有效括号的位置是连续的,而“连续”需要用下标差值来量化。做完基础题后再去刷新题,对栈的理解会完全不同。

5.3 带通配符的括号匹配:从确定性到贪心

有些扩展题会让字符串里出现*,它可以被当作左括号、右括号或者空字符,问是否能形成有效括号序列。这种问题可以引入两个栈分别记录左括号和星号的下标,也可以使用贪心算法维护一个未匹配左括号的数量区间。它不再要求严格的最近匹配,因为*给了你“变通”的可能。做这道题时,你会重新反思标准栈解法里“必须弹出栈顶”的规则,原来只是多种可能性中最严格的一种。

5.4 栈在解析场景中的迁移

括号匹配只是栈应用的一个缩影。LeetCode 第 71 题“简化路径”也会用到栈:把路径按/切分,遇到..弹出栈顶,遇到普通目录名入栈,最后拼接返回。核心还是“处理当前项时要回顾最近的一项”。你如果能把 20、22、32、71 这四题串起来看,会发现它们都在处理同一个母题:如何用栈维护“最近上下文”。

6. 从能 AC 到面试能讲:我的刷题心得与练习顺序

6.1 刷题过程中最容易写的三个 Bug

第一个 Bug 是忘记最后检查栈空,只判断遍历过程不报错就返回 True,导致"((("被误判为有效。第二个 Bug 是遇到右括号时先return False再弹出栈顶,导致左右数量匹配但顺序正确的字符串也失败。第三个 Bug 是stack[-1]在栈空时取不到元素,Python 会抛IndexError,有些语言返回未定义行为。这三个问题在面试手写时特别容易暴露,因为面试官会不断给你边界用例。

排查技巧很简单:写完后用四组用例自测。

  • ""应返回 True。
  • "]"应返回 False。
  • "(("应返回 False。
  • "([)]"应返回 False。

如果代码能一次性通过这四种基本用例,再跑 LeetCode 的提交用例就稳了。

6.2 从写对代码到讲清楚思路

面试时不要上来就写代码。我一般会先说:“这个问题的核心是括号的匹配具有后进先出特性,所以我会用栈。遇到左括号入栈,遇到右括号检查栈顶,如果类型不匹配或栈为空则无效,遍历完毕后栈必须为空。”这个回答把数据结构选择和算法流程一次说清,比闷头写代码加分很多。

之后如果面试官追问“为什么不用计数器”,你可以回答“计数器无法区分括号类型之间的顺序关系,比如([)]会让计数器归零但实际无效”。这个追问几乎是必问,提前准备就不会慌。

6.3 练习路径建议

如果你刚开始刷题,我建议按这个顺序来:

  1. 第 20 题“有效的括号”:打底,理解栈的入栈、弹栈、栈顶判断。
  2. 第 22 题“括号生成”:加深对括号序列结构的理解,练习回溯。
  3. 第 32 题“最长有效括号”:练习栈存下标、动态规划两种思路,有一定难度。
  4. 第 71 题“简化路径”:换一个场景用栈,确认自己不是只会背括号题。

每道题之间间隔一天,不要同一天连做四题,否则容易混淆。做完每题后,尝试不看代码再写一遍,直到能在五分钟左右流畅完成。

6.4 关于这道题,我最后想多说一句

这题我已经写了不下二十遍,但每次讲给不同水平的人听,都会发现新的表达方式。给初学者讲,我会把栈比喻成“一叠盘子”,最近的盘子必须先拿走;给工作多年的工程师讲,我会直接说“这就是编译原理里的词法分析基础”。你掌握的程度,取决于你能用多少种方式解释它。如果你觉得已经会了,不妨尝试不借助任何变量名提示,只凭脑中推演完成代码,这个训练对面试手写帮助很大。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询