65.9K
CodeProject 正在变化。 阅读更多。
Home

STL Set 算法运算符

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2017 年 12 月 30 日

CPOL

3分钟阅读

viewsIcon

19883

downloadIcon

167

重载运算符, 用于在 STL Set 算法上编写简洁的代码

目录

引言

自从我开始使用 Python 并且这促使我思考如何重新设计我的库使其更具 Python 风格,如果让我从头开始实现它们的话。在本系列的第一篇文章中,我想将 Python 中用于处理集合代数的奇妙且直观的运算符引入 C++ 世界。 这些运算符不过是语法糖,可以减少需要编写的代码量。

Python 集合运算符表

集合交集

C++ 参考: std::set_intersection

  • 可交换

set_intersection 是一种算法,它产生一个由两个集合共有的元素组成的集合。 它是可交换的,这意味着即使两个集合的位置互换,该算法也会返回相同的结果。

void intersection_example()
{
    std::vector<int> v1{ 1,2,3,4,5,6,7,8 };
    std::vector<int> v2{         5,  7,  9,10 };
    std::sort(v1.begin(), v1.end());
    std::sort(v2.begin(), v2.end());

    std::vector<int> v_intersection;

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

    for (int n : v_intersection)
        std::cout << n << ' ';
}

输出

5 7

这是使用 & 运算符进行交集的示例。

void intersection_example()
{
    std::vector<int> v1{ 1,2,3,4,5,6,7,8 };
    std::vector<int> v2{         5,  7,  9,10 };

    std::vector<int> v_intersection = s(v1) & s(v2);

    for (int n : v_intersection)
        std::cout << n << ' ';
}

我跳过了显示运算符示例的输出,因为它与上述相同。

s 是一个函数,而不是类。 如果 s 是一个类,要实例化它,必须指定一个容器类型(见下文)。

std::vector<int> v_intersection = s<std::vector<int>>(v1) & s<std::vector<int>>(v2);

为了利用自动类型推导,s 必须是一个除了返回 wrapper 类之外什么都不做的函数。

#include <algorithm>
#include <iterator>

template<typename T>
struct wrapper
{
    wrapper(T& container) : cont(container) {}
    T& cont;
};

template<typename T>
wrapper<T> s(T& s_cont)
{
    return wrapper<T>(s_cont);
}

& 运算符函数检查是否对容器进行排序。 由于 std::sort 仅适用于随机访问迭代器,因此我们不能将此函数与具有非随机访问迭代器的 STL listslist 一起使用。 在我 15 年的工作中,我还没有在任何代码库中看到对 list 的任何使用。

template<typename T>
T operator&(wrapper<T>& left, wrapper<T>& right)
{
    T& c1 = left.cont;
    T& c2 = right.cont;
    if (!std::is_sorted(c1.begin(), c1.end()))
        std::sort(c1.begin(), c1.end());
    if (!std::is_sorted(c2.begin(), c2.end()))
        std::sort(c2.begin(), c2.end());

    T v_intersection;

    std::set_intersection(c1.begin(), c1.end(),
        c2.begin(), c2.end(),
        std::back_inserter(v_intersection));

    return v_intersection;
}

所有集合算法的先决条件都要求范围已排序,因此需要此 is_sorted 检查。

集合并集

C++ 参考: std::set_union

  • 可交换

set_union 是一种算法,它产生一个由两个集合的元素组成的集合。 对于出现在交集中的元素,它总是从第一个集合中选择它们,而不是从第二个集合中选择。

void union_example()
{
    std::vector<int> v1 = { 1, 2, 3, 4, 5 };
    std::vector<int> v2 = {       3, 4, 5, 6, 7 };
    std::sort(v1.begin(), v1.end());
    std::sort(v2.begin(), v2.end());

    std::vector<int> dest1;

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

    for (const auto &i : dest1) {
        std::cout << i << ' ';
    }
    std::cout << '\n';
}

输出

1 2 3 4 5 6 7

所需的代码量少得多,因此代码更简洁。

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

    std::vector<int> dest1 = s(v1) | s(v2);

    for (int n : dest1)
        std::cout << n << ' ';
}

| 运算符几乎类似于 & 运算符,只是算法不同。

template<typename T>
T operator|(wrapper<T>& left, wrapper<T>& right)
{
    T& c1 = left.cont;
    T& c2 = right.cont;
    if (!std::is_sorted(c1.begin(), c1.end()))
        std::sort(c1.begin(), c1.end());
    if (!std::is_sorted(c2.begin(), c2.end()))
        std::sort(c2.begin(), c2.end());

    T dest1;

    std::set_union(c1.begin(), c1.end(),
        c2.begin(), c2.end(),
        std::back_inserter(dest1));

    return dest1;
}

