Java版LeetCode热题100之字符串解码:深度解析与实战指南
本文将全面剖析 LeetCode 热题第394题《字符串解码》,从题目理解、算法设计(栈 vs 递归)、代码实现,到复杂度分析、面试技巧、实际应用场景,层层递进,帮助你彻底掌握这一经典字符串处理问题。
一、原题回顾
题目描述:
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为:k[encoded_string],表示其中方括号内部的encoded_string正好重复k次。注意k保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数k,例如不会出现像3a或2[4]的输入。
测试用例保证输出的长度不会超过10510^5105。
示例:
示例 1:
输入:s = "3[a]2[bc]" 输出:"aaabcbc"示例 2:
输入:s = "3[a2[c]]" 输出:"accaccacc"示例 3:
输入:s = "2[abc]3[cd]ef" 输出:"abcabccdcdcdef"示例 4:
输入:s = "abc3[cd]xyz" 输出:"abccdcdcdxyz"提示:
- 1≤s.length≤301 \leq s.length \leq 301≤s.length≤30
s由小写英文字母、数字和方括号'[]'组成s保证是一个有效的输入。s中所有整数的取值范围为[1, 300]
二、原题分析
2.1 核心挑战
本题的关键在于处理嵌套结构。例如"3[a2[c]]"中,2[c]是内层编码,需先解码为"cc",再与外层"a"拼接为"acc",最后重复 3 次。
这天然具有递归或栈的特性:
- 遇到
[表示进入新一层; - 遇到
]表示当前层结束,需回溯并处理。
2.2 输入特点
- 无空格:简化了词法分析;
- 数字仅表示重复次数:不会出现
3a这种非法输入; - 括号匹配有效:无需处理不匹配情况;
- 嵌套合法:如
2[3[a]]合法,但2[3[a]不会出现。
2.3 输出约束
- 输出长度 ≤10510^5105:避免无限膨胀,可放心构建字符串。
三、答案构思
3.1 方法一:栈模拟(推荐)
核心思想:
将字符串视为一系列TOKEN(数字、字母、[、]),用栈维护当前未完成的解码片段。
操作规则:
- 遇到数字:解析完整数字(如
"12"),压栈; - 遇到字母或
[:直接压栈; - 遇到
]:- 弹出栈顶元素,直到遇到
[,得到待重复字符串; - 弹出
[; - 弹出栈顶数字
k; - 将字符串重复
k次,压回栈。
- 弹出栈顶元素,直到遇到
最终栈中所有元素拼接即为结果。
3.2 方法二:递归解析(优雅)
核心思想:
将字符串看作文法结构,按 LL(1) 文法递归解析。
BNF 定义:
String → Digits [ String ] String | Alpha String | ε Digits → Digit+ Alpha → a|b|...|z Digit → 0|1|...|9递归逻辑:
- 若当前字符是数字:解析
k,跳过[,递归解析内部String,跳过],重复k次; - 若是字母:取当前字母,递归解析剩余部分;
- 若是
]或结尾:返回空串(终止条件)。
四、完整答案(Java实现)
4.1 方法一:栈模拟
importjava.util.*;classSolution{privateintptr;publicStringdecodeString(Strings){LinkedList<String>stack=newLinkedList<>();ptr=0;while(ptr<s.length()){charch=s.charAt(ptr);if(Character.isDigit(ch)){// 解析完整数字Stringnum=getDigits(s);stack.addLast(num);}elseif(ch=='['||Character.isLetter(ch)){// 字母或左括号直接入栈stack.addLast(String.valueOf(ch));ptr++;}else{// ch == ']'ptr++;// 跳过 ']'// 弹出直到 '['LinkedList<String>parts=newLinkedList<>();while(!"[".equals(stack.peekLast())){parts.addFirst(stack.removeLast());// 保持顺序}stack.removeLast();// 移除 '['// 获取重复次数intrepeat=Integer.parseInt(stack.removeLast());Stringinner=String.join("",parts);// 构建重复字符串StringBuilderrepeated=newStringBuilder();for(inti=0;i<repeat;i++){repeated.append(inner);}stack.addLast(repeated.toString());}}returnString.join("",stack);}privateStringgetDigits(Strings){StringBuildernum=newStringBuilder();while(ptr<s.length()&&Character.isDigit(s.charAt(ptr))){num.append(s.charAt(ptr++));}returnnum.toString();}}4.2 方法二:递归解析
classSolution{privateStrings;privateintindex;publicStringdecodeString(Stringstr){this.s=str;this.index=0;returndecode();}privateStringdecode(){if(index>=s.length()||s.charAt(index)==']'){return"";}StringBuilderresult=newStringBuilder();charch=s.charAt(index);if(Character.isDigit(ch)){// 解析数字 kintk=0;while(index<s.length()&&Character.isDigit(s.charAt(index))){k=k*10+(s.charAt(index)-'0');index++;}index++;// 跳过 '['Stringdecoded=decode();// 递归解析内部index++;// 跳过 ']'// 重复 k 次for(inti=0;i<k;i++){result.append(decoded);}}else{// 字母:直接加入result.append(ch);index++;}// 递归解析剩余部分returnresult+decode();}}✅ 两种方法均通过所有测试用例。
五、代码分析
5.1 栈方法详解
ptr全局指针:避免在函数间传递索引;getDigits():正确解析多位数字(如"12");- 弹出顺序处理:
- 使用
addFirst()保证弹出的字符顺序正确; - 或使用
Collections.reverse()(官方题解做法);
- 使用
- 栈存储
String:统一处理数字、字母、子串。
5.2 递归方法详解
- 递归终止条件:
index越界 或 遇到]; - 数字解析:
k = k * 10 + (ch - '0')高效转整数; - 自动拼接:
result + decode()天然处理连续结构(如"2[ab]3[cd]"); - 无显式栈:依赖函数调用栈,代码更简洁。
5.3 示例执行过程(以"3[a2[c]]"为例)
栈方法:
| 步骤 | 操作 | stack 状态 |
|---|---|---|
| 1 | push “3” | [“3”] |
| 2 | push “[” | [“3”, “[”] |
| 3 | push “a” | [“3”, “[”, “a”] |
| 4 | push “2” | [“3”, “[”, “a”, “2”] |
| 5 | push “[” | [“3”, “[”, “a”, “2”, “[”] |
| 6 | push “c” | [“3”, “[”, “a”, “2”, “[”, “c”] |
| 7 | 遇到 “]” → 弹出到 “[” → 得 “c”,repeat=2 → push “cc” | [“3”, “[”, “a”, “cc”] |
| 8 | 遇到 “]” → 弹出到 “[” → 得 “acc”,repeat=3 → push “accaccacc” | [“accaccacc”] |
递归方法:
decode() ├─ digit: k=3 │ ├─ skip '[' │ ├─ decode() → "a" + decode() │ │ ├─ digit: k=2 │ │ │ ├─ skip '[' │ │ │ ├─ decode() → "c" │ │ │ ├─ skip ']' │ │ │ └─ return "cc" │ │ └─ return "acc" │ ├─ skip ']' │ └─ return "accaccacc" └─ return "accaccacc"六、时间复杂度与空间复杂度分析
6.1 时间复杂度
- 设输入长度为nnn,输出长度为SSS(S≤105S \leq 10^5S≤105);
- 栈方法:
- 遍历输入:O(n)O(n)O(n);
- 每个输出字符被压栈一次、拼接一次:O(S)O(S)O(S);
- 总计:O(n+S)=O(S)O(n + S) = O(S)O(n+S)=O(S)(因S≫nS \gg nS≫n);
- 递归方法:
- 同样每个字符处理一次,拼接SSS个字符;
- 总计:O(S)O(S)O(S)。
💡 注意:虽然
String拼接在 Java 中若用+可能低效,但现代 JVM 会优化为StringBuilder,且题目保证S≤105S \leq 10^5S≤105,可接受。
6.2 空间复杂度
- 栈方法:
- 栈中存储的总字符数 ≈SSS;
- 空间:O(S)O(S)O(S);
- 递归方法:
- 函数调用栈深度 = 嵌套层数 ≤n/2n/2n/2(最坏如
"1[1[1[...]]]"); - 不考虑输出空间,额外空间:O(n)O(n)O(n);
- 若考虑输出,则也是O(S)O(S)O(S)。
- 函数调用栈深度 = 嵌套层数 ≤n/2n/2n/2(最坏如
✅结论:递归方法在额外空间上更优,但栈方法更直观可控。
七、常见问题解答(FAQ)
Q1:为什么栈方法中数字要作为字符串压栈?
答:为了统一栈元素类型。若用
Object栈,需频繁类型转换,易出错。全用String更安全。
Q2:递归方法中,为什么decode()能处理连续结构如"2[ab]3[cd]"?
答:因为每次
decode()返回后,会继续调用decode()解析剩余部分,并用+拼接,天然支持连接。
Q3:能否用StringBuilder优化字符串拼接?
答:可以!尤其在栈方法中,构建重复字符串时用
StringBuilder更高效。但递归方法中因需返回String,改动较大。
Q4:如果输入有大写字母或特殊字符怎么办?
答:题目限定小写字母,但若扩展,只需修改
isLetter()判断逻辑即可,算法不变。
八、优化思路
8.1 优化1:栈中存储StringBuilder
- 将栈元素改为
StringBuilder,避免频繁字符串拼接; - 但需处理数字(仍需
String或Integer),类型不统一,得不偿失。
8.2 优化2:递归中使用StringBuilder参数
privatevoiddecode(StringBuilderresult){// 直接 append 到 result,避免返回拼接}- 减少字符串拷贝;
- 但破坏函数纯度,调试略难。
8.3 优化3:预分配输出空间
- 已知S≤105S \leq 10^5S≤105,可初始化
StringBuilder(100000); - 减少动态扩容开销。
✅实际建议:在面试中,清晰正确的代码比微优化更重要。上述优化可在追问时提出。
九、数据结构与算法基础知识点回顾
9.1 栈(Stack)
- LIFO特性适合处理嵌套、撤销、表达式求值;
- 本题中用于暂存未完成的解码单元。
9.2 递归与分治
- 递归:将问题分解为相同形式的子问题;
- 分治:
k[inner]rest→ 解inner→ 重复 → 解rest。
9.3 编译原理基础:LL(1) 文法
- BNF(巴科斯范式):描述语言语法;
- LL(1):从左到右扫描,最左推导,1 个向前看符号;
- 本题文法无二义性,适合递归下降解析。
9.4 字符串处理技巧
- 数字解析:
num = num * 10 + (ch - '0'); - 避免
+拼接:在循环中用StringBuilder; - 边界处理:
index指针需及时更新。
十、面试官提问环节(模拟)
Q1:你的递归解法空间复杂度是多少?
答:额外空间是函数调用栈深度,最坏为O(n)O(n)O(n)(如
"1[1[1[...]]]")。若考虑输出,则为O(S)O(S)O(S)。
Q2:如果要求原地解码(不使用额外空间),可能吗?
答:不可能。因为输出长度可能远大于输入(如
"100[a]"→ 100 字符),必须分配新空间。
Q3:如何处理无效输入,如括号不匹配?
答:可在栈方法中加校验:
- 遇到
]时若栈空或无[,抛异常;- 结束时栈非空,说明有未闭合
[。
Q4:如果数字很大(如10910^9109),会有什么问题?
答:
- 输出长度爆炸(109×len10^9 \times len109×len),超出内存;
- 需限制
k范围或流式输出;- 但题目已限定k≤300k \leq 300k≤300,无需考虑。
Q5:能否用正则表达式解决?
答:可以,但效率低且难处理嵌套。例如:
while(s.contains("[")){s=s.replaceAll("(\\d+)\$$([a-z]*)\$$",match->repeat(match.group(2),Integer.parseInt(match.group(1))));}但正则无法处理嵌套(如
"3[a2[c]]"需多轮),且性能差,不推荐。
十一、这道算法题在实际开发中的应用
11.1 配置文件解析
- 某些 DSL(领域特定语言)支持重复语法,如:
items:3[item{type:button}] - 解码器可将其展开为 3 个按钮配置。
11.2 数据压缩与传输
- 自定义轻量级压缩协议:
3[hello]表示"hellohellohello"; - 适用于 IoT 设备等带宽受限场景。
11.3 游戏脚本系统
- 游戏关卡描述:
2[enemy{type:goblin}]生成 2 个哥布林; - 地图生成器:
4[room{size:large}]创建 4 个大房间。
11.4 模板引擎
- 简易模板:
3[<li>Item</li>]生成列表项; - 虽不如专业引擎强大,但适合简单场景。
11.5 日志模式展开
- 日志采样模式:
5[INFO: User logged in]表示重复日志; - 用于测试日志处理系统。
💡本质:任何需要“展开重复模式”的场景,都可用此算法。
十二、相关题目推荐
| 题号 | 题目 | 关联点 |
|---|---|---|
| [20] | 有效的括号 | 括号匹配基础 |
| [227] | 基本计算器 II | 表达式解析 |
| [726] | 原子的数量 | 化学式解析(类似嵌套) |
| [735] | 行星碰撞 | 栈的应用 |
| [856] | 括号的分数 | 括号嵌套计算 |
| [880] | 索引处的解码字符串 | 本题变种(不构造全串) |
| [1190] | 反转每对括号间的子串 | 栈+字符串处理 |
| [1544] | 整理字符串 | 栈消除 |
🔔重点推荐:
- LeetCode 880:要求返回解码后第 K 个字符,不能构造全串,需数学优化;
- LeetCode 726:化学式
Mg(OH)2→MgO2H2,同样嵌套解析。
十三、总结与延伸
13.1 核心总结
- 字符串解码是栈和递归的经典应用场景;
- 栈方法:直观、可控,适合复杂扩展;
- 递归方法:代码简洁,体现文法解析思想;
- 两者时间复杂度均为O(S)O(S)O(S),空间各有优劣;
- 关键在于正确处理嵌套和数字解析。
13.2 延伸思考
- 变种1:支持负数或浮点数?(如
-2[ab])- 需扩展词法分析,但算法框架不变。
- 变种2:支持变量?(如
x=ab; 2[x])- 需符号表,结合解释器模式。
- 变种3:流式解码(输入是流,输出也是流)
- 用迭代器或回调,避免全量加载。
13.3 学习建议
- 手写两种解法:加深理解;
- 画递归树:理解调用过程;
- 尝试变种题:如 LeetCode 880;
- 学习编译原理:了解文法、词法分析、递归下降。
🌟最后寄语:字符串解码看似简单,却融合了栈、递归、文法解析等多重知识。掌握它,不仅能搞定面试,更能提升你处理复杂文本的能力。编程之路,贵在融会贯通!