想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。
📖 第33课:有效的括号
模块:栈与队列 | 难度:Easy ⭐⭐⭐ LeetCode 链接:leetcode.cn/problems/va… 前置知识:无(栈的入门题) 预计学习时间:15分钟
🎯 题目描述
给定一个只包含 '(', ')', '{', '}', '[', ']' 的字符串 s,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合
- 左括号必须以正确的顺序闭合
- 每个右括号都有一个对应的相同类型的左括号
示例:
输入:s = "()"
输出:true
输入:s = "()[]{}"
输出:true
输入:s = "(]"
输出:false
输入:s = "([])"
输出:true
约束条件:
1 <= s.length <= 10^4s仅由括号'()[]{}'组成
🧪 边界用例(面试必考)
| 用例类型 | 输入 | 期望输出 | 考察点 |
|---|---|---|---|
| 单对括号 | s="()" | true | 基本匹配 |
| 多对括号 | s="()[]{}" | true | 多种括号混合 |
| 嵌套括号 | s="{[]}" | true | 括号嵌套 |
| 类型不匹配 | s="(]" | false | 类型检查 |
| 顺序错误 | s="([)]" | false | 顺序检查 |
| 只有左括号 | s="(((" | false | 缺少右括号 |
| 只有右括号 | s=")))" | false | 缺少左括号 |
| 长度为奇数 | s="(()" | false | 快速判断 |
💡 思路引导
生活化比喻
想象你在检查一段代码的括号是否匹配...
🐌 笨办法:每次找到一对匹配的括号就删除,重复这个过程。比如
"{[]}"→ 先删[]得"{}"→ 再删{}得空字符串。但这需要反复遍历字符串,效率低。🚀 聪明办法:用一个栈(像一摞盘子):
- 遇到左括号就放到盘子堆顶(入栈)
- 遇到右括号就检查盘子堆顶是否是对应的左括号:
- 如果匹配,拿走堆顶的盘子(出栈)
- 如果不匹配,或者堆是空的,说明括号无效
- 最后检查盘子堆是否全部拿走(栈为空)
只需要遍历一次字符串,O(n) 时间!
关键洞察
括号匹配的本质是"最近的左括号优先匹配",这正是栈的 LIFO(后进先出)特性!
🧠 解题思维链
这一节模拟你在面试中"从零开始思考"的过程。
Step 1:理解题目 → 锁定输入输出
- 输入:只包含括号的字符串
s - 输出:布尔值,表示括号是否有效匹配
- 限制:只有 6 种字符,长度最多 10^4
Step 2:先想笨办法(暴力法)
最直接的思路:
- 在字符串中查找一对相邻的匹配括号(如
(),[],{}) - 删除这对括号,得到新字符串
- 重复步骤 1-2,直到无法找到匹配的括号
- 检查最终字符串是否为空
- 时间复杂度:O(n²) — 最坏情况下每次删除后要重新扫描
- 空间复杂度:O(n) — 需要创建新字符串
- 瓶颈在哪:每次删除后要重新扫描,重复操作太多
Step 3:瓶颈分析 → 优化方向
分析暴力法的核心问题:
- 核心问题:如何高效地"配对最近的左括号"?
- 关键观察:当遇到右括号时,它应该匹配最近的未匹配的左括号
- 数据结构选择:"最近"→ 后进先出 → 栈
Step 4:选择武器
- 选用:栈(Stack)
- 理由:
- 遇到左括号就入栈(等待被匹配)
- 遇到右括号就检查栈顶是否是对应的左括号
- 如果匹配就出栈,如果不匹配或栈空就返回 False
- 最后检查栈是否为空(所有左括号都被匹配)
🔑 模式识别提示:当题目涉及配对、嵌套、最近匹配等关键词,优先考虑"栈"
🔑 解法一:栈匹配(标准解法)
思路
用栈存储所有遇到的左括号,遇到右括号时检查栈顶是否匹配。
图解过程
示例: s = "{[]}"
初始: 栈 = []
Step 1: 遍历到 '{'(左括号)
入栈: 栈 = ['{']
Step 2: 遍历到 '['(左括号)
入栈: 栈 = ['{', '[']
Step 3: 遍历到 ']'(右括号)
检查栈顶: 栈顶是 '[',匹配 ']'
出栈: 栈 = ['{']
Step 4: 遍历到 '}'(右括号)
检查栈顶: 栈顶是 '{',匹配 '}'
出栈: 栈 = []
Step 5: 遍历结束,检查栈是否为空
栈为空 → 返回 True
错误示例演示:s = "([)]"
初始: 栈 = []
Step 1: '(' → 栈 = ['(']
Step 2: '[' → 栈 = ['(', '[']
Step 3: ')' → 栈顶是 '[', 不匹配 ')'
返回 False
Python代码
def isValid(s: str) -> bool:
"""
解法一:栈匹配
思路:左括号入栈,右括号检查栈顶是否匹配
"""
# 快速判断:长度为奇数必定无效
if len(s) % 2 == 1:
return False
# 括号映射表:右括号 -> 左括号
pairs = {
')': '(',
']': '[',
'}': '{'
}
stack = []
for char in s:
if char in pairs: # 遇到右括号
# 检查栈是否为空,或栈顶是否匹配
if not stack or stack[-1] != pairs[char]:
return False
stack.pop() # 匹配,出栈
else: # 遇到左括号
stack.append(char)
# 最后检查栈是否为空(所有括号都匹配)
return len(stack) == 0
# ✅ 测试
print(isValid("()")) # 期望输出:True
print(isValid("()[]{}")) # 期望输出:True
print(isValid("(]")) # 期望输出:False
print(isValid("([)]")) # 期望输出:False
print(isValid("{[]}")) # 期望输出:True
复杂度分析
- 时间复杂度:O(n) — 遍历字符串一次,每个字符入栈/出栈一次,均为 O(1) 操作
- 具体地说:如果字符串有 1000 个字符,需要约 1000 次入栈/出栈操作
- 空间复杂度:O(n) — 最坏情况下(全是左括号),栈中存储 n 个字符
优缺点
- ✅ 时间 O(n) 最优,一次遍历解决
- ✅ 逻辑清晰,易于理解和实现
- ✅ 可扩展(支持更多类型的括号)
⚡ 解法二:计数法(仅限单一括号类型)
优化思路
注意:这个方法只适用于单一类型括号(如只有 ()),不适用于本题的混合括号。
如果只有 ():
- 用一个计数器,遇到
(就 +1,遇到)就 -1 - 如果计数器变成负数,说明右括号多于左括号,返回 False
- 最后检查计数器是否为 0
def isValid_single_type(s: str) -> bool:
"""
仅适用于单一类型括号(如只有 ())
"""
count = 0
for char in s:
if char == '(':
count += 1
elif char == ')':
count -= 1
if count < 0:
return False
return count == 0
为什么不适用于本题?
因为 "([)]" 用计数法:
(: count1=1[: count2=1): count1=0 ✅]: count2=0 ✅- 最后都是 0,但实际上是无效的(顺序错误)
所以混合括号必须用栈,记录括号的类型和顺序。
🐍 Pythonic 写法
栈匹配的解法已经很简洁,可以用 Python 的特性进一步优化:
def isValid_pythonic(s: str) -> bool:
"""
Pythonic 写法:更简洁的栈匹配
"""
if len(s) % 2: # 长度为奇数
return False
pairs = {')': '(', ']': '[', '}': '{'}
stack = []
for char in s:
if char in pairs: # 右括号
if not stack or stack.pop() != pairs[char]:
return False
else: # 左括号
stack.append(char)
return not stack # 等价于 len(stack) == 0
这个写法的亮点:
- 用
if len(s) % 2:简洁判断奇数 - 用
stack.pop()一步完成出栈和获取值 - 用
not stack代替len(stack) == 0(更 Pythonic)
⚠️ 面试建议:解法一已经很清晰,面试时建议用清晰版本,不要过度追求简洁导致可读性下降。
📊 解法对比
| 维度 | 解法一:栈匹配 | 解法二:计数法 |
|---|---|---|
| 时间复杂度 | O(n) | O(n) |
| 空间复杂度 | O(n) | O(1) |
| 适用场景 | 混合括号 | 仅单一类型 |
| 代码难度 | 简单 | 极简单 |
| 面试推荐 | ⭐⭐⭐ | ⭐(不适用本题) |
面试建议:
- 直接讲解法一的栈匹配,这是标准且唯一正确的解法
- 可以提到"如果只有单一类型括号,可以用计数法 O(1) 空间",展示你的思考深度
- 重点强调:栈是处理括号匹配、嵌套结构的经典工具
🎤 面试现场
模拟面试中的完整对话流程,帮你练习"边想边说"。
面试官:请你判断一个字符串的括号是否有效匹配。
你:(审题30秒)好的,这道题要求判断括号是否有效,需要满足左右配对、类型一致、顺序正确。
我的思路是用栈:
- 遍历字符串,遇到左括号就入栈
- 遇到右括号就检查栈顶是否是对应的左括号
- 如果匹配就出栈
- 如果不匹配或栈空就返回 False
- 最后检查栈是否为空
这样只需要一次遍历,时间 O(n),空间 O(n)。
面试官:为什么用栈?
你:因为括号匹配的关键是"最近的左括号优先匹配"。
比如 "{[]}":
- 遇到
]时,应该匹配最近的[,而不是更早的{ - 这正是**栈的后进先出(LIFO)**特性:最后入栈的左括号最先被匹配
如果用其他数据结构:
- 数组需要记录位置,O(n²) 查找
- 队列是先进先出,无法匹配最近的
所以栈是最合适的。
面试官:能否优化空间?
你:对于混合类型括号,必须用栈记录括号的类型和顺序,无法优化到 O(1)。
但如果只有单一类型括号(如只有 ()),可以用计数法:
- 遇到
(计数 +1,遇到)计数 -1 - 如果计数变负数或最后不为 0,返回 False
- 这样是 O(1) 空间
但本题有三种括号混合,必须区分类型,所以必须用栈。
面试官:测试一下边界情况。
你:
- 长度为奇数:直接返回 False(无法完全配对)
- 只有左括号
"(((": 栈非空,返回 False - 只有右括号
")))":栈空时遇到右括号,返回 False - 顺序错误
"([)]":栈顶[不匹配),返回 False
都正确。
高频追问
| 追问 | 应答策略 |
|---|---|
| "如果有多种括号和其他字符怎么办?" | 在遍历时跳过非括号字符,只处理括号。可以在 if 判断中加 if char in '()[]{}' |
| "如果要返回第一个不匹配的位置?" | 在遍历时记录当前索引 i,遇到不匹配时返回 i。需要修改返回值类型 |
| "能否用递归实现?" | 理论上可以,但递归栈深度可能达到 O(n),而且逻辑复杂,不如迭代清晰 |
| "实际工程中有什么应用?" | 1)编译器的语法检查 2)文本编辑器的括号高亮 3)正则表达式引擎的括号匹配 |
🎓 知识点总结
Python技巧卡片 🐍
# 技巧1: 使用字典映射右括号到左括号
pairs = {')': '(', ']': '[', '}': '{'}
if char in pairs: # 判断是否是右括号
if stack[-1] == pairs[char]: # 检查栈顶是否匹配
# 技巧2: list 作为栈使用
stack = []
stack.append(item) # 入栈(添加到末尾)
top = stack[-1] # 查看栈顶(不弹出)
item = stack.pop() # 出栈(弹出末尾)
is_empty = not stack # 判断栈是否为空(Pythonic)
# 技巧3: 快速判断奇数
if len(s) % 2 == 1: # 或 if len(s) % 2:
return False
# 技巧4: 字符串成员检查(O(1))
if char in '()[]{}': # 判断字符是否是括号
...
💡 底层原理(选读)
栈的底层实现
Python 的
list可以当栈使用:
append(x): 在列表末尾添加元素,O(1) 平摊时间pop(): 删除并返回列表末尾元素,O(1) 时间[-1]: 访问列表末尾元素,O(1) 时间为什么
append是 O(1) 平摊时间?
- Python list 内部是动态数组,有预留容量
- 大部分
append是 O(1)(容量够用)- 偶尔需要扩容(容量不够),会重新分配更大的数组并复制,O(n)
- 平摊下来每次
append是 O(1)为什么不用 deque?
Python 的
collections.deque是双端队列,也可以当栈:from collections import deque stack = deque() stack.append(x) # 入栈 stack.pop() # 出栈对于本题,
list和deque性能差不多,但list更简单直接。deque的优势在于头部操作也是 O(1),而list的头部操作是 O(n)。括号匹配的编译原理应用
编译器在词法分析和语法分析阶段大量使用栈:
- 括号匹配:检查代码的括号、花括号、方括号是否配对
- 表达式求值:
3 + 5 * 2需要用栈处理运算符优先级- 函数调用栈:每次函数调用压栈,返回时弹栈
例如 Python 解释器在执行前会检查括号匹配,就是用栈实现的。
算法模式卡片 📐
- 模式名称:栈匹配(配对问题)
- 适用条件:涉及配对、嵌套、消除、最近匹配等问题
- 识别关键词:"括号匹配"、"配对"、"消除相邻"、"嵌套结构"
- 核心思路:用栈的 LIFO 特性,遇到开始符号入栈,遇到结束符号检查栈顶是否匹配
- 模板代码:
def is_valid_brackets(s: str) -> bool:
"""栈匹配模板"""
pairs = {')': '(', ']': '[', '}': '{'} # 右->左映射
stack = []
for char in s:
if char in pairs: # 遇到结束符号
if not stack or stack.pop() != pairs[char]:
return False
else: # 遇到开始符号
stack.append(char)
return not stack
变体问题:
- 括号匹配 → 右括号检查栈顶
- 删除重复字母 → 维护递增栈
- 每日温度 → 单调栈
- 表达式求值 → 运算符栈 + 操作数栈
易错点 ⚠️
-
忘记检查栈是否为空
- ❌ 错误:遇到右括号直接
stack.pop()或stack[-1],栈空时报错 - ✅ 正确:先检查
if not stack再操作 - 示例:输入
"))",第一个)时栈空,需要返回 False
- ❌ 错误:遇到右括号直接
-
最后忘记检查栈是否为空
- ❌ 错误:遍历结束直接返回 True
- ✅ 正确:
return not stack或len(stack) == 0 - 示例:输入
"(((",遍历结束但栈非空,应该返回 False
-
映射表键值颠倒
- ❌ 错误:
pairs = {'(': ')', ...}左括号映射右括号 - ✅ 正确:
pairs = {')': '(', ...}右括号映射左括号 - 原因:我们是用右括号去查找对应的左括号,所以右括号是键
- ❌ 错误:
-
弹出栈顶后没有检查值
- ❌ 错误:
stack.pop(); if stack[-1] == pairs[char]先弹出再检查(逻辑错) - ✅ 正确:
if stack[-1] == pairs[char]: stack.pop()先检查再弹出 - 或者:
if stack.pop() != pairs[char]: return False弹出同时检查
- ❌ 错误:
🏗️ 工程实战(选读)
这个算法思想在真实项目中的应用,让你知道"学了有什么用"。
-
场景1:代码编辑器的括号高亮 VSCode、PyCharm 等编辑器在你输入代码时,会实时检查括号匹配,并高亮配对的括号。底层就是用栈实现的括号匹配算法,时间复杂度 O(n),可以做到实时响应。
-
场景2:编译器的语法检查 C/C++、Java 编译器在编译前会检查代码的语法,其中括号匹配是第一步。编译器用栈检查所有的
(,{,[是否配对,不配对会报 "unexpected token" 错误。 -
场景3:Markdown 解析器 Markdown 的链接语法
[文本](URL)需要检查方括号和圆括号的配对。解析器用栈匹配[和],(和),确保语法正确后才渲染成超链接。
🏋️ 举一反三
完成本课后,试试这些同类题目来巩固知识:
| 题目 | 难度 | 相关知识点 | 提示 |
|---|---|---|---|
| LeetCode 22. 括号生成 | Medium | 回溯 + 栈思想 | 生成所有有效括号组合 |
| LeetCode 32. 最长有效括号 | Hard | 栈 + DP | 找最长的有效括号子串 |
| LeetCode 1021. 删除最外层的括号 | Easy | 栈计数 | 每个有效括号去掉最外层 |
| LeetCode 856. 括号的分数 | Medium | 栈 | 括号匹配 + 分数计算 |
📝 课后小测
试试这道变体题,不要看答案,自己先想5分钟!
题目:给定一个字符串,删除字符串中所有相邻且相同的字符,返回最终字符串。例如 "abbaca" → "ca"(删除 bb 得 "aaca",再删除 aa 得 "ca")。
💡 提示(实在想不出来再点开)
类似括号匹配,用栈存储字符,遇到与栈顶相同的字符就弹出(消除),否则入栈。
✅ 参考答案
def removeDuplicates(s: str) -> str:
"""
删除相邻重复字符(类似括号匹配)
"""
stack = []
for char in s:
if stack and stack[-1] == char:
stack.pop() # 与栈顶相同,消除
else:
stack.append(char) # 不同,入栈
return ''.join(stack)
# 测试
print(removeDuplicates("abbaca")) # 输出: "ca"
print(removeDuplicates("azxxzy")) # 输出: "ay"
核心思路:
- 与括号匹配类似,只是匹配条件从"左右配对"变成"相邻相同"
- 用栈存储字符,遇到与栈顶相同的字符就消除(弹栈)
- 最后栈中剩余的字符就是结果
- 时间 O(n),空间 O(n)
进阶变体:
- LeetCode 1047. 删除字符串中的所有相邻重复项(本题)
- LeetCode 1209. 删除字符串中的所有相邻重复项 II(删除 k 个相邻相同)
如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。