hàm băm cho chuỗi


124

Tôi đang làm việc trên bảng băm bằng ngôn ngữ C và tôi đang thử nghiệm hàm băm cho chuỗi.

Chức năng đầu tiên tôi đã thử là thêm mã ascii và sử dụng modulo (% 100) nhưng tôi đã nhận được kết quả kém với lần kiểm tra dữ liệu đầu tiên: 40 lần va chạm cho 130 từ.

Dữ liệu đầu vào cuối cùng sẽ chứa 8 000 từ (đó là một kho lưu trữ nhị nguyên trong một tệp). Bảng băm được khai báo là int table [10000] và chứa vị trí của từ trong tệp txt.

Câu hỏi đầu tiên là thuật toán nào tốt nhất cho chuỗi băm? và làm thế nào để xác định kích thước của bảng băm?

cảm ơn trước !

:-)


11
Nếu bảng băm của bạn có 10K mục nhập, tại sao bạn lại sử dụng modulo 100? Nhận được 40 va chạm trong số 130 từ không có gì đáng ngạc nhiên với một mô-đun nhỏ như vậy.
Carey Gregory

13
Xem burtleburtle.net/bob/hash/evahash.htmlpartow.net/programming/hashfunctions là những tài nguyên về các phép băm khác nhau (từ chung đến chuỗi đến tiền điện tử).

3
Để làm rõ @CareyGregory: Bạn có nhận ra rằng, như một sự thật toán học cơ bản, 130 mục trong 100 thùng (tức là mod 100) phải tạo ra 30 lần va chạm (trong đó va chạm được tính là mỗi lần một mục thứ hai, thứ ba, v.v. được đưa vào một cái xô), đúng không? Vì vậy, bạn chỉ cao hơn một chút.
derobert

4
@lilawood: OK, đó là những gì tôi đã tìm, nhưng để kiểm tra tốt hơn, bạn nên sử dụng 80 từ với bảng băm gồm 100 mục nhập. Điều đó sẽ cung cấp cho bạn tỷ lệ tương tự như dữ liệu trực tiếp của bạn và không gây ra xung đột.
Carey Gregory

4
Có thể trùng lặp của Tốt Hash Function cho Strings
MJ Rayburn

Câu trả lời:


185

Tôi đã có kết quả tốt với djb2Dan Bernstein.

unsigned long
hash(unsigned char *str)
{
    unsigned long hash = 5381;
    int c;

    while (c = *str++)
        hash = ((hash << 5) + hash) + c; /* hash * 33 + c */

    return hash;
}

37
trang được liên kết trong câu trả lời rất thú vị.
Adrien Plisson

2
làm thế nào chương trình hết vòng lặp while ?? = S
Daniel N.

1
@ danfly09 Khi c bằng không. Tương đương với while (c = * str ++) sẽ là (0! = (C = * str ++))
rxantos

5
@Josepas hàm băm lý tưởng nên trả về một size_thoặc giá trị không dấu khác như vậy (chẳng hạn như giá trị dài không dấu trong mã này). Người gọi có trách nhiệm lấy modulo của kết quả để phù hợp với bảng băm. Người gọi kiểm soát vùng bảng được băm thành; không phải là chức năng. Nó chỉ trả về một số không có dấu.
WhozCraig

6
kinh ngạc. thuật toán này đã đánh bại thuật toán băm Murmur, băm biến thể FNV và nhiều loại khác! +1
David Haim

24

Đầu tiên, bạn thường không muốn sử dụng hàm băm mật mã cho bảng băm. Một thuật toán rất nhanh theo các tiêu chuẩn mật mã nhưng vẫn rất chậm so với các tiêu chuẩn bảng băm.

Thứ hai, bạn muốn đảm bảo rằng mọi bit của đầu vào có thể / sẽ ảnh hưởng đến kết quả. Một cách dễ dàng để làm điều đó là xoay kết quả hiện tại theo một số bit, sau đó XOR mã băm hiện tại với byte hiện tại. Lặp lại cho đến khi bạn đến cuối chuỗi. Lưu ý rằng bạn thường không muốn vòng quay là bội số của kích thước byte.

Ví dụ: giả sử trường hợp phổ biến là 8 byte byte, bạn có thể xoay 5 bit:

int hash(char const *input) { 
    int result = 0x55555555;

    while (*input) { 
        result ^= *input++;
        result = rol(result, 5);
    }
}

