最长有效括号问题(letcode 32)

问题描述

给定一个只包含 '(' 和 ')' 的字符串,找出最长的包含有效括号的子串的长度。

示例

输入: "(()"
输出: 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 这串字符是否满足要求。那么就会有以下公式:

  • 字符串长度为 2 时:boolean[ i ][ i + 1] = s.charAt(i) == '(' && s.charAt(i + 1) == ')' 
  • 字符串长度大于2时:boolean[ i ][ j ] =  (  boolean[ i + 1 ][ j - 1 ]  && s.charAt(i) == '('  &&  s.charAt(j) == ')'  )                              || ( boolean[ i ][ k ] && boolean[ k + 1 ][ 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 结尾的最长有效括号长度,那么就可以得到以下结论:

  • 如果 s[ i ] == ')' && s[ i - 1 ] == '(',那么record[ i ] = record[ i - 2 ] + 2
  • 如果 s[ i ] == ')' && s[ i - 1 ] == ')' 并且 s[ i - record[i - 1] - 1] == ‘(’,那么record[i] = record[i-1] + 2 + record[i - record[i-1] -2]

其中上述结论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;
}

 

你可能感兴趣的:(算法)