Hashset vs Treeset


496

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

У яких випадках я повинен використовувати HashSetпонад TreeSet?

Відповіді:


860

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

HashSet

  • клас пропонує постійну продуктивність часу для основних операцій (додавання, видалення, вміст та розмір).
  • це не гарантує, що порядок елементів буде залишатися незмінним у часі
  • Продуктивність ітерації залежить від початкової потужності та коефіцієнта навантаження HashSet.
    • Цілком безпечно прийняти коефіцієнт навантаження за замовчуванням, але ви можете вказати початкову потужність, яка приблизно вдвічі перевищує розмір, до якого ви очікуєте, що набір зросте.

TreeSet

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

Важливі моменти:

  • Обидва гарантують збір елементів без копій
  • Зазвичай швидше додавати елементи до HashSet, а потім перетворювати колекцію в TreeSet для відсортованого проходження без дублікатів.
  • Жодна з цих реалізацій не синхронізована. Тобто, якщо декілька потоків отримують доступ до набору одночасно, і принаймні один з потоків модифікує набір, він повинен бути синхронізований зовні.
  • LinkedHashSet є в деякому сенсі проміжним між HashSetі TreeSet. Реалізована як хеш-таблиця із пов'язаним списком, що проходить через неї, однак вона забезпечує впорядковану вставкою ітерацію, яка не є такою, як відсортований обхід, гарантований TreeSet .

Отже, вибір використання повністю залежить від ваших потреб, але я вважаю, що навіть якщо вам потрібна замовлена ​​колекція, ви все одно віддаєте перевагу HashSet для створення набору та перетворення його в TreeSet.

  • напр SortedSet<String> s = new TreeSet<String>(hashSet);

38
Лише я вважаю, що твердження "HashSet набагато швидше, ніж TreeSet (постійний час проти часового часу ...)" явно помиляється? По-перше, мова йде про складність у часі, а не про абсолютний час, і O (1) може бути у занадто багатьох випадках повільніше, ніж O (f (N)). По-друге, що O (logN) є «майже» O (1). Я не був би здивований, якби для багатьох поширених випадків TreeSet перевершив HashSet.
lvella

22
Я просто хочу другий коментар Ivella. часова складність НЕ те саме, що час роботи, і O (1) не завжди кращий за O (2 ^ n). Перекручений приклад ілюструє суть: розгляньте хеш-набір, використовуючи алгоритм хешу, який вимагає виконання 1 трильйона машинних інструкцій (O (1)) проти будь-якої загальної реалізації сортування міхура (O (N ^ 2) avg / najгіє) для 10 елементів . Сорт бульбашки виграє кожного разу. Сенс у тому, що алгоритми класів вчать всіх думати про наближення, використовуючи складність у часі, але в реальному світі постійні фактори МАТЕРІАЛЬНО .
Пітер Ехлерт

17
Можливо, це лише я, але чи не рада спочатку додати все до хештету, а потім приховати це до набору дерев жахливим? 1) Вставка в хеш -сет відбувається лише швидко, якщо ви знаєте розмір вашого набору даних заздалегідь, інакше ви платите повторне хешування O (n), можливо, кілька разів. і 2) Ви все одно платите за вкладку TreeSet при перетворенні набору. (з помстою, бо ітерація через хешсет не є надзвичайно ефективною)
TinkerTank

5
Ця порада заснована на тому, що для набору ви повинні перевірити, чи є предмет дублікатом, перш ніж додавати його; тому ви заощадите час, усуваючи дублікати, якщо ви використовуєте хеш-версію над деревом. Однак, враховуючи ціну, яку потрібно заплатити за створення другого набору для недублів, відсоток дублікатів повинен бути справді великим, щоб подолати цю ціну і зробити її економією часу. І звичайно, це для середніх і великих наборів, тому що для невеликого набору набір дерев, можливо, швидше, ніж хешсет.
SylvainL

