Зачем кому-то использовать set вместо unordered_set?


155

Представляем C ++ 0x, unordered_setкоторый доступен во boostмногих других местах. Я понимаю, что unordered_setэто хеш-таблица со O(1)сложностью поиска. С другой стороны, setэто не что иное, как дерево со log(n)сложностью поиска. Зачем кому-то использовать setвместо unordered_set? т.е. есть ли необходимость в этом set?


24
По сути, ваш вопрос заключается в том, есть ли необходимость в дереве.
Винко Врсалович

2
Думаю, я ясно сказал в первой строке, что это какой-то глупый вопрос. Мне чего-то не хватало, и теперь я получил ответ :)
AraK

2
Настоящая причина в том, что все не так черно, как кажется. Между ними много серых и других цветов. Вы должны помнить, что эти контейнеры - инструменты. Иногда производительность не имеет решающего значения, а удобство гораздо важнее. Если бы все люди искали наиболее эффективное решение, мы бы никогда не использовали C ++ (не говоря уже о Python) в первую очередь и постоянно писали и оптимизировали код на машинном языке.
AturSams,

(Зачем кому-то использовать общее имя для реализации / интерфейса с обещаниями, выходящими за рамки тех, что подразумеваются под этим именем, создавая неловкую ситуацию для тех, у кого нет?)
greybeard

Ответы:


228

Когда для кого-то, кто хочет перебирать элементы набора, порядок имеет значение.


Это упорядочено в соответствии с порядком вставки или в соответствии с реальным сравнением с использованием операторов < >?
SomethingSomething

3
По умолчанию он упорядочен с использованием std :: less; вы можете переопределить это и указать свой собственный оператор сравнения. cplusplus.com/reference/set/set
moonshadow

Или иногда, когда вы хотите только повторить, даже если порядок не имеет значения.
mfnx

332

Неупорядоченные наборы должны оплачивать свое среднее время доступа O (1) несколькими способами:

  • setиспользует меньше памяти, чем unordered_setдля хранения того же количества элементов.
  • Для небольшого количества элементов поиск в файле setможет быть быстрее, чем поиск в unordered_set.
  • Несмотря на то, что многие операции выполняются быстрее в среднем для unordered_set, они часто гарантируют лучшую сложность наихудшего случая для set(например insert).
  • Такая set сортировка элементов полезна, если вы хотите получить к ним доступ по порядку.
  • Вы можете лексикографически сравнивать разные setс с <, <=, >и >=. unordered_sets не требуются для поддержки этих операций.


10
+1, все отличные баллы. Люди склонны упускать из виду тот факт, что хэш-таблицы имеют среднее время доступа O (1) , что означает, что иногда они могут иметь большие задержки. Это различие может быть важным для систем реального времени.
j_random_hacker 03

Хорошие моменты, однако здесь ( en.cppreference.com/w/cpp/container/unordered_set/operator_cmp ) указано, что мы можем сравнивать unordered_sets.
Michiel uit het Broek

5
Определите «небольшое количество элементов»
Санджай Варма

4
@SunjayVarma обычно 100 элементов - хороший отрезок между ними. Если вы сомневаетесь, ничто не может заменить тестирование производительности обоих в вашем конкретном случае использования.
Нейт

3
@MichieluithetBroek Указано только сравнение на равенство, а не ordering ( <).
лисярус

29

Всякий раз, когда вы предпочитаете дерево хеш-таблице.

Например, хеш-таблицы в худшем случае имеют значение «O (n)». O (1) - средний случай. В худшем случае деревья - "O ( log n)".


19
/ Сбалансированные / деревья равны O (ln n) в худшем случае. Вы можете получить O (n) деревья (по сути, связанные списки).
strager

6
Если вы можете написать достаточно интеллектуальную хеш-функцию, вы почти всегда можете получить O (1) perf из хеш-таблицы. Если вы не можете написать такую ​​хэш-функцию, если вам нужно выполнить итерацию «по порядку» по вашему набору, тогда вам следует использовать дерево. Но вам не следует использовать дерево, потому что вы боитесь «O (n) худшего случая».
Джастин Л.

