【力扣100题】84.字符串解码

一、题目描述

给定一个经过编码的字符串,返回它解码后的字符串。

编码规则为:k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。

注意:

  • k 保证为正整数
  • 你可以认为输入字符串总是有效的
  • 输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的
  • 原始数据不包含数字,所有的数字只表示重复的次数 k

示例:

示例 输入 输出
示例1 s = "3a" "aaabcbc"
示例2 s = "3a2\[c]" "accaccacc"
示例3 s = "2abc3cdef" "abcabccdcdcdef"
示例4 s = "abc3cdxyz" "abccdcdcdxyz"

提示:

  • 1 <= s.length <= 30
  • s 由小写英文字母、数字和方括号 '\[\]' 组成
  • s 保证是一个有效的输入
  • s 中所有整数的取值范围为 1, 300
  • 测试用例保证输出的长度不会超过 10^5

二、解题思路总览

拿到这道题,首先思考:如何处理嵌套的方括号?

例如 3[a2[c]]

  • 内层 [c] 表示 "c" 重复 2 次 → "cc"
  • 外层 3[...] 表示结果重复 3 次 → "cc" ×3 → "cccccc"

核心难点: 外层需要等待内层解码完成后,才能应用重复次数。

这正是 栈(Stack) 的经典应用场景:记录上下文,等内层处理完再处理外层

核心思路:

步骤 操作 说明
1 遍历字符串的每个字符 依次处理
2 遇到数字 累加到 k(注意可能是多位数)
3 遇到字母 加入当前结果字符串
4 遇到 '[' 入栈:保存"之前的累积结果"和"重复次数 k"
5 遇到 ']' 出栈:将当前结果重复 k 次,加到栈顶结果上

为什么要用栈?

对比 不用栈(错误思路) 用栈(正确思路)
嵌套处理 无法处理嵌套 栈保存外层上下文,内层先处理
多位数k 只能处理单位数 累加k可以处理任意位数
顺序保证 无法保证正确顺序 栈的LIFO保证先内后外

三、完整代码(方法一:栈 + pair)

cpp 复制代码
class Solution {
public:
    string decodeString(string s) {
        stack<pair<string, int>> st;
        string res;
        int k = 0;

        for (char c : s) {
            if (isalpha(c)) {
                // 字母:直接追加到当前结果
                res += c;
            } else if (isdigit(c)) {
                // 数字:累加到 k(注意多位数)
                k = k * 10 + (c - '0');
            } else if (c == '[') {
                // 左括号:当前累积结果和k一起入栈
                st.emplace(move(res), k);
                k = 0;  // 重置k,准备解析下一个数字
            } else {
                // 右括号:解码完成,出栈合并
                auto [pre_res, pre_k] = st.top();
                st.pop();
                // 重复 pre_k 次,累加到 pre_res
                while (pre_k--) {
                    pre_res += res;
                }
                // 更新当前结果,继续处理外层
                res = move(pre_res);
            }
        }
        return res;
    }
};

四、其他解法

方法二:两个栈(数字栈 + 字符串栈)
cpp 复制代码
class Solution {
public:
    string decodeString(string s) {
        stack<int> numSt;      // 数字栈
        stack<string> strSt;   // 字符串栈
        string cur = "";
        int k = 0;

        for (char c : s) {
            if (isdigit(c)) {
                k = k * 10 + (c - '0');
            } else if (isalpha(c)) {
                cur += c;
            } else if (c == '[') {
                // 入栈:保存当前字符串和数字
                numSt.push(k);
                strSt.push(cur);
                k = 0;
                cur = "";
            } else if (c == ']') {
                // 出栈:取出外层字符串,重复后合并
                int repeat = numSt.top();
                numSt.pop();
                string pre = strSt.top();
                strSt.pop();
                // 重复 repeat 次
                for (int i = 0; i < repeat; i++) {
                    pre += cur;
                }
                cur = pre;
            }
        }
        return cur;
    }
};

改进点: 用两个独立的栈分别存储数字和字符串,语义更清晰。


方法三:递归法(用函数调用模拟栈)
cpp 复制代码
class Solution {
public:
    string decodeString(string s) {
        int pos = 0;
        return decode(s, pos);
    }

private:
    string decode(const string& s, int& pos) {
        string res;
        int k = 0;

        while (pos < s.size()) {
            char c = s[pos];
            if (isdigit(c)) {
                k = k * 10 + (c - '0');
                pos++;
            } else if (isalpha(c)) {
                res += c;
                pos++;
            } else if (c == '[') {
                pos++;  // 跳过 '['
                string inner = decode(s, pos);  // 递归解析内层
                // inner 重复 k 次
                while (k--) {
                    res += inner;
                }
                k = 0;  // 重置k
            } else if (c == ']') {
                pos++;  // 跳过 ']'
                return res;  // 返回当前层级结果
            }
        }
        return res;
    }
};

