Hashset против Treeset

Я всегда любил деревья, эти красивые O(n*log(n)) и их опрятность. Однако каждый инженер-программист, которого я когда-либо знал, многозначительно спрашивал меня, зачем мне использовать TreeSet. Исходя из опыта CS, я не думаю, что имеет значение то, что вы используете, и я не хочу возиться с хэш-функциями и бакетами (в случае Java).

В каких случаях мне следует использовать HashSet вместо TreeSet?


person heymatthew    schedule 23.09.2009    source источник


Ответы (14)


HashSet намного быстрее, чем TreeSet (постоянное время по сравнению с временем журнала для большинства операций, таких как добавление, удаление и содержит), но не предлагает таких гарантий упорядочения, как TreeSet.

HashSet

  • класс предлагает постоянное время выполнения основных операций (добавление, удаление, содержание и размер).
  • это не гарантирует, что порядок элементов останется постоянным с течением времени.
  • iteration performance depends on the initial capacity and the load factor of the HashSet.
    • It's quite safe to accept default load factor but you may want to specify an initial capacity that's about twice the size to which you expect the set to grow.

TreeSet

  • гарантирует log (n) временные затраты для основных операций (добавить, удалить и содержать)
  • гарантирует, что элементы набора будут отсортированы (по возрастанию, естественный или указанный вами через его конструктор) (реализует _ 1_)
  • не предлагает никаких параметров настройки для выполнения итераций
  • предлагает несколько удобных методов для работы с упорядоченным набором, например _ 2_, last(), _ 4_ и tailSet() и т. Д.

Важные точки:

  • Оба гарантируют сбор элементов без дублирования.
  • Как правило, быстрее добавлять элементы в HashSet, а затем преобразовывать коллекцию в TreeSet для отсортированного обхода без дублирования.
  • Ни одна из этих реализаций не синхронизирована. То есть, если несколько потоков обращаются к набору одновременно, и хотя бы один из потоков изменяет набор, он должен быть синхронизирован извне.
  • LinkedHashSet в некотором смысле занимает промежуточное положение между HashSet и TreeSet. Однако, реализованный в виде хеш-таблицы со связанным списком, через нее проходит она обеспечивает итерацию с упорядоченной вставкой, которая отличается от отсортированного обхода, гарантированного TreeSet.

Таким образом, выбор использования полностью зависит от ваших потребностей, но я считаю, что даже если вам нужна упорядоченная коллекция, вы все равно должны предпочесть HashSet для создания Set, а затем преобразовать его в TreeSet.

  • e.g. SortedSet<String> s = new TreeSet<String>(hashSet);
