📌 题目链接: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]]" 为例,逐步演示栈操作:
-
初始化空栈
stk,指针ptr = 0。 -
遍历字符串:
-
遇到
'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"→ 入栈
- 弹出直到
-
-
最终栈:
["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!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!