題目描述(題目難度,困難)
給定一個只包含 '('
和 ')'
的字符串,找出最長的包含有效括號的子串的長度。
示例 1:
輸入: “(()”
輸出: 2
解釋: 最長有效括號子串爲 “()”
示例 2:
輸入: “)()())”
輸出: 4
解釋: 最長有效括號子串爲 “()()”
題目求解
解法一
括號字符串依次入棧,刪除匹配的成對括號。最後棧中留下的都是無法匹配的斷點。這些斷點的差值減一就是斷點間有效括號串的長度,取這些長度的最大值即可。
例如括號字符串 “)()((())(”,最後留在棧中的字符下標爲 0 3 8。根據這三個斷點可以得到,兩個有效括號子串的長度分別爲 3-0-1 = 2 和 8-3-1=4。所以最長的長度就是 4。
public int longestValidParentheses(String s) {
if(s.length() <= 1) return 0;
List<Integer> stack = new ArrayList<>();
for(int i = 0; i < s.length(); ++i){
if(!stack.isEmpty()
&& s.charAt(i) == ')'
&& s.charAt(stack.get(stack.size()-1)) == '(') stack.remove(stack.size()-1);
else stack.add(i);
}
stack.add(s.length());
int longest = stack.get(0);
int diff;
for(int i = 1; i < stack.size(); ++i){
if((diff = stack.get(i)-stack.get(i-1)-1) > longest) longest = diff;
}
return longest;
}
解法二
同樣是藉助棧,遍歷一遍 s 字符串,就可以獲得結果。
與解法一不同的是,解法二隻有左括號下標能進棧,右括號下標不會進棧。
在左括號下標出棧時,同時更新最長的有效括號子串長度,比解法一少了後面作差比較的環節。
start 記錄的是不可能與之前括號串連成有效括號串的下界,更新 longest 分爲兩種情況:
- 棧空,說明當前最長的有效括號子串是有若干段有效括號子串連接而成的。所以長度要從下界開始算起。對應這種情況 ‘)()(())’,最長的有效長度子串 ‘()(())’,是由 ‘()’、’(())’ 兩個有效括號子串連接起來的,所以要以第一個 ‘)’ 爲界,計算長度。
- 棧非空,棧中還有 ‘(’,說明當前有效括號子串還沒能和前一個有效括號子串連起來,所以只用當前有效括號子串長度來更新。對應這種情況 ‘()((())’,棧中有多餘的 ‘(’,表示兩個有效括號子串還沒連起來,應該分開計算。
int longestValidParentheses(String s) {
int longest = 0, start = 0;
Deque<Integer> stack = new ArrayDeque<>();
for(int i = 0; i < s.length(); ++i){
char c = s.charAt(i);
if(c == '(') stack.push(i);
else if(c == ')'){
if(stack.isEmpty()) start = i + 1;
else{
stack.pop();
int t;
if(stack.isEmpty()){
if((t = i-start+1) > longest) longest = t;
}else if((t = i-stack.peek()) > longest) longest = t;
}
}
}
return longest;
}
解法三:
解法三是一種沒有用到棧,效率很高的算法。
算法分別從前到後和從後到前兩次遍歷 s 括號字符串,兩次遍歷結果的較大值,就是 s 括號字符串中最長有效括號子串的長度。
以從前到後遍歷爲例,算法使用一個 count 計數器,來指示當前有效括號子串是否肯定結束。
count 計數器的計數流程是如果當前字符是 ‘(’,count 計數器就自加,如果當前字符是 ‘)’,count 計數器就自減。
如果 count 小於 0,說明 ‘)’ 數量已經超過了 ‘(’ 的數量,就可以肯定的說上一個有效括號子串已經結束,更新最長有效括號子串長度。重置變量,開始下一輪計數。
但如果 count 大於 0,則無法說明當前有效括號子串結束。
也就是說如果遍歷結束時,count 變量大於 0,則無法確定最後一輪計數中,有效括號子串的長度。
這個算法理解的難點在於,我們要清楚地知道有效括號子串結束的條件,有兩種,一種是右括號數量大於左括號數量,另一種是在左括號數量還大於右括號數量時字符串結束,這導致了有效括號子串被動的結束。
此算法只考慮了第一種情況,沒考慮第二種情況,但再做一次反向遍歷,就可以解決這個問題,這也是此算法的巧妙之處。
public int longestValidParentheses(String s) {
int a = getLen(s, 1, '('); // 從前到後遍歷
int b = getLen(s, -1, ')'); // 從後到前遍歷
return Math.max(a, b);
}
private int getLen(String s , int d, char c){
int start = 0, end = s.length();
if(d == -1){
start = s.length()-1;
end = -1;
}
int max = 0, count = 0, len = 0;
for(int i = start;i != end; i += d){
count += (s.charAt(i) == c ? 1 : -1);
++len;
if(count < 0){
count = 0;
len = 0;
}else if(count == 0){
if(len > max) max = len;
}
}
return max;
}
那麼既然知道了只有當 count > 0 時,才需要反向遍歷,則我們可以稍微優化代碼如下。
public int longestValidParentheses(String s) {
int count = 0, len = 0, max = 0;
for(int i = 0; i < s.length(); ++i){
if(s.charAt(i) == '(') ++count;
else --count;
++len;
if(count < 0) count = len = 0;
else if(count == 0 && len > max) max = len;
}
if(count > 0){
count = len = 0;
for(int i = s.length()-1; i >= 0; --i){
if(s.charAt(i) == ')') ++count;
else --count;
++len;
if(count < 0) count = len = 0;
else if(count == 0 && len > max) max = len;
}
}
return max;
}