Як HashTables розглядають зіткнення?


97

Я чув на своїх класах ступенів, що a HashTableрозмістить новий запис у "наступному доступному" сегменті, якщо новий запис Key зіткнеться з іншим.

Як би HashTableвсе-таки повернути правильне значення, якщо це зіткнення трапляється під час виклику одного з ключем зіткнення?

Я припускаю, що тип Keysare Stringі hashCode()повертає значення за замовчуванням, породжене, скажімо, Java.

Якщо я реалізую власну функцію хешування та використовую її як частину таблиці пошуку (тобто a HashMapабо Dictionary), які стратегії існують для боротьби зі зіткненнями?

Я навіть бачив примітки, що стосуються простих чисел! Інформація не так зрозуміла з пошуку Google.

Відповіді:


92

Хеш-таблиці розглядають зіткнення одним із двох способів.

Варіант 1: Завдяки тому, що кожен сегмент містить зв’язаний список елементів, які хешуються до цього сегмента. Ось чому погана хеш-функція може дуже повільно виконувати пошук у хеш-таблицях.

Варіант 2: Якщо всі записи хеш-таблиці заповнені, то хеш-таблиця може збільшити кількість сегментів, які вона має, а потім перерозподілити всі елементи таблиці. Хеш-функція повертає ціле число, і хеш-таблиця повинна взяти результат хеш-функції та модифікувати його відповідно до розміру таблиці таким чином, щоб він міг бути впевненим, що потрапить у сегмент. Отже, збільшуючи розмір, він буде повторно обробляти і запускати обчислення за модулем, які, якщо вам пощастить, можуть надсилати об’єкти в різні сегменти.

Java використовує як варіант 1, так і 2 у своїх реалізаціях хеш-таблиць.


1
У випадку першого варіанту, чи є якась причина, чому замість масиву або навіть двійкового дерева пошуку використовується зв’язаний список?

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

2
Якщо використовується ланцюжок, як нам дають ключ, як ми знаємо, який предмет повернути?
ChaoSXDemon

1
@ChaoSXDemon, ви можете обходити список у ланцюжку за ключем, дублікати ключів не є проблемою, оскільки проблема полягає в двох різних ключах, що мають однаковий хеш-код.
ams

1
@ams: Який з них є кращим? чи існує якесь обмеження для зіткнення Хеша, після якого 2-й пункт виконується JAVA?
Шашанк Вівек,

77

Коли ви говорили про "Хеш-таблиця помістить новий запис у" наступний доступний "сегмент, якщо новий запис Ключа зіткнеться з іншим.", Ви говорите про стратегію відкритої адресації дозволу зіткнень хеш-таблиці.


Існує кілька стратегій хеш-таблиці для вирішення зіткнення.

Перший вид великих методів вимагає, щоб ключі (або вказівники на них) зберігалися в таблиці разом із відповідними значеннями, що додатково включає:

  • Окреме прикування

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

  • Відкрита адресація

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

  • З’єднане хешування
  • Зозуля гарбуза
  • Робін Гуд хешинг
  • Хешшю з двома варіантами
  • Хешпіт хешшюна

Іншим важливим методом боротьби зі зіткненнями є динамічна зміна розміру , яка має кілька способів:

  • Зміна розміру шляхом копіювання всіх записів
  • Збільшення розміру
  • Монотонні клавіші

РЕДАГУВАТИ : вищезазначене запозичено з wiki_hash_table , куди вам слід заглянути, щоб отримати додаткову інформацію.


3
"[...] вимагає, щоб ключі (або вказівники на них) зберігалися в таблиці разом із відповідними значеннями". Дякую, це те, що не завжди одразу стає зрозумілим, читаючи про механізми зберігання значень.
mtone

27

Існує декілька прийомів для обробки зіткнень. Поясню деякі з них

Прив’язка: при ланцюжку ми використовуємо індекси масивів для зберігання значень. Якщо хеш-код другого значення також вказує на той самий індекс, тоді ми замінюємо це значення індексу пов'язаним списком, і всі значення, що вказують на цей індекс, зберігаються у пов'язаному списку, а фактичний індекс масиву вказує на головку пов'язаного списку. Але якщо є лише один хеш-код, що вказує на індекс масиву, тоді значення безпосередньо зберігається в цьому індексі. Під час отримання значень застосовується та сама логіка. Це використовується в Java HashMap / Hashtable, щоб уникнути зіткнень.

