Тезисы о std::set и std::multiset в C++

от автора

Приветствую всех читателей! Меня зовут Максим, и я хочу поделиться с вами своими знаниями о программировании. Я не являюсь профессиональным разработчиком, и программирование для меня — это хобби и способ автоматизации рутинных задач на работе. В этой статье я расскажу вам об ассоциативном контейнере std::set и std::multiset в C++. Надеюсь, что эта информация будет полезной и интересной для всех, кто хочет узнать что-то новое о программировании.

std::set

std::set — это ассоциативный контейнер, который содержит упорядоченный набор уникальных объектов типа Key.

Основные свойства:

  • Элементы расположены в отсортированном порядке (по умолчанию по возрастанию);

  • Все элементы уникальны (дубликаты не допускаются);

  • Реализован как самобалансирующееся двоичное дерево поиска (обычно красно-черное дерево);

  • Операции вставки, удаления и поиска выполняются за O(log n);

  • Элементы нельзя изменять напрямую, чтобы не нарушить порядок.

Пример использования:

#include <iostream> #include <set>  int main() {     std::set<int> numbers = {5, 3, 8, 1, 3, 7}; // дубликат 3 будет проигнорирован          // Вывод элементов (будут отсортированы)     for (int num : numbers) {         std::cout << num << " "; // 1 3 5 7 8     }          // Вставка элемента     numbers.insert(4);          // Поиск элемента     if (numbers.find(5) != numbers.end()) {         std::cout << "\n5 found in set";     }          // Удаление элемента     numbers.erase(3);          return 0; }

std::multiset

std::multiset очень похож на std::set, но с одним ключевым отличием — он может содержать несколько элементов с одинаковыми значениями.

Основные свойства:

  • Элементы упорядочены;

  • Допускаются дубликаты;

  • Реализован как самобалансирующееся двоичное дерево поиска;

  • Операции вставки, удаления и поиска выполняются за O(log n).

Пример использования:

#include <iostream> #include <set>  int main() {     std::multiset<int> numbers = {5, 3, 8, 1, 3, 7}; // оба значения 3 будут сохранены          // Вывод элементов     for (int num : numbers) {         std::cout << num << " "; // 1 3 3 5 7 8     }          // Вставка дубликата     numbers.insert(3); // теперь три значения 3          // Подсчет количества определенных элементов     std::cout << "\nNumber of 3s: " << numbers.count(3); // 3          // Удаление всех элементов со значением 3     numbers.erase(3); // удалит все три значения          return 0; }

Общие методы для set и multiset

Оба контейнера поддерживают:

  • insert() — добавление элемента;

  • erase() — удаление элемента;

  • find() — поиск элемента;

  • count() — количество элементов с заданным ключом;

  • size() — количество элементов;

  • empty() — проверка на пустоту;

  • clear() — очистка контейнера;

  • lower_bound()upper_bound()equal_range() — для работы с диапазонами.

Когда использовать?

Используйте std::set, если:

  • Нужны уникальные элементы;

  • Важна сортировка;

  • Требуется быстрый поиск.

Используйте std::multiset, если:

  • Можно дубликаты;

  • Нужно хранить элементы отсортированными;

  • Требуется доступ к элементам с одинаковыми значениями.

Выводы о std::set и std::multiset

  1. Ключевое различие:

    • std::set хранит только уникальные элементы;

    • std::multiset разрешает дубликаты.

  2. Общие черты:

    • Оба контейнера отсортированы по умолчанию;

    • Операции выполняются за O(log n);

    • Поддерживают стандартные методы вставки, удаления и поиска.

  3. Рекомендации по использованию:

    • std::set – если нужны уникальные элементы с автоматической сортировкой (например, список уникальных ID).

    • std::multiset – если допустимы повторы (например, хранение оценок студентов, где у нескольких может быть одинаковый балл).

  4. Особенности работы

    • В set попытка вставить дубликат просто игнорируется (без ошибки).

    • В multiset метод erase(value) удаляет все элементы с таким значением (если нужно удалить один, используйте erase(iterator)).

    • count(key) в set возвращает 0 или 1, а в multiset – количество вхождений.

Примерное сравнение контейнеров

Критерий

std::set

std::multiset

Уникальность

Только уникальные

Допускает дубликаты

Сортировка

Да (по умолчанию <)

Да

Вставка дублей

Игнорирует

Сохраняет

