1. 字符串解码问题概述
字符串解码(String Decoding)是LeetCode上经典的栈应用问题,编号为394题。题目要求我们实现一个算法,能够将经过特定编码规则压缩的字符串还原为原始形式。这类问题在实际开发中非常常见,比如JSON解析、模板引擎处理、配置文件读取等场景都会遇到类似的嵌套结构解析需求。
编码规则可以简单描述为:k[encoded_string],其中k是一个正整数,表示方括号内的encoded_string需要重复k次。例如:
- "3[a]"解码后是"aaa"
- "3[a2[c]]"解码后是"accaccacc"
- "2[abc]3[cd]ef"解码后是"abcabccdcdcdef"
这个问题看似简单,但当遇到多层嵌套时,处理逻辑就会变得复杂。我在实际面试中多次遇到候选人在这道题上翻车,主要原因是没有正确理解栈在处理嵌套结构时的核心作用。
2. 问题分析与解法选择
2.1 暴力法与递归解法
最直观的解法可能是暴力递归:每当遇到一个数字时,就递归处理后面的字符串。这种方法理论上可行,但实现起来容易出错,特别是处理嵌套和并列结构时边界条件较多。我在早期尝试时发现递归解法代码量往往较大,且难以处理像"100[leetcode]"这样的极端case。
2.2 栈解法的优势
栈是解决这类嵌套结构问题的利器。它的核心优势在于:
- 后进先出特性完美匹配嵌套结构的处理顺序
- 可以同时保存数字和字符串两种类型的数据
- 时间复杂度O(n)与输入字符串长度线性相关
在实际编码中,我们需要维护两个栈:
- 数字栈(count_stack):保存遇到的重复次数k
- 字符串栈(str_stack):保存当前层需要重复的字符串片段
这种双栈结构是我经过多次优化后确定的最佳实践,相比单栈方案更清晰易懂。
3. 详细实现步骤与代码解析
3.1 算法流程拆解
让我们通过示例"3[a2[c]]"来逐步拆解处理流程:
- 初始化:当前字符串res = "",数字num = 0
- 遇到'3':累积数字num = 3
- 遇到'[':将res和num分别入栈,重置res和num
- str_stack: [""]
- count_stack: [3]
- 遇到'a':res = "a"
- 遇到'2':num = 2
- 遇到'[':再次入栈
- str_stack: ["", "a"]
- count_stack: [3, 2]
- 遇到'c':res = "c"
- 遇到']':弹出count=2,前层res="a",拼接得到res="a"+"cc"="acc"
- 遇到']':弹出count=3,前层res="",拼接得到res=""+"accaccacc"="accaccacc"
3.2 Python实现代码
python复制def decodeString(s: str) -> str:
str_stack = []
count_stack = []
current_str = ""
current_num = 0
for char in s:
if char.isdigit():
current_num = current_num * 10 + int(char)
elif char == '[':
str_stack.append(current_str)
count_stack.append(current_num)
current_str = ""
current_num = 0
elif char == ']':
count = count_stack.pop()
prev_str = str_stack.pop()
current_str = prev_str + current_str * count
else:
current_str += char
return current_str
这段代码有几个关键点需要注意:
- 数字可能是多位数,需要用current_num * 10累加
- 遇到'['时需要保存当前状态并重置
- 遇到']'时需要从栈中恢复之前的状态
- 普通字符直接追加到current_str
4. 边界条件与常见错误
4.1 典型错误案例
在实际编码练习中,我发现以下几个常见错误:
-
数字处理不完整:只处理了单数字符,遇到"100[a]"会出错
- 错误示例:直接使用int(char)而不用current_num累加
-
栈使用不当:混淆了字符串栈和数字栈的顺序
- 错误示例:count_stack.append(current_str)
-
嵌套处理错误:没有正确重置current_str和current_num
- 错误示例:在遇到'['时忘记重置current_num
4.2 测试用例设计
为了验证代码的正确性,我建议测试以下典型case:
python复制test_cases = [
("3[a]2[bc]", "aaabcbc"),
("3[a2[c]]", "accaccacc"),
("2[abc]3[cd]ef", "abcabccdcdcdef"),
("10[a]", "aaaaaaaaaa"),
("1[a1[b1[c]]]", "abc"),
("", ""),
("a", "a"),
("3[a]2[b4[F]c]", "aaabFFFFcbFFFFc")
]
特别要注意空字符串、单字符、多层嵌套和并列结构的情况。我在面试中经常看到候选人只测试简单case就认为代码正确,实际上边界条件才是真正考验算法鲁棒性的地方。
5. 算法优化与变种问题
5.1 性能优化思路
虽然栈解法已经是较优方案,但仍有一些优化空间:
-
字符串拼接优化:Python中字符串是不可变对象,频繁拼接会产生大量临时对象。可以使用列表代替字符串,最后join。
优化后代码:
python复制def decodeString(s: str) -> str: stack = [] current_chars = [] current_num = 0 for c in s: if c.isdigit(): current_num = current_num * 10 + int(c) elif c == '[': stack.append((''.join(current_chars), current_num)) current_chars = [] current_num = 0 elif c == ']': prev_str, num = stack.pop() current_chars = [prev_str + ''.join(current_chars) * num] else: current_chars.append(c) return ''.join(current_chars) -
迭代器处理:可以使用生成器来逐个处理字符,减少索引操作。
5.2 相关变种问题
掌握了基础解法后,可以尝试以下变种问题:
- 带转义的字符串解码:如"3[a\]2[b]]"应该解码为"a]a]a]bb"
- 多分隔符支持:支持多种括号如{}、()等
- 流式处理:当输入是数据流时如何逐步解码
- 编码器实现:实现反向的字符串编码功能
6. 实际工程应用场景
字符串解码算法看似简单,但在实际工程中有广泛应用:
- 配置文件解析:如处理"3x[server]2x[database]"这样的配置模板
- 模板引擎:处理类似"Hello {3x[user]}"的嵌套模板
- 数据压缩:简单的重复模式压缩算法
- 协议解析:某些自定义协议中的重复字段表示
我在实际项目中曾用类似算法处理过日志压缩场景。系统产生的日志中有大量重复模式(如连续的错误报告),使用类似的编码规则可以减少日志体积。解码算法的性能直接影响日志分析系统的吞吐量,因此优化后的栈实现比递归版本更适合生产环境。
7. 面试技巧与注意事项
作为LeetCode热题,字符串解码经常出现在面试中。根据我的面试经验,以下几点值得注意:
- 先沟通再编码:不要直接开始写代码,先和面试官确认输入输出要求
- 举例说明:用具体例子解释你的算法思路
- 边界讨论:主动提出要测试的边界case
- 复杂度分析:能清晰说明时间空间复杂度
- 代码风格:良好的变量命名和适当的注释
一个常见的面试陷阱是面试官会问:"如果输入字符串非常大(比如几个GB),你的算法还能工作吗?"这时就需要讨论内存优化方案,比如使用生成器逐步处理而不是一次性加载整个字符串。
8. 个人实战心得
经过多次实践和教学,我总结了以下几点心得:
- 画图辅助:在处理复杂嵌套时,画出入栈出栈的示意图能极大帮助理解
- 分步调试:使用print语句输出每一步的栈状态和变量值
- 测试驱动:先写测试case再实现代码,确保覆盖所有边界条件
- 性能考量:在OJ系统上,Python的字符串拼接方式可能影响运行时间
- 举一反三:理解栈的这种用法后,可以解决许多类似问题,如计算器、HTML标签验证等
最后提醒一点:这道题的变种非常多,有些公司会要求处理更复杂的编码规则。核心思路不变,但需要灵活调整栈中保存的数据结构。建议在掌握基础解法后,尝试自己设计一些变种问题来练习。
