1. 问题背景与需求分析
- 删除字符串中的所有相邻重复项是LeetCode上一道经典的字符串处理题目,属于栈结构应用的典型场景。题目要求我们重复删除字符串中所有相邻且相同的字符,直到无法继续删除为止。
这道题看似简单,但实际处理时需要特别注意几个关键点:
- 相邻重复项的删除可能引发新的相邻重复(例如"abbaca"删除"bb"后会得到"aaca",产生新的"aa")
- 需要处理多层嵌套的重复情况(如"abbbaca")
- 最终结果必须确保没有任何相邻重复字符存在
在实际工程中,类似的需求经常出现在文本处理、数据清洗等场景。比如日志去重、DNA序列分析、编译器语法检查等场景都会用到这种相邻重复检测的算法。
2. 栈结构的解题思路
2.1 为什么选择栈结构
栈的"后进先出"(LIFO)特性使其成为解决这类相邻匹配问题的理想选择。每次遇到新字符时,我们只需要与栈顶元素比较即可判断是否形成相邻重复,无需回溯整个字符串。
与直接使用字符串操作相比,栈解法的时间复杂度可以从O(n^2)优化到O(n),因为:
- 每个字符最多入栈和出栈一次
- 避免了字符串拼接操作
- 减少了不必要的字符比较
2.2 算法步骤详解
以下是使用栈结构的完整算法流程:
- 初始化一个空栈
- 遍历字符串中的每个字符:
a. 如果栈不为空且当前字符等于栈顶字符:- 弹出栈顶字符(表示删除这对相邻重复)
b. 否则: - 将当前字符压入栈中
- 弹出栈顶字符(表示删除这对相邻重复)
- 将栈中剩余字符拼接为结果字符串
以"abbaca"为例:
- 初始栈:[]
- 'a':压入 → ['a']
- 'b':压入 → ['a','b']
- 'b':与栈顶'b'匹配 → 弹出 → ['a']
- 'a':与栈顶'a'匹配 → 弹出 → []
- 'c':压入 → ['c']
- 'a':压入 → ['c','a']
- 最终结果:"ca"
3. 代码实现与优化
3.1 基础Python实现
python复制def removeDuplicates(s: str) -> str:
stack = []
for char in s:
if stack and stack[-1] == char:
stack.pop()
else:
stack.append(char)
return ''.join(stack)
这个实现的时间复杂度是O(n),空间复杂度最坏情况下也是O(n)(当没有重复字符时)。
3.2 性能优化技巧
- 预分配空间:对于已知最大可能长度的字符串,可以预分配栈的容量以减少动态扩容开销
- 双指针法:可以用字符串本身作为栈,通过双指针模拟栈操作,减少额外空间使用
- 早期终止:当剩余未处理字符数小于当前栈大小时,可以提前终止循环
优化后的双指针实现:
python复制def removeDuplicates(s: str) -> str:
res = list(s)
end = -1 # 模拟栈顶指针
for char in s:
if end >= 0 and res[end] == char:
end -= 1 # 出栈
else:
end += 1
res[end] = char # 入栈
return ''.join(res[:end+1])
4. 边界条件与测试用例
4.1 必须考虑的边界情况
- 空字符串输入
- 全重复字符串(如"aaaaa")
- 无重复字符串(如"abcde")
- 多层嵌套重复(如"abbbaca")
- 大小写敏感情况(题目通常认为'a'和'A'不同)
4.2 推荐测试用例
python复制test_cases = [
("", ""), # 空字符串
("a", "a"), # 单字符
("aa", ""), # 全重复
("abbaca", "ca"), # 示例
("abbbaca", "abaca"), # 多层重复
("aabccba", ""), # 完全消除
("abcddcba", ""), # 对称消除
("aabbccdd", ""), # 连续对
("abcde", "abcde"), # 无重复
]
5. 算法复杂度分析
5.1 时间复杂度
两种实现的时间复杂度都是O(n),其中n是输入字符串的长度。因为:
- 每个字符只被处理一次
- 栈的push/pop操作都是O(1)
- 最后的join操作也是O(n)
5.2 空间复杂度
- 普通栈实现:O(n)最坏情况空间
- 双指针优化:O(1)额外空间(如果允许修改输入字符串)或O(n)(如果需要保持输入不变)
6. 相关题目与扩展
6.1 LeetCode相似题目
- 20.有效的括号 - 同样使用栈处理匹配问题
- 844.比较含退格的字符串 - 退格处理类似删除操作
- 1209.删除字符串中的所有相邻重复项II - 本题的扩展版本,要求删除k个相邻重复
6.2 工程应用场景
- 文本编辑器中的连续空格压缩
- 日志系统中重复错误的合并
- 数据清洗中的连续重复值处理
- 编译器语法分析中的括号匹配检查
7. 常见错误与调试技巧
7.1 新手常见错误
- 忘记处理栈为空的情况,导致索引错误
- 使用字符串拼接而非列表join,导致O(n^2)时间复杂度
- 错误理解题目要求,比如:
- 认为只需要删除一对重复
- 忽略删除后可能产生的新重复
- 错误处理大小写
7.2 调试建议
- 打印栈状态:在每次操作后打印当前栈内容
- 可视化处理过程:用箭头标注当前处理字符和栈顶关系
- 边界测试:特别关注空输入和全重复情况
- 性能测试:用超长字符串测试算法效率
8. 不同语言实现对比
8.1 Java实现
java复制public String removeDuplicates(String s) {
StringBuilder stack = new StringBuilder();
for (char c : s.toCharArray()) {
int length = stack.length();
if (length > 0 && stack.charAt(length - 1) == c) {
stack.deleteCharAt(length - 1);
} else {
stack.append(c);
}
}
return stack.toString();
}
Java中使用StringBuilder模拟栈,注意其deleteCharAt()和append()方法的用法。
8.2 C++实现
cpp复制string removeDuplicates(string s) {
string stack;
for (char c : s) {
if (!stack.empty() && stack.back() == c) {
stack.pop_back();
} else {
stack.push_back(c);
}
}
return stack;
}
C++中可以直接使用string作为栈,利用其back()和pop_back()方法。
8.3 JavaScript实现
javascript复制function removeDuplicates(s) {
const stack = [];
for (const c of s) {
if (stack.length && stack[stack.length-1] === c) {
stack.pop();
} else {
stack.push(c);
}
}
return stack.join('');
}
JavaScript数组已经具备栈操作的所有方法,实现起来非常直观。
9. 实际工程中的优化考虑
在实际工程项目中应用此类算法时,还需要考虑:
- 内存管理:对于超大字符串,需要考虑内存使用情况
- 并行处理:是否可以分块处理然后合并结果
- 增量处理:对于流式数据如何实时处理
- Unicode支持:正确处理多字节字符和组合字符
- 性能监控:添加统计信息收集,监控实际运行效率
一个工程优化的Python实现可能如下:
python复制def remove_duplicates_prod(s: str, chunk_size=1024) -> str:
"""生产环境适用的去重函数,支持大字符串处理"""
stack = []
buffer = []
for i in range(0, len(s), chunk_size):
chunk = s[i:i+chunk_size]
for char in chunk:
if stack and stack[-1] == char:
stack.pop()
else:
stack.append(char)
# 定期清理内存
if len(stack) > 2 * chunk_size:
buffer.extend(stack)
stack = []
buffer.extend(stack)
return ''.join(buffer)
10. 算法选择与变种问题
10.1 何时选择非栈解法
虽然栈解法是最优的,但在某些特殊情况下可以考虑:
- 正则表达式:对于简单情况可以用正则,如re.sub(r'(.)\1', '', s),但无法处理多层嵌套
- 递归解法:代码简洁但效率低,不推荐
- 双端队列:当需要从两端操作时可以考虑
10.2 题目变种
- 删除k个相邻重复:需要记录连续出现次数
- 保留至少m个重复:设定最小保留数量
- 加权删除:不同字符有不同删除优先级
- 跨行处理:处理多行文本中的重复
以删除k个相邻重复为例的解法:
python复制def removeDuplicatesK(s: str, k: int) -> str:
stack = [] # 存储字符和计数
for char in s:
if stack and stack[-1][0] == char:
stack[-1][1] += 1
if stack[-1][1] == k:
stack.pop()
else:
stack.append([char, 1])
return ''.join(char * cnt for char, cnt in stack)
