LeetCode 括号问题专栏

LeetCode刷题最好按照标签归类来刷,本文主要对leetcode上的主要括号问题做一个梳理。
括号问题实际上在字符串中常出现,还有一些表达式求值的问题上也有应用,因此本次做一个总结。

  1. LC20 有效括号
  2. LC22 括号生成
  3. LC23 最长有效括号
  4. LC678. 有效的括号字符串
  5. LC1190. 反转每对括号间的子串
  6. LC1249 移除无效的括号
  7. LC394 字符串解码 (0713补充)

括号问题的核心

括号都是左右括号成对出现的,因此有效的括号必须满足以下条件:

  1. 在整个字符串中 左右括号的数目必须相同
  2. 在遍历字符串的过程中,左括号的数目总是要大于等于右括号的数目。
    常用的思路:
    使用栈结构
    对于遍历到左扩号,压入栈中;
    对于遍历到右括号,首先判断栈是否为空
    1)栈为空,说明当前的右扩号在之前的遍历过程中没有与之匹配的左括号,这个右括号是不成对的。
    2)取出栈顶元素,判断是否是左扩号,如果是则说明与当前遍历到的右括号是一对,将元素出栈。

LC20 有效括号

实际上就是利用上述的括号问题的核心内容来解题,只不过这里是有几种不同的括号,需要进行区分。

class Solution {
        public boolean isValid(String s) {
            if (s == null || s.length() == 0) {
                return false;
            }
            char[] arr = s.toCharArray();
            Stack<Character> stack = new Stack<>();
            for (int i = 0; i < arr.length; i++) {
                char a = arr[i];
                if ((a == '(' || a == '[' || a == '{')) {
                    stack.push(a);
                } else {
                //当前是右括号,并且栈是空或者栈顶元素与当前符号不匹配
                    if (a==')' && (stack.isEmpty() || stack.peek() != '(')) {
                        return false;
                    }else if (a==']' && (stack.isEmpty() || stack.peek() != '[')) {
                        return false;
                    }if (a=='}' && (stack.isEmpty() || stack.peek() != '{')) {
                        return false;
                    }else{
                        //能够匹配,出栈
                        stack.pop();
                    }
                }
            }
            return stack.isEmpty();
        }
    }

LC22 括号生成

给定一个数n,生成一组有效括号(组成n对),这个问题实际上可以用回溯的思想来解决(后面针对回溯也会做一个专题),同时也是对括号核心内容的应用。
实际上就是遍历整个搜索树,选取满足要求的路径。
括号生成的搜索树
我们可以使用递归的方式来求解,其中需要知道的状态是:

  1. 当前生成的是第几个字符
  2. 当前产生的左括号有多少个
    然后递归遍历整个搜索树,实际上就是前序遍历二叉树,只不过我们要记录路径而已。为了减少不必要的递归,我们可以进行剪枝,即写出递归的终止条件
    1)当左括号的数目超过n时,此时可以直接退出
    2)当左括号的数目小于右括号,此时不可能是有效,直接退出
    3)当前是最后的字符,且满足左右括号数目相同,添加路径
    class Solution {
        public List<String> generateParenthesis(int n) {
            List<String> res = new ArrayList<>();
            dfs(2 * n, 0, 0, new StringBuilder(), res);
            return res;
        }
        
        void dfs(int len, int index, int left, StringBuilder sb, List<String> res) {
            int cur_right = index - left;
            if (left < cur_right || left > len / 2) {
                return;
            }
            if (index == len) {
                if (left == cur_right) {
                    res.add(sb.toString());
                }
                return;
            }
            sb.append("(");
            left++;
            f(len, index + 1, left, sb, res);
            sb.delete(sb.length() - 1, sb.length());
            left--;
            sb.append(')');
            f(len, index + 1, left, sb, res);
            sb.delete(sb.length() - 1, sb.length());
            left--;
        }
    }

LC23 最长有效括号

一般求解最值问题我们可以考虑动态规划。
动态规划其实很类似数学归纳法,我们从整体着手,思考问题,推出状态方程,写代码的时候则是从基数开始进行初始化,然后利用状态方程进行迭代。

