【问题标题】:In-place C++ set intersection就地 C++ 设置交集
【发布时间】:2010-12-18 21:56:30
【问题描述】:

在 C++ 中相交两个集合的标准方法是执行以下操作:

std::set<int> set_1;  // With some elements
std::set<int> set_2;  // With some other elements
std::set<int> the_intersection;  // Destination of intersect
std::set_intersection(set_1.begin(), set_1.end(), set_2.begin(), set_2.end(), std::inserter(the_intersection, the_intersection.end()));

我将如何进行就地设置的交叉点?也就是说,我希望 set_1 有调用 set_intersection 的结果。显然,我可以只做一个set_1.swap(the_intersection),但这比就地相交效率要低得多。

【问题讨论】:

    标签: c++ stl set


    【解决方案1】:

    我想我明白了:

    std::set<int>::iterator it1 = set_1.begin();
    std::set<int>::iterator it2 = set_2.begin();
    while ( (it1 != set_1.end()) && (it2 != set_2.end()) ) {
        if (*it1 < *it2) {
            set_1.erase(it1++);
        } else if (*it2 < *it1) {
            ++it2;
        } else { // *it1 == *it2
                ++it1;
                ++it2;
        }
    }
    // Anything left in set_1 from here on did not appear in set_2,
    // so we remove it.
    set_1.erase(it1, set_1.end());
    

    有人发现任何问题吗?两组的大小似乎是 O(n)。根据cplusplus.com,std::set erase(position) 是摊销常数,而 erase(first,last) 是 O(log n)。

    【讨论】:

    • 继续是多余的,我会重新排列为if(*it1&lt;*it2) else if(*it2&lt;*it1) else ...,这样您使用的唯一比较运算符就是小于 - 这就是set 的工作原理。
    • 对!因为它是 if-else if 等。我想会检查以下条件。谢谢,我会编辑答案。
    • set_1.erase(it1++) 对于某些容器(例如向量)是不正确的,即使它在您的情况下是有效的。您应该使用对所有容器都有效的it1 = set_1.erase(it1)
    • 在这种情况下,it1 会正确递增吗?也就是说,set_1.erase(it1) 会增加迭代器吗?
    • set::erase 不会递增迭代器,但许多实现会将递增的迭代器作为返回值。我不认为它是原始 C++ 规范的一部分,例如参见 cplusplus.com/reference/stl/set/erase
    【解决方案2】:

    您可以轻松浏览set_1,检查每个元素以查看它是否存在于set_2 中,如果不存在则将其删除。由于集合是排序的,您可以在线性时间内比较它们,使用迭代器擦除元素是amortized constant time。我不会指望它比你开始时更有效,如果它对你很重要,基准测试将是明智的。

    【讨论】:

    • 在所有方面都足够真实。在我看来,直觉上应该可以一次遍历两个集合并在适当的位置进行交叉。我只是不立即明白怎么做。
    • 对平衡二叉树的单个元素的擦除操作在O(log N)中运行。
    • @ThomasMcLeod 不,它是摊销常数。当我写答案时我不知道,但我现在知道了,我已经更新以反映这一点。
    • 有趣的是标准要求稀疏树平衡。我原以为这将是一个实现细节。
    【解决方案3】:

    这不是直接回答问题,但也许有人觉得这很有帮助。

    std::vector 的情况下,使用标准算法以set_1.begin() 作为输出迭代器是安全的(见下文),而clang/gcc/microsoft 实现会工作。请注意,set_2 可以是任何东西,而不仅仅是 std::vector

    std::vector<int> set_1;  // With some elements
    std::vector<int> set_2;  // With some other elements
    auto end = std::set_intersection(
                         set_1.begin(), set_1.end(), 
                         set_2.begin(), set_2.end(), 
                         set_1.begin() // intersection is written in set_1
                        );
    set_1.erase(end, set_1.end()); // erase redundant elements
    

    更新

    感谢@Keith,他发现 C++ Standard (25.4.5.3) 需要 next:

    The resulting range shall not overlap with either of the original ranges
    

    所以我最初提出的是错误,但在主要的 STL 实现中有效的解决方案。如果您想安全起见并且不想要额外的分配,请将您选择的实现复制到您的代码库并使用它而不是std::set_intersection。我真的不明白这种限制的原因,如果你知道答案,请发表评论。

    【讨论】:

    • 你有参考资料来证明这是安全的吗?我想这样做,但我发现的唯一来源(所有这些都是非权威的)说e.g.“结果范围不能与任何一个输入范围重叠。”
    • 好收获!严格来说你是对的,cppreference 页面明确禁止它。我不知道这个限制的起源是什么。我从可能的实现和接下来的两个事实中得出“安全性”。集合交集的大小不超过set_1 的大小,并且在此过程中没有迭代器无效。据我所知,违反规则的唯一副作用是在过程中自我分配set_1 的元素。
    • cppreference 引用了标准中的段落(25.4.5.3),所以不幸的是不能依赖。一种解决方法是将标准实现复制到您的代码库中,这将保证它。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-02-04
    • 2013-12-09
    • 2021-06-09
    • 1970-01-01
    • 2021-05-24
    • 2019-03-20
    • 2012-04-12
    相关资源
    最近更新 更多