什么是删除重复项和排序向量的最有效方法?

372

我需要对一个可能有很多元素的C++向量进行去重并排序。

我目前有下面的代码,但它不起作用。

vec.erase(
      std::unique(vec.begin(), vec.end()),
      vec.end());
std::sort(vec.begin(), vec.end());

如何正确地执行此操作?

另外,先删除重复项(类似上面的代码)还是先进行排序更快?如果我先进行排序,执行std::unique后是否保证仍然排序?

或者还有其他(可能更有效的)方法来完成所有这些操作吗?


4
我假设您没有在插入之前进行检查以避免首先出现重复项的选项? - Joe
没错,那将是理想的。 - Kyle Ryan
44
建议对上面的代码进行纠正,或者明确指出它是错误的。std::unique假定范围已经排序。 - Matthieu M.
2
使用集合代替 - Ivan
你必须先使用sort,然后再使用erase+unique。 - user1438233
26个回答

733

我同意R. PateTodd Gardner的看法: 在这里使用 std::set 可能是个好主意。即使你被迫使用向量(vector),如果有足够多的重复项,创建一个集合(set)来完成繁重的工作可能会更有效。

现在我们比较三种方法:

只使用向量(vector),sort + unique

sort( vec.begin(), vec.end() );
vec.erase( unique( vec.begin(), vec.end() ), vec.end() );

手动转换为集合

set<int> s;
unsigned size = vec.size();
for( unsigned i = 0; i < size; ++i ) s.insert( vec[i] );
vec.assign( s.begin(), s.end() );

转换为集合(使用构造函数)

set<int> s( vec.begin(), vec.end() );
vec.assign( s.begin(), s.end() );

以下是这些方法在重复项数量变化时的性能表现:

vector和set方法的比较

概括:当重复项的数量足够大时,将数据转换为集合再转回向量实际上更快

出于某种原因,在使用我使用的随机数据时,手动执行集合转换似乎比使用集合构造函数更快。


79
我很震惊发现构造函数的方法表现一直比手动方法差得可测。除了一些微小的额外开销之外,它本来应该和手动操作一样。有人能解释一下吗? - Ari
24
好的,谢谢您提供图表。您能否说明“Number of Duplicates”所表示的单位是什么?(也就是说,“足够大”大约是多少)? - Kyle Ryan
8
@Kyle:这相当大。我在这张图中使用了1到1000之间、100个和10个随机抽取的整数数据集,每个数据集包含1,000,000个整数。 - Nate Kohl
8
我认为你的结果是错误的。在我的测试中,元素重复越多,向量(比较)的速度越快,实际上是相反的情况。你是否开启了优化并关闭了运行时检查?在我的测试中,向量始终更快,取决于重复数目,最高可达100倍。使用VS2013,cl /Ox -D_SECURE_SCL=0进行编译。 - davidnr
62
X轴的说明似乎缺失了。 - BartoszKP
显示剩余19条评论

116

我重新做了Nate Kohl的分析并得到不同的结果。对于我的测试案例,直接对向量进行排序始终比使用集合更有效率。我添加了一个新的更高效的方法,使用 unordered_set

请记住,unordered_set 方法仅在您拥有适用于需要去重和排序的类型的良好哈希函数时才有效。对于整数,这很容易!(标准库提供了一个默认哈希,即恒等函数。)此外,不要忘记在最后进行排序,因为无序集合是无序的:)

我深入研究了 setunordered_set 的实现,并发现构造函数实际上会为每个元素构造一个新的节点,然后检查其值以确定是否应将其插入(至少在 Visual Studio 实现中如此)。

以下是5种方法:

f1: 只使用 vectorsort + unique

sort( vec.begin(), vec.end() );
vec.erase( unique( vec.begin(), vec.end() ), vec.end() );

f2: 转换为set(使用构造函数)

set<int> s( vec.begin(), vec.end() );
vec.assign( s.begin(), s.end() );

f3: 手动转换为 set 类型

set<int> s;
for (int i : vec)
    s.insert(i);
vec.assign( s.begin(), s.end() );

f4:转换为unordered_set(使用构造函数)

unordered_set<int> s( vec.begin(), vec.end() );
vec.assign( s.begin(), s.end() );
sort( vec.begin(), vec.end() );

f5: 将代码手动转换为unordered_set

unordered_set<int> s;
for (int i : vec)
    s.insert(i);
vec.assign( s.begin(), s.end() );
sort( vec.begin(), vec.end() );

我用一个包含1亿个int类型的向量进行了测试,这些整数是在区间[1,10]、[1,1000]和[1,100000]中随机选择的。

结果如下(时间以秒为单位,数字越小表示速度越快):

range         f1       f2       f3       f4      f5
[1,10]      1.6821   7.6804   2.8232   6.2634  0.7980
[1,1000]    5.0773  13.3658   8.2235   7.6884  1.9861
[1,100000]  8.7955  32.1148  26.5485  13.3278  3.9822

4
对于整数,可以使用基数排序,它比std::sort快得多。 - Changming Sun
3
快速提示,要使用sortunique方法,您必须#include <algorithm> - Davmrtl
4
@ChangmingSun 我想知道为什么优化器似乎在 f4 上失败了?与 f5 相比,数字差别很大。这对我来说没有任何意义。 - sandthorn
1
@sandthorn 如我在答案中所解释的那样,该实现为输入序列中的每个元素构建一个节点(包括动态分配),这对于最终成为重复值的每个值来说都是浪费的。优化器无法知道它可以跳过它。 - alexk7
2
有趣的是,使用手动转换的 f5 运行速度比使用构造函数的 f4 快得多! - galactica
显示剩余5条评论

76

std::unique只会移除相邻的重复元素: 你需要先对向量进行排序,才能按照你的意图运行它。

std::unique被定义为稳定的,因此在对其运行 unique 后,向量仍将保持排序状态。


44

我不确定您将用它用于什么,因此我不能以100%的确切性说出,但通常当我想到“排序、唯一”的容器时,我会想到std::set。它可能更适合您的用例:

std::set<Foo> foos(vec.begin(), vec.end()); // both sorted & unique already
否则,按照其他回答所指出的,在调用unique之前进行排序是正确的方法。

很到点子!std::set被指定为一个排序的不重复集合。大多数实现使用高效的有序二叉树或类似的结构。 - notnoop
1
是的,参见23.1.4.9,“关联容器迭代器的基本属性是它们按键的非降序遍历容器,其中非降序由用于构造它们的比较定义”。 - Todd Gardner
1
@MadCoder:一个集合被实现成有序的并不一定“合理”。实际上,也有使用哈希表实现的无序集合。事实上,大多数人在可用时更喜欢使用哈希表。但是,在C++中,命名约定恰好是有序关联容器被简单地命名为“set”/“map”(类似于Java中的TreeSet / TreeMap);而散列关联容器则被称为“hash_set”/“hash_map”(SGI STL)或“unordered_set”/“unordered_map”(TR1)(类似于Java中的HashSet和HashMap)。 - newacct
@Todd 请问有人可以确认或否认以下陈述吗?如果您想要稳定且满足n.log(n)的需求,请选择std::set(或从头开始编写代码,实现红黑树)。如果您可以接受不稳定性并且满足log(n)的需求,请选择std::unordered_set(或从头开始编写代码,实现哈希表)。请注意,n.log(n)和log(n)与我的陈述中的计算有关,并且我假设有足够的空间。 - qqqqq
@qqqqq,“稳定”和“不稳定”与唯一集合无关。 set和unordered_set之间的区别在于排序;如果您需要按排序顺序访问元素,则使用set。 如果排序对您不重要,请使用unordered_set(或对于自定义类型,您的决策可能受实现哪些运算符的驱动)。 如果您需要平均低延迟查找和排序遍历,请同时使用两者。 我不确定您所说的第二个log(n)是什么意思。 构建一个set是nlog(n),但是unordered_set平均为n。 此外,构建通常比查找/插入/删除不太相关。 - Todd Gardner
显示剩余2条评论

24

std::unique 只适用于连续重复的元素,因此最好先进行排序。然而,它是稳定的,所以你的向量将保持已排序状态。


23

这是一个为你解决问题的模板:

template<typename T>
void removeDuplicates(std::vector<T>& vec)
{
    std::sort(vec.begin(), vec.end());
    vec.erase(std::unique(vec.begin(), vec.end()), vec.end());
}

按以下方式调用:

removeDuplicates<int>(vectorname);

2
+1 进行模板化处理吧! - 但是你可以只写 removeDuplicates(vec),而不需要显式地指定模板参数。 - Faisal Vali
10
甚至更好的是,直接使用模板迭代器(begin 和 end),这样可以在除了 vector 之外的其他结构上运行它。 - Kyle Ryan
模板编程,太棒了!对于小型列表的快速修复,完全采用STL风格。+1感谢。 - QuantumKarl
@Kyle - 仅适用于具有 erase() 方法的其他容器,否则您必须返回新的结束迭代器,并让调用代码截断容器。 - Toby Speight
这是危险的。它有一个副作用,即排序,但从名称上并不明显。 - Jeffrey Faust
这是危险的。它有一个副作用,即排序,这个副作用从名称上并不明显。 - undefined

11
如果您不想更改元素的顺序,则可以尝试此解决方案:
template <class T>
void RemoveDuplicatesInVector(std::vector<T> & vec)
{
    set<T> values;
    vec.erase(std::remove_if(vec.begin(), vec.end(), [&](const T & value) { return !values.insert(value).second; }), vec.end());
}

1
也许可以使用unordered_set代替set(如果有boost :: remove_erase_if,则使用它) - gast128
不确定这是否有效。std::ranges::remove_ifpred受到std::indirect_binary_predicate的限制,该限制包含了std::predicate,其组件regular_invocable“_要求[它...]是保持相等性的_” - &根据values等状态,使其不能保持相等性。我认为旧的remove_if也是如此。 - underscore_d

9
假设 a 是一个向量,使用 a.erase(unique(a.begin(),a.end()),a.end()); 可以删除其中任何连续的重复元素,该操作的时间复杂度为O(n)

3
连续的重复元素。好的,首先需要进行 std::sort 排序。 - v.oddou

9

效率是一个复杂的概念。存在时间与空间考虑,以及一般测量(其中您只会得到模糊的答案,例如O(n))与具体测量(例如,根据输入特征,冒泡排序可能比快排快得多)。

如果您有相对较少的重复项,则排序后跟随唯一和擦除似乎是正确的选择。如果您有相对较多的重复项,则从向量创建集并让其执行操作可以轻松击败它。

不要只专注于时间效率。排序+唯一+擦除在O(1)空间中运行,而集合构建在O(n)空间中运行。两者都不直接适用于映射-归约并行化(针对真正巨大的数据集)。


你需要什么才能拥有map/reduce的能力?我唯一能想到的是分布式归并排序,但在最终合并中仍然只能使用一个线程。 - Zan Lynx
1
是的,您必须有一个控制节点/线程。但是,您可以根据需要将问题划分为多个部分,以限制控制/父线程处理的工作/子线程数量和每个叶节点必须处理的数据集大小。并非所有问题都可以轻松地使用map-reduce解决,我只是想指出有些人处理类似(表面上)的优化问题,其中处理10TB的数据被称为“星期二”。 - Roger Pate

7
在调用 unique 之前需要对它进行排序,因为 unique 只会移除相邻的重复项。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接