Функции хеширования

Хеширование делением

  • Числовое значение ключа делится на размер хеш-таблицы: hash(k)=kmodM
  • Хорошо работает в ситуации, когда ключи взяты из равномерного распределения
  • Такое способ не очень быстр (потому что деление - "медленная" операция)

Проблемы выбора M

  • M не должно быть степенью двойки. Иначе для M=2p хеш будет просто p младших битов ключа (что допустимо только в случае, если несколько последних знаков числа распраделены равномерно)
  • Хороши простые числа, не очень близкие к степени 2.
  • Идущие подряд значения ключей "порождают" идущие подряд значения хешей. Это может быть как плюсом, так и минусом алгоритма.

Сравните с 10й системой счисления при делении на 10k: нет неожиданности, что в остатке будет k наименее значащих цифр изначального числа. То же верно и для систем с основанием 2.

Хеширование при помощи умножения

  • hash(k)=(AkmodW) / (W/M)
  • A взаимно простое с W
  • на практике удобно W=2w,M=2m, w - размер машинного слова (16, 32, 64, как правило) Тогда хеш-функция превращается в hash(k)=(Akmod2w) / 2wm, которую можно записать как простую функцию со "сдвигом":

    1  hash(key, A, M):
    2      return A * x >> (w - M)
    
  • Akmod2w - аналогично хешированию делением, но A вносит "возмущение" в ключ k

  • Операция "сдвига" раскладывается на две операции:
    • собственно "деление" на 2(wm)
    • удаление дробной части происходит естественным образом за счет "сдвига"

Такой подход очень быстр из-за простоты базовых операций (выполняющихся быстрее деления).

Более того, подобная функция близка к универсальной, если A - случайное нечетное число из {1,...,2w1}. Про это мы еще поговорим.

Вопрос: почему нечетное?

Хеш-функции для строк и последовательностей

  • хеш должен зависеть от каждого символа
  • и хеш должен зависеть от каждого символа по-разному!
  • строки с одинаковыми символами в разном порядке, желательно, должны хешироваться по-разному

Пример простой хеш-функции для последовательностей: хеш-функция Пирсона.

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 (add-rotate-xor)

Быстрые и используемые на практике наборы функций для хеширования последовательностей.

Базируются на следующем принципе:

  • Сложение двух чисел по моудлю
  • Сдвиг элементов
  • XOR

Варианты ARX-алгоритма SipHash используются во многих языках, в том числе, python3.4+, Ruby, Perl.

</font>

в начало

Схемы адресации

Квадратичный пробинг

  • hash(k,i)=(hash(k)+с1i+с2i2)modM.
  • Лучше линейного, но нужно подбирать с1,с2,M.

Несколько популярных вариантов выбора констант

  • hash(k)=(hash(k)+i2)modM, где c1=0,c2=1,Mпростое>3, фактор заполнения α<1/2
  • hash(k)=(hash(k)+(i+i2) / 2)modM, где c1=c2=1/2,M=2k
  • hash(k)=(hash(k)+1ii2)modM, где M3 mod 4

Почему α<1/2? Пусть есть сдивги x и y, указывающие на одну локацию, но xy, и 0x,yM/2.

hash(k)+x2hash(k)+y2modM
x2y2modM
x2y20 modM
(xy)(x+y)0 modM

xy0,x+y0 - значит, существует M/2 различных мест для записи.

Чем квадратичный пробинг лучше линейного?

Равномерность распределения хешей по таблице зависит от количества возможных вариантов обхода при пробинге.

  • Максимально M! вариантов обхода, столько требуется для "честного" равномерного хеширования
  • Для линейного пробинга - всего M вариантов, по числу стартовых точек
  • Для квадратичного пробинга так же всего M, но сами последовательности пробинга распределены более равномерно

Двойное хеширование

Идея: использовать 2 хеш-функции, чтобы получить M2 последовательностей пробинга.

Формально, это

hash(k,i)=(hash1(k)+ihash2(k))modM

Для обхода всех ячеек при пробинге hash2(k) должна всегда возвращать взаимно простое с M число. Простой способ сделать это:

  • Выбирать размер хеш-таблицы как степень 2: M=2p
  • hash2 всегда возвращает нечетное число
  • К сожалению, на практике, если размер таблицы 2p, выбрать функцию может быть тяжело

Тем не менее, C# HashTable использует двойное хеширование

Количество попыток при пробинге

  • В случае, если распределение ключей близко к равномерному (хорошая хеш-функция и пробинг)
  • фактор заполненности таблицы равен α<1,

Количество попыток пробинга равно в среднем: 11α

  • на практике все может быть хуже :)

</font>

в начало

Универсальное хеширование

Универсальное хеширование случайно выбирает хеш-функцию из семейтсва хеш-функций, чтобы осложнить жизнь злоумышленнику (или просто избежать ситуации с "плохими" данными).

H - семейство хеш-функций, отображающих ключи во множество {0..M1}. При этом, для пары ключей k,l количество хеш-функций, для которых hash(k)=hash(l), не более |H|/M

  • то есть шанс коллизии для случайно выбранной хеш-функции и двух случайных ключей не более 1/M
  • это позволяет получить близкое к идеальному поведение

Построение класса универсальных хеш-функций

  • Выберем p достаточно большое, чтобы любой ключ k попадал в диапазон от 0 до p1
  • Zp={0,1..p1}, Zp={1,2..p1} - множества чисел
  • hashab(k)=((ak+b)modp)modM - хеш-функция
  • H={hashab:aZpbZp} - всего p(p1) функций

Как получилось, что это универсальные хеш-функции

r=(ak+b)modp
s=(al+b)modp

rsa(kl) (modp) - так как p - простое, и (kl),a не равны 0 по модулю p.

Поэтому коллизий (ak+b)modp не возникает. Более того, все пары (r,s) будут различными для каждой пары (a,b).

Осталось проверить, что коллизий не возникает и при взятииmodM. Поскольку разные r,s равновероятны, вероятность коллизии kиl равна вероятности того, что rs(mod M). Ее можно вычислить так:

p/m1((p+m1)/m)1=(p1)/m

- количество значений для r таких, что sr и sr(mod M). Поскольку есть (p1) возможных s, вероятность будет

((p1)/m)(p1)=1/m,
что удовлетворяет требованию к универсальной хеш-функции.

</font>

в начало

Пример кода на python (нерабочий, но поясняющий идею создания семейства функций)

In [ ]:
# Так

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
In [ ]:
# Или так

class UnivHash:
    def __init__(self, a, b):
        pass
        
    def __call__(self, key):
        pass

Домашняя работа

  • Для открытой адресации реализуйте двойное хеширование
  • Для метода цепочек реализуйте создание универсальной хеш-функции

(Также см. предыдущую домашнюю работу)

</font>

в начало

Сслыки

</font>

в начало