
上一章节,用栈解决了括号配对问题,这一章节,继续学习栈的实际案例
📖 一、题目描述
LeetCode 1047. 删除字符串中的所有相邻重复项
给出由小写字母组成的字符串
s,重复项删除操作会选择两个相邻且相同的字母,并删除它们。在
S上反复执行重复项删除操作,直到无法继续删除。在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例
示例 1:
输入:"abbaca"
输出:"ca"
解释:
"abbaca" → "ca"
示例 2:
输入:"azxxzy"
输出:"ay"
解释:
"azxxzy" → "ay"
提示
1 <= s.length <= 20000字符串只由小写英文字母组成
🧠 二、思路讲解:为什么"栈"是这题的灵魂?
关键观察
题目说"删除两个相邻且相同的字母 ",这其实是一个 "配对消除" 问题。
这种题,栈是天然解法。
就像我们玩 消消乐:
上面一个方块 = "a"
-
下面一个方块 = "a"
-
两个相同 → 消除
消除 → 往栈里 push,匹配 → 往栈里 pop。
这就是栈的核心应用之一:括号匹配 / 相邻配对。
💻 三、代码实现
🔵 JavaScript 实现(2 种写法)
写法 1:用数组模拟栈(最常用,最清晰)
核心思路:
遇到一个字符
c,看栈顶是不是和它一样一样 → 配对消除 (把栈顶 pop 掉,
c也不入栈)不一样 → 入栈 (
push(c))遍历完整个字符串,栈里剩下的就是答案
javascript
/**
* @param {string} s
* @return {string}
*/
function removeDuplicates(s) {
const stack = [];
for (const c of s) {
// 栈顶和当前字符相同 → 配对消除(弹出栈顶)
// 栈顶和当前字符不同 → 入栈
if (stack.length > 0 && stack[stack.length - 1] === c) {
stack.pop();
} else {
stack.push(c);
}
}
return stack.join('');
}
输入:"abbaca",详细图解

写法 2:双指针(原地修改,最优解)
双指针的好处:O(1) 额外空间(不算输入输出)。
javascript
/**
* @param {string} s
* @return {string}
*/
function removeDuplicates(s) {
const arr = s.split(''); // 字符串转数组(JS 字符串不可变)
let slow = 0; // 慢指针:指向"栈顶"的下一个位置
for (let fast = 0; fast < arr.length; fast++) {
// slow > 0 说明"栈"非空
// arr[slow - 1] 就是栈顶元素
if (slow > 0 && arr[slow - 1] === arr[fast]) {
slow--; // 匹配,栈顶"弹出"(慢指针回退)
} else {
arr[slow] = arr[fast]; // 不匹配,栈顶"压入"
slow++;
}
}
return arr.slice(0, slow).join('');
}
输入:"abbaca",

