Какие целочисленные хеш-функции подходят для приема целочисленного хеш-ключа?


Ответы:


47

Мультипликативный метод Кнута:

hash(i)=i*2654435761 mod 2^32

В общем, вы должны выбрать множитель в порядке размера вашего хэша (2^32 в примере) и не имеет с ним общих множителей. Таким образом, хеш-функция равномерно покрывает все ваше хеш-пространство.

Изменить: самый большой недостаток этой хеш-функции заключается в том, что она сохраняет делимость, поэтому, если все ваши целые числа делятся на 2 или 4 (что нередко), их хеши тоже будут. Это проблема хеш-таблиц - в итоге вы можете использовать только 1/2 или 1/4 ведра.


37
Это действительно плохая хеш-функция, хотя и привязанная к известному имени.
Сеун Осева,

6
Это совсем не плохая хеш-функция, если она используется с простыми размерами таблиц. Также он предназначен для закрытого хеширования. Если значения хеш-функции распределяются неравномерно, мультипликативное хеширование гарантирует, что коллизии с одним значением вряд ли "нарушат" элементы с другими значениями хеш-функции.
Паоло Бонзини

11
Для любопытных эта константа выбрана равной размеру хэша (2 ^ 32), деленному на Phi
awdz9nld

7
Паоло: Метод Кнута «плохой» в том смысле, что он не
обрушивается

10
При ближайшем рассмотрении оказывается, что 2654435761 на самом деле простое число. Вероятно, поэтому он был выбран, а не 2654435769.
karadoc 05

150

Я обнаружил, что следующий алгоритм обеспечивает очень хорошее статистическое распределение. Каждый входной бит влияет на каждый выходной бит с вероятностью около 50%. Коллизий нет (каждый вход приводит к другому выходу). Алгоритм работает быстро, за исключением случаев, когда ЦП не имеет встроенного блока умножения целых чисел. Код C, при условии, что intон 32-битный (для Java замените >>на >>>и удалите unsigned):

unsigned int hash(unsigned int x) {
    x = ((x >> 16) ^ x) * 0x45d9f3b;
    x = ((x >> 16) ^ x) * 0x45d9f3b;
    x = (x >> 16) ^ x;
    return x;
}

Магическое число было рассчитано с помощью специальной многопоточной тестовой программы, которая работала в течение многих часов и рассчитывала лавинный эффект (количество выходных битов, которые меняются при изменении одного входного бита; в среднем должно быть почти 16), независимость от изменения выходного бита (выходные биты не должны зависеть друг от друга) и вероятность изменения каждого выходного бита при изменении любого входного бита. Вычисленные значения лучше, чем у 32-битного финализатора, используемого MurmurHash , и почти так же хороши (не совсем), как при использовании AES . Небольшое преимущество заключается в том, что одна и та же константа используется дважды (это немного ускорило работу в последний раз, когда я тестировал, не уверен, что это все еще так).

Вы можете изменить процесс (получить значение входного сигнала от хэша) , если заменить 0x45d9f3bс 0x119de1f3мультипликативной инверсии ):

unsigned int unhash(unsigned int x) {
    x = ((x >> 16) ^ x) * 0x119de1f3;
    x = ((x >> 16) ^ x) * 0x119de1f3;
    x = (x >> 16) ^ x;
    return x;
}

Для 64-битных чисел я предлагаю использовать следующее, даже если оно может быть не самым быстрым. Этот основан на splitmix64 , который, похоже, основан на статье в блоге Better Bit Mixing (mix 13).

uint64_t hash(uint64_t x) {
    x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
    x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb);
    x = x ^ (x >> 31);
    return x;
}

Для Java используйте long, добавить Lк константе, заменить >>на >>>и удалить unsigned. В этом случае реверсирование более сложное:

uint64_t unhash(uint64_t x) {
    x = (x ^ (x >> 31) ^ (x >> 62)) * UINT64_C(0x319642b2d24d8ec3);
    x = (x ^ (x >> 27) ^ (x >> 54)) * UINT64_C(0x96de1b173f119089);
    x = x ^ (x >> 30) ^ (x >> 60);
    return x;
}

