Хеш-таблицы
Деревья поиска обладают не слишком высокой плотностью расположения данных в памяти: в лучшем случае они представляют собой набор массивов-уровней, но при этом в них проблематично добавлять новые элементы.
Наиболее же гибкие разновидности деревьев состоят из большого количества блоков маленького размера, расположенных в памяти где повезёт.
При этом архитектура современных ЭВМ не рассчитана на частые операции доступа к большому количеству различных участков памяти: если суммарная длина этих участков больше размера кэша процессора, приходится периодически перекладывать данные из оперативной памяти в кэш, что весьма временезатратно.
Поэтому чем локальнее расположены данные, тем быстрее можно с ними работать.
Идеальная с этой точки зрения структура данных — массив. Хеш-таблицы представляют собой некую модификацию идеи массива, позволяющую использовать массив в качестве эффективного ассоциативного контейнера для довольно широкого спектра видов ключей.
Хеш-функции
Массивы с точки зрения математики — модели для отображений \([0,N) \rightarrow X\), где \([0,N)\) — множество натуральных чисел от \(0\) до \(N\).
Ассоциативные контейнеры — модели для отображений \(K \rightarrow 1+V\).
Массив можно превратить в ассоциативный контейнер, выбрав некоторую функцию \(h: K \rightarrow [0, N)\) и взяв \(X = 1+V\):
def new(size): return [None]*size
def put(array, key, value):
array[h(key)] = [value]
def get(array, key):
result = array[h(key)]
if result == None: raise KeyError ## тот самый элемент множества 1
return result[0]
Это — наиболее примитивная версия хеш-таблицы. Та самая функция \(h\), преобразующая ключи в индексы массива, традиционно называется хеш-функцией. Обычно от хеш-функции (в аспекте реализации хеш-таблиц) требуются две вещи:
- высокая скорость работы
- относительно равномерное распределение значений
Но, независимо от качества хеш-функции, обычно приходится иметь дело с ситуацией, когда разные ключи имеют одно и то же значение хеш-функции. Такие ситуации традиционно называются коллизиями. И с ними нужно что-то делать (впрочем, отказывать в хранении — тоже иногда вполне разумная политика; как и в некоторых случаях — перезаписывать уже хранящиеся в таблице данные).
Сначала мы обсудим один из способов эффективного хеширования, а затем — способы разрешения конфликтов.
Полиномиальные хеши последовательностей
Допустим, что мы умеем каким-то образом элементам множества \(A\) сопоставлять целые числа. Научимся сопоставлять целые числа последовательностям таких элементов так, чтобы наиболее распространённые операции над последовательностями соответствовали каким-то простым операциям над соответствующими этим последовательностям числами.
Для простоты будем считать, что в последовательности находятся не элементы множества \(A\), а соответствующие этим элементам числа.
Полиномиальным хешем последовательности \(a_1, a_2, \ldots a_n\) относительно параметров \(a_0\), \(d\) и \(N\) назовём значение выражения
\[ H(a) = (a_0 d^{n} + a_1 d^{n-1} + a_2 d^{n-2} + a_3 d^{n-3} + \ldots + a_n d^{0}) \bmod N \]
Для \(N=2^{32}\) очень популярны значения параметров \(d=31\) и \(a_0=7\).
Главная особенность полиномиальных хешей — возможность быстрой их конкатенации: если \(a\) и \(b\) — две последовательности, то
\[ H(ab) = ((H(a) - a_0)d^{|b|} + H(b)) \bmod N \]
где \(|b|\) — длина последовательности \(b\).
Нетрудно заметить, что сложность такой операции: логарифм длины последовательности \(b\), поскольку возведение в степень по фиксированному модулю — как раз логарифмическая операция.
Хеш-таблицы со списками кандидатов
Появится после окончания срока сдачи домашнего задания.
Хеш-таблицы с неточным расположением данных
Появится после окончания срока сдачи домашнего задания.