Câu trả lời:
Phương pháp nhân của Knuth:
hash(i)=i*2654435761 mod 2^32
Nói chung, bạn nên chọn một hệ số theo thứ tự của kích thước băm của bạn ( 2^32
trong ví dụ) và không có hệ số chung nào với nó. Bằng cách này, hàm băm bao phủ đồng nhất tất cả không gian băm của bạn.
Chỉnh sửa: Nhược điểm lớn nhất của hàm băm này là nó bảo toàn tính chất chia hết, vì vậy nếu các số nguyên của bạn chia hết cho 2 hoặc 4 (điều này không có gì lạ), thì hàm băm của chúng cũng sẽ như vậy. Đây là một vấn đề trong bảng băm - bạn có thể chỉ sử dụng 1/2 hoặc 1/4 số nhóm.
Tôi thấy thuật toán sau cung cấp một phân phối thống kê rất tốt. Mỗi bit đầu vào ảnh hưởng đến mỗi bit đầu ra với xác suất khoảng 50%. Không có xung đột (mỗi đầu vào dẫn đến một đầu ra khác nhau). Thuật toán nhanh chóng ngoại trừ nếu CPU không có đơn vị nhân số nguyên được tích hợp sẵn. Mã C, giả sử int
là 32 bit (đối với Java, thay thế >>
bằng >>>
và xóa unsigned
):
unsigned int hash(unsigned int x) {
x = ((x >> 16) ^ x) * 0x45d9f3b;
x = ((x >> 16) ^ x) * 0x45d9f3b;
x = (x >> 16) ^ x;
return x;
}
Con số kỳ diệu đã được tính toán bằng cách sử dụng một chương trình kiểm tra đa luồng đặc biệt chạy trong nhiều giờ, tính toán hiệu ứng tuyết lở (số lượng bit đầu ra thay đổi nếu một bit đầu vào duy nhất được thay đổi; trung bình phải gần 16), tính độc lập của thay đổi bit đầu ra (các bit đầu ra không được phụ thuộc vào nhau) và xác suất thay đổi trong mỗi bit đầu ra nếu bất kỳ bit đầu vào nào bị thay đổi. Các giá trị được tính toán tốt hơn so với bộ hoàn thiện 32 bit được MurmurHash sử dụng và gần tốt (không hoàn toàn) như khi sử dụng AES . Một lợi thế nhỏ là cùng một hằng số được sử dụng hai lần (nó đã làm cho nó nhanh hơn một chút vào lần cuối tôi thử nghiệm, không chắc liệu có còn như vậy không).
Bạn có thể đảo ngược quá trình (lấy giá trị đầu vào từ hàm băm) nếu bạn thay thế 0x45d9f3b
bằng 0x119de1f3
( nghịch đảo nhân ):
unsigned int unhash(unsigned int x) {
x = ((x >> 16) ^ x) * 0x119de1f3;
x = ((x >> 16) ^ x) * 0x119de1f3;
x = (x >> 16) ^ x;
return x;
}
Đối với các số 64-bit, tôi khuyên bạn nên sử dụng cách sau, thậm chí nghĩ rằng nó có thể không phải là nhanh nhất. Cái này dựa trên splitmix64 , có vẻ như dựa trên bài viết blog 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;
}
Đối với Java, sử dụng long
, thêm L
vào hằng số, thay thế >>
bằng >>>
và loại bỏ unsigned
. Trong trường hợp này, việc đảo ngược phức tạp hơn:
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;
}
Cập nhật: Bạn cũng có thể muốn xem dự án Trình kiểm tra hàm băm , nơi các hằng số khác (có thể tốt hơn) được liệt kê.
x = ((x >> 32) ^ x)
và sau đó sử dụng các phép nhân 32 bit ở trên. Tôi không chắc điều gì tốt hơn. Bạn cũng có thể muốn xem xét 64-bit finalizer cho Murmur3
Phụ thuộc vào cách dữ liệu của bạn được phân phối. Đối với một bộ đếm đơn giản, chức năng đơn giản nhất
f(i) = i
sẽ tốt (tôi nghi ngờ là tối ưu, nhưng tôi không thể chứng minh điều đó).
Các hàm băm nhanh và tốt có thể được tạo từ các hoán vị nhanh với chất lượng thấp hơn, như
Để mang lại một hàm băm với chất lượng vượt trội, như được chứng minh với PCG để tạo số ngẫu nhiên.
Trên thực tế, đây cũng là công thức rrxmrrxmsx_0 và hàm băm âm u đang sử dụng, cố ý hoặc vô tình.
Cá nhân tôi đã tìm thấy
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);
}
đủ tốt.
Một hàm băm tốt nên
Đầu tiên chúng ta hãy xem xét chức năng nhận dạng. Nó thỏa mãn 1. nhưng không thỏa mãn 2.:
Bit đầu vào n xác định bit đầu ra n với mối tương quan là 100% (màu đỏ) và không có bit nào khác, do đó chúng có màu xanh lam, tạo ra một đường màu đỏ hoàn hảo.
Một xorshift (n, 32) cũng không khá hơn là bao, cho ra một dòng rưỡi. Vẫn thỏa mãn 1., bởi vì nó không thể đảo ngược với ứng dụng thứ hai.
Phép nhân với một số nguyên không dấu sẽ tốt hơn nhiều, xếp tầng mạnh hơn và lật nhiều bit đầu ra hơn với xác suất 0,5, đó là những gì bạn muốn, có màu xanh lục. Nó thỏa mãn 1. như đối với mỗi số nguyên không đều có một nghịch đảo nhân.
Kết hợp cả hai sẽ cho kết quả sau, vẫn thỏa mãn 1. vì sự hợp thành của hai hàm bijective tạo ra một hàm bijective khác.
Một ứng dụng thứ hai của phép nhân và xorshift sẽ mang lại kết quả sau:
Hoặc bạn có thể sử dụng phép nhân trường Galois như GHash , chúng đã trở nên nhanh chóng hợp lý trên các CPU hiện đại và có chất lượng vượt trội trong một bước.
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
, nhưng tôi không thể, vì vậy tôi đang sử dụng ^=
. 0^1 = 1
do đó không không xâm phạm. Về việc khởi tạo với {}
trình biên dịch của tôi chưa bao giờ bị phàn nàn, đó có thể không phải là giải pháp tốt nhất, nhưng những gì tôi muốn với đó là khởi tạo tất cả về 0 để tôi có thể làm ^=
hoặc |=
. Tôi nghĩ rằng tôi đã dựa trên mã đó trên blogpost này, điều này cũng mang lại sự đảo ngược, rất hữu ích: D
Trang này liệt kê một số hàm băm đơn giản có xu hướng giảm dần nói chung, nhưng bất kỳ hàm băm đơn giản nào cũng có những trường hợp bệnh lý mà nó không hoạt động tốt.
Phương pháp nhân 32 bit (rất nhanh) xem @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-bit và 64-bit (phân phối tốt) tại: MurmurHash
Có một cái nhìn tổng quan tốt đẹp về một số thuật toán băm tại Eternally Confuzzled . Tôi khuyên bạn nên sử dụng hàm băm một lần của Bob Jenkins để nhanh chóng đạt đến tuyết lở và do đó có thể được sử dụng để tra cứu bảng băm hiệu quả.
Câu trả lời phụ thuộc vào rất nhiều thứ như:
Tôi khuyên bạn nên xem qua họ các hàm băm Merkle-Damgard như SHA-1, v.v.
Tôi không nghĩ rằng chúng ta có thể nói rằng một hàm băm là "tốt" nếu không biết trước dữ liệu của bạn! và không biết bạn sẽ làm gì với nó.
Có cấu trúc dữ liệu tốt hơn bảng băm cho kích thước dữ liệu không xác định (tôi giả sử bạn đang thực hiện băm cho bảng băm ở đây). Cá nhân tôi sẽ sử dụng bảng băm khi tôi biết mình có một số phần tử "hữu hạn" cần được lưu trữ trong một lượng bộ nhớ hạn chế. Tôi sẽ thử và thực hiện một phân tích thống kê nhanh về dữ liệu của mình, xem nó được phân phối như thế nào, v.v. trước khi tôi bắt đầu nghĩ về hàm băm của mình.
Đối với các giá trị băm ngẫu nhiên, một số kỹ sư cho biết số nguyên tố tỷ lệ vàng (2654435761) là một lựa chọn tồi, với kết quả thử nghiệm của tôi, tôi thấy rằng nó không đúng; thay vào đó, 2654435761 phân phối các giá trị băm khá tốt.
#define MCR_HashTableSize 2^10
unsigned int
Hash_UInt_GRPrimeNumber(unsigned int key)
{
key = key*2654435761 & (MCR_HashTableSize - 1)
return key;
}
Kích thước bảng băm phải là lũy thừa của hai.
Tôi đã viết một chương trình thử nghiệm để đánh giá nhiều hàm băm cho số nguyên, kết quả cho thấy GRPrimeNumber là một lựa chọn khá tốt.
Tôi đã thử:
Với kết quả thử nghiệm của mình, tôi nhận thấy rằng Golden Ratio Prime Number luôn có ít xô trống hơn hoặc không xô rỗng và độ dài chuỗi va chạm ngắn nhất.
Một số hàm băm cho số nguyên được khẳng định là tốt, nhưng kết quả thử nghiệm cho thấy rằng khi total_data_entry / total_bucket_number = 3, độ dài chuỗi dài nhất lớn hơn 10 (số va chạm tối đa> 10) và nhiều nhóm không được ánh xạ (nhóm trống ), rất tệ, so với kết quả của thùng rỗng bằng 0 và độ dài chuỗi dài nhất 3 bằng Golden Ratio Prime Number Hashing.
BTW, với kết quả thử nghiệm của tôi, tôi thấy một phiên bản của các hàm băm shift-xor khá tốt (Nó được chia sẻ bởi mikera).
unsigned int Hash_UInt_M3(unsigned int key)
{
key ^= (key << 13);
key ^= (key >> 17);
key ^= (key << 5);
return key;
}
Tôi đã sử dụng splitmix64
(chỉ trong câu trả lời của Thomas Mueller ) kể từ khi tôi tìm thấy chủ đề này. Tuy nhiên, gần đây tôi tình cờ phát hiện ra rrxmrrxmsx_0 của Pelle Evensen , mang lại phân phối thống kê tốt hơn rất nhiều so với bản hoàn thiện MurmurHash3 ban đầu và các bản kế nhiệm của nó ( splitmix64
và các hỗn hợp khác). Đây là đoạn mã trong 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 cũng cung cấp một phân tích chuyên sâu về bộ trộn 64 bit được sử dụng trong bước cuối cùng MurmurHash3
và các biến thể gần đây hơn.