ฟังก์ชันแฮชสำหรับสตริง


124

ฉันกำลังทำงานกับตารางแฮชในภาษาซีและฉันกำลังทดสอบฟังก์ชันแฮชสำหรับสตริง

ฟังก์ชั่นแรกที่ฉันได้ลองคือเพิ่มรหัส ascii และใช้ modulo (% 100) แต่ฉันได้ผลลัพธ์ที่ไม่ดีกับการทดสอบข้อมูลครั้งแรก: 40 การชนกันสำหรับ 130 คำ

ข้อมูลอินพุตสุดท้ายจะมี 8,000 คำ (เป็นพจนานุกรมที่เก็บไว้ในไฟล์) ตารางแฮชถูกประกาศเป็นตาราง int [10000] และมีตำแหน่งของคำในไฟล์ txt

คำถามแรกคืออัลกอริทึมใดที่ดีที่สุดสำหรับการแฮชสตริง และจะกำหนดขนาดของตารางแฮชได้อย่างไร?

ขอบคุณล่วงหน้า !

:-)


11
ถ้าตารางแฮชของคุณมี 10K รายการทำไมคุณถึงใช้โมดูโล 100? การได้รับการชนกัน 40 ครั้งจาก 130 คำไม่น่าแปลกใจที่มีโมดูลัสเล็ก ๆ
Carey Gregory

13
ดูburtleburtle.net/bob/hash/evahash.htmlและpartow.net/programming/hashfunctionsซึ่งเป็นแหล่งข้อมูลเกี่ยวกับการแฮชต่างๆ (ตั้งแต่แบบทั่วไปไปจนถึงสตริงไปจนถึงการเข้ารหัสลับ)

4
เพื่อชี้แจง @CareyGregory: คุณตระหนักดีว่าตามความจริงทางคณิตศาสตร์พื้นฐาน 130 รายการใน 100 ถัง (เช่น mod 100) จะต้องสร้างการชนกัน 30 ครั้ง (โดยที่การชนกันจะนับเป็นทุกครั้งที่มีการใส่รายการที่สองสาม ฯลฯ ถัง) ถูกต้อง? คุณสูงกว่านั้นเพียงเล็กน้อย
derobert

4
@lilawood: ตกลงนั่นคือสิ่งที่ฉันคิด แต่เพื่อเป็นการทดสอบที่ดีกว่าคุณควรใช้ 80 คำกับตารางแฮช 100 รายการ นั่นจะทำให้คุณมีสัดส่วนเดียวกันกับข้อมูลสดของคุณและจะไม่บังคับให้ชนกัน
Carey Gregory

คำตอบ:


186

ฉันมีผลลัพธ์ที่ดีกับdjb2โดย Dan 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
หน้าที่เชื่อมโยงในคำตอบนั้นน่าสนใจมาก
Adrien Plisson

2
โปรแกรมทำงานออกจาก while loop ได้อย่างไร ?? = S
Daniel N.

1
@ danfly09 เมื่อ c เป็นศูนย์ ค่าเทียบเท่าของ while (c = * str ++) จะเป็น (0! = (c = * str ++))
rxantos

5
@ Josepas ฟังก์ชันแฮชควรส่งคืนsize_tค่าที่ไม่ได้ลงชื่อหรือค่าอื่น ๆ (เช่นค่าที่ไม่ได้ลงนามแบบยาวในรหัสนี้) โทรเป็นผู้รับผิดชอบสำหรับการโมดูโลผลเพื่อให้พอดีกับมันตารางแฮช ผู้โทรควบคุมช่องตารางที่แฮช ไม่ใช่ฟังก์ชัน เพียงส่งคืนหมายเลขที่ไม่ได้ลงชื่อ
WhozCraig

6
น่าอัศจรรย์ อัลกอริทึมนี้เอาชนะนรกจาก Murmur hash แฮชตัวแปร FNV และอื่น ๆ อีกมากมาย! +1
David Haim

24

อันดับแรกโดยทั่วไปคุณไม่ต้องการใช้แฮชเข้ารหัสสำหรับตารางแฮช อัลกอริทึมที่เร็วมากตามมาตรฐานการเข้ารหัสยังคงช้าอย่างมากตามมาตรฐานตารางแฮช

ประการที่สองคุณต้องการให้แน่ใจว่าทุกบิตของอินพุตสามารถ / จะส่งผลต่อผลลัพธ์ วิธีง่ายๆวิธีหนึ่งในการทำเช่นนี้คือการหมุนผลลัพธ์ปัจจุบันตามจำนวนบิตจากนั้น XOR รหัสแฮชปัจจุบันด้วยไบต์ปัจจุบัน ทำซ้ำจนกว่าจะถึงจุดสิ้นสุดของสตริง โปรดทราบว่าโดยทั่วไปคุณไม่ต้องการให้การหมุนเป็นจำนวนเท่ากันของขนาดไบต์

