【问题标题】:Efficient intersection of two sets两组的有效交集
【发布时间】:2018-05-09 11:54:00
【问题描述】:

我有两个集合(或地图),需要有效处理它们的交集。 我知道有两种方法可以做到这一点:

  • 像 std::set_intersection 一样遍历两个映射:O(n1+n2)
  • 遍历一张地图并在另一张地图中查找元素:O(n1*log(n2))

根据大小,这两个解决方案中的任何一个都明显更好(已计时),因此我需要根据大小在这些算法之间切换(这有点混乱) - 或者找到一个优于两者的解决方案,例如使用 map.find() 的一些变体,将前一个迭代器作为提示(类似于 map.emplace_hint(...)) - 但我找不到这样的函数。

问题:是否可以直接使用 STL 或某些兼容库来结合两种解决方案的性能特征?

请注意,性能要求使这与之前的问题有所不同,例如 Efficient intersection of sets?

【问题讨论】:

  • 什么“性能要求”使这与链接的问题不同?你只是说你需要它高效,而另一个问题是要求高效地做到这一点......
  • 性能要求因调用而异,因此我无法静态选择一种替代方法。链接的问题中未解决该部分。
  • 在这个优化级别(不仅仅是使用标准库),我们确实需要查看示例数据和基准。一旦获得实际数据、编译器和硬件,您就可以随时进行更多优化。如果没有这些信息,这个问题实际上与链接的问题并没有太大的不同,即使它表示愿意根据手头的情况切换方法(标准库可能已经这样做了)。
  • @wally 标准 set_intersection 的实现可能会切换方法,但是否有任何实现可以做到这一点?如果有,怎么做?
  • if ((n1+n2) < n1*log2(n2)) 怎么样,然后选择更好的? (当然还需要考虑n2*log2(n1)))。顺便说一句,这不是一个不同的要求,它只是询问在一般情况下如何有效地做到这一点。很抱歉吹毛求疵,只是想更好地理解你的真正意思,尽管我仍然觉得这很接近被骗

标签: c++ algorithm data-structures


【解决方案1】:

几乎在所有情况下std::set_intersection 都是最佳选择。 仅当集合包含非常少量的元素时,另一种解决方案可能会更好。 由于原木的性质以二为底。 比例为:

n = 2,log(n)= 1
n = 4,log(n)= 2
n = 8,log(n)= 3
.....
n = 1024 log(n) = 10