5
@PeterOehlert: будь ласка, вкажіть орієнтир для цього. Я розумію вашу думку, але різниця між обома наборами ледь не має значення при невеликих розмірах колекції. І як тільки множина зростає до точки, де реалізація має значення, log (n) стає проблемою. Загалом, хеш-функції (навіть складні) набирають величини швидше, ніж кілька пропусків кешу (які у вас є на величезних деревах майже на кожному рівні доступу), щоб знайти / отримати доступ / додати / змінити лист. Принаймні, такий мій досвід роботи з цими двома наборами на Java.
Bouncner

38

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

Це на відміну від символу a HashSet, який розповсюджує записи по всій пам'яті, незалежно від їх клавіш.

Коли вартість затримки читання з жорсткого диска в тисячу разів перевищує вартість зчитування з кеша або оперативної пам'яті, а коли дані дійсно доступні з локацією, TreeSetвибір може бути набагато кращим.


3
Чи можете ви продемонструвати, що якщо два порядки є поряд у порядку, TreeSet розміщує їх поруч один з одним у структурі даних, а значить, і в пам'яті ?
Девід Сороко

6
Для Java зовсім неважливо. Елементи набору - це Об'єкти в будь-якому випадку і вказують де-небудь ще, тож ви нічого не економите.
Ендрю Галлаш

Окрім інших зауважень, зроблених щодо відсутності локальності в Java взагалі, реалізація TreeSet/ TreeMapне з боку OpenJDK не оптимізована. Хоча можна використовувати b-дерево порядку 4 для подання червоно-чорного дерева і, таким чином, покращити локальність та кеш-ефективність, але це не так, як працює реалізація. Натомість кожен вузол зберігає вказівник на власний ключ, власне значення, його батьківський і лівий і правий дочірні вузли, що видно у вихідному коді JDK 8 для TreeMap.Entry .
kbolino

25

HashSetє O (1) для доступу до елементів, тому це, безумовно, має значення. Але підтримувати порядок об’єктів у наборі неможливо.

TreeSetє корисним, якщо для вас має значення підтримка порядку (у значеннях, а не в порядку вставки). Але, як ви зазначали, ви торгуєте замовленням на повільний час для доступу до елемента: O (log n) для основних операцій.

З javadocs дляTreeSet :

Ця реалізація забезпечує гарантовану вартість журналу (п) час для основних операцій ( add, removeі contains).


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 буде доданий як перший Object в TreeSet. І будь-який об’єкт, доданий після цього, дасть NullPointerException у методі порівняння до порівняльного.
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

Базуючись на чудовій візуальній відповіді на Картах від @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               
╚══════════════╩═══════════════════════════════════════════════════════════════╝

13

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

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

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


будь-які точки даних щодо цих великих елементів, таких як 10K або більше
kuhajeyan

11

Якщо ви не вставляєте достатньо елементів, що призводить до частого переробки (або зіткнення, якщо ваш HashSet не може змінити розмір), HashSet, безумовно, дає вам перевагу постійного доступу до часу. Але на наборах з великим ростом або усадкою ви можете реально отримати кращі показники роботи з Treesets, залежно від реалізації.

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


7

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

Дискусія там також вказує на цікавий "середній" підхід до питання "Дерево проти Хеша". Java надає LinkedHashSet, що представляє собою HashSet з "орієнтованим на вставку" зв'язаним списком, що проходить через нього, тобто останній елемент у зв'язаному списку також є останнім часом вставленим у хеш. Це дозволяє уникнути непорозуміння невпорядкованого хешу, не несучи при цьому збільшення витрат TreeSet.


4

TreeSet є одним з двох відсортованих колекцій (інший TreeMap). Він використовує червоно-чорну структуру дерева (але ви це знали) і гарантує, що елементи будуть у порядку зростання, відповідно до природного порядку. За бажанням ви можете створити TreeSet з конструктором, який дозволяє вам надати колекції свої власні правила щодо того, яким повинен бути порядок (а не покладатися на впорядкування, визначене класом елементів), використовуючи Порівняльний або Порівняльний

і LinkedHashSet - це впорядкована версія HashSet, яка підтримує подвійно пов'язаний Список для всіх елементів. Використовуйте цей клас замість HashSet, коли ви дбаєте про порядок ітерації. Коли ви повторюєте HashSet, порядок непередбачуваний, тоді як LinkedHashSet дозволяє переглядати елементи в тому порядку, у якому вони були вставлені