Chỉnh sửa: Cũng lưu ý rằng 10000 khe hiếm khi là lựa chọn tốt cho kích thước bảng băm. Bạn thường muốn một trong hai điều: bạn muốn một số nguyên tố làm kích thước (bắt buộc để đảm bảo tính đúng đắn với một số loại độ phân giải băm) hoặc nếu không, lũy thừa của 2 (vì vậy việc giảm giá trị xuống phạm vi chính xác có thể được thực hiện đơn giản mặt nạ bit).


Đây không phải là c, nhưng tôi sẽ được quan tâm đến suy nghĩ của bạn để trả lời có liên quan này: stackoverflow.com/a/31440118/3681880
Suragch

1
@Suragch: Kể từ khi tôi viết bài này, khá nhiều bộ vi xử lý đã bắt đầu đưa vào một trong hai phần cứng đặc biệt để tăng tốc tính toán SHA, điều này khiến nó trở nên cạnh tranh hơn nhiều. Điều đó nói rằng, tôi nghi ngờ mã của bạn khá an toàn như bạn nghĩ - ví dụ: số dấu phẩy động IEEE có hai mẫu bit khác nhau (0 và -0) sẽ tạo ra cùng một hàm băm (chúng sẽ so sánh bằng nhau ).
Jerry Coffin,

@Jerry Coffin tôi cần thư viện nào cho hàm rol ()?
thanos.a

@ thanos.a: Tôi không biết nó đang ở trong thư viện, nhưng việc cuộn của riêng bạn chỉ mất một hoặc hai dòng mã. Dịch chuyển một đoạn sang trái, đoạn kia sang phải và hoặc chúng cùng nhau.
Jerry Coffin,

8

Wikipedia hiển thị một hàm băm chuỗi đẹp có tên Jenkins One At A Time Hash. Nó cũng trích dẫn các phiên bản cải tiến của hàm băm này.

uint32_t jenkins_one_at_a_time_hash(char *key, size_t len)
{
    uint32_t hash, i;
    for(hash = i = 0; i < len; ++i)
    {
        hash += key[i];
        hash += (hash << 10);
        hash ^= (hash >> 6);
    }
    hash += (hash << 3);
    hash ^= (hash >> 11);
    hash += (hash << 15);
    return hash;
}

8

Có một số triển khai bảng băm hiện có cho C, từ thư viện tiêu chuẩn C hcreate / hdestroy / hsearch, đến những triển khai trong APRglib , cũng cung cấp các hàm băm dựng sẵn. Tôi thực sự khuyên bạn nên sử dụng những thứ đó hơn là phát minh ra bảng băm hoặc hàm băm của riêng bạn; chúng đã được tối ưu hóa rất nhiều cho các trường hợp sử dụng phổ biến.

Tuy nhiên, nếu tập dữ liệu của bạn là tĩnh, giải pháp tốt nhất của bạn có lẽ là sử dụng một hàm băm hoàn hảo . gperf sẽ tạo một hàm băm hoàn hảo cho bạn cho một tập dữ liệu nhất định.


hsearch tìm kiếm bằng cách so sánh các chuỗi hoặc chuỗi ptr địa chỉ? Tôi nghĩ rằng nó chỉ là kiểm tra địa chỉ ptr? Tôi đã thử sử dụng các con trỏ khác nhau nhưng cùng một giá trị chuỗi. hsearch không thành công khi không tìm thấy phần tử
mk ..

3

djb2 ​​có 317 lần va chạm cho từ điển tiếng Anh 466k này trong khi MurmurHash không có lần nào cho 64 bit băm và 21 lần cho 32 bit (dự kiến ​​sẽ có khoảng 25 cho 466k băm 32 bit ngẫu nhiên). Khuyến nghị của tôi là sử dụng MurmurHash nếu có, nó rất nhanh, vì nó mất vài byte cùng một lúc. Nhưng nếu bạn cần một hàm băm đơn giản và ngắn gọn để sao chép và dán vào dự án của mình, tôi khuyên bạn nên sử dụng phiên bản một byte từng lần thì thầm:

uint32_t inline MurmurOAAT32 ( const char * key)
{
  uint32_t h(3323198485ul);
  for (;*key;++key) {
    h ^= *key;
    h *= 0x5bd1e995;
    h ^= h >> 15;
  }
  return h;
}

