第3章|栈与队列
Unit 01 · LeetCode #20 有效的括号
← Unit 00 基础
U01
Valid ParenthesesMatch · Top · Pending Openings
右括号出现时,
它只能关闭最近尚未关闭的左括号。
括号匹配并不是比较左右括号总数,而是维护嵌套顺序。栈顶始终代表当前最内层、最先需要被关闭的左括号。
LeetCode #20 题目模型三类括号、类型匹配与正确闭合顺序
题目要求
给定一个只包含 ()[]{} 六种括号字符的字符串 s,判断它是否有效。有效字符串要求:左括号必须由相同类型的右括号闭合;括号必须按正确顺序闭合;每个右括号都有对应的左括号。
class Solution:
def isValid(self, s: str) -> bool:
pass
输入、输出与约束
- 输入:括号字符串
s - 输出:有效返回
True,否则返回False 1 ≤ len(s) ≤ 10⁴s仅由()[]{}组成
示例
输入:s = "()[]{}"
输出:true
输出:true
输入:s = "(]"
输出:false
输出:false
输入:s = "([])"
输出:true
输出:true
输入:s = "([)]"
输出:false
输出:false
复杂度目标
一次从左到右扫描:时间复杂度 O(n);最坏情况下全部为左括号,栈空间复杂度 O(n)。
Pending Opening
栈中只保存尚未被右括号匹配的左括号。已经完成配对的括号立即离开待处理状态。
Nearest Unclosed
栈顶是最近出现且尚未关闭的左括号。新的右括号必须首先与它匹配。
Type Matching
)→(、]→[、}→{。数量相等不能代替类型与顺序检查。
Final Empty Stack
扫描结束后栈必须为空;否则仍有左括号没有获得对应的右括号。
认知任务 1
根据当前字符和栈顶,决定本步应该压栈、弹栈还是失败
答案不能只看当前字符。右括号的动作取决于栈是否为空,以及栈顶左括号的类型。
括号扫描实验台状态尚未执行
输入字符串与扫描指针
读取当前字符前,先观察栈顶。
尚未匹配的左括号栈
) → (
] → [
} → {
读取字符
识别类型
检查栈顶
修改栈
确定状态
请先回答右侧问题,动画将展示本步状态变化。
扫描位置0
栈深度0
期待左括号—
当前结论待定
把栈契约编译为 Python填写关键表达式;主答案采用显式版本,便于与动画逐行对应。
if
if
执行匹配:
读取左括号:
扫描结束:return
关键不是写出能运行的表达式,而是让每一行都遵守“栈只保存未匹配左括号”的契约。
class Solution:
def isValid(self, s: str) -> bool:
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 not stack
时间复杂度:O(n)
空间复杂度:O(n)
代码与括号栈逐行联动点击代码行可跳转到该行第一次执行的状态。
1class Solution:
2 def isValid(self, s: str) -> bool:
3 pairs = {")":"(", "]":"[", "}":"{"}
4 stack = []
6 for index, char in enumerate(s):
7 if char in pairs:
8 if not stack or stack[-1] != pairs[char]:
9 return False
10 stack.pop()
11 else:
12 stack.append(char)
14 return not stack
执行状态
当前输入s = "([])"
index / char—
stack
期待匹配—
判断 / 返回初始化
进入 Solution 类定义。
完成代码联动后进入 LeetCode #921 原题与迁移学习代码正确、查看参考答案、一键填入或播放完整轨迹后解锁。
迁移题:LeetCode #921 使括号有效的最少添加
迁移不从代码开始。先确认新题允许什么操作、要求返回什么,再判断 #20 中哪些状态仍然必要,哪些状态可以被压缩。
1. 认识 #921 原题
2. 对照判断与修复
3. 映射四个状态变化
4. 拼装并运行官方样例
LeetCode #921|使括号有效的最少添加
依据 LeetCode 官方题面整理 ↗
题目要求
给定一个只包含圆括号的字符串 s。一次操作可以在字符串任意位置插入一个 ( 或 )。返回把字符串变成有效括号串所需的最少插入次数。
class Solution:
def minAddToMakeValid(self, s: str) -> int:
# 返回最少插入次数
约束
1 ≤ len(s) ≤ 1000s[i]只可能是(或)
官方示例
输入:s = "())"
输出:1
补入一个左括号即可修复多出的右括号。
输出:1
补入一个左括号即可修复多出的右括号。
输入:s = "((("
输出:3
三个未闭合左括号分别需要一个右括号。
输出:3
三个未闭合左括号分别需要一个右括号。
有效括号串
空串有效;两个有效串拼接仍有效;在有效串外包上一对圆括号仍有效。本题允许修复,而不是发现错误后立即失败。
字符类型只有一种括号,不再需要区分
()[]{} 三种类型。允许操作只能插入括号,不能删除、替换或重新排列原字符。
优化目标返回最少插入次数,不是返回修复后的字符串。
扫描策略意外右括号需要补左括号;末尾剩余左括号需要补右括号。
读题检查 A|函数最终应该返回什么?
读题检查 B|为什么官方示例 s="())" 的答案是 1?
已经掌握:#20 有效的括号
输入:三种括号 ()[]{}
目标:判断是否有效,返回 bool
状态:stack 保存未匹配左括号的类型与顺序
意外右括号:立即 return False
扫描结束:stack 必须为空
准备迁移:#921 最少添加
输入:只有圆括号 ()
目标:统计最少修复成本,返回 int
状态:balance 只保存未匹配左括号数量
意外右括号:additions += 1,然后继续扫描
扫描结束:剩余 balance 都要补右括号
保持不变仍然从左到右扫描;右括号只能消耗此前尚未匹配的左括号;已完成匹配的状态立即移除。
目标改变#20 遇到无法匹配的右括号就判错;#921 不能结束,而要把这次冲突转换成一次必需的插入成本。
为什么 stack 可以压缩成 balance?#20 必须记住栈顶究竟是
(、[ 还是 {;#921 的所有左括号完全同质。后续决策只需要知道“当前有没有可用左括号、共有多少个”,不再需要保存每个元素的类型和顺序。先判断迁移幅度:下面哪句话最准确?
槽位 A|需要哪两个整数状态?
槽位 B|读取左括号时怎样更新?
槽位 C|读取右括号时怎样处理“可匹配”和“意外出现”两种情况?
槽位 D|扫描结束后为什么返回两部分之和?
两类缺口不会互相抵消:
additions 记录扫描途中已经出现的“缺左括号”事件;最终 balance 记录仍未关闭的左括号,每一个都还缺一个右括号。保留的核心语义状态始终表示“扫描前缀中仍未解决的括号需求”;匹配成功时立即消费一个未匹配左括号。
删除的类型信息不再需要
pairs 和字符栈,因为所有未匹配左括号都等价,只保留数量 balance。新增的修复成本不能匹配的右括号不再导致失败,而是让
additions 增加1;最后再补齐剩余左括号。#20:冲突即失败
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 not stack#921:冲突转化为修复成本
balance = 0 additions = 0 for char in s: if char == "(": balance += 1 elif balance > 0: balance -= 1 else: additions += 1 return additions + balance
官方样例运行轨迹:s = "())"0/0
未匹配左括号 balance0
已补左括号 additions0
当前最少添加数0
迁移不变量balance 表示前缀中尚未匹配的左括号;additions 表示前缀中已经无法由未来字符补救、必须插入的左括号数量。
可选挑战:脱离槽位,独立重写完整代码
编辑器保留了函数、状态初始化和循环骨架。请自行补回左括号、右括号和最终返回逻辑。
先完整运行官方样例,再尝试脱离槽位重写。
Unit 01 Skill:最近未闭合对象优先匹配
扫描括号字符串时,先识别未完成状态需要保存哪些信息。多种括号需要用栈保存类型与顺序;只有一种同质括号时,可以把栈压缩为数量。题目目标从“判断是否合法”变为“计算最少修复成本”后,原本的失败分支也要转化为可累计状态。
主问题#20:栈保存未匹配左括号
迁移问题#921:balance + additions
迁移类型容器状态压缩为计数状态
保持的不变量状态只描述扫描前缀中尚未解决的需求
目标变化布尔判定 → 最小修复成本
复杂度时间 O(n),额外空间 O(1)