热题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,然后再考虑他们的顺序。

相关推荐
代码小将2 小时前
Leetcode209做题笔记
java·笔记·算法
专注_每天进步一点点2 小时前
idea 启动Springboot项目在编译阶段报错:java: OutOfMemoryError: insufficient memory
java·spring boot·intellij-idea
dhxhsgrx3 小时前
PYTHON训练营DAY25
java·开发语言·python
Musennn3 小时前
leetcode 15.三数之和 思路分析
算法·leetcode·职场和发展
不知几秋4 小时前
数字取证-内存取证(volatility)
java·linux·前端
CM莫问6 小时前
<论文>(微软)避免推荐域外物品:基于LLM的受限生成式推荐
人工智能·算法·大模型·推荐算法·受限生成
康谋自动驾驶7 小时前
康谋分享 | 自动驾驶仿真进入“标准时代”:aiSim全面对接ASAM OpenX
人工智能·科技·算法·机器学习·自动驾驶·汽车
chxii7 小时前
5java集合框架
java·开发语言
C++ 老炮儿的技术栈7 小时前
什么是函数重载?为什么 C 不支持函数重载,而 C++能支持函数重载?
c语言·开发语言·c++·qt·算法
yychen_java8 小时前
R-tree详解
java·算法·r-tree