3

Дано багато відповідей, виходячи з технічних міркувань, особливо щодо виконання. На мою думку, вибір між TreeSetі HashSetмає значення.

Але я б сказала, що вибір повинен спочатку спиратися на концептуальні міркування.

Якщо для об'єктів, якими потрібно маніпулювати, природне впорядкування не має сенсу, тоді не використовуйте TreeSet.
Це відсортований набір, оскільки він реалізує SortedSet. Таким чином, це означає, що вам потрібно перекрити функцію compareTo, яка повинна відповідати тій, що повертає функцію equals. Наприклад, якщо у вас є набір об'єктів класу під назвою Student, я не думаю, щоTreeSetце мало б сенс, оскільки немає природного впорядкування між студентами. Ви можете замовити їх за середньою оцінкою, добре, але це не "природне замовлення". Функція compareToповертає 0 не лише тоді, коли два об'єкти представляють одного учня, але і тоді, коли двоє різних учнів мають однакову оцінку. У другому випадку equalsповернеться помилковим (якщо ви не вирішите зробити останнє поверненням істинним, коли двоє різних учнів мають однакову оцінку, що зробить, що equalsфункція має оманливе значення, а не сказати неправильне значення.)
Зауважте, що ця послідовність між equalsі compareToнеобов’язково, але настійно рекомендується. Інакше контракт інтерфейсу Setрозірваний, що робить ваш код оманливим для інших людей, таким чином, також, можливо, призводить до несподіваної поведінки.

Це посилання може бути хорошим джерелом інформації щодо цього питання.


3

Навіщо яблука, коли можна апельсини?

Серйозно, хлопці та дівчата - якщо ваша колекція велика, читається і записується в газиліони разів, і ви платите за цикли процесора, то вибір колекції є актуальним ТІЛЬКИ, якщо вам НЕОБХІДНО це зробити краще. Однак у більшості випадків це насправді не має значення - кілька мілісекунд тут і там залишаються непоміченими по-людськи. Якщо це дійсно так важливо, чому ви не пишете код в асемблері чи C? [ще одна дискусія]. Тож справа в тому, якщо ви раді використовувати будь-яку колекцію, яку ви вибрали, і це вирішує вашу проблему [навіть якщо це не конкретно найкращий тип колекції для завдання] вибити себе з себе. Програмне забезпечення є ковким. Оптимізуйте код, де це необхідно. Дядько Боб каже, що передчасна оптимізація - корінь усього зла. Дядько Боб так говорить


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));
    }
}

1
У публікації сказано: Як правило, швидше додавати елементи до HashSet, а потім перетворювати колекцію в TreeSet для відсортованого проходження без дублікатів. Встановити <String> s = новий TreeSet <String> (hashSet); Мені цікаво, чому б не встановити <String> s = new TreeSet <String> () безпосередньо, якщо ми знаємо, що він буде використовуватися для сортованої ітерації, тому я зробив це порівняння і результат показав, що швидше.
gli00001

"У яких випадках я б хотів використовувати HashSet над TreeSet?"
Остін Генлі

1
моя думка полягає в тому, що якщо вам потрібно замовити, використовувати TreeSet поодинці краще, ніж ставити все в HashSet, а потім створити TreeSet на основі цього HashSet. Я взагалі не бачу значення HashSet + TreeSet від початкової публікації.
gli00001

@ gli00001: ти пропустив точку. Якщо ви цього не зробите завжди потрібен сортування вашого набору елементів, але ви збираєтесь ним маніпулювати досить часто, тоді вам варто буде використовувати хеш-пакет, щоб отримати перевагу від швидших операцій більшу частину часу. Для випадкових випадків, коли потрібно обробляти елементи по порядку, тоді просто загортайте набір дерев. Це залежить від вашого випадку використання, але це не так багато випадків незвичного використання (а це, ймовірно, передбачає набір, який не містить занадто багато елементів і зі складними правилами впорядкування).
haylem
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.