LeetCode hot100——394.字符串解码:Java 双栈模拟

一句话说明核心方法

用两个栈(数字栈 + 字符串栈)手动模拟递归:遇到 [ 就把"重复次数和当前进度"存档并开新层,遇到 ] 就把新层内容按次数展开、和上一层进度拼回去。


思路推导

题意转化 :k[encoded_string] 是一种支持嵌套 的重复记法,比如 3[a2[c]] 要从最里层往外展开成 accaccacc。

关键观察 :嵌套展开天然对应"递归":读到 [ 相当于进入一个新函数,读到 ] 相当于函数返回。而递归的本质就是系统帮你维护一个调用栈------那我们干脆手动维护这个栈,把每层需要的两个状态存起来:

  • 这一层要重复几次(数字);
  • 这一层展开前已经攒下的前缀(字符串)。

算法选择 :双栈 + 一个"当前层"变量 cur(StringBuilder) + 一个"当前数字累积器" num:

  • 遇到数字:num = num * 10 + (ch - '0'),支持多位数(如 12[a]);
  • 遇到 [:存档------numStack.push(num)、strStack.push(cur),然后 num = 0、cur = 新空串,专心构造括号里的内容;
  • 遇到 ]:收尾------弹出 k 和 last,新内容 = last + cur 重复 k 次,赋回 cur;
  • 遇到字母:直接 cur.append(ch)。

与 LeetCode 20 的区别 :20 题栈里只存字符、配对完就丢;本题栈里存的是跨层存活的状态 (数字、前缀),] 弹出的不是"丢弃"而是"合并回上层"。


过程示意(s = "3a2\[c]")

复制代码
读 3   : num = 3
读 [   : numStack=[3]   strStack=[""]   cur=""      num=0
读 a   : cur = "a"
读 2   : num = 2
读 [   : numStack=[2,3] strStack=["a",""] cur=""    num=0
读 c   : cur = "c"
读 ]   : k=2, last="a"  → cur = "a" + "c"×2 = "acc"
读 ]   : k=3, last=""   → cur = ""  + "acc"×3 = "accaccacc"

扫描结束,栈空,return cur = "accaccacc"

可以清楚看到"存档 → 干活 → 合并"的节奏:每次 [ 把现场封存,每次 ] 把成果上交。


Java 完整代码

java

复制代码
import java.util.ArrayDeque;
import java.util.Deque;

class Solution {
    public String decodeString(String s) {
        Deque<Integer> numStack = new ArrayDeque<>();
        Deque<String> strStack = new ArrayDeque<>();
        int num = 0;                       // 当前正在累积的数字
        StringBuilder cur = new StringBuilder(); // 当前层正在构造的内容

        for (char ch : s.toCharArray()) {
            if (Character.isDigit(ch)) {
                // 多位数累积:如 "12" → 1*10 + 2
                num = num * 10 + (ch - '0');
            } else if (ch == '[') {
                // 存档:记下重复次数与上一层前缀,再开新层
                numStack.push(num);
                strStack.push(cur.toString());
                num = 0;
                cur = new StringBuilder();
            } else if (ch == ']') {
                // 收尾:取出次数与上层前缀,展开后合并回去
                int k = numStack.pop();
                StringBuilder last = new StringBuilder(strStack.pop());
                for (int i = 0; i < k; i++) {
                    last.append(cur);
                }
                cur = last;
            } else {
                // 普通字母,直接追加到当前层
                cur.append(ch);
            }
        }
        return cur.toString();
    }
}

关键代码逐行解释

  • num = num * 10 + (ch - '0');

    数字可能是多位的(k 最大 300)。每读到一个数字字符就"整体左移一位再加个位","12" 会被读成 1 → 12。写成 num = ch - '0' 在 12[a] 上直接翻车。

  • numStack.push(num); strStack.push(cur.toString());

    [ 是层与层的分界。必须把 num 和 cur 同时 存档:前者是这层的重复次数,后者是这层开始前已经攒好的前缀(如 3[a2[c]] 里内层开始前的 "a")。cur.toString() 存的是快照,之后 cur 指向新对象,互不干扰。

  • num = 0; cur = new StringBuilder();

    存档后立刻清零、开新层。num = 0 防止上个数字泄漏进下一层;new 一个新 StringBuilder 而不是 cur.setLength(0) 复用,是因为旧内容已经以快照形式进栈了,复用会破坏栈里的数据。

  • StringBuilder last = new StringBuilder(strStack.pop()); + for (int i = 0; i < k; i++) last.append(cur);

    ] 的合并动作:先恢复上层前缀 last,再把当前层内容 cur 追加 k 次。顺序不能反 ------last 是"括号前"的内容,必须在最前面。注意先把栈里的 String 包回 StringBuilder 再循环 append,避免在循环里做字符串 + 拼接。

  • return cur.toString();

    输入保证合法(所有括号闭合),扫描结束时 numStack、strStack 必然已空,最外层的 cur 就是完整答案。