ตัวอย่างเช่นสมมติว่ากรณีทั่วไปคือ 8 บิตไบต์คุณอาจหมุนได้ 5 บิต:

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

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

แก้ไข: โปรดทราบว่า 10,000 สล็อตไม่ค่อยเป็นตัวเลือกที่ดีสำหรับขนาดตารางแฮช โดยปกติคุณต้องการหนึ่งในสองสิ่ง: คุณต้องการจำนวนเฉพาะเป็นขนาด (จำเป็นเพื่อให้แน่ใจว่ามีความถูกต้องกับความละเอียดแฮชบางประเภท) หรือไม่ก็กำลัง 2 (ดังนั้นการลดค่าลงในช่วงที่ถูกต้องสามารถทำได้ด้วยวิธีง่ายๆ บิตหน้ากาก)


นี่ไม่ใช่ c แต่ฉันสนใจในความคิดของคุณสำหรับคำตอบที่เกี่ยวข้องนี้: stackoverflow.com/a/31440118/3681880
Suragch

1
@Suragch: ตั้งแต่ฉันเขียนสิ่งนี้โปรเซสเซอร์ไม่กี่ตัวเริ่มรวมฮาร์ดแวร์พิเศษอย่างใดอย่างหนึ่งเพื่อเร่งการคำนวณ SHA ซึ่งทำให้สามารถแข่งขันได้มากขึ้น ที่กล่าวว่าฉันสงสัยว่ารหัสของคุณค่อนข้างปลอดภัยอย่างที่คุณคิดตัวอย่างเช่นตัวเลขจุดลอยตัวของ IEEE มีรูปแบบบิตที่แตกต่างกันสองแบบ (0 และ -0) ซึ่งควรสร้างแฮชเดียวกัน (จะเปรียบเทียบว่าเท่ากัน )
Jerry Coffin

@Jerry Coffin ฉันต้องการไลบรารีใดสำหรับฟังก์ชัน Rol ()
thanos

@ thanos.a: ฉันไม่รู้ว่ามันอยู่ในไลบรารี แต่การหมุนของคุณใช้โค้ดเพียงบรรทัดหรือสองบรรทัด เลื่อนชิ้นหนึ่งไปทางซ้ายอีกชิ้นหนึ่งไปทางขวาและหรือรวมกัน
Jerry Coffin

8

Wikipedia แสดงฟังก์ชันแฮชสตริงที่ดีที่เรียกว่า Jenkins One At A Time Hash นอกจากนี้ยังเสนอราคาแฮชเวอร์ชันที่ปรับปรุงแล้ว

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 ตั้งแต่ไลบรารีมาตรฐาน C hcreate / hdestroy / hsearch ไปจนถึงในAPRและglibซึ่งมีฟังก์ชันแฮชที่สร้างไว้ล่วงหน้า ฉันขอแนะนำให้ใช้สิ่งเหล่านี้แทนการประดิษฐ์ฟังก์ชันแฮชหรือแฮชของคุณเอง ได้รับการปรับให้เหมาะสมอย่างมากสำหรับกรณีการใช้งานทั่วไป

ถ้าชุดของคุณเป็นแบบคงที่ แต่ทางออกที่ดีที่สุดของคุณน่าจะใช้กัญชาที่สมบูรณ์แบบ gperfจะสร้างแฮชที่สมบูรณ์แบบสำหรับคุณสำหรับชุดข้อมูลที่กำหนด


hsearch ค้นหาโดยการเปรียบเทียบสตริงหรือที่อยู่ ptr สตริง? ฉันคิดว่ามันเป็นเพียงการตรวจสอบที่อยู่ ptr? ฉันลองใช้พอยน์เตอร์ที่แตกต่างกัน แต่มีค่าสตริงเดียวกัน hsearch ล้มเหลวโดยระบุว่าไม่พบองค์ประกอบ
mk ..

3

djb2 ​​มีการชนกัน 317 ครั้งสำหรับพจนานุกรมภาษาอังกฤษ 466k นี้ในขณะที่ MurmurHash ไม่มีแฮช 64 บิตและ 21 สำหรับแฮช 32 บิต (คาดว่าจะมีประมาณ 25 ครั้งสำหรับแฮชสุ่ม 32 บิต 466k) คำแนะนำของฉันใช้MurmurHashถ้ามีมันเร็วมากเพราะใช้ครั้งละหลายไบต์ แต่ถ้าคุณต้องการฟังก์ชันแฮชที่เรียบง่ายและสั้นเพื่อคัดลอกและวางลงในโปรเจ็กต์ของคุณฉันขอแนะนำให้ใช้พึมพำแบบทีละไบต์ต่อครั้ง:

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

