最长有效括号 图解题解
最长有效括号子串藏在哪里?用栈记路标,每遇到非法括号重设起点,一次扫描把长度算清楚。
把每个字符的下标当「路标」压入栈:遇到左括号存路标等配对;遇到右括号先弹一个——弹完栈还有东西,说明配对成功,合法长度就是「当前位置」减去「栈顶剩余路标」;弹完栈空,说明这个右括号是新的非法分割墙,把它的下标压进去当新起点。一个哨兵 -1 先垫底,让第一段有效串也能用同一公式算出长度。
这道题到底在问什么
- 输入
- s = ")()())"
- 输出
- 4
- 输入
- s = "(()"
- 输出
- 2
最优解:为什么这么做
一句话答案:LeetCode 32 最长有效括号的经典解法是栈里存下标:先压入 -1 当边界哨兵,遇左括号压下标,遇右括号先弹栈——弹后栈非空就用 i 减新栈顶得到以 i 结尾的有效段长度,弹空则说明这个右括号是断点、把 i 压入当新边界。一次遍历,时间 O(n)、空间 O(n)。
最长有效括号这道题在问什么
在一个只含左右括号的串里,找最长的一段连续子串,使它是完全合法、能全部配对的括号串。注意两点:一是要求连续子串而不是子序列,中间一个落单的右括号就会把有效段拦腰切断;二是答案只要长度这个数字,不用输出子串本身。
会判合法为什么还不够,栈里为什么存下标
用栈判断括号串是否合法是基本功:左括号进栈,右括号消掉一个栈顶。但本题要的是长度,只知道「配上了」远远不够,还得知道这一段合法串从哪里开始。这逼出关键的一步改造:栈里不存括号字符,改存下标。位置信息进了栈,长度才有得算——某段合法串的长度,就是它右端点减去左端点前面那个位置。
于是问题聚焦成一件事:扫到位置 i 收获一个配对时,怎么快速知道「以 i 结尾的有效段」左端在哪。答案藏在栈的形态里。
栈底为什么始终是当前段左边界的前一位
算法维持这样一条不变量:栈底永远存着「当前有效段左边界的前一个下标」,栈里其余元素是还没配对的左括号下标。开局先压 -1,代表最初的有效段可以从下标 0 起步。扫描规则:遇左括号压 i 占位;遇右括号先弹一个——若弹后栈非空,说明配对成功,i - stack[-1] 就是以 i 结尾的最长有效长度,拿去刷新最大值;若弹空了,说明这个右括号没有搭档,它是新的断点,把 i 压入接任边界。
断点接任边界这步是灵魂:后面的有效段无论多长都跨不过这个孤儿右括号,所以从它之后重新起算,正好由「i 减栈底」自动兑现。
相邻有效段的拼接为什么自动发生
很多人担心 ()(()) 这种「两段挨着」的情形要不要特判,其实不用。以串 ()(()) 为例:扫到最后一个右括号时,中间所有配对的下标都已被成双弹掉,栈里只剩最初的 -1,i - (-1) = 6 一次量出整段长度。原因在于:只要两段之间没有断点,中间的下标就不会滞留在栈里,i 减栈顶天然跨过所有已配对的部分,合并不需要任何额外逻辑。
复杂度与常见翻车点
时间 O(n),每个字符至多进出栈一次;空间 O(n),最坏全是左括号全部压栈。三个高频错误:忘了先压 -1,第一段合法串的长度就量不出来;右括号弹空后忘记把 i 压回去,断点丢失、后面全部算错;以及栈里存字符而不是下标,长度无从谈起。若追求 O(1) 空间,还有左右各扫一遍的双计数器解法,以及 dp[i] 表示以 i 结尾最长有效长度的动态规划解法,思路殊途同归。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这条,下面每一帧都在套它。栈底永远是“当前这段的左边界”。
- 4上排是输入串(固定不变),下面竖栈里存的是“下标”。栈底 -1 是哨兵,代表“有效段的左边界在 -1 之后”。
- 5第 0 个是 ')',弹完栈空了 → 它没人配,自己当“断点”,把下标 0 压进去做新的左边界。
- 6第 1 个是 '(',还不知道谁来配它,先把下标 1 压栈占位。
- 7第 2 个是 ')',弹掉一个后栈顶是 0,说明从 1 到 2 这段是合法的,长度 2。当前最长 = 2。
- 8第 3 个是 '(',还不知道谁来配它,先把下标 3 压栈占位。
- 9第 4 个是 ')',弹掉一个后栈顶是 0,说明从 1 到 4 这段是合法的,长度 4。当前最长 = 4。
- 10第 5 个是 ')',弹完栈空了 → 它没人配,自己当“断点”,把下标 5 压进去做新的左边界。
- 11第 6 个是 '(',还不知道谁来配它,先把下标 6 压栈占位。
- 12第 7 个是 ')',弹掉一个后栈顶是 5,说明从 6 到 7 这段是合法的,长度 2。当前最长 = 4。
- 13第 8 个是 '(',还不知道谁来配它,先把下标 8 压栈占位。
- 14第 9 个是 '(',还不知道谁来配它,先把下标 9 压栈占位。
- 15第 10 个是 ')',弹掉一个后栈顶是 8,说明从 9 到 10 这段是合法的,长度 2。当前最长 = 4。
- 16第 11 个是 ')',弹掉一个后栈顶是 5,说明从 6 到 11 这段是合法的,长度 6。当前最长 = 6。
- 17第 12 个是 '(',还不知道谁来配它,先把下标 12 压栈占位。
- 18第 13 个是 ')',弹掉一个后栈顶是 5,说明从 6 到 13 这段是合法的,长度 8。当前最长 = 8。
- 19第 14 个是 '(',还不知道谁来配它,先把下标 14 压栈占位。
- 20第 15 个是 '(',还不知道谁来配它,先把下标 15 压栈占位。
- 21第 16 个是 ')',弹掉一个后栈顶是 14,说明从 15 到 16 这段是合法的,长度 2。当前最长 = 8。
- 22第 17 个是 '(',还不知道谁来配它,先把下标 17 压栈占位。
- 23第 18 个是 ')',弹掉一个后栈顶是 14,说明从 15 到 18 这段是合法的,长度 4。当前最长 = 8。
- 24第 19 个是 ')',弹掉一个后栈顶是 5,说明从 6 到 19 这段是合法的,长度 14。当前最长 = 14。
- 25走完全串,过程中量到的最大长度就是答案:最长有效括号子串长度 = 14。
⚠️ 容易写错的地方
✗ 错:栈里存括号字符
✓ 对:栈里存下标
要算长度必须知道位置,存字符算不出 i−栈顶
✗ 错:忘了先压 -1
✓ 对:初始压 -1 当边界
没有边界基准,第一段合法串就量不出长度
✗ 错:弹空后不补边界
✓ 对:弹空就把当前下标压进去
这个落单的 ) 是断点,后面的有效段要从它之后重新算
完整代码(Python / C++ / Java)
Python
def longestValidParentheses(s: str) -> int:
stack = [-1] # 先压左边界
best = 0
for i, c in enumerate(s):
if c == "(":
stack.append(i) # 压下标占位
else:
stack.pop()
if stack:
best = max(best, i - stack[-1])
else:
stack.append(i) # 新边界
return bestC++
int longestValidParentheses(string s){
stack<int> st; st.push(-1);
int best = 0;
for(int i = 0; i < (int)s.size(); i++){
if(s[i] == '(') st.push(i);
else {
st.pop();
if(!st.empty()) best = max(best, i - st.top());
else st.push(i);
}
}
return best;
}Java
public int longestValidParentheses(String s){
Deque<Integer> st = new ArrayDeque<>();
st.push(-1);
int best = 0;
for(int i = 0; i < s.length(); i++){
if(s.charAt(i) == '(') st.push(i);
else {
st.pop();
if(!st.isEmpty()) best = Math.max(best, i - st.peek());
else st.push(i);
}
}
return best;
}复杂度
时间
O(n)
每个字符进出栈各一次
空间
O(n)
最坏全是左括号都压下标
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最长有效括号 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么栈里存下标而不是括号?+
本题要的是“长度”,必须知道位置。栈底始终保存“当前有效段左边界的前一个下标”,用 i 减去它就直接得到这一段的长度。
除了栈还有别的解法吗?+
有。可以用 DP:dp[i] 表示以 i 结尾的最长有效长度,按 s[i]==')' 时看 s[i-1] 和 dp[i-1] 跨过去的位置转移;也可以左右各扫一遍用计数器,O(1) 空间。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最长有效括号 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。