Разрешение коллизий — различия между версиями
Martoon (обсуждение | вклад) м (→Удаление элемента без пометок) |
(→Удаление элемента без пометок) |
||
| Строка 71: | Строка 71: | ||
{{Утверждение | {{Утверждение | ||
|about=о времени работы | |about=о времени работы | ||
| − | |statement=Асимптотически время работы <tex>delete</tex> и <tex>find</tex> совпадают | + | |statement=Асимптотически время работы <tex>\mathrm{delete}</tex> и <tex>\mathrm{find}</tex> совпадают |
|proof= | |proof= | ||
| − | Заметим что указатель <tex>j</tex> в каждой итерации перемещается вперёд на <tex>q</tex> (с учётом рекурсивных вызовов <tex>delete</tex>). То есть этот алгоритм последовательно пройдёт по цепочке от удаляемого элемента до последнего - с учётом вызова <tex>find</tex> собственно для нахождения удаляемого элемента, мы посетим все ячейки цепи. | + | Заметим что указатель <tex>j</tex> в каждой итерации перемещается вперёд на <tex>q</tex> (с учётом рекурсивных вызовов <tex>\mathrm{delete}</tex>). То есть этот алгоритм последовательно пройдёт по цепочке от удаляемого элемента до последнего - с учётом вызова <tex>\mathrm{find}</tex> собственно для нахождения удаляемого элемента, мы посетим все ячейки цепи. |
}} | }} | ||
| Строка 81: | Строка 81: | ||
Теперь докажем почему этот алгоритм работает. Собственно нам требуется сохранение трёх условий. | Теперь докажем почему этот алгоритм работает. Собственно нам требуется сохранение трёх условий. | ||
* В редактируемой цепи не остаётся дырок | * В редактируемой цепи не остаётся дырок | ||
| − | Докажем по индукции. Если на данной итерации мы просто удаляем элемент (база), то после него ничего нет, всё верно. Если же нет, то вызванный в конце <tex>delete</tex> (см. псевдокод) заметёт созданную дыру (скопированный элемент), и сам, по предположению, новых не создаст. | + | Докажем по индукции. Если на данной итерации мы просто удаляем элемент (база), то после него ничего нет, всё верно. Если же нет, то вызванный в конце <tex>\mathrm{delete}</tex> (см. псевдокод) заметёт созданную дыру (скопированный элемент), и сам, по предположению, новых не создаст. |
* Элементы, которые уже на своих местах, не должны быть сдвинуты. | * Элементы, которые уже на своих местах, не должны быть сдвинуты. | ||
Это учтено. | Это учтено. | ||
Версия 23:25, 26 мая 2015
| Определение: |
| Коллизия хеш-функции — это равенство значений хеш-функции на двух различных блоках данных. |
Разрешение коллизий в хеш-таблице, задача, решаемая несколькими способами. Можно использовать списки, а можно открытую адресацию.
При использовании списков особых проблем не возникает, так как там в каждой ячейке хранится список всех элементов. При добавлении необходимо просто добавить элемент в начало списка.
При открытой адресации будет иначе: в каждой ячейке хеш-таблицы хранится только один элемент. Тогда при добавлении, если ячейка свободна, мы просто записываем добавляемый элемент в эту ячейку. Однако если эта ячейка занята — необходимо поместить добавляемый элемент в какую-нибудь другую свободную ячейку. Такие ситуации нередки, так как невозможно использовать хеш-функцию, не дающую коллизий, а каждой ячейке таблицы соответствует одно значение хеш-функции. Далее мы рассмотрим несколько стратегий поиска свободного места в данном случае.
Содержание
Стратегии поиска
Последовательный поиск
При попытке добавить элемент в занятую ячейку начинаем последовательно просматривать ячейки и так далее, пока не найдём свободную ячейку. В неё и запишем элемент.
Линейный поиск
Выбираем шаг . При попытке добавить элемент в занятую ячейку начинаем последовательно просматривать ячейки и так далее, пока не найдём свободную ячейку. В неё и запишем элемент. По сути последовательный поиск - частный случай линейного, где .
Квадратичный поиск
Шаг не фиксирован, а изменяется квадратично: . Соответственно при попытке добавить элемент в занятую ячейку начинаем последовательно просматривать ячейки и так далее, пока не найдём свободную ячейку.
Проверка наличия элемента в таблице
Проверка осуществляется аналогично добавлению: мы проверяем ячейку и другие, в соответствии с выбранной стратегией, пока не найдём искомый элемент или свободную ячейку.
При поиске элемента может получится так, что мы дойдём до конца таблицы. Обычно поиск продолжается, начиная с другого конца, пока мы не придём в ту ячейку, откуда начинался поиск.
Проблемы данных стратегий
Проблем две — крайне нетривиальное удаление элемента из таблицы и образование кластеров — последовательностей занятых ячеек.
Кластеризация замедляет все операции с хеш-таблицей: при добавлении требуется перебирать всё больше элементов, при проверке тоже. Чем больше в таблице элементов, тем больше в ней кластеры и тем выше вероятность того, что добавляемый элемент попадёт в кластер. Для защиты от кластеризации используется Двойное хеширование и хеширование кукушки.
Удаление элемента без пометок
Рассуждение будет описывать случай с линейным поиском хеша. Будем при удалении элемента сдвигать всё последующие на позиций назад. При этом:
- если в цепочке встречается элемент с другим хешем, то он должен остаться на своём месте (такая ситуация может возникнуть если оставшаяся часть цепочки была добавлена позже этого элемента)
- в цепочке не должно оставаться "дырок", тогда любой элемент с данным хешем будет доступен из начала цепи
Учитывая это будем действовать следующим образом: при поиске следующего элемента цепочки будем пропускать все ячейки с другим значением хеша, первый найденный элемент копировать в текущую ячейку, и затем рекурсивно его удалять. Если такой следующей ячейки нет, то текущий элемент можно просто удалить, сторонние цепочки при этом не разрушатся (чего нельзя сказать про случай квадратичного поиска).
Псевдокод
delete(i)
j = i + q
while table[j] == null || table[j].key != table[i].key
if (table[j] == null)
table[i] = null
exit
j += q
table[i] = table[j]
delete(j);
Хеш-таблицу считаем зацикленной
| Утверждение (о времени работы): |
Асимптотически время работы и совпадают |
| Заметим что указатель в каждой итерации перемещается вперёд на (с учётом рекурсивных вызовов ). То есть этот алгоритм последовательно пройдёт по цепочке от удаляемого элемента до последнего - с учётом вызова собственно для нахождения удаляемого элемента, мы посетим все ячейки цепи. |
Вариант с зацикливанием мы не рассматриваем, поскольку если взаимнопросто с размером хеш-таблицы, то для зацикливания в ней вообще не должно быть свободных позиций
Теперь докажем почему этот алгоритм работает. Собственно нам требуется сохранение трёх условий.
- В редактируемой цепи не остаётся дырок
Докажем по индукции. Если на данной итерации мы просто удаляем элемент (база), то после него ничего нет, всё верно. Если же нет, то вызванный в конце (см. псевдокод) заметёт созданную дыру (скопированный элемент), и сам, по предположению, новых не создаст.
- Элементы, которые уже на своих местах, не должны быть сдвинуты.
Это учтено.
- В других цепочках не появятся дыры
Противное возможно только в том случае, если какой-то элемент был действительно удалён. Удаляем мы только последнюю ячейку в цепи, и если бы на её месте возникла дыра для сторонней цепочки, это бы означало что элемент, стоящий на позиций назад, одновременно принадлежал нашей и другой цепочкам, что невозможно.
Двойное хеширование
Двойное хеширование — метод борьбы с коллизиями, возникающими при открытой адресации, основанный на использовании двух хеш-функций для построения различных последовательностей исследования хеш-таблицы.
Принцип двойного хеширования
При двойном хешировании используются две независимые хеш-функции и . Пусть — это наш ключ, — размер нашей таблицы, — остаток от деления на , тогда сначала исследуется ячейка с адресом , если она уже занята, то рассматривается , затем и так далее. В общем случае идёт проверка последовательности ячеек где
Таким образом, операции вставки, удаления и поиска в лучшем случае выполняются за , в худшем — за , что не отличается от обычного линейного разрешения коллизий. Однако в среднем, при грамотном выборе хеш-функций, двойное хеширование будет выдавать лучшие результаты, за счёт того, что вероятность совпадения значений сразу двух независимых хеш-функций ниже, чем одной.
Выбор хеш-функций
может быть обычной хеш-функцией. Однако чтобы последовательность исследования могла охватить всю таблицу, должна возвращать значения:
- не равные
- независимые от
- взаимно простые с величиной хеш-таблицы
Есть два удобных способа это сделать. Первый состоит в том, что в качестве размера таблицы используется простое число, а возвращает натуральные числа, меньшие . Второй — размер таблицы является степенью двойки, а возвращает нечетные значения.
Например, если размер таблицы равен , то в качестве можно использовать функцию вида
Пример
Показана хеш-таблица размером 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