如果集合的长度超过 5-10 个元素,则 O(n1*log(n2) 比 O(n1 + n2) 复杂得多。

这样的功能被添加到 STL 中是有原因的,并且它是这样实现的。它还将使代码更具可读性。

对于长度小于 20 的集合,选择排序比合并或快速排序更快,但很少使用。

【讨论】:

  • 首先是有 5 个元素和 >1000 个元素的情况,所以它发生了。不过,我不同意这个结论。对于选择排序与快速排序,它取决于算法的复杂性。我没有测试 ++ 与在地图中查找的相对性能 - 但它们看起来更相似。如果 (n1+n2) 和 n2*log2(n1) 的常数相同,则 n1=1000 得到 n2=111,n1=10 000 得到 n2=813。
  • 我提到了选择排序,因为它的复杂度为 O(n^2),但它比快速排序或合并排序对于少量元素的性能更好。但是,如果集合的大小相等,那么对于具有 10 个元素的集合,您将有 10 + 10 个操作或 10*3.32(log(10) 操作 - 超过 30 个。find 更快的唯一可能方法是当您的集合少于 4 个元素时,log(n) 将小于 2。
  • @PetarVelev 这些集合的长度可能非常不同。如果我们有一组 1024 和另一组 100 个元素,它是 1024+100=1124 与 100*log2(1024)=1000 - 我发现 > 1000 个元素的情况不仅与
  • @HansOlsson 您正在将苹果与橙子进行比较。 std::set_intersection 指定为“最多 2 * (N1+N2-1) 次比较”,而 std::set::find 指定为 O(log(size())) .对于一个您有 严格计数 的操作,另一个是渐近界
  • @Caleth 从理论上讲,保证是不同的,但实际上 set_intersection 实际上使用了很多比较,并且 set::find 有一个小常数,对于小集合没有重大开销,即它是
【解决方案2】:

对于实现为二叉树的集合,实际上一种算法,它结合了您提到的两个过程的优点。本质上,您可以像 std::set_intersection 那样进行合并,但是在一棵树中迭代时,您会跳过所有小于另一棵树中当前值的所有分支。

生成的交集需要 O(min(n1 log n2, n2 log n1, n1 + n2),这正是您想要的。

不幸的是,我很确定 std::set 不提供可以支持此操作的接口。

我过去做过几次,在加入倒排索引和类似的事情时。通常我会使用 skipTo(x) 操作来制作迭代器,该操作将前进到下一个元素 >= x。为了满足我承诺的复杂性,它必须能够在 log(N) 摊销时间内跳过 N 个元素。然后一个交叉点是这样的:

void get_intersection(vector<T> *dest, const set<T> set1, const set<T> set2)
{
    auto end1 = set1.end();
    auto end2 = set2.end();
    auto it1 = set1.begin();
    if (it1 == end1)
        return;
    auto it2 = set2.begin();
    if (it2 == end2)
        return;
    for (;;)
    {
        it1.skipTo(*it2);
        if (it1 == end1)
            break;
        if (*it1 == *it2)
        {
            dest->push_back(*it1);
            ++it1;
        }
        it2.skipTo(*it1);
        if (it2 == end2)
            break;
        if (*it2 == *it1)
        {
            dest->push_back(*it2);
            ++it2;
        }
    }
}

它可以使用迭代器向量轻松扩展到任意数量的集合,并且几乎任何有序集合都可以扩展以提供所需的迭代器——排序数组、二叉树、b-树、跳过列表等。

【讨论】:

  • 好的,这似乎就是我想要的。你有任何参考吗?是否有 boost 或类似的实现?
  • 不幸的是,我认为您必须自己动手。密钥通常是一个具有高效跳过直到操作的集合。如果您看到一个,我将更新答案以描述它是如何实现的。
【解决方案3】:

我不知道如何使用标准库来做到这一点,但如果你编写了自己的平衡二叉搜索树,这里是如何实现有限的“带提示查找”。 (根据您的其他要求,BST 重新实现也可以省略父指针,这可能会在性能上胜过 STL。)

假设提示值小于要找到的值,并且我们知道提示节点所属的左子树的提示节点的祖先堆栈。首先在提示节点的右子树中正常搜索,根据需要将节点推入堆栈(为下次提示准备)。如果这不起作用,则当堆栈顶部节点的值小于查询值时,弹出堆栈。从弹出的最后一个节点开始搜索(如果有),根据需要推送。

我声称,当使用这种机制以升序连续搜索值时,(1)每个树的边缘最多遍历一次,并且(2)每个查找最多遍历两个下降路径的边缘。给定具有 n2 个节点的二叉树中的 2*n1 条下降路径,边的成本为 O(n1 log n2)。也是 O(n2),因为每条边都遍历一次。

【讨论】:

    【解决方案4】:

    关于性能要求,O(n1 + n2) 在大多数情况下是一个非常好的复杂度,因此只有在紧密循环中进行此计算时才值得考虑。

    如果你真的需要它,组合方法也不错,也许是这样的?

    伪代码:

    x' = set_with_min_length([x, y])
    y' = set_with_max_length([x, y])
    if (x'.length * log(y'.length)) <= (x'.length + y'.length):
         return iterate_over_map_find_elements_in_other(y', x')
    
    return std::set_intersection(x, y)
    

    我认为您不会找到一种算法能够克服这些复杂性中的任何一个,但很高兴被证明是错误的。

    【讨论】:

    • @Caleth 呃.. 现在我明白了,这里太热了;)我完全忽略了前两行。
    猜你喜欢
    • 1970-01-01
    • 2011-01-24
    • 2011-12-23
    • 2011-11-26
    • 1970-01-01
    • 1970-01-01
    • 2013-07-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多