7
stager: Чтобы быть педантичным, да. Однако мы говорим о наборе в C ++, который обычно реализуется как сбалансированное двоичное дерево поиска . Чтобы говорить о сложности, нам нужно указать фактическую операцию. В этом контексте очевидно, что мы говорим о поиске.
Mehrdad Afshari

1
Джастин Л: Это лишь одна из причин, по которой вы можете предпочесть дерево. Суть моего ответа - первая строчка. Всякий раз, когда вы предпочитаете древовидную структуру данных хеш-таблице. Во многих случаях деревья предпочтительнее хеш-таблиц. Хеш-таблицы особенно плохо подходят для таких вещей, как «пересечения диапазонов».
Mehrdad Afshari

2
stl-деревья - это почти повсеместно реализованные красно-черные деревья, усовершенствованное самобалансирующееся дерево. Действительно, бывают случаи, когда поиск O (n) в худшем случае неприемлем. Веб-служба, которая предоставляет и интерфейс для хранения пользовательских значений, не должна использовать хэш-карту, поскольку злоумышленник может эффективно создать DoS, сохраняя специально созданные значения. Критические, чувствительные ко времени системы могут также не допускать O (n) поиска, управления воздушным движением и т.д. Хотя в целом вы правы, используйте хэш-карты по умолчанию и переключайте древовидную версию только тогда, когда у вас есть реальная необходимость.
deft_code 02

18

Используйте набор, когда:

  1. Нам нужны упорядоченные данные (отдельные элементы).
  2. Нам нужно будет распечатать / получить доступ к данным (в отсортированном порядке).
  3. Нам нужен предшественник / преемник элементов.

Используйте unordered_set, когда:

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

Примеры:

задавать:

Ввод: 1, 8, 2, 5, 3, 9

Выход: 1, 2, 3, 5, 8, 9

Неупорядоченный_набор:

Ввод: 1, 8, 2, 5, 3, 9

Вывод: 9 3 1 8 2 5 (возможно, этот порядок зависит от хеш-функции)

Главное отличие:

введите описание изображения здесь

Примечание: (в некоторых случаях setудобнее) например, используя в vectorкачестве ключа

set<vector<int>> s;
s.insert({1, 2});
s.insert({1, 3});
s.insert({1, 2});

for(const auto& vec:s)
    cout<<vec<<endl;   // I have override << for vector
// 1 2
// 1 3 

Причина vector<int>может быть такой же ключевой, setпотому что vectorпереопределение operator<.

Но если вы используете, unordered_set<vector<int>>вам нужно создать хеш-функцию для vector<int>, потому что вектор не имеет хеш-функции, поэтому вы должны определить ее, например:

struct VectorHash {
    size_t operator()(const std::vector<int>& v) const {
        std::hash<int> hasher;
        size_t seed = 0;
        for (int i : v) {
            seed ^= hasher(i) + 0x9e3779b9 + (seed<<6) + (seed>>2);
        }
        return seed;
    }
};

vector<vector<int>> two(){
    //unordered_set<vector<int>> s; // error vector<int> doesn't  have hash function
    unordered_set<vector<int>, VectorHash> s;
    s.insert({1, 2});
    s.insert({1, 3});
    s.insert({1, 2});

    for(const auto& vec:s)
        cout<<vec<<endl;
    // 1 2
    // 1 3
}

вы можете видеть, что в некоторых случаях unordered_setвсе сложнее.

В основном цитируется по: https://www.geeksforgeeks.org/set-vs-unordered_set-c-stl/ https://stackoverflow.com/a/29855973/6329006


6

Поскольку std :: set является частью Стандартного C ++, а unordered_set - нет. C ++ 0x НЕ является стандартом, как и Boost. Для многих из нас важна мобильность, а это значит, что нужно придерживаться стандарта.


