Всем доброго времени суток!
Сегодня мне захотелось написать статью о таких зачастую важных на олимпиадах элементах STL, как map и set, то есть, о множествах. Понимаю, что практически все участники div. 1, которые пишут на С++ с ними знакомы и для них статья будет малополезной. Однако, как мне кажется, многие участники div. 2 напротив, могут о них не знать. В первую очередь на них и ориентирована данная статья. Также хочу отметить, что буду рад всякой критике, если Вы сочтёте её необходимой.
Итак, приступим. Как многие знают, "set" переводится с английского как "множество". Отсюда можно сделать логичный вывод, что set — это некая структура данных, которая реализует множество как математический объект. В С++11 есть две различные реализации множества. Первая пришла к нам из С++98. Это обычный set и основан он на реализации красно-чёрного дерева. Вторая реализация — unordered_set, которая была "узаконена" с введением стандарта С++11. Она основана на хеш-таблицах.
Для сета определены следующие полезные для нас операции:
1. Создание.
Как и для множество других контейнеров stl, чтобы объявить set, необходимо прописать
set<type_here> set_name_here;
Можно также использовать конструктор, чтобы сразу инициализировать контейнер. Инициализацию можно провести другим контейнером или парой итераторов [first, last).
2. Добавление.
Выполняется посредством перегруженной функции insert. Синтаксис её вызова:
set<*тип данных элементов, хранящихся в a*> a;
a.insert(*аргументы здесь*);
Вот её основные варианты:
pair<iterator,bool> insert( const value_type& value );
Принимает value — значение, которое следует вставить в множество и возвращает пару — итератор, указывающий на вставленный элемент и bool'еву переменную, обозначающую "успешность" вставки. Т.е. 1 — вставка произошла, 0 — нет. Данная функция выполняется за логарифм от размера контейнера, то есть, за O(logn).
iterator insert( iterator hint, const value_type& value );
Принимает значение value и итератор hint и пытается вставить элемент как можно ближе к итератору hint. Если вставка будет произведена сразу после или перед итератора hint, то функция отработает за O(1), иначе за O(logn).
template< class InputIt >
void insert( InputIt first, InputIt last );
Добавляет в set все элементы, находящиеся на промежутке [first;last) за O((last - first)logn). InputIt должен соответствовать требованиям InputIterator, т.е. поддерживать следующие операции:
- Сравнение (a!=b);
- Разыменование (*a);
- Инкремент (a++ и ++a);
- Инкремент значения (*a++);
При этом все указатели и итераторы на элементы set остаются в рабочем состоянии. Стоит также отметить, что set поддерживает лишь одно вхождение элемента с одинаковым значением, если же в множестве уже есть элемент с таким значением, он добавлен не будет. Однако, есть структура multiset, которая поддерживает множественные вхождения одного и того же элемента. Однако, операции над ней, за исключением небольших нюансов тождественны операциям над set и поэтому здесь она рассматриваться не будет.
3. Поиск элемента.
Существует несколько различных функций для поиска элементов в set. Вот они:
size_type count( const Key& key ) const;
Считает количество вхождений элемента с ключём key в контейнер. Очевидно, что в силу свойств контейнера, эта функция возвращает либо 0, либо 1. Работает за O(logn).
iterator find( const Key& key );
Находит элемент с ключём key. Если элемент найден, возвращает итератор на него, иначе на end() контейнера. Работает за O(logn).
iterator lower_bound( const Key& key );
Возвращает итератор на первый элемент в множестве, который больше, либо равен искомому. O(logn).
iterator upper_bound( const Key& key );
Возвращает итератор на первый элемент в множестве, который строго больше искомого. O(logn).
Обратите внимание! Для правильного использования этих функций, надо вызывать их как функции-члены контейнера.
set<int> t;
lower_bound(t.begin(),t.end(),number); // Неправильно! Работает за О(n)
t.lower_bound(number); // Правильно!
4. Удаление.
Существует также перегруженная функция erase(), которая позволяет удалять элементы. Вот её основные варианты:
iterator erase( const_iterator position );
Удаляет элемент, находящийся в позиции, указанной итератором. Амортизированная сложность — O(1).
size_type erase( const key_type& key );
Удаляет все элементы с ключевым значением key. Понятно, что, если это просто set, а не multiset, кол-во таких элементов не превышает 1. Работает за O(logn + count(key)).
void erase( iterator first, iterator last );
Удаляет все элементы в диапазоне [first;last). Работает за O(logn + distance(first, last)).
Теперь поговорим о map. Название происходит от mapping — ассоциативный массив. Операции на нём практически тождественны set, однако в элементах контейнера хранится не одно значение, а пары ключ-значение. Сортировка в данном случае будет проходить таким же образом, как при обычной сортировке пар — вначале приоритетом идёт сравнение первых элементов в паре, потом, в случае их равности, сравниваются вторые элементы. Также стоит отметить весьма удобную реализацию обращения по индексам. Например, вот такой код:
map<string,int> test_map;
test_map["ten"]=10;
test_map["ten"]=8;
Вначале создаст в контейнере test_map пару ("ten",10), а затем изменит второй элемент в ней на 8.
unordered_set и unordered_map поддерживают большинство операций, поддерживаемых set и map, но, в среднем, выполняют их за О(1). Несмотря на это, их не всегда выгодно использовать, т.к. unordered означает неупорядоченный, т.е. элементы в них не поддерживаются в отсортированном порядке. В связи с этим, мы не сможем использовать такие контейнеры для, например, пирамидной сортировки, а также мы не сможем за О(1) получать доступ к наибольшему и наименьшему элементу, как в set и map. И, как следствие неупорядоченности, мы не сможем использовать на этих контейнерах операции lower_bound и upper_bound.
Напоследок могу предложить несколько задач, которые можно достаточно легко (по сравнению с решением без STL) решить с использованием этих структур данных:
Что ж, надеюсь, что моя статья была полезной кому-нибудь. С нетерпением жду ваших отзывов о ней :)
P.S. понимаю, что в статье материал раскрыт не полностью, хоть я и старался это сделать. Если после прочтения остались какие-то вопросы или замечания, можете их высказать.
Я бы сказал, за O(N log N). Логарифм бинарного поиска плюс линия — доступ к элементу в сете в общем случае. Верно, нет?
Неверно. set не умеет получать элемент по номеру. Если бы умел, кстати (RB-дерево это умеет с хранением дополнительной информации), то не за линию, а за log.
А lower_bound для не-random access контейнеров работает за линию — просто пробегается по всем элементам при помощи итератора.
Если асимптотика O(N), то lower_bound должен при вызове кешировать контейнер. Я сомневаюсь, что он так работает.
"А lower_bound для не-random access контейнеров работает за линию — просто пробегается по всем элементам при помощи итератора." -- а факт поддержки метода можно проверять на шаблонах на этапе компиляции, и в соответствии с этим выбирать имплиментацию шаблона? Не покажите, а то я никогда такого не видел?
Насколько я вижу код, в той реализации, что у меня [реализацию скрыл в правке] проверок не делается, но это все равно O(N) + O(log N) ибо каждый раз когда мы делаем len итераций, мы уменьшаем нашу длину пополам
len/2
, а длина суммарно уменьшится наO(N)
, то есть мы сделаем не больше 2 N операций лишних по сравнению с бинпоиском. Но это дольше, чем просто 1 раз пройти за линию, да.Зачем? Если не экономить количество сравнений, то можно просто пройти по всему контейнеру за линию.
Ой, что только на этих шаблонах нельзя сделать... Они чуть ли не Тьюринг-полные. Да, и такое тоже можно. SFINAE, type_traits, static_assert и прочие страшные слова. Например
Как использовать set, если там не готовые типы вроде int, double, а свои типы? Я слышал, что нужно писать свой компаратор, но никогда его связать с set'ом не получалось, можете описать как это делать правильно?
Достаточно переопределить оператор < , так же, как и для сортировки.
А как это следует делать, если хочется использовать для хранения в set другой предикат, нежели тот, который используется для обычных сравнений?
Какой например предикат?
Можно написать
если Вы об этом.
В любом случае, нужно ввести на множестве отношение порядка. А сравнения > и == выражаются через <, так что переопределяется только один оператор.
Нет, я о том, чтобы использовать разные функции для обычного < и для хранения в set'e. Например, я хочу, чтобы у меня был set, в котором числа хранятся не по обычному порядку, а, скажем, лексикографически. Но при этом я не хочу перегружать оператор <, чтобы если я использовал его вне set, он бы работал как стандартный оператор <, а не сравнивающий лексикографически.
Использовать второй аргумент шаблона std::set:
Спасибо Avitella, добавил
const
Да, похоже, это именно то, что меня интересовало, спасибо =)
Может не сработать. Если что, попробуй добавить const.
На счет задачи 13 помню сколько она шуму подняла на УОИ из-за того что ее очень легко можно было решить на С++, а на любой другой среде было трудно.
Возможно, тем, кому интересна, нова и полезна данная тема, будут также интересны и полезны простые задачи с informatics.mccme.ru с номерами от 3749 по 3769. (Кстати, буду благодарен, если кто-то скажет точно, чьи это задачи, и как к ним штатно добраться по ссылкам, а не по номерам.)
http://informatics.mccme.ru/moodle/mod/statements/view.php?id=4535
Думаю, было бы полезно добавить следующие "пользующиеся спросом" рецепты:
1) Правильное удаление части элементов из контейнера
2) Использование собственных типов в set/unordered_set в качестве ключа (т.е. когда требуется реализовать компаратор/хэшер).
1) По-моему, это неправильное удаление части элементов из контейнера :) http://stackoverflow.com/questions/6438086/iterator-invalidation-rules
Как минимум, если я не ошибаюсь, то p++ произойдет после erase, а итератор на erased-элемент является invalidated сразу после удаления(кажется, даже для всех контейнеров), поэтому у меня есть подозрение, что это UB.
Приведенный мною способ правильный, потому что p++ возвращает значение, которое было перед инкрементом (в отличие от ++p). Способ придумал не я, это из книги Мейерса по STL.
Действительно, ты прав.
Не думал, что для set и map нет красивых и быстро работающих способов сделать это без использования итераторов.
1) Почему не так?
Проверил, вроде работает, и выглядит лучше.
Мне было бы куда более интересно узнать различие между unordered_set/set. (unordered_map/map)
unordered_X реализован на хеш-таблице, просто X на RB-дереве.
Вкратце это описано. Как уже отметил предыдущий комментатор, unordered реализован на хеш-таблицах. В целом, unordered структуры поддерживают такие же операции, что и обычные, но за O(n) в худшем случае и О(1) амортизированно, за исключением того, что хранящиеся в них значения неупорядоченны, следовательно, их нельзя использовать в качестве приоритетной очереди или подобной структуры, которая выдавала бы min/max за О(1) или сортировала бы элементы.
А чтобы положить свой класс в unordered set/unordered map необходимо ли реализовать свою хэш функцию для класса? Если да, то можно пример привести?
Да, надо для своего класса сделать специализацию шаблона
std::hash
. Проще всего вызватьstd::hash
для всех членов класса и как-то это скомбинировать. Пример можно найти здесь: http://en.cppreference.com/w/cpp/utility/hashБольшое спасибо, за то, что ты пишешь хорошие, полезные, статьи на ДВУХ языках. Мало кто так делает, а жалко. Такие посты очень сильно повышают наполненность полезной информацией Codeforces. Спасибо! =)
а как работают мультисет и мультимап? и как из них корректно удалять одиночные элементы?
find-ом получить итератор, а потом вызвать erase
Ага, тоже вчера на контесте это обнаружил:(
Зашла в комментарии написать об этой возможности :) Конечно, перед удалением надо проверить, что итератор валиден.
В set<> все хранится в отсортированном порядке. A можно ли как нибудь узнать индекс элемента в нем ?
Нельзя, хоть оно внутри и BST, но функциональности мало. Можно использовать gnu'шные структуры или своё дерево (AVL там или Treap).
Рахмет!
Is there any english translation of this blog ?
nope :c
just right click on the text and there should be an option to translate
How do we access adjacent elements in a set? For e.g. if my number is
x
then I use*lower_bound(s.begin(), s.end(), x + 1)
for the right bound, but how do I get the left bound?