简单理解:用双指针模拟栈的进出。
-
fast:顺序扫数组的"眼睛" -
slow:栈顶位置(指向当前结果数组的末尾)
逻辑:
// 准备入栈的元素 = 数组[fast]
// 当前栈顶元素 = 数组[slow-1]
if (栈顶==准备入栈的元素) {
// 抵消 → 出栈
slow--; // 栈顶退一格
} else {
// 不一样 → 入栈
数组[slow] =数组[fast];
slow++;
}
双指针的本质:
-
slow是 栈顶指针(指向下一个要写入的位置) -
fast是 遍历指针(遍历原始字符串) -
arr[slow - 1]就是 栈顶元素 -
整个过程原地修改数组,没有额外空间!
为什么 slow 不需要真的"删除"栈顶元素?
-
slow--就相当于"弹出栈顶",因为slow减 1 后,原来slow-1位置的元素就不再属于结果数组 -
后面
arr[slow] = arr[fast]会直接覆盖那个位置 -
最后
arr.slice(0, slow)截取[0, slow)区间,就是结果
🐍 Python 实现(2 种写法)
和 JavaScript 一样的两种思路,Python 写法更简洁。
写法 1:用 list 模拟栈(最常用,最清晰)
python
class Solution:
def removeDuplicates(self, s: str) -> str:
stack = []
for c in s:
# 栈顶和当前字符相同 → 配对消除(弹出栈顶)
# 栈顶和当前字符不同 → 入栈
if stack and stack[-1] == c:
stack.pop() # Python 的真值判断:空列表是 False,不用写 len(stack) > 0
else:
stack.append(c)
return ''.join(stack)
Python 特性:
stack[-1]取栈顶,比stack[stack.length-1]优雅
if stack and ...中,空列表是False,自动跳过,不用写len(stack) > 0
''.join(stack)列表转字符串,比+快得多3 行核心代码,秒杀 90% 的提交
写法 2:双指针(原地修改,最优解)
python
class Solution:
def removeDuplicates(self, s: str) -> str:
# 把字符串转成 list(因为 Python 字符串不可变)
arr = list(s)
slow = 0 # 慢指针:指向"栈顶"的下一个位置
for fast in range(len(arr)):
# slow > 0 说明"栈"非空
# arr[slow - 1] 就是栈顶元素
if slow > 0 and arr[slow - 1] == arr[fast]:
slow -= 1 # 匹配,栈顶"弹出"(慢指针回退)
else:
arr[slow] = arr[fast] # 不匹配,栈顶"压入"
slow += 1
return ''.join(arr[:slow])
Python 字符串不可变 ,必须先转成
list才能原地修改。双指针的好处:O(1) 额外空间(不算输入输出)。
⏱️ 四、复杂度分析
时间复杂度:代码跑得有多快(看循环跑了几次)
空间复杂度:代码占了多少额外内存(除了输入,还申请了啥)
一、写法 1:数组模拟栈
| 维度 | 结论 | 解释 |
|---|---|---|
| ⏱️ 时间复杂度 | O(n) | 字符串有 n 个字符,就扫 n 次,每次就看一眼栈顶 → 总共 O(n) |
| 💾 空间复杂度 | O(n) | 最坏情况所有字符都不消,栈里要装 n 个 → O(n) |
怎么数时间复杂度?
for (const c of s) { // 字符串多长,这个 for 就跑几次
if (...) { ... } // 每次就看一眼、push 或 pop,都是"瞬间"完成
else { ... }
}
一层 for + 每次干点小事 = O(n),这就是时间复杂度。
怎么数空间复杂度?
不用想复杂,只数额外申请了啥:
| 申请的东西 | 算不算额外空间 |
|---|---|
| 输入的字符串 s | ❌ 不算 |
| 自己 new 的 stack 数组 | ✅ 算 |
| 几个变量(i、slow 这种) | ❌ 不算(就 1 个) |
写法 1 额外申请了一个 stack 数组,最坏装 n 个字符 → O(n)。
二、写法 2:双指针
| 维度 | 结论 | 小白版解释 |
|---|---|---|
| ⏱️ 时间复杂度 | O(n) | 同样是 1 层 for 循环扫 n 次,跟写法 1 一样 |
| 💾 空间复杂度 | O(1) ✅ | 没额外申请数组,只多了 2 个变量(slow、fast) |
为什么空间能从 O(n) 变成 O(1)?
写法 1:用了 stack 数组存字符 → 占 O(n) 额外空间写法 2:直接在原数组上改,不另开数组 → 只多 2 个变量,变量是固定的 → O(1)
💡 O(1) 是什么意思? 就是"不管输入多长,占的空间永远是固定的,不会变多"。
三、两种写法对比
| 写法 1(数组栈) | 写法 2(双指针) | |
|---|---|---|
| ⏱️ 时间 | O(n) | O(n) |
| 💾 空间 | O(n) | O(1) ✅ |
| 📖 难度 | ⭐ 直观 | ⭐⭐ 需思考 |
🎯 时间一样,写法 2 更省空间;
面试先写写法 1,追问空间优化再补写法 2。
🎁 五、彩蛋:这题还能怎么拓展?
你以为这题就完了?Too young!
这道题的"灵魂"是 "相邻配对消除"。掌握这个套路,下面这些题你也能秒杀:
🔗 拓展题推荐
1️⃣ LeetCode 20. 有效的括号 ------ 栈的入门题,和这题异曲同工(上一篇已讲解)
2️⃣ LeetCode 71. 简化路径 ------ 用栈处理"..""."
3️⃣ LeetCode 844. 比较含退格的字符串 ------ 这题的"反向"版
4️⃣ LeetCode 1544. 整理字符串 ------ 这题的"大小写敏感"版
5️⃣ LeetCode 1209. 删除字符串中的所有相邻重复项 II ------ 这题的"删除 K 个"版
核心套路:遇到"相邻 / 配对 / 消除"字眼,第一反应是栈或双指针!
💡 面试加分项
如果你面试时碰到这题,编编建议你:
-
先说思路:这题是"配对消除"问题,栈是天然解法
-
先写常规写法:数组模拟栈,清晰易懂
-
再补双指针 :如果面试官追问"能不能优化空间",直接秀
-
提一句复杂度:O(n) 时间,O(n)/O(1) 空间
-
拓展一下:提一句"这类相邻消除问题都能用栈做"
这一套下来,面试官一定会对你刮目相看。
📌 关注编编,下期带你:
下期,继续拆解算法 🚀