Хэшсет против Трисет

497

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

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

heymatthew
источник

Ответы:

861

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

HashSet

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

TreeSet

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

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

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

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

  • например SortedSet<String> s = new TreeSet<String>(hashSet);
sactiw
источник
38
Только я нахожу утверждение «HashSet намного быстрее, чем TreeSet (постоянное время по сравнению с временем регистрации ...)», совершенно неверно? Во-первых, речь идет о сложности времени, а не об абсолютном времени, и O (1) во многих случаях может быть медленнее, чем O (f (N)). Во-вторых, O (logN) является «почти» O (1). Я не удивлюсь, если во многих распространенных случаях TreeSet превзойдет HashSet.
lvella
22
Я просто хочу прокомментировать комментарий Ивеллы. временная сложность - это НЕ то же самое, что время выполнения, и O (1) не всегда лучше, чем O (2 ^ n). Извращенный пример иллюстрирует эту мысль: рассмотрим хеш-набор с использованием алгоритма хеширования, для выполнения которого потребовалось 1 триллион машинных инструкций (O (1)) по сравнению с любой обычной реализацией пузырьковой сортировки (O (N ^ 2) avg / худший) для 10 элементов. , Пузырьки будут побеждать каждый раз. Дело в том , алгоритмах классы учат всех думать о приближении с использованием временной сложности , но в реальном мире факторов постоянных ЗНАЧЕНИЯ часто.
Питер Элерт
17
Может быть, это только я, но разве не совет сначала добавить все в хэш-набор, а затем превратить его в ужасный набор? 1) Вставка в хэш-набор выполняется быстро, только если вы заранее знаете размер своего набора данных, в противном случае вы платите O (n) повторное хэширование, возможно, несколько раз. и 2) Вы платите за вставку TreeSet в любом случае при конвертации набора. (с удвоенной силой, потому что итерация по хэш-сету не очень эффективна)
TinkerTank
5
Этот совет основан на том факте, что для набора необходимо проверить, является ли элемент дубликатом, прежде чем добавлять его; поэтому вы сэкономите время на удалении дубликатов, если будете использовать хэш-набор над набором деревьев. Однако, учитывая цену, которую придется заплатить за создание второго набора для недубликатов, процент дубликатов должен быть действительно велик, чтобы преодолеть эту цену и сэкономить время. И, конечно, это для средних и больших наборов, потому что для небольшого набора набор деревьев, возможно, быстрее, чем хэш-набор.
SylvainL
5
@PeterOehlert: пожалуйста, предоставьте тест для этого. Я понимаю вашу точку зрения, но разница между двумя наборами едва ли имеет значение при небольших размерах коллекций. И как только набор растет до точки, где реализация имеет значение, log (n) становится проблемой. Как правило, значения хеш-функций (даже сложных) на порядок быстрее, чем несколько промахов в кеше (которые вы имеете на огромных деревьях практически для каждого уровня доступа) для поиска / доступа / добавления / изменения листа. По крайней мере, таков мой опыт работы с этими двумя наборами в Java.
Bouncner
38

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

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

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

Карл Андерсен
источник
3
Можете ли вы продемонстрировать, что если две записи находятся рядом в порядке, TreeSet размещает их рядом друг с другом в структуре данных и, следовательно, в памяти ?
Дэвид Сороко
6
Совершенно не имеет значения для Java. Элементы набора в любом случае являются объектами и указывают куда-то еще, поэтому вы ничего не экономите.
Эндрю Галлаш
Помимо других комментариев, сделанных по поводу отсутствия локальности в Java, реализация OpenJDK TreeSet/ TreeMapне оптимизирована для локальности. Хотя можно использовать b-дерево порядка 4 для представления красно-черного дерева и, таким образом, улучшить локальность и производительность кэша, это не то, как работает реализация. Вместо этого каждый узел хранит указатель на свой собственный ключ, свое собственное значение, свой родительский и левый и правый дочерние узлы, что видно из исходного кода JDK 8 для TreeMap.Entry .
Кболино
25

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

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

Из Javadocs дляTreeSet :

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

duffymo
источник
22

1.HashSet позволяет нулевой объект.

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

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

например

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

 HashSet<String> hs = new HashSet<String>();
 hs.add(null); // runs fine