ขนาดที่เหมาะสมที่สุดของตารางแฮชคือ - สั้น - ใหญ่ที่สุดเท่าที่จะทำได้ในขณะที่ยังพอดีกับหน่วยความจำ เนื่องจากโดยปกติเราไม่ทราบหรือต้องการค้นหาจำนวนหน่วยความจำที่เรามีอยู่และอาจมีการเปลี่ยนแปลงขนาดตารางแฮชที่เหมาะสมที่สุดคือประมาณ 2 เท่าของจำนวนองค์ประกอบที่คาดว่าจะจัดเก็บในตาราง การจัดสรรมากกว่านั้นจะทำให้ตารางแฮชของคุณเร็วขึ้น แต่ผลตอบแทนที่ลดลงอย่างรวดเร็วการทำให้ตารางแฮชของคุณมีขนาดเล็กกว่านั้นจะทำให้ตารางแฮชของคุณช้าลงอย่างมาก เนื่องจากมีการแลกเปลี่ยนที่ไม่ใช่เชิงเส้นระหว่างความซับซ้อนของพื้นที่และเวลาสำหรับตารางแฮชโดยมีปัจจัยการโหลดที่เหมาะสมที่สุดคือ 2-sqrt (2) = 0.58 ...


2

ประการแรกการชนกัน 40 ครั้งสำหรับ 130 คำที่แฮชเป็น 0..99 ไม่ดีหรือไม่? คุณไม่สามารถคาดหวังการแฮชที่สมบูรณ์แบบได้หากคุณไม่ได้ทำตามขั้นตอนเฉพาะเพื่อให้มันเกิดขึ้น ฟังก์ชันแฮชธรรมดาจะไม่มีการชนกันน้อยกว่าเครื่องกำเนิดไฟฟ้าแบบสุ่มเกือบตลอดเวลา

ฟังก์ชั่นแฮชที่มีชื่อเสียงที่ดีคือMurmurHash3

สุดท้ายเกี่ยวกับขนาดของตารางแฮชนั้นขึ้นอยู่กับประเภทของตารางแฮชที่คุณมีอยู่โดยเฉพาะอย่างยิ่งไม่ว่าที่เก็บข้อมูลจะขยายได้หรือช่องเดียว หากที่เก็บข้อมูลสามารถขยายได้มีทางเลือกอีกครั้ง: คุณเลือกความยาวที่เก็บข้อมูลเฉลี่ยสำหรับข้อ จำกัด ด้านหน่วยความจำ / ความเร็วที่คุณมี


1
n - m * (1 - ((m-1)/m)^n) = 57.075...จำนวนที่คาดหวังของการชนกัญชา การชนกัน 40 ครั้งดีกว่าที่คาดไว้โดยบังเอิญ (46 ถึง 70 ที่ p-score 0.999) ฟังก์ชันแฮชที่เป็นปัญหานั้นมีความสม่ำเสมอมากกว่าหากเป็นแบบสุ่มหรือเรากำลังพบเห็นเหตุการณ์ที่หายากมาก
Wolfgang Brehm

2

แม้ว่าdjb2ตามที่นำเสนอใน stackoverflow โดย cnicutarนั้นเกือบจะดีกว่า แต่ฉันคิดว่ามันก็คุ้มค่าที่จะแสดงแฮชK&Rด้วย:

1) เห็นได้ชัดว่าเป็นอัลกอริธึมแฮชที่แย่มากดังที่นำเสนอใน K&R ฉบับที่ 1 (ที่มา )

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

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

    return hash;
}

2) อาจเป็นอัลกอริธึมแฮชที่ดีดังที่นำเสนอใน K&R เวอร์ชัน 2 (ตรวจสอบโดยฉันในหน้า 144 ของหนังสือ); หมายเหตุ: อย่าลืมลบออก% HASHSIZEจากคำสั่ง return หากคุณวางแผนที่จะทำ modulus sizing-to-your-length นอกอัลกอริทึมแฮช นอกจากนี้ฉันขอแนะนำให้คุณสร้าง return และประเภท "hashval" unsigned longแทนการใช้ simple unsigned(int)

