📖 第33课:有效的括号

3 阅读14分钟

想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。

📖 第33课:有效的括号

模块:栈与队列 | 难度:Easy ⭐⭐⭐ LeetCode 链接leetcode.cn/problems/va… 前置知识:无(栈的入门题) 预计学习时间:15分钟


🎯 题目描述

给定一个只包含 '(', ')', '{', '}', '[', ']' 的字符串 s,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合
  2. 左括号必须以正确的顺序闭合
  3. 每个右括号都有一个对应的相同类型的左括号

示例:

输入:s = "()"
输出:true
输入:s = "()[]{}"
输出:true
输入:s = "(]"
输出:false
输入:s = "([])"
输出:true

约束条件:

  • 1 <= s.length <= 10^4
  • s 仅由括号 '()[]{}' 组成

🧪 边界用例(面试必考)

用例类型输入期望输出考察点
单对括号s="()"true基本匹配
多对括号s="()[]{}"true多种括号混合
嵌套括号s="{[]}"true括号嵌套
类型不匹配s="(]"false类型检查
顺序错误s="([)]"false顺序检查
只有左括号s="((("false缺少右括号
只有右括号s=")))"false缺少左括号
长度为奇数s="(()"false快速判断

💡 思路引导

生活化比喻

想象你在检查一段代码的括号是否匹配...

🐌 笨办法:每次找到一对匹配的括号就删除,重复这个过程。比如 "{[]}" → 先删 []"{}" → 再删 {} 得空字符串。但这需要反复遍历字符串,效率低。

🚀 聪明办法:用一个(像一摞盘子):

  1. 遇到左括号就放到盘子堆顶(入栈)
  2. 遇到右括号就检查盘子堆顶是否是对应的左括号:
    • 如果匹配,拿走堆顶的盘子(出栈)
    • 如果不匹配,或者堆是空的,说明括号无效
  3. 最后检查盘子堆是否全部拿走(栈为空)

只需要遍历一次字符串,O(n) 时间!

关键洞察

括号匹配的本质是"最近的左括号优先匹配",这正是栈的 LIFO(后进先出)特性!


🧠 解题思维链

这一节模拟你在面试中"从零开始思考"的过程。

Step 1:理解题目 → 锁定输入输出

  • 输入:只包含括号的字符串 s
  • 输出:布尔值,表示括号是否有效匹配
  • 限制:只有 6 种字符,长度最多 10^4

Step 2:先想笨办法(暴力法)

最直接的思路:

  1. 在字符串中查找一对相邻的匹配括号(如 (), [], {}
  2. 删除这对括号,得到新字符串
  3. 重复步骤 1-2,直到无法找到匹配的括号
  4. 检查最终字符串是否为空
  • 时间复杂度:O(n²) — 最坏情况下每次删除后要重新扫描
  • 空间复杂度:O(n) — 需要创建新字符串
  • 瓶颈在哪:每次删除后要重新扫描,重复操作太多

Step 3:瓶颈分析 → 优化方向

分析暴力法的核心问题:

  • 核心问题:如何高效地"配对最近的左括号"?
  • 关键观察:当遇到右括号时,它应该匹配最近的未匹配的左括号
  • 数据结构选择:"最近"→ 后进先出 →

Step 4:选择武器

  • 选用:栈(Stack)
  • 理由:
    1. 遇到左括号就入栈(等待被匹配)
    2. 遇到右括号就检查栈顶是否是对应的左括号
    3. 如果匹配就出栈,如果不匹配或栈空就返回 False
    4. 最后检查栈是否为空(所有左括号都被匹配)

🔑 模式识别提示:当题目涉及配对嵌套最近匹配等关键词,优先考虑""


🔑 解法一:栈匹配(标准解法)

思路

用栈存储所有遇到的左括号,遇到右括号时检查栈顶是否匹配。

图解过程

示例: 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)
适用场景混合括号仅单一类型
代码难度简单极简单
面试推荐⭐⭐⭐⭐(不适用本题)

