题目
来源:LeetCode.
给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例 1:
输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"
示例 2:
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
示例 3:
输入:s = ""
输出:0
提示:
- 0 < = s . l e n g t h < = 3 ∗ 1 0 4 0 <= s.length <= 3 * 10^4 0<=s.length<=3∗104
- s [ i ] s[i] s[i] 为 ‘(’ 或 ‘)’
接下来看一下解题思路:
思路一:栈:
可以在遍历给定字符串的过程中去判断到目前为止扫描的子串的有效性,同时能得到最长有效括号的长度。
具体做法是我们始终保持栈底元素为当前已经遍历过的元素中「最后一个没有被匹配的右括号的下标」,这样的做法主要是考虑了边界条件的处理,栈里其他元素维护左括号的下标:
- 如果是 ‘(’,将下标入栈;
- 如果是 ‘)’,弹出栈顶元素,表示匹配当前右括号;
- 如果栈为空,说明当前右括号没有匹配的左括号,将下标放入栈中来更新(最后一个没有被匹配的右括号下标)
- 如果栈不为空,当前右括号下标减去栈顶元素即为(以该右括号结尾的最长有效括号长度);
从前往后遍历字符串并更新答案即可。
注意,如果一开始栈为空,第一个字符为左括号的时候我们会将其放入栈中,这样就不满足提及的「最后一个没有被匹配的右括号的下标」,为了保持统一,我们在一开始的时候往栈中放入一个值为
−
1
−1
−1 的元素。
public static int longestValidParentheses(String s) {
if (s == null || s.length() <= 0) {
return 0;
}
int n = s.length();
if (n < 2) {
return 0;
}
Stack<Integer> stack = new Stack<>();
int max = 0;
stack.push(-1);
//1.如果是 '(',将下标入栈;
//2.如果是 ')',弹出栈顶元素,表示匹配当前右括号;
// 如果栈为空,说明当前右括号没有匹配的左括号,将下标放入栈中来更新(最后一个没有被匹配的右括号下标)
// 如果栈不为空,当前右括号下标减去栈顶元素即为(以该右括号结尾的最长有效括号长度);
for (int i = 0; i < n; ++i) {
if (s.charAt(i) == '(') {
stack.push(i);
} else {
stack.pop();
if (stack.isEmpty()) {
stack.push(i);
} else {
max = Math.max(max, i - stack.peek());
}
}
}
return max;
}
复杂度分析
时间复杂度:
O
(
n
)
O(n)
O(n),
n
n
n 是给定字符串的长度。我们只需要遍历字符串一次即可。
空间复杂度:
O
(
n
)
O(n)
O(n)。栈的大小在最坏情况下会达到
n
n
n,因此空间复杂度为
O
(
n
)
O(n)
O(n) 。
思路二:正向逆向结合法:
可以用两个计数器
left
\textit{left}
left 和
right
\textit{right}
right 来统计左括号和右括号出现的次数 。
首先,我们从左到右
遍历字符串,对于遇到的每个
‘(’
\text{‘(’}
‘(’,增加
left
\textit{left}
left 计数器,对于遇到的每个
‘)’
\text{‘)’}
‘)’ ,增加
right
\textit{right}
right 计数器。每当
left
\textit{left}
left 计数器与
right
\textit{right}
right 计数器相等时,计算当前有效字符串的长度,并且记录目前为止找到的最长子字符串。当
right
\textit{right}
right 计数器比
left
\textit{left}
left 计数器大时,将
left
\textit{left}
left 和
right
\textit{right}
right 计数器同时变回
0
0
0。
这样的做法贪心地考虑了以当前字符下标结尾的有效括号长度,每次当右括号数量多于左括号数量的时候之前的字符都扔掉不再考虑,重新从下一个字符开始计算,但这样会漏掉一种情况,就是遍历的时候左括号的数量始终大于右括号的数量,即 (()
,这种时候最长有效括号是求不出来的。
解决的方法也很简单,只需要从右往左遍历用类似的方法计算即可,只是这个时候判断条件反了过来;
完整思路如下:
- 两遍扫描
- 第一次:
- 从
左到右
扫描:遇到左括号,++left;遇到右括号 ++right;当 left = right,计算当前有效括号的长度;并记录到目前为止找到最长的子串;如果right > left
, left和right同时置零;
- 从
- 第二次:
- 从
右到左
扫描:遇到左括号,++left;遇到右括号 ++right;当 left = right,计算当前有效括号的长度;并记录到目前为止找到最长的子串;如果left > right
, left和right同时置零;
- 从
- 第一次:
public static int longestValidParentheses3(String s) {
if (s == null || s.length() <= 0) {
return 0;
}
int n = s.length();
if (n < 2) {
return 0;
}
int maxLen = 0;
// 两遍扫描
// 第一次:
// 从左到右扫描:遇到左括号,++left;遇到右括号 ++right;当 left = right,
// 计算当前有效括号的长度;并记录到目前为止找到最长的子串;如果 right > left, left和right同时置零;
// 第二次:
// 从右到左扫描:遇到左括号,++left;遇到右括号 ++right;当 left = right,
// 计算当前有效括号的长度;并记录到目前为止找到最长的子串;如果 left > right, left和right同时置零;
int left = 0;
int right = 0;
// 从左向右扫描
for (int i = 0; i < n; ++i) {
if (s.charAt(i) == '(') {
++left;
} else {
++right;
}
if (right > left) {
left = right = 0;
}
if (left == right) {
maxLen = Math.max(maxLen, right + left);
}
}
left = right = 0;
// 从右向左扫描
for (int i = n - 1; i >= 0; --i) {
if (s.charAt(i) == ')') {
++right;
} else {
++left;
}
if (left > right) {
left = right = 0;
}
if (left == right) {
maxLen = Math.max(maxLen, right + left);
}
}
return maxLen;
}
复杂度分析
时间复杂度:
O
(
n
)
O(n)
O(n),其中
n
n
n 为字符串长度。我们只要正反遍历两边字符串即可。
空间复杂度:
O
(
1
)
O(1)
O(1)。我们只需要常数空间存放若干变量。
思路三:动态规划:
定义 dp [ i ] \textit{dp}[i] dp[i] 表示以下标 i i i 字符结尾的最长有效括号的长度。将 dp \textit{dp} dp 数组全部初始化为 0 0 0 。显然有效的子串一定以 ‘)’ \text{‘)’} ‘)’ 结尾,因此可以知道以 ‘(’ \text{‘(’} ‘(’ 结尾的子串对应的 dp \textit{dp} dp 值必定为 0 0 0 ,只需要求解 ‘)’ \text{‘)’} ‘)’ 在 dp \textit{dp} dp 数组中对应位置的值。
从前往后遍历字符串求解
dp
\textit{dp}
dp 值,每两个字符检查一次
:
- s [ i ] = ‘)’ s[i] = \text{‘)’} s[i]=‘)’ 且 s [ i − 1 ] = ‘(’ s[i - 1] = \text{‘(’} s[i−1]=‘(’,也就是字符串形如 “ … … ( ) ” “……()” “……()”,可以推出:
dp
[
i
]
=
dp
[
i
−
2
]
+
2
\textit{dp}[i]=\textit{dp}[i-2]+2
dp[i]=dp[i−2]+2
可以进行这样的转移,是因为结束部分的
"
(
)
"
"()"
"()" 是一个有效子字符串,并且将之前有效子字符串的长度增加了
2
2
2 。
-
s
[
i
]
=
‘)’
s[i] = \text{‘)’}
s[i]=‘)’ 且
s
[
i
−
1
]
=
‘)’
s[i - 1] = \text{‘)’}
s[i−1]=‘)’,也就是字符串形如
“
…
…
)
)
”
“……))”
“……))”,可以推出:
如果 s [ i − dp [ i − 1 ] − 1 ] = ‘(’ s[i - \textit{dp}[i - 1] - 1] = \text{‘(’} s[i−dp[i−1]−1]=‘(’,那么
dp [ i ] = dp [ i − 1 ] + dp [ i − dp [ i − 1 ] − 2 ] + 2 \textit{dp}[i]=\textit{dp}[i-1]+\textit{dp}[i-\textit{dp}[i-1]-2]+2 dp[i]=dp[i−1]+dp[i−dp[i−1]−2]+2
考虑如果倒数第二个 ‘)’ \text{‘)’} ‘)’ 是一个有效子字符串的一部分(记作 s u b s sub_s subs),对于最后一个 ‘)’ \text{‘)’} ‘)’ ,如果它是一个更长子字符串的一部分,那么它一定有一个对应的 ‘(’ \text{‘(’} ‘(’ ,且它的位置在倒数第二个 ‘)’ \text{‘)’} ‘)’ 所在的有效子字符串的前面(也就是 s u b s sub_s subs的前面)。因此,如果子字符串 s u b s sub_s subs的前面恰好是 ‘(’ \text{‘(’} ‘(’ ,那么就用 2 2 2 加上 s u b s sub_s subs的长度( dp [ i − 1 ] \textit{dp}[i-1] dp[i−1])去更新 dp [ i ] \textit{dp}[i] dp[i]。同时,也会把有效子串 “ ( s u b s ) ” “(sub_s)” “(subs)”之前的有效子串的长度也加上,也就是再加上 dp [ i − dp [ i − 1 ] − 2 ] \textit{dp}[i-\textit{dp}[i-1]-2] dp[i−dp[i−1]−2]。
最后的答案即为 dp \textit{dp} dp 数组中的最大值。
状态转移方程的三步推导:
- 判断 i − dp [ i − 1 ] − 1 i-\textit{dp}[i-1]-1 i−dp[i−1]−1 是否为 " ( " "(" "(",则基础长度为 2 2 2。
2. 内部连在一起的最长有效括号,
dp
[
i
−
1
]
\textit{dp}[i-1]
dp[i−1];状态转移方程:
dp
[
i
]
=
2
\textit{dp}[i] = 2
dp[i]=2
3. 外部连在一起的最长有效括号,
dp
[
i
−
d
p
[
i
−
1
]
−
2
]
\textit{dp}[i-dp[i - 1] - 2]
dp[i−dp[i−1]−2];状态转移方程:
dp
[
i
]
=
2
+
d
p
[
i
−
1
]
\textit{dp}[i] = 2 + dp[i - 1]
dp[i]=2+dp[i−1]
状态转移方程:
dp
[
i
]
=
2
+
d
p
[
i
−
1
]
+
d
p
[
i
−
d
p
[
i
−
1
]
−
2
]
\textit{dp}[i] = 2 + dp[i - 1] + dp[i - dp[i - 1] - 2]
dp[i]=2+dp[i−1]+dp[i−dp[i−1]−2]
分别将
1
,
4
,
5
1, 4, 5
1,4,5 带入到状态转移方程:
dp
[
1
]
=
2
+
d
p
[
0
]
+
d
p
[
−
1
]
=
2
+
0
+
0
=
2
\textit{dp}[1] = 2 + dp[0] + dp[-1] = 2 + 0 + 0 = 2
dp[1]=2+dp[0]+dp[−1]=2+0+0=2
dp
[
4
]
=
2
+
d
p
[
3
]
+
d
p
[
2
]
=
2
+
0
+
0
=
2
\textit{dp}[4] = 2 + dp[3] + dp[2] = 2 + 0 + 0 = 2
dp[4]=2+dp[3]+dp[2]=2+0+0=2
dp
[
5
]
=
2
+
d
p
[
4
]
+
d
p
[
1
]
=
2
+
2
+
2
=
6
\textit{dp}[5] = 2 + dp[4] + dp[1] = 2 + 2 + 2 = 6
dp[5]=2+dp[4]+dp[1]=2+2+2=6
思考:
思考1:
当求解到 6 6 6 号位置的时候,因为 6 6 6 号前面有 3 , 4 , 5 3,4,5 3,4,5 三个连续匹配的括号,所以需要查看 3 3 3 号前面是否有连续匹配的括号,如果有就相加;
思考2:
当求解到 2 2 2 号位置的时候,因为前面 0 , 1 0,1 0,1是连续匹配的,所以需要查看 − 1 -1 −1 位置的值,这时候需要做边界处理;
public static int longestValidParentheses1(String s) {
if (s == null || s.length() <= 0) {
return 0;
}
int n = s.length();
if (n < 2) {
return 0;
}
int max = 0;
int[] dp = new int[n];
// 状态转移方程:dp[i] = 2 + dp[i - 1] + dp[i - dp[i - 1] - 2];
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 (((i - dp[i - 1] - 1) >= 0) && (s.charAt(i - dp[i - 1] - 1) == '(')) {
dp[i] = 2 + dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0 );
}
// 更新最大值
max = Math.max(max, dp[i]);
}
}
return max;
}
复杂度分析
时间复杂度:
O
(
n
)
O(n)
O(n),其中
n
n
n 为字符串的长度。我们只需遍历整个字符串一次,即可将
dp
\textit{dp}
dp 数组求出来。
空间复杂度:
O
(
n
)
O(n)
O(n)。我们需要一个大小为
n
n
n 的
dp
\textit{dp}
dp 数组。