leetcode 678.有效的括号字符串 | 刷题打卡

833 阅读4分钟

题目链接

双栈法

思路

先简化一下,如果这道题没有*,只有()的话,只需要用一个栈就搞定了。

  1. 创建一个栈用来存(
  2. 遍历字符串,如果当前字符为(就入栈,如果当前字符为)并且栈不为空就出栈一个(
  3. 如果最后栈是空的,证明所有括号都是匹配的,是个有效的括号字符串

然后原题是多了一个*,并且*的万能的:可以当(,也可以当),也可以当""。一个栈只有入栈和出栈两种操作,只能解决能够成队匹配的字符。现在加多了一个不确定是什么的字符*,显然,一个栈不够用了。如果一个不够,那就用两个

  1. 创建两个栈,一个用来存(,一个用来存*
  2. 遍历字符串,遇到(*进入对应的栈,注意这里入栈的是下标,因为后面要用到
  3. 遇到),先消耗(出栈)(的栈,消耗完了再消耗*的栈,如果两个栈都为空,则证明没有可以和)匹配的字符了,此时直接返回false
  4. 遍历完成之后,两个栈都可能还有数据,上面的逻辑是把*当做()匹配,*栈里的剩下的*要作为)(栈里的匹配
  5. 因为是两个不同的栈,所以不能保证顺序问题,这个时候就要用下标了,因为*是作为)的,)要在(后面,所以如果*栈顶储存的下标比(栈栈顶要大,则证明顺序不对,直接返回false
  6. 最后看看(栈是否为空,如果为空则证明全部匹配,返回true,否则是无效字符串,返回false

最后为什么要看(栈为不为空呢?

*可以当空字符串,有没有多的都无所谓,只要(能匹配完就行了

图解

测试用例:"((*(**))",结果应该返回true

image-20210802091738858


image-20210802091842332


image-20210802091954508

代码

function checkValidString(s: string): boolean {
    const leftBracketsStack = []; // ”(“栈
    const starStack = []; // "*"栈

    for (let i = 0; i < s.length; i++) {
        const elem = s[i];
        if (elem === '(') {
            leftBracketsStack.push(i);
        } else if (elem === '*') {
            starStack.push(i);
        } else {
            if (leftBracketsStack.length > 0) {
                leftBracketsStack.pop(); // 遇到")",先抵消"("栈的"("
            } else if (starStack.length > 0) {
                starStack.pop(); // 然后将"*"栈的"*"作为"("抵消
            } else {
                return false; // 没有可以抵消的"(",直接返回false
            }
        }
    }

    // 剩下的"*"作为")"和"("抵消,所以要保证弹出的"*"下标比"("大,是"()"这样匹配的,而不是这样")("
    while (leftBracketsStack.length > 0 && starStack.length > 0) {
        if (leftBracketsStack.pop() > starStack.pop()) return false;
    }

    return leftBracketsStack.length > 0 ? false : true; // 最后所有的"("都应该被抵消完才是有效的
};

双向比较法

思路

如果一个字符串是有效的,那么不管是从左边开始看还是从右边开始看,都是可以完全匹配的。(这个怎么证明我也不懂)

代码

function checkValidString(s: string): boolean {
    let left = 0, right = 0;

    for (let i = 0; i < s.length; i++) {
        left += s[i] === ')' ? -1 : 1; // 从左边看
        right += (s[s.length - 1 - i] === '(') ? -1 : 1; // 从右边看

        if (left < 0 || right < 0) return false;
    }
    return true;
};

其实+1-1也可以看成栈的入栈和出栈两种操作,leftright可以看成是两个栈

function checkValidString(s: string): boolean {
    let left = [], right = [], leftTop = true, rightTop = true;

    for (let i = 0; i < s.length; i++) {
        // 从左边开始比
        if (s[i] === ')') {
            leftTop = left.pop();
        } else {
            left.push(true);
        }
        // 从右边开始比
        if (s[s.length - 1 - i] === '(') {
            rightTop = right.pop();
        } else {
            right.push(true);
        }
        // 栈中无可匹配内容,是无效字符串
        if (!leftTop || !rightTop) return false;
    }
    return true;
};

动态规划

动态规划的思路用一句话解释就是:大事化小,小事化了。找出规律,从小到大缓存所有的解,最后得出算出最终的解。

如果完全没了解过动态规划,可以看看下面两篇文章:

思路

判断字符串是否为有效的括号字符串时,字符越多,需要考虑的情况就越多。

可以将字符串拆解成一个个子字符串,例如:有一个长度为4的字符串(()),如果要保证区间[0, 3],也就是整个字符串是有效的括号字符串,只需要保证03能组成括号并且区间[1, 2]是有效字符串就行了。这里只是其中一种情况,还有其它情况。

但是,这里我们可以看出,需要数据结构表示这种区间的形式,并且遍历的时候是从最小子区间开始遍历的。

这里我们可以定义一个二维数组:d[i][j]代表字符串的[i, j]子区间是否是有效括号字符串

  • [i, j]为有效字符串:d[i][j] = true
  • [i, j]不是效字符串:d[i][j] = false

遍历:

// length为目标字符串长度
for (let len = 0; len < length; len++) { // 每次遍历字符串的范围为len
  for (let i = 0; i < length - len; i++) {
    let j = i + len;
  }
}

画个图看看是怎么遍历的:

image-20210830145914196

不难看出,这里我们遍历的字符串区间长度会越来越大,所以最后dp[0][s.length - 1]就是我们要的结果。

最后我们看看最小集的规律和边界是啥

  • 规律:如果想要区间[i, j]是有效括号字符串,这里有两种情况:
    1. 当前区间dp[i][j]true,并且他的子区间dp[i + 1][j - 1]也为ture,例:(())
    2. 当前区间dp[i][j]true,并且可以在里面找到一个k,使得被k截断的两个区间dp[i][ k]dp[k+1][j]也为true,例:()()
  • 边界:就是粒度最小的情况,也就就字符串只有一个字符的情况,当这个字符为*为有效,否则为无效

代码

function checkValidString(s: string): boolean {
    const { length } = s;
    // 初始化dp二维数组
    const dp: boolean[][] = Array.from(new Array(length), x => new Array(length).fill(false));

    for (let len = 0; len < length; len++) { // 每次遍历字符串的范围为len
        for (let i = 0; i < length - len; i++) {
            let j = i + len;

            // 1. 如果是范围是0,就是字符串只有一个字符时
            if (len === 0) {
                dp[i][j] = s[i] === '*'; // 当前字符为*号的时候为true,因为*是万能的
                continue;
            }

            // 2. 如果当前区间第一个字符为左括号(这里遇到*也当左括号),并且最后一个字符是右括号
            if ((s[i] === '(' || s[i] === '*') && (s[j] === ')' || s[j] === '*')) {
                // 范围为1时证明只有这两个字符,直接返回true,否则看子区间是不是合法的
                dp[i][j] = (len === 1) ? true : dp[i + 1][j - 1];
            }

            // 在i到j的区间字符串里找一个k,看看是否有一个k能满足,被k截断的两个区间[i, k]和[k+1,j]中能构成有效字符串
            for (let k = i; k < j; k++) {
                dp[i][j] = dp[i][j] || (dp[i][k] && dp[k + 1][j]);

                if (dp[i][j] === true) {
                    break;
                }
            }
        }
    }

    return dp[0][length - 1];
};

贪心算法

贪心算法的思想很简单,能贪最大的就贪贪最大,不能贪再退而求其次,以此类推。

也就是每次找局部最优解,最后组成全局最优解。

但是,有时候局部最优解并不能保证最后组成的全局解是最优的,所以贪心的难点其实再如何判断能否使用这个算法。

思路

我们可以计算(的个数来判断字符串是否是有效的。

这里因为*可以万能的,贪心嘛,就两种情况:

  • 能贪,就把*(
  • 实在不行了,且(个数不为0再当)

代码

function checkValidString(s: string): boolean {
    let min = 0; // 左括号可能的最小数
    let max = 0; // 左括号可能的最大数

    for (let i = 0; i < s.length; i++) {
        const c = s[i];
        
        // 如果是左括号,最大数和最小数都加一
        if (c === '(') {
            min++;
            max++;
        } else if (c === '*') {
            // 最小数,能匹配就匹配,减1
            if (min !== 0) min--;
            // 最大数,每次遇到*都当左括号,故加1
            max++;
        } else {
            // 最小数,能匹配就匹配,减1
            if (min !== 0) min--;
            // 最大数如果减完之后小于0,证明右括号多了,后面再怎么也无法匹配了,直接返回false
            if (--max < 0) return false;
        }
    }

    return min === 0; // 如果是有效的,最后最小数肯定是刚好匹配完的
};

DFS

思路

使用栈或数字计算左括号的个数,遇到*就分成三种情况考虑

  • 当成(
  • 当成)
  • 当成空字符串

只要其中一种成立就可以了

图解

其实就是一棵树的深度优先遍历(DFS)

image-20210831110601715

代码

function checkValidString(s: string, start: number = 0, count: number = 0): boolean {
    if (count < 0) return false; // 如果左括号数小于0,返回false

    for (let i = start; i < s.length; i++) {
        const c = s[i];

        if (c === '(') {
            count++;
        } else if (c === ')') {
            if (count-- === 0) return false; // 没有左括号可以抵消,直接返回false
        } else {
            return checkValidString(s, i + 1, count + 1) || // 作为"("
                checkValidString(s, i + 1, count - 1) || // 作为")",抵消一个”(“
                checkValidString(s, i + 1, count); // 作为""
        }
    }

    return count === 0; // 最后刚好匹配完就是有效的
};

注意📢:这个解法并没有AC,遇到"************************************************************"这个测试用例的时候超时了,减少*的个数,答案是对的。虽然没有AC,但是这个思路还是可以学习一下的