题解正文
题目描述
问题分析
本题给出一串由 (
和 )
组成的字符串,要求出能够正确匹配的最长子串的长度,比如 (()
这个字符串能够匹配的最大长度是2.
解题思路
方法一:(朴素做法)
- 一开始看到这个题目就想的很简单,我认为这就是个普通的括号匹配问题,直接用入栈出栈的思想来解决就好了(只是用栈的思想,实际实现的时候不需要用栈),就是在遇到左括号时就将
leftCount
计数器加一,遇到右括号时就将leftCount
计数器减一,并且Length
加二(表示获取两个合法字符);直到遇到右括号时leftCount
为0,说明遇到非法右括号,当前串不再匹配,子串的Length
停止增长,然后重新开始寻找下一个正确匹配的子串;当我们分析完整个字符串,就可以比较前面找到的各个正确匹配的子串,得出最长子串长度。 - 这个思路看似正确,但是提交之后并不能通过,看一下例子就知道:对于字符串
()(()
而言,它的最长匹配子串是2,但是我们的程序求出的结果是4,这是因为这个串直到最后也没有出现过匹配错误(右括号出现时leftCount
为0),所以导致匹配子串长度不断增加,但是到最后左括号都没有被完全匹配完,所以实际上整个子串是不匹配的,而我们的程序没检测出来。 - 我想到的解决方案是记录每一个未匹配的左括号的位置,然后将这些未匹配左括号作为分割点,将子串进行分割,然后求解分割后的各个子串长度最大值即可,使用这种方法编写程序能够通过检测。
考虑到实际上使用未改进算法时,只在最后一个子串出现上述错误,我们可以之对最后一个子串进行分割,然后与前面的串进行长度比较。
关于为什么只有最后一个子串会出现上面的错误:因为前面的子串Length
停止增长是因为遇到右括号时leftCount
为0,也就是说此时已经没有未匹配的左括号了,因此不存在未完全匹配完的左括号从而导致错误。
方法二:(动态规划方法)
这个方法是参考评论区大神做法做的,算法复杂度和上面的算法是一个量级的,但是减少了需多不必要的重复操作,所以时间复杂度更优,具体思路如下:
- 这个方法将求解源字符串以第s[index]字符结尾的子串的最长匹配子串长度
longestSubset[index]
,于是可以得到如下的状态转移方程:
l o n g e s t S u b s e t [ i n d e x ] = longestSubset[index] = longestSubset[index]=
0 , 0, 0,当s[index]
为(
;
l o n g e s t S u b s e t [ i n d e x − 2 ] + 2 , longestSubset[index-2]+2, longestSubset[index−2]+2,当s[index-1]
为(
且s[index]
为)
;
l o n g e s t S u b s e t [ i n d e x − 1 ] + 2 + l o n g e s t S u b s e t [ i n d e x − 2 − l o n g e s t S u b s e t [ i n d e x − 1 ] ] , longestSubset[index-1]+2+longestSubset[index-2-longestSubset[index-1]], longestSubset[index−1]+2+longestSubset[index−2−longestSubset[index−1]],当s[index-1]
为(
且s[index-1-longestSubset[index-1]]
为(
;
(需要注意一下下标越界的判断,即在每次按下标访问时提前判断下标是否合法,不合法就用0代替该值或者跳过该步骤,具体判断见代码实现) - 当我们完成了整个
longestSubset[index]
数组的求解,遍历它取最大值就是本题答案。
算法步骤
方法一:
遍历字符串
若当前字符为‘(’
将当前字符位置作为元素入栈 leftParenthese;
若当前字符为‘)’
若 leftParenthese 长度为0
若当前子串长度length 大于 整个当前最长匹配子串长度maxLength,用length更新maxLength;
若 leftParenthese 长度大于0
栈顶元素出栈 leftParenthese ;当前子串长度length 加二;
遍历栈 leftParenthese
使用栈中元素值(代表未匹配左括号在源字串的位置)对最后一个子串进行分割,将最后一个子串分割为n份,并求出其中长度最大值 ,记为 maxLength1;
比较 maxLength 和 maxLength1,取其中大的作为结果;
方法二:
longestSubset[length(s)] 数组初始化为{};
遍历字符串s,当前下标记为index
若当前字符为‘(’
longestSubset[index] 设为 0 ;
若当前字符为‘)’
若前一个字符为‘(’
longestSubset[index] 设为 longestSubset[index-2]+2;
若前 index-1-longestSubset[index-1] 个字符为 `(`
longestSubset[index] 设为 longestSubset[index-1]+2+longestSubset[index-2-longestSubset[index-1]];
若此步求出的 longestSubset[index] 大于当前的maxLength,替换maxlength的值;
复杂度分析
方法一:
使用两个循环遍历,每个循环遍历数组的长度最多是n,所以算法是O(2n)的,也就是O(n)的时间复杂度;
空间复杂度上,使用了若干个临时变量和一个栈,栈中元素不超过n个,所以空间复杂度是O(n)的;
方法二:
使用一个循环遍历,循环遍历数组的长度最多是n,所以算法是O(n)的,也就是O(n)的时间复杂度;
空间复杂度上,使用了若干个临时变量和一个长度为n的数组,所以空间复杂度是O(n)的;
虽然理论上这两个算法都是O(n)的,但是前一个方法用了两次遍历(两个O(n)),并且这两次遍历是不能合为一步的,因此方法一的复杂度要比方法二大一倍,并且由于栈操作比数组操作开销更大,因此方法一的复杂度实际上比方法二的复杂度超过2倍,从后面结果分析的 runTime 可以看出这一点:方法一RunTime = 12ms,方法二RunTime = 4ms。
代码实现&结果分析
方法一:
- 代码实现
class Solution { public: int longestValidParentheses(string s) { stack<int> leftParentheses; int maxCount = 0; int count = 0; int index = 0; int start = 0; int length = s.length(); while (index != length) { if (s[index] == '(') { leftParentheses.push(index-start); } else { if (leftParentheses.size() == 0) { maxCount = maxCount > count ? maxCount : count; count = 0; start = index+1; } else { leftParentheses.pop(); //cout << s.substr(index) << endl; count+=2; } } index++; } if (maxCount < count) { if (leftParentheses.size() > 0) { int end = 0; int start = count+leftParentheses.size(); int tempCount = 0; while (!leftParentheses.empty()) { end = start; start = leftParentheses.top(); int temp = end - start - 1; tempCount = temp > tempCount ? temp : tempCount; leftParentheses.pop(); } tempCount = tempCount > start ? tempCount : start; maxCount = maxCount > tempCount ? maxCount : tempCount; } else { maxCount = count; } } return maxCount; } };
- 提交结果:
方法二:
- 代码实现:
class Solution { public: int longestValidParentheses(string s) { if (s.length() == 0) return 0; int maxCount = 0; int length = s.length(); int longestSubset[length] = {}; for (int i = 1; i < length; ++i) { if (s[i] == ')') { if (s[i-1] == '(') { longestSubset[i] = 2 + (i >= 2 ? longestSubset[i-2] : 0); } else if (i-1-longestSubset[i-1] >= 0 && s[i-1-longestSubset[i-1]] == '(') { longestSubset[i] = 2 + longestSubset[i-1] + ( i-2-longestSubset[i-1] >= 0 ? longestSubset[i-2-longestSubset[i-1]] : 0 ); } maxCount = maxCount > longestSubset[i] ? maxCount : longestSubset[i]; } } return maxCount; } };
- 结果分析:
虽然理论上这两个算法都是O(n)的,但是前一个方法用了两次遍历(两个O(n)),并且这两次遍历是不能合为一步的,因此方法一的复杂度要比方法二大一倍,并且由于栈操作比数组操作开销更大,因此方法一的复杂度实际上比方法二的复杂度超过2倍,从runTime可以看出这一点:方法一RunTime = 12ms,方法二RunTime = 4ms。