我们假设 dp[i]表示以i下标位置字符为结尾向左边扩充能够找到的连续的有效括号的长度。思考一下,这样定义之后如果求解状态方程。
dp[i] 与dpj
如果i位置是’(’,那么dp[i]=0;
如果i位置是’)’,那么我们需要考虑它的前一个位置i-1的字符,
1)如果s[i-1]=’(’,那么说明[i-1,i]就是一个有效的括号,那么这个是最长的有效括号么,也就是说dp[i]是否就止于此。显然不是,我们还需要考虑i-2位置前是否有有效括号,这里就能看出来和我们的定义相吻合了,不就是dp[i-2]么,因此dp[i]=dp[i-2]+2
2)如果s[i-1]=’)’,如s=’)(())’ 那么我们得找到与当前这个’)‘相匹配最远左括号位置,实际上就是 i-dp[i-1]-1处 ,记 pre = i-dp[i-1]-1
如果s[pre]=’(‘那么dp[i]=dp[i-1]+2+(pre>=1?dp[pre-1]:0)
实际上这1)2)两者可以合并起来,因为1)中相当于dp[i-1]=0 pre=i-1

 class Solution {
        public int longestValidParentheses(String s) {
            if (s == null || s.length() == 0) {
                return 0;
            }
            int[] f = new int[s.length()];
            int res = 0;
            int pre = 0;
            for (int i = 1; i < s.length(); i++) {
                char tmp = s.charAt(i);
                if (tmp == ')') {
                    pre = i - f[i - 1] - 1;
                    // 对应两种情况 ()  ||    )(()())
                    //pre指向的当前的)应该要匹配的位置
                    if(pre>=0 && s.charAt(pre)=='(' ){
                        f[i] = f[i - 1] + 2 + (pre >= 1 ? f[pre - 1] : 0);
                    }
                }
                res = Math.max(res,f[i]);
            }
            return res;
        }
    }

运行时间

LC678. 有效的括号字符串

字符串中有号,可以替代左括号或者右括号。
使用栈结构来进行操作,使用两个栈分别存储左括号的下标和星号的下标
遍历字符串,如果碰到左括号就将对应字符下标压入栈中
如果是
号,对应下标也压入栈中,
如果是右括号,那么此时需要考虑
如果栈1不为空,那么将其出栈,与当前右括号进行匹配
否则如果栈2不为空 那么将其出栈,用作左括号与当前右括号匹配
如果都为空,返回false,说明当前右括号没有相应的左括号或者星号预期匹配
此时判断栈1和栈2的大小,栈1中存储的都是右括号,栈2中存储的都是星号
如果栈1的大小大于栈2,那么一定不能匹配
否则,需要比较栈1栈顶的元素大小是否小于栈2栈顶的元素大小,这样*号才能与左括号匹配上,否则这个栈顶的左括号必然不能匹配。

    class Solution {
        public boolean checkValidString(String s) {
            if (s == null || s.length() == 0) {
                return false;
            }

            // stack store left
            Stack<Integer> stack1 = new Stack<>();
            // stack store star
            Stack<Integer> stack2 = new Stack<>();
            char[] arr = s.toCharArray();

            for (int i = 0; i < arr.length; i++) {
                char c = arr[i];
                if (c == '(') {
                    stack1.push(i);
                } else if (c == ')') {
                    // before this has no left and star
                    if (!stack1.isEmpty()) {
                        stack1.pop();
                    } else if (!stack2.isEmpty()) {
                        //has star, use star as left to match
                        stack2.pop();
                    } else {
                         return false;
                    }
                } else {
                    stack2.push(i);
                }
            }
            // has no enough star to match left
            if (stack2.size() < stack1.size()) {
                return false;
            }
            // star use as right  star index must be larger than left
            while (!stack1.isEmpty() && !stack2.isEmpty()) {
                if (stack1.peek() > stack2.peek()) {
                    return false;
                }
                stack1.pop();
                stack2.pop();
            }
            return true;
        }
    }

LC1190. 反转每对括号间的子串

要反转括号内的数据,而且是从内到外,而我们遍历的顺序是从外到内,因此可以使用栈结构。
用一个变量str记录上一次遍历到左括号到本次遍历到左括号的之间的字符串。
1)本次遍历到左括号时,将str压入栈中,然后将str设置为空字符串,重新记录本次左括号到下次左括号之间的字符;
2)本次遍历到字符时,将字符添加到str后;
3)本次遍历到右括号时,说明需要进行结算,把本次括号之间的字符进行反转,即将str反转,同时将栈中的栈顶元素与该反转后的字符串进行拼接,赋值给str

    class Solution {
        public String reverseParentheses(String s) {

            if (s == null || s.length() == 0) {
                return "";
            }
            char[] arr = s.toCharArray();
            Stack<String> stack = new Stack<>();
            String str = "";
            for (int i = 0; i < arr.length; i++) {
                char c = arr[i];
                if (c == '(') {
                    // push stack
                    stack.push(str);
                    str = "";
                } else if (c == ')') {
                    // pop data and reverse
                    // 反转当前括号的数,并与前一个括号的数进行合并
                    String tmp = reverse(str);
                    str = stack.peek() + tmp;
                    stack.pop();
                } else {
                    str += c;
                }
            }
            return str;
        }
        public String reverse(String s) {
            char[] arr = s.toCharArray();
            int i = 0, j = arr.length - 1;
            while (i < j) {
                char tmp = arr[i];
                arr[i] = arr[j];
                arr[j] = tmp;
                i++;
                j--;
            }
            return String.valueOf(arr);
        }

    }