2
Если я правильно его понимаю, он не спрашивает, почему люди в настоящее время все еще используют set. Он информирует себя о C ++ 0x.
Йоханнес Шауб - лит,

2
Может быть. Я думал, что все знают, что хеш-таблицы и деревья решают разные проблемы.

24
Ну, это стандарт в настоящее время (потребовалось всего несколько лет)
Клейтон Хьюз

6

Рассмотрим алгоритмы Sweepline. Эти алгоритмы совершенно не работают с хеш-таблицами, но прекрасно работают со сбалансированными деревьями. Чтобы дать вам конкретный пример алгоритма Sweepline, рассмотрим алгоритм фортуны. http://en.wikipedia.org/wiki/Fortune%27s_algorithm


1
Я думаю, что такая ссылка слишком сложна для данного вопроса. (Пришлось искать)
hectorpal

4

Еще одна вещь в дополнение к тому, что уже упоминали другие люди. Хотя ожидаемая амортизированная сложность для вставки элемента в unordered_set составляет O (1), время от времени она будет принимать O (п) , поскольку потребности хэш-таблицы , чтобы быть перестроена (количество ковшей необходимо изменить) - даже с «хорошая» хеш-функция. Точно так же, как вставка элемента в вектор время от времени требует O (n), потому что базовый массив необходимо перераспределить.

Вставка в набор всегда занимает не более O (log n). В некоторых приложениях это может быть предпочтительнее.


4

g++ 6.4 stdlibc ++ упорядоченный и неупорядоченный набор тестов

Я протестировал эту доминирующую реализацию Linux C ++, чтобы увидеть разницу:

введите описание изображения здесь

Полная информация о тестах и ​​их анализ приведены по адресу: Какова основная структура данных набора STL в C ++? и я не буду их здесь повторять.

«BST» означает «протестировано с помощью, std::setа« хэш-карта »означает« протестировано с помощью » std::unordered_set. «Куча» - это то, std::priority_queueчто я проанализировал в: Куча против двоичного дерева поиска (BST)

Вкратце:

  • график ясно показывает, что в этих условиях вставка хэш-карты всегда была намного быстрее, когда элементов более 100 тыс., и разница растет по мере увеличения количества элементов

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

  • кривые ясно показывают, что заказанный std::setоснован на BST и на std::unordered_setоснове хэш-карты. В справочном ответе я дополнительно подтвердил, что GDB пошагово отлаживал код.

Аналогичный вопрос для mapvs unordered_map: есть ли преимущество использования map над unordered_map в случае тривиальных ключей?


3

Простите меня, еще кое-что, что стоит отметить в отношении отсортированного свойства:

Если вам нужен диапазон данных в контейнере, например: вы сохранили время в наборе , и вам нужно время с 2013-01-01 по 2014-01-01.

Для unordered_set это невозможно.

Конечно, этот пример будет более убедительным для случаев использования между map и unordered_map .


2

Хотя этот ответ может быть запоздалым на 10 лет, стоит отметить, что он std::unordered_setтакже имеет недостатки в безопасности.

Если хеш-функция предсказуема (это обычно так, если она не применяет контрмеры, такие как рандомизированная соль), злоумышленники могут вручную обрабатывать данные, которые вызывают коллизии хешей и заставляют все вставки и поиски занимать время O (n). .

Это можно использовать для очень эффективных и элегантных атак типа «отказ в обслуживании».

Многие (большинство?) Реализации языков, которые используют хэш-карты внутри компании, столкнулись с этим:


1

Я бы сказал, что удобно иметь отношения, если вы хотите преобразовать их в другой формат.

Также возможно, что при более быстром доступе время для построения индекса или памяти, используемой при создании и / или доступе к нему, больше.


+1, обозначение Big Oh скрывает постоянные факторы, и для типичных размеров задач часто наиболее важны постоянные факторы.
j_random_hacker 03

1

Если вы хотите, чтобы все было отсортировано, вы должны использовать set вместо unordered_set. unordered_set используется вместо набора, когда порядок хранения не имеет значения.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.