Хорошие и плохие хэш-функции
В прошлом сообщении упомянул, что самый очевидный способ написать свою хэш-функцию - “для каждого ключа-строки считаем сумму числовых значений символов в строке” - является неправильным. Резонный вопрос - что с ним не так?
Если подумать, можно сформулировать два критерия хорошей хэш функции - она должна быстро вычисляться и она должна давать минимальное количество коллизий. Т.е. если мы размещаем в хэш-таблице на m элементов n ключей, то в каждой ячейке должно оказаться примерно по n / m элементов.
Наш вариант хэш-функции явно быстро вычисляется, но что с распределением ключей по хэш-таблице? Рассчитаем такую хэш-функцию для 1000 случайных строк (длиной по 10 символов) и посмотрим сколько слов попадёт в каждую из ячеек хэш-таблицы на 100 элементов (хотелось бы чтобы везде по 10)
import random
import string
def generate_random_string(length):
letters = string.ascii_lowercase
rand_string = ''.join(random.choice(letters) for i in range(length))
return rand_string
def hash_function(string):
hash = 0
for ch in string:
hash += ord(ch)
return hash % 100
hash_table = list((0 for i in range(100)))
for _ in range(1000):
hash_table[hash_function(generate_random_string(10))] += 1
print(hash_table)
Строки случайные, поэтому раз на раз не приходится, но скорее всего получится что-то такое:
[24, 17, 17, 13, 17, 18, 15, 10, 20, 14, 17, 14, 18, 11, 15, 15, 10, 15, 12, 8, 15, 9, 8, 7, 3, 4, 9, 7, 6, 8, 9, 10, 13, 6, 5, 6, 2, 4, 3, 2, 3, 1, 8, 4, 2, 5, 2, 2, 3, 2, 3, 2, 6, 5, 2, 1, 2, 4, 6, 10, 5, 7, 13, 5, 10, 11, 9, 8, 5, 6, 10, 4, 10, 9, 10, 12, 14, 7, 10, 19, 14, 15, 14, 17, 11, 23, 12, 10, 11, 12, 22, 9, 22, 11, 17, 13, 13, 26, 16, 19]
Не идеально, но более менее равномерно получилось. Ок, это были случайные строки, посмотрим что с реальными данными.
Возьмём 1000 самых частых английских слов https://github.com/powerlanguage/word-lists/blob/master/1000-most-common-words.txt и посмотрим распределение для них:
[175, 638, 88, 118, 181, 279, 8, 51, 270, 126, 1298, 353, 143, 11, 369, 290, 384, 149, 52, 99, 14, 81, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 370, 87, 197]
На равномерное распределение совсем не похоже. Заметная часть ячеек пустая, большинство ключей группируются в небольшом количестве ячеек - соответственно, после того как хэш-функция даст ссылку на одну из таких ячеек, надо будет последовательно перебрать все значения там, чтобы убедиться есть там нужный ключ или нет.
Хорошая хэш-функция даёт равномерное распределение по хэш-таблице даже для незначительно различающихся строк. Собственно, один из показателей “правильности” хэш-функции это то, что незначительная разница на входе даёт огромную разницу на выходе (лавинный эффект).
Хэш-функций существует довольно много, под разные задачи. Одна из популярных - murmur2 (https://ru.wikipedia.org/wiki/MurmurHash2). Переписывать алгоритм на python сейчас не буду - воспользуюсь готовым модулем
from murmurhash2 import murmurhash2
SEED = 3242157231
hash_table = list((0 for i in range(100)))
for word in words:
key = bytes(word, 'utf8')
hash_table[murmurhash2(key, SEED) % 100] += 1
print(hash_table)
[405, 0, 0, 0, 0, 0, 0, 0, 14, 0, 998, 126, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 138, 82, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 384, 0, 290, 0, 0, 143, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 279, 1, 638, 370, 570, 369, 0, 0, 0, 0, 0, 0, 0, 175, 11, 0, 0, 0, 0, 197, 0, 118, 0, 0, 0, 0, 0, 149, 88, 0, 0, 0, 181, 2] - неожиданно :)
Вообще я ожидал увидеть практически равномерное распределение. Надо разбираться