Обновление: вы также можете посмотреть проект Hash Function Prospector , где перечислены другие (возможно, лучшие) константы.


2
первые две строчки точно такие же! здесь опечатка?
Kshitij Banerjee

3
Нет, это не опечатка, вторая строка еще больше перемешивает биты. Использование только одного умножения не так хорошо.
Thomas Mueller

3
Я изменил магическое число, потому что в соответствии с контрольным примером я написал значение 0x45d9f3b, обеспечивающее лучшую путаницу и распространение , особенно то, что если один выходной бит изменяется, каждый другой выходной бит изменяется примерно с той же вероятностью (в дополнение к тому, что все выходные биты изменяются с изменением такая же вероятность при изменении входного бита). Как вы меряли 0x3335b369 у вас лучше работает? Подходит ли вам int 32 бит?
Thomas Mueller

3
Я ищу хорошую хеш-функцию для 64-битного unsigned int до 32-битного unsigned int. В этом случае магическое число будет таким же? Я сдвинул 32 бита вместо 16 бит.
Алессандро

3
Я считаю, что в этом случае лучше было бы увеличить коэффициент, но вам нужно будет провести несколько тестов. Или (это то, что я делаю) сначала используйте, x = ((x >> 32) ^ x)а затем используйте 32-битное умножение, указанное выше. Я не уверен, что лучше. Вы также можете посмотреть 64-битный финализатор для Murmur3
Томас Мюллер

29

Зависит от того, как распределяются ваши данные. Для простого счетчика простейшая функция

f(i) = i

будет хорошо (подозреваю оптимально, но не могу это доказать).


3
Проблема заключается в том, что обычно используются большие наборы целых чисел, которые делятся на общий множитель (адреса памяти с выравниванием по словам и т. Д.). Теперь, если ваша хеш-таблица делится на один и тот же множитель, вы получите только половину (или 1/4, 1/8 и т. Д.) Ведра.
Рафал Довгирд,

8
@Rafal: Вот почему в ответе написано «для простого счетчика» и «Зависит от того, как распределяются ваши данные»
erikkallen

5
На самом деле это реализация Sun метода hashCode () в java.lang.Integer grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…
Хуанде Каррион

5
@JuandeCarrion Это вводит в заблуждение, потому что это не хеш, который используется. После перехода к использованию мощности двух размеров таблиц Java повторно хеширует каждый возвращаемый хэш .hashCode(), см. Здесь .
Esailija 01

8
Функция идентификации довольно бесполезна в качестве хеша во многих практических приложениях из-за ее свойств распределения (или их отсутствия), если, конечно, локальность не является желаемым атрибутом
awdz9nld

12

Быстрые и хорошие хэш-функции могут быть составлены из быстрых перестановок с меньшими качествами, например

  • умножение на нечетное целое число
  • бинарные вращения
  • xorshift

Чтобы получить функцию хеширования с превосходным качеством, как показано с PCG для генерации случайных чисел.

Фактически, это также рецепт, который rrxmrrxmsx_0 и murmur hash используют, сознательно или неосознанно.

Я лично нашел

uint64_t xorshift(const uint64_t& n,int i){
  return n^(n>>i);
}
uint64_t hash(const uint64_t& n){
  uint64_t p = 0x5555555555555555ull; // pattern of alternating 0 and 1
  uint64_t c = 17316035218449499591ull;// random uneven integer constant; 
  return c*xorshift(p*xorshift(n,32),32);
}

быть достаточно хорошим.

Хорошая хеш-функция должна

  1. быть биективным, чтобы не терять информацию, если возможно, и иметь наименьшее количество конфликтов
  2. каскадировать как можно больше и равномернее, т.е. каждый входной бит должен переворачивать каждый выходной бит с вероятностью 0,5.

Давайте сначала посмотрим на функцию идентификации. Он удовлетворяет 1., но не 2.:

функция идентичности

Входной бит n определяет выходной бит n с корреляцией 100% (красный) и никакие другие, поэтому они синие, что дает идеальную красную линию.

Xorshift (n, 32) не намного лучше, давая полторы строки. Все еще удовлетворяет 1., потому что он обратим со вторым приложением.

xorshift

