算法背景
给定一个只包含 '(' 和 ')' 的字符串,找出其中最长有效(匹配)括号子串的长度。例如 "(()" 的最长有效括号子串是 "()",长度为2;而 ")()())" 的最长有效括号子串是 "()()",长度为4。
这个问题是字符串处理中的经典题目,主要考察对栈结构的理解以及动态规划的状态设计能力。
核心思路
单调栈法:维护一个栈,存储尚未匹配的括号下标。遇到 '(' 入栈,遇到 ')' 尝试出栈匹配。通过记录匹配区间的长度来更新最大值。
动态规划法:定义 dp[i] 为以第 i 个字符结尾的最长有效括号子串长度。根据 s[i] 是左括号还是右括号,以及前面已匹配的状态进行状态转移。
Java实现
import java.util.Stack;
/**
* 最长有效括号子串长度计算
* 提供单调栈法和动态规划法两种实现
*/
public class LongestValidParentheses {
/**
* 单调栈法
* 时间复杂度:O(n)
* 空间复杂度:O(n)
*/
public int stackMethod(String s) {
if (s == null || s.length() == 0) return 0;
Stack<Integer> stack = new Stack<>();
// 初始放入-1,作为基准边界
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;
}
/**
* 动态规划法
* 时间复杂度:O(n)
* 空间复杂度:O(n)
*/
public int dpMethod(String s) {
if (s == null || s.length() == 0) return 0;
int n = s.length();
// dp[i]表示以s[i]结尾的最长有效括号子串长度
int[] dp = new int[n];
int maxLen = 0;
for (int i = 1; i < n; i++) {
if (s.charAt(i) == ')') {
if (s.charAt(i - 1) == '(') {
// 情形1:...(),直接匹配一对
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 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) {
dp[i] += dp[i - dp[i - 1] - 2];
}
}
maxLen = Math.max(maxLen, dp[i]);
}
// 以左括号结尾,dp[i]保持为0
}
return maxLen;
}
/**
* 双指针法(最优空间复杂度)
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
public int twoPointerMethod(String s) {
if (s == null || s.length() == 0) return 0;
int left = 0, right = 0, maxLen = 0;
// 从左向右扫描
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;
// 从右向左扫描(处理左括号过多的情况)
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;
}
/**
* 主函数:测试与演示
*/
public static void main(String[] args) {
LongestValidParentheses solver = new LongestValidParentheses();
String[] testCases = {
"(()",
")()())",
"",
"((()))",
"()(()",
"())(()"
};
System.out.println("三种方法对比测试:\n");
for (String test : testCases) {
System.out.printf("输入: "%s"%n", test);
System.out.printf(" 单调栈法: %d%n", solver.stackMethod(test));
System.out.printf(" 动态规划法: %d%n", solver.dpMethod(test));
System.out.printf(" 双指针法: %d%n", solver.twoPointerMethod(test));
System.out.println();
}
}
}
复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 核心思想 |
|---|---|---|---|
| 单调栈法 | O(n) | O(n) | 利用栈维护未匹配括号的下标边界 |
| 动态规划法 | O(n) | O(n) | 状态转移记录以当前位置结尾的最长长度 |
| 双指针法 | O(n) | O(1) | 双向扫描统计左右括号数量平衡 |
双指针法是最优解,通过两次扫描分别处理右括号过多和左括号过多的情况,将空间复杂度优化到O(1)。
应用场景
- 编译器设计:检查代码括号匹配并定位错误位置
- 数学表达式解析:验证公式括号的合法性
- DNA序列分析:配对结构的最长匹配片段分析
总结
本文介绍了求解最长有效括号问题的三种Java实现:单调栈法直观易懂,动态规划法展示了良好的状态设计思想,双指针法则在空间复杂度上达到最优。掌握这三种方法有助于深刻理解栈、DP和双指针三大算法技巧在字符串问题中的灵活应用。