问题简介
题目描述
给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例说明
✅ 示例 1:
输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"
✅ 示例 2:
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
✅ 示例 3:
输入:s = ""
输出:0
解题思路
📌 本题要求找出最长连续有效括号子串的长度。有效括号即合法的括号匹配序列。
我们可以采用以下几种主流方法:
💡 方法一:动态规划(DP)
核心思想:
定义 dp[i] 表示以 s[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] + 2 + dp[i - dp[i-1] - 2]
- 情况1:
注意边界检查!
💡 方法二:栈(Stack)
核心思想:
用栈存储下标,初始压入 -1 作为基准。
- 遇到
'(':压入当前下标。 - 遇到
')':- 弹出栈顶。
- 若栈为空:说明当前
')'无法匹配,压入当前下标作为新基准。 - 否则:当前有效长度 =
i - stack.peek(),更新最大值。
栈中始终保存"最后一个未匹配的右括号位置"或起始点。
💡 方法三:双指针(左右扫描)
核心思想:
从左到右扫描,记录 left 和 right 括号数量:
- 当
left == right:更新最大长度为2 * right - 当
right > left:重置计数器(说明右括号多,非法)
但仅从左到右会漏掉如 "(()" 这种情况(左多右少),所以还需从右到左再扫一次 ,此时当 left > right 时重置。
两次扫描覆盖所有情况。
代码实现
java
// Java 实现
class Solution {
// 方法一:动态规划
public int longestValidParentheses(String s) {
if (s == null || s.length() < 2) return 0;
int n = s.length();
int[] dp = new int[n];
int maxLen = 0;
for (int i = 1; i < n; i++) {
if (s.charAt(i) == ')') {
if (s.charAt(i - 1) == '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
} else if (dp[i - 1] > 0) {
int matchIndex = i - dp[i - 1] - 1;
if (matchIndex >= 0 && s.charAt(matchIndex) == '(') {
dp[i] = dp[i - 1] + 2 + (matchIndex > 0 ? dp[matchIndex - 1] : 0);
}
}
maxLen = Math.max(maxLen, dp[i]);
}
}
return maxLen;
}
// 方法二:栈
public int longestValidParenthesesStack(String s) {
Stack<Integer> stack = new Stack<>();
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 {
maxLen = Math.max(maxLen, i - stack.peek());
}
}
}
return maxLen;
}
// 方法三:双指针
public int longestValidParenthesesTwoPass(String s) {
int left = 0, right = 0, maxLen = 0;
// Left to Right
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') left++;
else right++;
if (left == right) {
maxLen = Math.max(maxLen, 2 * right);
} else if (right > left) {
left = right = 0;
}
}
left = right = 0;
// Right to Left
for (int i = s.length() - 1; i >= 0; i--) {
if (s.charAt(i) == '(') left++;
else right++;
if (left == right) {
maxLen = Math.max(maxLen, 2 * left);
} else if (left > right) {
left = right = 0;
}
}
return maxLen;
}
}
go
// Go 实现
func longestValidParentheses(s string) int {
// 方法一:动态规划
if len(s) < 2 {
return 0
}
n := len(s)
dp := make([]int, n)
maxLen := 0
for i := 1; i < n; i++ {
if s[i] == ')' {
if s[i-1] == '(' {
if i >= 2 {
dp[i] = dp[i-2] + 2
} else {
dp[i] = 2
}
} else if dp[i-1] > 0 {
matchIndex := i - dp[i-1] - 1
if matchIndex >= 0 && s[matchIndex] == '(' {
dp[i] = dp[i-1] + 2
if matchIndex > 0 {
dp[i] += dp[matchIndex-1]
}
}
}
if dp[i] > maxLen {
maxLen = dp[i]
}
}
}
return maxLen
}
// 方法二:栈
func longestValidParenthesesStack(s string) int {
stack := []int{-1} // 基准
maxLen := 0
for i, ch := range s {
if ch == '(' {
stack = append(stack, i)
} else {
stack = stack[:len(stack)-1] // pop
if len(stack) == 0 {
stack = append(stack, i) // 新起点
} else {
if i-stack[len(stack)-1] > maxLen {
maxLen = i - stack[len(stack)-1]
}
}
}
}
return maxLen
}
// 方法三:双指针
func longestValidParenthesesTwoPass(s string) int {
left, right, maxLen := 0, 0, 0
// Left to Right
for i := 0; i < len(s); i++ {
if s[i] == '(' {
left++
} else {
right++
}
if left == right {
if 2*right > maxLen {
maxLen = 2 * right
}
} else if right > left {
left, right = 0, 0
}
}
left, right = 0, 0
// Right to Left
for i := len(s) - 1; i >= 0; i-- {
if s[i] == '(' {
left++
} else {
right++
}
if left == right {
if 2*left > maxLen {
maxLen = 2 * left
}
} else if left > right {
left, right = 0, 0
}
}
return maxLen
}
示例演示
以 s = ")()())" 为例:
-
DP 过程:
i: 0 1 2 3 4 5
s: ) ( ) ( ) )
dp:0 0 2 0 4 0 → max = 4 -
栈过程:
i=-1: stack=[-1]
i=0: ')' → pop → stack empty → push 0 → stack=[0]
i=1: '(' → push → [0,1]
i=2: ')' → pop → stack=[0] → len=2-0=2
i=3: '(' → push → [0,3]
i=4: ')' → pop → stack=[0] → len=4-0=4 → max=4
i=5: ')' → pop → stack empty → push 5 -
双指针:
-
左→右:在 i=4 时 left=2, right=2 → len=4
-
右→左:无更优解
✅ 所有方法均得 4
答案有效性证明
| 方法 | 正确性依据 |
|---|---|
| ✅ DP | 状态转移覆盖所有以 ) 结尾的有效子串,且利用已有最优解构建当前解 |
| ✅ 栈 | 利用栈天然匹配特性,通过下标差计算长度,基准点确保连续性 |
| ✅ 双指针 | 两次扫描分别处理"右多"和"左多"情况,数学上完备 |
所有方法均能处理边界如
"","(",")","(()",")()())"等。
复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 💡 动态规划 | O(n) | O(n) |
| 💡 栈 | O(n) | O(n) |
| 💡 双指针 | O(n) | O(1) ✅(最优空间) |
双指针法在空间上最优,适合内存敏感场景。
问题总结
📌 关键洞察:
- 有效括号必须以
')'结尾。 - 连续性要求不能跳过字符。
- 单向扫描会遗漏"左括号过剩"情况,需双向处理。
✅ 推荐解法:
- 面试首选 栈(直观、易写、通用性强)
- 追求空间效率用 双指针
- DP 适合展示动态规划思维
❌ 常见错误:
- 忘记边界检查(如
i-2 < 0) - 栈未初始化
-1 - 双指针只做单向扫描
github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions