1. 问题概述与核心挑战
字符串解码问题(LeetCode #394)是算法面试中的经典题型,主要考察对嵌套结构的处理能力。题目要求将形如k[encoded_string]的编码字符串解码,其中k为正整数,encoded_string可能包含嵌套编码。例如3[a2[c]]应解码为accaccacc。
这个问题的核心难点在于处理多层嵌套结构。想象一下拆俄罗斯套娃的过程——我们需要从最内层的套娃开始拆解,逐步处理外层。在实际编码中,这种嵌套特性天然适合用栈或递归来解决。
提示:在真实面试场景中,面试官通常会期待你首先识别出这是一个栈或递归的应用场景。你可以这样开场:"这个问题有明显的嵌套特性,让我联想到栈的后进先出特性,或者递归的自相似结构..."
2. 双栈解法详解
2.1 算法原理
双栈法使用两个独立的栈结构:
- 计数栈(countStack):存储遇到的重复次数k
- 字符串栈(stringStack):存储已构建的部分字符串
当遇到[时,我们将当前计数和当前字符串分别压栈;遇到]时,弹出栈顶元素进行字符串拼接。这种方法直观反映了问题的嵌套结构。
2.2 完整实现代码
import java.util.Stack; public class DoubleStackSolution { public String decodeString(String s) { Stack<Integer> countStack = new Stack<>(); Stack<StringBuilder> stringStack = new Stack<>(); StringBuilder current = new StringBuilder(); int currentNum = 0; for (char ch : s.toCharArray()) { if (Character.isDigit(ch)) { // 处理多位数情况 currentNum = currentNum * 10 + (ch - '0'); } else if (ch == '[') { // 遇到左括号,保存当前状态 countStack.push(currentNum); stringStack.push(current); // 重置状态 current = new StringBuilder(); currentNum = 0; } else if (ch == ']') { // 遇到右括号,处理重复 StringBuilder temp = current; current = stringStack.pop(); int repeatTimes = countStack.pop(); for (int i = 0; i < repeatTimes; i++) { current.append(temp); } } else { // 普通字符直接追加 current.append(ch); } } return current.toString(); } }2.3 关键点解析
- 数字处理:数字可能由多个字符组成(如"23"),需要用
currentNum = currentNum * 10 + (ch - '0')累积计算 - 状态保存:遇到
[时需要保存当前结果和数字,因为要开始处理新的嵌套层级 - 字符串构建:使用StringBuilder而非String直接拼接,避免创建大量临时对象
- 嵌套处理:通过栈的LIFO特性,自然实现了从内到外的解码顺序
2.4 复杂度分析
- 时间复杂度:O(n×m),其中n是输出字符串长度,m是嵌套层数。最坏情况下每个字符可能被处理m次。
- 空间复杂度:O(d),d是最大嵌套深度,决定了栈的空间使用。
3. 递归解法深度剖析
3.1 递归思路
递归法将每个k[...]视为一个子问题,通过函数调用栈替代显式的栈结构。这种方法更直观地反映了问题的递归本质:解码字符串 = 解码外层 + 处理内层重复。
3.2 递归实现
public class RecursiveSolution { private int index = 0; public String decodeString(String s) { return decode(s); } private String decode(String s) { StringBuilder result = new StringBuilder(); while (index < s.length() && s.charAt(index) != ']') { if (!Character.isDigit(s.charAt(index))) { // 直接追加字母字符 result.append(s.charAt(index++)); } else { // 解析数字部分 int k = 0; while (index < s.length() && Character.isDigit(s.charAt(index))) { k = k * 10 + (s.charAt(index++) - '0'); } // 跳过'[' index++; // 递归解码子串 String decodedSubstring = decode(s); // 跳过']' index++; // 重复添加解码后的子串 for (int i = 0; i < k; i++) { result.append(decodedSubstring); } } } return result.toString(); } }3.3 递归的优缺点
优点:
- 代码简洁直观,直接反映问题本质
- 不需要显式维护栈结构
- 对于熟悉递归的开发者更易理解
缺点:
- 存在栈溢出风险(Java默认栈大小约1MB)
- 递归调用有一定性能开销
- 调试可能更困难
实际经验:在LeetCode的测试用例范围内,递归解法通常不会栈溢出。但在生产环境中处理深度嵌套时,建议使用显式栈结构。
4. 单栈优化方案
4.1 设计思路
单栈法通过自定义栈元素合并计数和字符串,减少栈的数量。我们定义一个内部类StackItem,包含两个字段:
- count:重复次数
- sb:部分字符串结果
4.2 代码实现
import java.util.Stack; public class SingleStackSolution { static class StackItem { int count; StringBuilder sb; StackItem(int count, StringBuilder sb) { this.count = count; this.sb = sb; } } public String decodeString(String s) { Stack<StackItem> stack = new Stack<>(); StringBuilder current = new StringBuilder(); int currentNum = 0; for (char ch : s.toCharArray()) { if (Character.isDigit(ch)) { currentNum = currentNum * 10 + (ch - '0'); } else if (ch == '[') { // 保存当前状态到栈 stack.push(new StackItem(currentNum, current)); // 重置状态 current = new StringBuilder(); currentNum = 0; } else if (ch == ']') { // 弹出栈顶元素 StackItem item = stack.pop(); StringBuilder temp = current; current = item.sb; // 重复添加 for (int i = 0; i < item.count; i++) { current.append(temp); } } else { current.append(ch); } } return current.toString(); } }4.3 性能对比
与双栈法相比:
- 空间效率略高(减少一个栈)
- 代码稍复杂(需要定义额外类)
- 实际运行效率差异不大
5. 常见问题与调试技巧
5.1 典型错误案例
案例1:数字处理不完整
// 错误写法:只处理单个数字字符 if (Character.isDigit(ch)) { k = ch - '0'; // 仅处理一位数 }案例2:栈操作顺序错误
// 错误写法:先弹字符串栈再弹数字栈 StringBuilder temp = stringStack.pop(); // 错误顺序 int count = countStack.pop();5.2 调试技巧
- 打印中间状态:在关键操作后打印栈内容
System.out.println("After [: countStack=" + countStack + ", stringStack=" + stringStack);- 单元测试用例:
@Test public void testNestedCase() { assertEquals("accaccacc", decodeString("3[a2[c]]")); assertEquals("abcabccdcdcdef", decodeString("2[abc]3[cd]ef")); }- 边界情况测试:
- 单层编码:"3[a]"
- 多层嵌套:"3[a2[c]]"
- 无编码部分:"abc3[cd]xyz"
- 大数字:"100[a]"
5.3 性能优化技巧
- 字符串构建优化:
// 使用StringBuilder的repeat方法(Java 11+) String repeated = temp.toString().repeat(count); current.append(repeated);- 预分配空间:
// 预估最终字符串大小 StringBuilder current = new StringBuilder(estimateFinalSize(s));- 避免装箱开销:
// 使用原生int栈(如GNU Trove的TIntStack) TIntStack countStack = new TIntStack();6. 扩展变体与实际应用
6.1 支持多种括号类型
实际应用中可能需要处理不同括号:
public String decodeMixedBrackets(String s) { // 先处理圆括号 s = decodeSpecific(s, '(', ')'); // 再处理方括号 s = decodeSpecific(s, '[', ']'); return s; }6.2 流式处理实现
对于网络流等场景,可以实现流式处理:
public class StreamDecoder { private Stack<Integer> countStack = new Stack<>(); private Stack<StringBuilder> stringStack = new Stack<>(); private StringBuilder current = new StringBuilder(); private int currentNum = 0; public void consume(char ch) { // 实现与双栈法类似的逻辑 } public String getResult() { return current.toString(); } }6.3 实际应用场景
- 配置文件解析:如解析
3[server]为serverserverserver - 模板引擎:处理循环结构
2[<div>{item}</div>] - 数据压缩:解压游程编码
A3B2C为AAABBCC
7. 面试策略与进阶思考
7.1 面试回答框架
- 问题分析:指出嵌套特性适合栈/递归
- 提出解法:先给出双栈法,再讨论递归法
- 优化讨论:考虑字符串构建、数字处理等优化点
- 边界情况:讨论空串、大数字、深度嵌套等情况
- 复杂度分析:明确时间空间复杂度
7.2 进阶问题思考
- 如何支持变量替换?如
x=2;x[a]→aa - 如何处理非法输入(非完整括号对)?
- 如何实现编码器(将字符串编码为压缩格式)?
7.3 学习资源推荐
LeetCode相关题目:
- #772 基本计算器III(类似嵌套结构)
- #385 迷你语法分析器(嵌套列表解析)
算法书籍:
- 《算法导论》栈与递归章节
- 《编程珠玑》字符串处理相关章节
在线课程:
- Coursera普林斯顿算法课程
- LeetCode探索卡片"栈与递归"
在实际开发中,这类字符串处理问题非常常见。掌握好栈和递归的应用,不仅能解决算法题,也能在实际工程中处理复杂的文本解析任务。建议读者在理解基础解法后,尝试实现几个变体问题,并思考如何将这些技术应用到自己的项目中。