一句话说明核心方法
用两个栈(数字栈 + 字符串栈)手动模拟递归:遇到 [ 就把"重复次数和当前进度"存档并开新层,遇到 ] 就把新层内容按次数展开、和上一层进度拼回去。
思路推导
题意转化 :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 个字符,从后往前按长度逆向定位,避免真实展开,是本题的反向思维版。