person sactiw    schedule 16.12.2010
comment
Может быть, будет больше смысла, если в вашем примере назначить новый TreeSet типу SortedSet. - person tanyehzheng; 15.06.2011
comment
Что с потреблением памяти? - person matdumsa; 14.11.2011
comment
@matdumsa TreeSet имеет указатели как в дочернем, так и в родительском направлениях, за это наверняка придется заплатить. - person Ahmet Alp Balkan; 26.11.2011
comment
Только я считаю, что утверждение, что HashSet намного быстрее, чем TreeSet (постоянное время по сравнению с лог-временем ...), явно неверно? Во-первых, речь идет о временной сложности, а не об абсолютном времени, и O (1) во многих случаях может быть медленнее, чем O (f (N)). Во-вторых, O (logN) почти O (1). Я не удивлюсь, если во многих распространенных случаях TreeSet превзойдет HashSet. - person lvella; 21.06.2012
comment
Я просто хочу повторить комментарий Ивеллы. временная сложность НЕ то же самое, что время выполнения, и O (1) не всегда лучше, чем O (2 ^ n). Извращенный пример иллюстрирует эту мысль: рассмотрим хеш-набор с использованием хеш-алгоритма, для выполнения которого потребовалось 1 триллион машинных инструкций (O (1)) по сравнению с любой распространенной реализацией пузырьковой сортировки (O (N ^ 2) в среднем / худшем) для 10 элементов. . Сортировка пузырьков выигрывает каждый раз. Дело в том, что классы алгоритмов учат всех думать об аппроксимациях с использованием временной сложности, но в реальном мире часто используются постоянные факторы MATTER. - person Peter Oehlert; 05.07.2012
comment
Возможно, это только я, но разве совет сначала добавить все в хэш-набор, а затем скрыть его в наборе деревьев не ужасен? 1) Вставка в хеш-набор выполняется быстро только в том случае, если вы заранее знаете размер своего набора данных, в противном случае вы платите O (n) за повторное хеширование, возможно, несколько раз. и 2) Вы все равно платите за установку TreeSet при преобразовании набора. (с удвоенной силой, потому что итерация через хешсет не очень эффективна) - person TinkerTank; 20.12.2012
comment
Этот совет основан на том факте, что для набора вы должны проверить, не является ли элемент дубликатом, прежде чем добавлять его; поэтому вы сэкономите время, удаляя дубликаты, если используете хэш-набор вместо древовидного набора. Однако, учитывая цену, которую нужно заплатить за создание второго набора для недубликатов, процент дубликатов должен быть действительно большим, чтобы преодолеть эту цену и сэкономить время. И, конечно же, это для средних и больших наборов, потому что для небольшого набора древовидный набор, возможно, быстрее, чем хэш-набор. - person SylvainL; 31.12.2012
comment
@PeterOehlert: укажите ориентир для этого. Я понимаю вашу точку зрения, но разница между обоими наборами практически не имеет значения при небольших размерах коллекции. И как только набор вырастет до точки, в которой реализация имеет значение, log (n) становится проблемой. В общем, хэш-функции (даже сложные) на порядок быстрее, чем несколько промахов кеша (которые у вас есть на огромных деревьях почти для каждого доступного уровня) для поиска / доступа / добавления / изменения листа. По крайней мере, таков мой опыт работы с этими двумя наборами на Java. - person Bouncner; 25.01.2013
comment
Это, вероятно, не так уж и важно, но HashSet, хотя и с постоянным временем, представляет собой около 120 байт-кодов, выполняемых для вставки. Так что массивы лучше, если список небольшой. (даже линейный поиск !!!) Во-вторых: HashMap.reset () настолько ужасен, что простой вызов нового HashMap значительно эффективнее. (Просто говорю, спасибо MIT Battlecode) - person ThePrimeagen; 13.11.2013
comment
Сложность НЕ определяет относительную производительность двух разных структур данных. Он определяет, как производительность данной структуры данных изменяется в зависимости от n. Очевидно, прямо в определении, но очень часто ошибались. Хеш-функция для большой строки переменной длины настолько загружает процессор, что вы можете сделать МНОГО простых сравнений за то же время. Престижность OP за указание, что вы ДОЛЖНЫ заранее знать размер окончательной хеш-таблицы. Деревья же, напротив, полностью самоподдерживаются. Они растут и уменьшаются по требованию, автоматически магическим образом. - person ; 06.05.2014
comment
Я склонен думать о хэшах как о сжатии с потерями, потому что это то, что они делают - сокращение диапазона с коллизиями. Поскольку открытое хеширование превращается в катастрофу коллизий, когда коэффициент загрузки превышает 0,50, вы должны добавить стоимость повторного хеширования к стоимости первичного хеширования в циклах ЦП. Если вы знаете достаточно о своих данных, чтобы избежать горячих точек, можете жить с неравномерным временем извлечения и большим количеством потраченного впустую табличного пространства, хэши, как правило, быстрее для небольших ключей. Лучше всего рядом с голым металлом, а хуже - рядом с пользователем. Они, как правило, постепенно превращаются в проблемы с обслуживанием, поскольку данные медленно меняются. - person ; 06.05.2014
comment
Однажды мне посоветовали использовать хеш-таблицу для хеширования кортежа из 28 переменных, около 20 из которых были двойными числами с плавающей запятой. Даже с 64-битными целыми числами это, вероятно, неосуществимо или вообще невозможно. Используя кортежи в C ++ STL, вы можете делать это с помощью карт (деревьев R-B) в течение всего дня без каких-либо проблем. - person ; 06.05.2014
comment
как насчет вычисления пересечения - какой из этих двух предпочтительнее? - person Tad; 15.09.2014
comment
Для HashSet - ›TreeSet код JDK 8 имеет более быструю линейную версию addAll, если входом является SortedSet, тем не менее, если входом является HashSet, он просто вызывает add () для каждого элемента. Это то же самое, что и выполнение индивидуального add () для каждого элемента непосредственно через TreeSet. Следовательно, я не думаю, что создание HashSet, а затем создание TreeSet с использованием HashSet на самом деле быстрее, чем просто использование TreeSet. Я не делал никаких оценок времени для того же самого, но похоже, что просто лучше напрямую создать TreeSet и использовать его, если вам в конечном итоге нужно, чтобы окончательная форма была как TreeSet. - person Quin; 26.08.2015
comment
Повторите свой последний абзац. Было бы справедливо сказать, что Tree set - это то, что вам нужно, если вы хотите непрерывно отсортированную коллекцию - person Richard Tingle; 09.12.2015
comment
Установить ‹String› set = new HashSet ‹› (); set.add (A); set.add (B); set.add (D); set.add (C); System.out.println (набор); - person Gundamaiah; 26.03.2018
comment
Вышеупомянутые отпечатки [A, B, C, D]. Поскольку Hashset не поддерживает порядок, а то, как он будет напечатан по порядку. - person Gundamaiah; 26.03.2018
comment
@Gundamaiah измените элементы набора на: Set<String> set= new HashSet<>() {{add("AZ"); add("DV"); add("BY"); add("EU"); add("CX");}}; Then System.out.println (set); напечатает: [EU, DV, CX, BY, AZ] Таким образом, вы можете видеть, что не поддерживается ни алфавитный порядок, ни порядок вставки. - person sactiw; 05.06.2020
comment
Учитывая большое количество предположений, сделанных здесь, я провел небольшой тест, который вы можете увидеть как paiza .io проект. - person julien.giband; 15.04.2021
comment
Изменить: учитывая большое количество предположений, сделанных здесь, я провел небольшой тест, который вы можете увидеть как проект paiza.io. Он строит несколько больших наборов случайных строк, а затем выполняет в них большое количество поисков, сравнивая HashSet и TreeSet. Затем он использует различные методы для сортировки хеш-набора. Как оказалось, использование TreeSet обычно намного медленнее, чем ожидалось. Что касается сортировки, упаковка с помощью TreeSet очень хорошо работает для HashSet среднего размера, но становится значительно медленнее, чем просто использование .stream().sort() для больших наборов. - person julien.giband; 15.04.2021

Одно еще не упомянутое преимущество TreeSet состоит в том, что он имеет большую "локальность", что является сокращением для выражения (1) если две записи находятся рядом в порядке, TreeSet помещает их рядом друг с другом в структуре данных и, следовательно, в памяти. ; и (2) это размещение использует принцип локальности, согласно которому аналогичные данные часто используются приложением с одинаковой частотой.

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

Когда стоимость задержки чтения с жесткого диска в тысячи раз превышает стоимость чтения из кеша или ОЗУ, и когда доступ к данным действительно осуществляется с локального доступа, TreeSet может быть гораздо лучшим выбором.

person Carl Andersen    schedule 30.09.2011
comment
Можете ли вы продемонстрировать, что если две записи находятся рядом в порядке, TreeSet помещает их рядом друг с другом в структуре данных и, следовательно, в памяти? - person David Soroko; 10.05.2015
comment
Совершенно неактуально для Java. Элементы набора в любом случае являются объектами и указывают куда-то еще, так что вы ничего не экономите. - person Andrew Gallasch; 08.07.2015
comment
Помимо других комментариев об отсутствии локальности в Java в целом, реализация OpenJDK _1 _ / _ 2_ не оптимизирована для локальности. Хотя можно использовать b-дерево порядка 4 для представления красно-черного дерева и, таким образом, улучшить локальность и производительность кеширования, реализация работает иначе. Вместо этого каждый узел хранит указатель на свой собственный ключ, свое собственное значение, его родительский и левый и правый дочерние узлы, что очевидно в исходный код JDK 8 для TreeMap.Entry. - person kbolino; 03.04.2020

Основываясь на прекрасном визуальном ответе на Картах от @shevchyk, вот мое мнение:

