*Математика хеширования
Разберемся с математикой, стоящей за хеш-функциями на примере хеш-таблиц. Мы будем использовать метод цепочек, но все утверждения будут верны и для открытой адресации.
Средняя длина цепочки
Итак, как мы с вами выяснили, время работы каждой операции в хеш-таблице будет зависеть от длины цепочки в бакете. Давайте честно вычислим среднюю длину цепочки для хеш-таблицы с $N$ элементами и $M = \alpha N$ бакетами.
Но вот вопрос - а по какому параметру брать среднюю длину цепочки? Если вы думаете, что по среднее по всем бакетам, то вы ошибаетесь, поскольку так наша средняя длина будет зависеть от самих элементов в хеш-таблице, а не от их количества. Поэтому мы будем брать среднюю длину ПО МНОЖЕСТВУ возможных ХЕШ-ФУНКЦИЙ $\mathcal H = {\{h_1, h_2, \ldots,\}}$.
Но из чего состоит всё это множество? Давайте возьмем вообще множество ВСЕХ ВОЗМОЖНЫХ функций, которые отображают $[0, A-1]$ в $[0, M-1]$, то есть всего в этом множестве будет $A^M$ различных функций. Среди таких функций мы будем равновероятно выбирать одну, которая и будет использоваться в хеш-таблице. Теперь мы готовы посчитать среднюю длину цепочки в бакете для нового элемента $a_n$: распишем через индикаторы того, что какой-то другой элемент $a_i$ попал в тот же бакет, что и $a_n$:
\[E_h[L] = E_h\left[\sum\limits_{i=0}^{n-1} I[h(a_i) = h(a_n)]\right]\]По линейности математического ожидания мы можем вынести сумму за знак ожидания:
\[E_h[L] = E_h\left[\sum\limits_{i=0}^{n-1} I[h(a_i) = h(a_n)]\right] = \sum\limits_{i=0}^{n-1} E_h[I[h(a_i) = h(a_n)]]\]Матожидание индикатора - это вероятность того, что событие произошло, то есть:
\[E_h[L] = \sum\limits_{i=0}^{n-1} E_h[I[h(a_i) = h(a_n)]] = \sum\limits_{i=0}^{n-1} P_h[h(a_i) = h(a_n)]\]Теперь мы можем посчитать вероятность того, что два элемента имеют одинаковый хеш. Поскольку мы выбираем хеш-функцию равновероятно, то вероятность того, что два элемента имеют одинаковый хеш будет равна:
\[P_h[h(a_i) = h(a_n)] = \begin{cases} \frac{1}{M}, & \text{если } a_i \neq a_n \\ 1, & \text{иначе} \end{cases}\]Таким образом, искомое матожидание можно оценить сверху как $1 + \frac{N}{M}$ (потому что в бакете запрещены одинаковые элементы):
\[E_h[L] = \sum\limits_{i=0}^{n-1} P_h[h(a_i) = h(a_n)] \leq 1 + \sum\limits_{i=0}^{n-1} \frac{1}{M} = 1 + \frac{N}{M} = 1 + \alpha = \Theta(\alpha)\]Таким образом, мы получили константную оценку для средней длины цепочки в бакете, следовательно хеш-таблица будет работать за $O(1)$ в среднем.
Где-то проблема
В нашем потрясающем доказательстве мы сделали фундаментально сильное допущение - мы предположили, что мы берем случайную хеш-функцию из множества всех возможных хеш-функций. Но можем ли мы в реальности это сделать? Нет! (подумайте, почему, прежде чем читать ответ!)
Почему не можем
Чтобы закодировать произвольную функцию из $[0, A-1]$ в $[0, M-1]$, нам нужно запомнить всю таблицу переходов $\forall a \in [0, A-1] \to [0, \ldots, M-1]$. То есть нам нужно $O(A \log M)$ памяти, чтобы закодировать произвольную функцию, а мы то хотели использовать $O(M)$ памяти. Поэтому мы не можем выбрать произвольную хеш-функцию.
Как же быть?
Идея заключается в том, чтобы как-то сузить наше множество хеш-функций до более узкого, при этом оно должно удовлетворять критерию:
- вероятность того, что два различных элемента имеют одинаковый хеш должна не превосходить $\frac{1}{M}$
Второе условие нам нужно, чтобы сохранить доказательство оценки времени работы хеш-таблицы (а именно, переход с оценкой вероятности). Такие семейства хеш-функций, удовлетвоящие нашим ограничениям, называются universal family.
И хотя мы уже знаем пример как минимум один пример такого симейства - это просто множество всех хеш-функций, оно нам не подойдет на практике, поскольку нам будут важны еще два свойства:
- можно легко выбрать хеш-функцию из этого семейства
- можно легко вычислить хеш-функцию для элемента
Далее мы будем искать именно такие семейства хеш-функций, которые удовлетворяют всем нашим требованиям.
Хорошее семейство
Сделаем несколько предположений:
- Пусть $x \in [0, 1, \ldots, P-1]$ - это элемент, который мы хотим захешировать и он ялвяется целым числом
- Пусть $P$ - это простое число (если $x$ ограничивался свеху не простым числом, то найдем ближайшее большее простое число)
- Пусть $M$ - это размер хеш-таблицы (количество бакетов)
Давайте докажем, что линейные хеш-функции удовлетворяют всем нашим требованиям:
\[\mathcal H = \left\{h(x) = (ax + b) \bmod P \bmod M\right\},\]где $1 \leq a < P$, $0 \leq b < P$ - случайные числа, которые мы будем выбирать из равномерного распределения. То есть среди такого множества хеш-функций мы умеем равновероятно выбирать случайную за $O(1)$, да сама функция вычисляется за $O(1)$. Осталось проверить, что вероятность того, что два различных элемента имеют одинаковый хеш не превосходит $\frac{1}{M}$.
Пусть $x$ и $y$ - два различных элемента, тогда вероятность того, что они имеют одинаковый хеш будет равна:
\[P_{a, b}[h(x) = h(y)] = P_{a, b}[(ax + b) \bmod P \bmod M = (ay + b) \bmod P \bmod M]\]Введём обозначения: $x’$ и $y’$:
\[\begin{cases} x' = (ax + b) \bmod P \\ y' = (ay + b) \bmod P \end{cases}\]Заметим, что при ограничениях $a \in [1, \ldots, P - 1]$, $b \in [0, \ldots, P - 1]$ и $P$ - простое найдется ровно одна подходящая пара $(a, b)$, это позволяет однозначно перейти от $x$ и $y$ к $x’$ и $y’$ и наоборот.
Ещё одно полезное замечение: если $x \neq y$, то после линейного отображения $x’ \neq y’$, поскольку $x’ - y’ = a(x - y) \bmod P$ и $P$ - простое, а $a \neq 0$.
Итак, перейдём в термины $x’$ и $y’$ (теперь вероятность будет браться по ним) и перенесём всё в одну сторону:
\[P_{a, b}[h(x) = h(y)] = P_{x' \neq y'}[h(x') = h(y')] = P_{x' \neq y'}[x' \bmod P \bmod M - y' \bmod P \bmod M = 0]\]Заметим, что мы можем сначала посчитать выражения в скобках, и только потом посчитать остатки от деления:
\[P_{x' \neq y'}[x' \bmod P \bmod M - y' \bmod P \bmod M = 0] = P_{x' \neq y'}[(x' - y') \bmod P \bmod M = 0]\]А последнюю вероятность посчитать очень легко: подойдут только пары, расстояние между которыми делится на $M$, то есть не больше чем каждая $M$-я:
\[P_{a, b}[h(x) = h(y)] = P_{x' \neq y'} \leq \frac{1}{M}\]Что и требовалось доказать.
То есть формально чтобы хеш-таблица работала за $O(1)$ в среднем, нужно, чтобы:
- ключи были целыми числами (в целом любой тип данных мы можем закодировать в целое число, просто оно получится очень большое)
- Мы знали некоторое простое число $P$, которое больше всех ключей
- В начале программы мы выбираем случайные числа $1 \leq a < P$ и $0 \leq b < P$ и используем их в качестве параметров хеш-функции $h(x) = (ax + b) \bmod P \bmod M$
На практике конечно мы делаем всё проще.
Упражнения
Задача 1. Предположим, что мы хотим захешировать строку $s_0s_1\ldots s_{n-1}$ (заранее известна ее максимальная длина). Пусть $\mathcal H$ - это некоторое универсальное семейство хеш-функций, $h_0, h_1, \ldots, h_n \in \mathcal H$ - случайно выбранные функции из $\mathcal H$. Тогда семейство $h_0, h_1, \ldots, h_n$ будет универсальным, для хеша:
\[h(s) = \left(h_0(s_0) + h_1(s_1) + \ldots + h_{n-1}(s_{n-1})\right) \bmod M\]Данный подход - это по сути первая вариация хеша строки, которую я упомянул. А далее мы сделали некоторый трюк - полиномиальный хеш, который позволяет нам не хранить все $h_i$ в памяти. И вот оказывается, что полиномиальный хеш тоже можно представить как универсальное семейство!
Задача 2. Пусть мы хотим хешировать строки вида $s=s_0s_1\ldots s_{n-1}$, а $P > \max(M, s_i)$ - простое число, тогда случайная хеш-функция $h_{int}$ из универсального семейства $\mathcal H: [0, \ldots, P-1] \to [0, \ldots, M-1]$ и случайная константа $Q \in [0, \ldots, P - 1]$ тоже образуют универсальное семейство хеш-функций вида:
\[h(s) = h_{int}\left((s_0 + Qs_1 + \ldots + Q^{n-1}s_{n-1}) \bmod P\right)\]Ссылки
- Лекции по алгоритмам в ШАД от Максима Бабенко
- Universal hashing на википедии