Операции над множествами фундаментальны для программирования, и объединение множеств  —  одна из важнейших. В C++ алгоритмом std::set_union два отсортированных диапазона эффективно объединяются в один, где все элементы уникальные.

Расскажем о таком же эффективном применении этого алгоритма в проектах на C++.

Объединение множеств на C++

В ходе этой операции объединяются два множества, в том числе все имеющиеся в них элементы. В C++ это реализуется алгоритмом std::set_union с отсортированными диапазонами элементов.

Вот базовый синтаксис:

std::set_union(InputIt1 first1, InputIt1 last1,
InputIt2 first2, InputIt2 last2,
OutputIt d_first);

Этой функцией принимается два входных диапазона и выходной итератор. А уникальные элементы обоих диапазонов объединяются в выходной.

Базовый std::set_union

Начнем с простого примера:

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

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

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

for (int num : result) {
std::cout << num << " ";
}
// Вывод: 1 2 3 4 5 6 7 8
}

В этом примере объединяется два отсортированных вектора. Результат содержит все уникальные элементы обоих векторов с сохранением порядка сортировки.

Объединение множеств пользовательскими типами данных

Эта операция не ограничивается встроенными типами, она применяется с пользовательскими классами. Но необходимо убедиться в корректности сортировки.

Вот пример с пользовательским классом Person:

#include <algorithm>
#include <vector>
#include <iostream>
#include <string>

struct Person {
std::string name;
int age;

bool operator<(const Person& other) const {
return name < other.name;
}
};

int main() {
std::vector<Person> team1 = {{"Alice", 25}, {"Bob", 30}, {"Charlie", 35}};
std::vector<Person> team2 = {{"Bob", 30}, {"David", 28}, {"Eve", 32}};
std::vector<Person> merged_team;

std::set_union(team1.begin(), team1.end(),
team2.begin(), team2.end(),
std::back_inserter(merged_team));

for (const auto& person : merged_team) {
std::cout << person.name << " (" << person.age << ") ";
}
// Вывод: Alice (25) Bob (30) Charlie (35) David (28) Eve (32)
}

Здесь мы определили пользовательское сравнение по именам Person. В ходе этой операции объединяются две команды, причем дубли удаляются  —  в примере это Bob.

Реальный сценарий: объединение списков пользователей

Задача: для разработки приложения соцсети предложить пользователю друзей из числа его школьных приятелей и коллег по работе.

Вот как при комбинировании этих списков применяется объединение множеств:

#include <algorithm>
#include <set>
#include <vector>
#include <iostream>
#include <string>

struct User {
std::string name;
int id;

bool operator<(const User& other) const {
return id < other.id;
}
};

std::vector<User> get_school_connections(int user_id) {
// Смоделированный запрос в базу данных
return {{{"Alice", 101}, {"Bob", 102}, {"Charlie", 103}}};
}

std::vector<User> get_work_connections(int user_id) {
// Смоделированный запрос в базу данных
return {{{"Bob", 102}, {"David", 104}, {"Eve", 105}}};
}

int main() {
int current_user_id = 100;
auto school_connections = get_school_connections(current_user_id);
auto work_connections = get_work_connections(current_user_id);

std::vector<User> all_connections;
std::set_union(school_connections.begin(), school_connections.end(),
work_connections.begin(), work_connections.end(),
std::back_inserter(all_connections));

std::cout << "Friend suggestions:" << std::endl;
for (const auto& user : all_connections) {
std::cout << user.name << " (ID: " << user.id << ")" << std::endl;
}
}

В примере показано, как используется объединение множеств при слиянии двух списков. Дубли при этом удаляются  —  тот же Bob, который появляется в обоих списках.

Оптимизация операций объединения множеств

Хотя std::set_union в целом эффективен, возможна его оптимизация:

1. Предварительная сортировка и резервирование места

Если входные диапазоны не отсортированы, сортируем их. Если известен максимально возможный размер результата, резервируем место в выходном контейнере:

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

std::sort(v1.begin(), v1.end());
std::sort(v2.begin(), v2.end());

result.reserve(v1.size() + v2.size()); // Максимально возможный размер

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

2. Использование контейнеров set

Часто выполняете операции над множествами? Тогда вместо std::vector используйте std::set:

#include <set>

std::set<int> s1 = {1, 2, 3, 4, 5};
std::set<int> s2 = {4, 5, 6, 7, 8};
std::set<int> result;

std::set_union(s1.begin(), s1.end(),
s2.begin(), s2.end(),
std::inserter(result, result.end()));

Множества всегда отсортированы, поэтому предварительная сортировка не нужна.

Большие наборы данных

Здесь использование памяти становится проблемой. Вот как выполняется объединение множеств на больших наборах данных без сохранения всего результата в памяти:

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

class SetUnionIterator {
std::vector<int>::const_iterator it1, end1, it2, end2;

public:
SetUnionIterator(const std::vector<int>& v1, const std::vector<int>& v2)
: it1(v1.begin()), end1(v1.end()), it2(v2.begin()), end2(v2.end()) {}

int operator*() const {
if (it1 == end1) return *it2;
if (it2 == end2) return *it1;
return std::min(*it1, *it2);
}

SetUnionIterator& operator++() {
if (it1 == end1) ++it2;
else if (it2 == end2) ++it1;
else if (*it1 < *it2) ++it1;
else if (*it2 < *it1) ++it2;
else { ++it1; ++it2; }
return *this;
}

bool operator!=(const SetUnionIterator&) const {
return it1 != end1 || it2 != end2;
}
};

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

SetUnionIterator begin(v1, v2);
SetUnionIterator end(v1, v2);

// Моделируется обработка без сохранения всего результата
for (auto it = begin; it != end; ++it) {
std::cout << *it << " ";
}
// Вывод: 1 2 3 5 7 9 11
}

При таком подходе обрабатывается объединение двух больших отсортированных диапазонов без сохранения всего результата. Это важно при работе с наборами данных, которые не помещаются в памяти.

Заключение: использование объединения множеств на C++

В C++ алгоритм std::set_union  —  это ценный инструмент для объединения отсортированных диапазонов. Эффективный и универсальный, он применяется со встроенными и пользовательскими типами. Поняв, как эффективно использовать объединение множеств, вы напишете более эффективный код для задач дедупликации, объединения данных, комбинирования списков.

Обобщим ключевые моменты:

  1. Прежде чем использовать std::set_union, сортируем входные диапазоны.
  2. Если операций над множествами выполняются часто, применяем std::set.
  3. В больших наборах данных пользовательскими итераторами обрабатываем объединение, не сохраняя результат целиком.

Освоив эти приемы, вы подготовитесь к самым разным задачам программирования C++, связанным с операциями над множествами.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи ryan: C++ Set Union: Practical Guide with Real-World Examples

Предыдущая статьяПочему стоит использовать Argo CD вместо (или вместе) с Helm в среде Kubernetes
Следующая статьяВеб-безопасность: стратегия защиты контента (CSP)