Приветствую всех читателей! Меня зовут Максим, и я хочу поделиться с вами своими знаниями о программировании. Я не являюсь профессиональным разработчиком, и программирование для меня — это хобби и способ автоматизации рутинных задач на работе. В этой статье я расскажу вам об ассоциативном контейнере 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
Ключевое различие:
std::setхранит только уникальные элементы;std::multisetразрешает дубликаты.
Общие черты:
Оба контейнера отсортированы по умолчанию;
Операции выполняются за O(log n);
Поддерживают стандартные методы вставки, удаления и поиска.
Рекомендации по использованию:
std::set– если нужны уникальные элементы с автоматической сортировкой (например, список уникальных ID).std::multiset– если допустимы повторы (например, хранение оценок студентов, где у нескольких может быть одинаковый балл).
Особенности работы
В
setпопытка вставить дубликат просто игнорируется (без ошибки).В
multisetметодerase(value)удаляет все элементы с таким значением (если нужно удалить один, используйтеerase(iterator)).count(key)вsetвозвращает 0 или 1, а вmultiset– количество вхождений.
Примерное сравнение контейнеров
Критерий |
|
|
|---|---|---|
Уникальность | Только уникальные | Допускает дубликаты |
Сортировка | Да (по умолчанию | Да |
Вставка дублей | Игнорирует | Сохраняет |
| Удаляет один (если есть) | Удаляет все вхождения |
Использование | Уникальные ключи | Повторяющиеся значения |
Добавление и удаление элементов в std::set и std::multiset
Добавление элементов
insert(): вставка элемента или диапазонаemplace(): создание элемента на месте (без копирования)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));
Удаление элементов
erase(): по значению, итератору или диапазонуclear(): полная очистка контейнера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возвращает первый найденный элемент
Производительность операций
Операция | Сложность | Примечания |
|---|---|---|
| O(log n) | Для вставки с подсказкой - O(1), если подсказка верна |
| O(1) | Не включает поиск элемента |
| O(log n) + m | m - количество удаляемых элементов |
| 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_bound,upper_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
Уникальные элементы:
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
Дубликаты элементов:
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 }
Сравнение методов поиска
Метод |
|
| Возвращаемое значение | Время работы |
|---|---|---|---|---|
| ✓ | ✓ | Итератор на первый найденный | O(log n) |
| ✓ | ✓ | Количество элементов (0/1) | O(log n) для set, O(log n + k) для multiset |
| ✓ | ✓ | bool (есть/нет) | O(log n) |
| ✓ | ✓ | Пара итераторов (диапазон) |
Практические рекомендации
Для проверки существования элемента:
В C++20+ предпочитайте
contains()как наиболее выразительный вариантВ более ранних стандартах используйте
count()илиfind() != end()
Для работы с диапазонами:
Используйте
lower_bound()/upper_bound()для поиска границДля точного поиска всех совпадений в
multisetиспользуйтеequal_range()
Для
multiset:Помните, что
find()возвращает только первый элементДля обработки всех дубликатов используйте
equal_range()или комбинациюlower_bound()/upper_bound()
Оптимизация:
Если нужно часто проверять существование без доступа к значению,
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)
Особенности работы с диапазонами
Для
std::set:equal_range(key)всегда возвращает диапазон из 0 или 1 элементаlower_bound(key)иupper_bound(key)будут равны, если значение отсутствует
Для
std::multiset:equal_range(key)возвращает все элементы с заданным значениемlower_bound(key)указывает на первый элемент ≥ заданного значенияupper_bound(key)указывает на первый элемент > заданного значения
Все операции работают за 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 }
Оптимизация производительности
Для частых операций с диапазонами:
Сохраняйте итераторы, если возможно
Используйте
equal_rangeвместо отдельных вызововlower_bound/upper_bound
При массовой обработке:
Сначала определите границы диапазона
Затем обрабатывайте элементы за один проход
std::multiset<Employee> employees; // Эффективная обработка всех сотрудников отдела "IT" auto [first, last] = employees.equal_range("IT"); std::for_each(first, last, [](const Employee& emp) { emp.process_salary(); });
