遇到 k[encodedString] 时,不能在读到 [ 后丢掉方括号外已经解码的内容。栈中需要为每个尚未闭合的 [ 保存两项信息:
扫描到 ] 时,当前层内容已经完整,将栈顶上下文弹出并合并:
多位重复次数要逐位累积,不能把 12[a] 当成 1[a] 或 2[a]。
给定一个经过编码的字符串 s,返回它解码后的字符串。编码规则为:
表示把方括号内的 encodedString 恰好重复 k 次。编码可以嵌套。
示例 1:
示例 2:
示例 3:
题目保证:
1 <= s.length <= 30。s 只包含小写英文字母、数字和方括号。3a 或 2[4] 这类输入。[1, 300],解码结果长度不超过 10^5。括号具有嵌套结构,最先遇到的 [ 最后闭合,最后遇到的 [ 最先闭合,处理顺序符合后进先出。
例如:
读取内层 2[c] 时,外层的重复次数 3 和已经得到的前缀 "a" 都暂时不能结算。必须先保存它们,等内层得到 "cc" 后,再恢复外层上下文。
这里使用的是“括号上下文栈”,不是单调栈。栈内元素之间没有大小关系。
扫描过程中维护:
current:当前括号层已经解码出的内容。repeat:最近连续读取、但还没有被 [ 消耗的重复次数。stack:从外到内保存所有尚未闭合括号的 [prefix, count]。处理每个字符前后都保持:
current 只属于当前最内层尚未闭合的括号;如果当前没有未闭合括号,它就是最外层结果。prefix 都是进入对应 [ 前已经完整解码的内容。count 都是对应括号内容最终需要重复的次数。[ 数量。重复次数可能有多位,因此使用:
例如依次读到 "1"、"2" 后,repeat 从 0 变成 1,再变成 12。
[[ 表示即将进入一层新的编码内容:
[current, repeat] 压栈。current 清空,用来收集新一层内容。repeat 清零,避免影响后续括号。字母属于当前层,直接追加到 current。
]当前层已经结束,从栈顶取回 [prefix, count]:
因为方括号正确嵌套,所以当前 ] 一定与最近压栈的 [ 配对。
以 s = "3[a2[c]]" 为例:
| 读入字符 | repeat | current | stack | 操作 |
|---|---|---|---|---|
3 | 3 | "" | [] | 累积重复次数 |
[ | 0 | "" | [["", 3]] | 保存外层上下文,进入新层 |
a | 0 | "a" | [["", 3]] | 追加字母 |
2 | 2 | "a" | [["", 3]] | 累积重复次数 |
[ | 0 | "" | [["", 3], ["a", 2]] | 保存 "a" 和次数 2 |
c | 0 | "c" | [["", 3], ["a", 2]] | 追加字母 |
] | 0 | "acc" | [["", 3]] | "a" + "c".repeat(2) |
] | 0 | "accaccacc" | [] | "" + "acc".repeat(3) |
扫描结束后,栈为空,current 就是完整答案。
思路参考:JoshCrozier/leetcode-javascript。本文重新整理了变量命名、字符判断、状态不变量和复杂度分析;原项目采用 MIT License。
使用 char >= '0' && char <= '9' 是为了明确只识别 ASCII 数字。原实现的 !isNaN(char) 在本题合法输入下也能运行,但它会把空白字符等可转换为数字的内容误判为数字,不适合作为通用的字符分类方式。
| 代码 | 含义 |
|---|---|
repeat = repeat * 10 + Number(char) | 累积多位重复次数 |
stack.push([current, repeat]) | 保存进入新括号前的外层上下文 |
current = '' | 开始收集新一层内容 |
repeat = 0 | 当前次数已经交给栈顶括号,重新计数 |
stack.pop() | 右括号与最近的左括号配对 |
prefix + current.repeat(count) | 解码当前层并接回外层前缀 |
可以按照扫描字符的四种情况验证不变量:
[ 使用的 repeat,当前层内容和栈不变。[ 时,将当前层的前缀与重复次数完整保存,再进入空的新层。] 时,当前层内容已经完整。由于括号正确嵌套,栈顶恰好是与它配对的外层上下文,合并后得到该外层目前已经解码的完整前缀。题目保证输入有效。扫描结束时所有括号都已闭合,栈为空;根据不变量,current 正好是整个字符串的解码结果,因此算法正确。
12[a] 必须得到 12 个 "a",数字要逐位累积。3[a2[c]] 必须先结算内层 2[c]。3[a]2[bc] 结算第二段时不能覆盖第一段结果。abc3[cd]xyz 中括号外的字符也要保留。[ 后必须令 current = '',否则内外层内容会混在一起。repeat = 0。shift(): 本题需要后进先出,应使用数组尾部的 push() 和 pop()。设:
n 为编码字符串长度。L 为最终解码结果长度。h 为最大括号嵌套深度。扫描输入和栈操作本身需要 O(n) 时间,但 JavaScript 字符串不可变,repeat() 与字符串拼接会创建新字符串。因此,更精确地说:
O(n + M),其中 M 是所有括号结算过程中实际生成、复制的字符串总长度。M 通常与输出长度 L 同阶;考虑大量重复次数为 1 的深层嵌套时,同一字符可能在多个层级被复制,保守上界为 O(hL)。O(n + hL),而不能只写成 O(n)。O(h + L):栈有至多 h 层,同时保存尚未合并的前缀和正在构造的解码内容。即使不把最终返回值计入额外空间,中间字符串仍可能占用 O(L) 空间。题目保证 L <= 10^5,否则即使编码字符串很短,解码结果也可能因重复而迅速膨胀。
递归函数负责解析当前位置到当前层 ] 之前的内容。遇到普通字母就追加,遇到数字就读取完整次数,再递归解析括号内部。
这种写法与编码文法很接近,但需要正确维护共享下标,且递归栈深度为 O(h)。题目中的输入长度很小,递归深度不会成为实际问题;迭代栈版本则更直接地展示了上下文如何保存和恢复。
也可以把字符全部压栈,遇到 ] 后向前弹出到 [,再继续弹出数字并展开。这种方法同样正确,但多位数字需要反向还原,且会频繁操作单个字符,通常不如直接保存 [prefix, count] 清晰。
进入新括号后,当前变量要改为收集内层内容。外层已经解码的前缀和控制当前括号的重复次数都暂时无法结算,必须一起保存,才能在遇到对应 ] 时恢复。
] 时一定应该弹出栈顶?方括号正确嵌套,最后打开的括号一定最先关闭。因此当前 ] 对应最近一次遇到的 [,也就是栈顶上下文。
repeat * 10 + digit?这是十进制数字的逐位构造规则。读入新数字相当于把已有数字左移一位十进制位,再加上当前位,因此能正确处理 12、300 等多位次数。
[ 后为什么必须同时清空current 和repeat?current 已作为外层前缀入栈,新层应从空字符串开始;repeat 已作为该括号的次数入栈,后续数字应重新计数。缺少任意一次重置都会污染内层状态。
算法只利用括号嵌套的后进先出关系,栈内元素并不维持递增或递减顺序,也不会根据大小弹栈,因此不属于单调栈问题。
O(n)?输入中的 100[a] 很短,输出却有 100 个字符。repeat() 和字符串拼接的成本与产生的字符串长度相关,所以至少要把输出规模和中间字符串复制计入分析。
需要拒绝孤立的 [ 或 ]、括号前缺少合法次数、数字后没有 [、括号不匹配、非法字符以及可能超过允许上限的展开长度。弹栈前也要检查栈是否为空。
递归下降更贴近嵌套文法,代码可以很自然;显式栈避免依赖调用栈,并把每层保存的状态展示得更清楚。两者的核心都是保存外层前缀、重复次数和当前解析位置。
12[a]。[ 时只保存次数,导致括号前缀丢失。current,造成内容重复或顺序错误。] 时只做 current.repeat(count),忘记接回 prefix。O(n),忽略输出长度和 JavaScript 字符串复制。value = value * 10 + digit 累积。先只回答第 1 题,再展开后续问题:
3[a2[c]] 的内层 [ 时,栈顶应该保存什么?[ 后没有把 repeat 清零,哪个输入会最先暴露问题?abc3[cd]xyz 可以检验前缀和后缀是否被正确保留?