原理概述
std::unique 的核心思想
在 STL 中,std::unique 的核心思想是对连续重复元素进行“就地压缩”,仅保留每组相邻重复中的一个副本并将它们向前移动到一起。该操作返回一个新结束迭代器,指向容器中还保留元素的末尾位置。通过这种方式,无需额外分配大量内存就能完成去重过程。
简而言之,std::unique 关注的是相邻重复,而不是全局重复;如果容器中存在非相邻重复元素,仍然需要在排序或额外处理后才能实现全局去重。该行为是算法设计的一个关键点,也是许多初学者容易误解的地方。
为了真正将重复的元素从容器中移除,通常需要与 erase 组合使用:v.erase(std::unique(v.begin(), v.end()), v.end()),其中 std::unique 返回的新结束迭代器指示需要擦除的区域起始位置。
STL 实现细节与设计要点
内部实现要点
ForwardIt 要求至少具备前向迭代能力,因而算法需要从头到尾逐步比较相邻元素并在必要时向前移动数据。该设计确保在大多数实现中拥有线性时间复杂度和常数额外空间。
在实现上,算法会维护一个“保留区”指针,逐步将不相等的元素向保留区移动;若当前元素与前一个保留的元素不相等,则将其放到保留区的下一个位置。整个过程的核心是使用移动语义来避免不必要的拷贝。
template<class ForwardIt>
ForwardIt unique(ForwardIt first, ForwardIt last) {if (first == last) return last;ForwardIt result = first;++first;for (; first != last; ++first) {if (!(*first == *result)) {++result;*result = std::move(*first);}}return ++result;
}
对于支持自定义等价关系的版本,还有一个带谓词的重载:bool pred(const T&, const T&),用来判断两元素是否被视为相同。该重载的实现原理与无谓词版本一致,只是比较逻辑由默认的相等比较改为谓词结果。
标准实现要点与注意事项
在标准库实现中,对谓词的支持意味着你可以传入一个自定义的比较函数,以实现对某些自定义类型的相等性定义。为了确保正确性,谓词应当满足等价关系的性质,否则可能导致不可预期的行为。
另外,返回的新结束迭代器并不代表容器“被清空”,它只是指示了去重后仍然需要保留的前段元素的边界。若要真正从容器中移除多余元素,仍需结合 erase 使用。

典型实现与示例
以下示例展示了标准实现的典型用法以及对自定义谓词的支持,包括移动语义的应用。请注意,示例仅用于理解算法工作原理,实际的标准库实现可能在细节上有所不同。
#include <vector>
#include <algorithm>
#include <iostream>int main() {std::vector v{1,1,2,3,3,4,4,5};// 无谓词版本auto it = std::unique(v.begin(), v.end());v.erase(it, v.end());for (int x : v) std::cout << x << ' ';// 输出: 1 2 3 4 5
}
正确使用 std::unique 的日常方法
与 erase 组合的典型用法
在实际编码中,最常见的模式是将 std::unique 与 erase 组合,以实现就地去重并真正从容器中移除多余元素。该组合在 vector、string、deque 等序列容器中都非常常用。这是一个标准且高效的做法,广泛应用于数据清洗和去重场景。
通过下述模式,可以确保在保留字节的同时实现极简代码:erase-掉尾部重复区域,同时避免额外的拷贝开销。
#include <vector>
#include <algorithm>
#include <iostream>int main() {std::vector v{1,1,2,2,3,4,4,5,5};auto it = std::unique(v.begin(), v.end());v.erase(it, v.end());for (int x : v) std::cout << x << ' ';
}
自定义谓词的去重行为
使用自定义等价关系
除了默认的相等比较,std::unique 还提供带谓词的版本,谓词用于定义两元素是否被视为“相等”。在某些场景下,按照自定义规则处理相邻元素可以实现更灵活的去重策略。
一个常见的应用是对字符串进行忽略大小写的相等比较,或对自定义结构体根据某些字段进行相等性判断。要使用谓词版本,需要提供一个签名为 bool pred(const T&, const T&) 的函数对象或 lambda。
#include <string>
#include <vector>
#include <algorithm>
#include <iostream>bool case_insensitive_equal(const std::string& a, const std::string& b) {if (a.size() != b.size()) return false;for (size_t i = 0; i < a.size(); ++i)if (std::tolower((unsigned char)a[i]) != std::tolower((unsigned char)b[i]))return false;return true;
}int main() {std::vector<std::string> v{"Ab","aB","AC","ad","Ad"};auto it = std::unique(v.begin(), v.end(), case_insensitive_equal);v.erase(it, v.end());for (const auto& s : v) std::cout << s << ' ';
}
边界条件与注意点
空区间与单元素情况
当区间为空或仅包含一个元素时,函数会直接返回,不会对容器做任何修改;这是一种对输入边界友好的实现特性。
关于非相邻重复的处理,请记住:std::unique 仅处理相邻重复,若要消除全局重复,通常需要先对区间进行排序或分组后再应用 unique。
在实际工程中,常见的做法是先对数据进行排序,再调用 std::unique,并最终使用 erase 以获得干净的、无重复的序列。
性能要点与最佳实践
避免不必要的拷贝与提升效率
使用移动语义在大对象中尤为重要,可以将昂贵的拷贝成本降至最低,从而提升大数据量场景下的性能。
若要实现不同数据结构的最优行为,应确保区间迭代器在理论上符合前向迭代器要求,并利用编译器优化对谓词进行内联。 正确的容器选择和访问模式对性能影响显著。
常见误区与兼容性提示
误区:unique 会删除所有重复元素
很多开发者误以为 std::unique 会删除任意位置的重复元素;实际上它只去除了相邻重复,需要结合 erase 调用才能从容器中彻底移除多余元素。
误区二:谓词版本会自动进行排序或全局去重;事实是谓词仅影响相等判定,前后的元素仍需遵循相同的顺序关系,若要实现全局去重通常需要额外的排序或分组步骤。


