保序去重必须用std::unordered_set辅助单次遍历,因其边扫边记“见过谁”,时间复杂度稳定O(n)、空间O(k),而std::sort+std::unique会打乱原始顺序且仅压缩相邻重复,导致语义错误。

保序去重必须用 std::unordered_set 辅助单次遍历,其他方案要么破坏顺序,要么性能崩盘。
为什么 std::sort + std::unique 不行
它只对相邻重复有效,而 std::sort 会彻底打乱原始索引关系。比如日志序列 {"login", "click", "login"},排序后变成 {"click", "login", "login"},std::unique 留下的是 {"click", "login"}——语义全错。
常见错误现象:
- 误以为
std::unique是“全局去重”,实际它不看全局,只压缩连续块 - 调用完
std::unique忘记用erase截断,导致后续访问越界或读到脏值 - 对
std::vector做了sort+unique后直接返回,结果顺序与业务逻辑冲突
std::unordered_set 辅助遍历的实操要点
核心是边扫边记“见过谁”,不是靠排序归并。时间复杂度稳定 O(n),空间 O(k)(k 为不同元素个数)。
- 用
std::unordered_set而非std::set:前者平均插入/查找O(1),后者O(log n)且强制排序 -
seen.insert(x).second是推荐写法——insert返回std::pair<iterator bool></iterator>,.second为true表示新插入,天然避免二次查表 - 对
std::string或自定义类型,确保已提供std::hash特化或哈希函数对象,否则编译失败 - 提前调用
result.reserve(vec.size()),防止反复push_back触发内存重分配
原地修改用 std::remove_if 的隐藏风险
有人想省空间,写类似这样的代码:
auto newEnd = std::remove_if(vec.begin(), vec.end(), [&seen](int x) {
if (seen.count(x)) return true;
seen.insert(x);
return false;
});
表面看没新建容器,但问题在于:std::remove_if 不保证遍历顺序与原容器一致(尤其在并行 STL 实现中),且 lambda 捕获的 seen 在多线程环境下非线程安全。更关键的是,它无法处理自定义类型的移动语义边界——若元素析构依赖顺序,remove_if 可能提前销毁不该删的对象。
简单说:保序 + 原地 + 安全 = 几乎不可能三者兼顾。老实用新容器构建结果最可靠。
UTF-8 字符串和自定义类型的注意事项
std::unordered_set<char></char> 对 ASCII 安全,但对 UTF-8 字符串无效——一个汉字占 3 字节,按 char 判重会把字节当独立字符切开,结果错乱。
- 正确做法:先用 ICU 或
std::codecvt_utf8(C++17 已弃用,建议用第三方库)转成std::u32string,再用std::unordered_set<char32_t></char32_t> - 自定义结构体去重,必须同时提供
operator==和哈希函数,例如:struct Point { int x, y; bool operator==(const Point& o) const { return x == o.x && y == o.y; } }; namespace std { template struct hash<point> { size_t operator()(const Point& p) const { return hash<int>{}(p.x) ^ (hash<int>{}(p.y) </int></int></point>
哈希函数里用异或加移位是常见 trick,但要注意避免全零碰撞;真实项目建议用 std::hash<:pair>></:pair> 组合。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











