集合算法:set_union、set_intersection、set_difference、set_symmetric_difference

集合操作,说白了就是数学里的并集、交集、差集这些概念。C++ STL 把这套东西搬到了容器上,用四个算法函数搞定。我当年刚接触时觉得这玩意儿不就是数学课上的东西吗?后来在项目里做数据清洗,才发现它有多实用。

先说说前提条件

这四个算法有个共同要求:输入的两个区间必须是有序的。默认按 operator< 升序排列。如果你传进去的是乱序数据,结果就是错的,而且编译器不会给你任何警告。

注意: 我曾经在代码里传了两个无序的 vector,结果 set_union 返回了一堆重复元素。排查了半天才发现是忘记排序了。嗯,从那以后我写集合算法前都会先确认数据是否有序。

set_union:求并集

把两个集合的元素合并到一起,重复的只保留一份。说白了就是「你有我有全都有」。

#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>

int main() {
    std::vector<int> v1 = {1, 2, 3, 4, 5};
    std::vector<int> v2 = {3, 4, 5, 6, 7};
    std::vector<int> result;

    std::set_union(v1.begin(), v1.end(),
                   v2.begin(), v2.end(),
                   std::back_inserter(result));

    // result: 1 2 3 4 5 6 7
    for (int x : result) std::cout << x << " ";
    return 0;
}

我个人习惯用 back_inserter 来接收结果,省得手动管理内存。你想想看,如果目标容器空间不够,程序直接崩掉,多冤。

set_intersection:求交集

只保留两个集合都有的元素。这个在项目里用得最多,比如找两个用户群体的共同特征。

std::vector<int> v1 = {1, 2, 3, 4, 5};
std::vector<int> v2 = {3, 4, 5, 6, 7};
std::vector<int> result;

std::set_intersection(v1.begin(), v1.end(),
                      v2.begin(), v2.end(),
                      std::back_inserter(result));

// result: 3 4 5
小技巧: 如果两个集合都是有序的,set_intersection 的时间复杂度是 O(n+m),比暴力遍历快得多。我在做日志分析时,经常用这个函数来找出两个时间段内都出现的错误码。

set_difference:求差集

返回在第一个集合中但不在第二个集合中的元素。注意方向:v1 - v2v2 - v1 结果不一样。

std::vector<int> v1 = {1, 2, 3, 4, 5};
std::vector<int> v2 = {3, 4, 5, 6, 7};
std::vector<int> result;

std::set_difference(v1.begin(), v1.end(),
                    v2.begin(), v2.end(),
                    std::back_inserter(result));

// result: 1 2

我建议你在使用差集时,先想清楚「谁减谁」。曾经有个同事把参数顺序搞反了,结果数据对不上,查了两天才发现是差集方向错了。

set_symmetric_difference:求对称差集

返回两个集合中「你有我没有,我有你没有」的元素。说白了就是并集减去交集。

std::vector<int> v1 = {1, 2, 3, 4, 5};
std::vector<int> v2 = {3, 4, 5, 6, 7};
std::vector<int> result;

std::set_symmetric_difference(v1.begin(), v1.end(),
                              v2.begin(), v2.end(),
                              std::back_inserter(result));

// result: 1 2 6 7

四个算法的关系图

下面这张图可以帮你快速理解它们之间的关系:

集合算法关系图 集合 A 1, 2, 3, 4, 5 集合 B 3, 4, 5, 6, 7 交集 3, 4, 5 算法对应关系 set_union A ∪ B = {1,2,3,4,5,6,7} set_intersection A ∩ B = {3,4,5} set_difference A - B = {1,2} set_symmetric_difference (A-B) ∪ (B-A) = {1,2,6,7}

自定义比较函数

如果你的元素不是基本类型,或者你想用不同的排序规则,可以传入自定义比较函数。比如按字符串长度排序:

bool compareByLength(const std::string& a, const std::string& b) {
    return a.size() < b.size();
}

std::vector<std::string> v1 = {"cat", "dog", "bird"};
std::vector<std::string> v2 = {"fish", "bird", "ant"};
std::vector<std::string> result;

std::set_union(v1.begin(), v1.end(),
               v2.begin(), v2.end(),
               std::back_inserter(result),
               compareByLength);
重要提醒: 自定义比较函数必须满足严格弱序(strict weak ordering)的要求。说白了就是:不能出现 a < b 且 b < a 同时成立的情况。我曾经用了一个不满足这个条件的比较函数,结果 set_union 返回的结果里出现了重复元素,调试了很久才发现问题。

性能与注意事项

算法 时间复杂度 空间复杂度 适用场景
set_union O(n+m) O(n+m) 合并两个有序列表
set_intersection O(n+m) O(min(n,m)) 查找共同元素
set_difference O(n+m) O(n) 找出独有元素
set_symmetric_difference O(n+m) O(n+m) 找出差异元素

核心要点:

  • 输入区间必须有序,否则结果不可预测
  • 目标容器要有足够的空间,建议用 back_inserter
  • 自定义比较函数要满足严格弱序
  • 这四个算法都是稳定的,不会改变元素的相对顺序

嗯,集合算法就这些内容。我个人觉得,掌握这四个函数的关键在于理解它们背后的数学概念,然后记住「有序」这个前提条件。你在实际项目中用上几次,自然就熟练了。

公众号:蓝海资料掘金营,微信 deep3321