思路: 递归天然具有"先处理内层,再处理外层"的特性,与栈的思路本质相同。


方法四:DFS暴力展开(简单但低效)
cpp 复制代码
class Solution {
public:
    string decodeString(string s) {
        while (s.find('[') != string::npos) {
            // 找到最内层的 [...]
            int left = s.rfind('[');  // 从右向左找第一个 '['
            int right = s.find(']', left);
            // 提取 k[string] 并展开
            string segment = s.substr(left, right - left + 1);
            // 解析 k 和 string
            int bracket = segment.find('[');
            int k = stoi(segment.substr(0, bracket));
            string inner = segment.substr(bracket + 1, segment.size() - bracket - 2);
            // 展开
            string expanded;
            while (k--) expanded += inner;
            // 替换回原字符串
            s.replace(left, right - left + 1, expanded);
        }
        return s;
    }
};

问题: find/replace 都是 O(n),整体复杂度 O(n^3)。仅作思路展示,不推荐


五、算法流程图

以输入 s = "3[a2[c]]" 为例,逐步展示算法执行过程:

复制代码
初始状态:
        s = "3 [ a 2 [ c ] ]"
        pos: 0 1 2 3 4 5 6 7 8
        res: ""
        k: 0
        st: [空]

Step 1: 遇到 '3',数字
        k = 0 * 10 + 3 = 3
        s = "3 [ a 2 [ c ] ]"
             ^
        res: "" k: 3 st: []

Step 2: 遇到 '[',左括号
        - 入栈:(res="", k=3)
        - 重置 res="" 和 k=0
        s = "3 [ a 2 [ c ] ]"
              ^
        res: ""   k: 0   st: [("",3)]

Step 3: 遇到 'a',字母
        res += 'a'
        s = "3 [ a 2 [ c ] ]"
               ^
        res: "a"   k: 0   st: [("", 3)]

Step 4: 遇到 '2',数字
        k = 0 * 10 + 2 = 2
        s = "3 [ a 2 [ c ] ]"
                  ^
        res: "a"   k: 2   st: [("", 3)]

Step 5: 遇到 '[',左括号
        - 入栈:(res="a", k=2)
        - 重置 res="" 和 k=0
        s = "3 [ a 2 [ c ] ]"
                    ^
        res: ""   k: 0   st: [("", 3), ("a", 2)]

Step 6: 遇到 'c',字母
        res += 'c'
        s = "3 [ a 2 [ c ] ]"
                      ^
        res: "c"   k: 0   st: [("", 3), ("a", 2)]

Step 7: 遇到 ']',右括号
        - 出栈:(pre_res="a", pre_k=2)
        - res="c" 重复 2 次 → "cc"
        - pre_res += "cc" → "acc"
        - res = "acc"
        s = "3 [ a 2 [ c ] ]"
                       ^
        res: "acc"   k: 0   st: [("", 3)]

Step 8: 遇到 ']',右括号
        - 出栈:(pre_res="", pre_k=3)
        - res="acc" 重复 3 次 → "accaccacc"
        - res = "accaccacc"
        s = "3 [ a 2 [ c ] ]"
                        ^
        res: "accaccacc"   k: 0   st: []

Step 9: 遍历结束
        返回 "accaccacc"

最终结果: "accaccacc"


六、逐行解析(方法一)

cpp 复制代码
stack<pair<string, int>> st;
string res;
int k = 0;

变量初始化: st 存外层上下文,res 存当前累积结果,k 存当前数字。


cpp 复制代码
if (isalpha(c)) {
    res += c;
}

字母处理: 直接追加到当前结果字符串。


cpp 复制代码
else if (isdigit(c)) {
    k = k * 10 + (c - '0');
}

数字处理: 累加到 k。这里 k = k * 10 + (c - '0') 是解析多位数数字的标准写法,例如 "12" 会正确解析为 12。


cpp 复制代码
else if (c == '[') {
    st.emplace(move(res), k);
    k = 0;
}

左括号处理: 当前累积的结果和重复次数 k 入栈保存,然后重置为下一层做准备。


cpp 复制代码
else {
    auto [pre_res, pre_k] = st.top();
    st.pop();
    while (pre_k--) {
        pre_res += res;
    }
    res = move(pre_res);
}

右括号处理: 这是解码的核心。出栈获取外层结果,res重复 pre_k 次后追加到外层结果上,然后更新 res继续处理外层。


七、复杂度分析

