热题100 - 394. 字符串解码

题目描述:

给定一个经过编码的字符串,返回它解码后的字符串。

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。

此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a2[4] 的输入。

示例 1:

ini 复制代码
输入: s = "3[a]2[bc]"
输出: "aaabcbc"

示例 2:

ini 复制代码
输入: s = "3[a2[c]]"
输出: "accaccacc"

示例 3:

ini 复制代码
输入: s = "2[abc]3[cd]ef"
输出: "abcabccdcdcdef"

示例 4:

ini 复制代码
输入: s = "abc3[cd]xyz"
输出: "abccdcdcdxyz"

提示:

  • 1 <= s.length <= 30
  • s 由小写英文字母、数字和方括号 '[]' 组成
  • s 保证是一个 有效 的输入。
  • s 中所有整数的取值范围为 [1, 300]

思路:

用栈实现,题目想说把一个规则给你,让你实现出来,然后把结果字符串给输出出来。

首先想到的是,要区分出来每个字符是什么类型。分类三类:

  • a-z
  • 0-9
  • ]

后面发现[也有用。他是用来把数字给分开的。

  • 对于a-z的情况,定义一个全局变量level,如果level == 0,意味着可以直接进入final。否则,先入栈
  • 对于0-9的情况,要考虑数字部分有没有完事儿,每完事儿要*10然后累加的。
  • 对于]的情况,栈要退出。

这就是我的思路。于是有了下面的实现:

我的实现:

Java 复制代码
class Solution {
    public String decodeString(String s) {
        StringBuilder finalSb = new StringBuilder();
        Deque<String> stack = new LinkedList<>();
        int temp = 0;
        boolean numberFlag = false;
        int level = 0;
        for (char c: s.toCharArray()) {
            if ('a' <= c && 'z' >= c || c == '[') {  // got a a-z
                if (numberFlag) {  // number end, so add level
                    level++;
                    numberFlag = false;
                    stack.push(String.valueOf(temp));
                    temp = 0;
                }
                if ( c != '[') {
                    if (level == 0) {
                        finalSb.append(c);
                    } else {
                        stack.push(String.valueOf(c));
                    }
                }
            } else if ('0' <= c && '9' >= c) {  // got a num
                numberFlag = true;
                temp *= 10;
                temp += (c - '0');
                continue;
            } else if (']' == c) {
                String top = stack.pop();
                StringBuilder sb = new StringBuilder();
                while(!(top.charAt(0) >= '0' && top.charAt(0) <= '9')) {  // while is not anumber
                    sb.insert(0, top);
                    top = stack.pop();
                }
                int times = Integer.valueOf(top);
                StringBuilder ssb = new StringBuilder();
                String s_ = sb.toString();
                while (times > 0) {
                    ssb.append(s_);
                    times--;
                }
                level--;
                String ss = ssb.toString();
                if (stack.isEmpty()) {
                    finalSb.append(ss);
                } else {
                    stack.push(ss);
                }
            }
        }
        return finalSb.toString();
    }
}

这是最终版。中间其实漏掉了挺多东西的,靠着调试一点一点改对了。像temp,numberFlag都是后加的。经验告诉我,这种numberFlag在逻辑好的代码里是不会出现的,但是我这里还是用了。

改进

我当然对这次的实现不太满意,于是请教了DS。于是有了下面的回答:

如果抛开原有思路,我会采用更简洁的双栈方案来实现字符串解码。这种方法将数字和字符串分别处理,代码结构清晰且符合直觉:

java 复制代码
class Solution {
    public String decodeString(String s) {
        // 数字栈保存当前需要重复的次数
        Deque<Integer> countStack = new ArrayDeque<>();
        // 字符串栈保存上层已构建的结果
        Deque<StringBuilder> stringStack = new ArrayDeque<>();
        
        StringBuilder currentStr = new StringBuilder();
        int currentNum = 0;

        for (char c : s.toCharArray()) {
            if (Character.isDigit(c)) {
                // 处理多位数累计
                currentNum = currentNum * 10 + (c - '0');
            } else if (c == '[') {
                // 遇到左括号时保存当前状态
                countStack.push(currentNum);
                stringStack.push(currentStr);
                currentStr = new StringBuilder();
                currentNum = 0;
            } else if (c == ']') {
                // 遇到右括号时构建重复字符串
                StringBuilder decodedStr = stringStack.pop();
                int repeatTimes = countStack.pop();
                decodedStr.append(currentStr.toString().repeat(repeatTimes));
                currentStr = decodedStr;
            } else {
                // 普通字符直接追加
                currentStr.append(c);
            }
        }
        
        return currentStr.toString();
    }
}