Сурен
источник
3
ts.add (null) это будет нормально работать в случае TreeSet, если null добавляется как первый объект в TreeSet. И любой объект, добавленный после этого, выдаст исключение NullPointerException в методе сравнения Comparator.
Shoaib Chikate
2
Вы действительно не должны добавлять nullк своему сету в любом случае.
пушистый
TreeSet<String> badassTreeSet = new TreeSet<String>(new Comparator<String>() { public int compare(String string1, String string2) { if (string1 == null) { return (string2 == null) ? 0 : -1; } else if (string2 == null) { return 1; } else { return string1.compareTo(string2); } } }); badassTreeSet.add("tree"); badassTreeSet.add("asdf"); badassTreeSet.add(null); badassTreeSet.add(null); badassTreeSet.add("set"); badassTreeSet.add("tree"); System.out.println(badassTreeSet);
Давид Хорват
21

Основываясь на прекрасном визуальном ответе на Maps от @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               
╚══════════════╩═══════════════════════════════════════════════════════════════╝
kiedysktos
источник
13

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

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

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

Кэти Ван Стоун
источник
любые данные указывают на эти крупные элементы, такие как 10K или более
kuhajeyan
11

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

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

JasonTrue
источник
7

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

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

Иосиф Вайсман
источник
4

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

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

Subhash Lagate
источник
3

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

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

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

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

Марек Стэнли
источник
3

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

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

user924272
источник
1

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

Николас Джордан
источник
-3
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

public class HashTreeSetCompare {

    //It is generally faster to add elements to the HashSet and then
    //convert the collection to a TreeSet for a duplicate-free sorted
    //Traversal.

    //really? 
    O(Hash + tree set) > O(tree set) ??
    Really???? Why?



    public static void main(String args[]) {

        int size = 80000;
        useHashThenTreeSet(size);
        useTreeSetOnly(size);

    }

    private static void useTreeSetOnly(int size) {

        System.out.println("useTreeSetOnly: ");
        long start = System.currentTimeMillis();
        Set<String> sortedSet = new TreeSet<String>();

        for (int i = 0; i < size; i++) {
            sortedSet.add(i + "");
        }

        //System.out.println(sortedSet);
        long end = System.currentTimeMillis();

        System.out.println("useTreeSetOnly: " + (end - start));
    }

    private static void useHashThenTreeSet(int size) {

        System.out.println("useHashThenTreeSet: ");
        long start = System.currentTimeMillis();
        Set<String> set = new HashSet<String>();

        for (int i = 0; i < size; i++) {
            set.add(i + "");
        }

        Set<String> sortedSet = new TreeSet<String>(set);
        //System.out.println(sortedSet);
        long end = System.currentTimeMillis();

        System.out.println("useHashThenTreeSet: " + (end - start));
    }
}
gli00001
источник
1
В сообщении говорится, что обычно быстрее добавить элементы в HashSet, а затем преобразовать коллекцию в TreeSet для сортированного обхода без дубликатов. Set <String> s = new TreeSet <String> (hashSet); Мне интересно, почему бы не установить <String> s = new TreeSet <String> () напрямую, если мы знаем, что он будет использоваться для отсортированной итерации, поэтому я сделал это сравнение, и результат показал, что быстрее.
gli00001
«В каких случаях я хотел бы использовать HashSet поверх TreeSet?»
Остин Хенли
1
Я хочу сказать, что если вам нужен порядок, лучше использовать TreeSet, чем помещать все в HashSet, а затем создавать TreeSet на основе этого HashSet. Я не вижу значения HashSet + TreeSet вообще из исходного поста.
gli00001
@ gli00001: вы упустили момент. Если вам не всегда нужно сортировать набор элементов, но вы будете манипулировать им довольно часто, тогда вам стоит использовать хэш-набор, чтобы в большинстве случаев получать выгоду от более быстрых операций. Для случайных раз , когда вам нужно обрабатывать элементы в порядке, а затем просто обернуть с TreeSet. Это зависит от вашего варианта использования, но это не так уж много необычного варианта использования (и это, вероятно, предполагает набор, который не содержит слишком много элементов и со сложными правилами упорядочения).
Хайлем