集合差集

C++ 参考: std::set_difference

  • 不可交换

set_difference 返回 第一个集合中不在第二个集合中的元素,并用 Python 中的减号运算符表示。 由于显而易见的原因,当参数交换位置时,结果是不同的。 set_difference 像减号运算一样是不可交换的。

void set_difference_example() 
{
    std::vector<int> v1{ 1, 2, 5, 5, 5,    9 };
    std::vector<int> v2{    2, 5,       7 };
    std::sort(v1.begin(), v1.end());
    std::sort(v2.begin(), v2.end());

    std::vector<int> diff;

    std::set_difference(v1.begin(), v1.end(), v2.begin(), v2.end(),
        std::inserter(diff, diff.begin()));

    for (auto i : v1) std::cout << i << ' ';
    std::cout << "minus ";
    for (auto i : v2) std::cout << i << ' ';
    std::cout << "is: ";

    for (auto i : diff) std::cout << i << ' ';
    std::cout << '\n';
}

输出

1 2 5 5 5 9 minus 2 5 7 is: 1 5 5 9

这是一个带有减号运算符的示例。

void set_difference_example()
{
    std::vector<int> v1{ 1, 2, 5, 5, 5, 9 };
    std::vector<int> v2{    2, 5,       7 };

    std::vector<int> diff = s(v1) - s(v2);

    for (auto i : v1) std::cout << i << ' ';
    std::cout << "minus ";
    for (auto i : v2) std::cout << i << ' ';
    std::cout << "is: ";

    for (auto i : diff) std::cout << i << ' ';
    std::cout << '\n';
}

减号运算符的代码如下所示

template<typename T>
T operator-(wrapper<T>& left, wrapper<T>& right)
{
    T& c1 = left.cont;
    T& c2 = right.cont;
    if (!std::is_sorted(c1.begin(), c1.end()))
        std::sort(c1.begin(), c1.end());
    if (!std::is_sorted(c2.begin(), c2.end()))
        std::sort(c2.begin(), c2.end());

    T diff;

    std::set_difference(c1.begin(), c1.end(),
        c2.begin(), c2.end(),
        std::back_inserter(diff));

    return diff;
}

集合对称差

C++ 参考: std::set_symmetric_difference

  • 可交换

set_symmetric_difference 计算存在于任一集合中但不同时存在于两个集合中的元素。

void set_symmetric_difference_example()
{
    std::vector<int> v1{ 1,2,3,4,5,6,7,8 };
    std::vector<int> v2{         5,  7,  9,10 };
    std::sort(v1.begin(), v1.end());
    std::sort(v2.begin(), v2.end());

    std::vector<int> v_symDifference;

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

    for (int n : v_symDifference)
        std::cout << n << ' ';
}

输出

1 2 3 4 6 8 9 10

set_symmetric_difference 由逻辑异或运算符表示。

void set_symmetric_difference_example()
{
    std::vector<int> v1{ 1,2,3,4,5,6,7,8 };
    std::vector<int> v2{         5,  7,  9,10 };

    std::vector<int> v_symDifference = s(v1) ^ s(v2);

    for (int n : v_symDifference)
        std::cout << n << ' ';
}

逻辑异或运算符的代码如下所示

template<typename T>
T operator^(wrapper<T>& left, wrapper<T>& right)
{
    T& c1 = left.cont;
    T& c2 = right.cont;
    if (!std::is_sorted(c1.begin(), c1.end()))
        std::sort(c1.begin(), c1.end());
    if (!std::is_sorted(c2.begin(), c2.end()))
        std::sort(c2.begin(), c2.end());

    T v_symDifference;

    std::set_symmetric_difference(c1.begin(), c1.end(),
        c2.begin(), c2.end(),
        std::back_inserter(v_symDifference));

    return v_symDifference;
}

超集和子集

C++ 参考: std::includes

  • 不可交换

STL includes 可用于确定一个集合是否是超集(返回一个布尔值)。 要检查它是否是子集,只需切换这两个集合。

