(c2)栈应用:括号匹配

官官方信息技术老师·14 页·深入(追求细节与边界)·0 次浏览·38 天前
数据结构栈算法边界代码实现

栈应用:括号匹配

看完你能从零写出括号匹配器,并说清新括号何时入栈、何时该报错

按 空格/→ 演示下一步

问一句
1 / 14 页

全部页面点击任意一页,跳回舞台从这页播放

数据结构栈算法边界代码实现

栈应用:括号匹配

看完你能从零写出括号匹配器,并说清新括号何时入栈、何时该报错

1第 1 页 · 栈应用:括号匹配

实例:哪些括号串合法

上一页说了思路:左括号进栈、遇到右括号就弹出配对。可什么样的串才算「配得上」?先别急着写代码,摆几个例子对照看看,合法与非法的差别其实一眼能看出规律。

合法:嵌套配对
如 ([]),右括号总是和最近入栈的左括号配对
合法:并列配对
如 ()()[],各对互不干扰,弹出后栈恰好清空
非法:次序错乱
如 ([)],先弹到的是 [ 但来的是 ),对不上
非法:右多
如 (])),栈已空还要弹,无括号可配
非法:左多
如 (((),串读完栈里还剩左括号没配
叠盘子取盘子对应 →括号匹配

左括号是叠上盘子,右括号是取最上面那只,类型必须对得上

2第 2 页 · 实例:哪些括号串合法

匹配的规则是什么

上一页我们看了几个例子:合法的串里,被晚写下的左括号,总是被先闭合。这不是巧合——把它们摞在桌面上对照,规律就浮现了:最近未匹配的左括号,最先被闭合。

观察实例
「(())」合法:内层先闭合,外层后闭合
归纳规律
待匹配的左括号中,最新出现的必须最先闭合
反例验证
「)(」或「(()」违反规律,故不合法
后进先出
「最新者最先处理」正是栈的特性
叠盘子对应 →待匹配括号

后洗好的盘子摞在顶上,也最先被取走

3第 3 页 · 匹配的规则是什么

第一次尝试:数数行不行

先试最省事的办法:一个计数器,看它能否判对括号串。

1
设计数器
遇'('加一,遇')'减一,从0出发
2
检查终点
串读完时计数器为0就判合法?
3
构造反例
)(:先减一,中途就破了,终点却是0
4
结论
只知道总数差,不知道'谁在等谁',必须记顺序
4第 4 页 · 第一次尝试:数数行不行

构思:栈的对口之处

上一页我们试过数数:只统计左右括号个数,结果分不清「( )」和「) (」。缺的是顺序信息——而顺序恰好是栈最擅长保存的东西:套碗时最后放上去的碗,总是最先被取出来。

左括号进栈
遇到 (、[、{ 就压入栈中,等它的配对伙伴到来
右括号查栈顶
遇到 )、]、} 就看栈顶:类型对上则弹出,继续扫描
两种失败时刻
栈顶类型不匹配,或右括号到来时栈已空
结束看栈空
扫描完栈必须为空,否则有括号没被配对
套碗收纳对应 →括号匹配

扣碗对应左括号进栈,取碗对应右括号找栈顶配对

5第 5 页 · 构思:栈的对口之处

算法流程图

从左到右扫描,每个字符走一次菱形判断:左括号入栈、右括号找栈顶对口,看三条出口怎么覆盖所有情况。

图解渲染中…
f1:扫描结束后栈里还有左括号没收口c2:右括号来了栈却空,右括号多余c3:查栈顶类型:圆对方、方对方、花对花s0:处理完一个字符回到取下一个,循环
6第 6 页 · 算法流程图

实现:核心代码

python

看四步动作如何落到代码:压栈、看栈顶、弹出配对、最后验空。

代码高亮加载中…

第5行入栈,第9行 peek 栈顶验证,第10行才弹出,第12行确保左括号不残留——四步对应流程图。

7第 7 页 · 实现:核心代码

逐步执行:栈状态跟踪

从左到右扫描括号串「([])」,每个箭头是读入一个字符后栈的新状态。

图解渲染中…
a1:节点内容表示当前栈里的元素,栈顶在右侧a4:扫描结束且栈为空,整串合法
8第 8 页 · 逐步执行:栈状态跟踪

反思:三种失败情形

前面跟踪栈状态时,合法串最后栈恰好清空。但如果括号串不合法呢?栈会在不同时刻、以不同方式暴露问题——把三种失败情形分辨清楚,才算真正吃透这个算法。

右括号失配
栈顶左括号与当前右括号类型不同,类型错位
栈提前空
右括号来了却无左括号可配,右多
栈非空结束
串读完栈里还剩左括号,左多
停车与取车对应 →括号匹配

取错车位、无车可取、离开时还有车没取,对应三种失败

9第 9 页 · 反思:三种失败情形

自测:判一判

点击作答

用栈判断括号串合法,算法结束时能宣布「合法」的完整条件是什么?

10第 10 页 · 自测:判一判

拓展:多种括号混用

多种括号混用时,用栈顶字典判断新括号该配谁,一次比较完成并行配对。

1
建配对字典
预存 {':'}', '[':']', '(':')'},查表代替逐个if判断
2
遇左括号
任何左括号都压栈,类型记在栈里待配
3
遇右括号
弹栈顶,查字典:栈顶对应的闭括号应等于当前右括号
4
交叉即失败
如 ([)] 中 ) 要求栈顶是 (,却是 [,立即判非法
5
([{}])验证
三层嵌套依次压入、逆序弹出,各次查表都命中
11第 11 页 · 拓展:多种括号混用

反思:栈思想的普适性

前面我们用栈解决了括号匹配,但值得停下来想想:为什么偏偏是栈?前一页的混用括号已经暗示了答案——嵌套的东西,总是最后打开、最先关闭。

后进先出
最后出现的左括号必须最先被匹配,正是 LIFO 的天然对应
括号嵌套
嵌套深度即栈深,任意时刻未匹配的左括号构成一条链
函数调用
递归调用栈:最后进入的函数最先返回,结构完全同构
表达式求值
运算符优先级压栈,内层子表达式先算,同样是嵌套消解
套娃拆开再合上对应 →LIFO 结构

最后套进去的娃最先取出,对应最后出现的左括号最先匹配

12第 12 页 · 反思:栈思想的普适性

要点收束

  • ✓数数只看数量,栈才能看住配对方向
  • ✓遇左括号入栈,遇右括号找栈顶销账
  • ✓结束时栈非空、中途弹空、不匹配都算失败
  • ✓扩展到多种括号,只需判断栈顶类型是否对应
  • ✓后进先出天然契合就近配对类问题
延伸主题:栈实现表达式求值用栈做函数调用栈分析单调栈:下一个更大元素
13第 13 页 · 要点收束

课后思考

先自己想,再看参考答案——卡住的地方才是收获。

1为什么这个算法一定能正确判完所有输入?它不会中途漏判或误判吗?

参考答案从不变式想:任何时刻栈里存的都是「尚未等到右括号的左括号」。扫完时栈空、且从未遇到失配,就说明每个左括号都等到了对口右括号。

2如果只需判断一种括号「()」,还需要栈吗?有没有更省空间的做法?

参考答案可以只用一个整数计数器:见左括号加一,见右括号减一,中途变负或结尾非零即失败。栈的价值在多种括号混用时才真正显现。

3数数法在单一括号时够用,为什么换成多种括号就一定要用栈?

参考答案计数器只知道「还差几个」,不知道「差的是哪种」。栈顶恰好记录最近等待的那个左括号,右括号必须先和它配对——后进先出正是嵌套结构的本质。

14第 14 页 · 课后思考