LEETCODE 394Medium

字符串解码

括号天然嵌套,嵌套就用栈:遇 [ 把“外层进度”存档,遇 ] 取档拼接,当前层永远只处理自己的事。

问题拆解

3[a2[c]] 这样的编码串展开成 accaccacck[str] 表示方括号里的内容重复 k 次,且允许任意嵌套。

如果没有嵌套,一层循环就能解决:读到数字记倍数,读到 ] 把括号里的串重复拼接。麻烦在嵌套——展开外层之前必须先展开内层,也就是说 ] 的匹配顺序是“后遇到的 [ 先闭合”,这正是栈的后进先出。

关键是想清楚栈里存什么。遇到 [ 时,当前攒了一半的字符串和倍数都要暂停,等内层解码完再回来用,所以把这两样压栈存档:倍数进数字栈,半成品字符串进字符串栈,然后清空工作区从零开始攒内层。遇到 ] 时弹出两者,把“外层半成品 + 内层结果 × 倍数”接成新的当前串。

栈里存的不是括号本身,而是“进入括号前的现场”:外层拼到一半的字符串加上等待生效的倍数,出括号时恢复现场继续拼。

双栈解码

数字可能是多位的(100[ab]),读到数字字符时要用 num = num * 10 + digit 累加,而不是直接当一位数用——这是本题最常见的错。

public String decodeString(String s) {
    Deque<Integer> numStack = new ArrayDeque<>();
    Deque<StringBuilder> strStack = new ArrayDeque<>();
    StringBuilder cur = new StringBuilder();
    int num = 0;
    for (char c : s.toCharArray()) {
        if (Character.isDigit(c)) {
            num = num * 10 + (c - '0'); // 数字可能多位
        } else if (c == '[') {
            numStack.push(num);   // 存档倍数
            strStack.push(cur);   // 存档外层半成品
            cur = new StringBuilder();
            num = 0;
        } else if (c == ']') {
            int k = numStack.pop();
            StringBuilder outer = strStack.pop();
            outer.append(cur.toString().repeat(k)); // 恢复现场并拼接
            cur = outer;
        } else {
            cur.append(c);
        }
    }
    return cur.toString();
}
def decodeString(s: str) -> str:
    num_stack = []
    str_stack = []
    cur = []  # 用列表攒字符,避免反复拼接字符串
    num = 0
    for c in s:
        if c.isdigit():
            num = num * 10 + int(c)  # 数字可能多位
        elif c == "[":
            num_stack.append(num)  # 存档倍数
            str_stack.append(cur)  # 存档外层半成品
            cur = []
            num = 0
        elif c == "]":
            k = num_stack.pop()
            outer = str_stack.pop()
            outer.extend(cur * k)  # 恢复现场并拼接
            cur = outer
        else:
            cur.append(c)
    return "".join(cur)
func decodeString(s string) string {
    var numStack []int
    var strStack []string
    cur := ""
    num := 0
    for _, c := range s {
        switch {
        case c >= '0' && c <= '9':
            num = num*10 + int(c-'0') // 数字可能多位
        case c == '[':
            numStack = append(numStack, num) // 存档倍数
            strStack = append(strStack, cur) // 存档外层半成品
            cur, num = "", 0
        case c == ']':
            k := numStack[len(numStack)-1]
            numStack = numStack[:len(numStack)-1]
            outer := strStack[len(strStack)-1]
            strStack = strStack[:len(strStack)-1]
            cur = outer + strings.Repeat(cur, k) // 恢复现场并拼接
        default:
            cur += string(c)
        }
    }
    return cur
}
pub fn decode_string(s: String) -> String {
    let mut num_stack: Vec<usize> = Vec::new();
    let mut str_stack: Vec<String> = Vec::new();
    let mut cur = String::new();
    let mut num = 0usize;
    for c in s.chars() {
        if c.is_ascii_digit() {
            num = num * 10 + c.to_digit(10).unwrap() as usize; // 数字可能多位
        } else if c == '[' {
            num_stack.push(num);                 // 存档倍数
            str_stack.push(std::mem::take(&mut cur)); // 存档外层半成品
            num = 0;
        } else if c == ']' {
            let k = num_stack.pop().unwrap();
            let mut outer = str_stack.pop().unwrap();
            outer.push_str(&cur.repeat(k)); // 恢复现场并拼接
            cur = outer;
        } else {
            cur.push(c);
        }
    }
    cur
}

走一遍 3[a2[c]]:读 3[a 后,栈里是 (3, ""),工作区 a;读 2[ 时再压 (2, "a"),工作区清空;读 c] 弹出 (2, "a")a + c×2 = acc;再读 ] 弹出 (3, "")acc×3 = accaccacc。每个 ] 只负责闭合最近的 [,嵌套多深都不乱。

这题也可以写成递归下降(遇 [ 递归、遇 ] 返回),本质是把显式栈换成调用栈,思路完全同构;双栈版胜在没有全局下标要维护,出错面更小。

复杂度

指标 复杂度 原因
时间 O(L) L 为输出串长度,每个输出字符被拼接常数次
空间 O(L) 栈里存档的半成品总量不超过输出规模

可以迁移的模式

  • 嵌套结构(括号、目录、表达式)配栈是条件反射,栈里存的应该是“进入下一层前的现场”;
  • 解析多位数字用 num = num * 10 + digit 累加,见到非数字字符才算读完;
  • 一对配对符号分别触发“压栈存档”和“弹栈恢复”,中间只管当前层——这套节奏同样适用于基本计算器类题目。

把“现场”从字符串换成运算中间值和符号,就是表达式求值那一族题。