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 = 0cur = 新空串,专心构造括号里的内容;
  • 遇到 ]:收尾------弹出 klast,新内容 = 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 会污染栈中数据。

  • ] 时拼接顺序写反 :先 curlast 会把前缀放到重复段后面。正确语义是:前缀在前,重复 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 个字符,从后往前按长度逆向定位,避免真实展开,是本题的反向思维版。
相关推荐
邪修king1 小时前
Re:Linux系统篇(二十五):文件系统(一):磁盘硬件底层原理:从物理结构到 CHS/LBA 寻址,搞懂硬盘数据的定位逻辑
java·linux·运维·gpt
驭渊的小故事1 小时前
Spring Boot 注解详解01
java·前端·spring boot
泡海椒1 小时前
jquick-pdf 超详细入门教程:Java 轻量级 HTML 模板生成 PDF 工具
java·开发语言
a187927218311 小时前
【算法】双指针与滑动窗口(三):相向双指针——比较、排除、收缩
算法·leetcode·双指针·滑动窗口·原理·相向双指针·算法讲解
蛋先生DX1 小时前
Java和Go都拍胸脯说"内存我包了",可它们到底是怎么下手的?
java·go·编程语言
SimonKing1 小时前
阅后即焚的加密便签:Cryptgeon,你口袋里的秘密信使
java·后端·程序员
ss2731 小时前
Java全栈实战 | 1.3-03 索引原理:联合索引 (a,b,c) 只查 b 走不了索引?B+ 树一图讲透所有索引玄学
java·数据库
AI 小老六1 小时前
Agent 记忆系统难在取舍
人工智能·算法·架构·agent·memory·harness
大熊背2 小时前
《Color constancy by characterization of illumination chromaticity》之色度色域最大化算法(一)
数码相机·算法·白平衡·色度色域