Как HashTables справляется с коллизиями?

98

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

Как все HashTableже вернуть правильное значение, если это столкновение произойдет при вызове одного обратно с помощью ключа столкновения?

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

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

Я даже видел заметки, относящиеся к простым числам! Информация не так понятна из поиска Google.

Alex
источник

Ответы:

93

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

Вариант 1. Если каждая корзина содержит связанный список элементов, хешируемых в эту корзину. Вот почему из-за плохой хеш-функции поиск в хеш-таблицах может быть очень медленным.

Вариант 2. Если все записи хеш-таблицы заполнены, хеш-таблица может увеличить количество имеющихся в ней сегментов, а затем перераспределить все элементы в таблице. Хеш-функция возвращает целое число, а хеш-таблица должна принимать результат хеш-функции и изменять его в соответствии с размером таблицы, чтобы можно было быть уверенным, что он попадет в корзину. Таким образом, увеличивая размер, он будет повторно хешировать и запускать вычисления по модулю, которые, если вам повезет, могут отправить объекты в разные корзины.

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

амс
источник
1
В случае первого варианта, по какой причине используется связанный список вместо массива или даже двоичного дерева поиска?
1
приведенное выше объяснение является высокоуровневым, я не думаю, что это имеет большое значение для связанного списка и массива. Я думаю, что двоичное дерево поиска было бы излишним. Кроме того, я думаю, что если вы углубитесь в такие вещи, как ConcurrentHashMap и другие, там будет много деталей реализации на низком уровне, которые могут повлиять на производительность, которые не учитываются в приведенном выше объяснении высокого уровня.
AM
2
Если используется цепочка, при получении ключа, как мы узнаем, какой элемент вернуть?
ChaoSXDemon
1
@ChaoSXDemon, вы можете перемещаться по списку в цепочке по ключу, дублирующиеся ключи не являются проблемой, проблема в том, что два разных ключа имеют одинаковый хэш-код.
AMS
1
@ams: Какой из них предпочтительнее? Есть ли предел для коллизии хэша, после которого 2-я точка выполняется JAVA?
Шашанк Вивек
79

Когда вы говорили о том, что «Hash Table поместит новую запись в« следующую доступную »корзину, если новая запись Key сталкивается с другой.», Вы говорите о стратегии открытой адресации разрешения конфликтов хеш-таблицы.


Для хеш-таблицы существует несколько стратегий разрешения конфликтов.

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

  • Отдельная цепочка

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

  • Открытая адресация

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

  • Объединенное хеширование
  • Кукушка хеширования
  • Робин Гуд хеширование
  • Хеширование с двумя вариантами
  • Хеширование в классическом стиле

Другой важный метод обработки столкновений - это динамическое изменение размера , которое также имеет несколько способов:

  • Изменение размера путем копирования всех записей
  • Постепенное изменение размера
  • Монотонные клавиши

РЕДАКТИРОВАТЬ : приведенное выше заимствовано из wiki_hash_table , куда вы должны пойти, чтобы получить дополнительную информацию.

Herohuyongtao
источник
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.

Zengr
источник
3
HashMaps очень интересны и глубоки! :)
Alex
1
Я думаю, что вопрос касается HashTables, а не HashMap
Прашант
10

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

Это на самом деле не так, по крайней мере , для Oracle JDK (это является деталью реализации , которая может варьироваться от различных реализаций API). Вместо этого каждая корзина содержит связанный список записей до Java 8 и сбалансированное дерево в Java 8 или выше.

тогда как HashTable по-прежнему вернет правильное значение, если это столкновение произойдет при вызове одного обратно с ключом столкновения?

Он использует, equals()чтобы найти действительно совпадающую запись.

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

Существуют различные стратегии обработки столкновений с разными преимуществами и недостатками. Статья Википедии о хэш-таблицах дает хороший обзор.

Майкл Боргвардт
источник
Это верно для обоих 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
Майкл Боргвардт,
@ Майкл Извини, это моя ошибка. Я неправильно интерпретировал код. естественно, 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 (журнал n) для самоуравновешенного дерева.

Ссылки:

Java 8 содержит следующие улучшения / изменения объектов HashMap в случае высоких коллизий.

  • Альтернативная хеш-функция String, добавленная в 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
В конкретном случае реализации hashmap вы действительно правы, но не потому, что вам нужно найти конец списка. В общем случае реализации связанного списка указатель хранится как в голове, так и в хвосте, и, следовательно, вставка может быть выполнена в O (1) путем непосредственного присоединения следующего узла к хвосту, но в случае хэш-карты метод вставки должен гарантировать отсутствие дубликатов и, следовательно, должен искать в списке, чтобы проверить, существует ли уже элемент, и, следовательно, мы получаем O (n). Итак, свойство set, наложенное на связанный список, вызывает O (N). Внесу поправку в свой ответ :)
Дэниел Валланд
4

Он будет использовать метод equals, чтобы увидеть, присутствует ли ключ, даже, особенно если в одной корзине более одного элемента.

Судно на воздушной подушке, полное угрей
источник
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);
}

Это добавляет новую запись в начало корзины со ссылкой на старую первую запись (или null, если таковой нет). Точно так же removeEntryForKey()метод просматривает список и заботится об удалении только одной записи, оставляя остальную часть списка нетронутой.

Итак, вот список связанных записей для каждого сегмента, и я очень сомневаюсь, что он изменился с _20на _22, поскольку так было с 1.2 и далее.

(Этот код (c) Sun Microsystems 1997-2007 гг. И доступен под GPL, но для копирования лучше использовать исходный файл, содержащийся в src.zip в каждом JDK от Sun / Oracle, а также в OpenJDK.)

Пало Эберманн
источник
1
Я пометил это как вики сообщества , поскольку на самом деле это не ответ, а скорее обсуждение других ответов. В комментариях просто не хватает места для таких цитирований кода.
Паоло Эберманн,
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/

Настой полыни н асфодель
источник