Разрешение коллизий
Поиск свободного места при закрытом хешировании - задача, возникающая при создании хеш-таблицы, использующей так называемое закрытое хеширование.
При использовании открытого хеширования такой проблемы не возникает, так как там в каждой ячейке хранится список всех элементов. При добавлении необходимо просто добавить элемент в начало списка.
Закрытое хеширование работает иначе: в каждой ячейке хеш-таблицы хранится только один элемент. Тогда при добавлении, если ячейка свободна, мы просто записываем добавляемый элемент в эту ячейку. Однако если эта ячейка занята - необходимо поместить добавляемый элемент в какую-нибудь другую свободную ячейку. Такие ситуации нередки, так как невозможно использовать хеш-функцию, не дающую коллизий, а каждой ячейке таблицы соответствует одно значение хеш-функции. Далее мы рассмотрим несколько стратегий поиска свободного места в данном случае.
Стратегии поиска
Последовательный поиск
При попытке добавить элемент в занятую ячейку
начинаем последовательно просматривать ячейки и так далее, пока не найдём свободную ячейку. В неё и запишем элемент.Линейный поиск
Выбираем шаг
. При попытке добавить элемент в занятую ячейку начинаем последовательно просматривать ячейки и так далее, пока не найдём свободную ячейку. В неё и запишем элемент. По сути последовательный поиск - частный случай линейного, где .Квадратичный поиск
Шаг
не фиксирован, а изменяется квадратично: . Соответственно при попытке добавить элемент в занятую ячейку начинаем последовательно просматривать ячейки и так далее, пока не найдём свободную ячейку.Проверка наличия элемента в таблице
Проверка осуществляется аналогично добавлению: мы проверяем ячейку
и другие, в соответствии с выбранной стратегией, пока не найдём искомый элемент или свободную ячейку.При поиске элемента может получится так, что мы дойдём до конца таблицы. Обычно поиск продолжается, начиная с другого конца. Однако, если мы придём в ту ячейку, откуда начинался поиск, то добавить элемент в текущую таблицу будет невозможно и необходимо провести операцию перехеширования.
Проблемы данных стратегий
Проблем две - крайне нетривиальное удаление элемента из таблицы и образование кластеров. Кластер - последовательность занятых клеток. Их наличие замедляет все операции с хеш-таблицей: при добавлении требуется перебирать всё больше элементов, при проверке тоже. Чем больше в таблице элементов, тем больше в ней кластеры и тем выше вероятность того, что добавляемый элемент попадёт в кластер. Для защиты от кластеризации используется двойное хеширование и хеширование кукушки.
Двойное хеширование
Двойное хеширование — метод борьбы с коллизиями, возникающими при закрытом хешировании, основанный на использовании двух хеш-функций для построения различных последовательностей исследования хеш-таблицы.
Принцип двойного хеширования
При двойном хешировании используются две независимые хеш-функции
и . Пусть — это наш ключ, — размер нашей таблицы, — остаток от деления на , тогда сначала исследуется ячейка с адресом , если она уже занята, то рассматривается , затем и так далее. В общем случае идёт проверка последовательности ячеек гдеТаким образом, операции вставки, удаления и поиска в лучшем случае выполняются за линейного разрешения коллизий. Однако в среднем, при грамотном выборе хеш-функций, двойное хеширование будет выдавать лучшие результаты, за счёт того, что вероятность совпадения значений сразу двух независимых хеш-функций ниже, чем одной.
, в худшем — за , что не отличается от обычного
Выбор хеш-функций
может быть обычной хеш-функцией. Однако чтобы последовательность исследования могла охватить всю таблицу, должна возвращать значения:
- не равные
- независимые от
- взаимно простые с величиной хеш-таблицы
Есть два удобных способа это сделать. Первый состоит в том, что в качестве размера таблицы используется простое число, а
возвращает натуральные числа, меньшие . Второй — размер таблицы является степенью двойки, а возвращает нечетные значения.Например, если размер таблицы равен
, то в качестве можно использовать функцию видаПример
Показана хеш-таблица размером 13 ячеек, в которой используются вспомогательные функции:
Мы хотим вставить ключ 14. Изначально
. Тогда . Но ячейка с индексом 1 занята, поэтому увеличиваем на 1 и пересчитываем значение хеш-функции. Делаем так, пока не дойдем до пустой ячейки. При получаем . Ячейка с номером 9 свободна, значит записываем туда наш ключ.Таким образом, основная особенность двойного хеширования состоит в том, что при различных
пара дает различные последовательности ячеек для исследования.Простая реализация
Пусть у нас есть некоторый объект
, в котором определено поле , от которого можно вычислить хеш-функции иТак же у нас есть таблица
величиной , состоящая из объектов типа .Вставка
add(item) x = h1(item.key) y = h2(item.key) for (i = 0; i < m; i++) if table[x] == null table[x] = item return x = (x + y) mod m table.resize() //ошибка, требуется увеличить размер таблицы
Поиск
search(key) x = h1(key) y = h2(key) for (i = 0; i < m; i++) if table[x] != null if table[x].key == key return table[x] else return null x = (x + y) mod m return null
Реализация с удалением
Что бы наша хеш-таблица поддерживала удаление, требуется добавить массив
типов , равный по величине массиву . Теперь при удалении мы просто будем помечать наш объект как удалённый, а при добавлении как не удалённый и замещать новым добавляемым объектом. При поиске, помимо равенства ключей, мы смотрим, удалён ли элемент, если да, то идём дальше.Вставка
add(item) x = h1(item.key) y = h2(item.key) for (i = 0; i < m; i++) if table[x] == null || deleted[x] table[x] = item deleted[x] = false return x = (x + y) mod m table.resize() //ошибка, требуется увеличить размер таблицы
Поиск
search(key) x = h1(key) y = h2(key) for (i = 0; i < m; i++) if table[x] != null if table[x].key == key && !deleted[x] return table[x] else return null x = (x + y) mod m return null
Удаление
remove(key) x = h1(key) y = h2(key) for (i = 0; i < m; i++) if table[x] != null if table[x].key == key deleted[x] = true else return x = (x + y) mod m
Разрешение коллизий с помощью цепочек
Каждая ячейка
массива содержит указатель на начало списка всех элементов, хеш-код которых равен , либо указывает на их отсутствие. Коллизии приводят к тому, что появляются списки размером больше одного элемента.Время, необходимое для вставки в наихудшем случае равно
. Это операция выполняет быстро, так как считается, что вставляемый элемент отсутствует в таблице, но если потребуется, то перед вставкой мы можем выполнить поиск этого элемента.Время работы поиска в наихудшем случае пропорционально длине списка, а если все
ключей захешировались в одну и ту же ячейку (создав список длиной ) время поиска будет равно плюс время вычисления хеш-функции, что ничуть не лучше, чем использование связного списка для хранения всех элементов.Удаления элемента может быть выполнено за
, как и вставка, при использовании двухсвязного списка.См. также
Литература
- Бакнелл Дж. М. Фундаментальные алгоритмы и структуры данных в Delphi, 2003
- Кнут Д. Э. Искусство программирования, том 3. Сортировка и поиск, 2-е издание, 2000
- Томас Кормен, Чарльз Лейзерсон, Рональд Ривест, Клиффорд Штайн. Алгоритмы. Построение и анализ, 2010
- Седжвик Р. Фундаментальные алгоритмы на C. Части 1-4. Анализ. Структуры данных. Сортировка. Поиск, 2003