美文网首页
算法 1.3 栈:删除最外层括号【leetcode 1021】

算法 1.3 栈:删除最外层括号【leetcode 1021】

作者: 珺王不早朝 | 来源:发表于2021-01-02 15:15 被阅读0次

    题目描述

    给出一个非空有效字符串S,考虑将其进行原语化分解。
    使得:S=P_1+P_2+...+P_k,其中P_i是有效括号字符串原语。
    对S进行原语化分解,删除分解中每个原语字符串的最外层括号,返回S。
    ➢ 有效括号字符串:例如:"( )","( ( ) ) ( )" 和 "( ( ) ( ( ) ) )"
    ➢ 原语:如果有效字符串S非空,且不能将其拆分为 S=A+B,我们称其为原语(primitive),其中A和B都是非空有效括号字符串。

    数据结构

    • 字符串、数组、栈

    算法思维

    • 遍历、计数器、累加、栈操作

    解题要点

    • 使用两个变量作为计数器,分别记录 "(" 和 ")" 的个数,左右括号数量相等时则说明发现了一个原语
    • 字符串截取时,区间的 左闭右开 性质
    • 栈的相关概念

    栈(stack)
    限定仅在表尾进行插入和删除操作的线性表
    栈顶(Top):操作数据的一端,即表尾
    栈底(Bottom):线性表的另一端,即表头
    特点:LIFO(Last In First Out),后进先出
    基本操作:
    ➢ 进栈:push(),在栈顶插入元素,入栈、压栈
    ➢ 出栈:pop(),从栈顶删除数据
    ➢ 查栈:peek(),查看栈顶数据,不取出
    ➢ 判断空:isEmpty()


    解题步骤

    一. Comprehend 理解题意
    题目基本信息
    • 有效括号字符串:括号必然成对出现
    • 给定字符串 S 非空,但返回结果可能为空
    • 原语化分解只需要进行一级
    • 返回的是各个原语删除了最外层括号后拼接成的新字符串
    附加信息
    • S.length <= 10000
    • S[i] 为 "(" 或 ")"
    解题思路
    • 解法一:分解、删除、合并
    • 解法二:定位、直取、合并
    二. Choose 选择数据结构与算法
    解法一:分解、删除再合并
    1. 先分解成字符串原语
    2. 再删除每部分最外层括号
    3. 返回各部分合并后的结果

    数据结构:字符串、数组
    算法思维:遍历、计数器、累加

    简化过程:既然最外层的括号是要删除的,直接取最外层括号内部的字符串是否更简单呢?

    解法二:定位、直取再合并
    1. 对原字符串进行原语识别
    2. 获取不包含最外层括号的子串
    3. 将各部分拼接返回

    数据结构:字符串、数组
    算法思维:遍历、计数器、累加

    三. Code 编码实现基本解法
    解法一:分解、删除再合并
    1. 定义容器存储原语子串: new ArrayList<String>();
    2. 定义左括号、右括号计数器: int left = 0, right = 0;
    3. 遍历字符串,读取到括号时对应计数器自增
    4. 检查是否到达原语结尾,截取原语子串并添加到容器中
    5. 遍历容器,删除最外层括号后合并成新串
    边界问题
    • 遍历字符串,注意索引越界:i < S.length()
    • 截取原语字符串时,注意起止索引:[start, end)
    细节问题
    • 需要记录上一次截取原语子串之后的位置
    • 删除原语子串的最外层括号,其实就是重新截取
    class Solution {
    
        public String removeOuterParentheses(String S) {
            int len = S.length();
            // 1.定义容器存储原语子串 
            List < String > list = new ArrayList < > ();
            // 2.定义左括号、右括号计数器 
            int left = 0, right = 0, lastOpr = 0;
            // 3.遍历字符串,读取到括号时对应计数器自增 
            for(int i = 0; i < len; i++) {
                char c = S.charAt(i);
                if(c == '(') {
                    left++;
                }
                else if(c == ')') {
                    right++;
                }
                // 4.检查是否到达某个原语结尾,截取原语子串添加到容器 
                if(left == right) {
                    list.add(S.substring(lastOpr, i + 1));
                    lastOpr = i + 1;
                }
            }
            // 5.遍历容器中的原语子串,删除最外层后合并成新串 
            StringBuilder sb = new StringBuilder();
            for(String s: list) {
                sb.append(s.substring(1, s.length() - 1));
            }
            return sb.toString();
        }
    }
    

    时间复杂度:O(n) -- 字符串的遍历 O(n),各个原语的遍历 O(n)
    空间复杂度:O(n) -- 原语子串 O(n),存放原语子串的容器 O(n),生成的新字符串 O(n),计数器 O(1)
    执行耗时:6 ms,击败了 49.10% 的Java用户
    内存消耗:38.6 MB,击败了 39.07% 的Java用户

    解法二:定位、直取再合并
    1. 定义容器存储删除外层括号后的原语子串: new StringBuilder();
    2. 定义左括号、右括号计数器: int left = 0, right = 0;
    3. 遍历字符串,读取到括号时对应计数器自增
    4. 检查是否到达原语结尾,截取不包含最外层的原语子串并拼接到容器中
    class Solution {
    
        public String removeOuterParentheses(String S) {
            int len = S.length();
            // 1.定义容器存储删除外层括号后的原语子串 
            StringBuilder sb = new StringBuilder();
            // 2.定义左括号、右括号计数器 
            int left = 0, right = 0, lastOpr = 0;
            // 3.遍历字符串,读取到括号时对应计数器自增 
            for(int i = 0; i < len; i++) {
                char c = S.charAt(i);
                if(c == '(') {
                    left++;
                }
                else if(c == ')') {
                    right++;
                }
                // 4.检查是否到达某个原语结尾,截取不包含最外层的原语子串添加到容器 
                if(left == right) {
                    sb.append(S.substring(++lastOpr, i));
                    lastOpr = i + 1;
                }
            }
            return sb.toString();
        }
    }
    

    时间复杂度:O(n) -- 字符串的遍历 O(n),各个原语的遍历 O(n)
    空间复杂度:O(n) -- 原语子串 O(n),存放原语子串的容器 O(n),生成的新字符串 O(n),计数器 O(1)
    执行耗时:4 ms,击败了 62.60% 的Java用户
    内存消耗:38.5 MB,击败了 51.77% 的Java用户

    四. Consider 思考更优解

    剔除无效代码,优化空间消耗

    • 只需要一个计数器:"(" 自增 ; ")" 自减
    • 如果字符串数据只是两两配对怎么处理?每个原语的开始和结束标识都不同,此时用计数器无法满足需求
    • 因此考虑:有没有一种支持后进先出的数据结构?
    最优解:栈解法

    数据结构:字符串、栈
    算法思维:遍历、栈操作、累加

    五. Code 编码实现最优解
    最优解:栈解法
    1. 使用数组模拟一个栈,临时存储字符,替代计数器: push(Character) ; pop(); isEmpty()
    2. 遍历字符串,根据情况进行入栈/出栈操作
      读取到左括号,左括号入栈;读取到右括号,左括号出栈
    3. 判断栈是否为空,若为空,找到了一个完整的原语
    4. 截取不含最外层括号的原语子串并进行拼接
    边界问题
    • 遍历字符串,注意索引越界:i < S.length()
    • 截取原语字符串时,注意起止索引:[start, end)
    细节问题
    • 需要记录上一次截取原语子串之后的位置
    • 右括号无需进栈
    class Solution {
    
        public String removeOuterParentheses(String S) {
            Stack < Character > stack = new Stack < > ();
            StringBuilder sb = new StringBuilder();
            int lastOpr = 0;
            //1.遍历字符串
            for(int i = 0; i < S.length(); i++) {
                char c = S.charAt(i);
                //2.如果是左括号,入栈
                if(c == '(') {
                    stack.push(c);
                }
                else if(c == ')') {
                    //3.如果是右括号,出栈
                    stack.pop();
                }
                //4.如果栈空了,则说明读取了一个原语
                if(stack.isEmpty()) {
                    sb.append(S.substring(++lastOpr, i));
                    lastOpr = i + 1;
                }
            }
            return sb.toString();
        }
    
        //自定义栈
        class Stack < E > {
            Object[] arr = new Object[10000];
            int index = -1;
            int size = 0;
            public void push(E e) {
                arr[++index] = e;
                size++;
            }
            public E pop() {
                E e = peek();
                //需要进行非空判断
                if(e != null) {
                    arr[index--] = null;
                    size--;
                }
                return e;
            }
            public E peek() {
                //需要进行非空判断
                return index < 0 ? null : (E) arr[index];
            }
            public boolean isEmpty() {
                return size == 0;
            }
        }
    
    }
    

    时间复杂度:O(n) -- 字符串的遍历 O(n),各个原语的遍历 O(n)
    空间复杂度:O(n) -- 原语子串 O(n),生成的新字符串 O(n),使用栈临时存储数据 O(n/2),存放原语子串的容器 O(n)计数器 O(1)
    执行耗时:9 ms,击败了 29.76% 的Java用户
    内存消耗:38.1 MB,击败了 97.68% 的Java用户

    栈解法的优化 #1
    1. 直接用数组取代栈
      创建数组、栈顶索引,使用数组操作入栈和出栈
    2. 将原字符串转为数组进行遍历: char[] s = S.toCharArray();
    3. 去掉截取子串的操作,将原语字符直接拼接
      读取到左括号:栈中有数据,则当前字符必属原语
      读取到右括号:匹配后栈大小不为 0,则当前字符必属原语
    class Solution {
    
        public String removeOuterParentheses(String S) {
            StringBuilder result = new StringBuilder();
            
            // 1.直接用数组取代栈 
            int index = -1; // 栈顶索引 
            int len = S.length();
            char[] stack = new char[len];
            char[] s = S.toCharArray();
    
            // 2.遍历字符串,根据情况进行入栈 / 出栈操作 
            for(int i = 0; i < len; i++) {
                char ch = s[i];
                if(ch == '(') { // 遇到左括号,左括号入栈 
                    // 3.去掉截取子串的操作,将原语字符直接拼接
                    if(index > -1) { // 此前有数据,当前必属原语 
                        result.append(ch);
                    }
                    stack[++index] = ch;
                }
                else /*if (ch == ')')*/ { // 遇到右括号,左括号出栈 
                    stack[index--] = '\u0000'; // 栈顶的左括号出栈 
                    if(index > -1) {
                        result.append(ch);
                    }
                }
            }
            return result.toString();
        }
    
    }
    

    时间复杂度:O(n) -- 遍历整个字符串 O(n)
    空间复杂度:O(n) -- 代替栈的数组 O(n),字符数组 O(n),生成的新字符串 O(n)
    执行耗时:3 ms,击败了 90.24% 的Java用户
    内存消耗:38.3 MB,击败了 97.90% 的Java用户

    栈解法的优化 #2
    • 用 计数器(int) 取代字符数组,实现栈的功能
      使用计数器的 自增和自减 模拟 入栈和出栈
      注意:之所以可以这样做是因为原语的起始和符号始终不变,始终为 "(" 和 ")"
    class Solution {
    
        public String removeOuterParentheses(String S) {
            //0.用计数器替换栈
            int size = 0; //栈大小计数器
            char[] charArr = S.toCharArray();
            StringBuilder sb = new StringBuilder();
    
            //1.遍历字符串 -> 遍历字符数组
            for(int i = 0; i < charArr.length; i++) {
                char c = charArr[i];
                //2.如果是左括号,且栈中有数据,则当前字符属于原语
                if(c == '(' && size++ != 0) sb.append(c);
                //3.如果是右括号,且配对后栈大小不为0,则当前字符属于原语
                else if(c == ')' && --size != 0) sb.append(c);
            }
            return sb.toString();
        }
    }
    

    时间复杂度:O(n) -- 遍历整个字符串 O(n)
    空间复杂度:O(n) -- 代替栈的数组 O(n),计数器 O(1),字符数组 O(n),生成的新字符串 O(n)
    执行耗时:2 ms,击败了 100.00% 的Java用户
    内存消耗:38.2 MB,击败了 93.63% 的Java用户

    六. Change 变形与延伸
    题目变形
    • (练习)删除最内层的括号
    • (练习)用链表实现一个栈(链栈)
    延伸扩展
    • (练习)逆波兰表达式求值:给定数字和运算符,根据规则计算出结果
    • 子程序调用:方法依次进栈,最后调用的最先结束

    相关文章

      网友评论

          本文标题:算法 1.3 栈:删除最外层括号【leetcode 1021】

          本文链接:https://www.haomeiwen.com/subject/sscqoktx.html