LeetCode 20简单栈
有效的括号 图解题解
括号嵌套乱成一团,怎么判断它是否合法?一个栈,从左扫到右,答案自然出来。
像叠盘子配锅盖:左括号就是放下一只锅,压在最上面;遇到右括号,就看最上面那口锅的盖子能不能盖上——能盖就把锅取走,不能盖就说明顺序乱了。最后锅架清空才算全部正确配对。后放的锅最先取,天然靠栈来维护「最近未闭合」的状态。
这道题到底在问什么
判断括号串是否正确闭合。每个左括号都要有对应的右括号,且顺序正确。
- s
- "([])"
- 输出
- true
最优解:一步一步想明白
- 3遇到左括号就压进栈;遇到右括号,就看栈顶的左括号能不能和它配成一对——能配就弹出栈顶,不能配就说明不合法。
- 4栈为空上面是要处理的字符串,下面是我们的栈。开始逐个字符处理。
- 5左括号 → 入栈'(' 是左括号,压入栈。栈:[ ( ]
- 6左括号 → 入栈'[' 也是左括号,压入栈。栈:[ (, [ ],栈顶是 '['。
- 7右括号 vs 栈顶 '['']' 是右括号。看栈顶是 '[',正好是一对!可以把栈顶弹出去。
- 8弹出 '['配对成功,弹出栈顶 '['。栈剩下:[ ( ]。
- 9右括号 vs 栈顶 '('')' 是右括号。栈顶是 '(',又是一对!弹出。
- 10弹出 '('弹出 '(',栈又空了。
- 11栈空 → 合法字符串走完,栈正好空了——说明每个左括号都找到了对应的右括号,返回 true。
- 12两种情况:① 右括号来了但栈顶配不上;② 字符串结束时栈还没空(有左括号没人配)。来看个反例。
- 13入栈处理 "(]":先把 '(' 压入栈。
- 14栈顶 '(' 配不上 ']'遇到 ']',可栈顶是 '(',对不上号——立刻返回 false。
- 15只有一种括号时,确实可以用一个计数器:左加一、右减一,全程不为负、最后归零就合法。但一旦有多种括号、还能交叉,比如 "([)]",光数个数就废了——来看这个最坑的例子。
- 16左括号 → 入栈先读第 0 个 '(',是左括号,压入栈。栈:[ ( ]。
- 17左括号 → 入栈,栈:[ (, [ ]再读第 1 个 '[',也是左括号,入栈。两个左括号都进来了。注意:要是只用计数器,此刻「左括号 = 2」,看不出谁套谁。
- 18栈顶是 '[',配不上 ')'读到 ')',它该配 '(';可最近的那个左括号是 '['——顺序乱了,返回 false。计数器只会说「左 2 右 1,还没数完」,根本发现不了这个交叉错误。栈记住了「最近的左括号是谁」,这正是计数器给不了的。
- 21凡是「最近出现的要最先被匹配/抵消」的问题,都该想到栈:逆波兰表达式、字符串解码、消消乐式的相邻消除……
- 27① 为什么用栈不用计数器?——只有一种括号能计数,但 "([)]" 这种多类型交叉必须靠栈记住「最近的左括号是谁」。② 能提前剪枝吗?——能:长度是奇数直接 false;右括号配不上立即 return。③ 为什么结束还要判栈空?——像 "(" 这种走完栈里还剩没配对的左括号,要返回 false。
- 28「栈 = 最近的先处理」是一整类题的内核:LC394 字符串解码(遇到 ] 弹出最近一段重复)、LC1047 相邻消除,再进阶到 LC739 / LC42 的单调栈(遇到更大的就回头结算)。学会这一道,栈这一类就入门了——更多在 栈专题 继续,或让小欧私教陪你把这套骨架套到下一题。
⚠️ 容易写错的地方
✗ 错:右括号不先判栈空就取 stack[-1]
✓ 对:先 if not stack: return False
像 "]" 开头时栈是空的,直接取会报错
✗ 错:循环结束直接 return True
✓ 对:return not stack
像 "(" 结束时栈还非空,应是 False
完整代码(Python / C++ / Java)
Python
class Solution:
def isValid(self, s):
stack = []
pairs = {')': '(', ']': '[', '}': '{'}
for ch in s:
if ch in pairs: # 右括号
if not stack or stack.pop() != pairs[ch]:
return False
else:
stack.append(ch) # 左括号入栈
return not stack # 栈空才合法C++
class Solution {
public:
bool isValid(string s) {
stack<char> st;
unordered_map<char,char> p = {{')','('},{']','['},{'}','{'}};
for (char ch : s) {
if (p.count(ch)) { // 右括号
if (st.empty() || st.top() != p[ch]) return false;
st.pop();
} else st.push(ch); // 左括号入栈
}
return st.empty();
}
};Java
class Solution {
public boolean isValid(String s) {
Deque<Character> st = new ArrayDeque<>();
Map<Character,Character> p = new HashMap<>();
p.put(')','('); p.put(']','['); p.put('}','{');
for (char ch : s.toCharArray()) {
if (p.containsKey(ch)) { // 右括号
if (st.isEmpty() || !st.pop().equals(p.get(ch))) return false;
} else st.push(ch); // 左括号入栈
}
return st.isEmpty();
}
}套路模板 · 配对/相邻抵消骨架(背下来)
# 凡是「相邻可抵消 / 最近优先匹配」都套这个骨架
stack = []
for x in 序列:
if 该入栈(x): # 把 x 留着等以后配
stack.append(x)
elif stack and 能抵消(stack[-1], x): # 和栈顶抵消
stack.pop() # 抵消就弹掉
else:
失败处理() # 栈空/对不上 → 视题 return 或 push x
return 收尾(stack) # 本题=要求栈空;别的题=返回栈内剩余// 凡是「相邻可抵消 / 最近优先匹配」都套这个骨架
stack<T> st;
for (auto& x : 序列) {
if (该入栈(x)) st.push(x); // 留着等以后配
else if (!st.empty() && 能抵消(st.top(), x))
st.pop(); // 抵消就弹掉
else 失败处理(); // 栈空/对不上
}
return 收尾(st); // 本题=要求 st.empty()// 凡是「相邻可抵消 / 最近优先匹配」都套这个骨架
Deque<T> st = new ArrayDeque<>();
for (T x : 序列) {
if (该入栈(x)) st.push(x); // 留着等以后配
else if (!st.isEmpty() && 能抵消(st.peek(), x))
st.pop(); // 抵消就弹掉
else 失败处理(); // 栈空/对不上
}
return 收尾(st); // 本题=要求 st.isEmpty()复杂度
时间复杂度
O(n)
每个字符最多入栈、出栈各一次 → O(n)
空间复杂度
O(n)
最坏全是左括号(如 "((((")全压栈 → O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 有效的括号 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
如果串里还混着字母、空格等非括号字符(如 "a(b)c")怎么办?+
在循环里只对括号做判断:遇到非括号字符直接跳过、不入栈也不出栈,其余逻辑不变。
进阶:LC921「使括号有效的最少添加」要返回还差几个,怎么改?+
不再一遇错就返回,而是统计——栈里最后剩的左括号数,加上途中遇到的「配不上的右括号」个数,两者之和就是要补的数量。骨架不变,把「return false」换成「计数 += 1」。
若题目改成只有一种括号(如 LC1021),还需要栈吗?+
不需要,一个计数器就够:左 +1、右 −1,全程不为负即合法。栈是为了应对多类型交叉才上的,单类型用计数器更省空间 O(1)。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。