Умножение на целое число без знака намного лучше, каскадирование сильнее и большее количество выходных битов с вероятностью 0,5, что вы хотите, зеленым цветом. Он удовлетворяет 1. так как для каждого нечетного целого числа существует мультипликативное обратное.

Knuth

Объединение этих двух дает следующий результат, по-прежнему удовлетворяющий 1., поскольку композиция двух биективных функций дает другую биективную функцию.

knuth • xorshift

Второе применение умножения и xorshift даст следующее:

предложенный хеш

Или вы можете использовать умножения поля Галуа, такие как GHash , они стали достаточно быстрыми на современных процессорах и имеют превосходное качество за один шаг.

   uint64_t const inline gfmul(const uint64_t& i,const uint64_t& j){           
     __m128i I{};I[0]^=i;                                                          
     __m128i J{};J[0]^=j;                                                          
     __m128i M{};M[0]^=0xb000000000000000ull;                                      
     __m128i X = _mm_clmulepi64_si128(I,J,0);                                      
     __m128i A = _mm_clmulepi64_si128(X,M,0);                                      
     __m128i B = _mm_clmulepi64_si128(A,M,0);                                      
     return A[0]^A[1]^B[1]^X[0]^X[1];                                              
   }

gfmul: Код выглядит псевдокодом, поскольку afaik вы не можете использовать скобки с __m128i. Все еще очень интересно. Первая строка, кажется, говорит: «возьмите унифицированный __m128i (I) и xor его с (параметром) i. Следует ли мне читать это как инициализировать I с 0 и xor с i? Если да, будет ли это то же самое, что и load I с i и выполнить не (операцию) на I?
января

@Jan я бы хотел, чтобы это было __m128i I = i; //set the lower 64 bits, но я не могу, поэтому я использую ^=. 0^1 = 1следовательно, никто не учитывается. Что касается инициализации с помощью {}моего компилятора, я никогда не жаловался, возможно, это не лучшее решение, но я хочу с этим все инициализировать до 0, чтобы я мог сделать ^=или |=. Я думаю, что я основал этот код на этом блоге, который также дает инверсию, очень полезную: D
Вольфганг Брем



3

На Eternal Confuzzled есть хороший обзор некоторых хэш-алгоритмов . Я бы порекомендовал одноразовый хэш Боба Дженкинса, который быстро достигает лавины и поэтому может использоваться для эффективного поиска по хеш-таблице.


4
Это хорошая статья, но она сосредоточена на хешировании строковых ключей, а не целых чисел.
Адриан Муат,

Для ясности: хотя методы, описанные в статье, будут работать с целыми числами (или могут быть адаптированы к ним), я предполагаю, что есть более эффективные алгоритмы для целых чисел.
Адриан Муат

2

Ответ зависит от многих вещей, например:

  • Где вы собираетесь его использовать?
  • Что ты пытаешься сделать с хешем?
  • Вам нужна криптографически безопасная хеш-функция?

Предлагаю вам взглянуть на семейство хэш-функций Меркла-Дамгарда, таких как SHA-1 и т. Д.


1

Я не думаю, что мы можем сказать, что хеш-функция «хорошая», не зная заранее ваших данных! и не зная, что вы собираетесь с этим делать.

Существуют структуры данных лучше, чем хеш-таблицы для неизвестных размеров данных (я предполагаю, что вы выполняете хеширование для хеш-таблицы здесь). Я бы лично использовал хеш-таблицу, когда знаю, что у меня есть «конечное» количество элементов, которые необходимо хранить в ограниченном объеме памяти. Я бы попытался провести быстрый статистический анализ моих данных, посмотреть, как они распределяются и т. Д., Прежде чем я начну думать о своей хэш-функции.


1

Для случайных значений хеш-функции некоторые инженеры сказали, что простое число золотого сечения (2654435761) - плохой выбор, но по результатам тестирования я обнаружил, что это не так; вместо этого 2654435761 довольно хорошо распределяет хеш-значения.

#define MCR_HashTableSize 2^10

unsigned int
Hash_UInt_GRPrimeNumber(unsigned int key)
{
  key = key*2654435761 & (MCR_HashTableSize - 1)
  return key;
}

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

