Россия, Пошатово |
Представление множеств. Хеширование
13.2. Хеширование со списками
На хеш-функцию с значениями можно смотреть как на способ свести вопрос о хранении одного большого множества к вопросу о хранении нескольких меньших. Именно, если у нас есть хеш-функция с значениями, то любое множество разбивается на подмножеств (возможно, пустых), соответствующих возможным значениям хеш-функции. Вопрос о проверке принадлежности, добавлении или удалении для большого множества сводится к такому же вопросу для одного из меньших (чтобы узнать, для какого, надо посмотреть на значение хеш-функции).
Эти меньшие множества удобно хранить с помощью ссылок; их суммарный размер равен числу элементов хешируемого множества. Следующая задача предлагает реализовать этот план.
13.2.1. Пусть хеш-функция принимает значения . Для каждого значения хеш-функции рассмотрим список всех элементов множества с данным значением хеш-функции. Будем хранить эти k списков с помощью переменных
Содержание: array [1..n] of T; Следующий: array [1..n] of 1..n; ПервСвоб: 1..n; Вершина: array [1..k] of 1..n;
так же, как мы это делали для k стеков ограниченной суммарной длины. Написать соответствующие программы. (Удаление по сравнению с открытой адресацией упрощается.)
Решение. Перед началом работы надо положить Вершина[i]=0 для всех i=1...k, и связать все места в список свободного пространства, положив ПервСвоб=1 и Следующий[i]=i+1 для i=1...n-1, а также Следующий[n]=0.
function принадлежит (t: T): boolean; | var i: integer; begin | i := Вершина[h(t)]; | {осталось искать в списке, начиная с i} | while (i <> 0) and (Содержание[i] <> t) do begin | | i := Следующий[i]; | end; {(i=0) or (Содержание [i] = t)} | принадлежит := (i<>0) and (Содержание[i]=t); end; procedure добавить (t: T); | var i: integer; begin | if not принадлежит(t) then begin | | i := ПервСвоб; | | {ПервСвоб <> 0 - считаем, что не переполняется} | | ПервСвоб := Следующий[ПервСвоб] | | Содержание[i]:=t; | | Следующий[i]:=Вершина[h(t)]; | | Вершина[h(t)]:=i; | end; end; procedure исключить (t: T); | var i, pred: integer; begin | i := Вершина[h(t)]; pred := 0; | {осталось искать в списке, начиная с i; pred - | предыдущий, если он есть, и 0, если нет} | while (i <> 0) and (Содержание[i] <> t) do begin | | pred := i; i := Следующий[i]; | end; {(i=0) or (Содержание [i] = t)} | if i <> 0 then begin | | {Содержание[i]=t, элемент есть, надо удалить} | | if pred = 0 then begin | | | {элемент оказался первым в списке} | | | Вершина[h(t)] := Следующий[i]; | | end else begin | | | Следующий[pred] := Следующий[i] | | end; | | {осталось вернуть i в список свободных} | | Следующий[i] := ПервСвоб; | | ПервСвоб:=i; | end; end;
13.2.2. (Для знакомых с теорией вероятностей.) Пусть хеш-функция с значениями используется для хранения множества, в котором в данный момент элементов. Доказать, что математическое ожидание числа действий в предыдущей задаче не превосходит , если добавляемый (удаляемый, искомый) элемент выбран случайно, причем все значения имеют равные вероятности (равные ).
Решение. Если - длина списка, соответствующего хеш-значению , то число операций не превосходит ; усредняя, получаем искомый ответ, так как .
Эта оценка основана на предположении о равных вероятностях. Однако в конкретной ситуации все может быть совсем не так, и значения хеш-функции могут "скучиваться": для каждой конкретной хеш-функции есть "неудачные" ситуации, когда число действий оказывается большим. Прием, называемый универсальным хешированием, позволяет обойти эту проблему. Идея состоит в том, что берется семейство хеш-функций, причем любая ситуация оказывается неудачной лишь для небольшой части этого семейства.
Пусть - семейство функций, каждая из которых отображает множество в множество из элементов (например, ). Говорят, что - универсальное семейство хеш-функций, если для любых двух различных значений и из множества вероятность события для случайной функции из семейства равна . (Другими словами, те функции из , для которых , составляют -ую часть всех функций в .)
Замечание. Более сильное требование к семейству могло бы состоять в том, чтобы для любых двух различных элементов и множества значения и случайной функции являются независимыми случайными величинами, равномерно распределенными на .
13.2.3. Пусть - произвольная последовательность различных элементов множества . Рассмотрим количество действий, происходящих при помещении элементов в множество, хешируемое с помощью функции из универсального семейства . Доказать, что среднее количество действий (усреднение - по всем из ) не превосходит .
Решение. Обозначим через количество элементов последовательности, для которых хеш-функция равна . (Числа зависят, конечно, от выбора хеш-функции.) Количество действий, которое мы хотим оценить, с точностью до постоянного множителя равно . (Если чисел попадают в одну хеш-ячейку, то для этого требуется примерно действий.) Эту же сумму квадратов можно записать как число пар , для которых . Последнее равенство, если его рассматривать как событие при фиксированных и , имеет вероятность при , поэтому среднее значение соответствующего члена суммы равно , а для всей суммы получаем оценку порядка , а точнее , если учесть члены с .
Эта задача показывает, что на каждый добавляемый элемент приходится в среднем операций. В этой оценке дробь имеет смысл "коэффициента заполнения" хеш-таблицы.
13.2.4. Доказать аналогичное утверждение для произвольной последовательности операций добавления, поиска и удаления (а не только для добавления, как в предыдущей задаче).
Указание. Будем представлять себе, что в ходе поиска, добавления и удаления элемент проталкивается по списку своих коллег с тем же хеш-значением, пока не найдет своего двойника или не дойдет до конца списка. Будем называть - -столкновением столкновение с . (Оно либо произойдет, либо нет - в зависимости от .) Общее число действий примерно равно числу всех происшедших столкновений плюс число элементов. При вероятность - -столкновения не превосходит . Осталось проследить за столкновениями между равными элементами. Фиксируем некоторое значение из множества и посмотрим на связанные с ним операции. Они идут по циклу: добавление - проверки - удаление - добавление - проверки - удаление - … Столкновения происходят между добавляемым элементом и следующими за ним проверками (до удаления включительно), поэтому общее их число не превосходит числа элементов, равных .
Теперь приведем примеры универсальных семейств. Очевидно, для любых конечных множеств и семейство всех функций, отображающих в , является универсальным. Однако этот пример с практической точки зрения бесполезен: для запоминания случайной функции из этого семейства нужен массив, число элементов в котором равно числу элементов в множестве . (А если мы можем себе позволить такой массив, то никакого хеширования не требуется!)
Более практичные примеры универсальных семейств могут быть построены с помощью несложных алгебраических конструкций. Через мы обозначаем множество вычетов по простому модулю , т.е. ; арифметические операции в этом множестве выполняются по модулю . Универсальное семейство образуют все линейные функционалы на со значениями в . Более подробно, пусть - произвольные элементы ; рассмотрим отображение
Мы получаем семейство из отображений параметризованное наборами .13.2.5. Доказать, что это семейство является универсальным.
Указание. Пусть и - различные точки пространства . Какова вероятность того, что случайный функционал принимает на них одинаковые значения? Другими словами, какова вероятность того, что он равен нулю на их разности ? Ответ дается таким утверждением: пусть - ненулевой вектор; тогда все значения случайного функционала на нем равновероятны.
В следующей задаче множество рассматривается как множество вычетов по модулю .
13.2.6. Семейство всех линейных отображений из в является универсальным.
Родственные хешированию идеи неожиданно оказываются полезными в следующей ситуации (рассказал Д. Варсанофьев). Пусть мы хотим написать программу, которая обнаруживала (большинство) опечаток в тексте, но не хотим хранить список всех правильных словоформ. Предлагается поступить так: выбрать некоторое и набор функций , отображающих русские слова добавлены запятые в 1,...,N в . В массиве из битов положим все биты равными нулю, кроме тех, которые являются значением какой-то функции набора на какой-то правильной словоформе. Теперь приближенный тест на правильность словоформы таков: проверить, что значения всех функций набора на этой словоформе попадают на места, занятые единицами. (Этот тест может не заметить некоторых ошибок, но все правильные словоформы будут одобрены.)