Hàm băm số nguyên nào tốt chấp nhận khóa băm số nguyên?


Câu trả lời:


47

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^32trong 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.


36
Đó là một hàm băm thực sự tồi, mặc dù được gắn với một cái tên nổi tiếng.
Seun Osewa

5
Nó không phải là một hàm băm tồi nếu được sử dụng với kích thước bảng nguyên tố. Ngoài ra, nó có nghĩa là để băm đóng . Nếu các giá trị băm không được phân phối đồng nhất, thì băm nhiều lần đảm bảo rằng các xung đột từ một giá trị không có khả năng "làm phiền" các mục với các giá trị băm khác.
Paolo Bonzini

11
Đối với người hiếu, hằng số này được chọn để trở thành kích thước băm (2 ^ 32) chia cho Phi
awdz9nld

7
Paolo: Phương pháp của Knuth là "xấu" theo nghĩa là nó không phải trận tuyết lở hiện trên các bit trên
awdz9nld

9
Kiểm tra kỹ hơn, hóa ra 2654435761 thực sự là một số nguyên tố. Vì vậy, có lẽ đó là lý do tại sao nó được chọn chứ không phải là 2654435769.
karadoc

149

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ử intlà 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ế 0x45d9f3bbằ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 Lvà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ê.


2
hai dòng đầu tiên giống hệt nhau! có lỗi đánh máy ở đây không?
Kshitij Banerjee

3
Không, đây không phải là lỗi đánh máy, dòng thứ hai trộn thêm các bit. Chỉ sử dụng một phép nhân là không tốt.
Thomas Mueller,

3
Tôi đã thay đổi số ma thuật bởi vì theo một trường hợp thử nghiệm, tôi đã viết giá trị 0x45d9f3b cung cấp sự nhầm lẫn và khuếch tán tốt hơn , đặc biệt là nếu một bit đầu ra thay đổi, mỗi bit đầu ra khác thay đổi với cùng một xác suất (ngoài ra tất cả các bit đầu ra thay đổi theo cùng xác suất nếu một bit đầu vào thay đổi). Làm thế nào để bạn đo lường 0x3335b369 hoạt động tốt hơn cho bạn? Có phải là một int 32 bit cho bạn?
Thomas Mueller

3
Tôi đang tìm kiếm một hàm băm đẹp cho int 64 bit unsigned int đến 32 bit unsigned int. Là đối với trường hợp đó, số kỳ diệu trên sẽ giống nhau? Tôi đã chuyển 32 bit thay vì 16 bit.
alessandro

3
Tôi tin rằng trong trường hợp đó, một hệ số lớn hơn sẽ tốt hơn, nhưng bạn sẽ cần phải chạy một số thử nghiệm. Hoặc (đây là những gì tôi làm) trước tiên sử dụng 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
Thomas Mueller

29

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 đó).


3
Vấn đề với điều này là thường có các tập hợp số nguyên lớn chia hết cho một thừa số chung (địa chỉ bộ nhớ được căn chỉnh theo từ, v.v.). Bây giờ nếu bảng băm của bạn chia hết cho cùng một thừa số, bạn sẽ chỉ sử dụng một nửa (hoặc 1/4, 1/8, v.v.) nhóm.
Rafał Dowgird

8
@Rafal: Đó là lý do tại sao phản ứng nói "cho một bộ đếm đơn giản" và "phụ thuộc vào cách dữ liệu của bạn được phân phối"
erikkallen

5
Đó thực sự là việc Sun thực hiện phương thức hashCode () trong java.lang.Integer grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…
Juande Carrion

5
@JuandeCarrion Điều đó gây hiểu lầm vì đó không phải là hàm băm đang được sử dụng. Sau khi chuyển sang sử dụng sức mạnh của hai kích thước bảng, Java sẽ lặp lại mọi hàm băm được trả về từ đó .hashCode(), xem tại đây .
Esailija

8
Chức năng nhận diện là khá vô dụng như một hash trong nhiều ứng dụng thực tế do tính chất phân phối của mình (hoặc thiếu đó), trừ khi, tất nhiên, địa phương là một thuộc tính mong muốn
awdz9nld

12

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ư

  • phép nhân với một số nguyên không đồng đều
  • phép quay nhị phân
  • xorshift

Để 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

  1. khách quan để không bị mất thông tin, nếu có thể và ít va chạm nhất
  2. thác càng nhiều càng tốt, tức là mỗi bit đầu vào nên lật mọi bit đầu ra với xác suất 0,5.

Đầ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.:

chức năng nhận dạng

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.

xorshift

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.

knuth

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.

knuth • xorshift

Một ứng dụng thứ hai của phép nhân và xorshift sẽ mang lại kết quả sau:

đề xuất băm

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];                                              
   }

