1. 最长有效括号问题解析
最长有效括号是LeetCode上经典的动态规划问题,编号32题。给定一个仅包含'('和')'的字符串,要求找出其中最长的有效括号子串的长度。有效括号字符串需满足:
- 每个左括号'('都有对应的右括号')'匹配
- 匹配的括号对必须正确嵌套
这个问题在面试中经常出现,因为它能很好地考察候选人对动态规划思想的理解和应用能力。我在实际面试和刷题过程中发现,很多同学第一次遇到这个问题时容易陷入暴力解法的误区,或者无法正确推导状态转移方程。
2. 动态规划解法详解
2.1 DP数组定义
我们定义dp[i]表示以第i个字符结尾的最长有效括号子串的长度。注意这里的关键是"以第i个字符结尾",这种定义方式在解决子串/子数组问题时非常常见。
初始化时,dp数组所有元素设为0,因为单个字符不可能形成有效括号对。对于字符串s,我们从索引1开始遍历(因为索引0无法形成括号对)。
2.2 状态转移分析
状态转移方程需要考虑两种情况:
- 当s[i]=')'且s[i-1]='('时:
code复制dp[i] = dp[i-2] + 2
这种情况处理的是形如"()"的简单匹配。此时当前有效长度为前i-2个字符的最长有效长度加上新匹配的这对括号。
- 当s[i]=')'且s[i-1]=')'时:
code复制if s[i-dp[i-1]-1] == '(':
dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2]
这种情况处理的是形如"(())"的嵌套匹配。我们需要检查当前')'是否有对应的'('可以匹配。
2.3 代码实现解析
typescript复制function longestValidParentheses(s: string): number {
let n = s.length;
const dp = new Array(n + 1).fill(0); // dp数组多一位方便处理边界
let ans = 0;
for(let i = 1; i < n; i++) {
if(s[i] === ')') {
if(s[i-1] === '(') {
dp[i+1] = dp[i-1] + 2; // 情况1
} else if(i-dp[i]-1 >= 0 && s[i-dp[i]-1] === '(') {
dp[i+1] = dp[i] + 2 + dp[i-dp[i]-1]; // 情况2
}
ans = Math.max(ans, dp[i+1]);
}
}
return ans;
};
注意:代码中dp数组的索引比常规多一位,这是为了避免处理i-2时的边界条件。实际面试中可以和面试官讨论这种处理方式。
3. 栈解法详解
3.1 栈的基本思路
除了动态规划,这个问题还可以用栈来解决。栈解法的核心思想是:
- 遇到'('时将其索引入栈
- 遇到')'时弹出栈顶元素
- 栈中始终保持最后一个未匹配的')'的索引
这种方法的时间复杂度也是O(n),但空间复杂度在最坏情况下会达到O(n)。
3.2 栈解法实现
typescript复制function longestValidParentheses(s: string): number {
let maxLen = 0;
const stack: number[] = [];
stack.push(-1); // 初始化栈底
for(let i = 0; i < s.length; i++) {
if(s[i] === '(') {
stack.push(i);
} else {
stack.pop();
if(stack.length === 0) {
stack.push(i); // 更新最后一个未匹配的')'索引
} else {
maxLen = Math.max(maxLen, i - stack[stack.length-1]);
}
}
}
return maxLen;
};
3.3 两种方法对比
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 动态规划 | O(n) | O(n) | 适合习惯DP思维的情况 |
| 栈 | O(n) | O(n) | 代码更简洁直观 |
4. 常见错误与调试技巧
4.1 典型错误案例
- 边界条件处理不当:
- 忘记处理空字符串情况
- 索引越界(特别是在处理i-dp[i]-1时)
- 初始化dp数组长度不足
- 状态转移方程错误:
- 忘记考虑嵌套情况(如"(())")
- 在情况2中忘记加上dp[i-dp[i]-1]
4.2 调试技巧
- 使用小测试用例逐步验证:
- 先测试"()"
- 再测试"()()"
- 然后测试"(())"
- 最后测试复杂情况"()(())"
- 打印dp数组:
typescript复制console.log(dp); // 查看dp数组变化
- 使用TypeScript的类型检查:
typescript复制interface TestCase {
input: string;
expected: number;
}
const testCases: TestCase[] = [
{input: "(()", expected: 2},
{input: ")()())", expected: 4},
{input: "", expected: 0}
];
5. 性能优化与变种问题
5.1 空间优化
动态规划解法可以优化到O(1)空间复杂度,通过观察可以发现我们只需要前几个dp值:
typescript复制function longestValidParentheses(s: string): number {
let left = 0, right = 0, maxLen = 0;
// 从左向右扫描
for(let i = 0; i < s.length; i++) {
if(s[i] === '(') left++;
else right++;
if(left === right) {
maxLen = Math.max(maxLen, 2 * right);
} else if(right > left) {
left = right = 0;
}
}
// 从右向左扫描
left = right = 0;
for(let i = s.length-1; i >= 0; i--) {
if(s[i] === '(') left++;
else right++;
if(left === right) {
maxLen = Math.max(maxLen, 2 * left);
} else if(left > right) {
left = right = 0;
}
}
return maxLen;
};
5.2 变种问题
- 统计所有有效括号子串数量
- 找出所有最长有效括号子串
- 包含多种括号类型的情况(如{}、[])
6. 面试实战建议
在面试中遇到这个问题时,建议采取以下步骤:
- 先明确问题要求,确认输入输出
- 提出暴力解法并分析其复杂度
- 逐步优化到动态规划或栈解法
- 边写代码边解释思路
- 主动提出测试用例并验证
提示:面试官可能会追问如何修改代码来处理其他类型的括号(如{}、[]),这时可以扩展栈解法,为每种括号类型维护单独的栈或状态。
我在实际刷题和面试中发现,理解这个问题的关键在于把大问题分解为子问题,并找到正确的状态转移方式。多次练习后,你会发现这类括号匹配问题都有相似的解决模式。