</font>
Проблемы выбора M
Сравните с 10й системой счисления при делении на : нет неожиданности, что в остатке будет наименее значащих цифр изначального числа. То же верно и для систем с основанием 2.
на практике удобно , - размер машинного слова (16, 32, 64, как правило) Тогда хеш-функция превращается в , которую можно записать как простую функцию со "сдвигом":
1 hash(key, A, M): 2 return A * x >> (w - M)
- аналогично хешированию делением, но вносит "возмущение" в ключ
Такой подход очень быстр из-за простоты базовых операций (выполняющихся быстрее деления).
Более того, подобная функция близка к универсальной, если A - случайное нечетное число из . Про это мы еще поговорим.
Вопрос: почему нечетное?
Пример простой хеш-функции для последовательностей: хеш-функция Пирсона.
1 PearsonHash(S, Table): 2 h := 0 3 for (i = 0; i < len(S); i++): 4 h := Table[h xor S[i]] // Выбор значения из таблицы Table 5 return h
- Что такое Table? Это перемешанный список чисел 0..255. Он может быть как случайно перемешан, так и подобран специальным образом, чтобы обеспечить идеальное хеширование для определенного набора данных (см. далее)
Свойства хеш-функции Пирсона:
Table
, моджно применять для архитектур с большим, чем 8, машинным словом1 PearsonHash64(S, Table): 2 for i in 0..7: 3 h = Table[(S[0] + i) % 256] // +i для учета сдвига 4 for each item in S: 5 h = Table[h xor item] 6 hh[i] = h 7 return concatenated hh
Быстрые и используемые на практике наборы функций для хеширования последовательностей.
Базируются на следующем принципе:
Варианты ARX-алгоритма SipHash используются во многих языках, в том числе, python3.4+, Ruby, Perl.
</font>
Несколько популярных вариантов выбора констант
Почему ? Пусть есть сдивги и , указывающие на одну локацию, но , и .
- значит, существует M/2 различных мест для записи.
Чем квадратичный пробинг лучше линейного?
Равномерность распределения хешей по таблице зависит от количества возможных вариантов обхода при пробинге.
Идея: использовать 2 хеш-функции, чтобы получить последовательностей пробинга.
Формально, это
Для обхода всех ячеек при пробинге должна всегда возвращать взаимно простое с число. Простой способ сделать это:
Тем не менее, C# HashTable использует двойное хеширование
Количество попыток пробинга равно в среднем:
</font>
Универсальное хеширование случайно выбирает хеш-функцию из семейтсва хеш-функций, чтобы осложнить жизнь злоумышленнику (или просто избежать ситуации с "плохими" данными).
- семейство хеш-функций, отображающих ключи во множество . При этом, для пары ключей количество хеш-функций, для которых , не более
Построение класса универсальных хеш-функций
Как получилось, что это универсальные хеш-функции
- так как - простое, и не равны 0 по модулю .
Поэтому коллизий не возникает. Более того, все пары будут различными для каждой пары .
Осталось проверить, что коллизий не возникает и при взятии. Поскольку разные равновероятны, вероятность коллизии равна вероятности того, что . Ее можно вычислить так:
- количество значений для таких, что и . Поскольку есть возможных s, вероятность будет
</font>
Пример кода на python (нерабочий, но поясняющий идею создания семейства функций)
# Так
def generate_hash_fh(p):
a, b = random.randint(0, p-1), random.randint(1, p-1)
return partial(universal_hash, a, b)
def universal_hash(a, b, k):
return ((a * k + b) % p) % m # p простое, 2^64
# Или так
class UnivHash:
def __init__(self, a, b):
pass
def __call__(self, key):
pass
</font>