void is_superset_example()
{
    std::vector<char> v1{ 'a', 'b', 'c', 'f', 'h', 'x' };
    std::vector<char> v2{ 'a', 'b', 'c' };
    std::vector<char> v3{ 'a', 'c' };
    std::vector<char> v4{ 'g' };
    std::vector<char> v5{ 'a', 'c', 'g' };
    std::sort(v1.begin(), v1.end());
    std::sort(v2.begin(), v2.end());
    std::sort(v3.begin(), v3.end());
    std::sort(v4.begin(), v4.end());
    std::sort(v5.begin(), v5.end());

    for (auto i : v1) std::cout << i << ' ';
    std::cout << "\nincludes:\n" << std::boolalpha;

    for (auto i : v2) std::cout << i << ' ';
    std::cout << ": " 
              << std::includes(v1.begin(), v1.end(), v2.begin(), v2.end()) << '\n';
    for (auto i : v3) std::cout << i << ' ';
    std::cout << ": " 
              << std::includes(v1.begin(), v1.end(), v3.begin(), v3.end()) << '\n';
    for (auto i : v4) std::cout << i << ' ';
    std::cout << ": " 
              << std::includes(v1.begin(), v1.end(), v4.begin(), v4.end()) << '\n';
    for (auto i : v5) std::cout << i << ' ';
    std::cout << ": " 
              << std::includes(v1.begin(), v1.end(), v5.begin(), v5.end()) << '\n';

    auto cmp_nocase = [](char a, char b) {
        return std::tolower(a) < std::tolower(b);
    };

    std::vector<char> v6{ 'A', 'B', 'C' };
    for (auto i : v6) std::cout << i << ' ';
    std::cout << ": (case-insensitive) "
        << std::includes(v1.begin(), v1.end(), v6.begin(), v6.end(), cmp_nocase)
        << '\n';
}

输出

a b c f h x
includes:
a b c : true
a c : true
g : false
a c g : false
A B C : (case-insensitive) true

>= 运算符示例在下面。 本文未显示 <= 运算符示例。

void is_superset_example()
{
    std::vector<char> v1{ 'a', 'b', 'c', 'f', 'h', 'x' };
    std::vector<char> v2{ 'a', 'b', 'c' };
    std::vector<char> v3{ 'a', 'c' };
    std::vector<char> v4{ 'g' };
    std::vector<char> v5{ 'a', 'c', 'g' };

    for (auto i : v1) std::cout << i << ' ';
    std::cout << "\nincludes:\n" << std::boolalpha;

    for (auto i : v2) std::cout << i << ' ';
    std::cout << ": " << (s(v1) >= s(v2)) << '\n';
    for (auto i : v3) std::cout << i << ' ';
    std::cout << ": " << (s(v1) >= s(v3)) << '\n';
    for (auto i : v4) std::cout << i << ' ';
    std::cout << ": " << (s(v1) >= s(v4)) << '\n';
    for (auto i : v5) std::cout << i << ' ';
    std::cout << ": " << (s(v1) >= s(v5)) << '\n';

    auto cmp_nocase = [](char a, char b) {
        return std::tolower(a) < std::tolower(b);
    };

    std::vector<char> v6{ 'A', 'B', 'C' };
    for (auto i : v6) std::cout << i << ' ';
    std::cout << ": (case-insensitive) "
        << std::includes(v1.begin(), v1.end(), v6.begin(), v6.end(), cmp_nocase)
        << '\n';
}

如不区分大小写的示例所示,用户目前无法选择在 >=<= 重载运算符中使用自定义比较器。 在这种情况下,必须直接调用 includes

// Returns true if left is superset of right?
template<typename T>
bool operator>=(wrapper<T>& left, wrapper<T>& right)
{
    T& c1 = left.cont;
    T& c2 = right.cont;

    if (!std::is_sorted(c1.begin(), c1.end()))
        std::sort(c1.begin(), c1.end());
    if (!std::is_sorted(c2.begin(), c2.end()))
        std::sort(c2.begin(), c2.end());

    return std::includes(
        c1.begin(), c1.end(),
        c2.begin(), c2.end());
}

// Returns true if left is subset of right?
template<typename T>
bool operator<=(wrapper<T>& left, wrapper<T>& right)
{
    T& c1 = left.cont;
    T& c2 = right.cont;

    if (!std::is_sorted(c1.begin(), c1.end()))
        std::sort(c1.begin(), c1.end());
    if (!std::is_sorted(c2.begin(), c2.end()))
        std::sort(c2.begin(), c2.end());

    return std::includes(
        c2.begin(), c2.end(),
        c1.begin(), c1.end());
}

“我用不着这些!”

在您急于声称您不需要这些集合算法之前,我想向您展示一个典型的选择示例,您可以在其中使用它。 想象一下,您正在为大学生编写一个选课网站。 在表格上,目前有学生添加的已选科目,以及学生可以选择的可用科目下拉列表。 添加后从可用下拉列表中删除科目是有意义的,因为您不希望学生意外地两次添加相同的科目。 计算可供选择的剩余科目的一种方法是,仅使用本文中介绍的减号运算符从完整科目集中减去已选科目集。

文章源代码托管在 Github

与集合交集性能相关的文章: 有序集合的交集

历史

  • 2017 年 12 月 30 日: 第一个版本
  • 2018 年 6 月 16 日: 1.1 版本 移除返回期间的 std::move 以启用“命名返回值优化”
© . All rights reserved.