gfmul: Mã có vẻ là mã giả, vì afaik bạn không thể sử dụng dấu ngoặc với __m128i. Vẫn rất thú vị. Dòng đầu tiên xuất hiện cho biết "lấy một __m128i (I) được đơn nguyên hóa và xor nó với (tham số) i. Tôi có nên đọc phần này là khởi tạo I với 0 và xor với i không? Nếu vậy, nó có giống như tải I với i không và thực hiện không (hoạt động) trên I?
Ngày

@Jan những gì tôi muốn là làm __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 = 1do đó 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
Wolfgang Brehm

6

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.


6
  • 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

  • Hàm băm số nguyên

3

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ả.


4
Đó là một bài viết hay, nhưng nó tập trung vào các khóa chuỗi băm, không phải số nguyên.
Adrian Mouat

Chỉ cần nói rõ, mặc dù các phương pháp trong bài viết sẽ hoạt động đối với số nguyên (hoặc có thể được điều chỉnh cho phù hợp), tôi cho rằng có những thuật toán hiệu quả hơn cho số nguyên.
Adrian Mouat

2

Câu trả lời phụ thuộc vào rất nhiều thứ như:

  • Bạn định tuyển dụng nó ở đâu?
  • Bạn đang cố gắng làm gì với hàm băm?
  • Bạn có cần một hàm băm an toàn bằng mật mã không?

Tôi khuyên bạn nên xem qua họ các hàm băm Merkle-Damgard như SHA-1, v.v.


1

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.


1

Đố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ử:

  1. total_data_entry_number / total_bucket_number = 2, 3, 4; trong đó total_bucket_number = kích thước bảng băm;
  2. ánh xạ miền giá trị băm vào miền chỉ mục xô; nghĩa là, chuyển đổi giá trị băm thành chỉ số thùng bằng Logical And Operation với (hash_table_size - 1), như được hiển thị trong Hash_UInt_GRPrimeNumber ();
  3. tính số va chạm của mỗi gầu;
  4. ghi lại thùng chưa được ánh xạ, tức là thùng rỗng;
  5. tìm ra số va chạm tối đa của tất cả các nhóm; nghĩa là, chiều dài chuỗi dài nhất;

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;
}

2
Nhưng sau đó tại sao không chuyển sản phẩm sang phải, vì vậy bạn giữ lại các bit hỗn hợp nhất? Đó là cách nó được cho là hoạt động
harold

1
@harold, số nguyên tố tỷ lệ vàng được chọn cẩn thận, mặc dù tôi nghĩ rằng nó sẽ không tạo ra bất kỳ sự khác biệt nào, nhưng tôi sẽ kiểm tra xem nó có tốt hơn nhiều với "các bit hỗn hợp nhất" không. Trong khi quan điểm của tôi là "Đó không phải là một lựa chọn tốt." không đúng, như kết quả thử nghiệm cho thấy, chỉ cần lấy phần dưới của các bit là đủ tốt, và thậm chí tốt hơn nhiều hàm băm.
Chen-ChungChia

(2654435761, 4295203489) là một tỷ lệ vàng của các số nguyên tố.
Chen-ChungChia

(1640565991, 2654435761) cũng là một tỷ lệ vàng của các số nguyên tố.
Chen-ChungChia

@harold, Dịch chuyển bên phải sản phẩm trở nên tồi tệ hơn, ngay cả khi chỉ cần dịch chuyển sang phải 1 vị trí (chia cho 2), nó vẫn trở nên tồi tệ hơn (mặc dù vẫn bằng không thùng rỗng, nhưng chiều dài chuỗi dài nhất lớn hơn); dịch chuyển sang phải nhiều vị trí hơn, kết quả trở nên tồi tệ hơn. Tại sao? Tôi nghĩ lý do là: chuyển quyền sản phẩm làm cho nhiều giá trị băm hơn không phải là đúng, chỉ là suy đoán của tôi, lý do thực sự liên quan đến lý thuyết số.
Chen-ChungChia

1

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ó ( splitmix64và 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 MurmurHash3và các biến thể gần đây hơn.


2
Chức năng này không mang tính chất sinh học. Với tất cả v trong đó v = ror (v, 25), cụ thể là tất cả 0 và tất cả 1, nó sẽ tạo ra cùng một sản lượng ở hai nơi. Đối với tất cả các giá trị v = ror64 (v, 24) ^ ror64 (v, 49), có ít nhất hai giá trị lớn hơn và giống nhau với v = ror (v, 28), mang lại 2 ^ 4 khác, tổng cộng khoảng 22 va chạm không cần thiết . Hai ứng dụng của splitmix có lẽ tốt và nhanh như nhau, nhưng vẫn có thể đảo ngược và không có va chạm.
Wolfgang Brehm
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.