std::unique仅压缩相邻重复元素,需先排序再配合erase才能真正去重;它时间复杂度o(n)、不依赖哈希,但要求相同值相邻,且自定义谓词须满足等价关系。

std::unique 不是“去重函数”,它只压缩相邻重复段;漏掉排序或 erase,结果一定出错。
std::unique 为什么只删相邻重复项
它内部只比较 *it 和 *(it-1),不查全局、不建哈希表、不回溯。输入是 {1,2,1,2},调完还是 {1,2,1,2}——因为根本没有相邻相等对。
这设计不是缺陷,而是刻意为之:轻量、稳定 O(n)、不依赖额外内存或可哈希性。但它要求你提前保证“相同值挨着”,最常用手段就是先 std::sort。
- 排序后:
{1,1,2,2}→unique才能识别并压缩 - 若数据天然有序(如时间戳递增的日志条目),可跳过排序,直接用
- 自定义类型必须提供
operator==或传 predicate,否则编译失败
std::unique + erase 必须成对出现
std::unique 返回的是新逻辑尾的迭代器,比如 v = {1,1,2,2,3} 经 unique 后变成 {1,2,3,2,3},返回指向第 4 个元素(即第一个 2)的迭代器。容器 size 没变,末尾仍是脏数据。
常见错误现象:v.size() 没变、打印出来“好像没去重”、后续遍历时访问到残留值。
- 必须显式调用
v.erase(new_end, v.end())才真正收缩容器 - 对
std::list可直接用成员函数l.unique(),它自动完成擦除,不用手写 erase - 对
std::vector或std::deque,三步缺一不可:sort → unique → erase
自定义 predicate 的坑:等价关系必须成立
传入的二元谓词(比如按 id 去重)不能只是“看着顺眼”,它得满足数学上的等价关系:自反、对称、传递。否则行为未定义——可能 crash、漏删、甚至静默错乱。
例如这个 predicate 就危险:[](const auto& a, const auto& b) { return std::abs(a.x - b.x) ——它不满足传递性(a≈b 且 b≈c,不代表 a≈c)。
- 安全做法:用
==或字段精确比对,如a.id == b.id - 若需模糊匹配(如字符串忽略大小写),应先 normalize 再用
==,而不是在 predicate 里做转换 - predicate 参数顺序是
(first, second),即判断second是否该被移除(当返回 true 时)
保序去重时别硬套 unique
如果原始顺序必须保留(比如日志按时间先后插入,但要去掉内容完全相同的重复条目),sort + unique 会打乱顺序,此时它根本不适用。
正确做法是用 std::unordered_set 记录已见元素,单次遍历构建新容器:
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6, 5};
std::unordered_set<int> seen;
std::vector<int> uniq;
for (int x : v) {
if (seen.insert(x).second) uniq.push_back(x);
}
// uniq = {3, 1, 4, 5, 9, 2, 6}
</int></int></int>
这种方案不修改原容器、不依赖排序、顺序严格保持,代价是额外 O(n) 空间和哈希开销——但比起错用 unique 导致逻辑 bug,这点成本几乎可以忽略。
真正容易被忽略的,是“unique 的返回值不是 size,也不是有效范围终点,而是一个要你亲手擦掉的分界线”。它不负责善后,只负责把该留的往前挪。你得盯住那个迭代器,然后自己动手删。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