╔══════════════╦═════════════════════╦═══════════════════╦═════════════════════╗
║   Property   ║       HashSet       ║      TreeSet      ║     LinkedHashSet   ║
╠══════════════╬═════════════════════╬═══════════════════╬═════════════════════╣
║              ║  no guarantee order ║ sorted according  ║                     ║
║   Order      ║ will remain constant║ to the natural    ║    insertion-order  ║
║              ║      over time      ║    ordering       ║                     ║
╠══════════════╬═════════════════════╬═══════════════════╬═════════════════════╣
║ Add/remove   ║        O(1)         ║     O(log(n))     ║        O(1)         ║
╠══════════════╬═════════════════════╬═══════════════════╬═════════════════════╣
║              ║                     ║   NavigableSet    ║                     ║
║  Interfaces  ║         Set         ║       Set         ║         Set         ║
║              ║                     ║    SortedSet      ║                     ║
╠══════════════╬═════════════════════╬═══════════════════╬═════════════════════╣
║              ║                     ║    not allowed    ║                     ║
║  Null values ║       allowed       ║ 1st element only  ║      allowed        ║
║              ║                     ║     in Java 7     ║                     ║
╠══════════════╬═════════════════════╩═══════════════════╩═════════════════════╣
║              ║   Fail-fast behavior of an iterator cannot be guaranteed      ║
║   Fail-fast  ║ impossible to make any hard guarantees in the presence of     ║
║   behavior   ║           unsynchronized concurrent modification              ║
╠══════════════╬═══════════════════════════════════════════════════════════════╣
║      Is      ║                                                               ║
║ synchronized ║              implementation is not synchronized               ║
╚══════════════╩═══════════════════════════════════════════════════════════════╝
person kiedysktos    schedule 12.04.2017

HashSet - это O (1) для доступа к элементам, поэтому это, безусловно, имеет значение. Но поддерживать порядок объектов в наборе невозможно.

TreeSet полезен, если для вас важно поддержание порядка (с точки зрения значений, а не порядка вставки). Но, как вы заметили, вы торгуете приказом на более медленное время для доступа к элементу: O (log n) для базовых операций.

Из javadocs для TreeSet:

Эта реализация обеспечивает гарантированные временные затраты журнала (n) для основных операций (add, remove и contains).

person duffymo    schedule 23.09.2009

1.HashSet допускает нулевой объект.

2.TreeSet не разрешает нулевой объект. Если вы попытаетесь добавить нулевое значение, это вызовет исключение NullPointerException.

3.HashSet намного быстрее, чем TreeSet.

e.g.

 TreeSet<String> ts = new TreeSet<String>();
 ts.add(null); // throws NullPointerException

 HashSet<String> hs = new HashSet<String>();
 hs.add(null); // runs fine
person SuReN    schedule 16.11.2012
comment
ts.add (null) он будет работать нормально в случае TreeSet, если null добавлен в качестве первого объекта в TreeSet. И любой объект, добавленный после этого, выдаст исключение NullPointerException в методе compareTo Comparator. - person Shoaib Chikate; 29.01.2015
comment
Вы действительно действительно не должны добавлять null в свой набор в любом случае. - person fluffy; 26.03.2016
comment
@ShoaibChikate Ваше утверждение неточно в моей версии Java (Oracle Corporation 11.0.4 + 10-LTS). Первый вставленный элемент всегда сравнивается с самим собой, поэтому выдается NullPointerException, если первым элементом является null. - person M. Justin; 06.10.2020
comment
Это не совсем так. Если TreeSet были созданы с помощью компаратора, который допускает нулевые значения, тогда могут быть добавлены нули. Per TreeSet.add(E e): Выдает: NullPointerException - если указанный элемент имеет значение NULL и в этом наборе используется естественный порядок, или его компаратор не разрешает элементы Null. Это успешно добавляет null к TreeSet: new TreeSet<>(Comparator.nullsLast(Comparator.naturalOrder())).add(null);. - person M. Justin; 06.10.2020

