【问题标题】:Can / should I use the ranges library to emulate set functions?我可以/应该使用范围库来模拟集合函数吗?
【发布时间】:2021-02-14 08:01:59
【问题描述】:

我想将view::filter 应用于一个向量,以便找到它与另一个向量的 set_intersection(或 set_difference 等)。

我对这样做的兴趣在于它允许您更改原始容器的特定子集(并且还可以节省 std::set_intersection 附带的一些复制)。

这或多或少是我想做的:

#include <iostream>
#include <vector>
#include <ranges>

int main() {
    std::vector<int> set1 = { 1, 2, 3, 4, 5, 6 },
                     set2 =          { 4, 5, 6, 7, 8, 9 };

    auto intersection = set1 | std::views::filter([&set2](int n)
    { return std::find(set2.cbegin(), set2.cend(), n) != set2.cend(); });

    for (int& v : intersection)
        v *= 2;

    for (auto v : set1)
        std::cout << v << " ";   // prints 1 2 3 8 10 12
}

但是,这段代码看起来效率很低,因为它会多次迭代 set2。使用ranges 库是否有更好的方法来实现这一点?

【问题讨论】:

  • 您可能希望从 set2 构造一个 unordered_set 并将其捕获到您的过滤器 lambda 中。
  • 你知道两个向量都会被排序,如图所示吗?

标签: c++ performance time-complexity c++20 std-ranges


【解决方案1】:

但是,这段代码看起来效率很低,因为它会多次迭代 set2。

它效率低下,而且这种方法通常也不是有效的。在您的示例中,元素都是不同的。但如果不是呢?

std::vector<int> a = { 1, 1, 2, 2, 3 };
std::vector<int> b = { 1, 4, 5 };

这两者的交集应该是{ 1 },但是在b 中查找a 的每个元素的方法会产生{ 1, 1 }。目前其他两个答案也有同样的问题。

最简单的做法是使用 range-v3,它的范围适配器比 C++20 多得多。特别是,它有一个名为views::set_intersection

namespace rv = ranges::views;

int main() {
    std::vector<int> a = {1, 1, 2, 2, 3};
    std::vector<int> b = {1, 3, 4, 5};

    std::cout << rv::set_intersection(a, b) << '\n'; // prints [1,3]
}

然后您可以根据需要使用它来更改原始容器:

namespace rv = ranges::views;

int main() {
    std::vector<int> a = {1, 1, 2, 2, 3};
    std::vector<int> b = {1, 3, 4, 5};

    for (int& i : rv::set_intersection(a, b)) {
        i *= 2;
    }

    std::cout << rv::all(a) << '\n'; // prints [2,1,2,2,6]
}

请注意,只有第一个 1 是双倍的。

【讨论】:

  • 这是我一直在寻找的答案。我假设您正在谈论这个库:ericniebler.github.io/range-v3。它与 C++20 中的范围库有什么关系?
  • @LiamGoodacre 这就是 C++20 Ranges 库的来源。
【解决方案2】:

假设在问题中,“到向量”、“向量的”和“std::set_intersection”被认为是重要的,我想我们只想处理向量(不将元素转移到另一个容器),它们是已排序(根据std::set_intersection() 的要求)。

我会使用可变闭包来将其内部状态从一次迭代更改为另一次迭代。 然后,我们可以使用与possible implementation of std::set_intersection 相同的技巧。 由于我们总是在set2 中前进(虽然元素被认为太小)并且从不回头,所以复杂度与N1×N2 不同,而是N1+N2 (如the documentation 中所述)。

    auto intersection = set1 | std::views::filter(
      [b2=cbegin(set2), e2=cend(set2)](int n) mutable
      {
        while((b2!=e2)&&(*b2<n))
        {
          advance(b2, 1);
        }
        return (b2!=e2)&&(*b2==n);
      });

【讨论】:

    【解决方案3】:

    为什么不直接将set2 的值集合到实际的std::set 中,这样检查set1 的元素是否在set2 中更有效?

    这样做您可以简单地转换set1,方法是返回未在set2 中的值并更改那些值:

    #include <iostream>
    #include <algorithm>
    #include <vector>
    #include <ranges>
    #include <set>
    
    int main() {
        std::vector<int> set1 = { 1, 2, 3, 4, 5, 6 };
        std::set<int>    set2 =          { 4, 5, 6, 7, 8, 9 };
        auto set1edit = set1 | std::ranges::views::transform( // O(set1.size())
                [&set2 = std::as_const(set2)](auto const& x){
                if (set2.find(x) != set2.end()) // O(log(set2.size()))
                    return x*2;
                else
                    return x;
                });
    
        for (int const& v : set1edit)
            std::cout << v << " ";   // prints 1 2 3 8 10 12
    }
    

    请注意,我已经强调了 transform 的复杂性是 O(set1.size()),因为您处理了 set1 中的每个 set1.size() 元素;将set1 设置为std::set 甚至没有意义,因为如果我们必须循环所有元素,std::vector 就可以了。

    另一方面,使用std::set&lt;&gt;::find 保证比使用std::find 更好的性能。

    正如评论所暗示的,如果您知道两个向量都已排序,那么一切都可以变得更容易,但我觉得您示例中的排序向量只是您尝试“过度”简化的结果.

    【讨论】:

      猜你喜欢
      • 2011-04-13
      • 2011-08-09
      • 2014-03-30
      • 2011-04-12
      • 2019-01-10
      • 2010-10-10
      • 2010-09-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多