Лінійне зондування: Цей прийом застосовується, коли в таблиці більше індексу, ніж значення, які слід зберегти. Техніка лінійного зондування працює над концепцією продовжувати нарощувати, поки не знайдете порожній слот. Псевдокод виглядає так:

index = h(k) 

while( val(index) is occupied) 

index = (index+1) mod n

Техніка подвійного хешування: У цій техніці ми використовуємо дві функції хешування h1 (k) та h2 (k). Якщо інтервал у h1 (k) зайнятий, то друга функція хешування h2 (k) використовується для збільшення індексу. Псевдокод виглядає так:

index = h1(k)

while( val(index) is occupied)

index = (index + h2(k)) mod n

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

Для отримання додаткової інформації див. Цей сайт .


18

Я настійно рекомендую вам прочитати цю публікацію в блозі, яка нещодавно з’явилася на HackerNews: Як працює HashMap у Java

Коротше, відповідь така

Що станеться, якщо два різних об'єкти ключа HashMap мають однаковий хеш-код?

Вони будуть зберігатися в одному сегменті, але не в наступному вузлі зв'язаного списку. А метод keys equals () буде використаний для ідентифікації правильної пари значень ключа в HashMap.


3
HashMaps дуже цікаві, і вони заглиблюються! :)
Alex

1
Я думаю, що питання стосується HashTables, а не HashMap
Прашант

10

Я чув на своїх класах ступенів, що HashTable розмістить новий запис у сегменті "наступний доступний", якщо новий запис Key зіткнеться з іншим.

Це насправді не так, по крайней мере , для Oracle JDK (це є деталлю реалізації , яка може варіюватися від різних реалізацій API). Натомість кожен сегмент містить зв’язаний список записів до Java 8 та збалансоване дерево в Java 8 або вище.

Тоді як би HashTable все-таки повернув правильне значення, якщо це зіткнення відбувається при виклику одного з ключем зіткнення?

Він використовує equals()для пошуку фактично відповідного запису.

Якщо я реалізую власну функцію хешування та використовую її як частину таблиці пошуку (тобто HashMap або Словник), які стратегії існують для боротьби зі зіткненнями?

Існують різні стратегії управління зіткненнями з різними перевагами та недоліками. Запис Вікіпедії про хеш-таблиці дає хороший огляд.


Це справедливо як для обох, так Hashtableі HashMapдля jdk 1.6.0_22 від Sun / Oracle.
Микита Рибак

@Nikita: не впевнений у Hashtable, і я зараз не маю доступу до джерел, але я на 100% впевнений, що HashMap використовує ланцюгове та нелінійне зондування у кожній окремій версії, яку я коли-небудь бачив у своєму налагоджувачі.
Майкл Борґвардт,

@Michael Ну, я зараз переглядаю джерело HashMap public V get(Object key)(та сама версія, що і вище). Якщо ви знайдете точну версію там, де з’являються ці зв’язані списки, мені буде цікаво знати.
Микита Рибак

@Niki: Я зараз розглядаю той самий метод, і я бачу, що він використовує цикл for для ітерації пов'язаного списку Entryоб'єктів:localEntry = localEntry.next
Майкл Борґвардт

@Michael Вибачте, це моя помилка. Я трактував код неправильно. природно, e = e.nextні ++index. +1
Микита Рибак

7

Оновлення з Java 8: Java 8 використовує самозбалансоване дерево для обробки зіткнень, покращуючи найгірший випадок з O (n) до O (log n) для пошуку. Використання самоврівноваженого дерева було введено в Java 8 як покращення в порівнянні з ланцюжком (використовувалося до Java 7), яке використовує пов'язаний список і має гірший випадок O (n) для пошуку (оскільки йому потрібно пройти список)

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

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

Обробка колізій призводить до найгірших результатів вставки та пошуку від O (1) у випадку відсутності обробки колізії до O (n) для ланцюжка (зв’язаний список використовується як вторинна структура даних) та O (log n) для самоврівноваженого дерева.

Список літератури:

Java 8 має наступні вдосконалення / зміни об'єктів HashMap у разі сильних зіткнень.

  • Альтернативну хеш-функцію рядка, додану в Java 7, було видалено.

  • Відра, що містять велику кількість зіткнених ключів, зберігатимуть свої записи у збалансованому дереві замість пов’язаного списку після досягнення певного порогу.

