LeetCode 第32题:最长有效括号

LeetCode 第32题:最长有效括号

题目描述

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

难度

困难

题目链接

点击在LeetCode中查看题目

图解思路

动态规划可视化

输入: "( ) ) ( ( ) )"
索引: 0 1 2 3 4 5 6
dp[i]: 0 2 0 0 0 2 4

解释:
dp[0] = 0  // '(' 不能单独形成有效括号
dp[1] = 2  // "()" 形成长度为2的有效括号
dp[2] = 0  // ')' 不能与前面形成有效括号
dp[3] = 0  // '(' 不能单独形成有效括号
dp[4] = 0  // '(' 不能单独形成有效括号
dp[5] = 2  // "()" 形成长度为2的有效括号
dp[6] = 4  // 可以和前面的"()"组合形成"(())"

栈方法可视化

输入串:  ( ) ) ( ( ) )
索引:    0 1 2 3 4 5 6

栈的变化过程:
[-1]           // 初始状态
[-1,0]         // 遇到'(',压入索引0
[-1]           // 遇到')',弹出栈顶,计算长度:1-(-1)=2
[2]            // 遇到')',重置栈底为2
[2,3]          // 遇到'(',压入索引3
[2,3,4]        // 遇到'(',压入索引4
[2,3]          // 遇到')',弹出栈顶,计算长度:5-3=2
[2]            // 遇到')',弹出栈顶,计算长度:6-2=4

动态规划状态转移

  1. 当遇到 '(' 时:
dp[i] = 0  // 不可能以左括号结尾
  1. 当遇到 ')' 且前一个是 '(' 时:
dp[i] = dp[i-2] + 2  // 形如 "...()"
  1. 当遇到 ')' 且前一个是 ')' 时:
if s[i-dp[i-1]-1] == '(':
    dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2]  // 形如 "((...))"

示例

示例 1:

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

示例 2:

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

示例 3:

输入:s = ""
输出:0

提示

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

解题思路

方法一:动态规划

动态规划是解决这类问题的经典方法。我们可以定义dp[i]表示以第i个字符结尾的最长有效括号的长度。

关键点:

  1. 当s[i]为'('时,dp[i]=0,因为不可能以左括号结尾
  2. 当s[i]为')'时,需要考虑两种情况:
    • s[i-1]为'(',形如"....()"
    • s[i-1]为')',形如".......))"

具体步骤:

  1. 创建dp数组,初始化为0
  2. 遍历字符串,当遇到')'时:
    • 如果前一个是'(',dp[i] = dp[i-2] + 2
    • 如果前一个是')',检查dp[i-1]前面的字符是否是'('

时间复杂度:O(n)
空间复杂度:O(n)

方法二:栈

使用栈可以很好地处理括号匹配问题。

具体步骤:

  1. 使用栈存储左括号的下标
  2. 遇到右括号时尝试匹配
  3. 通过下标差计算长度

方法三:双向扫描

这是一个非常巧妙的解法,不需要额外空间。

具体步骤:

  1. 从左到右扫描,统计左右括号数量
  2. 从右到左再次扫描
  3. 取两次扫描的最大值

代码实现

C# 实现(动态规划)

public class Solution {
    public int LongestValidParentheses(string s) {
        if (string.IsNullOrEmpty(s)) return 0;
  
        int maxLen = 0;
        int[] dp = new int[s.Length];
  
        for (int i = 1; i < s.Length; i++) {
            if (s[i] == ')') {
                if (s[i-1] == '(') {
                    dp[i] = (i >= 2 ? dp[i-2] : 0) + 2;
                } else if (i - dp[i-1] > 0 && s[i - dp[i-1] - 1] == '(') {
                    dp[i] = dp[i-1] + 2 + 
                            (i - dp[i-1] >= 2 ? dp[i - dp[i-1] - 2] : 0);
                }
                maxLen = Math.Max(maxLen, dp[i]);
            }
        }
  
        return maxLen;
    }
}

C# 实现(栈方法)

public class Solution {
    public int LongestValidParentheses(string s) {
        int maxLen = 0;
        Stack<int> stack = new Stack<int>();
        stack.Push(-1);
  
        for (int i = 0; i < s.Length; i++) {
            if (s[i] == '(') {
                stack.Push(i);
            } else {
                stack.Pop();
                if (stack.Count == 0) {
                    stack.Push(i);
                } else {
                    maxLen = Math.Max(maxLen, i - stack.Peek());
                }
            }
        }
  
        return maxLen;
    }
}

C# 实现(双向扫描)

public class Solution {
    public int LongestValidParentheses(string s) {
        int maxLen = 0;
        int left = 0, right = 0;
  
        // 从左到右扫描
        for (int i = 0; i < s.Length; i++) {
            if (s[i] == '(') left++;
            else right++;
    
            if (left == right) {
                maxLen = Math.Max(maxLen, 2 * right);
            } else if (right > left) {
                left = right = 0;
            }
        }
  
        // 从右到左扫描
        left = right = 0;
        for (int i = s.Length - 1; i >= 0; i--) {
            if (s[i] == '(') left++;
            else right++;
    
            if (left == right) {
                maxLen = Math.Max(maxLen, 2 * left);
            } else if (left > right) {
                left = right = 0;
            }
        }
  
        return maxLen;
    }
}

执行结果

动态规划版本:

  • 执行用时:72 ms
  • 内存消耗:36.3 MB

栈方法版本:

  • 执行用时:68 ms
  • 内存消耗:36.1 MB

双向扫描版本:

  • 执行用时:64 ms
  • 内存消耗:35.9 MB

代码亮点

  1. 🎯 三种解法各具特色,适用不同场景
  2. 💡 动态规划解法状态转移清晰,易于理解
  3. 🔍 栈方法实现优雅,代码简洁
  4. 🎨 双向扫描空间复杂度最优,思路巧妙

常见错误分析

  1. 🚫 忘记处理空字符串或长度为1的输入
  2. 🚫 动态规划中数组越界问题
  3. 🚫 栈方法中忘记初始化-1索引
  4. 🚫 双向扫描中重置条件判断错误

解法对比

解法 时间复杂度 空间复杂度 优点 缺点
动态规划 O(n) O(n) 思路清晰,易于理解 需要额外空间
栈方法 O(n) O(n) 实现简单,直观 需要额外空间
双向扫描 O(n) O(1) 空间复杂度最优 需要扫描两次

相关题目

posted @   旧厂街小江  阅读(1)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示