Что такое хэш-таблица?

Это способ создать ассоциативный массив, который бы работал так же быстро, как обычный массив.

Почему обычный массив быстро работает?

Потому что для размещения элементов используется нумерация от 0 до N-1 (где N - количество элементов в массиве), поэтому для вычисления адреса памяти для доступа к элементу достаточно умножить индекс на размер элемента и прибавить к начальному адресу массива.

Почему нельзя точно также создать ассоциативный массив?

Потому что залогом эффективности массива является индексирование всех элементов целыми числами от 0 до N-1. В ассоциативном же массиве ключами могут быть экземпляры любого объекта, которые нельзя однозначно переводить в целые числа от 0 до N-1.

И в этом нам поможет хэширование?

Да, именно так, Функция хэширования выдаёт конкретное целое число от 0 до N-1 для любого экземпляра любого объекта.

Как можно получить целое число из экземпляра любого объекта?

Для этого в классе object есть метод getHashCode(), задача которого конвертировать любой экземпляр объекта в целое 32-битное число от -maxint до +maxint. При создании пользовательского класса для ключей, следует озаботиться переопределением этого метода и составлением хорошей формулы для вычисления хэш-кода. Это должна быть “чистая функция”: для одинаковых экземпляров необходимо выдавать одинаковые числа, а для разных - разные.

А понял! А потом нужно найти остаток от деления на N, да?

Совершенно верно, найти остаток от деления и вычислить абсолютное значение, чтобы избежать работы с отрицательными индексами. Именно такая формула лежит в основе хэш-функции:

Math.Abs(K.GetHashCode() % N)

А хватит ли N чисел для всех возможных вариантов ключей?

Нет, конечно. Множество возможных вариантов экземпляров ключей - число очень большое, непредсказуемое! Например, существует больше триллиона (256^5) значений для всех вариантов строчек из пяти ASCII символов. Во время хэширования мы можем получать одинаковые значения для совершенно разных ключей как на этапе вычисления функции getHashCode(реже), так и на этапе нахождения остатка (чаще). Эти совпадения называются коллизиями.

Можно привести пример коллизий из реальной жизни?

Например, если “хэшировать” людей по росту в миллиметрах, то будет возникать коллизии на первом этапе хэширования для людей одинакового возраста. 

А если, например, записывать режим дня на циферблате, то события в 6 и в 18 часов окажутся в одном месте. Это коллизия на этапе нахождения остатка. 

Как разрешать коллизии?

Есть разные способы, например “метод цепочек”, “метод открытой адресации”, “метод двойного хэширования”. Каждый способ определяет правило размещения и поиска элементов с одинаковыми хэш-значениями.

Можно поподробнее о методе цепочек?

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

А как мы сможем найти в этом списке элемент по ключу?

Для этого придётся хранить значение ключа. В обыкновенном массиве коллизий не бывает, каждый элемент имеет свой уникальный индекс от 0 до N-1, поэтому нет необходимости дополнительно хранить сам индекс. А в хэш-таблице мы обязаны в каждом элементе хранить не только значение, но и сам ключ, чтобы во время перебора элементов с одинаковыми хэш-значениями мы смогли однозначно найти нужный, либо рапортовать, что ключ не найден. Получаются накладные расходы на хранения всех ключей.

Можно перечислить все действия для добавления элемента в хэш-таблицу?

  • Первое. Вычислить хэш-код ключа, получим число int32

  • Второе. Найти остаток от деления на размер таблицы N - получим индекс.

  • Третье. Пройти по односвязному списку, чтобы убедиться, что такого ключа ещё нет.

  • Четвёртое. Разместить новый элемент в начало односвязного списка.

Вот карта сокровищ хэширования:

А всем места хватит?

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

А можно увеличить размер хэш-таблицы?

Да, конечно, не можно, а нужно! При достижении порогового значения необходимо выполнять рехэширование: создать новую таблицу удвоенного размера и перенести все элементы из старой таблицы в новую. При этом хэш-значения всех элементов изменятся, так как теперь будет вычисляться остаток от деления на новое значение N. В результате, количество коллизий может уменьшиться, а длина списков сократится, что ускорит время поиска элемента по ключу.

Как интересно. Можно узнать про другие алгоритмы?

Конечно, записывайтесь на курс “Алгоритмы и структуры данных” в Отус. Онлайн образование, мы там на вебинарах рассматриваем очень много разных алгоритмов, не только про хэширование, но и про двоичные деревья поиска, теорию графов, способы сжатия данных и шифрования, и многое другое.

Комментарии (6)