给定一个只包含
'('
和')'
的字符串,找出最长的包含有效括号的子串的长度。
输入: "(()" 输出: 2 解释: 最长有效括号子串为 "()"
对于这个问题,最容易想到的就是通过暴力解决:遍历所有可能出现的长度为偶数的子串,判断它是否满足题目要求,找出其中长度最大的那种情况即可。
在判断一个字符串是否有效括号的时候可以通过栈来做,对于 “(” 我们入栈,对于 “)” 我们出栈,其中如果出栈时栈内为空,就说明这个字符串不满足有效括号,只有在字符串遍历完 栈为空,并且没有无法出栈的问题时,才说明这个字符串满足情况。这里我通过数字模拟入栈出栈过程,栈为空就用0表示,入栈数字加1,出栈数字减1
public int longestValidParentheses(String s) {
if (s.isEmpty()) {
return 0;
}
int length = s.length() % 2 == 1 ? s.length() - 1 : s.length();
for (int i = length; i >= 2; i = i - 2) {
for (int j = 0; j <= s.length() - i; j++) {
String temp = s.substring(j, j + i);
int jundge = 0;
for (int k = 0; k < temp.length(); k++) {
if (temp.charAt(k) == '(') {
jundge++;
} else {
if (jundge == 0) {
break;
}
jundge--;
}
if (k == temp.length() - 1 && jundge == 0) {
return i;
}
}
}
}
return 0;
}
因为题目要求计算最长括号子串,所以我们从可能出现的最长偶数子串计算起。对于字符串截取操作,我们也可以通过下标来实现,优化代码。优化后主要改动部分如下:
int jundge = 0;
for (int k = j; k < j + i; k++) {
if (s.charAt(k) == '(') {
jundge++;
} else {
if (jundge == 0) {
break;
}
jundge--;
}
if (k == j + i - 1 && jundge == 0) {
return i;
}
}
一般情况下,使用暴力法总会有多余的判断,例如如果这个字符串最后一位是 ( ,那么它肯定不行。对此我们使用动态规划来描述这个问题,看看能否优化解题方案:
我们使用 boolean[ i ][ j ] 来表示下标 从 i ~ j 这串字符是否满足要求。那么就会有以下公式:
通过上面两个公式,我们就可以使用 dp 解决该问题:
public int longestValidParentheses3(String s) {
if (s.isEmpty()) {
return 0;
}
boolean[][] record = new boolean[s.length()][s.length()];
int result = 0;
for (int i = 0; i < s.length() - 1; i++) {
if (s.charAt(i) == '(' && s.charAt(i + 1) == ')') {
record[i][i + 1] = true;
result = 2;
}
}
int length = s.length() % 2 == 1 ? s.length() - 1 : s.length();
for (int i = 4; i <= length; i = i + 2) {
for (int j = 0; j <= s.length() - i; j++) {
record[j][j + i - 1] = record[j + 1][j + i - 2] && s.charAt(j) == '(' && s.charAt(j + i - 1) == ')';
if (!record[j][j + i - 1]) {
for (int k = 1; j + k < j + i - 2; k = k + 2) {
if (record[j][j + k] && record[j + k + 1][j + i - 1]) {
record[j][j + i - 1] = true;
break;
}
}
}
if (record[j][j + i - 1] && result < i) {
result = i;
}
}
}
return result;
}
通过观察上述dp,我们会发现公式二中的 k 很难确定,可能还需要遍历所有情况才能判断。因此我们对上述 dp 进行优化,假设我们现在用 record[ i ] 来记录以下标 i 结尾的最长有效括号长度,那么就可以得到以下结论:
其中上述结论1模拟连接连续 () 的场景,结论2模拟连接非连续 () 的场景,优化后得dp代码如下:
public int longestValidParentheses4(String s) {
if (s.isEmpty()) {
return 0;
}
int[] record = new int[s.length()];
int result = 0;
for (int i = 1; i < s.length(); i++) {
if (s.charAt(i) == ')' && s.charAt(i - 1) == '(') {
if (i > 1) {
record[i] = record[i - 2] + 2;
} else {
record[i] = 2;
}
} else if (s.charAt(i) == ')' && s.charAt(i - 1) == ')' && i - record[i - 1] > 0 && s.charAt(i - record[i - 1] - 1) == '(') {
if (i - record[i - 1] - 2 >= 0) {
record[i] = record[i - 1] + record[i - record[i - 1] - 2] + 2;
} else {
record[i] = record[i - 1] + 2;
}
}
result = result > record[i] ? result : record[i];
}
return result;
}
这次的dp代码只需要一层循环即可完成,相比前面几种方法的三层循环,效率提高了很多,并且也带给我一些启发:
暴力法中通过栈的思想来判断一个字符串是否满足括号条件,这里我们可以对这种思想进行改进:我们入栈一个数字表示满足条件字符串的开始位置,遇到 “(”入栈下标,遇到 ")" 出栈栈顶数字,出栈后当前满足条件字符串的长度即当前坐标减去栈顶坐标。如果出栈后栈为空,说明这个字符串已经到头,将开始位置标记为当前位置,计算后续满足条件的字符串长度,选出其中最长的字符串即可。
public int longestValidParentheses5(String s) {
int result = 0;
Stack stack = new Stack<>();
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 {
result = result > (i - stack.peek()) ? result : i - stack.peek();
}
}
}
return result;
}
该方法通过坐标的差计算满足条件的子串长度,栈顶值为上一个未匹配 “(” 的下标,当前 i 为匹配后的下标,这两个数的差即为匹配的长度。一开始栈内 push(-1) 可以理解为多计算一个“(”,当出现 “)” 和它匹配时,说明当前字符串已经不满足条件,即原来串中所有 “(” 已经匹配完成。这个匹配串的长度为当前的 i 减去这个开始的下标后数值加1 ,因为无论如何开始下标都是0,所以这里push(-1),后面的计算中因为 i 值都比真正开始下标少1,因此不需要再加1。
最后介绍一种时间复杂度为n,空间复杂度为1的方法。关于为什么要从左到右和从右到左遍历两遍,我是这样理解的:
下面的方法是有且仅当左括号和右括号的数量相同时才做计算。从左到右遍历是以左括号为基准,如果右括号的数量大于左括号就从头统计。这样在左括号数量较多时,可能产生统计的值偏少。但是这种情况在从右向左的遍历中就不会有问题,反之亦然。因此这里需要左右依次遍历一遍,取最大值。
public int longestValidParentheses6(String s) {
int left = 0, right = 0, result = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') {
left++;
} else {
right++;
if (left == right) {
result = result > 2 * left ? result : 2 * left;
} else if (right > left) {
left = 0;
right = 0;
}
}
}
left = 0;
right = 0;
for (int i = s.length() - 1; i >= 0; i--) {
if (s.charAt(i) == '(') {
left++;
if (left == right) {
result = result > 2 * left ? result : 2 * left;
} else if (left > right) {
left = 0;
right = 0;
}
} else {
right++;
}
}
return result;
}