一、题目描述
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: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 | 原子的数量 | 困难 | 栈+计数 |
推荐刷题顺序:
- 20(有效的括号)→ 栈入门
- 本题(394.字符串解码)→ 栈进阶
- 155(最小栈)→ 栈设计
- 726(原子的数量)→ 综合应用
十、总结
| 维度 | 内容 |
|---|---|
| 考察知识点 | 栈、字符串解析、递归 |
| 难度 | 中等 |
| 核心思维 | 用栈保存外层上下文,内层先处理完再处理外层 |
| 关键技巧 | 数字累加、字符串拼接、move语义 |
| 推荐解法 | 方法一(pair栈)或方法二(双栈) |
| 边界情况 | 多位数k、嵌套层数、空字符串 |
一句话总结: 字符串解码的核心是用栈保存"遇到 '' 时已经解析出的部分",等内层 '' 处理完后再拼回去。栈的LIFO特性天然保证了"先内后外"的处理顺序。