📌 题目链接: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=1,stk.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) 空间的巧妙解法
📌 核心思想
-
正向遍历:统计
left和right。- 当
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)),并解释为何单次遍历不够。
🧩 解题思路(分步骤)
-
明确问题:找最长连续有效括号子串。
-
排除暴力:O(n³) 不可行。
-
考虑结构特性:
- 有效括号必须成对出现。
- 右括号必须有对应左括号在其左侧。
-
选择合适工具:
- DP:记录以每个位置结尾的最优解。
- 栈:天然适合括号匹配。
- 贪心:利用计数器避免额外空间。
-
处理边界:
- 空字符串。
- 全为
'('或')'。 - 有效串在中间、开头或结尾。
📊 算法分析
| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 动态规划 | 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!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!