Какие целочисленные хеш-функции подходят для приема целочисленного хеш-ключа?
Ответы:
Мультипликативный метод Кнута:
hash(i)=i*2654435761 mod 2^32
В общем, вы должны выбрать множитель в порядке размера вашего хэша (2^32
в примере) и не имеет с ним общих множителей. Таким образом, хеш-функция равномерно покрывает все ваше хеш-пространство.
Изменить: самый большой недостаток этой хеш-функции заключается в том, что она сохраняет делимость, поэтому, если все ваши целые числа делятся на 2 или 4 (что нередко), их хеши тоже будут. Это проблема хеш-таблиц - в итоге вы можете использовать только 1/2 или 1/4 ведра.
Я обнаружил, что следующий алгоритм обеспечивает очень хорошее статистическое распределение. Каждый входной бит влияет на каждый выходной бит с вероятностью около 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 , где перечислены другие (возможно, лучшие) константы.
x = ((x >> 32) ^ x)
а затем используйте 32-битное умножение, указанное выше. Я не уверен, что лучше. Вы также можете посмотреть 64-битный финализатор для Murmur3
Зависит от того, как распределяются ваши данные. Для простого счетчика простейшая функция
f(i) = i
будет хорошо (подозреваю оптимально, но не могу это доказать).
.hashCode()
, см. Здесь .
Быстрые и хорошие хэш-функции могут быть составлены из быстрых перестановок с меньшими качествами, например
Чтобы получить функцию хеширования с превосходным качеством, как показано с 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.:
Входной бит n определяет выходной бит n с корреляцией 100% (красный) и никакие другие, поэтому они синие, что дает идеальную красную линию.
Xorshift (n, 32) не намного лучше, давая полторы строки. Все еще удовлетворяет 1., потому что он обратим со вторым приложением.
Умножение на целое число без знака намного лучше, каскадирование сильнее и большее количество выходных битов с вероятностью 0,5, что вы хотите, зеленым цветом. Он удовлетворяет 1. так как для каждого нечетного целого числа существует мультипликативное обратное.
Объединение этих двух дает следующий результат, по-прежнему удовлетворяющий 1., поскольку композиция двух биективных функций дает другую биективную функцию.
Второе применение умножения и 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];
}
__m128i I = i; //set the lower 64 bits
, но я не могу, поэтому я использую ^=
. 0^1 = 1
следовательно, никто не учитывается. Что касается инициализации с помощью {}
моего компилятора, я никогда не жаловался, возможно, это не лучшее решение, но я хочу с этим все инициализировать до 0, чтобы я мог сделать ^=
или |=
. Я думаю, что я основал этот код на этом блоге, который также дает инверсию, очень полезную: D
На этой странице перечислены некоторые простые хеш-функции, которые в целом работают прилично, но у любого простого хеша есть патологические случаи, когда он не работает.
32-битный мультипликативный метод (очень быстрый) см. @Rafal
#define hash32(x) ((x)*2654435761)
#define H_BITS 24 // Hashtable size
#define H_SHIFT (32-H_BITS)
unsigned hashtab[1<<H_BITS]
....
unsigned slot = hash32(x) >> H_SHIFT
32-битные и 64- битные (хорошее распространение) по адресу: MurmurHash
На Eternal Confuzzled есть хороший обзор некоторых хэш-алгоритмов . Я бы порекомендовал одноразовый хэш Боба Дженкинса, который быстро достигает лавины и поэтому может использоваться для эффективного поиска по хеш-таблице.
Ответ зависит от многих вещей, например:
Предлагаю вам взглянуть на семейство хэш-функций Меркла-Дамгарда, таких как SHA-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 - довольно хороший выбор.
Я пытался:
По результатам тестирования я обнаружил, что простое число золотого сечения всегда имеет меньше пустых контейнеров или нулевое количество пустых контейнеров и самую короткую длину цепочки столкновений.
Некоторые хеш-функции для целых чисел считаются хорошими, но результаты тестирования показывают, что когда 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;
}
Я использую 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
и более поздних вариантах.