时间、空间复杂度

  • 时间复杂度:O(S),S 为解码后字符串长度

    不能只按输入长度 n 算:"300[abc]" 输入 9 个字符,输出 900 个。每个输出字符恰好由一次 append 产生,加上 n 次扫描判断,总量与 S 同阶;题目保证 S ≤ 10^5。

  • 空间复杂度:O(S)

    最终的 cur 与中途各层快照(栈里的 String)加起来与输出长度同阶;双栈深度为嵌套深度,至多 O(n)。


易错点

  • 多位数只读一位 :直接 num = ch - '0' 会把 "12[a]" 解成 "a"。必须用 num * 10 + (ch - '0') 累积,并在遇到 [ 时清零。

  • [ 时忘记重置 cur :不 new 新的 StringBuilder(或忘了清空),内层内容会和外层前缀混在一起;同理,栈里必须存 cur.toString() 快照而不是存引用,否则后续 append 会污染栈中数据。

  • ] 时拼接顺序写反 :先 cur 后 last 会把前缀放到重复段后面。正确语义是:前缀在前,重复 k 份的当前层在后。

  • 入栈时机 :num 必须在遇到 [ 时入栈,而不是遇到数字时------数字读完可能还有下一位,[ 才代表"这个数定型了"。


可复用模板

"双栈模拟嵌套展开"的通用骨架:

java

复制代码
Deque<Integer> numStack = new ArrayDeque<>();   // 每层的重复次数
Deque<StringBuilder> strStack = new ArrayDeque<>(); // 每层开始前的现场
int num = 0;
StringBuilder cur = new StringBuilder();

for (char ch : input) {
    if (isDigit(ch))      num = num * 10 + (ch - '0');   // 累积参数
    else if (isOpen(ch))  { push 现场; 重置 num/cur; }    // 进层:存档
    else if (isClose(ch)) { cur = 弹出现场 + cur × num; }  // 出层:合并
    else                  cur.append(ch);                 // 干活
}
return cur;

变体提示:

  • 单栈法 :只用一个 Deque<StringBuilder>,数字单独用变量累积,[ 时压入当前串,] 时弹出拼接------本质相同,少一个栈。
  • 递归法 :写一个带全局下标 idx 的递归函数,[ 递归、] 返回,让 JVM 调用栈代替手动双栈,逻辑更直白但深层嵌套有栈溢风险。
  • 不需要真的展开时(如 880 题"索引处的解码字符串"):只追踪长度、用逆向定位跳过重复段,把 O(S) 压到 O(n)。

相似题及区别

  • LeetCode 20 有效的括号 :同样"遇右括号弹栈",但 20 是校验 ------弹出即丢弃;394 是求值------弹出后要合并回上层。
  • LeetCode 71 简化路径 :栈存路径段,.. 弹出、其余压入,最后 join;没有数字参数,单栈即可。
  • LeetCode 1047 删除字符串中的所有相邻重复项:单栈消消乐,配对条件是"与栈顶相等",不存在嵌套展开。
  • LeetCode 880 索引处的解码字符串 :同样是 k[...] 记法,但只要求第 N 个字符,从后往前按长度逆向定位,避免真实展开,是本题的反向思维版。
相关推荐
panxianren1 小时前
Bun 1.4 的 bun test --parallel 将 4 秒的测试套件缩短到约 1 秒
java·javascript
sunshine22 girl2 小时前
Java学习七 Java 项目实战2-注册业务
java·学习
奋发向前wcx3 小时前
CSP-J复赛模拟赛4 王晨旭补题 2026.10.5
算法
省钱兄--zs3 小时前
北京24小时自助健身房系统软件开发实战:从架构设计到部署指南
java·数据仓库·spring boot·小程序·需求分析
youdexiang3 小时前
Android录音软件时间关键词检索功能分析
java·人工智能
sunshine22 girl4 小时前
Java学习七 Java 项目实战1-项目创建和主页面搭建
java·开发语言·学习
code2cat4 小时前
Java进阶篇之AtomicStampedReference:引用回到原值,版本戳仍能记住变化
java·cas·并发编程·aba
念何架构之路5 小时前
zap采样器与性能优化内幕
算法·性能优化·哈希算法
泡茶喝茶写代码6 小时前
A股量化数据工程:从 REST 接口到策略信号(第 8 篇):成长能力因子:营收与利润增速
java·python·股票数据api·股票数据api接口·股票量化数据api·股票量化数据接口·股票数据api数据
晒太羊的猫6 小时前
hot100动态规划刷题
算法·动态规划