Причина, по которой чаще всего используется HashSet, заключается в том, что операции (в среднем) составляют O (1) вместо O (log n). Если набор содержит стандартные элементы, вы не будете «возиться с хеш-функциями», как это было сделано за вас. Если набор содержит пользовательские классы, вы должны реализовать hashCode для использования HashSet (хотя в Effective Java показано, как это сделать), но если вы используете TreeSet, вы должны сделать его Comparable или предоставить Comparator. Это может быть проблемой, если в классе нет определенного порядка.

Я иногда использовал TreeSet (или фактически TreeMap) для очень маленьких наборов / карт (‹10 элементов), хотя я не проверял, есть ли реальный выигрыш от этого. Для больших наборов разница может быть значительной.

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

person Kathy Van Stone    schedule 23.09.2009
comment
любые точки данных для этих больших элементов, таких как 10K или более - person kuhajeyan; 24.06.2015

Если вы не вставляете достаточно элементов, чтобы приводить к частым повторным хешированиям (или столкновениям, если ваш HashSet не может изменить размер), HashSet, безусловно, дает вам преимущество постоянного доступа по времени. Но на наборах с большим увеличением или уменьшением вы можете получить лучшую производительность с помощью Treesets, в зависимости от реализации.

Амортизированное время может быть близко к O (1) при работающем красно-черном дереве, если мне не изменяет память. У книги Окасаки было бы лучшее объяснение, чем я могу. (Или просмотрите его список публикаций)

person JasonTrue    schedule 23.09.2009

Реализации HashSet, конечно, намного быстрее - меньше накладных расходов, потому что нет упорядочивания. Хороший анализ различных реализаций Set в Java представлен по адресу http://java.sun.com/docs/books/tutorial/collections/implementations/set.html.

Обсуждение там также указывает на интересный подход «золотой середины» к вопросу о дереве и хэше. Java предоставляет LinkedHashSet, который представляет собой HashSet с проходящим через него связанным списком, ориентированным на вставку, то есть последний элемент в связанном списке также является последним элементом, вставленным в Hash. Это позволяет избежать неправильного использования неупорядоченного хэша без увеличения стоимости TreeSet.

person Joseph Weissman    schedule 23.09.2009

TreeSet - это одна из двух отсортированных коллекций (вторая - TreeMap). Он использует красно-черную древовидную структуру (но вы это знали) и гарантирует, что элементы будут располагаться в порядке возрастания в соответствии с естественным порядком. При желании вы можете создать TreeSet с конструктором, который позволяет вам дать коллекции свои собственные правила для того, каким должен быть порядок (вместо того, чтобы полагаться на порядок, определенный классом элементов), используя Comparable или Comparator

и LinkedHashSet - это упорядоченная версия HashSet, которая поддерживает двусвязный список для всех элементов. Используйте этот класс вместо HashSet, когда вам важен порядок итераций. Когда вы перебираете HashSet, порядок непредсказуем, в то время как LinkedHashSet позволяет вам перебирать элементы в том порядке, в котором они были вставлены.

person subhash laghate    schedule 10.12.2010

Зачем есть яблоки, если можно есть апельсины?

Серьезно, ребята и девчонки - если ваша коллекция большая, читается и записывается миллионы раз, и вы платите за циклы процессора, то выбор коллекции актуален ТОЛЬКО в том случае, если вам НУЖНО, чтобы она работала лучше. Однако в большинстве случаев это не имеет особого значения - несколько миллисекунд тут и там остаются незамеченными с точки зрения человека. Если это действительно так важно, почему вы не пишете код на ассемблере или C? [указать на другое обсуждение]. Итак, суть в том, что если вы довольны использованием той коллекции, которую выбрали, и это решает вашу проблему (даже если это не лучший тип коллекции для данной задачи), вы выбиваете себе самоуверенность. Программное обеспечение податливо. При необходимости оптимизируйте свой код. Дядя Боб говорит, что преждевременная оптимизация - это корень всех зол. Так говорит дядя Боб

person user924272    schedule 16.05.2017
comment
Даже если вы используете свой набор через Set<T> ссылку, вам немедленно нужно выбрать конкретный класс для его создания и предоставить необходимые методы (равно, сравнить, хэш-код). Это не никакая оптимизация, а только попытка сделать правильный выбор, чтобы вам не пришлось его менять позже. - person Michel Billaud; 25.03.2021

Даже спустя 11 лет никто не подумал упомянуть о очень важной разнице.

