Valid Parentheses 是 Grind 75 中非常经典的一道题。
题目本身不算难,却特别适合用来理解 Stack,也就是“栈”。
很多人背得出这句话:
遇到左括号就入栈,遇到右括号就出栈。
但真正写代码时,还是容易卡住:
- 为什么一定要用 Stack?
- 右括号出现时,应该和谁比较?
- 什么情况下可以出栈?
- 字符串扫描结束后,为什么还要检查 Stack?
下面用流程图和一个完整例子,把整个过程拆开讲清楚。
一、题目要求
给定一个只包含以下六种字符的字符串:
( ) [ ] { }
判断字符串中的括号是否有效。
有效字符串需要满足三个条件:
- 左括号必须由相同类型的右括号闭合;
- 括号必须按照正确顺序闭合;
- 每个右括号都必须有对应的左括号。
示例
输入:"()"
输出:true
输入:"()[]{}"
输出:true
输入:"(]"
输出:false
输入:"([)]"
输出:false
输入:"{[]}"
输出:true
二、为什么这道题适合使用 Stack?
先看一个有效字符串:
([{}])
左括号出现的顺序是:
( → [ → {
右括号关闭的顺序却是:
} → ] → )
也就是说:
最后出现的左括号,最先被关闭。
具体配对关系是:
( [ { } ] )
└──┘
└────────┘
└─────────────┘
最里面的 { 最先遇到对应的 }。
然后是 [ 和 ]。
最外面的 ( 最后才和 ) 配对。
这正好符合 Stack 的特点:
Last In, First Out 后进先出
可以把 Stack 想成一摞盘子。
最后放上去的盘子 必须最先拿走
括号匹配也是同样的道理:
最近出现、还没有关闭的左括号 必须最先被右括号关闭
三、Stack 解法的核心规则
从左到右扫描字符串。
遇到左括号
下面三种字符都是左括号:
(
[
{
将它放入 Stack。
这个动作叫作:
入栈 Push
遇到右括号
下面三种字符都是右括号:
) ] }
这时不能直接出栈,而是要先检查两件事。
第一件事:Stack 是否为空?
如果 Stack 是空的,说明当前右括号前面没有可以和它配对的左括号。
例如:
")"
第一个字符就是右括号,显然无效。
第二件事:栈顶括号是否匹配?
右括号只能和对应类型的左括号配对:
) 对应 (
] 对应 [
} 对应 {
例如当前字符是 ],Stack 顶部必须是 [。
匹配成功,才能把栈顶左括号移除。
这个动作叫作:
出栈 Pop
如果类型不同,立即返回 false。
四、完整算法流程图
┌─────────────────────┐ │ 开始执行 │ └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ 创建一个空的 Stack │ └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ 从左到右读取一个字符 │ └──────────┬──────────┘ │ ▼ ┌───────────────┐ │ 字符是否存在? │ └───────┬───────┘ 是 │ 否 │ ┌───────▼────────┐ │ 是否为左括号? │ └───────┬────────┘ 是 │ 否 │ ┌───────▼────────┐ │ 将左括号压入栈中 │ │ Stack.push() │ └───────┬────────┘ │ └───────────────┐ │ ▼ 继续读取下一个字符
遇到右括号时,流程继续如下:
当前字符是右括号 │ ▼ ┌────────────────┐ │ Stack 是否为空?│ └───────┬────────┘ 是 │ 否 │ ┌───────────▼───────┐ │ 直接返回 false │ │ 没有左括号可匹配 │ └───────────────────┘ 否 │ ▼ ┌───────────────────────┐ │ 栈顶左括号是否与它匹配?│ └───────────┬───────────┘ 否 │ 是 │ ┌───────────▼───────┐ │ 直接返回 false │ │ 括号类型不匹配 │ └───────────────────┘ 是 │ ▼ ┌────────────────────┐ │ 弹出 Stack 栈顶元素│ │ Stack.pop() │ └──────────┬─────────┘ │ ▼ 继续读取下一个字符
当所有字符都扫描结束后:
┌─────────────────────┐ │ 所有字符已经扫描完毕 │ └──────────┬──────────┘ │ ▼ ┌─────────────────┐ │ Stack 是否为空? │ └────────┬────────┘ 是 │ 否 │ ┌───────────▼──────┐ ┌────────────────┐ │ 返回 true │ │ 返回 false │ │ 所有括号均已配对 │ │ 仍有左括号未关闭│ └──────────────────┘ └────────────────┘
把整个流程压缩成一句话就是:
左括号入栈 右括号检查栈顶 匹配成功就出栈 最后栈为空才有效
五、完整图解:([{}]) 如何匹配成功?
字符串:
( [ { } ] )
字符索引:
0 1 2 3 4 5
我们从左到右逐个读取。
约定:
Stack 最右边是栈顶
第 1 步:读取 (
当前字符:
(
这是一个左括号,所以将它压入 Stack。
读取前:
Stack: [ ]
读取字符:
( [ { } ] )
↑
执行入栈:
stack.push("(")
读取后:
Stack: [ ( ]
↑
栈顶
当前状态:
已经读取:(
还未读取:[{}])
Stack: [ ( ]
第 2 步:读取 [
当前字符是左方括号 [。
继续入栈。
读取字符:
( [ { } ] )
↑
入栈前:
Stack: [ ( ]
执行:
stack.push("[")
入栈后:
Stack: [ ( , [ ]
↑
栈顶
当前状态:
已经读取:([
还未读取:{}])
Stack: [ ( , [ ]
第 3 步:读取 {
当前字符是左大括号 {。
继续入栈。
读取字符:
( [ { } ] )
↑
入栈前:
Stack: [ ( , [ ]
执行:
stack.push("{")
入栈后:
Stack: [ ( , [ , { ]
↑
栈顶
当前状态:
已经读取:([{
还未读取:}])
Stack: [ ( , [ , { ]
第 4 步:读取 }
当前字符是右大括号 }。
遇到右括号后,先看 Stack 是否为空。
Stack: [ ( , [ , { ]
Stack 不为空,可以继续比较。
右大括号 } 需要匹配:
{
当前 Stack 栈顶正好是 {。
当前右括号:}
需要的左括号:{
Stack 栈顶:{
比较结果:
{ ↔ }
匹配成功
因此将 { 出栈。
出栈前:
Stack: [ ( , [ , { ]
↑
执行:
stack.pop()
出栈后:
Stack: [ ( , [ ]
↑
当前状态:
已经读取:([{}
还未读取:])
Stack: [ ( , [ ]
第 5 步:读取 ]
当前字符是右方括号 ]。
它需要匹配左方括号 [。
当前栈顶正好是 [:
当前右括号:] 需要的左括号:[ Stack 栈顶:[ 比较: [ ↔ ] 匹配成功
执行出栈:
出栈前: Stack: [ ( , [ ] ↑ 执行: stack.pop() 出栈后: Stack: [ ( ] ↑
当前状态:
已经读取:([{}]
还未读取:)
Stack: [ ( ]
第 6 步:读取 )
当前字符是右圆括号 )。
它需要匹配左圆括号 (。
当前 Stack 栈顶正好是 (。
当前右括号:) 需要的左括号:( Stack 栈顶:( 比较: ( ↔ ) 匹配成功
执行出栈:
出栈前: Stack: [ ( ] ↑ 执行: stack.pop() 出栈后: Stack: [ ]
现在字符串已经扫描结束。
Stack 也为空。
所有左括号都找到了对应的右括号 所有括号的关闭顺序也正确 结果:true
七、完整过程汇总
| 步骤 | 当前字符 | 判断 | Stack 操作 | 操作后的 Stack |
| 1 | ( | 左括号 | 入栈 | [(] |
| 2 | [ | 左括号 | 入栈 | [(, [] |
| 3 | { | 左括号 | 入栈 | [(, [, {] |
| 4 | } | 与栈顶 { 匹配 | 出栈 | [(, [] |
| 5 | ] | 与栈顶 [ 匹配 | 出栈 | [(] |
| 6 | ) | 与栈顶 ( 匹配 | 出栈 | [] |
最后 Stack 为空,因此返回:
true
八、失败示例一:(]
字符串:
( ]
第 1 步
读取 (,左括号入栈。
Stack: [ ( ]
第 2 步
读取 ]。
] 需要匹配的左括号是 [。
但是当前 Stack 栈顶是 (。
当前右括号:] 需要的左括号:[ 实际栈顶:( 比较: ( ≠ [
括号类型不同,所以立即返回:
false
这里的问题不是括号数量,而是括号类型不匹配。
九、失败示例二:([)]
这个例子更容易让人困惑。
字符串中有:
一对圆括号 一对方括号
括号数量和类型看起来都没问题,但关闭顺序错了。
字符串:
( [ ) ]
先读取前两个左括号:
读取 (: Stack: [ ( ] 读取 [: Stack: [ ( , [ ] ↑ 栈顶
接下来读取 )。
) 需要匹配 (。
但当前栈顶是最近进入的 [:
当前右括号:) 需要的左括号:( 实际栈顶:[ 比较: [ ≠ (
因此返回:
false
不能越过还没有关闭的 [,去寻找下面的 (。
错误结构:
( [ ) ] ↑ ↑ 圆括号跨过了方括号
正确结构应该是:
( [ ] ) ↑ ↑ ↑ ↑
这也说明,括号匹配不能只看数量,还必须检查嵌套顺序。
十、失败示例三:第一个字符就是右括号
字符串:
")("
第一个字符是 )。
此时:
Stack: [ ]
Stack 为空,说明前面没有左圆括号可以和它配对。
流程如下:
读取右括号 ) │ ▼ Stack 是否为空? │ 是 │ ▼ 返回 false
因此不需要继续扫描后面的字符。
十一、失败示例四:扫描结束后 Stack 不为空
看这个字符串:
"((("
三个字符都是左括号。
它们会依次入栈:
读取第一个 (: Stack: [ ( ] 读取第二个 (: Stack: [ ( , ( ] 读取第三个 (: Stack: [ ( , ( , ( ]
字符串已经扫描完了,但 Stack 里还剩三个左括号。
这意味着它们没有对应的右括号。
所以最后不能直接返回 true,而是必须检查:
Stack 是否为空?
此时 Stack 不为空,所以结果是:
false
十二、JavaScript 解法
function isValid(s) {
// 有效括号一定成对出现。
// 如果字符串长度是奇数,可以直接返回 false。
if (s.length % 2 !== 0) {
return false;
}
const stack = [];
// 每种右括号对应的左括号
const pairs = {
")": "(",
"]": "[",
"}": "{"
};
for (const char of s) {
// 遇到左括号,直接入栈
if (char === "(" || char === "[" || char === "{") {
stack.push(char);
continue;
}
// 遇到右括号时,如果 Stack 为空,
// 说明没有左括号可以和它匹配
if (stack.length === 0) {
return false;
}
// 查看最近进入 Stack 的左括号
const top = stack.pop();
// 检查括号类型是否匹配
if (top !== pairs[char]) {
return false;
}
}
// Stack 为空,说明所有括号都完成了配对
return stack.length === 0;
}
十三、代码流程逐行解释
1. 创建 Stack
const stack = [];
JavaScript 中可以使用数组模拟 Stack。
主要使用两个方法:
stack.push()
负责入栈。
stack.pop()
负责出栈。
2. 建立括号对应关系
const pairs = {
")": "(",
"]": "[",
"}": "{"
};
这张表表示:
遇到 ),栈顶必须是 (
遇到 ],栈顶必须是 [
遇到 },栈顶必须是 {
例如当前字符是 ]:
pairs["]"]
得到:
[
因此代码可以检查:
top !== pairs[char]
也就是:
实际栈顶 是否等于 当前右括号需要的左括号
3. 遇到左括号就入栈
if (char === "(" || char === "[" || char === "{") {
stack.push(char);
continue;
}
例如当前 Stack 是:
[ ( , [ ]
当前字符是 {:
stack.push("{");
结果变成:
[ ( , [ , { ]
continue 表示当前字符处理完了,继续读取下一个字符。
4. 遇到右括号先检查空栈
if (stack.length === 0) {
return false;
}
例如:
输入:"]"
此时没有任何左括号入栈。
Stack: [ ]
右括号没有可以匹配的对象,所以直接返回 false。
5. 取出栈顶左括号
const top = stack.pop();
例如:
出栈前:
Stack: [ ( , [ , { ]
执行:
const top = stack.pop();
top 得到:
{
Stack 变成:
Stack: [ ( , [ ]
6. 检查括号类型
if (top !== pairs[char]) {
return false;
}
假设当前字符是:
}
那么:
pairs["}"]
结果是:
{
如果刚刚出栈的 top 也是 {,说明匹配成功。
如果 top 是 [ 或 (,说明括号类型不匹配,直接返回 false。
7. 最后检查 Stack
return stack.length === 0;
扫描结束后有两种情况。
Stack 为空
Stack: [ ]
说明所有左括号都已经被正确关闭。
返回:
true
Stack 不为空
Stack: [ ( , [ ]
说明还有左括号没有找到对应的右括号。
返回:
false
十四、Python 解法
def isValid(s: str) -> bool:
if len(s) % 2 != 0:
return False
stack = []
pairs = {
")": "(",
"]": "[",
"}": "{"
}
for char in s:
if char in "([{":
stack.append(char)
continue
if not stack:
return False
top = stack.pop()
if top != pairs[char]:
return False
return len(stack) == 0
Python 中:
stack.append(char)
相当于入栈。
stack.pop()
相当于出栈。
整体思路与 JavaScript 完全相同。
十五、时间复杂度和空间复杂度
假设字符串长度为 n。
时间复杂度:O(n)
每个字符只需要从左到右读取一次。
每个左括号最多入栈一次,每个匹配成功的左括号最多出栈一次。
因此时间复杂度是:
O(n)
空间复杂度:O(n)
最坏情况下,字符串全部都是左括号:
(((([[{{
这些左括号都需要暂时保存在 Stack 中。
所以最坏情况下,Stack 的大小与字符串长度相同。
空间复杂度是:
O(n)
十六、常见错误
错误一:只统计括号数量
有人会分别统计:
( 和 ) 的数量
[ 和 ] 的数量
{ 和 } 的数量
但下面这个字符串中,各类括号数量完全相同:
([)]
结果仍然是无效的。
因为括号匹配不仅要看数量,还要看关闭顺序。
错误二:遇到右括号就直接出栈
错误写法:
const top = stack.pop();
在出栈前没有检查 Stack 是否为空。
例如输入:
")"
此时 Stack 中没有任何左括号。
正确做法是先检查:
if (stack.length === 0) {
return false;
}
错误三:只要是左括号就算匹配
下面的字符串中,确实有一个左括号和一个右括号:
(]
但它们类型不同。
必须严格检查:
( 只能匹配 )
[ 只能匹配 ]
{ 只能匹配 }
错误四:扫描结束后直接返回 true
例如:
"((("
扫描过程中没有出现错误的右括号,但左括号始终没有关闭。
因此扫描结束后必须判断:
stack.length === 0
十七、面试时怎么解释?
面试中可以这样回答:
这道题需要判断每个右括号,是否与最近一个尚未匹配的左括号配对。最近出现的左括号需要最先关闭,符合 Stack 后进先出的特点。
遍历字符串时,遇到左括号就入栈;遇到右括号时,先检查 Stack 是否为空,然后比较栈顶左括号是否与当前右括号匹配。匹配成功就出栈,不匹配就立即返回 false。
字符串遍历结束后,只有 Stack 为空,才说明所有括号都完成了正确配对。
时间复杂度是 O(n),空间复杂度是 O(n)。
十八、最后总结
Valid Parentheses 最重要的一句话是:
当前右括号必须匹配最近出现、 并且尚未关闭的左括号。
“最近进入的左括号最先被关闭”,正好就是 Stack 的:
后进先出 Last In, First Out
完整流程可以记成四步:
1. 遇到左括号:入栈 2. 遇到右括号: 先检查 Stack 是否为空 3. 比较右括号和栈顶左括号: 匹配就出栈,不匹配就返回 false 4. 扫描结束: Stack 为空返回 true Stack 不为空返回 false
真正理解入栈、检查栈顶和出栈的顺序后,这道题就不需要死记代码了。
评论
0 条正在加载评论...