算法特点及优势:

  1. 双栈分治: • 数字栈单独处理数值计算 • 字符串栈保存每层上下文 • 时间复杂度 O(n),空间复杂度 O(n)

  2. 状态管理 : • 遇到[时冻结当前状态 • 遇到]时解冻并合并结果 • 完美处理嵌套结构(如3[a2[c]]

  3. 性能优化 : • 全程使用StringBuilder避免字符串拷贝 • 利用String.repeat()简化重复逻辑(JDK11+特性)

  4. 边界处理 : • 自动处理多位数(如100[abc]) • 正确处理前缀字符(如abc3[d]) • 兼容无括号场景(如纯字符串abcdef

执行示例解析 (以输入3[a2[c]]为例):

  1. 遍历到字符3:累计数字 currentNum=3
  2. 遇到[:数字3入栈,空StringBuilder入栈,重置状态
  3. 遍历到a:currentStr = "a"
  4. 遍历到2:currentNum=2
  5. 遇到[:数字2入栈,当前字符串"a"入栈,重置状态
  6. 遍历到c:currentStr = "c"
  7. 遇到]:弹出数字2,弹出字符串"a",构建"a"+"c".repeat(2) → "acc"
  8. 再次遇到]:弹出数字3,弹出空字符串,构建"acc".repeat(3) → "acccaccacc"

这种实现不仅保持了算法的高效性,更通过清晰的逻辑分层让代码具有极佳的可读性,非常便于后续维护和扩展。

我的评价:

真是优秀的实现啊。核心思想我这么理解:

  • 双栈,分别是Integer栈和StringBuilder的栈
  • 遇到左括号,数字,当前的sb入栈,重置状态
  • 遇到右括号,分别从两个栈获得数字和sb,使用String.repeat(int)来简化操作
  • 使用Character.isDigit()判断数字了,其实还有个isLetter可以判断字符

升级后的实现

在完整理解了DS的思路之后,我仿照DS的写法也写了一遍:

Java 复制代码
class Solution {
    public String decodeString(String s) {
        Deque<Integer> nums = new LinkedList<>();
        Deque<StringBuilder> sbs = new LinkedList();
        int currentNum = 0;
        StringBuilder currentSb = new StringBuilder();

        for (char c : s.toCharArray()) {
            // number
            if (Character.isDigit(c)) {
                currentNum = currentNum * 10 + (c - '0');
            } else if (c == '[') {     // left
                sbs.push(currentSb);
                nums.push(currentNum);
                currentSb = new StringBuilder();
                currentNum = 0;
            } else if (c == ']') {  // right
                var dequeSb = sbs.pop();
                int repeatNum = nums.pop();
                dequeSb.append(currentSb.toString().repeat(repeatNum));
                currentSb = dequeSb;
            } else {
                currentSb.append(c);
            }
        }

        return currentSb.toString();
    }
}

好像除了命名(countStack, stringStack, 以及喜欢叫StringBuilder string而不是sb)之外,基本上是一样的。

我想说这才是值得我们去学习的代码 - 简洁,直观,容易维护。

注意到一点,其实这个代码的写法和思维的顺序还挺不同的。像我会按照例子的顺序去考虑,比如先数字,然后是字符,最后是结尾括号(当然我漏掉了开头括号一开始)。但是DS这个代码一开始考虑的就很全,先数字,而且考虑多位,然后是开头括号,然后是终止括号,最后是字符。这个也值得学习,从一开头就考虑所有的case,然后再考虑他们的顺序。

相关推荐
想跑步的小弱鸡3 小时前
Leetcode hot 100(day 3)
算法·leetcode·职场和发展
战族狼魂3 小时前
CSGO 皮肤交易平台后端 (Spring Boot) 代码结构与示例
java·spring boot·后端
xyliiiiiL4 小时前
ZGC初步了解
java·jvm·算法
杉之5 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
爱的叹息5 小时前
RedisTemplate 的 6 个可配置序列化器属性对比
算法·哈希算法
hycccccch5 小时前
Canal+RabbitMQ实现MySQL数据增量同步
java·数据库·后端·rabbitmq
独好紫罗兰5 小时前
洛谷题单2-P5713 【深基3.例5】洛谷团队系统-python-流程图重构
开发语言·python·算法
每次的天空6 小时前
Android学习总结之算法篇四(字符串)
android·学习·算法
天天向上杰6 小时前
面基JavaEE银行金融业务逻辑层处理金融数据类型BigDecimal
java·bigdecimal
请来次降维打击!!!7 小时前
优选算法系列(5.位运算)
java·前端·c++·算法