LeetCode 32. 最长有效括号
问题描述
给定一个仅包含 '('
和 ')'
的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例:
示例 1:
输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"
示例 2:
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
示例 3:
输入:s = ""
输出:0
算法思路
动态规划(DP):
- 状态定义:
dp[i]
表示以s[i]
结尾的最长有效括号长度 - 状态转移:
- 当
s[i] = ')'
时:- 若
s[i-1] = '('
:dp[i] = dp[i-2] + 2
- 若
s[i-1] = ')'
且s[i - dp[i-1] - 1] = '('
:dp[i] = dp[i-1] + 2 + dp[i - dp[i-1] - 2];
- 若
- 当
- 初始化:
dp[0] = 0
- 结果:
max(dp[i])
栈方法:
- 栈底元素:始终存储最后一个未匹配的右括号位置(初始为
-1
) - 遍历字符串:
- 遇
'('
:压入当前索引 - 遇
')'
:- 弹出栈顶(匹配一个左括号)
- 栈空:压入当前索引(作为新边界)
- 栈非空:计算当前有效长度 =
i - stack.peek()
- 遇
代码实现
方法一:动态规划
class Solution {
public int longestValidParentheses(String s) {
int n = s.length();
if (n < 2) return 0;
// dp[i] 表示以 s.charAt(i) 结尾的最长有效括号长度
int[] dp = new int[n];
int maxLen = 0;
for (int i = 1; i < n; i++) {
if (s.charAt(i) == ')') {
// 情况1:前一个字符是 '(',形成 "()",如 "(()"
if (s.charAt(i - 1) == '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
}
// 情况2:前一个字符是 ')',形成 "))",如 "()(()())"
else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
// 核心公式:当前有效长度 = 内部有效长度 + 2 + 前面的有效长度
dp[i] = dp[i - 1] + 2;
if (i - dp[i - 1] - 2 >= 0) {
dp[i] += dp[i - dp[i - 1] - 2];
}
}
maxLen = Math.max(maxLen, dp[i]);
}
// 以 '(' 结尾的子串无效,dp[i] 保持为 0
}
return maxLen;
}
}
方法二:栈方法(推荐)
class Solution {
public int longestValidParentheses(String s) {
Deque<Integer> stack = new ArrayDeque<>();
stack.push(-1); // 初始边界,表示最后一个未匹配右括号位置
int maxLen = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') {
// 遇到左括号,压入当前索引
stack.push(i);
} else {
// 遇到右括号,弹出栈顶元素(匹配一个左括号)
stack.pop();
if (stack.isEmpty()) {
// 栈空说明当前右括号无匹配,作为新边界
stack.push(i);
} else {
// 计算当前有效长度:i - 栈顶元素(即有效子串起始位置)
maxLen = Math.max(maxLen, i - stack.peek());
}
}
}
return maxLen;
}
}
算法分析
- 时间复杂度:O(n)
- 两种方法都只需遍历字符串一次
- 空间复杂度:
- 动态规划:O(n)
- 栈方法:O(n)(最坏情况全为
'('
)
算法过程(栈方法)
s = ")()())"
:
- 初始化:
stack = [-1]
,maxLen=0
- i=0:
')'
→ 弹出-1
→ 栈空 → 压入0
→stack=[0]
- i=1:
'('
→ 压入1
→stack=[0,1]
- i=2:
')'
→ 弹出1
→ 栈非空 →len=2-0=2
→maxLen=2
- i=3:
'('
→ 压入3
→stack=[0,3]
- i=4:
')'
→ 弹出3
→ 栈非空 →len=4-0=4
→maxLen=4
- i=5:
')'
→ 弹出0
→ 栈空 → 压入5
→stack=[5]
- 结果:
maxLen=4
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
String s1 = ")()())";
System.out.println("Test 1: " + solution.longestValidParentheses(s1)); // 4
// 测试用例2:嵌套括号
String s2 = "(()())";
System.out.println("Test 2: " + solution.longestValidParentheses(s2)); // 6
// 测试用例3:不连续有效括号
String s3 = "()(()";
System.out.println("Test 3: " + solution.longestValidParentheses(s3)); // 2
// 测试用例4:全无效
String s4 = "))))";
System.out.println("Test 4: " + solution.longestValidParentheses(s4)); // 0
// 测试用例5:空字符串
String s5 = "";
System.out.println("Test 5: " + solution.longestValidParentheses(s5)); // 0
// 测试用例6:单对括号
String s6 = "()";
System.out.println("Test 6: " + solution.longestValidParentheses(s6)); // 2
// 测试用例7:复杂嵌套
String s7 = "()(())(";
System.out.println("Test 7: " + solution.longestValidParentheses(s7)); // 6
}
关键点
-
动态规划核心逻辑:
"()"
型:dp[i] = dp[i-2] + 2
"))"
型:dp[i] = dp[i-1] + 2 + dp[i - dp[i-1] - 2]
-
栈方法核心逻辑:
- 栈底始终保存最后一个未匹配右括号位置
- 遇到
')'
时:- 弹出后栈空 → 压入当前索引(新边界)
- 栈非空 → 更新最大长度
-
边界处理:
- 动态规划:注意
i-2
和i-dp[i-1]-2
的索引有效性 - 栈方法:初始压入
-1
处理边界计算
- 动态规划:注意
常见问题
-
为什么栈方法要压入
-1
?- 提供初始边界,使
i - stack.peek()
能正确计算从开头开始的子串长度
- 提供初始边界,使
-
动态规划中如何处理
"))"
型?- 需要检查跨过内部有效子串前的字符是否为
'('
- 累加内部有效长度(
dp[i-1]
)和前面的有效长度(dp[i - dp[i-1] - 2]
)
- 需要检查跨过内部有效子串前的字符是否为
-
哪种方法更优?
- 栈方法更直观易实现,动态规划空间效率略高(但栈方法更常用)