1. 字符串解码问题解析
今天我们来深入探讨LeetCode第394题"字符串解码"的解题思路和实现细节。这道题在面试中经常出现,考察的是对字符串处理和栈结构的理解运用能力。
1.1 问题描述与示例分析
题目要求我们解码一个经过特定编码的字符串。编码规则是:k[encoded_string],表示方括号内的encoded_string需要重复k次。其中k保证是正整数,输入字符串总是有效的,且不包含额外的空格。
让我们仔细分析几个示例:
示例1:
输入:"3[a]2[bc]"
输出:"aaabcbc"
解析:a重复3次,bc重复2次,然后拼接起来
示例2:
输入:"3[a2[c]]"
输出:"accaccacc"
解析:这里出现了嵌套结构,先解内层的2[c]得到cc,然后a+cc=acc,最后重复3次
1.2 解题思路分析
面对这类字符串解码问题,我们需要考虑以下几点:
- 如何处理嵌套的括号结构?
- 如何记录当前需要重复的字符串?
- 如何保存之前处理的状态以便后续恢复?
栈结构完美契合这些需求。当遇到'['时,我们需要保存当前状态;遇到']'时,我们需要取出之前保存的状态进行处理。具体来说:
- 数字栈:保存遇到的重复次数k
- 字符串栈:保存遇到'['时已经解码的字符串部分
2. 算法实现详解
2.1 代码结构解析
让我们逐行分析给出的Java解决方案:
java复制class Solution {
public String decodeString(String s) {
StringBuilder sb = new StringBuilder(); // 当前正在构建的字符串
Stack<Integer> numStack = new Stack<>(); // 数字栈
Stack<String> strStack = new Stack<>(); // 字符串栈
int digit = 0; // 当前正在解析的数字
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (Character.isDigit(c)) {
digit = digit * 10 + c - '0'; // 处理多位数
} else if (Character.isLetter(c)) {
sb.append(c); // 直接添加到当前字符串
} else if (c == '[') {
numStack.push(digit); // 保存当前数字
strStack.push(sb.toString()); // 保存当前字符串
sb = new StringBuilder(); // 重置当前字符串
digit = 0; // 重置数字
} else if (c == ']') {
int pop = numStack.pop(); // 取出重复次数
StringBuilder popped = new StringBuilder(strStack.pop()); // 取出之前保存的字符串
for (int j = 0; j < pop; j++) {
popped.append(sb); // 重复添加当前字符串
}
sb = popped; // 更新当前字符串
}
}
return sb.toString();
}
}
2.2 关键步骤说明
-
数字处理:
- 当遇到数字字符时,我们需要考虑多位数的情况
- 使用
digit = digit * 10 + c - '0'来累积数字值 - 例如"123"会被解析为1→12→123
-
字母处理:
- 直接添加到当前字符串构建器sb中
- 这部分字符属于当前最内层的编码字符串
-
遇到'[':
- 将当前累积的数字压入数字栈
- 将当前构建的字符串压入字符串栈
- 重置数字和当前字符串构建器
- 这相当于进入一个新的嵌套层级
-
遇到']':
- 从数字栈弹出重复次数k
- 从字符串栈弹出之前保存的字符串
- 将当前字符串重复k次并附加到弹出的字符串后
- 更新当前字符串为这个新构建的字符串
- 这相当于退出当前嵌套层级
3. 算法复杂度分析
3.1 时间复杂度
- 我们需要遍历整个输入字符串一次,时间复杂度为O(n)
- 对于每个']',我们需要进行字符串拼接操作,最坏情况下可能需要O(m)时间,其中m是输出字符串的长度
- 因此总体时间复杂度为O(n + m)
3.2 空间复杂度
- 使用了两个栈结构,最坏情况下可能需要存储O(n)个元素
- 字符串构建器在最坏情况下可能需要O(m)空间
- 因此空间复杂度为O(n + m)
4. 边界条件与测试用例
4.1 常见边界情况
-
没有嵌套的最简单情况:
输入:"3[a]"
输出:"aaa" -
多层嵌套情况:
输入:"3[a2[c]]"
输出:"accaccacc" -
混合嵌套和非嵌套:
输入:"2[abc]3[cd]ef"
输出:"abcabccdcdcdef" -
开头和结尾有普通字符:
输入:"abc3[cd]xyz"
输出:"abccdcdcdxyz"
4.2 特殊测试用例
-
最大重复次数:
输入:"300[a]"
输出:300个a组成的字符串 -
单字符重复:
输入:"1[a]"
输出:"a" -
空字符串:
输入:""(根据题目描述不会出现)
输出:""
5. 算法优化与变种
5.1 递归解法
除了使用栈的迭代解法,我们还可以考虑递归方法:
java复制class Solution {
private int index = 0;
public String decodeString(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 decoded = decodeString(s);
index++; // 跳过']'
while (k-- > 0) {
result.append(decoded);
}
}
}
return result.toString();
}
}
递归解法更加简洁,但需要注意递归深度和字符串拼接的性能问题。
5.2 性能优化建议
- 对于Java实现,StringBuilder的使用已经比较高效,但可以预分配更大的初始容量以减少扩容开销
- 在拼接重复字符串时,可以考虑使用String.repeat()方法(Java 11+)
- 对于特别长的字符串,可以考虑分块处理
6. 常见错误与调试技巧
6.1 常见错误类型
-
数字处理错误:
- 忘记处理多位数情况
- 数字和字符混合的情况(题目已保证不会出现)
-
栈操作错误:
- 压栈和出栈的顺序不对应
- 忘记重置数字或字符串构建器
-
边界条件处理:
- 没有处理开头或结尾的普通字符
- 对于嵌套层级较深的情况处理不当
6.2 调试技巧
-
打印中间状态:
- 在每次压栈和出栈时打印栈内容
- 跟踪当前数字和字符串构建器的值
-
使用可视化工具:
- 画出栈的变化过程
- 标记当前处理位置
-
分步验证:
- 先处理简单情况
- 逐步增加复杂度
7. 实际应用场景
字符串解码算法在实际开发中有多种应用:
- 配置文件解析:处理带有重复模式的配置项
- 数据压缩:解压经过简单编码的数据
- 模板引擎:处理带有循环结构的模板
- 协议解析:解码特定格式的网络协议数据
理解这种嵌套结构的处理方法,对于处理各种层次化数据格式(如JSON、XML)也有帮助。
8. 扩展思考
- 如果允许数字和字母混合(如"3a2[b]")该如何处理?
- 如果重复次数k可以是0或负数该怎么处理?
- 如果方括号可以不成对出现该怎么处理?
- 如何编码这个算法(即实现字符串编码功能)?
这些扩展问题可以帮助我们更深入地理解字符串处理的各种边界情况和算法设计思路。
在实际面试中,除了写出正确的代码,还需要能够清晰地解释算法思路、分析复杂度,并讨论可能的优化方向。这道题很好地考察了候选人对数据结构的理解和对边界条件的处理能力。