字符串减法指从字符串a中移除字符串b中的字符,分集合差(如"hello"−"lo"→"he")和多集差(按频次,如"aabbcc"−"abc"→"abcc");常用unordered_set实现集合差,array实现多集差。

什么是“字符串减法”?先明确操作语义
“字符串减法”不是 C++ 标准库里的概念,也没有 operator- 默认支持。实际场景中,它通常指:从字符串 a 中移除所有在字符串 b 中出现的字符(按出现次数配对,或仅按集合去重)。两种常见语义必须分清:
-
集合差:把
b当作字符集合,从a中删掉所有属于该集合的字符(如"hello"−"lo"→"he") -
多集差(带频次):按字符频次做减法,
b中每个字符最多抵消a中同字符一次(如"aabbcc"−"abc"→"abcc",不是"abc")
不明确语义就写算法,大概率跑偏。多数业务需求是集合差;高频文本处理(如 diff、编辑距离预处理)可能需要多集差。
集合差:用 std::unordered_set 实现 O(n) 清洗
这是最常用、也最容易写出高性能解法的场景。核心思路是:先将 b 的字符装进哈希集合,再单趟扫描 a,保留不在集合中的字符。
示例代码(含关键细节):
立即学习“C++免费学习笔记(深入)”;
std::string string_subtract(const std::string& a, const std::string& b) {
std::unordered_set<char> remove_set(b.begin(), b.end()); // 自动去重,O(|b|)
std::string result;
result.reserve(a.size()); // 预分配,避免多次 realloc
for (char c : a) {
if (remove_set.find(c) == remove_set.end()) {
result.push_back(c);
}
}
return result;
}
注意点:
- 用
std::unordered_set而不是std::set:前者平均 O(1) 查找,后者 O(log |b|),批量时差距明显 -
result.reserve(a.size())很关键——没这句,短字符串可能没事,但处理 MB 级a时,反复扩容会拖慢 2–3 倍 - 如果
b含重复字符(如"aaa"),unordered_set构造自动去重,符合集合差定义
多集差(带频次):用 std::array<int></int> 替代哈希表
当要按出现次数做减法(即 “a 中每个字符最多被 b 消耗一次”),需统计 b 中各字符频次。此时用 std::array<int></int> 比 std::unordered_map<char int></char> 更快——无哈希计算、无内存分配、CPU cache 友好。
实操建议:
- 假设输入为 ASCII 或 Latin-1 字符(
unsigned char范围),直接用static_cast<unsigned char>(c)</unsigned>下标访问 - 初始化数组为 0,遍历
b累加频次;再遍历a,对每个字符检查计数是否 > 0,是则减一并跳过,否则保留 - 不要用
std::vector<int>(256)</int>——动态分配有开销;std::array在栈上,零成本
典型错误:用 map 存频次后,对每个 a[i] 都调用 map[c]-- —— 这会触发默认构造(插入新键),导致意外增加键值对,逻辑出错。
批量处理时,别忽略内存与迭代器失效风险
如果要对 vector
-
std::string的erase()在中间删除会导致后续字符内存搬移,O(n²) 复杂度,绝对不能边查边删 - 更糟的是:用
for (auto it = s.begin(); it != s.end(); ++it)配合erase(it)——erase返回新迭代器,不接住就造成悬垂迭代器,UB - 安全做法只有两种:① 先标记要删的位置(如 bool vector),再倒序 erase;② 用 “双指针覆盖” 写入新串(推荐,缓存局部性好)
真正影响性能的往往不是算法本身,而是字符串对象的生命周期管理——比如反复构造临时 std::string 返回值,在 tight loop 里会触发大量小内存分配。必要时改用输出参数 std::string& out 复用缓冲区。


















