题目
给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
思路
方法一:动态规划
定义dp[i]
表示以下标i结尾的最长有效括号的长度,并全部初始化为0
注意到有效的子串一定是以’)‘结尾,即如果以’(‘结尾的子串对应的dp值必定为0,只需要求解’)'在dp数组中对应位置的值
从前往后遍历字符串求解dp值,每两个字符检查一次:
- 如果
s[i] = ')'
且s[i-1] = '('
,也就是字符串是类似于"......()"
,可以推出来此时的dp为:dp[i] = dp[i-2] + 2
- 如果
s[i] = ')'
且s[i-1] = ')'
,也就是字符串类似于"......))"
,可以推出,如果s[i - dp[i-1] - 1] = '('
,那么:dp[i] = dp[i−1] + dp[i − dp[i−1] − 2] + 2
如果倒数第二个')'
是一个有效子串的一部分(记为sub
),对于最后一个')'
,如果它是一个更长子串的一部分,那么它一定有一个对应的'('
,且它的位置在倒数第二个')'
所在的有效子串前面(即sub
的前面),比如"((...))"
中,"(...)"
就是sub
。
所以,如果sub
子串的前面恰好是'('
,那么正好用 2 加上sub
的长度dp[i-1]
(因为前后都已经确定了,相当于sub
的前面是一个'('
,后面是一个')'
,长度为2,再加上自身的长度dp[i-1]
),即dp[i-1] + 2
;
同时还要考虑有效子串sub
之前的有效子串的长度也加上,也就是再加上dp[ i - dp[i-1] - 2]
,i - dp[i-1] - 2
表示当前下标i减去sub的长度,减去sub
前后的'('
和')'
最后答案即为dp数组中的最大值
java代码如下:
class Solution {
public int longestValidParentheses(String s){
int maxans = 0;
int[] dp = new int[s.length()];
for(int i = 0; i < s.length(); i++){
if(s.charAt(i) == ')'){
if(s.charAt(i - 1) == '('){//对应于"......()"这种情况
dp[i] = (i - 2 >= 0 ? dp[i-2] : 0) + 2;//i - 2 >= 0表示除去这对括号()外还有别的子串
} else if (i - dp[i-1] > 0 && s.charAt(i - dp[i-1] - 1) == '('){//对应于"......))"这种情况,i - dp[i-1] > 0 表示子串sub前面还有有效子串,s.charAt(i - dp[i-1] - 1) == '('表示有效子串sub的前面一个字符为'(',正好对应上i位置的')'
dp[i] = dp[i-1] + 2 + (i - dp[i-1] - 2 >= 0 ? dp[i - dp[i-1] -2] : 0);//i - dp[i-1] - 2 >= 0 表示除去中间的子串sub和挨着子串的前后的'('和')'之外,前面还有有效的子串
}
maxans = Math.max(dp[i],maxans);
}
}
return maxans;
}
}
时间复杂度:O(n),n为字符串的长度,只需要遍历一次字符串即可求出dp数组的值
空间复杂度:O(n),需要一个大小为n的dp数组
方法二:栈的思想
通过栈,可以在遍历给定字符串的过程中去判断到目前为止扫描的子串的有效性,同时能得到最长的有效括号的长度;
始终保持栈底元素为,当前已经遍历过的元素中「最后一个没有被匹配的右括号')'
的下标」,栈中其他元素(除了栈底元素外)维护左括号的下标:
- 对于遇到的每个
‘(’
,将它的下标入栈 - 对于遇到的每个
‘)’
,先弹出栈顶元素表示匹配了当前右括号:
(1)如果栈为空,说明没有左括号可以进行匹配了,即当前的右括号为没有被匹配的右括号,将其下标放入栈中来更新前面提到的栈底元素——「最后一个没有被匹配的右括号')'
的下标」
(2)如果栈不为空,那么剩下的就是之前的右括号的下标,用当前右括号的下标减去栈底元素(之前的右括号的下标),即为「以该右括号为结尾的最长有效括号的长度」
这里要注意一点,如果一开始栈为空,第一个字符为左括号的时候会直接将其放入栈中,这样就不满足提及的「最后一个没有被匹配的右括号的下标」,为了保持统一,需要在一开始的时候往栈中放入一个值为 −1 的元素。
算法流程图如下:
java代码如下:
class Solution{
public int longestValidParentheses(String s){
int maxans = 0;
Deque<Integer> stack = new LinkedList<>();
stack.push(-1);
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 {//如果栈不为空
maxans = Math.max(maxans, i - stack.peek());//用当前右括号的下标i减去栈底元素(之前的右括号的下标),即为「以该右括号为结尾的最长有效括号的长度」
}
}
}
return maxans;
}
}
时间复杂度: O(n),n为字符串长度,只需要遍历一次字符串
空间复杂度: O(n),栈的大小最坏可以到达n
方法三:不需要使用额外空间
思路:使用两个计数器,left和right。首先从左到右遍历字符串,对于遇到的每个'('
,增加left计数器,对于遇到的每个')'
,增加right计数器。每当left和right计数器相等时,计算当前有效字符串的长度,并且记录目前为止找到的最长字符串;当 right 计数器比 left 计数器大时,将 left 和 right 计数器同时变回 0。
这种做法贪心的考虑了以当前字符下标结尾的有效括号长度,每当右括号数量多余左括号数量的时候,之前的字符串都扔掉不再考虑(计数器重置为0),重新从下一个字符开始计算(计数)
不过这样会漏掉一种情况,就是遍历的时候左括号的数量始终大于右括号的数量,即(()
,这种时候最长有效括号无法求出
所以需要从右向左遍历,用类似的方法计算即可,不过判断条件反了过来:
- 当left计数器比right计数器大的时候,同时置0
- 当left == right时,计算当前有效字符串的长度,并记录目前为止找到的最长字符串
java代码如下:
class Solution {
public int longestValidParentheses(String s){
int left = 0, right = 0;
int 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;
}
}
流程图如下:
时间复杂度:O(n),n为字符串长度,只要正反遍历两边字符串即可
空间复杂度:O(1),只要常数空间存在若干变量