Я написал тестовую программу для оценки многих хэш-функций для целых чисел, результаты показывают, что GRPrimeNumber - довольно хороший выбор.

Я пытался:

  1. total_data_entry_number / total_bucket_number = 2, 3, 4; где total_bucket_number = размер хеш-таблицы;
  2. преобразовать домен хеш-значения в домен индекса корзины; то есть преобразовать хеш-значение в индекс корзины с помощью логической операции и (hash_table_size - 1), как показано в Hash_UInt_GRPrimeNumber ();
  3. рассчитать количество столкновений каждого ведра;
  4. записать сегмент, который не был отображен, то есть пустой сегмент;
  5. узнать максимальное количество столкновений всех ведер; то есть наибольшая длина цепи;

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

Некоторые хеш-функции для целых чисел считаются хорошими, но результаты тестирования показывают, что когда total_data_entry / total_bucket_number = 3, самая длинная длина цепочки больше 10 (максимальное число столкновений> 10), и многие сегменты не отображаются (пустые сегменты ), что очень плохо по сравнению с результатом нулевого пустого ведра и максимальной длины цепочки 3 при хешировании простых чисел золотого сечения.

Кстати, с результатами моего тестирования я обнаружил, что одна версия хэш-функций shift-xor довольно хороша (ее разделяет mikera).

unsigned int Hash_UInt_M3(unsigned int key)
{
  key ^= (key << 13);
  key ^= (key >> 17);    
  key ^= (key << 5); 
  return key;
}

2
Но тогда почему бы не изменить продукт правильно, сохранив самые смешанные части? Это было так , как это должно было работать
Harold

1
@harold, простое число золотого сечения тщательно выбрано, хотя я думаю, что это не будет иметь никакого значения, но я проверю, намного ли лучше с «наиболее смешанными битами». Хотя я считаю, что «это не лучший выбор». неверно, как показывают результаты тестирования, просто захватить нижнюю часть битов достаточно, и даже лучше, чем многие хеш-функции.
Chen-ChungChia

(2654435761, 4295203489) - золотое сечение простых чисел.
Chen-ChungChia

(1640565991, 2654435761) также является золотым сечением простых чисел.
Chen-ChungChia

@harold, Сдвиг продукта вправо становится хуже, даже если просто сдвинуть вправо на 1 позицию (деленную на 2), все равно станет хуже (хотя по-прежнему нулевое пустое ведро, но самая длинная длина цепочки больше); сдвигаясь вправо на большее количество позиций, результат становится хуже. Зачем? Я думаю, причина в следующем: смещение продукта вправо делает больше хеш-значений не взаимно простыми, просто я предполагаю, настоящая причина связана с теорией чисел.
Chen-ChungChia

1

Я использую splitmix64(указано в ответе Томаса Мюллера ) с тех пор, как нашел эту ветку. Однако недавно я наткнулся на rrxmrrxmsx_0 Пелле Эвенсена , который давал намного лучшее статистическое распределение, чем исходный финализатор MurmurHash3 и его преемники ( splitmix64и другие смеси). Вот фрагмент кода на C:

#include <stdint.h>

static inline uint64_t ror64(uint64_t v, int r) {
    return (v >> r) | (v << (64 - r));
}

uint64_t rrxmrrxmsx_0(uint64_t v) {
    v ^= ror64(v, 25) ^ ror64(v, 50);
    v *= 0xA24BAED4963EE407UL;
    v ^= ror64(v, 24) ^ ror64(v, 49);
    v *= 0x9FB21C651E98DF25UL;
    return v ^ v >> 28;
}

Pelle также предоставляет углубленный анализ 64-битного микшера, используемого на последнем этапе MurmurHash3и более поздних вариантах.


2
Эта функция не является биективной. Для всех v, где v = ror (v, 25), а именно всех 0 и всех 1, он будет производить одинаковый результат в двух местах. Для всех значений v = ror64 (v, 24) ^ ror64 (v, 49), которые по крайней мере два больше и то же самое с v = ror (v, 28), давая еще 2 ^ 4, всего около 22 ненужных столкновений . Два применения сплитмикса, вероятно, так же хороши и столь же быстры, но все же обратимы и свободны от столкновений.
Вольфганг Брем,
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.