32. 最长有效括号

32. 最长有效括号

题目链接(困难)

题目描述

给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

数据范围:

  • 0 <= s.length <= 3 * 10^4
  • s[i] 为 '(' 或 ')'

示例

示例 1:

输入: s = "(()"
输出: 2
解释: 最长有效括号子串是 "()"。

示例 2:

输入: s = ")()())"
输出: 4
解释: 最长有效括号子串是 "()()"。

示例 3:

输入: s = ""
输出: 0

核心思路

本题要求最长连续有效括号子串,和 LC 20(判断整体是否有效)不同,需要找最长的一段。

四种经典解法:

  1. 栈:用栈存未匹配的下标,栈底保存「最后一个未匹配的右括号下标」,用当前下标减栈顶得到长度;
  2. 动态规划:dp[i] 表示「以 s[i] 结尾的最长有效括号长度」,按 s[i] 和 s[i-1] 的情况分类转移;
  3. 双指针(两次遍历):正向扫一遍统计左右括号数,反向扫一遍处理多余的左括号,空间 $O(1)$;
  4. 暴力:枚举所有子串判断有效性,$O(n^3)$,不推荐。

推荐:栈(思路直观)或双指针(空间最优)。


方法一:栈

思路及解法

核心思想:用栈保存未匹配的左括号下标,并额外在栈底放一个「最后一个未匹配的右括号下标」作为边界。

流程:

  1. 初始化栈 stack = [-1],-1 作为边界,表示「当前位置之前没有有效的起点」;
  2. 遍历字符串,对每个下标 i:
    • 若 s[i] == '(':把 i 入栈;
    • 若 s[i] == ')':
      • 先弹出栈顶(尝试匹配一个左括号);
      • 若弹出后栈为空,说明这个 ) 没有匹配的左括号,它成为新的边界,把 i 入栈;
      • 若弹出后栈非空,说明匹配成功,此时 i - stack[-1] 就是以 i 结尾的最长有效括号长度,更新答案。

为什么栈底保存边界:

  • 栈底元素是「最后一个未匹配的位置」;
  • 当前下标 i 减去栈底元素,正好是「从栈底之后到 i 的合法区间长度」。

举例:s = ")()())"

i s[i] 操作 栈 当前长度 ans
初始 — — [-1] — 0
0 ')' 弹出 -1,栈空,入栈 0 [0] — 0
1 '(' 入栈 1 [0, 1] — 0
2 ')' 弹出 1,栈非空,长度 2 - 0 = 2 [0] 2 2
3 '(' 入栈 3 [0, 3] — 2
4 ')' 弹出 3,栈非空,长度 4 - 0 = 4 [0] 4 4
5 ')' 弹出 0,栈空,入栈 5 [5] — 4

答案 4,正确。

代码

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        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])

        return ans

复杂度分析

  • 时间复杂度:$O(n)$,每个下标最多入栈、出栈一次。
  • 空间复杂度:$O(n)$,栈最多存 n 个下标。

方法二:动态规划

思路及解法

定义 dp[i] 表示「以 s[i] 结尾的最长有效括号长度」。

转移分类:

情况 1:s[i] == '('

  • 以左括号结尾不可能构成有效括号,dp[i] = 0。

情况 2:s[i] == ')'

  • 若 s[i-1] == '(':s[i-1] 和 s[i] 直接配对成 "()",长度为 2,加上 s[i-2] 结尾的有效长度:
  • 若 s[i-1] == ')':s[i-1] 已经形成了一段有效括号,长度为 dp[i-1]。要让它和 s[i] 配对,需要在 dp[i-1] 这段之前找一个左括号,位置是 i - dp[i-1] - 1:
    • 若 s[i - dp[i-1] - 1] == '(',配对成功:

其中 dp[i - dp[i-1] - 2] 是配对左括号之前的有效长度。

边界:i - dp[i-1] - 1 >= 0 才可能配对;i - dp[i-1] - 2 >= 0 才加上前面的长度。

答案:max(dp)。

举例:s = "(()())"

i s[i] 说明 dp[i]
0 '(' 左括号 0
1 '(' 左括号 0
2 ')' s[1]='(' 配对 dp[0] + 2 = 2
3 '(' 左括号 0
4 ')' s[3]='(' 配对 dp[2] + 2 = 4
5 ')' s[4]=')',往前找 i-dp[4]-1 = 0,s[0]='(' 配对 dp[4] + 2 + dp[-1] = 6

答案 6,正确。

代码

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        n = len(s)
        dp = [0] * n
        ans = 0

        for i in range(1, n):
            if s[i] == ')':
                if s[i - 1] == '(':
                    # 情况:s[i-1] 和 s[i] 直接配对
                    dp[i] = (dp[i - 2] if i >= 2 else 0) + 2
                elif i - dp[i - 1] - 1 >= 0 and s[i - dp[i - 1] - 1] == '(':
                    # 情况:s[i-1] 结尾有一段有效括号,往前找左括号配对
                    dp[i] = dp[i - 1] + 2
                    if i - dp[i - 1] - 2 >= 0:
                        dp[i] += dp[i - dp[i - 1] - 2]
                ans = max(ans, dp[i])

        return ans

复杂度分析

  • 时间复杂度:$O(n)$,遍历一次。
  • 空间复杂度:$O(n)$,dp 数组。

方法三:双指针(两次遍历,$O(1)$ 空间)✅ 推荐

思路及解法

核心思想:用两个计数器 left、right 分别统计当前区间内左、右括号的数量。