Считаете ли вы, что если HashSet равно TreeSet, то верно и обратное? Взгляните на этот код:

TreeSet<String> treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
HashSet<String> hashSet = new HashSet<>();
treeSet.add("a");
hashSet.add("A");
System.out.println(hashSet.equals(treeSet));
System.out.println(treeSet.equals(hashSet));

Попробуйте угадать результат, а затем наведите указатель мыши на фрагмент, чтобы увидеть, каков реальный результат. Готовый? Ну вот:

ложь
истина

Правильно, они не содержат отношения эквивалентности для компаратора, несовместимого с равенством. Причина этого в том, что TreeSet использует компаратор для определения эквивалентности, а HashSet использует equals. Внутри они используют HashMap и TreeMap, поэтому вы должны ожидать такого поведения и с упомянутыми Map.

Первоначальный ответ

person Aniket Sahrawat    schedule 22.07.2020

Редактировать сообщение (полное переписывание) Когда порядок не имеет значения, вот когда. Оба должны выдавать Log (n) - было бы полезно посмотреть, быстрее ли один из них более чем на пять процентов. HashSet может дать тест O (1) в цикле, чтобы выяснить, есть ли это.

person Nicholas Jordan    schedule 23.09.2009

Было дано множество ответов, основанных на технических соображениях, особенно в отношении производительности. На мой взгляд, выбор между TreeSet и HashSet имеет значение.

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

Если для объектов вам нужно манипулировать, естественный порядок не имеет смысла, тогда не используйте TreeSet.
Это отсортированный набор, поскольку он реализует SortedSet. Это означает, что вам нужно переопределить функцию compareTo, которая должна соответствовать тому, что возвращает функция equals. Например, если у вас есть набор объектов класса Student, то я не думаю, что TreeSet будет иметь смысл, поскольку между учениками нет естественного порядка. Вы можете отсортировать их по средней оценке, но это не «естественный порядок». Функция compareTo вернет 0 не только тогда, когда два объекта представляют одного и того же учащегося, но также и тогда, когда у двух разных учеников одинаковая оценка. Во втором случае equals вернет false (если вы не решите сделать последнее возвращать true, когда два разных студента имеют одинаковую оценку, что приведет к тому, что функция equals будет иметь вводящее в заблуждение значение, если не сказать неправильное значение.)
Обратите внимание, что соответствие между equals и compareTo необязательно, но настоятельно рекомендуется. В противном случае контракт интерфейса Set будет нарушен, что приведет к тому, что ваш код будет вводить в заблуждение других людей, что также может привести к неожиданному поведению.

Эта ссылка может быть хорошим источником информации по этому вопросу.

person Marek Stanley    schedule 11.02.2013

person    schedule
comment
В сообщении говорится, что обычно быстрее добавлять элементы в HashSet, а затем преобразовывать коллекцию в TreeSet для отсортированного обхода без дублирования. Установить ‹String› s = new TreeSet ‹String› (hashSet); Мне интересно, почему бы не установить ‹String› s = new TreeSet ‹String› () напрямую, если мы знаем, что он будет использоваться для отсортированной итерации, поэтому я провел это сравнение, и результат показал, что он быстрее. - person gli00001; 26.09.2012
comment
В каких случаях я хотел бы использовать HashSet вместо TreeSet? - person Austin Henley; 26.09.2012
comment
Я хочу сказать, что если вам нужен порядок, лучше использовать только TreeSet, чем помещать все в HashSet, а затем создавать TreeSet на основе этого HashSet. Я вообще не вижу значения HashSet + TreeSet из исходного сообщения. - person gli00001; 08.10.2012
comment
@ gli00001: вы упустили суть. Если вам не всегда нужно сортировать ваш набор элементов, но вы собираетесь манипулировать им довольно часто, то вам стоит использовать хэш-набор, чтобы получить выгоду от более быстрых операций. большую часть времени. В редких случаях, когда вам нужно обработать элементы по порядку, просто используйте древовидный набор. Это зависит от вашего варианта использования, но это не так уж и редко (и, вероятно, предполагает набор, который не содержит слишком много элементов и со сложными правилами упорядочивания). - person haylem; 15.11.2012