uint64_t inline MurmurOAAT64 ( const char * key)
{
  uint64_t h(525201411107845655ull);
  for (;*key;++key) {
    h ^= *key;
    h *= 0x5bd1e9955bd1e995;
    h ^= h >> 47;
  }
  return h;
}

Kích thước tối ưu của bảng băm - trong ngắn hạn - càng lớn càng tốt trong khi vẫn vừa với bộ nhớ. Bởi vì chúng ta thường không biết hoặc không muốn tra cứu dung lượng bộ nhớ có sẵn và thậm chí nó có thể thay đổi, kích thước bảng băm tối ưu là khoảng gấp đôi số phần tử dự kiến ​​sẽ được lưu trữ trong bảng. Phân bổ nhiều hơn thế sẽ làm cho bảng băm của bạn nhanh hơn nhưng với lợi nhuận giảm đi nhanh chóng, làm cho bảng băm của bạn nhỏ hơn sẽ làm cho bảng băm của bạn chậm hơn theo cấp số nhân. Điều này là do có sự cân bằng phi tuyến tính giữa độ phức tạp không gian và thời gian cho các bảng băm, với hệ số tải tối ưu là 2-sqrt (2) = 0,58 ... rõ ràng.


2

Đầu tiên, 40 va chạm cho 130 từ được băm thành 0..99 có xấu không? Bạn không thể mong đợi quá trình băm hoàn hảo nếu bạn không thực hiện các bước cụ thể để nó xảy ra. Một hàm băm thông thường sẽ không có ít va chạm hơn một bộ tạo ngẫu nhiên hầu hết thời gian.

Một hàm băm có danh tiếng tốt là MurmurHash3 .

Cuối cùng, liên quan đến kích thước của bảng băm, nó thực sự phụ thuộc vào loại bảng băm mà bạn nghĩ đến, đặc biệt, cho dù các nhóm có thể mở rộng hay một khe. Nếu nhóm có thể mở rộng, một lần nữa có một sự lựa chọn: bạn chọn chiều dài nhóm trung bình cho các ràng buộc về bộ nhớ / tốc độ mà bạn có.


1
Số lần va chạm băm dự kiến ​​là n - m * (1 - ((m-1)/m)^n) = 57.075.... 40 va chạm tốt hơn những gì có thể mong đợi một cách tình cờ (46 đến 70 với điểm p là 0,999). Hàm băm được đề cập đồng nhất hơn là nếu nó là ngẫu nhiên hoặc chúng ta đang chứng kiến ​​một sự kiện rất hiếm.
Wolfgang Brehm

2

Mặc dù djb2, như được trình bày trên stackoverflow bởi cnicutar , gần như chắc chắn là tốt hơn, tôi nghĩ cũng đáng để hiển thị các băm K&R :

1) Rõ ràng là một thuật toán băm khủng khiếp , như được trình bày trong K&R ấn bản đầu tiên ( nguồn )

unsigned long hash(unsigned char *str)
{
    unsigned int hash = 0;
    int c;

    while (c = *str++)
        hash += c;

    return hash;
}

2) Có lẽ là một thuật toán băm khá tốt, như được trình bày trong K&R phiên bản 2 (được tôi xác minh trên trang 144 của cuốn sách); NB: hãy chắc chắn xóa % HASHSIZEkhỏi câu lệnh trả về nếu bạn định thực hiện điều chỉnh kích thước mô-đun-thành-độ dài mảng của bạn bên ngoài thuật toán băm. Ngoài ra, tôi khuyên bạn nên tạo kiểu trả về và "hashval" unsigned longthay vì kiểu đơn giản unsigned(int).

unsigned hash(char *s)
{
    unsigned hashval;

    for (hashval = 0; *s != '\0'; s++)
        hashval = *s + 31*hashval;
    return hashval % HASHSIZE;
}

Lưu ý rằng rõ ràng từ hai thuật toán rằng một lý do khiến hàm băm phiên bản đầu tiên quá khủng khiếp là bởi vì nó KHÔNG tính đến thứ tự ký tự chuỗi , do đó hash("ab")sẽ trả về cùng giá trị hash("ba"). Tuy nhiên, điều này không đúng với hàm băm phiên bản thứ 2, sẽ (tốt hơn nhiều!) Trả về hai giá trị khác nhau cho các chuỗi đó.

Các hàm băm GCC C ++ 11 được sử dụng cho unordered_map(mẫu bảng băm) và unordered_set(mẫu bộ băm) dường như như sau.

Mã:

