☰
LeetCode字符串解码问题:双栈与递归解法详解
2026/11/6 5:35:48 网站建设 项目流程

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 关键点解析

  1. 数字处理:数字可能由多个字符组成(如"23"),需要用currentNum = currentNum * 10 + (ch - '0')累积计算
  2. 状态保存:遇到[时需要保存当前结果和数字,因为要开始处理新的嵌套层级
  3. 字符串构建:使用StringBuilder而非String直接拼接,避免创建大量临时对象
  4. 嵌套处理:通过栈的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 递归的优缺点

优点:

  1. 代码简洁直观,直接反映问题本质
  2. 不需要显式维护栈结构
  3. 对于熟悉递归的开发者更易理解

缺点:

  1. 存在栈溢出风险(Java默认栈大小约1MB)
  2. 递归调用有一定性能开销
  3. 调试可能更困难

实际经验:在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 调试技巧

  1. 打印中间状态:在关键操作后打印栈内容
System.out.println("After [: countStack=" + countStack + ", stringStack=" + stringStack);
  1. 单元测试用例:
@Test public void testNestedCase() { assertEquals("accaccacc", decodeString("3[a2[c]]")); assertEquals("abcabccdcdcdef", decodeString("2[abc]3[cd]ef")); }
  1. 边界情况测试:
  • 单层编码:"3[a]"
  • 多层嵌套:"3[a2[c]]"
  • 无编码部分:"abc3[cd]xyz"
  • 大数字:"100[a]"

5.3 性能优化技巧

  1. 字符串构建优化:
// 使用StringBuilder的repeat方法(Java 11+) String repeated = temp.toString().repeat(count); current.append(repeated);
  1. 预分配空间:
// 预估最终字符串大小 StringBuilder current = new StringBuilder(estimateFinalSize(s));
  1. 避免装箱开销:
// 使用原生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 实际应用场景

  1. 配置文件解析:如解析3[server]为serverserverserver
  2. 模板引擎:处理循环结构2[<div>{item}</div>]
  3. 数据压缩:解压游程编码A3B2C为AAABBCC

7. 面试策略与进阶思考

7.1 面试回答框架

  1. 问题分析:指出嵌套特性适合栈/递归
  2. 提出解法:先给出双栈法,再讨论递归法
  3. 优化讨论:考虑字符串构建、数字处理等优化点
  4. 边界情况:讨论空串、大数字、深度嵌套等情况
  5. 复杂度分析:明确时间空间复杂度

7.2 进阶问题思考

  1. 如何支持变量替换?如x=2;x[a]→aa
  2. 如何处理非法输入(非完整括号对)?
  3. 如何实现编码器(将字符串编码为压缩格式)?

7.3 学习资源推荐

  1. LeetCode相关题目:

    • #772 基本计算器III(类似嵌套结构)
    • #385 迷你语法分析器(嵌套列表解析)
  2. 算法书籍:

    • 《算法导论》栈与递归章节
    • 《编程珠玑》字符串处理相关章节
  3. 在线课程:

    • Coursera普林斯顿算法课程
    • LeetCode探索卡片"栈与递归"

在实际开发中,这类字符串处理问题非常常见。掌握好栈和递归的应用,不仅能解决算法题,也能在实际工程中处理复杂的文本解析任务。建议读者在理解基础解法后,尝试实现几个变体问题,并思考如何将这些技术应用到自己的项目中。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询