Разрешение коллизий — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Удаление элемента без пометок (в разработке))
Строка 53: Строка 53:
 
Учитывая это будем действовать следующим образом: при поиске следующего элемента цепочки будем пропускать все ячейки с другим значением хеша, первый найденный элемент копировать в текущую ячейку, и затем рекурсивно его удалять. Если такой следующей ячейки нет, то текущий элемент можно просто удалить, сторонние цепочки при этом не разрушатся (кстати это неверно для квадратичного поиска).
 
Учитывая это будем действовать следующим образом: при поиске следующего элемента цепочки будем пропускать все ячейки с другим значением хеша, первый найденный элемент копировать в текущую ячейку, и затем рекурсивно его удалять. Если такой следующей ячейки нет, то текущий элемент можно просто удалить, сторонние цепочки при этом не разрушатся (кстати это неверно для квадратичного поиска).
  
=== Псевдокод ===
+
 
 +
''' Псевдокод '''
 +
 
 
   delete(i)  
 
   delete(i)  
 
       j = i + q
 
       j = i + q
 
       while table[j] == null || table[j].key != table[i].key
 
       while table[j] == null || table[j].key != table[i].key
         if (talbe[j] == null)
+
         if (table[j] == null)
 
             table[i] = null
 
             table[i] = null
 
             exit
 
             exit
Строка 64: Строка 66:
 
       delete(j);       
 
       delete(j);       
  
Массив считаем зацикленным
+
Хеш-таблицу считаем зацикленной
  
=== Время работы ===
+
{{Утверждение
Заметим что указатель j в каждой итерации перемещается вперёд на q (с учётом рекурсивных вызовов delete). То есть этот алгоритм просто проходит по всей цепочке, и асимптотика у delete такая же как у find.
+
|about=о времени работы
 +
|statement=<tex>O(delete)=O(find)</tex>
 +
|proof=
 +
Заметим что указатель <tex>j</tex> в каждой итерации перемещается вперёд на <tex>q</tex> (с учётом рекурсивных вызовов <tex>delete</tex>). То есть этот алгоритм просто проходит по всей цепочке
 +
}}
  
Случай зацикливания мы не рассматриваем, поскольку если q взаимнопросто с размером хеш-таблицы, то для зацикливания в ней вообще не должно быть свободных элементов
+
Случай зацикливания мы не рассматриваем, поскольку если <tex>q</tex> взаимнопросто с размером хеш-таблицы, то для зацикливания в ней вообще не должно быть свободных элементов
  
 
==Двойное хеширование==
 
==Двойное хеширование==

Версия 19:21, 18 мая 2013

Определение:
Коллизия хеш-функции — это равенство значений хеш-функции на двух различных блоках данных.


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

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

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

Стратегии поиска

Последовательный поиск

При попытке добавить элемент в занятую ячейку [math]i[/math] начинаем последовательно просматривать ячейки [math]i+1, i+2, i+3[/math] и так далее, пока не найдём свободную ячейку. В неё и запишем элемент.

Последовательный поиск, частный случай линейного поиска.

Линейный поиск

Выбираем шаг [math]q[/math]. При попытке добавить элемент в занятую ячейку [math]i[/math] начинаем последовательно просматривать ячейки [math]i+(1 \cdot q), i+(2 \cdot q), i+(3 \cdot q)[/math] и так далее, пока не найдём свободную ячейку. В неё и запишем элемент. По сути последовательный поиск - частный случай линейного, где [math]q=1[/math].

Линейный поиск с шагом q.

Квадратичный поиск

Шаг [math]q[/math] не фиксирован, а изменяется квадратично: [math]q = 1,4,9,16...[/math]. Соответственно при попытке добавить элемент в занятую ячейку [math]i[/math] начинаем последовательно просматривать ячейки [math] i+1, i+4, i+9[/math] и так далее, пока не найдём свободную ячейку.

Квадратичный поиск.

Проверка наличия элемента в таблице

Проверка осуществляется аналогично добавлению: мы проверяем ячейку [math]i[/math] и другие, в соответствии с выбранной стратегией, пока не найдём искомый элемент или свободную ячейку.

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

Проблемы данных стратегий

Проблем две — крайне нетривиальное удаление элемента из таблицы и образование кластеров — последовательностей занятых ячеек.

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


Удаление элемента без пометок (в разработке)

Рассуждение будет описывать случай с линейным поиском хеша. Будем при удалении элемента сдвигать всё последующие на шаг назад. При этом:

  • если в цепочке встречается элемент с другим хешем, то он должен остаться на своём месте (такая ситуация может возникнуть если оставшаяся часть цепочки была добавлена позже этого элемента)
  • в цепочке не должно оставаться "дырок", тогда любой элемент с данным хешем будет доступен из начала цепи

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


Псевдокод

  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);      

Хеш-таблицу считаем зацикленной

Утверждение (о времени работы):
[math]O(delete)=O(find)[/math]
[math]\triangleright[/math]
Заметим что указатель [math]j[/math] в каждой итерации перемещается вперёд на [math]q[/math] (с учётом рекурсивных вызовов [math]delete[/math]). То есть этот алгоритм просто проходит по всей цепочке
[math]\triangleleft[/math]

Случай зацикливания мы не рассматриваем, поскольку если [math]q[/math] взаимнопросто с размером хеш-таблицы, то для зацикливания в ней вообще не должно быть свободных элементов

Двойное хеширование

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

Принцип двойного хеширования