unsigned hash(char *s)
{
    unsigned hashval;

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

หมายเหตุว่ามันเป็นที่ชัดเจนจากทั้งสองขั้นตอนวิธีการที่หนึ่งในเหตุผลที่กัญชา 1 ฉบับเป็นสาหัสเพื่อเป็นเพราะมันไม่คำนึงถึงตัวอักษรที่สั่งซื้อดังนั้นจึงจะกลับค่าเช่นเดียวกับhash("ab") hash("ba")อย่างไรก็ตามสิ่งนี้ไม่เป็นเช่นนั้นกับแฮชรุ่นที่ 2 ซึ่งจะ (ดีกว่ามาก!) ส่งคืนค่าที่แตกต่างกันสองค่าสำหรับสตริงเหล่านั้น

ฟังก์ชันแฮช GCC C ++ 11 ที่ใช้สำหรับunordered_map(เทมเพลตตารางแฮช) และunordered_set(เทมเพลตชุดแฮช) จะเป็นดังนี้

  • นี่เป็นคำตอบบางส่วนสำหรับคำถามที่ว่าฟังก์ชันแฮช GCC C ++ 11 ที่ใช้คืออะไรโดยระบุว่า GCC ใช้ "MurmurHashUnaligned2" โดย Austin Appleby ( http://murmurhash.googlepages.com/ )
  • ในไฟล์ "gcc / libstdc ++ - v3 / libsupc ++ / hash_bytes.cc" ที่นี่ ( https://github.com/gcc-mirror/gcc/blob/master/libstdc++-v3/libsupc++/hash_bytes.cc ) ฉันพบ การใช้งาน นี่คือค่าส่งคืน "32-bit size_t" ตัวอย่างเช่น (ดึง 11 ส.ค. 2017):

รหัส:

// 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

ฉันได้ลองใช้ฟังก์ชันแฮชเหล่านี้แล้วและได้ผลลัพธ์ดังต่อไปนี้ ฉันมีรายการประมาณ 960 ^ 3 แต่ละรายการยาว 64 ไบต์ 64 ตัวอักษรในลำดับที่แตกต่างกันค่าแฮช 32 บิต รหัสจากที่นี่

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

สิ่งหนึ่งที่แปลกคือฟังก์ชันแฮชเกือบทั้งหมดมีอัตราการชนกัน 6% สำหรับข้อมูลของฉัน


แม้ว่าลิงก์นี้อาจตอบคำถามได้ แต่ควรรวมส่วนสำคัญของคำตอบไว้ที่นี่และระบุลิงก์เพื่อการอ้างอิง คำตอบแบบลิงก์เท่านั้นอาจไม่ถูกต้องหากหน้าที่เชื่อมโยงเปลี่ยนไป
thewaywewere

โหวตให้ตารางที่ดีการโพสต์ซอร์สโค้ดสำหรับแฮชแต่ละอันในคำตอบของคุณก็เป็นสิ่งสำคัญเช่นกัน มิฉะนั้นลิงก์อาจเสียหายและเราโชคไม่ดี
Gabriel Staples

จำนวนการชนที่คาดไว้ควรเป็น 9.112499989700318E + 7 หรือ 0.103 * 960³หากแฮชเป็นแบบสุ่มจริงๆดังนั้นฉันจะไม่แปลกใจเลยถ้าพวกมันอยู่รอบ ๆ ค่านั้น แต่ 0.0616 * 960³ดูเหมือนจะไม่ค่อยดีนักเกือบจะเหมือนกับว่า แฮชมีการกระจายอย่างเท่าเทียมกันมากกว่าที่คาดไว้โดยบังเอิญและที่ความยาว 64 ไบต์ขีด จำกัด นี้ควรเข้าหาอย่างแน่นอน คุณสามารถแชร์ชุดสตริงที่แฮชเพื่อให้ฉันลองสร้างซ้ำได้ไหม
Wolfgang Brehm

0

สิ่งหนึ่งที่ฉันใช้แล้วได้ผลดีมีดังต่อไปนี้ (ฉันไม่รู้ว่ามีการกล่าวถึงไปแล้วหรือไม่เพราะฉันจำชื่อไม่ได้)

คุณคำนวณตาราง T ล่วงหน้าด้วยตัวเลขสุ่มสำหรับแต่ละอักขระในตัวอักษรของคีย์ของคุณ [0,255] คุณแฮชคีย์ของคุณ 'k0 k1 k2 ... kN' โดยรับ T [k0] xor T [k1] xor ... xor T [kN] คุณสามารถแสดงให้เห็นได้อย่างง่ายดายว่านี่เป็นแบบสุ่มเหมือนกับตัวสร้างตัวเลขสุ่มของคุณและมีความเป็นไปได้ในการคำนวณมากและหากคุณพบเจอกับอินสแตนซ์ที่แย่มากโดยมีการชนกันจำนวนมากคุณก็สามารถทำซ้ำทั้งหมดโดยใช้ตัวเลขสุ่มชุดใหม่


ถ้าฉันจำไม่ผิดนี่เป็นปัญหาเดียวกันกับ K&R 1st ในคำตอบของ Gabriel เช่น "ab" และ "ba" จะแฮชเป็นค่าเดียวกัน
Johann Oskarsson
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.