C++unique去除相邻重复元素

2026-04-11 14:15:36 430阅读 0评论

std::unique 不是“去重神器”:它只删相邻重复,而且得靠你手动收尾

刚学 C++ 时,我翻到 std::unique 这个函数,心里一喜:“终于不用手写循环去重了!”结果第一次用就翻车——原容器里明明有 1,2,2,3,3,3,4,调完 unique 一看,打印出来还是七个数,最后三个居然还是 3,3,4
后来才明白:std::unique 从不真正“删除”任何元素,它只是把重复项挤到末尾,再返回一个新终点迭代器。它干的活,更像“整理行李箱”:把相同衣服叠在一起塞进角落,但箱子大小没变,你得自己把多出来的空包拉链拉上。


它到底做了什么?

假设你有一段 vector<int> v = {1,2,2,3,3,3,4};
执行:

auto new_end = std::unique(v.begin(), v.end());

此时 v 的内存状态其实是:{1,2,3,4,3,3,4} —— 前四元素是“去重后保留的”,后面三个是被覆盖掉的旧值(具体值取决于实现,但一定不是你想要的“干净结果”)。
new_end 指向第 5 个位置(即 v.begin() + 4),告诉你:“从这儿开始,都是冗余的”。

关键点来了:unique 只比较相邻元素,且仅移动值,不改变容器大小。
它不会碰 {2,1,2,1} 这种非相邻重复;也不会主动 erase——那一步,得你亲手补上。


为什么非要配 erase?因为容器不知道你“想留几个”

C++ 容器的 size 是硬指标。unique 返回的迭代器只是逻辑分界线,就像快递员把包裹堆在门口说“这些是你的”,但门框不会自动缩小。
所以标准写法永远是:

v.erase(std::unique(v.begin(), v.end()), v.end());

这行代码的含义很朴实:unique 划出的“冗余区”整段切掉
注意:erase 的第一个参数必须是 unique 的返回值,第二个必须是 end()——少一个字符,程序就可能越界或漏删。


实际踩坑场景:字符串、自定义类型、逆序处理

  • 字符串处理:有人想清理 "aaabbbcccaaa" 中的连续重复字母,直接 unique(s.begin(), s.end()) 就够了。但若字符串是 "abac",它完全不动——因为 aa 不相邻。这时候得先 sortunique?别急,那已经不是 unique 的职责了,那是 std::setstd::unordered_set 的地盘。

  • 自定义类型:比如 vector<Person> 按姓名去重。unique 默认用 == 比较,所以你得确保 Person 重载了 operator==,或者传入自定义谓词:

    auto new_end = std::unique(v.begin(), v.end(),
      [](const Person& a, const Person& b) {
          return a.name == b.name;
      });

    这里有个隐性前提:数据必须已按 name 排好序或至少相邻同名。否则,张三、李四、张三,unique 会放过第二个“张三”。

  • 逆序去重?不行,但可以倒着走unique 只向前看相邻项。想保留最后一次出现的重复项(比如 {1,2,2,3,2}{1,3,2}),得先反转,unique,再反转回来。不过这种需求,往往说明该换思路了——用 unordered_map 记录最后索引更直白。


一个容易被忽略的细节:unique 是稳定操作

它不打乱原有顺序。{5,1,1,2,2,1}unique 后变成 {5,1,2,1,2,1}(前 4 位有效),顺序关系完全保留。这点比某些“先排序再去重”的方案更尊重原始语义——比如日志时间序列里,你只想合并连续相同的事件状态,但绝不能让“启动→运行→运行→停止”变成“启动→停止→运行”。


别把它当万能解药:什么时候该换别的工具?

  • 要全局去重(不管是否相邻)→ 用 std::setstd::unordered_set 构建新容器。
  • 要统计频次 → std::mapstd::unordered_map
  • 要保序去重且数据量小 → 手写 vector 遍历 + find,反而更清晰。
  • unique 的真正主场,永远是:你明确知道重复只发生在连续位置,且不想额外开空间、不介意原地修改。日志压缩、协议帧解析、文本行预处理……这些地方它快、轻、无感。

最后一句实在话

std::unique 不是语法糖,它是 C++ 对“局部整理”这件事的精准抽象。它不替你做决定,但给你一把趁手的尺子和剪刀。
用对了,一行代码解决十年老问题;用错了,你会在调试器里盯着那串“怎么还有重复”的输出,怀疑人生。
下次看到它,别急着复制粘贴——先问自己:这些重复,真的挨着吗?我准备好擦屁股(erase)了吗?我需要的,真的是“挤走重复”,而不是“彻底不要重复”?

答案清楚了,代码自然就稳了。

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
验证码
评论列表 (暂无评论,430人围观)

还没有评论,来说两句吧...

目录[+]