栈3:括号匹配问题的7道题

Posted 纵横千里,捭阖四方

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了栈3:括号匹配问题的7道题相关的知识,希望对你有一定的参考价值。

​如何判断括号匹配相关的问题是栈的一大典型应用,在LeetCode中至少有7道

相关的题目,如下:

20. 有效的括号

856. 括号的分数

32. 最长有效括号

678. 有效的括号字符串

1111. 有效括号的嵌套深度

1614. 括号的最大嵌套深度

1541. 平衡括号字符串的最少插入次数

这些题目也有明显的相关关系,有些只是条件稍微改了一下,但还是一样的题目,所以我们只选择几个典型的来分析一下。

括号的问题之所以需要栈是因为括号要成对出现,左侧先到则必须等待右侧的,期间收到的信息都只保存不处理,而右侧的出现之后就必须处理一下。入栈和出栈能够完美满足这种要求。

1. LeetCode20. 有效的括号

先看题目要求:

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。有效字符串需满足:

(1)左括号必须用相同类型的右括号闭合。

(2)左括号必须以正确的顺序闭合。

我们遍历给定的字符串 s。当我们遇到一个左括号时会期望在后续的遍历中,有一个相同类型的右括号将其闭合。由于后遇到的左括号要先闭合,因此我们可以将这个左括号放入栈顶。

遇到一个右括号时,需要将一个相同类型的左括号闭合。此时,可以取出栈顶的左括号并判断它们是否是相同类型的括号。如果不是相同的类型,或者栈中并没有左括号,那么字符串 s 无效,返回 False。为了快速判断括号的类型,我们可以使用哈希表存储每一种括号。哈希表的键为右括号,值为相同类型的左括号。

在遍历结束后,如果栈中没有左括号,说明我们将字符串 ss 中的所有左括号闭合,返回True,否则返回 False。

另外有一个小技巧,对于只有括号的情况,有效字符串的长度一定为偶数,因此如果字符串的长度为奇数,我们可以直接返回False,省去后续的遍历判断过程。

class Solution {    public boolean isValid(String s) {        int n = s.length();        if (n % 2 == 1) {            return false;        }        Map<Character, Character> pairs = new HashMap<Character, Character>() {{            put(')', '(');            put(']', '[');            put('}', '{');        }};        Deque<Character> stack = new LinkedList<Character>();        for (int i = 0; i < n; i++) {            char ch = s.charAt(i);            if (pairs.containsKey(ch)) {                if (stack.isEmpty() || stack.peek() != pairs.get(ch)) {                    return false;                }                stack.pop();            } else {                stack.push(ch);            }        }        return stack.isEmpty();    }}

2.LeetCode 678 有效的括号字符串

这也是道中规中矩的题目,也是出现频率最高的题目,先看要求:

给定一个只包含三种字符的字符串:( ,) 和 *,写一个函数来检验这个字符串是否为有效字符串。有效字符串具有如下规则:

  1. 任何左括号 ( 必须有相应的右括号 )。

  2. 任何右括号 ) 必须有相应的左括号 ( 。

  3. 左括号 ( 必须在对应的右括号之前 )。

  4. * 可以被视为单个右括号 ) ,或单个左括号 ( ,或一个空字符串。

  5. 一个空字符串也被视为有效字符串。

这个题可以根据栈的特性直接写:

lass Solution {    public boolean checkValidString(String s) {        Stack<Integer> sr = new Stack<Integer>();        Stack<Integer> sa = new Stack<Integer>();        int index = 0;        for (int i = 0; i < s.length(); i++) {            char c = 0;            if (c == '(') {                sr.push(index);            } else if (c == '*') {                sa.push(index);            } else {                if (!sr.empty()) {                    sr.pop();                } else if (!sa.empty()) {                    sa.pop();                } else {                    return false;                }            }            index++;        }​        while (!sr.empty() && !sa.empty()) {            if (sr.peek() > sa.peek()) {                return false;            }            sr.pop();            sa.pop();        }        if (!sr.empty()) {            return false;        }        return true;    }

上面的代码略长,有没有更精简的实现呢?

我们可以考虑一下双指针的方法:

有效的字符串,即从左向右看是有效的,从右向左看也是有效的

如果在遍历过程中,left或者right小于0,则是无效,核心代码5行搞定。

class Solution {    public boolean checkValidString(String s) {      int left = 0, right = 0, size = s.length();        for (int i = 0; i < size; ++i) {            //从左向右看左括号能否有效            left += (s.charAt(i) == ')') ? -1 : +1;            //从右向左看右括号能否有效            right += (s.charAt(size - 1 - i) == '(') ? -1 : +1;            if (left < 0 || right < 0) return false;        }        return true;    }}

3.LeetCode856题:括号的分数

这个题就是上面题的变形,先看题意:

给定一个平衡括号字符串 S,按下述规则计算该字符串的分数:

  1. () 得 1 分。

  2. AB 得 A + B 分,其中 A 和 B 是平衡括号字符串。

  3. (A) 得 2 * A 分,其中 A 是平衡括号字符串。

示例:

1.输入”()“,  就输出12.输入”(())“, 就输出23.输入”()()“,也输出24.输入”(()(()))“,就输出6

字符串 S 中的每一个位置都有一个“深度”,即该位置外侧嵌套的括号数目。例如,字符串 (()(.())) 中的 . 的深度为 2,因为它外侧嵌套了 2 层括号:(__(.__))。

我们用一个栈来维护当前所在的深度,以及每一层深度的得分。当我们遇到一个左括号 ( 时,我们将深度加一,并且新的深度的得分置为 0。当我们遇到一个右括号 ) 时,我们将当前深度的得分乘二并加到上一层的深度。这里有一种例外情况,如果遇到的是 (),那么只将得分加一。

下面给出了字符串 (()(())) 每次对应的栈的情况:

[0, 0] ([0, 0, 0] (([0, 1] (()[0, 1, 0] (()([0, 1, 0, 0] (()(([0, 1, 1] (()(()[0, 3] (()(())[6] (()(()))

实现方法:

public int scoreOfParentheses(String S) {    Stack<Integer> stack = new Stack();    stack.push(0); // The score of the current frame    for (char c: S.toCharArray()) {        if (c == '(')            stack.push(0);        else {            int v = stack.pop();            int w = stack.pop();            stack.push(w + Math.max(2 * v, 1));        }    }    return stack.pop();}

4.LeetCode 32. 最长有效括号

这个题目有些困难,先看题意:

给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

 示例:

输入:s = "(()"输出:2解释:最长有效括号子串是 "()"输入:s = ")()())"输出:4解释:最长有效括号子串是 "()()"

一般和”最“有关的题目都优先考虑动态规划能不能解决问题,但是这里要先只考虑栈,看看能不能解决。

对于这题的第一直觉是找到每个可能的子串后判断它的有效性,但这样的时间复杂度会达到 O(n^3),无法通过所有测试用例。但是通过栈,我们可以在遍历给定字符串的过程中去判断到目前为止扫描的子串的有效性,同时能得到最长有效括号的长度。

具体做法是我们始终保持栈底元素为当前已经遍历过的元素中「最后一个没有被匹配的右括号的下标」,这样的做法主要是考虑了边界条件的处理,栈里其他元素维护左括号的下标:

对于遇到的每个 ‘(’ ,我们将它的下标放入栈中

对于遇到的每个 ‘)’ ,我们先弹出栈顶元素表示匹配了当前右括号:

  • 如果栈为空,说明当前的右括号为没有被匹配的右括号,我们将其下标放入栈中来更新我们之前提到的「最后一个没有被匹配的右括号的下标」

  • 如果栈不为空,当前右括号的下标减去栈顶元素即为「以该右括号为结尾的最长有效括号的长度」

我们从前往后遍历字符串并更新答案即可。

需要注意的是,如果一开始栈为空,第一个字符为左括号的时候我们会将其放入栈中,这样就不满足提及的「最后一个没有被匹配的右括号的下标」,为了保持统一,我们在一开始的时候往栈中放入一个值为 -1−1 的元素。

class Solution {    public int longestValidParentheses(String s) {        int maxans = 0;        Deque<Integer> stack = new LinkedList<Integer>();        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 {                    maxans = Math.max(maxans, i - stack.peek());                }            }        }        return maxans;    }} 

其他几个题目我们就不分析了,请读者自行研究一下怎么做。

以上是关于栈3:括号匹配问题的7道题的主要内容,如果未能解决你的问题,请参考以下文章

Java 栈 如何实现括号匹配

用自定义链式栈解决力扣括号匹配问题

用自定义链式栈解决力扣括号匹配问题

用栈检测括号匹配

用栈实现括号匹配的检验

栈(括号匹配)