Вищезазначені зміни забезпечують роботу O (log (n)) у найгірших випадках ( https://www.nagarro.com/en/blog/post/24/performance-improvement-for-hashmap-in-java-8 )


Чи можете ви пояснити, наскільки гіршим варіантом вставки для зв’язаного списку HashMap є лише O (1), а не O (N)? Мені здається, що якщо у вас коефіцієнт зіткнень 100% для не дублюючих ключів, вам в кінцевому підсумку доведеться обходити кожен об'єкт у HashMap, щоб знайти кінець пов'язаного списку, так? Чого мені не вистачає?
mbm29414

У конкретному випадку реалізації хеш-карти ви насправді маєте рацію, але не тому, що вам потрібно знайти кінець списку. У загальному випадку реалізації зв'язаного списку вказівник зберігається як на голову, так і на хвіст, а отже, вставка може бути виконана в O (1), приєднавши наступний вузол до хвоста безпосередньо, але у випадку хеш-карти, метод вставки потрібно забезпечити відсутність дублікатів, і, отже, повинен здійснити пошук у списку, щоб перевірити, чи елемент вже існує, і, отже, ми закінчуємо O (n). Отож саме властивість set, накладена на пов’язаний список, є причиною O (N). Я
внесу


4

Оскільки існує певна плутанина щодо того, який алгоритм використовує Java HashMap (у реалізації Sun / Oracle / OpenJDK), тут відповідні фрагменти вихідного коду (з OpenJDK, 1.6.0_20, на Ubuntu):

/**
 * Returns the entry associated with the specified key in the
 * HashMap.  Returns null if the HashMap contains no mapping
 * for the key.
 */
final Entry<K,V> getEntry(Object key) {
    int hash = (key == null) ? 0 : hash(key.hashCode());
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

Цей метод (цитування з рядків 355 - 371) викликається при пошуку записів у таблиці, наприклад, від get(), containsKey()та деяких інших. Цикл for тут проходить через пов'язаний список, сформований об'єктами введення.

Ось код для об'єктів введення (рядки 691-705 + 759):

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    final int hash;

    /**
     * Creates new entry.
     */
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

  // (methods left away, they are straight-forward implementations of Map.Entry)

}

Відразу після цього приходить addEntry()метод:

/**
 * Adds a new entry with the specified key, value and hash code to
 * the specified bucket.  It is the responsibility of this
 * method to resize the table if appropriate.
 *
 * Subclass overrides this to alter the behavior of put method.
 */
void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);
}

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

Отже, ось зв’язаний список входів для кожного сегмента, і я дуже сумніваюся, що це змінилося з _20на _22, оскільки так було з 1.2.

(Цей код є (c) 1997-2007 Sun Microsystems і доступний під GPL, але для копіювання краще використовувати оригінальний файл, що міститься у src.zip у кожному JDK від Sun / Oracle, а також у OpenJDK.)


1
Я позначив це як спільноту wiki , оскільки насправді це не відповідь, більше обговорення інших відповідей. У коментарях просто не вистачає місця для такого цитування коду.
Paŭlo Ebermann

3

ось дуже проста реалізація хеш-таблиці в Java. лише у знаряддях put()та get(), але ви можете легко додати все, що вам подобається. він покладається на hashCode()метод Java, який реалізований усіма об'єктами. ви можете легко створити власний інтерфейс,

interface Hashable {
  int getHash();
}

і змусити його реалізуватись за допомогою клавіш, якщо хочете.

public class Hashtable<K, V> {
    private static class Entry<K,V> {
        private final K key;
        private final V val;

        Entry(K key, V val) {
            this.key = key;
            this.val = val;
        }
    }

    private static int BUCKET_COUNT = 13;

    @SuppressWarnings("unchecked")
    private List<Entry>[] buckets = new List[BUCKET_COUNT];

    public Hashtable() {
        for (int i = 0, l = buckets.length; i < l; i++) {
            buckets[i] = new ArrayList<Entry<K,V>>();
        }
    }

    public V get(K key) {
        int b = key.hashCode() % BUCKET_COUNT;
        List<Entry> entries = buckets[b];
        for (Entry e: entries) {
            if (e.key.equals(key)) {
                return e.val;
            }
        }
        return null;
    }

    public void put(K key, V val) {
        int b = key.hashCode() % BUCKET_COUNT;
        List<Entry> entries = buckets[b];
        entries.add(new Entry<K,V>(key, val));
    }
}

2

Існують різні методи розв’язання зіткнень, деякі з них - окремий ланцюжок, відкрита адресація, хешування Робіна Гуда, хешування зозулі тощо.

Java використовує окремий ланцюжок для вирішення зіткнень у таблицях хешу. Тут є чудове посилання на те, як це відбувається: http://javapapers.com/core-java/java-hashtable/

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.