面试建议

  1. 直接讲解法一的栈匹配,这是标准且唯一正确的解法
  2. 可以提到"如果只有单一类型括号,可以用计数法 O(1) 空间",展示你的思考深度
  3. 重点强调:栈是处理括号匹配、嵌套结构的经典工具

🎤 面试现场

模拟面试中的完整对话流程,帮你练习"边想边说"。

面试官:请你判断一个字符串的括号是否有效匹配。

:(审题30秒)好的,这道题要求判断括号是否有效,需要满足左右配对、类型一致、顺序正确。

我的思路是用:

  1. 遍历字符串,遇到左括号就入栈
  2. 遇到右括号就检查栈顶是否是对应的左括号
    • 如果匹配就出栈
    • 如果不匹配或栈空就返回 False
  3. 最后检查栈是否为空

这样只需要一次遍历,时间 O(n),空间 O(n)。

面试官:为什么用栈?

:因为括号匹配的关键是"最近的左括号优先匹配"。

比如 "{[]}"

  • 遇到 ] 时,应该匹配最近的 [,而不是更早的 {
  • 这正是**栈的后进先出(LIFO)**特性:最后入栈的左括号最先被匹配

如果用其他数据结构:

  • 数组需要记录位置,O(n²) 查找
  • 队列是先进先出,无法匹配最近的

所以栈是最合适的。

面试官:能否优化空间?

:对于混合类型括号,必须用栈记录括号的类型和顺序,无法优化到 O(1)。

但如果只有单一类型括号(如只有 ()),可以用计数法:

  • 遇到 ( 计数 +1,遇到 ) 计数 -1
  • 如果计数变负数或最后不为 0,返回 False
  • 这样是 O(1) 空间

但本题有三种括号混合,必须区分类型,所以必须用栈。

面试官:测试一下边界情况。

  1. 长度为奇数:直接返回 False(无法完全配对)
  2. 只有左括号 "(((": 栈非空,返回 False
  3. 只有右括号 ")))":栈空时遇到右括号,返回 False
  4. 顺序错误 "([)]":栈顶 [ 不匹配 ),返回 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()  # 出栈

对于本题,listdeque 性能差不多,但 list 更简单直接。 deque 的优势在于头部操作也是 O(1),而 list 的头部操作是 O(n)。

括号匹配的编译原理应用

编译器在词法分析语法分析阶段大量使用栈:

  1. 括号匹配:检查代码的括号、花括号、方括号是否配对
  2. 表达式求值3 + 5 * 2 需要用栈处理运算符优先级
  3. 函数调用栈:每次函数调用压栈,返回时弹栈

例如 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

变体问题

  • 括号匹配 → 右括号检查栈顶
  • 删除重复字母 → 维护递增栈
  • 每日温度 → 单调栈
  • 表达式求值 → 运算符栈 + 操作数栈

易错点 ⚠️

  1. 忘记检查栈是否为空

    • ❌ 错误:遇到右括号直接 stack.pop()stack[-1],栈空时报错
    • ✅ 正确:先检查 if not stack 再操作
    • 示例:输入 "))",第一个 ) 时栈空,需要返回 False
  2. 最后忘记检查栈是否为空

    • ❌ 错误:遍历结束直接返回 True
    • ✅ 正确:return not stacklen(stack) == 0
    • 示例:输入 "(((",遍历结束但栈非空,应该返回 False
  3. 映射表键值颠倒

    • ❌ 错误:pairs = {'(': ')', ...} 左括号映射右括号
    • ✅ 正确:pairs = {')': '(', ...} 右括号映射左括号
    • 原因:我们是用右括号去查找对应的左括号,所以右括号是键
  4. 弹出栈顶后没有检查值

    • ❌ 错误: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 学习资料都在这里,后续复习和拓展会更省时间。