LeetCode 32. 最长有效括号

题目描述

给你一个只包含 '('')' 的字符串,找出最长有效括号子串的长度。

有效括号要求左右括号正确匹配,并且子串必须是连续的。

例如:

复制代码
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"

最初思路

一开始想到的是双指针:

复制代码
让 l 指向合法括号区间里的 '('
让 r 指向可以匹配的 ')'

但实际写的时候会发现,l 怎么移动并不好处理。因为有效括号不仅要求数量匹配,还要求中间的结构也匹配,比如 ")()""())()" 这类情况会打断连续区间。

后面改成用栈记录左括号下标,匹配成功后标记有效位置,再统计最长连续有效区间。

问题出在哪里

第一版栈写法中,遇到 ')' 时直接执行匹配:

复制代码
isValid[i] = isValid[st[top--]] = true

但如果当前右括号前面没有可以匹配的左括号,top 就是 -1

例如:

复制代码
s = ")()"

第一轮遍历时:

复制代码
i = 0
s[i] = ')'
top = -1

这时访问 st[top] 就会访问非法下标。

所以遇到 ')' 时,必须先判断栈里是否还有未匹配的 '('

正确思路

这个解法分成两步:

复制代码
1. 用栈找出所有能成功匹配的括号位置
2. 扫描标记数组,统计最长连续 true 的长度

栈里保存的是还没有被匹配的 '(' 的下标。

遍历字符串时:

复制代码
遇到 '(':
    把当前下标入栈

遇到 ')':
    如果栈非空,说明可以和最近的一个 '(' 匹配
    把当前 ')' 和栈顶 '(' 的位置都标记成有效
    然后弹出这个 '('

最后,所有被标记为 true 的位置都属于某个有效括号片段。最长有效括号子串,就是最长连续 true 的长度。

关键不变量

栈中的下标始终表示:

复制代码
已经遍历过,但还没有被匹配的 '(' 的下标。

因此,当遇到一个 ')'top >= 0 时:

复制代码
st[top] 就是当前 ')' 能匹配到的最近一个未匹配 '(' 的下标。

匹配成功后执行 top--,这个 '(' 就从栈里弹出,后面不会再被别的 ')' 重复匹配。

手推过程

以这个字符串为例:

复制代码
s = "())()"

遍历过程:

复制代码
i = 0, '(' 入栈,栈中有 [0]
i = 1, ')' 匹配 st[top] = 0,标记 0 和 1
i = 2, ')' 但栈空,无法匹配
i = 3, '(' 入栈,栈中有 [3]
i = 4, ')' 匹配 st[top] = 3,标记 3 和 4

标记数组可以理解为:

复制代码
下标: 0 1 2 3 4
字符: ( ) ) ( )
标记: T T F T T

最长连续 true 的长度是 2,所以答案是 2

边界处理

需要特别注意两类边界:

复制代码
1. 当前字符是 ')',但栈为空:不能匹配,直接跳过
2. 字符串为空,或者没有任何有效括号:答案应该是 0

所以 ans 初始值应该设为 0,而不是 -1

伪代码

下面的伪代码与后面的 Java 实现一一对应:

复制代码
n = 字符串长度
创建数组 st 作为栈,保存 '(' 的下标
top = -1
创建布尔数组 isValid,标记每个位置是否属于有效括号

遍历字符串中的每个下标 i:
    如果 s[i] 是 '(':
        top 加 1
        st[top] = i
    否则如果 top >= 0:
        把 i 标记为有效
        把 st[top] 标记为有效
        top 减 1

ans = 0
cnt = 0
遍历 isValid:
    如果当前位置为 true:
        cnt 加 1
        用 cnt 更新 ans
    否则:
        cnt = 0

返回 ans

Java 代码

复制代码
class Solution {
    public int longestValidParentheses(String s) {
        int n = s.length();
        int[] st = new int[n];
        int top = -1;
        boolean[] isValid = new boolean[n];
        char[] c = s.toCharArray();

        for (int i = 0; i < n; i++) {
            if (c[i] == '(') {
                st[++top] = i;
            } else if (top >= 0) {
                isValid[i] = isValid[st[top--]] = true;
            }
        }

        int ans = 0;
        int cnt = 0;
        for (boolean v : isValid) {
            if (v) {
                cnt++;
                ans = Math.max(ans, cnt);
            } else {
                cnt = 0;
            }
        }

        return ans;
    }
}

易错点

1. 空栈时不能匹配右括号

遇到 ')' 时,不能直接访问 st[top]

必须先判断:

复制代码
top >= 0

否则像 ")()" 这种以右括号开头的字符串,会在第一轮就访问非法下标。

2. st[top] 保存的是尚未匹配的左括号

st[top] 不是任意一个左括号,而是最近的、还没有被匹配的 '(' 下标。

匹配成功后:

复制代码
top--

这个左括号就已经被使用,后面不能再匹配。

3. 最长的是连续有效区间

标记为 true 的位置必须连续,才属于同一个有效括号子串。

例如:

复制代码
s = "())()"
标记:T T F T T

虽然一共有四个位置被标记为有效,但中间被 F 断开,所以最长长度是 2,不是 4

4. 答案初始值应该是 0

如果字符串为空,或者没有任何有效括号,比如:

复制代码
s = ""
s = ")))"

答案都应该是 0

建议测试用例

复制代码
s = ")()"
输出:2

s = "())()"
输出:2

s = ")()())"
输出:4

s = ""
输出:0

s = ")))"
输出:0

复杂度分析

  • 时间复杂度:O(n),一次遍历字符串做匹配,一次遍历标记数组统计最长连续有效区间。
  • 空间复杂度:O(n),使用了栈数组和 isValid 标记数组。

总结

这题的关键是先把"括号是否匹配"和"有效子串是否连续"拆开。

栈负责解决匹配问题:

复制代码
当前 ')' 只能匹配最近的、尚未匹配的 '('

isValid 数组负责解决连续长度问题:

复制代码
被标记的位置连续多少个,最长有效括号子串就是多少。

这份写法虽然不是最省空间的栈写法,但思路清晰:先标记所有有效位置,再统计最长连续有效区间。

相关推荐
倒头就睡的小比特2 天前
算法竞赛C++常用的STL
c++·算法
小羊没烦恼!2 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
猎头南楼2 天前
知识社区推荐系统实践:新用户冷启动与长短期兴趣建模的挑战 资深推荐算法工程师
人工智能·深度学习·算法·机器学习
旖旎夜光2 天前
力控面试题 01.01: 判定字符是否唯一(位运算) —— 题解
c++·学习·算法·leetcode·力控
wzdark2 天前
大规模并行计算中的负载均衡算法研究4
算法
Because_of_Her12 天前
并查集-听课笔记
笔记·算法·并查集
码流子2 天前
高速公路安全监测实践:碰撞监测预警+物联网底座,从感知到处置的闭环
大数据·人工智能·物联网·算法·架构
another heaven2 天前
【算法/C++ MD5算法能否逆解码?原理、C++实现与同类哈希算法对比】
c++·算法·哈希算法
wzdark2 天前
从算法设计模式看编程思维的抽象能力4
算法
2601_962218612 天前
万象生鲜系统称重自动多退少补算法解决生鲜非标品痛点
大数据·数据库·人工智能·python·算法