erase(value)

Удаляет один (если есть)

Удаляет все вхождения

Использование

Уникальные ключи

Повторяющиеся значения


Добавление и удаление элементов в std::set и std::multiset

Добавление элементов

  1. insert(): вставка элемента или диапазона

  2. emplace(): создание элемента на месте (без копирования)

  3. emplace_hint(): вставка с подсказкой позиции

Вставка в std::set (уникальные элементы)

std::set<int> unique_numbers;  // 1. Простая вставка (возвращает pair<iterator, bool>) auto result = unique_numbers.insert(42); if (result.second) {     std::cout << "Insert successful\n"; }  // 2. Вставка с подсказкой позиции auto hint = unique_numbers.lower_bound(40); unique_numbers.insert(hint, 41);  // Более эффективная вставка  // 3. Emplace - создание на месте unique_numbers.emplace(43);  // 4. Вставка диапазона std::vector<int> numbers_to_add = {44, 45, 46}; unique_numbers.insert(numbers_to_add.begin(), numbers_to_add.end());

Вставка в std::multiset (с дубликатами)

std::multiset<int> multi_numbers;  // 1. Вставка дубликатов разрешена multi_numbers.insert(10); multi_numbers.insert(10);  // ОК - дубликат  // 2. Emplace multi_numbers.emplace(11); multi_numbers.emplace(11);  // Дубликат разрешен  // 3. Вставка диапазона int values[] = {12, 12, 13}; multi_numbers.insert(std::begin(values), std::end(values));

Удаление элементов

  1. erase(): по значению, итератору или диапазону

  2. clear(): полная очистка контейнера

  3. extract(): (C++17) — извлечение узла без удаления

Удаление из std::set

std::set<std::string> names = {"Alice", "Bob", "Charlie"};  // 1. Удаление по значению (возвращает количество удаленных - 0 или 1) size_t count = names.erase("Bob");  // count = 1  // 2. Удаление по итератору auto it = names.find("Alice"); if (it != names.end()) {     names.erase(it); }  // 3. Удаление диапазона names.erase(names.begin(), names.end());  // Очистка всего контейнера  // 4. Извлечение узла (C++17) if (auto node = names.extract("Charlie"); !node.empty()) {     // Можно модифицировать и вставить в другой set     node.value() = "Charles";     names.insert(std::move(node)); }

Особенности операций

Для std::set:

  • insert возвращает pair<iterator, bool> (итератор и флаг успеха)

  • При попытке вставить дубликат, insert не изменяет контейнер

  • erase по значению возвращает 0 или 1

Для std::multiset:

  • insert всегда успешен и возвращает итератор

  • erase по значению удаляет все совпадения и возвращает их количество

  • find возвращает первый найденный элемент

Производительность операций

Операция

Сложность

Примечания

insert/emplace

O(log n)

Для вставки с подсказкой — O(1), если подсказка верна

erase по итератору

O(1)

Не включает поиск элемента

erase по значению

O(log n) + m

m — количество удаляемых элементов

extract

O(1)

Для последующей вставки в другой контейнер

Полезные советы

Эффективная вставка

auto hint = my_set.lower_bound(value); my_set.insert(hint, new_value);  // Оптимизированная вставка

Безопасное удаление во время итерации

