【问题标题】:How to find the intersection of two STL sets?如何在 C++ 中找到两个 std::set 的交集?
【发布时间】:2012-11-07 00:52:14
【问题描述】:

我一直试图在 C++ 中找到两个 std::set 之间的交集,但我一直收到错误。

我为此创建了一个小样本测试

#include <iostream>
#include <vector>
#include <algorithm>
#include <set>
using namespace std;

int main() {
  set<int> s1;
  set<int> s2;

  s1.insert(1);
  s1.insert(2);
  s1.insert(3);
  s1.insert(4);

  s2.insert(1);
  s2.insert(6);
  s2.insert(3);
  s2.insert(0);

  set_intersection(s1.begin(),s1.end(),s2.begin(),s2.end());
  return 0;
}

后一个程序不会产生任何输出,但我希望有一个新的集合(我们称之为s3),其值如下:

s3 = [ 1 , 3 ]

相反,我得到了错误:

test.cpp: In function ‘int main()’:
test.cpp:19: error: no matching function for call to ‘set_intersection(std::_Rb_tree_const_iterator<int>, std::_Rb_tree_const_iterator<int>, std::_Rb_tree_const_iterator<int>, std::_Rb_tree_const_iterator<int>)’

我从这个错误中了解到,set_intersection 中没有接受Rb_tree_const_iterator&lt;int&gt; 作为参数的定义。

另外,我想std::set.begin() 方法返回的就是这种类型的对象,

有没有更好的方法在 C++ 中找到两个 std::set 的交集?最好是内置函数?

非常感谢!

【问题讨论】:

  • “我希望有一个新的集合(我们称之为 s3)” 但你没有,你也没有。我不明白你期望结果去哪里。此外,您没有阅读文档以找出要传递的参数。

标签: c++ std stl-algorithm stdset


【解决方案1】:

您尚未为set_intersection 提供输出迭代器

template <class InputIterator1, class InputIterator2, class OutputIterator>
OutputIterator set_intersection ( InputIterator1 first1, InputIterator1 last1,
                                  InputIterator2 first2, InputIterator2 last2,
                                  OutputIterator result );

通过做类似的事情来解决这个问题

...;
set<int> intersect;
set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(),
                 std::inserter(intersect, intersect.begin()));

您需要一个 std::insert 迭代器,因为该集合现在是空的。我们不能使用std::back_inserterstd::front_inserter,因为 set 不支持这些操作。

【讨论】:

  • 我想了解为什么对集合进行如此基本的操作需要如此神秘冗长的咒语。为什么不是一个简单的set&lt;T&gt;&amp; set::isect(set&lt;T&gt;&amp;) 方法,这需要吗? (我要求set&lt;T&gt;&amp; set::operator^(set&lt;T&gt;&amp;),但这可能是一座太远的桥梁。)
  • @RyanV.Bissell 这与&lt;algorithm&gt; 中的几乎所有算法的设计相似,因此如果没有别的,那就是一致性。我认为,这种风格也为您提供了灵活性。并允许将算法与多个容器一起使用,尽管这可能不会在这里发生。而且您的签名可能不起作用,您可能需要返回一个值。在复制语义之前的日子里,我认为这是一个双重副本。我已经有一段时间没有做过 c++了,所以用一小撮或 3 点盐就可以了
  • 我仍然认为自己是一个 STL 新手,所以盐粒的应用也适用。我的评论编辑窗口已过期,因此我无法更正按引用返回的错误。我的评论不是对一致性的抱怨,而是一个诚实的问题,即为什么这种语法一定尝起来如此苦涩。也许我应该把它作为一个 SO 问题。
  • 实际上,大部分 C++ 标准库都是以这种晦涩难懂的方式设计的。虽然设计的优雅是显而易见的(w.r.t 通用性,但不仅如此),API 的复杂性具有毁灭性的影响(主要是因为人们不断地重新发明轮子,因为他们无法使用编译器附带的轮子)。在另一个世界里,设计师会因为偏爱他们的乐趣而不是用户的乐趣而受到打击。在这个世界上……好吧,至少我们有 StackOverflow。
  • 这是一个“通用语法”——你也可以在一个向量和一个列表上做 set_intersection 并将结果存储到一个双端队列中,你应该能够有效地做这件事(当然,在调用它之前注意对两个源容器进行排序是您的问题)。我不觉得这很糟糕,我唯一遇到的问题是set 容器的方法也可能与另一组相交。传递容器而不是 .begin() - .end() 的主题是另一回事 - 一旦 C++ 有了概念,这将得到修复。