// Implementation of Murmur hash for 32-bit size_t.
size_t _Hash_bytes(const void* ptr, size_t len, size_t seed)
{
  const size_t m = 0x5bd1e995;
  size_t hash = seed ^ len;
  const char* buf = static_cast<const char*>(ptr);

  // Mix 4 bytes at a time into the hash.
  while (len >= 4)
  {
    size_t k = unaligned_load(buf);
    k *= m;
    k ^= k >> 24;
    k *= m;
    hash *= m;
    hash ^= k;
    buf += 4;
    len -= 4;
  }

  // Handle the last few bytes of the input array.
  switch (len)
  {
    case 3:
      hash ^= static_cast<unsigned char>(buf[2]) << 16;
      [[gnu::fallthrough]];
    case 2:
      hash ^= static_cast<unsigned char>(buf[1]) << 8;
      [[gnu::fallthrough]];
    case 1:
      hash ^= static_cast<unsigned char>(buf[0]);
      hash *= m;
  };

  // Do a few final mixes of the hash.
  hash ^= hash >> 13;
  hash *= m;
  hash ^= hash >> 15;
  return hash;
}

2

Tôi đã thử các hàm băm này và nhận được kết quả như sau. Tôi có khoảng 960 ^ 3 mục nhập, mỗi mục dài 64 byte, 64 ký tự theo thứ tự khác nhau, giá trị băm 32 bit. Mã từ đây .

Hash function    | collision rate | how many minutes to finish
==============================================================
MurmurHash3      |           6.?% |                      4m15s
Jenkins One..    |           6.1% |                      6m54s   
Bob, 1st in link |          6.16% |                      5m34s
SuperFastHash    |            10% |                      4m58s
bernstein        |            20% |       14s only finish 1/20
one_at_a_time    |          6.16% |                       7m5s
crc              |          6.16% |                      7m56s

Một điều kỳ lạ là hầu như tất cả các hàm băm có tỷ lệ xung đột 6% cho dữ liệu của tôi.


Mặc dù liên kết này có thể trả lời câu hỏi, nhưng tốt hơn hết bạn nên đưa các phần thiết yếu của câu trả lời vào đây và cung cấp liên kết để tham khảo. Các câu trả lời chỉ có liên kết có thể trở nên không hợp lệ nếu trang được liên kết thay đổi.
thewaywewere

Đã ủng hộ cho một bảng tốt, đăng mã nguồn cho mỗi hàm băm đó trong câu trả lời của bạn cũng là điều cần thiết. Nếu không, các liên kết có thể bị đứt và chúng ta không gặp may.
Gabriel Staples

Số lần va chạm dự kiến ​​phải là 9.112499989700318E + 7 hoặc 0.103 * 960³ nếu các băm thực sự là ngẫu nhiên, vì vậy tôi sẽ không ngạc nhiên nếu chúng đều ở xung quanh giá trị đó, nhưng 0,0616 * 960³ có vẻ hơi lệch, gần như là các băm được phân phối đồng đều hơn so với những gì có thể mong đợi một cách tình cờ và ở độ dài 64 byte, giới hạn này chắc chắn nên được tiếp cận. Bạn có thể chia sẻ bộ chuỗi mà bạn đã băm để tôi cố gắng tái tạo nó được không?
Wolfgang Brehm

0

Một điều tôi đã sử dụng với kết quả tốt là như sau (Tôi không biết liệu nó đã được đề cập chưa vì tôi không thể nhớ tên của nó).

Bạn tính toán trước một bảng T với một số ngẫu nhiên cho mỗi ký tự trong bảng chữ cái của khóa [0,255]. Bạn băm khóa 'k0 k1 k2 ... kN' bằng cách lấy T [k0] xor T [k1] xor ... xor T [kN]. Bạn có thể dễ dàng chỉ ra rằng điều này cũng ngẫu nhiên như trình tạo số ngẫu nhiên của bạn và về mặt tính toán của nó rất khả thi và nếu bạn thực sự gặp phải một trường hợp rất xấu với nhiều va chạm, bạn chỉ có thể lặp lại toàn bộ bằng cách sử dụng một loạt số ngẫu nhiên mới.


Nếu tôi không nhầm thì điều này cũng gặp phải vấn đề giống như K&R 1st trong câu trả lời của Gabriel; tức là "ab" và "ba" sẽ băm thành cùng một giá trị.
Johann Oskarsson
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.