При двойном хешировании используются две независимые хеш-функции [math] h_1(k) [/math] и [math] h_2(k) [/math]. Пусть [math] k [/math] — это наш ключ, [math] m [/math] — размер нашей таблицы, [math]n \mod m [/math] — остаток от деления [math] n [/math] на [math] m [/math], тогда сначала исследуется ячейка с адресом [math] h_1(k) [/math], если она уже занята, то рассматривается [math] (h_1(k) + h_2(k)) \mod m [/math], затем [math] (h_1(k) + 2 \cdot h_2(k)) \mod m [/math] и так далее. В общем случае идёт проверка последовательности ячеек [math] (h_1(k) + i \cdot h_2(k)) \mod m [/math] где [math] i = (0, 1, \; ... \;, m - 1) [/math]

Таким образом, операции вставки, удаления и поиска в лучшем случае выполняются за [math]O(1)[/math], в худшем — за [math]O(m)[/math], что не отличается от обычного линейного разрешения коллизий. Однако в среднем, при грамотном выборе хеш-функций, двойное хеширование будет выдавать лучшие результаты, за счёт того, что вероятность совпадения значений сразу двух независимых хеш-функций ниже, чем одной.

[math]\forall x \neq y \; \exists h_1,h_2 : p(h_1(x)=h_1(y))\gt p((h_1(x)=h_1(y)) \land (h_2(x)=h_2(y)))[/math]

Выбор хеш-функций

[math] h_1 [/math] может быть обычной хеш-функцией. Однако чтобы последовательность исследования могла охватить всю таблицу, [math] h_2 [/math] должна возвращать значения:

  • не равные [math] 0 [/math]
  • независимые от [math] h_1 [/math]
  • взаимно простые с величиной хеш-таблицы

Есть два удобных способа это сделать. Первый состоит в том, что в качестве размера таблицы используется простое число, а [math] h_2 [/math] возвращает натуральные числа, меньшие [math] m [/math]. Второй — размер таблицы является степенью двойки, а [math] h_2 [/math] возвращает нечетные значения.

Например, если размер таблицы равен [math] m [/math], то в качестве [math] h_2 [/math] можно использовать функцию вида [math] h_2(k) = k \mod (m-1) + 1 [/math]

Вставка при двойном хешировании

Пример

Показана хеш-таблица размером 13 ячеек, в которой используются вспомогательные функции:

[math] h(k,i) = (h_1(k) + i \cdot h_2(k)) \mod 13 [/math]

[math] h_1(k) = k \mod 13 [/math]

[math] h_2(k) = 1 + k \mod 11 [/math]

Мы хотим вставить ключ 14. Изначально [math] i = 0 [/math]. Тогда [math] h(14,0) = (h_1(14) + 0\cdot h_2(14)) \mod 13 = 1 [/math]. Но ячейка с индексом 1 занята, поэтому увеличиваем [math] i [/math] на 1 и пересчитываем значение хеш-функции. Делаем так, пока не дойдем до пустой ячейки. При [math] i = 2 [/math] получаем [math] h(14,2) = (h_1(14) + 2\cdot h_2(14)) \mod 13 = 9 [/math]. Ячейка с номером 9 свободна, значит записываем туда наш ключ.

Таким образом, основная особенность двойного хеширования состоит в том, что при различных [math] k [/math] пара [math] (h_1(k),h_2(k)) [/math] дает различные последовательности ячеек для исследования.

Простая реализация

Пусть у нас есть некоторый объект [math] item [/math], в котором определено поле [math] key [/math], от которого можно вычислить хеш-функции [math] h_1(key)[/math] и [math] h_2(key) [/math]

Так же у нас есть таблица [math] table [/math] величиной [math] m [/math], состоящая из объектов типа [math] item [/math].

Вставка

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

Реализация с удалением

Что бы наша хеш-таблица поддерживала удаление, требуется добавить массив [math]deleted[/math] типов [math]bool[/math], равный по величине массиву [math]table[/math]. Теперь при удалении мы просто будем помечать наш объект как удалённый, а при добавлении как не удалённый и замещать новым добавляемым объектом. При поиске, помимо равенства ключей, мы смотрим, удалён ли элемент, если да, то идём дальше.

Вставка

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

Разрешение коллизий с помощью списков

Каждая ячейка [math]i[/math] массива [math]H[/math] содержит указатель на начало списка всех элементов, хеш-код которых равен [math]i[/math], либо указывает на их отсутствие. Коллизии приводят к тому, что появляются списки размером больше одного элемента.

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

Разрешение коллизий при помощи цепочек.

Время работы поиска в наихудшем случае пропорционально длине списка, а если все [math]n[/math] ключей захешировались в одну и ту же ячейку (создав список длиной [math]n[/math]) время поиска будет равно [math]\Theta(n)[/math] плюс время вычисления хеш-функции, что ничуть не лучше, чем использование связного списка для хранения всех [math]n[/math] элементов.

Удаления элемента может быть выполнено за [math]O(1)[/math], как и вставка, при использовании двухсвязного списка.

См. также

Литература

  • Бакнелл Дж. М. Фундаментальные алгоритмы и структуры данных в Delphi, 2003
  • Кнут Д. Э. Искусство программирования, том 3. Сортировка и поиск, 2-е издание, 2000
  • Томас Кормен, Чарльз Лейзерсон, Рональд Ривест, Клиффорд Штайн. Алгоритмы. Построение и анализ, 2010
  • Седжвик Р. Фундаментальные алгоритмы на C. Части 1-4. Анализ. Структуры данных. Сортировка. Поиск, 2003

Ссылки