275 lines
22 KiB
HTML
275 lines
22 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Страница 7</title>
|
||
</head>
|
||
<body>
|
||
<p style="text-align: center">
|
||
<i><b>Страница 7</b></i>
|
||
</p>
|
||
<hr />
|
||
<h1 style="text-align: center">Решение проблем коллизий</h1>
|
||
<h2 style="text-align: center">
|
||
Открытая и прямая адресации [5, c. 170-179]
|
||
</h2>
|
||
<p>Оглавление:</p>
|
||
<ul>
|
||
<li><a href="#description">Описание.</a></li>
|
||
<li><a href="#linear">Линейное пробирование.</a></li>
|
||
<li><a href="#quadratic">Квадратичное пробирование.</a></li>
|
||
<li><a href="#pseudorandom">Псевдослучайное пробирование.</a></li>
|
||
<li><a href="#double">Двойное хеширование.</a></li>
|
||
</ul>
|
||
<div>
|
||
<h3 style="text-align: center"><a name="description">Описание</a></h3>
|
||
<p style="text-align: justify">
|
||
Безусловно, прямое связывание имеет свои преимущества. Основное из них
|
||
заключается в том, что общее количество значений не зависит от
|
||
количества блоков. Однако есть у таких хеш-таблиц и некоторые
|
||
недостатки. Например, если элементов в блоках накопится слишком много,
|
||
поиск нужного займет продолжительное время. Чтобы сократить временные
|
||
затраты, число блоков можно увеличить, но тогда вы рискуете получить ряд
|
||
пустых, которые потребуют места в памяти и не будут использоваться
|
||
таблицей.
|
||
</p>
|
||
<p style="text-align: justify">
|
||
Еще один способ реализации хеш-таблиц — <i>открытая адресация</i>.
|
||
В этом случае значения хранятся в массиве, а функция хеширования
|
||
представляет собой некоторые расчеты.
|
||
</p>
|
||
<p style="text-align: justify">
|
||
Хеш-таблица с <i>прямой адресацией</i> отличается от хеш-таблицы с
|
||
открытой адресацией тем, что в первом случае гарантировано должна
|
||
использоваться идеальная хеш-функция. Это обусловлено тем, что в данной
|
||
хеш-таблице должны отсутствовать коллизии, соответственно, как и методы
|
||
их разрешения. Стоит отметить, что под данное описание подходит
|
||
<i>идеальное хеширование</i> хеш-таблицы<sup>1</sup>.
|
||
</p>
|
||
<p style="text-align: justify">
|
||
В разных видах открытой адресации используются различные функции
|
||
хеширования. Неодинакова и политика разрешения коллизий, но в общем
|
||
случае она выглядит так: для каждого значения в массиве подбирается
|
||
несколько ячеек, и если первая уже занята, алгоритм пробует использовать
|
||
вторую, затем третью и так до тех пор, пока не найдет свободную или не
|
||
придет к выводу, что таковой нет.
|
||
</p>
|
||
<p style="text-align: justify">
|
||
Серия ячеек, которую алгоритм подбирает для значения, называется пробной
|
||
последовательностью. По ее средней длине хорошо оценивать наполненность
|
||
хештаблицы. В идеале пробная последовательность должна равняться 1 или
|
||
2, большие цифры говорят о полной таблице.
|
||
</p>
|
||
<p style="text-align: justify">
|
||
Иногда политика разрешения коллизий такова, что для элемента может не
|
||
найтись свободной ячейки, даже когда она есть. Если пробная
|
||
последовательность повторяет саму себя перед тем, как проверить
|
||
очередную запись, некоторые записи могут остаться неиспользованными.
|
||
</p>
|
||
<p style="text-align: justify">
|
||
Чтобы найти элемент в хеш-таблице, алгоритм следует за пробной
|
||
последовательностью, пока не произойдет одно из трех событий.
|
||
</p>
|
||
<ol>
|
||
<li style="text-align: justify">
|
||
Если пробная последовательность сумела отыскать элемент, задача
|
||
выполнена.
|
||
</li>
|
||
<li style="text-align: justify">
|
||
Если пробная последовательность находит пустую запись в массиве,
|
||
элемента нет.
|
||
</li>
|
||
<li style="text-align: justify">
|
||
Пробная последовательность проверяет <i>M</i> записей (по размеру
|
||
массива) — и алгоритм приходит к выводу, что значение
|
||
отсутствует. Последовательность может перебрать не все элементы, но
|
||
если пройдет по всем, вы будете знать, что они точно пересмотрены или
|
||
что целевой элемент не найден. Она также может проверить в цикле одну
|
||
и ту же позицию несколько раз. В любом случае значение не должно
|
||
присутствовать, поскольку иначе оно бы добавлялось к массиву с
|
||
использованием той же пробной последовательности.
|
||
</li>
|
||
</ol>
|
||
<p style="text-align: justify">
|
||
При разумном заполнении хеш-таблицы открытая адресация работает очень
|
||
быстро. Если длина пробной последовательности равна 1 или 2, добавление
|
||
и нахождение элементов выполняются за время <i>O(1)</i>. Но если массив
|
||
из N элементов существенно переполнен, производительность снижается. В
|
||
наихудшем случае алгоритм придет к выводу, что элемента в массиве нет,
|
||
за время <i>O(N)</i>. Поиск присутствующих элементов также будет
|
||
выполняться крайне медленно.
|
||
</p>
|
||
<p style="text-align: justify">
|
||
Вы можете увеличить размер массива, чтобы уменьшить коэффициент
|
||
наполненности хеш-таблицы. Для этого создайте новый массив и рехешируйте
|
||
элементы в нем. Для каждого из них операция займет <i>O(1)</i> времени,
|
||
а общая производительность алгоритма составит <i>O(N)</i>.
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<h3 style="text-align: center">
|
||
<a name="linear">Линейное пробирование</a>
|
||
</h3>
|
||
<p style="text-align: justify">
|
||
В линейном пробировании политика разрешения коллизий добавляет к каждой
|
||
ячейке постоянное число (чаще всего 1), называемое шагом по индексу,
|
||
которое генерирует пробную последовательность. При каждом очередном
|
||
добавлении берется размер массива по модулю, стало быть, при
|
||
необходимости последовательность возвращается к началу массива.
|
||
</p>
|
||
<p style="text-align: justify">
|
||
Предположим, в хеш-таблице 100 элементов, а правило хеширования звучит
|
||
следующим образом: N связано с ячейкой N mod 100. Тогда пробная
|
||
последовательность для значения 2197 проверяет ячейки 97, 98, 99, 0, 1,
|
||
2 и т. д.
|
||
</p>
|
||
<p style="text-align: justify">
|
||
На рисунке 7.1 представлен массив из 10 записей, который уже содержит
|
||
несколько значений. Чтобы добавить в него новое значение 71, используя
|
||
линейную пробную последовательность, нужно связать его с ячейкой 71 mod
|
||
10 = 1. Но эта ячейка уже занята значением 61, поэтому алгоритм
|
||
переходит к ячейке 2, которая тоже заполнена. Следующей должна быть
|
||
ячейка 3 — она свободна, и алгоритм размещает там 71.
|
||
</p>
|
||
<div style="text-align: center">
|
||
<img src="7.1.png" width="800" />
|
||
<p style="text-align: center">
|
||
Рисунок 7.1 - линейная пробная последовательность
|
||
</p>
|
||
</div>
|
||
<p style="text-align: justify">
|
||
Преимущество данного метода — в его простоте. Если необходимо,
|
||
пробная последовательность пройдет по каждой ячейке массива и вставит
|
||
элемент в свободное место, если оно еще осталось. Но есть и
|
||
сопутствующий недостаток — так называемая
|
||
<i>первичная кластеризация</i>, которая проявляется в образовании
|
||
больших блоков смежных записей и приводит к длинным пробным
|
||
последовательностям. В результате при добавлении нового элемента и его
|
||
хешировании к какой-либо записи в кластере пробная последовательность
|
||
вынуждена пройти через весь кластер, чтобы найти свободную ячейку.
|
||
</p>
|
||
<p style="text-align: justify">
|
||
Чтобы понять, как формируются кластеры, рассмотрим пример пустой
|
||
хеш-таблицы. Предположим, в ней содержится <i>N</i> записей и существует
|
||
вероятность <i>1/N</i>, что она закончится в любой данной позиции при
|
||
добавлении случайного числа. А теперь представим, что <i>K</i> —
|
||
конечная позиция таблицы. Существует вероятность <i>1/N</i>, что новое
|
||
случайное число попадет в позицию K и линейное пробирование попытается
|
||
расположить элемент в позиции <i>K + 1</i>. Но та же вероятность
|
||
характерна и для случая, при котором новый элемент будет связан с
|
||
позицией <i>K + 1</i> напрямую. Значит, существует вероятность
|
||
<i>2/N</i>, что элемент займет конечную позицию <i>K + 1</i> и
|
||
сформируется малый кластер.
|
||
</p>
|
||
<p style="text-align: justify">
|
||
Через какое-то время таких кластеров станет много. Чем больше они будут,
|
||
тем больше вероятность того, что новый элемент добавится в конец одного
|
||
из них. В итоге более мелкие кластеры сольются в более крупные, массив
|
||
заполнится ими — и возникнут длинные пробные последовательности.
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<h3 style="text-align: center">
|
||
<a name="quadratic">Квадратичное пробирование</a>
|
||
</h3>
|
||
<p style="text-align: justify">
|
||
Возникновение больших кластеров при линейном пробировании связано с тем,
|
||
что новые элементы связываются с ячейками, стоящими в конце группы, и
|
||
постепенно увеличивают ее. Предотвратить подобную ситуацию помогает
|
||
<i>квадратичное пробирование</i>. Для создания пробной
|
||
последовательности в качестве шага по индексу берется квадрат количества
|
||
ячеек. Другими словами, если в линейном пробировании существует
|
||
последовательность <i>K, K + 1, K + 2, K + 3,</i> ... то в квадратичном
|
||
варианте она будет выглядеть так:
|
||
<i>K, K + 12, K + 22, K + 32, ...</i> В этом случае, если два
|
||
элемента окажутся связанными с разными позициями в одном и том же
|
||
кластере, они не обязательно будут придерживаться одной пробной
|
||
последовательности и попадут в конец кластера.
|
||
</p>
|
||
<p style="text-align: justify">
|
||
На рисунке 7.2 показана хеш-таблица, в начале которой есть группа из
|
||
пяти элементов. Новое значение 71 получает пробную последовательность
|
||
<i>1, 1 + 12 = 2, 1 + 22 = 5, 1 + 32 = 10</i> и не добавляется к
|
||
существующему кластеру. Значение <i>93 </i>поначалу связано с тем же
|
||
кластером, но согласно собственной пробной последовательности
|
||
<i>3, 3 + 12 = 4, 3 + 22 = 7</i> также не попадает в него.
|
||
</p>
|
||
<div style="text-align: center">
|
||
<img src="7.2.png" width="800" />
|
||
<p style="text-align: center">Рисунок 7.2 - Квадратичное пробирование</p>
|
||
</div>
|
||
<p style="text-align: justify">
|
||
Квадратичное пробирование предотвращает первичную, но не
|
||
<i>вторичную кластеризацию</i>, при которой значения, связанные с
|
||
одинаковой начальной позицией в массиве, получают одну и ту же пробную
|
||
последовательность, иногда очень длинную. В результате образуется точно
|
||
такая же группа элементов, но уже не собранных вместе, а распределенных
|
||
по всему массиву.
|
||
</p>
|
||
<p style="text-align: justify">
|
||
Еще один недостаток квадратичного пробирования связан с тем, что оно
|
||
может не найти свободную позицию, даже если в хеш-таблице их несколько.
|
||
Дело в том, что с каждым разом перемещение по массиву происходит все
|
||
дальше и дальше, и незаполненная ячейка попросту пропускается.
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<h3 style="text-align: center">
|
||
<a name="pseudorandom">Псевдослучайное пробирование</a>
|
||
</h3>
|
||
<p style="text-align: justify">
|
||
Это пробирование подобно линейному, за исключением того, что шаг по
|
||
индексу формирует псевдослучайная функция изначально связанной ячейки.
|
||
Предположим, что это ячейка <i>K</i>, тогда пробная последовательность
|
||
будет выглядеть следующим образом: <i>K, K + p, K + 2p, ...</i> где p
|
||
определяется псевдослучайной функцией.
|
||
</p>
|
||
<p style="text-align: justify">
|
||
Подобно квадратичному пробированию, псевдослучайное предотвращает только
|
||
первичную кластеризацию, но страдает от вторичной: значения, связанные с
|
||
одной и той же начальной позицией, размещаются в хеш-таблице согласно
|
||
одной и той же пробной последовательности. Точно так же псевдослучайное
|
||
пробирование может пропускать некоторые неиспользуемые записи.
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<h3 style="text-align: center">
|
||
<a name="double">Двойное хеширование</a>
|
||
</h3>
|
||
<p style="text-align: justify">
|
||
Чтобы избавиться от вторичной кластеризации значения, связанные с одной
|
||
и той же начальной ячейкой, должны получать разные пробные
|
||
последовательности. И здесь пригодится двойное хеширование. Оно похоже
|
||
на псевдослучайное пробирование, только шаг для индекса задается не
|
||
псевдослучайной функцией начальной ячейки, а второй функцией
|
||
хеширования.
|
||
</p>
|
||
<p style="text-align: justify">
|
||
Допустим, значения <i>A</i> и <i>B</i> связаны с позицией <i>K</i>. В
|
||
псевдослучайном пробировании шаг по индексу <i>p</i> генерируется
|
||
функцией <i>F<sub>1</sub> (K)</i>, затем оба значения используют пробную
|
||
последовательность <i>K, K + p, K + 2p, K + 3p, ...</i> В двойном
|
||
хешировании для связывания начальных значений <i>А</i> и
|
||
<i>В</i> применяется функция псевдослучайного хеширования
|
||
<i>F<sub>2</sub></i
|
||
>. В итоге при одном и том же начальном <i>K</i> образуются две пробные
|
||
последовательности с различными шагами по индексу:
|
||
<i>p<sub>A</sub> = F<sub>2</sub> (A)</i> и
|
||
<i>p<sub>B</sub> = F<sub>2</sub> (B)</i> соответственно.
|
||
</p>
|
||
<p style="text-align: justify">
|
||
Несмотря на то, что двойное хеширование хорошо справляется с первичной и
|
||
вторичной кластеризациями, оно точно так же, как и псевдослучайное
|
||
пробирование, может пропускать неиспользуемые записи.
|
||
</p>
|
||
</div>
|
||
<hr />
|
||
<p>
|
||
<sup>1</sup>Данный метод хеширования хорошо описан в книге Томаса Кормена
|
||
[2]
|
||
</p>
|
||
</body>
|
||
</html>
|