LC1249 移除无效的括号

实际上就是对于括号问题核心内容的直接应用,只不过我们需要记录没有成对的单独括号的位置,在遍历的时候剔除就可。具体参见代码:

    class Solution {
        public String minRemoveToMakeValid(String s) {
            if (s == null || s.length() == 0) {
                return "";
            }
            //存储下标
            Stack<Integer> stack = new Stack<>();
            //存储未能够匹配的下标
            Set<Integer> notMatchSet = new HashSet<>();
            for (int i = 0; i < s.length(); i++) {
                Character c = s.charAt(i);
                if(c == '('){
                    stack.push(i);
                }else if (c == ')'){
                    if(stack.isEmpty()){
                        notMatchSet.add(i);
                    }else {
                        stack.pop();
                    }
                }
            }
            //如果此时栈中还有元素 说明有新增的元素未匹配上 也要加入到集合 如 ”))((“
            while (!stack.isEmpty()){
                notMatchSet.add(stack.pop());
            }
            StringBuilder sb = new StringBuilder();
            for(int i=0;i<s.length();i++){
                if(!notMatchSet.contains(i)){
                    sb.append(s.charAt(i));
                }
            }
            return sb.toString();
        }
    }

LC394 字符串解码

本题和LC1190十分类似,只不过我们需要使用两个栈,一个数字栈用来存储字符串重复的次数,一个字符串栈,用来两个存储遍历过程中两个左括号”[“之间的字符串。
当遇到右括号时,进行结算。具体代码及详细解释如下:

    class Solution {
   
        private boolean isLetter(char c) {
            if (c >= 'a' && c <= 'z') {
                return true;
            }
            if (c >= 'A' && c <= 'Z') {
                return true;
            }
            return false;
        }

 		/**
         * 数字: 判断下一个是否是数字,得到重复次数
         * 左括号 [:入符号栈
         * 右括号 ]:进行结算
         * 字母:用一个变量先存起来,遇到左括号就入栈
         *
         * @param s
         * @return
         */
        public String decodeString(String s) {
            if (s == null || s.length() == 0) {
                return "";
            }
            char[] arr = s.toCharArray();
            Stack<Integer> numStack = new Stack<>();
            Stack<String> opsStack = new Stack<>();
            int len = arr.length;
            //用来记录两个左括号之间的字符串
            //遇到左括号,将其压栈后,重置为空,继续记录到下一个左括号的值
            //遇到右括号,说明该字符串为与当前右括号相匹配的左括号之间的字符串值
            String tmp = "";
            for (int i = 0; i < len; i++) {
                char c = arr[i];
                //当前是数字
                if (Character.isDigit(c)) {
                    int j = i;
                    int num = 0;
                    while (j < len && Character.isDigit(arr[j])) {
                        num = 10 * num + arr[j++] - '0';
                    }
                    numStack.push(num);
                    i = j - 1;
                } else if (isLetter(c)) {
                    tmp += String.valueOf(c);
                } else if (c == '[') {
                    // [前的字符串压入栈中
                    opsStack.push(tmp);
                    // 清空字符串
                    tmp = "";
                } else {
                    //遇到了右括号,要开始进行结算 此时从数字栈中弹出数据 表示该字符的次数
                    StringBuilder sb = new StringBuilder();
                    if (!numStack.isEmpty()) {
                        // 当前字符串 tmp 的重复次数
                        int cnt = numStack.pop();
                        for (int j = 0; j < cnt; j++) {
                            sb.append(tmp);
                        }
                    }
                    // 栈顶元素 拼接当前 ] 内的字符串
                    tmp = opsStack.peek() + sb.toString();
                    opsStack.pop();
                }
            }
            return tmp;
        }

    }

版权声明:本文为yjpeng125原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。