LeetCode 20「有效的括号」是一道经典的栈(Stack)算法题,主要考察栈的后进先出特性、哈希映射以及边界条件判断。
题目描述
给定一个只包含以下字符的字符串 s:
'(', ')', '{', '}', '[', ']'
判断字符串中的括号是否有效。
有效的括号必须满足:左括号和右括号类型相同,并且按照正确的顺序闭合。
例如:
s = "()[]{}" // true
s = "([{}])" // true
s = "(]" // false
s = "([)]" // false
s = "(()" // false
其中 "([)]" 虽然左右括号数量相同,但闭合顺序错误,因此也是无效的。
为什么使用栈?
这道题最重要的特点是:最后出现的左括号,必须最先被匹配。 这点至关重要。
例如:
([{}])
括号的匹配顺序是:
{ 先与 } 匹配
[ 再与 ] 匹配
( 最后与 ) 匹配
这恰好符合栈的 后进先出(LIFO) 特性。
栈可以理解成一摞盘子,最后放进去的盘子,最先被拿出来。
在 JavaScript 中,可以直接使用数组模拟栈:
javascript
const stack = []
stack.push("(")
stack.push("[")
console.log(stack)
// ["(", "["]
stack.pop()
// 返回 "[",并从数组中移除
console.log(stack)
// ["("]
这里:
-
push():将元素添加到栈顶。 -
pop():移除并返回栈顶元素。 -
stack.length:获取当前栈中元素数量。
因此,我们可以把遇到的左括号放入栈中。当遇到右括号时,就取出最近的左括号进行比较。
JavaScript 实现
javascript
var isValid = function(s) {
const stack = []
const map = {
')': '(',
']': '[',
'}': '{'
}
for (const ch of s) {
if (map[ch]) {
if (stack.pop() !== map[ch]) {
return false
}
} else {
stack.push(ch)
}
}
return stack.length === 0
}
整段代码的核心思路就是:
遇到左括号入栈,遇到右括号出栈比较,最后检查栈是否为空。
哈希映射 map 的作用
代码中定义了一个普通对象:
const map = {
')': '(',
']': '[',
'}': '{'
}
它用来保存右括号和对应左括号的关系。
例如:
map[')'] // "("
map[']'] // "["
map['}'] // "{"
这里的:
map[ch]
属于 JavaScript 对象的动态属性访问。
假设:
const ch = ')'
那么:
map[ch]
就相当于:
map[')']
结果为:
"("
而如果:
const ch = '('
由于 map 中没有 '(' 这个键:
map['('] // undefined
因此我们可以通过:
if (map[ch])
判断当前字符是不是右括号。
如果是右括号,就检查栈顶;如果不是,就把当前左括号放进栈里。
需要注意,这里的 map 是普通 JavaScript 对象,不是 new Map() 创建的集合类型。
for...of 是怎么遍历的?
代码:
for (const ch of s) {
for...of 用来依次获取字符串中的字符。
例如:
const s = "([])"
for (const ch of s) {
console.log(ch)
}
输出:
(
[
]
)
也就是说,每次循环时,ch 都表示当前遍历到的括号。
为什么需要 stack.pop()?
最关键的判断是:
if (stack.pop() !== map[ch]) {
return false
}
假设当前字符串为:
s = "([])"
执行到右括号 ] 时,栈里保存的是:
stack = ["(", "["]
此时:
stack.pop()
返回:
"["
而:
map["]"]
结果同样是:
"["
两者相等,说明匹配正确,可以继续。
如果遇到的是 ):
stack.pop() // "["
map[")"] // "("
两者不相等,就说明括号类型或闭合顺序错误,直接:
return false
另外,如果遇到右括号时栈已经为空,pop() 会返回 undefined,同样会判断为无效。
用完整例子理解执行过程
以:
s = "([{}])"
为例,逐个遍历字符:
| 当前字符 | 执行操作 | 操作后的栈 |
|---|---|---|
( |
左括号入栈 | ["("] |
[ |
左括号入栈 | ["(", "["] |
{ |
左括号入栈 | ["(", "[", "{"] |
} |
弹出 {,匹配成功 |
["(", "["] |
] |
弹出 [,匹配成功 |
["("] |
) |
弹出 (,匹配成功 |
[] |
所有括号都匹配成功,最终栈为空,因此结果为:
true
反过来,如果字符串是:
s = "([)]"
当遍历到 ) 时,栈顶还是 [。
stack.pop() // "["
map[")"] // "("
二者不相等,立即返回 false。
这也说明:栈不仅能判断括号类型是否匹配,还能保证括号闭合顺序正确。
为什么最后还要判断 stack.length === 0?
代码最后:
return stack.length === 0
这是为了防止字符串中还有没有闭合的左括号。
例如:
s = "((("
遍历结束之后:
stack = ["(", "(", "("]
因为始终没有右括号,所以之前的匹配判断不会报错。
但是:
stack.length === 0
// false
说明还有左括号没有匹配成功。
如果所有括号都正确匹配,栈最终一定为空:
stack = []
stack.length === 0
// true
因此,判断有效括号需要同时满足两个条件:
-
每次遇到右括号,都能匹配最近的左括号。
-
所有字符遍历结束之后,栈必须为空。
时间复杂度与空间复杂度
时间复杂度:O(n)
代码只需要遍历一次字符串。每个字符最多入栈一次、出栈一次,因此整体时间复杂度是 O(n)。
空间复杂度:O(n)
最坏情况下,字符串全是左括号:
s = "((((("
所有字符都会进入栈中,因此需要最多 O(n) 的额外空间。
总结
这道题的核心是使用栈来维护尚未匹配的左括号。
javascript
var isValid = function(s) {
const stack = []
const map = {
')': '(',
']': '[',
'}': '{'
}
for (const ch of s) {
if (map[ch]) {
if (stack.pop() !== map[ch]) return false
} else {
stack.push(ch)
}
}
return stack.length === 0
}
整个算法可以概括为:
遍历字符串
↓
遇到左括号
↓
push() 入栈
↓
遇到右括号
↓
pop() 取出最近的左括号
↓
检查是否匹配
↓
不匹配 → false
↓
全部遍历结束
↓
栈为空 → true
栈不为空 → false
一句话记住:左括号入栈,右括号匹配栈顶,最后栈必须为空。
这也是栈解决括号匹配问题最经典的应用。