【LeetCode 热题 100】394. 字符串解码
Problem: 394. 字符串解码
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。
文章目录
- 整体思路
- 完整代码
- 时空复杂度
- 时间复杂度:O(N + M)
- 空间复杂度:O(N) 或 O(M)
整体思路
这段代码旨在解决一个涉及嵌套结构的 字符串解码 (Decode String) 问题。编码规则是 k[encoded_string]
,表示 encoded_string
这部分内容重复 k
次。由于括号可以嵌套,例如 3[a2[c]]
,这个问题具有天然的递归或栈式结构。
该算法巧妙地采用了 双栈(Two Stacks) 的迭代方法来处理这种嵌套关系,避免了显式递归。
-
数据结构选择:
strStack
(字符串栈): 用于保存遇到[
之前的字符串部分。当解码一个内部括号时,栈顶的字符串就是其解码结果需要拼接的前缀。numStack
(数字栈): 用于保存与[
对应的重复次数k
。StringBuilder curr
: 用于高效地构建当前正在处理的、位于同一嵌套层级的字符串。int k
: 一个临时变量,用于解析可能由多位数字组成的重复次数。
-
核心遍历与状态管理逻辑:
- 算法通过单次遍历输入字符串
s
的每个字符来驱动。根据字符的类型,执行不同的状态转换:- 遇到数字 (
'0'-'9'
): 将其累加到k
变量中。k = k * 10 + c - '0'
这个技巧可以正确地解析多位数(如 “10”, “123”)。 - 遇到左括号 (
'['
): 这标志着进入了一个新的、更深的嵌套层级。此时,必须“保存”当前层级的状态,以便稍后恢复。- 将当前的重复次数
k
压入numStack
。 - 将当前已经构建好的字符串
curr
压入strStack
。 - 重置
k
和curr
,为新的嵌套层级做准备。
- 将当前的重复次数
- 遇到右括号 (
']'
): 这标志着一个嵌套层级的结束。此时,需要“恢复”上一层级的状态并执行解码。- 从
numStack
弹出重复次数repeat
。 - 从
strStack
弹出上一层级的字符串前缀prev
。 - 将当前
curr
所代表的字符串重复repeat
次。 - 将重复后的字符串与前缀
prev
合并,形成新的curr
。这就完成了从内层到外层的解码与合并。
- 从
- 遇到字母 (else): 这是一个普通字符,直接追加到当前层级的字符串构建器
curr
的末尾。
- 遇到数字 (
- 算法通过单次遍历输入字符串
-
返回结果:
- 当遍历完整个输入字符串后,
curr
中就包含了完全解码后的最终字符串,将其返回即可。
- 当遍历完整个输入字符串后,
这个双栈方法优雅地模拟了递归调用过程:[
相当于递归深入,]
相当于递归返回。
完整代码
class Solution {/*** 解码一个按特定规则编码的字符串。* @param s 编码后的字符串,例如 "3[a2[c]]"* @return 解码后的字符串,例如 "accaccacc"*/public String decodeString(String s) {// strStack: 用于保存遇到'['之前的字符串部分,作为解码时的前缀。Deque<String> strStack = new ArrayDeque<>();// numStack: 用于保存'['前的重复次数 k。Deque<Integer> numStack = new ArrayDeque<>();// k: 临时变量,用于解析可能的多位数字。int k = 0;// curr: 一个 StringBuilder,用于高效地构建当前嵌套层级的字符串。StringBuilder curr = new StringBuilder();// 遍历输入字符串的每一个字符for (char c : s.toCharArray()) {if (c >= '0' && c <= '9') {// 如果是数字,更新 k 的值。k * 10 的技巧可以处理多位数。k = k * 10 + c - '0';} else if (c == '[') {// 如果是左括号,表示进入新的嵌套层级。// 1. 将当前的重复次数 k 压入数字栈numStack.push(k);// 2. 将当前已构建的字符串 curr 压入字符串栈strStack.push(curr.toString());// 3. 重置 k 和 curr,为新层级做准备k = 0;curr = new StringBuilder();} else if (c == ']') {// 如果是右括号,表示一个嵌套层级结束,需要解码。// 1. 弹出该层级对应的重复次数int repeat = numStack.pop();// 2. 弹出上一层的字符串前缀String prev = strStack.pop();// 3. 将当前层级的字符串(curr)重复指定次数String repeated = curr.toString().repeat(repeat);// 4. 将重复后的字符串与前缀合并,更新为新的当前层字符串curr = new StringBuilder(prev + repeated);} else {// 如果是普通字母,直接追加到当前层级的字符串中curr.append(c);}}// 循环结束后,curr 中即为最终完全解码的字符串return curr.toString();}
}
时空复杂度
时间复杂度:O(N + M)
- N 的部分:算法的主体是一个
for
循环,它遍历输入字符串s
一次。设s
的长度为N
。这个扫描过程本身是 O(N) 的。 - M 的部分:
- 在循环内部,最耗时的操作是
curr.toString().repeat(repeat)
和随后的字符串拼接。 - 这些操作的总成本不取决于
N
,而是取决于最终解码后字符串的长度,我们称之为M
。 - 每一个最终生成在结果字符串中的字符,都是通过
append
或repeat
操作产生的。所有这些生成操作的总和与最终字符串的长度M
成正比。
- 在循环内部,最耗时的操作是
- 综合分析:
- 总时间复杂度是扫描输入字符串的时间加上生成输出字符串的时间。
- 因此,最终的时间复杂度为 O(N + M),其中
N
是输入字符串的长度,M
是输出字符串的长度。
空间复杂度:O(N) 或 O(M)
- 主要存储开销:空间主要由两个栈
strStack
和numStack
以及StringBuilder curr
占用。 - 空间大小:
numStack
的大小与嵌套深度成正比,最多为 O(N)。strStack
存储的是中间的字符串片段。在最坏的情况下,例如2[a2[b2[c...]]]
,栈中存储的字符串总长度可能与最终输出字符串的长度M
相关。- 然而,在更典型的情况下,例如
a[b[c...]]
,栈中存储的字符串总长度(“a”, “ab”, “abc” …)可以被输入长度N
所限制。 - 考虑到各种情况,一个比较合理的上界是 O(N + M),但通常在分析中会简化为 O(N) 或 O(M),这取决于哪一个在特定用例中是主导因素。在大多数面试场景下,将其分析为 O(N) 是被接受的,因为它与输入的规模和结构直接相关。
综合分析:
算法的辅助空间复杂度主要由栈的内容决定,其大小与输入的嵌套深度和字符串片段长度有关。一个合理的估计是 O(N)。