思路分析
- 核心数据结构:使用两个栈,分别存储「重复次数 k」和「待拼接的字符串」;
- 遍历逻辑:
- 遇到数字:拼接完整的数字(处理多位数,如 100 [abc]);
- 遇到左括号[:将当前数字和当前字符串分别入栈,然后重置数字和字符串;
- 遇到右括号]:弹出栈顶的数字和字符串,将当前字符串重复对应次数后拼接到弹出的字符串后;
- 遇到普通字符:直接拼接到当前字符串。
代码实现
java
public String decodeString(String s) {
// 存储重复次数的栈
Stack<Integer> countStack = new Stack<>();
// 存储待拼接字符串的栈
Stack<String> strStack = new Stack<>();
// 当前拼接的字符串
StringBuilder currentStr = new StringBuilder();
// 当前累积的数字(处理多位数)
int currentNum = 0;
// 遍历每个字符
for (char c : s.toCharArray()) {
if (Character.isDigit(c)) {
// 处理多位数,如"123[abc]"中的123
currentNum = currentNum * 10 + (c - '0');
} else if (c == '[') {
// 左括号:将当前数字和字符串入栈,重置
countStack.push(currentNum);
strStack.push(currentStr.toString());
currentNum = 0;
currentStr.setLength(0); // 清空当前字符串
} else if (c == ']') {
// 右括号:弹出数字和字符串,拼接重复后的结果
int repeatCount = countStack.pop();
String prevStr = strStack.pop();
// 重复当前字符串repeatCount次
StringBuilder repeatedStr = new StringBuilder(prevStr);
for (int i = 0; i < repeatCount; i++) {
repeatedStr.append(currentStr);
}
// 更新当前字符串为拼接后的结果
currentStr = repeatedStr;
} else {
// 普通字符:直接拼接
currentStr.append(c);
}
}
return currentStr.toString();
}
复杂度分析
- 时间复杂度 O (n)(每个字符仅遍历一次,重复拼接的总次数等于最终字符串长度)
- 空间复杂度 O (n)(栈的深度不超过嵌套层数)。
具体分析
一、先明确问题本质
编码规则:k[encoded_string] → 括号内的字符串重复k次,且支持嵌套(如3[a2[bc]])。
核心需求:把嵌套的、带重复次数的编码字符串,还原成普通字符串。
举个例子:
- 输入
3[a2[bc]]→ 先解内层2[bc]得到bcbc→ 再解外层3[abcbc]得到abcbcabcbcabcbc。
二、核心难点分析
- 嵌套结构处理 :括号是嵌套的,必须先解内层、再解外层(如先处理
2[bc],再处理外层的3[...]); - 多位数处理 :重复次数
k可能是多位数(如100[abc]),不能把1、0、0拆成单个数字处理; - 字符串拼接效率 :如果用普通
String拼接,频繁创建对象会导致性能低下(尤其输出长度接近10^5时)。
三、解法选择:为什么用「栈」?
栈的核心特性是后进先出(LIFO),完美匹配嵌套结构的"先处理内层、再处理外层"需求:
- 遇到左括号
[:相当于"进入内层",需要保存当前的"重复次数"和"已拼接的字符串"(前缀); - 遇到右括号
]:相当于"退出内层",需要取出之前保存的"重复次数"和"前缀",把内层字符串重复后拼接到前缀后。
可以把栈理解为"临时保存现场"的工具:进入内层前,把外层的状态(次数+前缀)存起来;退出内层后,取出外层状态,合并内层结果。
四、分步拆解核心思路(结合示例3[a2[bc]])
我们用两个栈配合:
countStack:存储"待使用的重复次数"(如3、2);strStack:存储"进入内层前的前缀字符串"(如空字符串、"a");
再用两个变量记录"当前状态":currentNum:累积当前的数字(处理多位数);currentStr:拼接当前层的普通字符/内层解码结果。
步骤1:初始化
countStack = [] # 空栈,存重复次数
strStack = [] # 空栈,存前缀字符串
currentNum = 0 # 初始数字为0
currentStr = "" # 初始字符串为空
步骤2:遍历每个字符(以3[a2[bc]]为例)
逐个处理字符:3 → [ → a → 2 → [ → b → c → ] → ]
字符1:3(数字)
- 逻辑:累积多位数(不能直接入栈,因为可能是多位数);
- 计算:
currentNum = 0 * 10 + (3 - '0') = 3; - 状态:
currentNum=3,其余不变。
字符2:[(左括号)
- 逻辑:进入内层,保存"外层状态"到栈中,然后重置当前状态;
- 操作:
countStack.push(3)→ 把外层重复次数3存入栈;strStack.push("")→ 外层前缀是空字符串,存入栈;currentNum = 0→ 重置数字,准备处理内层的数字;currentStr = ""→ 重置字符串,准备拼接内层字符;
- 状态:
countStack=[3],strStack=[""],currentNum=0,currentStr=""。
字符3:a(普通字符)
- 逻辑:普通字符直接拼接到当前字符串;
- 操作:
currentStr += "a"→currentStr="a"; - 状态:
currentStr="a",其余不变。
字符4:2(数字)
- 逻辑:累积多位数;
- 计算:
currentNum = 0 * 10 + (2 - '0') = 2; - 状态:
currentNum=2,其余不变。
字符5:[(左括号)
- 逻辑:进入更深层,保存当前层状态到栈,重置当前状态;
- 操作:
countStack.push(2)→ 把内层重复次数2存入栈;strStack.push("a")→ 当前层前缀是"a",存入栈;currentNum = 0→ 重置数字;currentStr = ""→ 重置字符串;
- 状态:
countStack=[3,2],strStack=["", "a"],currentNum=0,currentStr=""。
字符6:b(普通字符)
- 逻辑:拼接到当前字符串;
- 操作:
currentStr += "b"→currentStr="b"; - 状态:
currentStr="b"。
字符7:c(普通字符)
- 逻辑:拼接到当前字符串;
- 操作:
currentStr += "c"→currentStr="bc"; - 状态:
currentStr="bc"。
字符8:](右括号)
- 逻辑:退出内层,取出栈中保存的状态,拼接重复后的字符串;
- 操作:
repeatCount = countStack.pop()→ 弹出内层次数2;prevStr = strStack.pop()→ 弹出内层前缀"a";- 把
currentStr("bc")重复2次,拼接到prevStr后 →prevStr + "bc"*2 = "a" + "bcbc" = "abcbc"; currentStr = "abcbc"→ 更新当前字符串为拼接结果;
- 状态:
countStack=[3],strStack=[""],currentNum=0,currentStr="abcbc"。
字符9:](右括号)
- 逻辑:退出外层,取出栈中保存的外层状态,拼接重复后的字符串;
- 操作:
repeatCount = countStack.pop()→ 弹出外层次数3;prevStr = strStack.pop()→ 弹出外层前缀"";- 把
currentStr("abcbc")重复3次,拼接到prevStr后 → "" + "abcbc"*3 = "abcbcabcbcabcbc"; currentStr = "abcbcabcbcabcbc";
- 状态:
countStack=[],strStack=[],currentNum=0,currentStr="abcbcabcbcabcbc"。
步骤3:遍历结束,返回结果
最终currentStr就是解码后的字符串:abcbcabcbcabcbc。
五、关键细节补充
1. 为什么用StringBuilder而不是String?
String是不可变对象,每次拼接(+=)都会创建新对象,比如重复1000次会创建1000个对象,性能极低;StringBuilder是可变的,所有拼接操作都在同一个对象上完成,时间复杂度从O(n²)降到O(n),满足输出长度≤10^5的要求。
2. 多位数处理的逻辑为什么是currentNum * 10 + (c - '0')?
- 字符
'0'-'9'的ASCII码是连续的,c - '0'可以把字符转成对应的数字(如'3' - '0' = 3); - 累积逻辑:比如处理
123时,先算0*10+1=1,再算1*10+2=12,最后算12*10+3=123,完美还原多位数。
3. 栈的"保存现场"逻辑是核心
| 操作时机 | 保存的内容 | 目的 |
|---|---|---|
左括号[ |
currentNum(次数)+ currentStr(前缀) |
进入内层前,把外层的状态存起来,避免丢失 |
右括号] |
弹出次数+前缀,拼接currentStr*次数 |
退出内层后,把内层结果合并到外层 |
六、思路总结(核心要点)
- 结构匹配:栈的"后进先出"完美适配括号的嵌套结构,先解内层、再解外层;
- 状态管理:用两个栈保存"外层状态",用两个变量记录"当前层状态",分离不同层级的上下文;
- 细节优化 :
- 多位数累积:
currentNum * 10 + (c - '0'); - 高效拼接:
StringBuilder替代String;
- 多位数累积:
- 遍历逻辑 :
- 数字→累积;左括号→存状态+重置;右括号→取状态+拼接;普通字符→直接拼。
这个思路的本质是"分层处理":把嵌套的编码字符串拆成多个层级,用栈隔离不同层级的状态,逐个层级解码后合并,最终得到完整结果。