【解决方案2】:

查看链接中的示例: http://en.cppreference.com/w/cpp/algorithm/set_intersection

您需要另一个容器来存储交叉点数据,下面的代码假设可以工作:

std::vector<int> common_data;
set_intersection(s1.begin(),s1.end(),s2.begin(),s2.end(), std::back_inserter(common_data));

【讨论】:

  • back_inserter 不适用于set,因为set 没有push_back 功能。
【解决方案3】:

std::set_intersection。您必须添加一个输出迭代器,您将在其中存储结果:

#include <iterator>
std::vector<int> s3;
set_intersection(s1.begin(),s1.end(),s2.begin(),s2.end(), std::back_inserter(s3));

查看Ideone查看完整列表。

【讨论】:

  • 请注意,如果您希望结果也是一个集合,那么 back_inserter 将不起作用,那么您需要像 Karthik 使用的 std::inserter。
【解决方案4】:

accepted answer 的第一条(投票率很高的)评论抱怨现有标准集操作缺少运算符。

一方面,我理解标准库中缺少此类运算符。另一方面,如果需要,很容易添加它们(为了个人的快乐)。 我超载了

  • operator *() 用于集合的交集
  • operator +() 用于集合的并集。

示例test-set-ops.cc:

#include <algorithm>
#include <iterator>
#include <set>

template <class T, class CMP = std::less<T>, class ALLOC = std::allocator<T> >
std::set<T, CMP, ALLOC> operator * (
  const std::set<T, CMP, ALLOC> &s1, const std::set<T, CMP, ALLOC> &s2)
{
  std::set<T, CMP, ALLOC> s;
  std::set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(),
    std::inserter(s, s.begin()));
  return s;
}

template <class T, class CMP = std::less<T>, class ALLOC = std::allocator<T> >
std::set<T, CMP, ALLOC> operator + (
  const std::set<T, CMP, ALLOC> &s1, const std::set<T, CMP, ALLOC> &s2)
{
  std::set<T, CMP, ALLOC> s;
  std::set_union(s1.begin(), s1.end(), s2.begin(), s2.end(),
    std::inserter(s, s.begin()));
  return s;
}

// sample code to check them out:

#include <iostream>

using namespace std;

template <class T>
ostream& operator << (ostream &out, const set<T> &values)
{
  const char *sep = " ";
  for (const T &value : values) {
    out << sep << value; sep = ", ";
  }
  return out;
}

int main()
{
  set<int> s1 { 1, 2, 3, 4 };
  cout << "s1: {" << s1 << " }" << endl;
  set<int> s2 { 0, 1, 3, 6 };
  cout << "s2: {" << s2 << " }" << endl;
  cout << "I: {" << s1 * s2 << " }" << endl;
  cout << "U: {" << s1 + s2 << " }" << endl;
  return 0;
}

编译和测试:

$ g++ -std=c++11 -o test-set-ops test-set-ops.cc 

$ ./test-set-ops     
s1: { 1, 2, 3, 4 }
s2: { 0, 1, 3, 6 }
I: { 1, 3 }
U: { 0, 1, 2, 3, 4, 6 }

$ 

我不喜欢的是运算符中返回值的副本。可能,这可以通过移动分配来解决,但这仍然超出了我的技能范围。

由于我对这些“新奇”移动语义的了解有限,我担心运算符返回可能会导致返回集合的副本。 Olaf Dietsche 指出这些担忧是不必要的,因为 std::set 已经配备了移动构造函数/赋值。

虽然我相信他,但我正在考虑如何检查这一点(例如“自我说服”)。实际上,这很容易。由于必须在源代码中提供模板,因此您可以简单地使用调试器逐步完成。因此,我在operator *()return s; 处放置了一个断点,并继续单步操作,这使我立即进入std::set::set(_myt&amp;&amp; _Right):等瞧——移动构造函数。感谢 Olaf,对我的启发。

为了完整起见,我也实现了相应的赋值运算符

  • operator *=() 用于集合的“破坏性”交集
  • operator +=() 用于集合的“破坏性”联合。

示例test-set-assign-ops.cc:

#include <iterator>
#include <set>

template <class T, class CMP = std::less<T>, class ALLOC = std::allocator<T> >
std::set<T, CMP, ALLOC>& operator *= (
  std::set<T, CMP, ALLOC> &s1, const std::set<T, CMP, ALLOC> &s2)
{
  auto iter1 = s1.begin();
  for (auto iter2 = s2.begin(); iter1 != s1.end() && iter2 != s2.end();) {
    if (*iter1 < *iter2) iter1 = s1.erase(iter1);
    else {
      if (!(*iter2 < *iter1)) ++iter1;
      ++iter2;
    }
  }
  while (iter1 != s1.end()) iter1 = s1.erase(iter1);
  return s1;
}

template <class T, class CMP = std::less<T>, class ALLOC = std::allocator<T> >
std::set<T, CMP, ALLOC>& operator += (
  std::set<T, CMP, ALLOC> &s1, const std::set<T, CMP, ALLOC> &s2)
{
  s1.insert(s2.begin(), s2.end());
  return s1;
}

// sample code to check them out:

#include <iostream>

using namespace std;

template <class T>
ostream& operator << (ostream &out, const set<T> &values)
{
  const char *sep = " ";
  for (const T &value : values) {
    out << sep << value; sep = ", ";
  }
  return out;
}

int main()
{
  set<int> s1 { 1, 2, 3, 4 };
  cout << "s1: {" << s1 << " }" << endl;
  set<int> s2 { 0, 1, 3, 6 };
  cout << "s2: {" << s2 << " }" << endl;
  set<int> s1I = s1;
  s1I *= s2;
  cout << "s1I: {" << s1I << " }" << endl;
  set<int> s2I = s2;
  s2I *= s1;
  cout << "s2I: {" << s2I << " }" << endl;
  set<int> s1U = s1;
  s1U += s2;
  cout << "s1U: {" << s1U << " }" << endl;
  set<int> s2U = s2;
  s2U += s1;
  cout << "s2U: {" << s2U << " }" << endl;
  return 0;
}

编译和测试:

$ g++ -std=c++11 -o test-set-assign-ops test-set-assign-ops.cc 

$ ./test-set-assign-ops
s1: { 1, 2, 3, 4 }
s2: { 0, 1, 3, 6 }
s1I: { 1, 3 }
s2I: { 1, 3 }
s1U: { 0, 1, 2, 3, 4, 6 }
s2U: { 0, 1, 2, 3, 4, 6 }

$

【讨论】:

  • std::set 已经实现了必要的移动构造函数和赋值运算符,因此无需担心。编译器也很可能使用return value optimization
  • @OlafDietsche 感谢您的评论。我检查了这一点并分别改进了答案。关于 RVO,我已经与我的同事进行了某些讨论,直到我在 VS2013 的调试器中向他们展示它不会发生(至少在我们的开发平台中)。实际上,除非代码对性能至关重要,否则它并不那么重要。在后一种情况下,我暂时不依赖 RVO。 (这实际上在 C++ 中并不难......)
  • @Scheff well Scheff(不是 Bose),很好的解释。
  • 即使现在 VS 对 C++17 保证省略的支持也很糟糕。
【解决方案5】:

在这里发表评论。我觉得是时候给set接口增加union、intersect操作了。让我们在未来的标准中提出这一点。我用std很久了,每次用set操作都希望std好一点。对于一些复杂的集合操作,比如相交,您可以简单地(更简单?)修改以下代码:

template <class InputIterator1, class InputIterator2, class OutputIterator>
  OutputIterator set_intersection (InputIterator1 first1, InputIterator1 last1,
                                   InputIterator2 first2, InputIterator2 last2,
                                   OutputIterator result)
{
  while (first1!=last1 && first2!=last2)
  {
    if (*first1<*first2) ++first1;
    else if (*first2<*first1) ++first2;
    else {
      *result = *first1;
      ++result; ++first1; ++first2;
    }
  }
  return result;
}

复制自http://www.cplusplus.com/reference/algorithm/set_intersection/

例如,如果你的输出是一个集合,你可以 output.insert(*first1)。此外,您的函数可能没有被模板化。如果您的代码可以比使用 std set_intersection 函数更短,请继续使用它。

如果你想合并两个集合,你可以简单地 setA.insert(setB.begin(), setB.end());这比 set_union 方法简单得多。但是,这不适用于矢量。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-03-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-15
    • 1970-01-01
    相关资源
    最近更新 更多