删除字符串中的所有相邻重复项 II 图解题解
这道题到底在问什么
- 输入
- s="deeedbbcccbdaa", k=3
- 输出
- "aa" (eee、bbb、ccc 等连环删除后只剩 aa)
- 输入
- s="abcd", k=2
- 输出
- "abcd" (没有任何 2 个相邻相同,原样返回)
先想最直接的笨办法
上方是输入串 s="abbbaaca",下方是空的计数栈。我们从最左边的字符开始,一个一个往栈里送。(动画第 4 步)
最优解:一步一步想明白
- 3一句话套路:把「连续多少个」记在栈顶,凑满 k 就弹掉。弹掉后下一个字符自然去和新的栈顶比较,新相邻关系被自动处理。
- 4上方是输入串 s="abbbaaca",下方是空的计数栈。我们从最左边的字符开始,一个一个往栈里送。
- 5读到第 0 个字符 a,栈为空,它要作为新的一段压入栈。
- 6a×1 压入栈顶。目前栈里拼起来是 "a",这就是「到此为止删不动的结果」。
- 7读到第 1 个字符 b,和栈顶 a 不同,它要作为新的一段压入栈。
- 8b×1 压入栈顶。目前栈里拼起来是 "ab",这就是「到此为止删不动的结果」。
- 9读到第 2 个字符 b,它和栈顶的 b×1 是同一个字符,把栈顶的连续次数加一。
- 10b 现在连续 2 个,还没到 k=3,先留在栈里继续等后面的 b。
- 11读到第 3 个字符 b,它和栈顶的 b×2 是同一个字符,把栈顶的连续次数加一。
- 12b 的连续次数达到 k=3,这一整段 b×3 被删除(弹出栈)。注意:删掉后,新的栈顶又要和后面的字符重新较量。
- 13删除后,栈顶换成了下面那段 a×1。接下来的字符会去和这个新栈顶比较,被删段左右的字符就这样有机会接到一起。
- 14读到第 4 个字符 a,它和栈顶的 a×1 是同一个字符,把栈顶的连续次数加一。
- 15a 现在连续 2 个,还没到 k=3,先留在栈里继续等后面的 a。
- 16读到第 5 个字符 a,它和栈顶的 a×2 是同一个字符,把栈顶的连续次数加一。
- 17a 的连续次数达到 k=3,这一整段 a×3 被删除(弹出栈)。注意:删掉后,新的栈顶又要和后面的字符重新较量。
- 18这一弹之后栈空了,后面的字符将作为全新的一段重新压入。
- 19读到第 6 个字符 c,栈为空,它要作为新的一段压入栈。
- 20c×1 压入栈顶。目前栈里拼起来是 "c",这就是「到此为止删不动的结果」。
- 21读到第 7 个字符 a,和栈顶 c 不同,它要作为新的一段压入栈。
- 22a×1 压入栈顶。目前栈里拼起来是 "ca",这就是「到此为止删不动的结果」。
- 23所有字符扫完。把栈里剩下的每段「字符 ×次数」依次拼起来,就是最终答案 "ca"。中间 bbb 删掉后,左右的 a 接到一起也凑满 3 被删,这正是计数栈自动处理新相邻的威力。
⚠️ 容易写错的地方
✗ 错:每次删除后从头重新扫描整串
✓ 对:用计数栈,删除后让下一个字符自然去比新栈顶
反复扫描是 O(n²/k) 甚至更差,计数栈一遍 O(n) 就处理完所有连环删除
✗ 错:只删一次就停
✓ 对:满 k 立刻弹出,后续字符继续和新栈顶比较
删除可能让左右两段接成新相邻并再次凑满 k,必须能连环触发
✗ 错:把次数判成 ≥ k 才弹
✓ 对:次数恰好 == k 时弹出
每次只加一,凑到 k 当帧就弹,绝不会越过 k,用 == 更直观也更安全
完整代码(Python / C++ / Java)
Python
class Solution:
def removeDuplicates(self, s: str, k: int) -> str:
stack = []
for ch in s:
if stack and stack[-1][0] == ch:
stack[-1][1] += 1
if stack[-1][1] == k:
stack.pop()
else:
stack.append([ch, 1])
return ''.join(ch * cnt for ch, cnt in stack)C++
#include <string>
#include <vector>
using namespace std;
class Solution {
public:
string removeDuplicates(string s, int k) {
vector<pair<char,int>> st;
for (char c : s) {
if (!st.empty() && st.back().first == c) {
if (++st.back().second == k) st.pop_back();
} else st.push_back({c, 1});
}
string ans;
for (auto [c, cnt] : st) ans.append(cnt, c);
return ans;
}
};Java
import java.util.*;
class Solution {
public String removeDuplicates(String s, int k) {
Deque<int[]> st = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (!st.isEmpty() && st.peek()[0] == c) {
st.peek()[1]++;
if (st.peek()[1] == k) st.pop();
} else st.push(new int[]{c, 1});
}
StringBuilder ans = new StringBuilder();
Iterator<int[]> it = st.descendingIterator();
while (it.hasNext()) {
int[] p = it.next();
for (int i = 0; i < p[1]; i++) ans.append((char) p[0]);
}
return ans.toString();
}
}复杂度
时间
O(n)
每个字符入栈、出栈各最多一次,一遍扫描
空间
O(n)
最坏没有任何删除,栈里要存下所有字符段
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 删除字符串中的所有相邻重复项 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
和简化版「k=2 删相邻重复项」相比,这题难在哪?+
k=2 时栈里只需存字符本身,遇到与栈顶相同就弹一个即可。k 一般化后,光存字符不够,必须额外记「连续几个」,凑满 k 才整段弹出。本质都是用栈把删除变成局部操作,只是状态从「字符」升级成「字符 + 计数」。
为什么计数栈能保证连环删除都被处理到?+
因为每次删除(弹出)后,栈顶自动换成更下面那一段,而下一个待处理的字符总是去和「当前栈顶」比较。被删段左右的字符就这样在栈顶相遇,若同字符就继续累计、再次满 k 再弹。整个连锁反应被一遍扫描自然覆盖,无需回退重扫。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 删除字符串中的所有相邻重复项 II 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。