这里写目录标题
- [LeetCode 394. 字符串解码|我是怎么从嵌套想到递归和栈的?](#LeetCode 394. 字符串解码|我是怎么从嵌套想到递归和栈的?)
-
- 一、真正的问题:怎么保证先处理里面,再处理外面?
- 二、递归函数应该怎么设计?
-
- [1. 需要一个 `i`](#1. 需要一个
i) - [2. 这一层到底要算出什么?](#2. 这一层到底要算出什么?)
- [3. 那从左往右扫描时,遇到不同字符怎么办?](#3. 那从左往右扫描时,遇到不同字符怎么办?)
- [4. 递归结束后要怎么继续解析下去](#4. 递归结束后要怎么继续解析下去)
- [1. 需要一个 `i`](#1. 需要一个
- 三、现在完整走一次递归:`3a2\[c]2b`
- 四、完整递归代码
-
- [多位数字为什么是 `number * 10 + 当前数字`?](#多位数字为什么是
number * 10 + 当前数字?) - [最外层为什么还有一个 `return result, i`?](#最外层为什么还有一个
return result, i?)
- [多位数字为什么是 `number * 10 + 当前数字`?](#多位数字为什么是
- 五、方法二:栈
-
- [遇到 `]` 以后,到底要做什么?](#遇到
]以后,到底要做什么?) - [`i` 和 `pop()` 到底谁在动?](#
i和pop()到底谁在动?) - 到这里,整个栈思路其实已经出来了
- [遇到 `]` 以后,到底要做什么?](#遇到
- 六、最后复盘:
LeetCode 394. 字符串解码|我是怎么从嵌套想到递归和栈的?
题目链接:394. 字符串解码
这道题刚开始看的时候,我其实没觉得特别难。
例如:3[a]
意思很直接:a 重复 3 次 → aaa
所以我最开始想的就是:
找到数字,知道重复几次;
再找到括号里的字符串,把它重复对应次数。
真正让我开始不知道怎么处理的是:3[a2[c]]
因为这里突然出现了嵌套。
一、真正的问题:怎么保证先处理里面,再处理外面?
例如:3[a2[c]]
最外面的 3[...] 暂时根本算不了。
因为它里面的:a2[c]
自己都还没有处理完成。
必须先:
text
2[c]
↓
cc
a2[c]
↓
acc
3[acc]
↓
accaccacc
所以:
程序怎么知道要先进去解决最里面那一层?
而且如果变成:3[a2[b4[c]]]
还得继续一层一层往里面走。
为什么这时候会想到递归?
后来我注意到一件事。
最外面的:3[...]
本质上是在做:
把括号里的内容解码出来,再重复 3 次。
而里面的:2[c]
做的还是:
把括号里的内容解码出来,再重复 2 次。
也就是说,里面和外面其实是在解决同一种问题。
text
外层
→ 解码一个 数字[...]
内层
→ 还是解码一个 数字[...]
这时候才会自然想到:
既然里面又出现了一个和外面一样的问题,那能不能让同一个函数再调用自己一次?
这就是递归。
二、递归函数应该怎么设计?
现在已经确定了一件事:
遇到嵌套时,可以让同一个函数继续进去处理下一层。
但"用递归"还不等于会写。
接下来真正要想的是:
一次递归到底要处理什么?
还是看:3[a2[c]]
最外层遇到第一个 [ 时,我们需要先把里面的:a2[c]
处理出来。
所以一次递归的任务其实可以先定成:
从当前这一层的第一个字符开始,一直向右解析,直到这一层结束。
这时候就自然碰到了两个问题:
我怎么告诉这个函数"你从哪里开始"?
它又怎么知道"处理到哪里该停"?
1. 需要一个 i
我们要处理的无论是数字、字母还是括号,本质上都是字符串里的字符。
要知道当前正在看哪个字符,最直接的办法就是记录它在字符串里的下标。
所以递归调用真正需要从上一层得到的信息,其实只有:
这一层从字符串的哪个位置开始。
于是才有:
python
dfs(i)
这里的 i 可以先简单理解成:
当前处理到的位置。
例如:
text
3 [ a 2 [ c ] ]
↑ ↑
| 新的一层从这里开始
当前遇到的 [
当外层走到 [ 时,[ 本身只是边界。
真正属于下一层的第一个字符是它后面的 a。
所以进入下一层时:
python
dfs(i + 1)
也就是:
从左括号后面的第一个字符开始处理。
那这一层什么时候结束?
还是看:a2[c]
这一层不能继续把外面的内容也一起处理。
所以:
当它遇到属于自己这一层的
]时,这一层就结束。
于是 i 同时帮我们确定了这一层的范围:
text
进入一层
↓
从 [ 后面的第一个字符开始
i 不断向右移动
↓
遇到当前层自己的 ]
↓
这一层结束
最外层稍微特殊一点。
它不是某个括号内部,所以:
text
从 i = 0 开始
↓
直到整个字符串结束
2. 这一层到底要算出什么?
边界确定以后,再看这一层内部到底在做什么。
例如当前层是:a2[c]
我们从左往右看。
首先遇到:a
这个字符肯定不能丢,因为最终结果里还需要它。
所以这一层需要先保存:
目前已经解析好的字符串。
用
python
result = ""
看到 a:
text
result = "a"
继续往右,又看到:2
这个 2 自己不是最终答案的一部分。
它的作用是:
告诉我们后面的括号内容要重复几次。
所以它更像这一层解析过程中暂时保存的一个状态。
于是还需要:
python
number = 0
所以一层递归里真正要维护的东西是这三个:
text
i → 我现在读到哪里
result → 这一层目前已经解码出来的内容
number → 接下来括号内容要重复多少次
这里也可以先区分一下它们的角色:
result是这一层最后真正要交给上一层的答案。- 而
number只是这一层构造result时暂时使用的值。
3. 那从左往右扫描时,遇到不同字符怎么办?
当前字符只有四种情况:
text
数字 → 继续组成 number
字母 → 加进当前 result
"["
→ 说明出现下一层
→ 暂停当前层
→ 调用 dfs() 进去处理
"]"
→ 当前这一层结束
→ 把结果返回上一层
所以递归的运行方式其实就是:
text
当前层一直往右读
↓
普通内容自己处理
碰到 [
↓
进去解决下一层
下一层碰到 ]
↓
把已经解码好的字符串交回来
当前层接着往右
例如:3[a2[c]]
第二层处理到:
text
result = "a"
number = 2
然后碰到 [。
这时候第二层暂停,让第三层去处理:c
第三层最后得到:
text
"c"
返回以后,第二层再用自己之前保存的:
text
number = 2
得到:
text
"a" + "c" * 2
↓
"acc"
这里也就能看出:
哪一层保存了这个 number,就由哪一层负责使用它。
最里面那层只负责把自己的内容解出来。
外面的重复次数属于外层,不需要传进去。
4. 递归结束后要怎么继续解析下去
例如第三层已经把:c
解出来了。
它当然要把:
text
"c"
交给第二层。
但如果只返回 "c",第二层还不知道:
刚才这一层到底走到原字符串哪个地方了?
我应该从哪里接着处理?
所以递归结束时需要同时告诉上一层:
text
我这一层解码出来了什么
+
我停在了哪个位置
因此才有:
python
return result, i
也就是返回:
python
(result, i)
上一层再用:
python
sub, i = dfs(i + 1)
分别接住:
text
sub → 下层已经解码好的字符串
i → 下层停下来的右括号位置
这样上一层才能跨过这个已经处理完的 ],继续往后扫描。
所以 i 在这里其实有两个连续的作用:
text
进入递归时 → 告诉这一层从哪里开始
递归结束时 → 告诉上一层我处理到哪里结束
它就像一直在原字符串上往前传递的"当前位置"。
三、现在完整走一次递归:3[a2[c]]2[b]
到这里再跑完整例子,会比一开始直接看代码清楚很多。
这个例子后面的:2[b]
正好可以用于说明:
子递归结束,不代表当前这一层也结束。
先标下标:
text
下标:0 1 2 3 4 5 6 7 8 9 10 11
字符:3 [ a 2 [ c ] ] 2 [ b ]
最外层从下标 0 开始。
第一层
首先看到:3
所以:
text
number = 3
result = ""
然后看到:[
说明出现下一层。
这一层暂停,让下一层从 a 开始处理。
第二层
看到:a
所以:
text
result = "a"
继续看到:2
所以:
text
number = 2
接着再次碰到:[
说明还有下一层。
第二层暂停。
第三层
这一层从:c
开始。
看到字母,所以:
text
result = "c"
接着碰到:]
这说明第三层结束了。
于是第三层返回:
text
解析结果:"c"
当前位置:这个 ] 的下标
回到第二层
第二层之前一直保存着:
text
result = "a"
number = 2
现在下一层返回:
text
sub = "c"
所以:
text
"a" + "c" * 2
↓
"acc"
这里有一个很重要的理解:
哪一层保存了这个
number,就由哪一层负责拿下一层返回的sub去乘。
第三层只负责得到:"c"
它根本不知道外面还有一个:2
那个 2 属于第二层。
number 用完以后要怎么处理?
因为:2[c] 已经处理完成。
这个 2 已经被消费掉了。
但当前第二层不一定马上结束。
例如如果这一层是:a2[c]3[d]
后面还有:3[d]
如果处理完 2[c] 后,还保留:
text
number = 2
那么后面看到:3
以后:
python
number = number * 10 + 3
就会变成:23
于是原本的:3[d]
就错误地变成:23[d]
所以一个:数字[...]
处理完成以后:
python
number = 0
表示:
这次重复次数已经使用完了,当前层后面如果再看到数字,要重新开始记录。
第二层什么时候结束?
刚才第三层返回时,位置停在最里面那个:]
第二层处理完:"c" * 2 以后,跨过这个已经处理好的右括号,继续往右。
结果马上又看到一个:]
这一次是第二层自己的右括号。
所以:
第二层结束。
它把:"acc"
和当前的位置返回给第一层。
回到第一层
第一层之前保存着:
text
number = 3
result = ""
现在拿到:
text
sub = "acc"
于是:
text
"acc" * 3
↓
"accaccacc"
但是第一层没有结束。
它只是刚刚暂停去解决了一个子问题。
回来以后,字符串后面还有:2[b]
这个例子完整的是这样的:
3[a2[c]]2[b]
所以继续向右扫描。
看到:2
重新记录:
text
number = 2
看到:[
再次进入下一层。
下一层得到:"b"
回来以后:
text
"b" * 2
↓
"bb"
拼到原来的结果后面:
text
accaccaccbb
直到整个字符串结束。
四、完整递归代码
现在我们已经一步一步得到:
text
一次递归需要知道从哪里开始 → i
当前层要保存已经处理好的内容 → result
当前层要保存重复次数 → number
数字 → 更新 number
字母 → 加入 result
[ → 让下一层递归去处理
] → 当前层结束,把 result 和位置返回
所以代码才自然变成:
python
class Solution:
def decodeString(self, s: str) -> str:
n = len(s)
def dfs(i):
result = ""
number = 0
while i < n:
if s[i].isdigit():
number = number * 10 + int(s[i])
i += 1
elif s[i].isalpha():
result += s[i]
i += 1
elif s[i] == "[":
sub, i = dfs(i + 1)
result += sub * number
number = 0
i += 1
elif s[i] == "]":
return result, i
return result, i
answer, _ = dfs(0)
return answer
这里再补两个之前容易卡住的地方。
多位数字为什么是 number * 10 + 当前数字?
例如: 123[a]
我们是一个字符一个字符看到:1 → 2 → 3
所以:
text
看到 1
0 × 10 + 1
= 1
看到 2
1 × 10 + 2
= 12
看到 3
12 × 10 + 3
= 123
也就是把原来的数字整体向左挪一位,再把新数字放到个位。
最外层为什么还有一个 return result, i?
普通递归层会通过:] 结束。
但最外层外面没有右括号。
所以它最终会因为:
python
i == len(s)
退出 while。
这时候仍然需要:
python
return result, i
把最终结果返回。
五、方法二:栈
其实栈不是我写完递归以后才想到的。
我最开始看到嵌套时,就有过一个很直接的想法:
既然暂时不知道外层该怎么算,那能不能先把看到的东西都存起来?
等碰到一个完整的右括号
],再从后往前把最近这一层处理掉。
理解完递归以后再回来想这个办法,反而清楚了很多:
递归是在进入内层之前,把外层状态交给 Python 帮我保存;
那如果不用递归,我就自己找一个东西,把暂时还处理不了的内容保存起来。
这时候栈就很合适。
栈是后进先出。
也就是后放进去的东西,会最先被拿出来。
而嵌套括号正好也是:
text
外层先出现
↓
内层后出现
↓
内层却先结束
例如:3[a2[c]]
从左往右扫描,在第一个 ] 出现之前,可以先全部压进栈:
text
["3", "[", "a", "2", "[", "c"]
这时候有一个很重要的观察:
第一个出现的
],一定是在结束当前最里面那一层。
因为最里面的括号最后打开,却最先闭合,正好符合栈的后进先出。
所以真正的处理可以等到 ] 出现以后再开始。
遇到 ] 以后,到底要做什么?
还是看现在的栈:
text
["3", "[", "a", "2", "[", "c"]
现在已经遇到了第一个 ]。
首先要解决的是:
这一层括号里的字符串是什么?
那就从栈顶开始往回拿。
第一次:
text
pop → "c"
但 pop() 一次只能拿一个元素。
如果里面不是 c,而是 abc 呢?
这也是我当时卡住的地方:
下一个字符到底怎么继续 pop 出来?
后来发现不是 pop() 自己会连续执行,而是让 while 一轮一轮重复执行它。
只要栈顶还不是:[
就继续:
python
stack.pop()
所以如果括号里是 abc:
text
pop c
↓
mid = "c"
pop b
↓
mid = "bc"
pop a
↓
mid = "abc"
看到 [
↓
停止
因为是从右往左拿,所以新拿出来的内容要放在前面:
python
mid = current + mid
而不是:
python
mid += current
否则 abc 就会被拼成 cba。
现在字符串已经拿完,栈顶就是:[
这个左括号只是这一层的边界标记。
既然这一层的内容已经找到了,它的任务也完成了,所以直接:
python
stack.pop()
把 [ 删除。
接下来才轮到:
这一层到底要重复几次?
也就是找左括号前面的数字。
我一开始在这里一直纠结:
数字什么时候算结束?
是不是要一直拿到碰见字母?
后来换了个角度以后就简单了:
不用猜什么时候停,只要栈顶还是数字,就继续 pop。
例如:12[abc]
压栈时:1 和 2
是分开放进去的。
往回拿的时候会先得到:2
再得到:1
所以同样要反着拼:
python
num = current + num
过程就是:
text
pop "2"
↓
num = "2"
pop "1"
↓
num = "12"
只要:
text
栈空了
或者
栈顶不再是数字
数字就收集完成。
现在这一层需要的两个东西终于都有了。
例如:
text
mid = "c"
num = "2"
所以:
text
"c" * 2
↓
"cc"
但这里又出现了我一开始没想通的问题:
"cc"已经算出来了,接下来放哪里?
肯定不能直接放进最终答案。
因为它仍然属于外面的:3[a2[c]] 或者说 外层 a 后面的一部分。
所以最自然的办法就是:
把已经解码好的
"cc"再压回栈。
于是:
text
["3", "[", "a", "2", "[", "c"]
经过一次处理以后,变成:
text
["3", "[", "a", "cc"]
其实就相当于把:
text
3[a2[c]]
暂时化简成了:
text
3[acc]
然后继续扫描原字符串。
马上又遇到第二个 ]。
这一次完全不用发明新的规则,继续重复刚才的过程:
text
往回拿字符串
↓
得到 "acc"
删掉 [
往前拿数字
↓
得到 3
"acc" * 3
↓
"accaccacc"
重新压回栈
所以嵌套其实就是这样:
每出现一个
],就把最近的一层解码掉,再把解码结果当成普通内容放回去。
一层一层往外消。
i 和 pop() 到底谁在动?
写到这里的时候,我一直有一个很奇怪的困惑:
遇到
]以后,i已经停在那里了。那我要从右往左找前面的内容,到底是谁在往回走?
我甚至想过再创建一个:j
专门从右往左遍历。
后来才发现完全不需要。
这里其实有两条完全不同的线:
text
i → 负责在原字符串 s 里从左往右扫描
stack.pop() → 负责在已经保存的内容里从右往左拿
所以碰到 ] 时:
text
i 暂时停在这个 ]
接下来处理:
text
mid
num
的整个过程中,i 都不用动。
不断变化的是:
python
stack.pop()
等这一层:
text
字符串找完
↓
数字找完
↓
解码完成
↓
结果重新压回栈
以后,这个 ] 才算真正处理结束。
这时候再:
python
i += 1
继续扫描原字符串。
所以:
i只负责往前扫原字符串;pop()只负责从栈里往回取。
两件事不是同一个方向,也不需要再额外创建一个反向指针。
到这里,整个栈思路其实已经出来了
text
从左往右扫描
↓
不是 ] → 先压入栈
↓
遇到 ]→ 最近的一层已经完整
↓
往回 pop, 直到看到 [ → 得到 mid
↓
删除 [
↓
继续 pop 连续数字 → 得到 num
↓
mid * num
↓
把结果重新压回 stack
↓
当前 ] 处理完成 → i += 1 → 继续向右
于是代码才变成:
python
class Solution:
def decodeString(self, s: str) -> str:
stack = []
i = 0
while i < len(s):
if s[i] != "]":
stack.append(s[i])
i += 1
else:
mid = ""
while stack[-1] != "[":
current = stack.pop()
mid = current + mid
stack.pop()
num = ""
while stack and stack[-1].isdigit():
current = stack.pop()
num = current + num
stack.append(mid * int(num))
i += 1
return "".join(stack)
🚩 最后还有一个我自己写代码时踩到的小坑。
我一开始写的是:
python
return mid
但拿:2[a]3[b]
一跑就错了。
因为 mid 只代表最近一次处理的那一层内容,并不代表整个字符串最后的答案。
例如最后栈里可能是:
text
["aa", "bbb"]
所以真正要返回的是:
python
"".join(stack)
得到:
text
aabbb
六、最后复盘:
递归这条路:
text
3[a2[c]]
↓
出现嵌套
怎么保证先处理里面?
↓
发现:里面和外面其实是同一种问题
↓
想到递归
↓
一次递归到底负责什么?→ 当前这一层
↓
调用它时要告诉它什么?→ 从哪里开始
↓
所以需要位置参数
↓
这一层自己需要记什么?
→ 已处理字符串 result
→ 重复次数 number
↓
什么时候进入下一层? → 遇到 [
↓
什么时候结束?→ 遇到当前层的 ]
↓
结束以后上一层还要继续 → 所以返回 result 和当前位置
栈这条路则是:
text
如果不用递归呢?
↓
那就先把暂时处理不了的内容存起来
↓
遇到第一个 ]
↓
说明最里面一层完整了
↓
往回 pop 到 [
↓
得到这一层字符串
↓
再往前找连续数字
↓
完成这一层解码
↓
结果重新压回栈
↓
等待下一个 ]
↓
嵌套一层层被消掉
所以递归和显式栈看起来是两套完全不同的代码,但真正解决的是同一个问题:
当外层还没有处理完,却必须先去解决内层时,怎么把外层当前的状态保存下来,等内层解决以后再回来继续。