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

题目描述

给出一个非空有效字符串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 变形与延伸
题目变形
  • (练习)删除最内层的括号
  • (练习)用链表实现一个栈(链栈)
延伸扩展
  • (练习)逆波兰表达式求值:给定数字和运算符,根据规则计算出结果
  • 子程序调用:方法依次进栈,最后调用的最先结束
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容