logo
Высокоцровневые методы информатики и првые методы информатики и программированияограммирования

4.5 Открытая адресация; способы вычисления последовательности испробованных мест: линейная последовательность проб, квадратичная последовательность проб, двойное хеширование

В отличие от хеширования с цепочками, при открытой адресации (open addressing) никаких списков нет, а все записи хранятся в самой хеш-таблице каждая ячейка таблицы содержит либо элемент динамического множества, либо NIL. Поиск заключается в том, что мы определённым образом просматриваем элементы таблицы, пока не найдём то, что ищем, или не удостоверимся, что элемента с таким ключом в таблице нет. Тем самым число хранимых элементов не может быть больше размера таблицы: коэффициент заполнения не больше 1.

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

Чтобы добавить новый элемент в таблицу с открытой адресацией, ячейки которой занумерованы целыми числами от 0 до т – 1, мы просматриваем её, пока не найдем свободное место. Если всякий раз просматривать ячейки подряд (от 0-й до (m – 1)-й), потребуется время (n), но суть в том, что порядок просмотра таблицы зависит от ключа. Иными словами, мы добавляем к хеш-функции второй аргумент – номер попытки (нумерацию начинаем с нуля), так что хеш-функция имеет вид

h: U x {0,1,..., m – 1} → {0, 1,...,m – 1}

(Uмножество ключей). Последовательность испробованных мест, или последовательность проб (probe sequence), для данного ключа k имеет вид

h(k,0),h(k,1),…,h(k,– 1);

функция h должна быть такой, чтобы каждое из чисел от 0 до т – 1 встретилось в этой последовательности ровно один раз (для каждого ключа все позиции таблицы должны быть доступны). Ниже приводится текст процедуры добавления в таблицу Т с открытой адресацией; в нем подразумевается, что записи не содержат дополнительной информации, кроме ключа. Если ячейка таблицы пуста, в ней записан NIL (фиксированное значение, отличное от всех ключей).

Листинг 4.4 – Процедура добавления элемента в таблицу с открытой адресацией

При поиске элемента с ключом k в таблице с открытой адресацией ячей­ки таблицы просматриваются в том же порядке, что и при добавлении в нее элемента с ключом k. Если при этом мы натыкаемся на ячейку, в которой записан NIL, то можно быть уверенным, что искомого элемента в таблице нет (иначе он был бы занесён в эту ячейку). (Внимание: предполагается, что никакие элементы из таблицы не удаляются!)

Вот текст процедуры поиска hash-search(если элемент с ключом k со­держится в таблице Т в позиции j, процедура возвращает j, в противном случае она возвращает nil).

Листинг 4.5 – Процедура поиска элемента в таблице с открытой адресацией

Удалить элемент из таблицы с открытой адресацией не так просто. Если просто записать на его место NIL, то в дальнейшем мы не сможем найти те элементы, в момент добавления которых в таблицу это место было занято (и из-за этого был выбран более далёкий элемент в последовательности испробо­ванных мест). Возможное решение – записывать на место удалённого элемента не nil, а специальное значение deleted («удалён»), и при добавлении рассма­тривать ячейку с записью deleted как свободную, а при поиске – как занятую (и продолжать поиск). Недостаток этого подхода в том, что время поиска мо­жет оказаться большим даже при низком коэффициенте заполнения. Поэтому, если требуется удалять записи из хеш-таблицы, предпочтение обычно отдают хешированию с цепочками.

В нашем анализе открытой адресации мы будем исходить из предположения, что хеширование равномерно (uniform) в том смысле, что все m! перестановок множества {0,1,...,m – 1} равновероятны. На практике это вряд ли так, хо­тя бы по той причине, что для этого необходимо, чтобы число возможных ключей было как минимум ≤ m!, где m – число хеш-значений. Поэтому обычно пользуются более или менее удачными суррогатами, вроде описываемого ниже двойного хеширования.

Обычно применяют такие три способа вычисления последовательности испробованных мест: линейный, квадратичный и двойное хеширование. В каждом из этих способов последовательность h(k, 0), h(k, 1),..., h(k, т – 1) будет перестановкой множества {0,1,..., m – 1} при любом значении ключа k, но ни один из этих способов не является равномерным по той причине, что они дают не более m2 перестановок из m! возможных. Больше всего разных перестановок получается при двойном хешировании; не удивительно, что и на практике этот способ дает лучшие результаты.

const

В = {подходящая константа};

empty = ' '; {10 пробелов}

deleted = '**********'; {10 символов *}

c = 1; {шаг перебора}

MaxCase = {подходящая константа}; {Max количество попыток}

type

TypeElem = string[10]

HTableOpen = array[0..B-1] of TypeElem;

Теперь приведем сами алгоритмы на примере линейного опробования. Для остальных методов повторного хеширования алгоритмы идентичны.

procedure Clear_HTableOpen(var A: HTableOpen);

{Процедура очистки хеш-таблицы}

var

IndexSeg: integer;

begin

for IndexSeg:=0 to B-l do A[IndexSeg] := empty;

end;

function Find_HTableOpen(x: TypeElem;

var A: HTableOpen;

var IndexSeg: integer): boolean;

{функция поиска элемента x в хеш-таблице. Принимает значение true, если найден и возвращает номер сегмента, в котором располагается найденный элемент, или принимает значение false и возвращает 0}

var

i: integer;

begin

i := 0;

repeat

IndexSeg := ((h(x) + c*i) mod B + {лин.опр.с цикл.переходом}

(h(x) + c*i) div B ) {смещение после перехода}

mod B; {ограничение смещения}

i := i + 1;

until (A[IndexSeg] = x) or {нашли}

(A[IndexSeg] = empty) or {дальше уже нет}

(i > MaxCase); {слишком долго ищем}

if A[IndexSeg] = x then begin

Find_HTableOpen := true;

end else begin

Find_HTableOpen := false;

IndexSeg := 0;

end;

end;

function Add_HTableOpen(x: TypeElem;

var A: HTableOpen): boolean;

{Процедура добавления элемента x в хеш-таблицу. Возвращает true, если элемент добавлен, и false – в обратном}

var

i,

IndexSeg: integer; {номер сегмента}

begin

if not Find_HTableOpen(x, A, IndexSeg) then begin

{Если в таблице элемент уже есть, то добавлять не надо}

i := 0;

repeat

IndexSeg := ((h(x) + c*i) mod B +

(h(x) + c*i) div B )

mod B;

i := i + 1;

until (A[IndexSeg] = empty) or {нашли место}

(A[IndexSeg] = deleted) or {тоже можно занять}

(i > MaxCase); {слишком долго ищем}

if (A[IndexSeg] = empty) or

(A[IndexSeg] = deleted) then begin

A[IndexSeg] := x;

Add_HTableOpen := true;

end else begin

Add_HTableOpen := false;

end;

end

end;

procedure Del_HTableOpen(x: TypeElem; var A: HTableOpen);

{Процедура удаления элемента x из хеш-таблицы}

var

IndexSeg: integer; {номер сегмента}

begin

if Find_HTableOpen(x, A, IndexSeg) then begin

{Если в таблице элемент уже нет, то удалять не надо}

A[IndexSeg] := deleted;

end

end;

Листинг 4.5 – Словарные операции при открытой адресации