Способы разрешения коллизий java

Что нужно знать об устройстве коллекций, основанных на хешировании

Всем привет. На связи Владислав Родин. В настоящее время я являюсь руководителем курса «Архитектор высоких нагрузок» в OTUS, а также преподаю на курсах, посвященных архитектуре ПО.

Помимо преподавания, как вы могли заметить, я занимаюсь написанием авторского материала для блога OTUS на хабре и сегодняшнюю статью хочу посвятить запуску нового потока курса «Алгоритмы для разработчиков».

Введение

Хеш-таблицы (HashMap) наравне с динамическими массивами являются самыми популярными структурами данных, применяемыми в production’е. Очень часто можно услышать вопросы на собеседованиях касаемо их предназначения, особенностей их внутреннего устройства, а также связанных с ними алгоритмов. Данная структура данных является классической и встречается не только в Java, но и во многих других языках программирования.

Постановка задачи

Давайте зададимся целью создать структуру данных, которая позволяет:

  • contains(Element element) проверять, находится в ней некоторый элемент или нет, за О(1)
  • add(Element element) добавлять элемент за О(1)
  • delete(Element element) удалять элемент за О(1)

Массив

Попробуем сделать эти операции поверх массива, который является самой простой структурой данных. Договоримся, что будем считать ячейку пустой, если в ней содержится null.

Проверка наличия

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

Добавление

Если нам надо добавить элемент абы куда, то вначале мы должны найти пустую ячейку, а затем записать в нее элемент. Таким образом, мы опять осуществим линейный поиск и получим асимптотику O(n).

Удаление

Чтобы удалить элемент, его нужно сначала найти, а затем в найденную ячейку записать null. Опять линейный поиск приводит нас к O(n).

Простейшее хеш-множество

Обратите внимание, что при каждой операции, мы сначала искали индекс нужной ячейки, а затем осуществляли операцию, и именно поиск портит нам асимптотику! Если бы мы научились получать данный индекс за O(1), то исходная задача была бы решена.

Читайте также:  Uuid java default value

Давайте теперь заменим линейный поиск на следующий алгоритм: будем вычислять значение некоторой фунции — хеш-функции, ставящей в соответствие объекту класса некоторое целое число. После этого будем сопоставлять полученное целое число индексу ячейки массива (это достаточно легко сделать, например, взяв остаток от деления этого числа на размер массива). Если хеш-функция написана так, что она считается за O(1) (а она так обычно и написана), то мы получили самую простейшую реализацию хеш-множества. Ячейка массива в терминах хеш-множества может называться bucket‘ом.

Проблемы простейшей реализации хеш-множества

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

Для разрешения коллизий придумано 2 метода: метод цепочек и метод открытой адресации.

Метод цепочек

Метод цепочек является наиболее простым методом разрешения коллизий. В ячейке массива мы будем хранить не элементы, а связанный список данных элементов. Потому как добавление в начало списка (а нам все равно в какую часть списка добавлять элемент) обладает асимптотикой О(1), мы не испортим общую асимптотику, и она останется равной О(1).

У данной реализации есть проблема: если списки будут очень сильно вырастать (в качестве крайнего случая можно рассмотреть хеш-функцию, которая возвращает константу для любого объекта), то мы получим асимптотику O(m), где m — число элементов во множестве, если размер массива фиксирован. Для избежания таких неприятностей вводится понятие коэффициент заполнения (он может быть равен, например, 1.5). Если при добавлении элемента оказывается, что доля числа элементов, находящихся в структуре данных по отношению к размеру массива, превосходит коэффициент заполнения, то происходит следующее: выделяется новый массив, размер которого превосходит размер старого массива (например в 2 раза), и структура данных перестраивается на новом массиве.

Читайте также:  Html5 javascript css framework

Данный метод разрешения коллизий и применяется в Java, а структура данных называется HashSet.

Метод открытой адресации

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

От хеш-множества к хеш-таблице (HashMap)

Давайте создадим структуру данных, которая позволяет так же быстро, как и хеш-множество (то есть за O(1)), добавлять, удалять, искать элементы, но уже по некоторому ключу.

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

Таким образом, вставка (put(Key key, Value value)) будет осуществляться так: мы посчитаем ячейку массива по объекту типа Key, получим номер bucket’а. Пройдемся по списку в bucket’е, сравнивая ключ с ключом в хранимых парах. Если нашли такой же ключ — просто вытесняем старое значение, если не нашли — добавляем пару.

Как осуществляется получение элемента по ключу? Да по похожему принципу: получаем bucket по ключу, проходимся по парам и возвращаем значение в паре, ключ в которой равен ключу в запросе, если такая пара есть, и null в противном случае.

Данная структура данных и называется хеш-таблицей.

Типичные вопросы на собеседовании

Q: Как устроены HashSet и HashMap? Как осуществляются основные операциии в данных коллекциях? Как применяются методы equals() и hashCode()?
A: Ответы на данные вопросы можно найти выше.

Q: Каков контракт на equals() и hashCode()? Чем он обусловлен?
A: Если объекты равны, то у них должны быть равны hashCode. Таким образом, hashCode должен определяться по подможноству полей, учавствующих в equals. Нарушение данного контракта может приводить к весьма интересным эффектам. Если объекты равны, но hashCode у них разный, то вы можете не достать значение, соответствующее ключу, по которому вы только что добавили объект в HashSet или в HashMap.

Читайте также:  Числовое поле в HTML5

Вывод

На собеседованиях очень любят задавать различные кейсы, связанные с этими структурами данных. При этом решение любого из них может быть выведено из понимания принципов их работы без всякой «зубрежки».

На этом все. Если вы дочитали материал до конца, приглашаю на бесплатный урок по теме «Секреты динамического программирования» в рамках урока мой коллега — Евгений Волосатов покажет как решить олимпиадную задачу используя идеи динамического программирования.

Источник

Разрешение коллизий в Java HashMap

Java HashMap использует метод put для вставки пары K/V в HashMap . Допустим, я использовал метод put и теперь HashMap имеет одну запись с ключом равным 10 и значением равным 17.

Если я вставлю 10,20 в эту HashMap , она просто заменит предыдущую запись этой записью из-за коллизии из-за одинакового ключа 10.

При столкновении ключей HashMap заменяет старую пару K/V на новую пару K/V.

Поэтому мой вопрос заключается в том, когда HashMap использует технику разрешения столкновений Chaining?

Почему он не сформировал связанный список с ключом 10 и значениями 17,20?

Когда вы вставляете пару (10, 17) , а затем (10, 20) , технически никакой коллизии не происходит. Вы просто заменяете старое значение новым для данного ключа 10 (поскольку в обоих случаях 10 равно 10, а также хэш-код для 10 всегда равен 10).

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

В качестве примера предположим, что две строки «abra ka dabra» и «wave my wand» дают хэш-коды 100 и 200 соответственно. Если предположить, что общий размер массива равен 10, то оба они окажутся в одном и том же ведре ( 100 % 10 и 200 % 10 ). Цепочка гарантирует, что всякий раз, когда вы делаете map.get( «abra ka dabra» ) , вы получаете правильное значение, связанное с ключом. В случае с хэш-картой в Java это делается с помощью метода equals .

Источник

Оцените статью