最长有效括号 图解题解
最长有效括号子串藏在哪里?用栈记路标,每遇到非法括号重设起点,一次扫描把长度算清楚。
把每个字符的下标当「路标」压入栈:遇到左括号存路标等配对;遇到右括号先弹一个——弹完栈还有东西,说明配对成功,合法长度就是「当前位置」减去「栈顶剩余路标」;弹完栈空,说明这个右括号是新的非法分割墙,把它的下标压进去当新起点。一个哨兵 -1 先垫底,让第一段有效串也能用同一公式算出长度。
这道题到底在问什么
- 输入
- s = "(()"
- 输出
- 2
- 解释
- 最长有效括号子串是 "()"
- 输入
- s = ")()())"
- 输出
- 4
- 解释
- 最长有效括号子串是 "()()"
- 输入
- s = ""
- 输出
- 0
先想最直接的笨办法
枚举所有子串是 O(n 平方) 个,每个再扫一遍判断合法是 O(n),合起来 O(n 的三次方)。字符串长度到 3 万时直接超时。(动画第 3 步)
最优解:一步一步想明白
- 3枚举所有子串是 O(n 平方) 个,每个再扫一遍判断合法是 O(n),合起来 O(n 的三次方)。字符串长度到 3 万时直接超时。
- 4遇到 "(" 就压入它的下标;遇到 ")" 先弹一个。弹完后栈空,说明这个 ")" 是新的非法边界;栈不空,当前合法长度就是 i 减去栈顶下标。
- 5一开始先压一个哨兵 −1,代表字符串开始前的边界。这样从下标 0 开始的合法串,也能用 i 减栈顶算出长度。
- 6执行:stack = [-1]; ans = 0我们用例子 ")()())" 来演。先把哨兵 −1 压进栈,它代表「开头之前」这条看不见的左墙。
- 7执行:stack.pop()开头就是右括号,肯定配不上谁。代码统一处理:遇到 ")" 一律先 pop。这一弹,连哨兵 −1 都被弹掉了,栈空了。
- 8执行:if not stack: stack.append(0)弹完栈空,说明这个 ")" 没配上。把它自己的下标 0 压回去,当新的左墙——后面的合法段都不许跨过它。
- 9执行:stack.append(1)左括号还没被匹配,把它的下标 1 压栈。栈底那个 0 仍然是不能跨过的边界。
- 10执行:stack.pop()这个右括号配上了下标 1 的左括号,弹掉它。弹完后栈顶露出 0,它就是当前合法段左边的边界。
- 11执行:ans = max(ans, 2 − 0) = 2栈不空,关键的一步来了:长度不是「刚才那一对」,而是 i 减栈顶。以下标 2 结尾的合法段是 1..2,长度 2。
- 12执行:stack.append(3)又一个左括号,压入下标 3。它可能跟后面的右括号组成新的一对,也可能跟前面的合法段连起来。
- 13执行:stack.pop()下标 3 和 4 配成一对,弹掉 3。重点:别只盯这一对,弹完后看栈顶是谁,决定它能不能跟前面连上。
- 14执行:ans = max(ans, 4 − 0) = 4栈顶还是 0,说明从下标 1 到 4 中间没被任何非法右括号切断。所以这段是 "()()",长度 4 — 这正是「存下标」的威力。
- 15执行:stack.pop()最后一个右括号没有对应的左括号。弹掉栈顶 0 后栈空,说明它要成为新的非法边界。
- 16执行:stack.append(5)非法右括号只负责切断后面的连续段,它自己不更新答案。当前最好成绩仍是前面算出的 4。
- 17执行:return ans整串扫完,ans 里存的就是最长有效括号子串长度。对 ")()())" 答案是 4。
- 18回放:i=4 弹掉 3 → 栈顶=0 → 长度 4−0=4把整题压成一句话:右括号先弹,弹完看栈顶。栈空就把自己当新墙;栈不空,i 减栈顶就是以 i 结尾的最长合法段。这一步是全题唯一的命门,记住它就够了。
- 21一旦答案跟距离、长度、区间有关,栈里往往该存下标而不是值——这是括号长度题最值钱的一句迁移点。
- 23错误写法:ans = i − 刚弹掉的 3 = 4 − 3 = 1(漏算前面)在 i=4 这一步,如果用「i 减刚弹掉的下标 3」,只会得到这一对的长度,把前面连着的 "()" 漏掉,答案被压成更小的值。必须用弹完后的栈顶 0,才能把相邻合法段接上得到 4。
- 28先吃透 20 题括号匹配(存字符),再到 32 题最长有效(存下标),就能迁移到 84 柱状图最大矩形、739 每日温度这类「栈存下标算距离」的题。想系统练可以点右侧的栈专题,或问问 AI 助教「下标栈还能解哪些题」。
⚠️ 容易写错的地方
✗ 错:栈初始化为空 []
✓ 对:初始化为 stack = [-1]
合法段从下标 0 开始时,需要 −1 当左边界,否则第一段长度会少算 1 甚至越界。
✗ 错:右括号弹空后什么都不做
✓ 对:栈空时把当前右括号下标压回去
这个 ")" 是新的非法边界,后面的连续合法段不能跨过它,必须留下当墙。
✗ 错:用 i − 被弹下标 算长度
✓ 对:弹完后用 i − stack[-1]
被弹的是刚匹配的左括号,真正的左边界在弹完之后的新栈顶,能让相邻合法段连起来。
✗ 错:栈里存括号字符 "(" ")"
✓ 对:本题栈里存下标
答案要的是长度,只有保留下标才能直接相减算出长度。
完整代码(Python / C++ / Java)
Python
class Solution:
def longestValidParentheses(self, s):
stack = [-1] # 哨兵:开头前的边界
ans = 0
for i, ch in enumerate(s):
if ch == '(':
stack.append(i) # 左括号压下标
else: # 右括号
stack.pop() # 先弹一个
if not stack: # 弹空了
stack.append(i) # 自己当新边界
else:
ans = max(ans, i - stack[-1]) # i 减栈顶
return ansC++
class Solution {
public:
int longestValidParentheses(string s) {
vector<int> st{-1}; // 哨兵边界
int ans = 0;
for (int i = 0; i < (int)s.size(); i++) {
if (s[i] == '(') st.push_back(i);
else { // 右括号
st.pop_back();
if (st.empty()) st.push_back(i);
else ans = max(ans, i - st.back());
}
}
return ans;
}
};Java
class Solution {
public int longestValidParentheses(String s) {
Deque<Integer> stack = new ArrayDeque<>();
stack.push(-1); // 哨兵边界
int ans = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') stack.push(i);
else { // 右括号
stack.pop();
if (stack.isEmpty()) stack.push(i);
else ans = Math.max(ans, i - stack.peek());
}
}
return ans;
}
}套路模板:下标栈算长度/距离(挖空骨架)
stack = [-1] # 哨兵:第一段/第一个距离的起点边界
ans = 0
for i, ch in enumerate(s):
if 是入栈条件(ch): # 如 '(' / 比栈顶高 / 未匹配
stack.append(i)
else:
stack.pop() # 先弹掉被结算的那个
if not stack: # 弹空 → 当前 i 是新边界
stack.append(i)
else:
ans = max(ans, i - stack[-1]) # 用「弹完后的栈顶」算跨度
return ansvector<int> st{-1}; // 哨兵边界
int ans = 0;
for (int i = 0; i < (int)s.size(); i++) {
if (/* 入栈条件 */) st.push_back(i);
else {
st.pop_back();
if (st.empty()) st.push_back(i); // 新边界
else ans = max(ans, i - st.back()); // 弹完后栈顶
}
}
return ans;Deque<Integer> st = new ArrayDeque<>();
st.push(-1); // 哨兵边界
int ans = 0;
for (int i = 0; i < s.length(); i++) {
if (/* 入栈条件 */) st.push(i);
else {
st.pop();
if (st.isEmpty()) st.push(i); // 新边界
else ans = Math.max(ans, i - st.peek()); // 弹完后栈顶
}
}
return ans;复杂度
时间复杂度
O(n)
扫一遍字符串;每个下标至多入栈一次、出栈一次,总操作不超过 2n。
空间复杂度
O(n)
最坏全是左括号(如 "(((("),栈要保存所有下标。
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最长有效括号 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
栈里到底存的是什么?为什么要哨兵 −1?+
存「还没匹配的下标」和「最近非法右括号的边界下标」。哨兵 −1 代表开头前的虚拟边界,让从 0 开始的合法段也能用 i 减栈顶直接算长度。
栈空时为什么要把当前 i 压进去?+
当前右括号没人匹配,是新的非法边界。压进去后,它就成了后面合法段不能跨过的左墙,保证不同合法段不会被错误地连起来。
能不能不用栈、用 O(1) 空间做?+
能。正向扫一遍维护 left/right 两个计数器:right==left 时更新答案,right 大于 left 时清零;再反向扫一遍处理「左括号偏多」的情况。两遍线性扫,空间 O(1),是高频追问的优化点。
DP 解法怎么定义状态?+
dp[i] = 以 i 结尾的最长有效长度。s[i]==')' 时:若 s[i-1]=='(' 则 dp[i]=dp[i-2]+2;若 s[i-1]==')' 且 s[i-dp[i-1]-1]=='(' 则 dp[i]=dp[i-1]+2+dp[i-dp[i-1]-2]。时间 O(n)、空间 O(n)。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。