刷爆力扣的极简解法|1047 删除相邻重复项!栈的快速实战案例

上一章节,用栈解决了括号配对问题,这一章节,继续学习栈的实际案例


📖 一、题目描述

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. 时间复杂度:代码跑得有多快(看循环跑了几次)

  2. 空间复杂度:代码占了多少额外内存(除了输入,还申请了啥)


一、写法 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 个"版

核心套路:遇到"相邻 / 配对 / 消除"字眼,第一反应是栈或双指针!

💡 面试加分项

如果你面试时碰到这题,编编建议你:

  1. 先说思路:这题是"配对消除"问题,栈是天然解法

  2. 先写常规写法:数组模拟栈,清晰易懂

  3. 再补双指针 :如果面试官追问"能不能优化空间",直接秀

  4. 提一句复杂度:O(n) 时间,O(n)/O(1) 空间

  5. 拓展一下:提一句"这类相邻消除问题都能用栈做"

这一套下来,面试官一定会对你刮目相看


📌 关注编编,下期带你:

下期,继续拆解算法 🚀