各方法复杂度对比
方法 时间复杂度 空间复杂度 说明
方法一:pair栈 O(n) O(n) 标准解法,稳定高效
方法二:双栈 O(n) O(n) 代码清晰,两个栈分别存储
方法三:递归 O(n) O(n) 代码简洁,但有递归栈溢出风险
方法四:暴力展开 O(n^3) O(n) 不可用,find/replace太慢

核心: 方法一、二、三都是 O(n),推荐使用。

方法一详细复杂度分析

时间复杂度:O(n)

  • 遍历字符串一次,O(n)
  • 每个字符最多入栈一次、出栈一次
  • 字符串拼接总长度 O(n)

空间复杂度:O(n)

  • 栈最多存储 O(n) 个 pair
  • 每个 pair 存储一个字符串(最长 O(n))
  • 最坏 O(n)

字符串拼接复杂度说明:

操作 复杂度 说明
res += c (单字符) 均摊 O(1) C++ string 有预留空间
pre_res += res 最坏 O(n) 但总拼接次数有限,均摊 O(n)

八、面试追问 FAQ

问题 回答要点
Q1: 为什么数字要用 k = k * 10 + (c - '0') 因为可能有 "12" 这样的多位数。如果直接 k = c - '0',只会取最后一位。
Q2: 递归法和栈法哪个更好? 生产环境推荐栈法,没有递归栈溢出风险。递归法代码更简洁,但深度可能达到10^5(极端情况),会爆栈。
Q3: 如何处理字符串拼接的性能问题? C++ string 会预分配空间,均摊是 O(1)。如果追求极致性能,可以用 string::reserve() 预先分配,或者用 vector<char>
Q4: 如果要支持其他括号如 {} 怎么办? 用 map 存储配对关系,或者写一个通用的配对检查函数。核心思路不变。
Q5: 面试时推荐哪种方法? 首选方法一或方法二,代码清晰不易出错。如果面试官问递归,再引出方法三。
Q6: move(res) 有什么作用? 避免字符串拷贝,将 res 的所有权转移给栈中的 pair,C++11特性。

九、相关题目

题号 题目 难度 关联点
394 字符串解码 中等 本题
20 有效的括号 简单 栈的基础应用
155 最小栈 中等 栈的设计
341 扁平化嵌套列表迭代器 中等 嵌套结构处理
726 原子的数量 困难 栈+计数

推荐刷题顺序:

  1. 20(有效的括号)→ 栈入门
  2. 本题(394.字符串解码)→ 栈进阶
  3. 155(最小栈)→ 栈设计
  4. 726(原子的数量)→ 综合应用

十、总结

维度 内容
考察知识点 栈、字符串解析、递归
难度 中等
核心思维 用栈保存外层上下文,内层先处理完再处理外层
关键技巧 数字累加、字符串拼接、move语义
推荐解法 方法一(pair栈)或方法二(双栈)
边界情况 多位数k、嵌套层数、空字符串

一句话总结: 字符串解码的核心是用栈保存"遇到 '' 时已经解析出的部分",等内层 '' 处理完后再拼回去。栈的LIFO特性天然保证了"先内后外"的处理顺序。


相关推荐
嵌入式ZYXC1 小时前
第9篇:《面试题:ADC前端为什么要加运放跟随器?什么情况下可以不加?》
stm32·单片机·嵌入式硬件·面试·职场和发展
MicroTech20252 小时前
量子隐形传态路线的瓶颈与突破,微算法科技(MLGO)以技术创新助力量子通信长距离组网
科技·算法·量子计算
洛水水2 小时前
【力扣100题】89.下一个排列
数据结构·算法·leetcode
洛水水2 小时前
【力扣100题】90.寻找重复数
算法·leetcode·职场和发展
鱼子星_2 小时前
【数据结构】排序的拓展——快速排序的生态多样性与归并排序沾染文件操作
c语言·数据结构·算法
alphaTao2 小时前
LeetCode 每日一题 2026/6/8-2026/6/14
算法·leetcode
KaMeidebaby2 小时前
卡梅德生物技术快报|噬菌体展示文库构建全流程解析 | 大豆球蛋白纳米抗体筛选实践
人工智能·python·tcp/ip·算法·机器学习
CC数学建模2 小时前
2026年第十六届APMCM 亚太地区大学生数学建模竞赛(中文赛项)赛题B题:高性能芯片热管理系统的优化问题完整思路、代码、模型、文章,全网首发高质量分享!
python·算法·数学建模
爱睡懒觉的焦糖玛奇朵2 小时前
【视觉检测之人员奔跑检测算法开发思路】
人工智能·python·深度学习·算法·yolo·视觉检测