Способы разрешения коллизий
В продолжение темы о хэшмапах, как структура данных таковые полагаются полностью на хэшфункцию - что логично. Но поскольку коллизии в общем случае неизбежны (исключения - идеальные хэш- и identity-функции), то их разрешать как-то всё равно да и приходится. И основополагающих принципа существует всего два:
1) Закрытая адресация (она же separate chaining) - способ был впервые описан во внутреннем меморандуме в IBM в 1953 году. Это как раз классическая схема, которая была использована и в старой гошной мапе. Базовый случай - хэшфункцией мы адресуем связной список из вхождений (вхождение есть пара ключ-значение), где уже линейно искомое значение и ищем. Гошная мапа просто немного уменьшила футпринт за счёт связного списка бакетов, где бакет держит сразу до 8 вхождений, и сравнения по верхушке хэша, что даёт хороший бонус для значений, у которых операция сравнения является довольно дорогой (те же строки).
2) Открытая адресация - появилась чуть позже, пускай и с разницей всего в пару лет (что неудивительно - основой послужил как раз-таки тот самый меморандум с закрытой адресацией). В ней больше не используется концепция двухмерного массива, и заместо все значения лежат в одном большом массиве (где вхождения называют слотами). Само разрешение коллизий тогда работает достаточно просто - всё так же адресуем слот в массиве по хэшу, и если он нам не подходит (для вставки - уже оккупирован, для лукапа - не совпадает ключ), тогда мы просто идём дальше и смотрим следующий слот. Правда, как именно мы выбираем следующий слот - тоже тема для отдельного разговора: есть linear probing, когда мы сдвигаем индекс слота на константу (обычно 1), есть quadratic probing, когда мы, соответственно, сдвигаем индекс уже на результат какого-нибудь квадратического уравнения. Есть совсем уж страшное double hashing, когда сдвиг разрешается второй хэш-функцией. Суть методов quadratic probing и double hashing заключается исключительно в том, чтобы снизить так званую кластеризацию - неравномерное распределение элементов, когда они скапливаются на каком-то одном промежутке (double hashing даёт наилучший результат, но по производительности самый худший, а вот quadratic probing есть золотая середина с достаточно тонко подобранными значениями). Плоха кластеризация тем, что без перебора слотов в общем случае не обойтись, а это линейная сложность - всё-таки хотелсь бы амортизированную константу, пускай качество самой хэш-функции и отыгрывает здесь гораздо большую роль - но мир, увы, не идеален.
Конечно, помимо вышеперечисленных двух способов, существуют и другие разновидности (Coalesced hashing, Cuckoo hashing, Hopscotch hashing, Robin Hood hashing), но все они, так или иначе, основываются на открытой адресации.
Фанфакт: "открытая адресация" открытая потому, что все элементы вот у нас в одном массивчике как надо лежат, и мы по ним по всем проходимся, когда как при закрытой адресации все слоты находятся в отдельной структуре данных, виртуально не имеющего ничего общего с массивом этих самых слотов. То есть, при открытой адресации мы адресуем слот со значением, а при закрытой - коробочку, по которой дальше уже как-то сам ходи ищи-свищи.
Post #524
253
- 👍 4
- ❤ 1