正向遍历:

  • 遇到 '(':left += 1;
  • 遇到 ')':right += 1;
  • 当 left == right 时,当前区间是有效的,更新 ans = max(ans, 2 * right);
  • 当 right > left 时,说明右括号多了,不可能再和前面的左括号匹配,把 left = right = 0 重置。

问题:正向遍历只能处理「右括号多余」的情况,无法处理「左括号多余」的情况。

例如 s = "(()",正向遍历:

  • left = 2, right = 1,从未出现 left == right,答案 0;
  • 但正确答案是 2("()")。

解决:反向遍历一遍,规则对称:

  • 当 left > right 时重置;
  • 当 left == right 时更新答案。

为什么两次遍历能覆盖所有情况:

  • 正向遍历能正确处理「右括号多余」的无效前缀;
  • 反向遍历能正确处理「左括号多余」的无效后缀;
  • 两者结合,覆盖所有有效子串。

代码

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        ans = 0

        # 正向遍历
        left = right = 0
        for ch in s:
            if ch == '(':
                left += 1
            else:
                right += 1
            if left == right:
                ans = max(ans, 2 * right)
            elif right > left:
                left = right = 0

        # 反向遍历
        left = right = 0
        for ch in reversed(s):
            if ch == '(':
                left += 1
            else:
                right += 1
            if left == right:
                ans = max(ans, 2 * left)
            elif left > right:
                left = right = 0

        return ans

复杂度分析

  • 时间复杂度:$O(n)$,两次遍历。
  • 空间复杂度:$O(1)$,只用两个计数器。

方法四:暴力法(不推荐)

思路及解法

枚举所有子串 s[i..j],判断是否为有效括号串,取最长的。

代码

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        def is_valid(sub: str) -> bool:
            bal = 0
            for ch in sub:
                bal += 1 if ch == '(' else -1
                if bal < 0:
                    return False
            return bal == 0

        n = len(s)
        ans = 0
        for i in range(n):
            for j in range(i + 2, n + 1, 2):   # 有效长度一定是偶数
                if is_valid(s[i:j]):
                    ans = max(ans, j - i)
        return ans

复杂度分析

  • 时间复杂度:$O(n^3)$,枚举 $O(n^2)$ 个子串,每个判断 $O(n)$。
  • 空间复杂度:$O(n)$,切片开销。

不推荐,仅作思路参考。


四种方法对比

方法 时间 空间 特点
栈 $O(n)$ $O(n)$ 思路直观,边界处理清晰
动态规划 $O(n)$ $O(n)$ 状态定义严谨,转移稍复杂
双指针 $O(n)$ $O(1)$ 空间最优,需两次遍历
暴力 $O(n^3)$ $O(n)$ 不推荐,仅作对比

推荐:

  • 面试开场:先讲栈,思路直观、易解释;
  • 追问优化空间:讲双指针,展示 $O(1)$ 空间的技巧;
  • 展示 DP 功底:讲动态规划,转移方程体现对结构的分情况分析。

关键细节

1. 栈底为什么放 -1

-1 是「虚拟的边界」,表示「当前位置之前没有有效的起点」。当栈弹出到空时,说明当前 ) 没有匹配,把它作为新的边界入栈。

长度计算:i - stack[-1] 而不是 i - stack[-1] + 1,因为 stack[-1] 是边界位置(不计入有效长度)。

2. 动态规划的两种情况

  • s[i-1] == '(':直接配对,dp[i] = dp[i-2] + 2;
  • s[i-1] == ')':s[i-1] 结尾有一段有效括号,往前找 i - dp[i-1] - 1 处的左括号配对,配对后还要加上这段之前的有效长度 dp[i - dp[i-1] - 2]。

易错点:边界判断 i - dp[i-1] - 1 >= 0 和 i - dp[i-1] - 2 >= 0 不能省。

3. 双指针为什么需要两次遍历

  • 正向遍历只能处理「右括号多余」:一旦 right > left,后面不可能与前面匹配,重置;
  • 反向遍历处理「左括号多余」:一旦 left > right,重置。

例子:s = "(()"

  • 正向:left=2, right=1,left > right 不重置,但也不更新答案,漏掉 "()";
  • 反向:s[2]=')' → right=1;s[1]='(' → left=1,left == right,答案 2。

两次遍历互补。

4. 有效括号长度一定是偶数

如果 j - i 是奇数,一定不可能是有效括号串,暴力法可以跳过。

5. 为什么答案是 max(dp) 而不是 dp[n-1]

有效括号子串不一定以最后一个字符结尾。例如 s = "()(()",dp 最后一位是 0,但答案在 dp[1] = 2。


总结

  • 核心问题:找最长连续有效括号子串;
  • 栈:
    • 栈底放边界 -1;
    • 遇 ) 弹出,栈空则当前 i 成为新边界;
    • 栈非空时 i - stack[-1] 为当前有效长度;
  • 动态规划:
    • dp[i] 表示以 i 结尾的最长有效长度;
    • 分 s[i-1] == '(' 和 s[i-1] == ')' 两种情况;
  • 双指针:
    • 正向 + 反向各遍历一次;
    • 正向处理右括号多余,反向处理左括号多余;
    • 空间 $O(1)$;
  • 通用套路:「最长有效括号」→ 栈 / DP / 双指针,三种都要会。

相关题目


32. 最长有效括号
https://mingsm17518.github.io/2026/10/26/刷题笔记/Leetcode/32. 最长有效括号/
作者
Ming
发布于
2026年10月26日
更新于
2026年10月26日
许可协议