【LeetCode Hot100 刷题日记 (71/100)】394. 字符串解码 —— 栈模拟与递归解析🧠

4 阅读5分钟

📌 题目链接:394. 字符串解码 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:字符串递归

⏱️ 目标时间复杂度:O(S) ,其中 S 为解码后字符串长度

💾 空间复杂度:O(S) (栈方法)或 O(|s|) (递归方法)


🧠 题目分析

本题要求我们对一个经过编码的字符串进行解码。编码规则是:k[encoded_string] 表示 encoded_string 重复 k 次。

关键点:

  • 输入保证有效,括号一定匹配;
  • 数字只用于表示重复次数(不会出现如 3a 这种非法输入);
  • 原始数据不含数字,所有数字都是重复系数;
  • 可能存在嵌套结构,如 "3[a2[c]]"

这本质上是一个带嵌套结构的字符串展开问题,非常适合用 递归 来处理——因为嵌套天然具有“先进后出”或“自相似”的特性。


⚙️ 核心算法及代码讲解

本题有两种主流解法:

方法一:栈模拟(推荐面试使用)

  • 使用来模拟解码过程;
  • 遇到 ] 时,开始弹出直到遇到 [,拼接内部字符串;
  • 弹出 [ 后,栈顶必为数字(题目保证格式合法),取出该数字 k
  • 将内部字符串重复 k 次后压回栈;
  • 最终栈中所有元素拼接即为结果。

优点:逻辑清晰,易于调试,符合人类“从内向外”展开的直觉。
面试价值高:考察对栈的理解、字符串处理、边界控制。

方法二:递归解析(编译原理视角)

  • 将字符串视为文法:String → Digits[String]String | AlphaString | ε
  • 从左到右递归解析,遇到数字就进入子表达式;
  • 遇到 ] 或结尾则返回当前层结果;
  • 自然处理嵌套,代码简洁。

优点:代码短,体现递归思维;
⚠️ 注意:递归深度可能达到 O(|s|),在极端嵌套下有栈溢出风险(但本题 |s| ≤ 30,安全)。


🧩 解题思路(栈方法,分步详解)

我们以输入 "3[a2[c]]" 为例,逐步演示栈操作:

  1. 初始化空栈 stk,指针 ptr = 0

  2. 遍历字符串

    • 遇到 '3' → 提取完整数字 "3" → 入栈:["3"]

    • 遇到 '[' → 入栈:["3", "["]

    • 遇到 'a' → 入栈:["3", "[", "a"]

    • 遇到 '2' → 提取 "2" → 入栈:["3", "[", "a", "2"]

    • 遇到 '[' → 入栈:["3", "[", "a", "2", "["]

    • 遇到 'c' → 入栈:["3", "[", "a", "2", "[", "c"]

    • 遇到 ']'(第一个):

      • 弹出直到 '[':得到 ["c"] → 反转(其实顺序不变)→ 拼成 "c"
      • 弹出 '['
      • 弹出数字 "2"repTime = 2
      • 构造 "cc" → 入栈:["3", "[", "a", "cc"]
    • 遇到 ']'(第二个):

      • 弹出直到 '[':得到 ["cc", "a"]反转["a", "cc"] → 拼成 "acc"
      • 弹出 '['
      • 弹出 "3"repTime = 3
      • 构造 "accaccacc" → 入栈
  3. 最终栈["accaccacc"] → 拼接输出。

🔔 关键细节:弹出子串时顺序是反的,必须 reverse


📊 算法分析

方法时间复杂度空间复杂度是否修改原串面试推荐度
栈模拟O(S)O(S)⭐⭐⭐⭐⭐
递归O(S)O(s)
  • S 是解码后字符串的总长度(可能远大于原串);
  • 栈方法的空间主要消耗在存储中间字符串;
  • 递归方法的空间消耗在函数调用栈,深度最多为嵌套层数(≤15,因 |s|≤30)。

💡 面试提示:优先写栈方法!它更通用,且能展示你对“状态管理”的理解。


💻 代码

C++

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class Solution {
public:
    // 辅助函数:从 ptr 开始提取连续数字
    string getDigits(string &s, size_t &ptr) {
        string ret = "";
        while (ptr < s.size() && isdigit(s[ptr])) {
            ret.push_back(s[ptr++]);
        }
        return ret;
    }

    // 辅助函数:将 vector<string> 拼接成一个字符串
    string getString(vector<string> &v) {
        string ret;
        for (const auto &str : v) {
            ret += str;
        }
        return ret;
    }

    string decodeString(string s) {
        vector<string> stk;      // 用 vector 模拟栈
        size_t ptr = 0;          // 当前遍历位置

        while (ptr < s.size()) {
            char cur = s[ptr];
            if (isdigit(cur)) {
                // 👉 提取完整数字并入栈
                string digits = getDigits(s, ptr);
                stk.push_back(digits);
            } else if (isalpha(cur) || cur == '[') {
                // 👉 字母或 '[' 直接入栈(转为 string)
                stk.push_back(string(1, s[ptr++])); 
            } else { // cur == ']'
                ++ptr; // 跳过 ']'
                vector<string> sub; // 存放待重复的子串(逆序)
                // 👉 弹出直到遇到 '['
                while (stk.back() != "[") {
                    sub.push_back(stk.back());
                    stk.pop_back();
                }
                reverse(sub.begin(), sub.end()); // 修正顺序
                stk.pop_back(); // 弹出 '['

                // 👉 此时栈顶是重复次数
                int repTime = stoi(stk.back());
                stk.pop_back();

                // 👉 构造重复字符串
                string t, o = getString(sub);
                while (repTime--) t += o;

                // 👉 将结果压回栈
                stk.push_back(t);
            }
        }

        // 👉 拼接最终结果
        return getString(stk);
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    Solution sol;
    vector<string> testCases = {
        "3[a]2[bc]",
        "3[a2[c]]",
        "2[abc]3[cd]ef",
        "abc3[cd]xyz"
    };
    vector<string> expected = {
        "aaabcbc",
        "accaccacc",
        "abcabccdcdcdef",
        "abccdcdcdxyz"
    };

    for (int i = 0; i < testCases.size(); i++) {
        string res = sol.decodeString(testCases[i]);
        cout << "Input: " << testCases[i] << "\n";
        cout << "Output: " << res << "\n";
        cout << "Expected: " << expected[i] << "\n";
        cout << (res == expected[i] ? "✅ PASS" : "❌ FAIL") << "\n\n";
    }

    return 0;
}

JS

/**
 * @param {string} s
 * @return {string}
 */
var decodeString = function(s) {
    const stack = [];
    let i = 0;

    // 辅助:提取数字
    const getDigits = () => {
        let num = '';
        while (i < s.length && /\d/.test(s[i])) {
            num += s[i++];
        }
        return num;
    };

    // 辅助:拼接数组
    const getString = (arr) => arr.join('');

    while (i < s.length) {
        const char = s[i];
        if (/\d/.test(char)) {
            stack.push(getDigits());
        } else if (/[a-z]/.test(char) || char === '[') {
            stack.push(s[i++]);
        } else { // char === ']'
            i++; // skip ']'
            let sub = [];
            while (stack[stack.length - 1] !== '[') {
                sub.push(stack.pop());
            }
            sub.reverse();
            stack.pop(); // remove '['
            const repTime = parseInt(stack.pop(), 10);
            const repeated = getString(sub).repeat(repTime);
            stack.push(repeated);
        }
    }

    return getString(stack);
};

// 测试用例
console.log(decodeString("3[a]2[bc]"));      // "aaabcbc"
console.log(decodeString("3[a2[c]]"));       // "accaccacc"
console.log(decodeString("2[abc]3[cd]ef"));  // "abcabccdcdcdef"
console.log(decodeString("abc3[cd]xyz"));    // "abccdcdcdxyz"

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!