当前:LC394 · 字符串解码 · 首次出现于 Day 13 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC394 · Decode String · 栈

字符串解码:嵌套展开,栈保存现场

数字管后面那对括号。读到 [ 把外层现场压起来,读到 ] 再拿出来乘。

扫描时维护 cur(当前层已拼好的串)和 num(当前正在读的数字)。遇到数字就 num=num×10+d;遇到 [ 把 (num, cur) 压栈并清空二者,开始记内层;遇到字母拼进 cur;遇到 ] 弹出 (times, prev),令 cur = prev + times×cur。也可以按同一规则递归:读到 [ 就递归解码括号内,返回后乘上前面的数字。时间与输出长度同阶。

时间 O(结果长度)空间 O(结果长度)结论先行 · 全文约 6 节
导读

这是 LeetCode 394. Decode String。编码规则是 k[encoded_string],表示把括号里的串重复 k 次。k 是正整数,可以有多位。括号保证配对,不会出现无数字的括号。把编码串还原成展开后的字符串。

主例 "3[a2[c]]"。最里面 2[c] 先变成 cc,再和前面的 a 拼成 acc,最外面再重复 3 次,得到 accaccacc。对照 "2[abc]3[cd]ef" → abcabc cdcdcd ef,两段并列,没有嵌套。

从左往右乘会乘错:还没看见内层就无法知道 3 要重复什么。缺的是「进入内层之前,外层已经拼到哪、这一层要重复几次」。本文按字符走完主例,看这两样东西怎样进栈、怎样在 ] 处拼回来。

先内后外,数字管的是后面那对括号

"3[a2[c]]" 不是「3 个 a,再 2 个 c」。3 管的是整段 a2[c],2 管的是 c。必须先把最内层变成具体字母,再往外乘。

左括号越晚出现越先闭合,这是栈。递归也是同一件事:读到 [ 就进入下一层调用,返回内层解码结果,再乘上这一层的数字。栈把递归的「返回地址」显式存成 prev 串和 times。

还要处理多位数字。12[a] 的 1 和 2 必须先拼成 12,不能读到 1 就开乘。所以数字不是读一个用一个,而是用 num = num×10 + d 攒着,直到看见 [ 才和当前串一起压栈。

现场是两样东西进内层之前必须记住:外层已经拼好的字母,以及内层结束之后要重复几次。少存任何一样,] 处都拼不回去。

读到 [ ,把外层的串和次数压起来

扫描到 [ 时,num 正好是这一层的重复次数,cur 正好是「写在这个括号前面、已经属于外层」的字母。把 (num, cur) 压栈,然后把两个变量清零,后面读到的字母只属于内层。

用手走主例。读 3,num=3。读第一个 [:压入 (3, ""),cur 和 num 清空——外层在括号前还没有字母。读 a,cur="a"。读 2,num=2。演示停在第二个 [ 前:栈里已有外层的 3 和空串,当前 cur 是 a、num 是 2。读这个 [:再压入 (2, "a"),cur 再次清空,准备读最内层。

遇到 [ 压栈存档
3[a2[c]]
栈(自底向上)
3×[]
cur = a
num = 2
压入 3、"",进入内层

读到 ] ,弹出来乘,接回外层

扫描到 ] 时,cur 是刚刚结束的这一层。弹出 (times, prev),做 cur = prev + strings.Repeat(cur, times):内层重复 times 次,接到外层已经有的字母后面。

主例接着走。读 c,cur="c"。读第一个 ]:弹出 (2, "a"),cur = "a" + 2×"c" = "acc"。外层的 (3, "") 还在栈里。读最后一个 ]:弹出 (3, ""),cur = "" + 3×"acc" = "accaccacc"。演示两帧就是这两次组合。扫描结束栈空,cur 就是答案。

并列的 "2[ab]3[c]" 同理:第一段展开成 abab 之后栈空,cur 留着;3 和 "abab" 在下一个 [ 被压下去,第二段展开成 ccc,接在 abab 后面。不需要在段与段之间特判。

遇到 ] 弹栈组合
3[a2[c]]
栈(自底向上)
cur = acc
num = 3
弹出 2、"a" → "a" + 2×"c" = "acc"

Go:栈解码

solution.goGo
func decodeString(s string) string {
stack := []int{}
strs := []string{}
cur, num := "", 0
for _, ch := range s {
switch {
case '0' <= ch && ch <= '9':
num = num*10 + int(ch-'0')
case ch == '[':
stack = append(stack, num)
strs = append(strs, cur)
cur, num = "", 0
case ch == ']':
times := stack[len(stack)-1]
stack = stack[:len(stack)-1]
prev := strs[len(strs)-1]
strs = strs[:len(strs)-1]
cur = prev + strings.Repeat(cur, times)
default:
cur += string(ch)
}
}
return cur
}

1次数和字符串分两个栈,成对压、成对弹,等价于栈里放一对 (times, prev)。

2数字按十进制累加。12[a] 会先把 num 攒成 12,再在 [ 处压栈。

3[ 存档并清空。主例两层 [ 分别压下 (3,"") 和 (2,"a")。

4] 弹出后做 prev + 重复。主例先得到 acc,再得到 accaccacc。

5普通字母只属于当前层,直接拼到 cur。

6题目保证括号配对,扫完栈空,cur 就是整段解码。

总结

[ 存档、] 还原。主例内层 2[c] 先变成 acc,再乘 3 得到 accaccacc。

  • 数字管的是后面那对括号,不是它前面的字母。3[a2[c]] 不是 3 个 a。
  • 进内层必须同时压「外层已有的串」和「这一层的次数」,少一样就拼不回去。
  • 多位数字先累加再压栈。时间按展开后的长度算,不是按编码串长度。
同族题目
LC20有效的括号(括号配对)LC150逆波兰表达式求值LC856括号的分数