for (auto it = my_set.begin(); it != my_set.end(); ) {     if (condition(*it)) {         it = my_set.erase(it);  // erase возвращает следующий валидный итератор     } else {         ++it;     } }

Работа с дубликатами в multiset

auto [first, last] = multi_set.equal_range(value); multi_set.erase(first, last);  // Удалить все элементы с value

Использование extract для модификации ключей (C++17+):

if (auto node = my_set.extract(key); !node.empty()) {     node.value() = modified_value;  // Изменяем значение     my_set.insert(std::move(node)); }

Поиск элементов в std::set и std::multiset

Основные методы поиска

Оба контейнера предоставляют следующие методы для поиска элементов:

1. find(key)

  • Возвращает итератор на найденный элемент

  • Если элемент не найден, возвращает end()

  • Время работы: O(log n)

auto it = my_set.find(42); if (it != my_set.end()) {     std::cout << "Element found: " << *it << "\n"; } else {     std::cout << "Element not found\n"; }

2. count(key)

  • Возвращает количество элементов с заданным значением

  • Для set: всегда 0 или 1

  • Для multiset: может быть больше 1

  • Время работы: O(log n) для set, O(log n + k) для multiset (где k — количество найденных элементов)

std::set<int> s = {1, 2, 3}; std::cout << s.count(2); // 1 std::cout << s.count(4); // 0  std::multiset<int> ms = {1, 2, 2, 3}; std::cout << ms.count(2); // 2

3. contains(key) (C++20)

  • Возвращает bool — существует ли элемент с таким значением

  • Более выразительная альтернатива count()

  • Время работы: O(log n)

if (my_set.contains(42)) {     std::cout << "Element exists\n"; }

Поиск в диапазонах

Для работы с диапазонами значений используются:

1. lower_bound(key)

  • Возвращает итератор на первый элемент ≥ key.

  • Полезен для поиска начальной границы диапазона.

2. upper_bound(key)

  • Возвращает итератор на первый элемент > key.

  • Полезен для поиска конечной границы диапазона.

3. equal_range(key)

  • Возвращает пару итераторов (lower_boundupper_bound).

  • Эффективнее, чем два отдельных вызова.

std::set nums = {10, 20, 30, 40, 50};  // Найти все элементы в диапазоне [20, 40) auto low = nums.lower_bound(20);  // 20 auto high = nums.upper_bound(40); // 50  for (auto it = low; it != high; ++it) {     std::cout << *it << " "; // 20 30 40 }  // Эквивалентно с equal_range auto [first, last] = nums.equal_range(30); for (auto it = first; it != last; ++it) {     std::cout << *it << " "; // 30 }

Особенности поиска в std::set

  1. Уникальные элементы:

    • find() возвращает максимум один элемент.

    • count() возвращает 0 или 1.

    • equal_range() возвращает диапазон из 0 или 1 элемента.

std::set<std::string> names = {"Alice", "Bob"};  auto it = names.find("Alice"); if (it != names.end()) {     std::cout << "Found: " << *it << "\n"; }  auto [first, last] = names.equal_range("Bob"); if (first != last) {     std::cout << "Found Bob\n"; }

Особенности поиска в std::multiset

  1. Дубликаты элементов:

    • find() возвращает итератор на первый найденный элемент.

    • count() возвращает количество всех найденных элементов.

    • equal_range() возвращает все элементы с заданным значением.

std::multiset<int> grades = {85, 90, 90, 95, 100};  // Найти первый элемент 90 auto it = grades.find(90);  // указывает на первый 90  // Количество элементов 90 size_t n = grades.count(90); // 2  // Получить все элементы 90 auto [first, last] = grades.equal_range(90); for (auto it = first; it != last; ++it) {     std::cout << *it << " "; // 90 90 }

Сравнение методов поиска

Метод

std::set

std::multiset

Возвращаемое значение

Время работы

find(key)

Итератор на первый найденный

O(log n)

count(key)

Количество элементов (0/1)

O(log n) для set, O(log n + k) для multiset

contains(key)

bool (есть/нет)

O(log n)

equal_range()

Пара итераторов (диапазон)

Практические рекомендации

  1. Для проверки существования элемента:

    • В C++20+ предпочитайте contains() как наиболее выразительный вариант

    • В более ранних стандартах используйте count() или find() != end()

  2. Для работы с диапазонами:

    • Используйте lower_bound()/upper_bound() для поиска границ

    • Для точного поиска всех совпадений в multiset используйте equal_range()

  3. Для multiset:

    • Помните, что find() возвращает только первый элемент

    • Для обработки всех дубликатов используйте equal_range() или комбинацию lower_bound()/upper_bound()

  4. Оптимизация:

    • Если нужно часто проверять существование без доступа к значению, contains() эффективнее find()

    • Для поиска диапазонов сначала определяйте границы, затем обрабатывайте элементы

// Эффективный поиск диапазона std::set<int> large_set = {...}; auto low = large_set.lower_bound(100); auto high = large_set.upper_bound(200);  // Обработка диапазона std::vector<int> result(low, high);

Работа с диапазонами в std::set и std::multiset

Основные методы для работы с диапазонами

Оба контейнера предоставляют специальные методы для эффективной работы с диапазонами элементов:

1. equal_range(key)

Возвращает пару итераторов, представляющих диапазон элементов с заданным значением.

auto [first, last] = my_set.equal_range(value); // first - начало диапазона // last - конец диапазона 

2. lower_bound(key)

Возвращает итератор на первый элемент, который не меньше заданного значения.

3. upper_bound(key)

Возвращает итератор на первый элемент, который больше заданного значения.

Примеры использования

Для std::set (уникальные элементы)

std::set numbers = {10, 20, 30, 40, 50};  // Поиск диапазона [20, 40] auto low = numbers.lower_bound(20);  // Указывает на 20 auto high = numbers.upper_bound(40); // Указывает на 50  for (auto it = low; it != high; ++it) {     std::cout << *it << " "; // Выведет: 20 30 40 }  // Использование equal_range (для set возвращает 0 или 1 элемент) auto range = numbers.equal_range(30); if (range.first != range.second) {     std::cout << "\nFound: " << *range.first; // 30 }

Для std::multiset (с дубликатами)

std::multiset scores = {65, 70, 70, 75, 80, 80, 80};  // Получение всех оценок 70 auto range = scores.equal_range(70); std::cout << "Scores 70: "; for (auto it = range.first; it != range.second; ++it) {     std::cout << *it << " "; // 70 70 }  // Поиск диапазона [70, 80) auto low = scores.lower_bound(70);  // Первый 70 auto high = scores.upper_bound(80); // Элемент после последнего 80  std::cout << "\nRange 70-80: "; for (auto it = low; it != high; ++it) {     std::cout << *it << " "; // 70 70 75 80 80 80 }

Практические сценарии использования

1. Поиск всех элементов в диапазоне значений

std::set names = {"Alice", "Bob", "Charlie", "David", "Eve"};  // Найти все имена между "B" и "D" включительно auto begin = names.lower_bound("B"); auto end = names.upper_bound("D");  for (auto it = begin; it != end; ++it) {     std::cout << *it << "\n"; // Bob, Charlie, David }

2. Удаление диапазона элементов

std::multiset temps = {15, 18, 18, 20, 22, 23, 25};  // Удалить все температуры от 18 до 23 (не включая 23) auto first = temps.lower_bound(18); auto last = temps.lower_bound(23); temps.erase(first, last);  // temps теперь содержит: 15, 23, 25

3. Подсчет элементов в диапазоне

std::multiset data = {1, 2, 2, 3, 3, 3, 4, 5};  auto low = data.lower_bound(2); auto high = data.upper_bound(4); size_t count = std::distance(low, high); // 6 элементов (2,2,3,3,3,4)

Особенности работы с диапазонами

  1. Для std::set:

    • equal_range(key) всегда возвращает диапазон из 0 или 1 элемента

    • lower_bound(key) и upper_bound(key) будут равны, если значение отсутствует

  2. Для std::multiset:

    • equal_range(key) возвращает все элементы с заданным значением

    • lower_bound(key) указывает на первый элемент ≥ заданного значения

    • upper_bound(key) указывает на первый элемент > заданного значения

  3. Все операции работают за O(log n) времени, так как используют бинарный поиск

Продвинутые техники

1. Использование пользовательских компараторов

struct CaseInsensitiveCompare {     bool operator()(const std::string& a, const std::string& b) const {         return std::lexicographical_compare(             a.begin(), a.end(), b.begin(), b.end(),             [](char c1, char c2) { return tolower(c1) < tolower(c2); }         );     } };  std::set<std::string, CaseInsensitiveCompare> words = {"Apple", "banana", "Cherry"};  // Поиск будет регистронезависимым auto range = words.equal_range("apple"); // Найдет "Apple"

2. Работа с временными диапазонами (C++20)

std::set numbers = {1, 2, 3, 4, 5};  // Получить view диапазона [2, 4] auto view = std::ranges::subrange(     numbers.lower_bound(2),     numbers.upper_bound(4) );  for (int n : view) {     std::cout << n << " "; // 2 3 4 } 

Оптимизация производительности

  1. Для частых операций с диапазонами:

    • Сохраняйте итераторы, если возможно

    • Используйте equal_range вместо отдельных вызовов lower_bound/upper_bound

  2. При массовой обработке:

    • Сначала определите границы диапазона

    • Затем обрабатывайте элементы за один проход

std::multiset<Employee> employees;  // Эффективная обработка всех сотрудников отдела "IT" auto [first, last] = employees.equal_range("IT"); std::for_each(first, last, [](const Employee& emp) {     emp.process_salary(); });


ссылка на оригинал статьи https://habr.com/ru/articles/925504/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *