矢量排序/唯一/擦除与复制到unordered_set的性能

时间:2013-07-14 14:08:39

标签: c++ c++11 vector stl duplicates

我有一个函数可以将网格中一个点列表的所有邻居输出到一定距离,这涉及很多重复项(我的邻居的邻居==我再次)。

我一直在尝试几种不同的解决方案,但我不知道哪种解决方案效率更高。下面是一些代码,演示了两个并排运行的解决方案,一个使用std :: vector sort-unique-erase,另一个使用std :: copy到std :: unordered_set。

我还尝试了另一个解决方案,即将包含邻居的向量传递给邻居函数,该函数将使用std :: find来确保在添加邻居之前不存在邻居。

所以有三个解决方案,但我无法完全理解哪个会更快。任何人的想法?

代码段如下:

// Vector of all neighbours of all modified phi points, which may initially include duplicates.
std::vector<VecDi> aneighs;
// Hash function, mapping points to their norm distance.
auto hasher = [&] (const VecDi& a) {
    return std::hash<UINT>()(a.squaredNorm() >> 2);
};
// Unordered set for storing neighbours without duplication.
std::unordered_set<VecDi, UINT (*) (const VecDi& a)> sneighs(phi.dims().squaredNorm() >> 2, hasher);

... compute big long list of points including many duplicates ...

// Insert neighbours into unordered_set to remove duplicates.
std::copy(aneighs.begin(), aneighs.end(), std::inserter(sneighs, sneighs.end()));

// De-dupe neighbours list.
// TODO: is this method faster or slower than unordered_set?
std::sort(aneighs.begin(), aneighs.end(), [&] (const VecDi& a, const VecDi&b) {
    const UINT aidx = Grid<VecDi, D>::index(a, phi.dims(), phi.offset());
    const UINT bidx = Grid<VecDi, D>::index(b, phi.dims(), phi.offset());
    return aidx < bidx;
});
aneighs.erase(std::unique(aneighs.begin(), aneighs.end()), aneighs.end());

2 个答案:

答案 0 :(得分:3)

这里很大程度上取决于输出集的大小(反过来,这将取决于您采样的邻居的距离)。

如果它很小,(不超过几十个项目),使用std::vectorstd::find的手动滚动设置实施可能会保持相当的竞争力。它的问题在于它是一个O(N 2 )算法 - 每次插入一个项目时,你必须搜索所有现有项目,因此每次插入都是已经存在的项目数量的线性组。因此,随着集合变大,其插入项目的时间大致呈二次方式增长。

使用std::set您每次插入只需进行大约log 2 (N)比较而不是N比较。这降低了从O(N 2 )到O(N log N)的整体复杂性。主要的缺点是它(至少通常)是作为由单独分配的节点构建的树实现的。这通常会降低其引用的位置 - 即,您插入的每个项目将包含数据本身和一些指针,遍历树意味着跟随指针。由于它们是单独分配的,因此很可能在树中(当前)相邻的节点在内存中不会相邻,因此您将看到相当多的缓存未命中。底线:虽然随着项目数量的增加,其速度增长相当慢,但所涉及的常量相当大 - 对于少量项目,它将开始相当慢(通常比你的手动版本慢一点)。

使用vector / sort / unique结合了前面每个的一些优点。将项目存储在向量中(没有针对每个项目的额外指针)通常会导致更好的缓存使用 - 相邻索引处的项目也位于相邻的内存位置,因此当您插入新项目时,新项目的位置可能会已经在缓存中了。主要的缺点是,如果你正在处理一个非常大的集合,这可能会使用更多的内存。当一个集合在插入每个项目时消除重复项目(即,只有当项目与集合中已有的项目不同时才会插入项目)这将插入所有项目,然后在最后删除所有项目重复。考虑到当前的内存可用性以及我可能正在访问的邻居数量,我怀疑这在实践中是一个主要的缺点,但在错误的情况下,它可能会导致严重的问题 - - 几乎任何虚拟内存的使用几乎肯定会使它成为净损失。

从复杂性的角度来看最后一个,它是O(N log N),有点像集合。不同之处在于,设置它实际上更像是O(N log M),其中N是邻居的总数,M是唯一邻居的数量。使用向量,它实际上是O(N log N),其中N(再次)是邻居的总数。因此,如果重复数量非常大,则一组可能具有显着的算法优势。

也可以在纯线性序列中实现类似集合的结构。这保留了集合仅存储唯一项目的优势,但也保留了向量的参考优势的局部性。我们的想法是将大部分当前集合排序,因此您可以在日志(N)复杂度中进行搜索。但是,当您插入新项时,只需将其放在单独的向量(或现有向量的未排序部分)中。当您执行新插入时,您还会对这些未排序的项目执行线性搜索。

当未分类的部分变得太大时(对于某些“太大”的定义),您对这些项进行排序并将它们合并到主组中,然后再次启动相同的序列。如果根据“log N”(其中N是已排序组中的项目数)定义“太大”,则可以保留整个数据结构的O(N log N)复杂度。当我玩它时,我发现未分类的部分可能比我预期的要大,但它开始引起问题。

答案 1 :(得分:1)

未排序的集合具有恒定的时间复杂度o(1)用于插入(平均),因此操作将是o(n)其中n是数字是移除前的元素。

对大小为n的元素列表进行排序是o(n log n),遍历列表以删除重复项是o(n)。 o(n log n)+ o(n)= o(n log n)

未排序的集合(与性能中的散列表类似)更好。

有关未分类设置时间的数据: http://en.cppreference.com/w/cpp/container/unordered_set