【LeetCode Hot100 刷题日记 (90/100)】32. 最长有效括号 —— 字符串、栈、动态规划、双指针、双向贪心🧠

4 阅读6分钟

📌 题目链接:32. 最长有效括号 - 力扣(LeetCode)

🔍 难度:困难 | 🏷️ 标签:字符串、栈、动态规划、双指针、双向贪心

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(n)(DP/栈)或 O(1)(双向贪心)


🧠 题目分析

本题要求在只包含 '('')' 的字符串中,找出最长连续且格式正确的括号子串的长度。

“有效括号”定义:每个左括号都有唯一对应的右括号与之匹配,且顺序合法。例如 "(()())" 是有效的,而 "())(""(()" 则不是。

这道题是 LeetCode Hot 100 中极具代表性的括号匹配类问题,考察对状态转移、栈思想、边界处理的综合理解。面试中常作为中高级算法题出现,尤其在考察候选人是否能从多角度思考优化方案。


⚙️ 核心算法及代码讲解

本题有三种主流解法,每种都体现了不同的算法思想:

1️⃣ 方法一:动态规划(DP)—— 状态转移的艺术

📌 核心思想

定义 dp[i] 表示以索引 i 结尾的最长有效括号子串长度

  • 由于有效括号必须以 ')' 结尾,因此当 s[i] == '(' 时,dp[i] = 0
  • 分两种情况处理 s[i] == ')'
情况 1:s[i - 1] == '('

形如 ...(),此时:

dp[i] = dp[i - 2] + 2;
情况 2:s[i - 1] == ')'

形如 ...)),此时需检查 s[i - dp[i - 1] - 1] 是否为 '('

  • 若是,则当前 ')' 与该 '(' 匹配,形成更长的有效串:
dp[i] = dp[i - 1] + dp[i - dp[i - 1] - 2] + 2;

💡 关键点i - dp[i - 1] - 1 是与当前 ')' 匹配的 '(' 的位置。

✅ 代码(带详细行注释)

class Solution {
public:
    int longestValidParentheses(string s) {
        int maxans = 0, n = s.length();
        vector<int> dp(n, 0); // dp[i] 表示以 i 结尾的最长有效括号长度
        for (int i = 1; i < n; 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] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
                }
                maxans = max(maxans, dp[i]); // 更新全局最大值
            }
        }
        return maxans;
    }
};

🎯 面试加分点:能清晰解释 i - dp[i - 1] - 1 的含义,并画出状态转移图。


2️⃣ 方法二:栈(Stack)—— 括号匹配的经典结构

📌 核心思想

  • 栈中存储未匹配的括号下标

  • 初始压入 -1 作为“基准”,用于计算长度。

  • 遇到 '(':压入下标。

  • 遇到 ')'

    • 弹出栈顶(尝试匹配)。
    • 若栈空:说明当前 ')' 无匹配,将其下标入栈作为新基准。
    • 若栈非空:当前有效长度 = i - stk.top()

💡 为什么压入 -1?
处理从索引 0 开始的有效串,如 "()",此时 i=1stk.top()=-1,长度 = 1 - (-1) = 2

✅ 代码(带详细行注释)

class Solution {
public:
    int longestValidParentheses(string s) {
        int maxans = 0;
        stack<int> stk;
        stk.push(-1); // 基准下标,便于计算长度
        for (int i = 0; i < s.length(); i++) {
            if (s[i] == '(') {
                stk.push(i); // 左括号下标入栈
            } else {
                stk.pop(); // 尝试匹配右括号
                if (stk.empty()) {
                    stk.push(i); // 无匹配,当前右括号作为新基准
                } else {
                    maxans = max(maxans, i - stk.top()); // 计算当前有效长度
                }
            }
        }
        return maxans;
    }
};

🎯 面试加分点:能解释为何栈底始终是“最后一个未匹配右括号的下标”。


3️⃣ 方法三:双向贪心(Two Pass)—— O(1) 空间的巧妙解法

📌 核心思想

  • 正向遍历:统计 leftright

    • right > left:重置计数器(说明右括号过多,无法匹配)。
    • left == right:更新最大长度。
  • 反向遍历:解决 left 始终大于 right 的情况(如 "(((")。

    • left > right:重置。
    • left == right:更新最大长度。

💡 为什么需要两次遍历?
单次遍历会漏掉 left > right 但最终可能有效的场景(如 "(()" 正向无法得到答案,但反向可得)。

✅ 代码(带详细行注释)

class Solution {
public:
    int longestValidParentheses(string s) {
        int left = 0, right = 0, maxlength = 0;
        // 正向遍历:处理 right <= left 的情况
        for (int i = 0; i < s.length(); i++) {
            if (s[i] == '(') left++;
            else right++;
            if (left == right)
                maxlength = max(maxlength, 2 * right);
            else if (right > left)
                left = right = 0; // 重置
        }
        left = right = 0;
        // 反向遍历:处理 left <= right 的情况
        for (int i = (int)s.length() - 1; i >= 0; i--) {
            if (s[i] == '(') left++;
            else right++;
            if (left == right)
                maxlength = max(maxlength, 2 * left);
            else if (left > right)
                left = right = 0; // 重置
        }
        return maxlength;
    }
};

🎯 面试加分点:能指出该方法的空间优势(O(1)),并解释为何单次遍历不够。


🧩 解题思路(分步骤)

  1. 明确问题:找最长连续有效括号子串。

  2. 排除暴力:O(n³) 不可行。

  3. 考虑结构特性

    • 有效括号必须成对出现。
    • 右括号必须有对应左括号在其左侧。
  4. 选择合适工具

    • DP:记录以每个位置结尾的最优解。
    • 栈:天然适合括号匹配。
    • 贪心:利用计数器避免额外空间。
  5. 处理边界

    • 空字符串。
    • 全为 '('')'
    • 有效串在中间、开头或结尾。

📊 算法分析

方法时间复杂度空间复杂度优点缺点
动态规划O(n)O(n)思路清晰,易于扩展需额外数组
O(n)O(n)直观,符合括号匹配直觉需栈空间
双向贪心O(n)O(1)空间最优需两次遍历,逻辑稍绕

💼 面试建议

  • 先写栈解法(最直观)。
  • 再优化到 O(1) 空间(展示深度思考)。
  • 若时间允许可提 DP(展示多角度思维)。

💻 代码

C++ 完整代码

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class Solution {
public:
    int longestValidParentheses(string s) {
        int maxans = 0, n = s.length();
        vector<int> dp(n, 0);
        for (int i = 1; i < n; 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] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
                }
                maxans = max(maxans, dp[i]);
            }
        }
        return maxans;
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    
    Solution sol;
    cout << sol.longestValidParentheses("(()") << "\n";      // 输出: 2
    cout << sol.longestValidParentheses(")()())") << "\n";   // 输出: 4
    cout << sol.longestValidParentheses("") << "\n";         // 输出: 0
    cout << sol.longestValidParentheses("()(()") << "\n";    // 输出: 2
    
    return 0;
}

JavaScript 完整代码

/**
 * @param {string} s
 * @return {number}
 */
var longestValidParentheses = function(s) {
    let maxans = 0;
    const n = s.length;
    const dp = new Array(n).fill(0);
    
    for (let i = 1; i < n; 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] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
            }
            maxans = Math.max(maxans, dp[i]);
        }
    }
    return maxans;
};

// 测试
console.log(longestValidParentheses("(()"));      // 2
console.log(longestValidParentheses(")()())"));   // 4
console.log(longestValidParentheses(""));         // 0
console.log(longestValidParentheses("()(()"));    // 2

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!