ตารางแฮชทำงานอย่างไร


494

ฉันกำลังมองหาคำอธิบายวิธีการใช้งานตารางแฮช - เป็นภาษาอังกฤษธรรมดาสำหรับคนธรรมดาอย่างฉัน!

ตัวอย่างเช่นฉันรู้ว่าต้องใช้กุญแจคำนวณแฮช (ฉันกำลังหาคำอธิบายว่าทำอย่างไร) จากนั้นก็ทำการโมดูโลบางอย่างเพื่อหาว่ามันอยู่ที่ไหนในอาเรย์ที่เก็บค่าไว้ แต่นั่นคือที่ความรู้ของฉันหยุด .

ทุกคนสามารถอธิบายกระบวนการได้หรือไม่

แก้ไข:ฉันไม่ได้ถามเฉพาะเกี่ยวกับวิธีคำนวณรหัสแฮช แต่ภาพรวมทั่วไปของวิธีการใช้ตารางแฮช


4
เมื่อเร็ว ๆ นี้ฉันได้เขียนบทความนี้ ( en.algoritmy.net/article/50101/Hash-table ) อธิบายหลายวิธีวิธีจัดเก็บและค้นหาข้อมูลด้วยสำเนียงบนตารางแฮชและกลยุทธ์ของพวกเขา )
malejpavouk

1
คุณอาจคิดว่าตารางแฮชเป็นอาเรย์เวอร์ชันขยายซึ่งไม่ได้ จำกัด อยู่แค่คีย์จำนวนเต็มต่อเนื่องเท่านั้น
user253751

1
นี่คืออีกอันหนึ่ง: intelligentjava.wordpress.com/2016/10/19/…
nesvarbu

คำตอบ:


913

นี่คือคำอธิบายในแง่ของคนธรรมดา

สมมติว่าคุณต้องการเติมห้องสมุดด้วยหนังสือไม่ใช่แค่เก็บไว้ในห้องสมุด แต่คุณต้องการที่จะหามันได้ง่ายเมื่อคุณต้องการ

ดังนั้นคุณตัดสินใจว่าถ้าคนที่ต้องการอ่านหนังสือรู้ชื่อหนังสือและชื่อที่แน่นอนในการบูตนั่นคือทั้งหมดที่ควรจะเป็น ด้วยชื่อหนังสือบุคคลที่ได้รับความช่วยเหลือจากบรรณารักษ์จะสามารถหาหนังสือเล่มนี้ได้ง่ายและรวดเร็ว

ดังนั้นคุณจะทำอย่างไร เห็นได้ชัดว่าคุณสามารถเก็บรายการที่คุณใส่แต่ละเล่มไว้ได้บ้าง แต่แล้วคุณมีปัญหาเช่นเดียวกับการค้นหาห้องสมุดคุณต้องค้นหารายการ ได้รับแล้วรายการจะเล็กลงและค้นหาได้ง่ายขึ้น แต่คุณก็ยังไม่ต้องการค้นหาตามลำดับจากปลายด้านหนึ่งของห้องสมุด (หรือรายการ) ไปยังอีกด้านหนึ่ง

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

แต่จะทำได้อย่างไร ด้วยความสุขุมเล็กน้อยเมื่อคุณเติมห้องสมุดและทำงานมากมายเมื่อคุณเติมห้องสมุด

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

ความสวยงามของโปรแกรมนี้คือในภายหลังเมื่อบุคคลกลับมาเพื่ออ่านหนังสือคุณจะป้อนชื่อหนังสือผ่านโปรแกรมอีกครั้งและรับหมายเลขหิ้งเดิมและหมายเลขสล็อตที่คุณได้รับมาคืนและนี่คือ ที่ตั้งของหนังสือ

โปรแกรมดังที่คนอื่น ๆ ได้กล่าวถึงแล้วเรียกว่าอัลกอริธึมการแฮชหรือการคำนวณแฮชและมักจะทำงานโดยนำข้อมูลที่ป้อนเข้ามา (ชื่อหนังสือในกรณีนี้) และคำนวณตัวเลขจากมัน

เพื่อความง่ายสมมติว่ามันแค่แปลงตัวอักษรและสัญลักษณ์แต่ละตัวให้เป็นตัวเลขแล้วนำมารวมกันทั้งหมด ในความเป็นจริงมันซับซ้อนกว่านั้นมาก แต่เราจะปล่อยไว้ก่อน

ความสวยงามของอัลกอริทึมดังกล่าวคือถ้าคุณป้อนอินพุตเดียวกันเข้าไปซ้ำแล้วซ้ำอีกมันจะคายหมายเลขเดิมทุกครั้ง

ตกลงดังนั้นโดยทั่วไปแล้วเป็นวิธีการทำงานของตารางแฮช

สิ่งทางเทคนิคดังต่อไปนี้

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

ดังนั้นสิ่งที่เราจะทำ? เราใช้สิ่งที่เรียกว่าการคำนวณโมดูลัสซึ่งโดยทั่วไปบอกว่าถ้าคุณนับจำนวนที่คุณต้องการ (เช่นหนึ่งพันล้านหมายเลข) แต่ต้องการอยู่ในช่วงที่เล็กกว่ามากทุกครั้งที่คุณถึงขีด จำกัด ของช่วงที่เล็กกว่านั้น 0 แต่คุณต้องติดตามว่าคุณมาไกลแค่ไหนในลำดับที่ยิ่งใหญ่

สมมติว่าเอาต์พุตของอัลกอริทึมแฮชอยู่ในช่วง 0 ถึง 20 และคุณจะได้รับค่า 17 จากหัวเรื่องเฉพาะ หากขนาดของห้องสมุดเป็นเพียงหนังสือ 7 เล่มคุณนับ 1, 2, 3, 4, 5, 6 และเมื่อคุณไปถึง 7 คุณจะเริ่มต้นที่ 0 เนื่องจากเราต้องนับ 17 ครั้งเรามี 1 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3 และตัวเลขสุดท้ายคือ 3

แน่นอนว่าการคำนวณโมดูลัสนั้นไม่ได้ทำอย่างนั้นมันทำกับการหารและส่วนที่เหลือ ส่วนที่เหลือของการหาร 17 ด้วย 7 คือ 3 (7 ไป 2 ครั้งเป็น 17 ที่ 14 และความแตกต่างระหว่าง 17 และ 14 คือ 3)

ดังนั้นคุณวางหนังสือในช่องหมายเลข 3

สิ่งนี้นำไปสู่ปัญหาต่อไป การชนกัน เนื่องจากอัลกอริทึมไม่มีวิธีการเว้นวรรคหนังสือเพื่อให้พวกเขาเติมห้องสมุดอย่างแน่นอน (หรือตารางแฮชถ้าคุณจะ) มันจะสิ้นสุดการคำนวณจำนวนที่เคยใช้มาก่อน ในแง่ของห้องสมุดเมื่อคุณไปถึงชั้นวางและหมายเลขสล็อตที่คุณต้องการใส่หนังสือเข้าไป

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

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

ฉันหวังว่าคำอธิบายนี้จะลงสู่พื้นดินมากกว่าถังและฟังก์ชั่น :)


ขอบคุณสำหรับคำอธิบายที่ยอดเยี่ยม คุณรู้หรือไม่ว่าฉันสามารถหารายละเอียดทางเทคนิคเพิ่มเติมเกี่ยวกับวิธีการนำไปใช้ในกรอบ 4.x .Net ได้อย่างไร
Johnny_D

ไม่มันเป็นเพียงตัวเลข คุณจะกำหนดหมายเลขแต่ละชั้นวางและช่องเริ่มต้นที่ 0 หรือ 1 และเพิ่มขึ้น 1 สำหรับแต่ละช่องบนชั้นวางนั้นจากนั้นให้นับหมายเลขบนชั้นถัดไป
Lasse V. Karlsen

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

1
@KyleDelaney: ไม่มีการแฮชแบบปิด (ซึ่งการชนถูกจัดการโดยการค้นหาที่ฝากข้อมูลสำรองซึ่งหมายถึงการใช้หน่วยความจำได้รับการแก้ไข แต่คุณใช้เวลาในการค้นหามากกว่าที่เก็บข้อมูล) สำหรับการแฮ็กแบบเปิดที่รู้จักกันในกรณีทางพยาธิวิทยา (ฟังก์ชั่นแฮชที่น่ากลัวหรืออินพุตที่สร้างขึ้นมาเพื่อชนโดยฝ่ายตรงข้าม / แฮ็กเกอร์) คุณสามารถจบลงด้วยที่ว่างเปล่าแฮ็คส่วนใหญ่ แต่การใช้หน่วยความจำทั้งหมด การจัดทำดัชนีลงในข้อมูลเป็นประโยชน์
Tony Delroy

3
@ KyleDelaney: ต้องการสิ่ง "@Tony" เพื่อรับการแจ้งเตือนความคิดเห็นของคุณ ดูเหมือนว่าคุณกำลังสงสัยเกี่ยวกับการผูกมัด: บอกว่าเรามีสามโหนดคุ้มค่าและตารางแฮชที่มีสามถังA{ptrA, valueA}, B{ptrB, valueB}, C{ptrC, valueC} [ptr1, ptr2, ptr3]ไม่ว่าจะมีการชนกันของข้อมูลหรือไม่เมื่อใส่เข้าไปการใช้งานหน่วยความจำจะได้รับการแก้ไข คุณอาจไม่มีการชนกัน: A{NULL, valueA} B{NULL, valueB} C{NULL, valueC}และ[&A, &B, &C], หรือการชนทั้งหมดA{&B, valueA} B{&C, valueB}, C{NULL, valueC}และ[NULL, &A, NULL]: ถังขยะเป็นโมฆะ "สูญเปล่า" หรือไม่? ครับไม่ได้ หน่วยความจำทั้งหมดที่ใช้เดียวกัน
Tony Delroy

104

การใช้งานและ Lingo:

  1. ตารางแฮชใช้เพื่อจัดเก็บและดึงข้อมูล (หรือบันทึก) อย่างรวดเร็ว
  2. บันทึกจะถูกเก็บไว้ในที่เก็บข้อมูลโดยใช้ปุ่มแฮช
  3. แป้นแฮชจะคำนวณโดยใช้อัลกอริทึมการแฮชกับค่าที่เลือก ( ค่าคีย์ ) ที่มีอยู่ในเรคคอร์ด ค่าที่เลือกนี้ต้องเป็นค่าทั่วไปสำหรับระเบียนทั้งหมด
  4. ที่เก็บข้อมูลแต่ละชุดสามารถมีหลายระเบียนที่จัดระเบียบตามลำดับเฉพาะ

ตัวอย่างโลกแห่งความจริง:

Hash & Co.ก่อตั้งขึ้นในปี 1803 และขาดเทคโนโลยีคอมพิวเตอร์ใด ๆ มีตู้เก็บเอกสารทั้งหมด 300 ตู้เพื่อเก็บข้อมูลรายละเอียด (บันทึก) สำหรับลูกค้าประมาณ 30,000 ราย แต่ละโฟลเดอร์ไฟล์ถูกระบุอย่างชัดเจนพร้อมกับหมายเลขไคลเอนต์ซึ่งเป็นหมายเลขเฉพาะตั้งแต่ 0 ถึง 29,999

เสมียนยื่นของเวลานั้นต้องดึงข้อมูลและจัดเก็บบันทึกลูกค้าสำหรับพนักงานที่ทำงานได้อย่างรวดเร็ว พนักงานตัดสินใจว่าจะมีประสิทธิภาพมากกว่าในการใช้วิธีการแปลงข้อมูลเพื่อจัดเก็บและเรียกดูบันทึกของพวกเขา

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

เพื่อดึงข้อมูลลูกค้าบันทึกพนักงานจะได้รับหมายเลขลูกค้าบนกระดาษ ใช้หมายเลขลูกค้าที่ไม่ซ้ำกันนี้ ( คีย์แฮ ) พวกเขาจะปรับ 300 โดยเพื่อกำหนดตู้เก็บเอกสารที่มีโฟลเดอร์ลูกค้า เมื่อพวกเขาเปิดตู้เก็บเอกสารพวกเขาจะค้นพบว่ามีหลายโฟลเดอร์ที่เรียงลำดับตามหมายเลขลูกค้า การค้นหาผ่านระเบียนพวกเขาจะค้นหาโฟลเดอร์ไคลเอนต์และดึงข้อมูลอย่างรวดเร็ว

ในตัวอย่างจริงของโลกของเราของเราบุ้งกี๋เป็นตู้เก็บเอกสารของเราและบันทึกเป็นโฟลเดอร์แฟ้ม


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

ดังที่ Simon ได้กล่าวถึงซึ่งฉันเชื่อว่าสำคัญมากคือส่วนที่แปลงแป้นพิมพ์นั้นคือการแปลงพื้นที่ขนาดใหญ่ (ความยาวตามอำเภอใจโดยปกติจะเป็นสตริง ฯลฯ ) และการแมปไปยังพื้นที่ขนาดเล็ก ถ้าสำคัญมากที่จะจำ!

ดังนั้นในตัวอย่างข้างต้นลูกค้าที่เป็นไปได้ 30,000 คนหรือมากกว่านั้นจะถูกจับคู่กับพื้นที่ขนาดเล็ก


แนวคิดหลักในการนี้คือการแบ่งชุดข้อมูลทั้งหมดของคุณเป็นเซ็กเมนต์เพื่อเร่งการค้นหาที่แท้จริงซึ่งโดยปกติจะใช้เวลานาน ในตัวอย่างของเราข้างต้นตู้เก็บเอกสาร 300 ตู้แต่ละตู้มีสถิติประมาณ 100 รายการ การค้นหา (โดยไม่คำนึงถึงลำดับ) ผ่าน 100 รายการนั้นเร็วกว่าการจัดการกับ 30,000 รายการ

คุณอาจสังเกตเห็นว่าบางคนทำสิ่งนี้จริงแล้ว แต่แทนที่จะใช้วิธีการแฮชเพื่อสร้างรหัสแฮชพวกเขาส่วนใหญ่จะใช้ตัวอักษรตัวแรกของนามสกุล ดังนั้นหากคุณมีตู้เก็บเอกสาร 26 ตู้แต่ละอันมีจดหมายจาก A ถึง Z คุณในทางทฤษฎีได้แบ่งส่วนข้อมูลของคุณและปรับปรุงกระบวนการจัดเก็บและเรียกคืน

หวังว่าจะช่วยได้

Jeach!


2
คุณอธิบายถึงกลยุทธ์การหลีกเลี่ยงการชนกันของตารางแฮชที่เรียกว่า "การกำหนดที่อยู่แบบเปิด" หรือ "การปิดที่อยู่" แบบแปรผัน (ใช่เศร้า แต่จริง) หรือ "การผูกมัด" มีอีกประเภทหนึ่งที่ไม่ได้ใช้กลุ่มรายการ แต่เก็บรายการ "แบบอินไลน์" แทน
Konrad Rudolph

2
คำอธิบายที่ยอดเยี่ยม ยกเว้นตู้เก็บเอกสารแต่ละตู้จะมี100เรคคอร์ดโดยเฉลี่ย(บันทึก 30k / 300 ตู้ = 100) อาจจะคุ้มค่ากับการแก้ไข
Ryan Tuck

@TonyD ไปที่ไซต์นี้sha-1 ออนไลน์และสร้างแฮช SHA-1 สำหรับTonyDที่คุณพิมพ์ในฟิลด์ข้อความ e5dc41578f88877b333c8b31634cf77e4911ed8cคุณจะจบลงด้วยความคุ้มค่าของการสร้างสิ่งที่ดูเหมือนว่า นี่คืออะไรมากกว่าจำนวนเลขฐานสิบหกที่มีขนาดใหญ่เป็น 160 บิต (20 ไบต์) จากนั้นคุณสามารถใช้สิ่งนี้เพื่อกำหนดที่เก็บข้อมูล (จำนวน จำกัด ) ที่จะใช้ในการจัดเก็บบันทึกของคุณ
Jeach

@ Tonyony ฉันไม่แน่ใจว่าคำว่า "hash key" ถูกอ้างถึงในเรื่องที่ขัดแย้งกันที่ไหน ถ้าเป็นเช่นนั้นโปรดระบุสถานที่สองแห่งขึ้นไป หรือคุณกำลังพูดว่า "เรา" ใช้คำว่า "รหัสแฮช" ในขณะที่ไซต์อื่น ๆ เช่น Wikipedia ใช้ "ค่าแฮชรหัสแฮชผลรวมแฮหรือจำนวนแฮช" ถ้าเป็นเช่นนั้นใครจะสนใจตราบใดที่คำที่ใช้นั้นสอดคล้องกันภายในกลุ่มหรือองค์กร โปรแกรมเมอร์มักจะใช้คำว่า "กุญแจ" ฉันจะยืนยันว่าตัวเลือกที่ดีอีกทางเลือกหนึ่งก็คือ แต่ฉันจะออกกฎโดยใช้ "รหัสแฮชผลรวมแฮหรือเพียงแค่แฮช" มุ่งเน้นไปที่อัลกอริทึมไม่ใช่คำพูด!
Jeach

2
@TonyD ฉันได้เปลี่ยนข้อความเป็น "พวกเขาจะโมดูลแฮชคีย์ 300" โดยหวังว่ามันจะสะอาดและชัดเจนขึ้นสำหรับทุกคน ขอบคุณ!
Jeach

64

สิ่งนี้กลายเป็นทฤษฎีที่ค่อนข้างลึก แต่โครงร่างพื้นฐานนั้นเรียบง่าย

โดยพื้นฐานแล้วฟังก์ชันแฮชเป็นเพียงฟังก์ชั่นที่นำสิ่งต่าง ๆ จากพื้นที่หนึ่ง (พูดถึงสตริงที่มีความยาวตามอำเภอใจ) และแมปไปยังพื้นที่ที่มีประโยชน์สำหรับการทำดัชนี

หากคุณมีพื้นที่เล็ก ๆ ของสิ่งที่ต้องแฮชคุณอาจหายไปโดยตีความสิ่งเหล่านั้นเป็นจำนวนเต็มและคุณเสร็จแล้ว (เช่น 4 ไบต์สตริง)

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

นี่หมายความว่าคุณต้องการให้มันไม่น่าจะมีผลลัพธ์เหมือนกันและคุณอาจต้องการให้ฟังก์ชันแฮชทำงานเร็ว

การปรับความสมดุลของคุณสมบัติทั้งสองนี้ (และอีกสองสามอย่าง) ทำให้คนจำนวนมากไม่ว่าง!

ในทางปฏิบัติคุณมักจะสามารถหาฟังก์ชั่นที่ใช้งานได้ดีสำหรับแอปพลิเคชันของคุณและใช้งานได้

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

ดังนั้นเมื่อคุณใช้เวลานานทุกรายการใน hashtable ของคุณ (อาร์เรย์) จะว่างเปล่าหรือมีหนึ่งรายการหรือรายการของรายการ การดึงข้อมูลนั้นเป็นเรื่องง่ายเหมือนกับการทำดัชนีในอาเรย์และคืนค่าหรือเดินรายการค่าและส่งกลับค่าที่ถูกต้อง

แน่นอนว่าในทางปฏิบัติคุณมักไม่สามารถทำสิ่งนี้ได้ ดังนั้นคุณจึงทำทุกอย่างตามลำดับกระจัดกระจาย (เฉพาะรายการที่คุณใช้จริง ๆ

มีแผนการและลูกเล่นมากมายที่จะทำให้งานนี้ดีขึ้น แต่นั่นคือพื้นฐาน


1
ขออภัยฉันรู้ว่านี่เป็นคำถาม / คำตอบเก่า แต่ฉันพยายามเข้าใจจุดสุดท้ายที่คุณทำ ตารางแฮชมีความซับซ้อนของเวลา O (1) อย่างไรก็ตามเมื่อคุณใช้อาร์เรย์หร็อมแหร็มคุณไม่จำเป็นต้องทำการค้นหาแบบไบนารีเพื่อค้นหาค่าของคุณหรือไม่? ณ จุดนั้นความซับซ้อนของเวลาไม่กลายเป็น O (log n) หรือไม่
herbrandson

@herbrandson: ไม่ ... อาร์เรย์ที่กระจัดกระจายนั้นหมายถึงว่ามีดัชนีจำนวนไม่มากที่มีค่าอยู่ - คุณยังสามารถจัดทำดัชนีโดยตรงไปยังองค์ประกอบอาร์เรย์เฉพาะสำหรับค่าแฮชที่คุณคำนวณจากคีย์ของคุณ ยังคงดำเนินการอาร์เรย์เบาบางไซมอนอธิบายเป็นสติเฉพาะในมากสถานการณ์ จำกัด : เมื่อขนาดถังเป็นคำสั่งของขนาดหน้าหน่วยความจำ (เทียบกับบอกว่าintกุญแจที่ 1 ใน 1,000 เบาบางและ 4k หน้า = หน้าเว็บส่วนใหญ่สัมผัส) และเมื่อ ถือว่า OS ทั้งหมด 0 หน้าได้อย่างมีประสิทธิภาพ (ดังนั้นทั้งหมดที่ไม่ได้ใช้ถังหน้าไม่จำเป็นต้องมีหน่วยความจำสำรองข้อมูล) เมื่อพื้นที่ที่อยู่อุดมสมบูรณ์ ....
โทนี่ Delroy

@TonyDelroy - มันเป็นความจริงมันเป็นเรื่องใหญ่ แต่ความคิดคือการให้ภาพรวมของสิ่งที่พวกเขาและทำไมไม่ใช้งานจริง รายละเอียดของหลังนั้นมีความเหมาะสมยิ่งขึ้นเมื่อคุณพยักหน้าในการขยายตัวของคุณ
simon

48

มีคำตอบมากมาย แต่ไม่มีคำตอบใดที่มองเห็นได้ง่ายและตารางแฮชสามารถ "คลิก" ได้ง่ายเมื่อมองเห็น

มักใช้ตารางแฮชเป็นอาร์เรย์ของรายการที่ลิงก์ หากเรานึกภาพตารางที่จัดเก็บชื่อของผู้คนหลังจากการแทรกสองสามครั้งมันอาจถูกจัดวางในหน่วยความจำด้านล่างโดยที่()ตัวเลขที่ถูกปกปิดเป็นค่าแฮชของข้อความ / ชื่อ

bucket#  bucket content / linked list

[0]      --> "sue"(780) --> null
[1]      null
[2]      --> "fred"(42) --> "bill"(9282) --> "jane"(42) --> null
[3]      --> "mary"(73) --> null
[4]      null
[5]      --> "masayuki"(75) --> "sarwar"(105) --> null
[6]      --> "margaret"(2626) --> null
[7]      null
[8]      --> "bob"(308) --> null
[9]      null

คะแนนน้อย:

  • แต่ละรายการอาร์เรย์ (ดัชนี[0], [1]... ) เป็นที่รู้จักกันเป็นถังและเริ่มต้น - อาจจะว่างเปล่า - เชื่อมโยงรายการค่า ( องค์ประกอบที่รู้จักกันในตัวอย่างนี้ - ชื่อของผู้คน )
  • แต่ละค่า (เช่น"fred"กับแฮช42) ถูกเชื่อมโยงจากที่ฝากข้อมูล[hash % number_of_buckets]เช่น42 % 10 == [2];%เป็นตัวดำเนินการแบบโมดูโล - ส่วนที่เหลือเมื่อหารด้วยจำนวนของถัง
  • ค่าข้อมูลหลายค่าอาจชนกันและเชื่อมโยงจากที่เก็บข้อมูลเดียวกันบ่อยครั้งเนื่องจากค่าแฮชของพวกเขาชนกันหลังจากการทำงานของโมดูโล (เช่น42 % 10 == [2]และ9282 % 10 == [2] ) แต่บางครั้งเนื่องจากค่าแฮชเหมือนกัน (เช่น"fred"และ"jane"ทั้งคู่แสดงด้วยแฮช42ด้านบน)
    • ตารางแฮชส่วนใหญ่จัดการการชน - ด้วยประสิทธิภาพที่ลดลงเล็กน้อย แต่ไม่มีความสับสนในการทำงาน - โดยการเปรียบเทียบค่าเต็ม (ข้อความที่นี่) ของค่าที่ต้องการหรือแทรกลงในแต่ละค่าที่มีอยู่แล้วในรายการที่เชื่อมโยงที่ฝากข้อมูลแฮช

ความยาวของรายการที่เชื่อมโยงนั้นเกี่ยวข้องกับปัจจัยการโหลดไม่ใช่จำนวนของค่า

ถ้าขนาดของตารางที่เติบโตขึ้นตารางแฮชการดำเนินการดังกล่าวข้างต้นมีแนวโน้มที่จะปรับขนาดตัวเอง (เช่นสร้างอาร์เรย์ขนาดใหญ่ของบุ้งกี๋สร้างปรับปรุง / เชื่อมโยงรายการใหม่มีจากลบอาร์เรย์เก่า) เพื่อให้อัตราส่วนของค่าถัง (aka โหลด ตัวคูณ ) อยู่ที่ไหนสักแห่งในช่วง 0.5 ถึง 1.0

Hans ให้สูตรจริงสำหรับปัจจัยการโหลดอื่น ๆ ในความคิดเห็นด้านล่าง แต่สำหรับค่าที่บ่งบอก: ด้วย load factor 1 และฟังก์ชั่นแฮชกำลังของการเข้ารหัสลับ 1 / e (~ 36.8%) ของถังจะว่างเปล่าอีก 1 / e (~ 36.8%) มีหนึ่งองค์ประกอบ 1 / (2e) หรือ ~ 18.4% สององค์ประกอบ 1 / (3! e) ประมาณ 6.1% สามองค์ประกอบ 1 / (4! e) หรือ ~ 1.5% สี่องค์ประกอบ 1 / (5! e) ~ .3% มี 5 อย่าง - ความยาวโซ่เฉลี่ยจากถังที่ไม่ว่างเปล่าคือ ~ 1.58 ไม่ว่าองค์ประกอบจะอยู่ในตารางกี่รายการ (เช่นมี 100 องค์ประกอบและ 100 ถังหรือ 100 ล้าน องค์ประกอบและ 100 ล้านฝากข้อมูล) ซึ่งเป็นเหตุผลที่เราบอกว่าการค้นหา / แทรก / ลบเป็นOดำเนินการเวลา (1)คงที่

ตารางแฮชสามารถเชื่อมโยงคีย์กับค่าต่างๆได้อย่างไร

จากการใช้ตารางแฮชตามที่อธิบายไว้ข้างต้นเราสามารถจินตนาการการสร้างประเภทค่าเช่นstruct Value { string name; int age; };และการเปรียบเทียบความเท่าเทียมกันและฟังก์ชันแฮชที่ดูเฉพาะที่nameฟิลด์ (ไม่สนใจอายุ) และจากนั้นมีบางสิ่งที่ยอดเยี่ยมเกิดขึ้น: เราสามารถเก็บValueบันทึกเช่น{"sue", 63}ในตาราง จากนั้นค้นหา "ฟ้อง" โดยไม่ทราบอายุของเธอค้นหาค่าที่เก็บไว้และกู้คืนหรืออัปเดตอายุของเธอ
- สุขสันต์วันเกิด Sue - ที่น่าสนใจไม่เปลี่ยนค่าแฮชดังนั้นไม่ต้องการให้เราย้ายระเบียนของ Sue ไปที่อื่น ถัง.

เมื่อเราทำเช่นนี้เรากำลังใช้ตารางแฮชเป็นแผนที่เชื่อมโยงคอนเทนเนอร์ akaและค่าที่เก็บนั้นสามารถถือได้ว่าประกอบด้วยคีย์ (ชื่อ) และฟิลด์อื่น ๆ หนึ่งฟิลด์หรือมากกว่านั้นยังคงเรียกว่าสับสน - ค่า ( ในตัวอย่างของฉันแค่อายุ) การใช้ตารางแฮชที่ใช้เป็นแผนที่เรียกว่าแผนที่แฮชแผนที่กัญชา

ตรงข้ามกับตัวอย่างก่อนหน้านี้ในคำตอบนี้ที่เราเก็บค่าที่ไม่ต่อเนื่องเช่น "sue" ซึ่งคุณคิดว่าเป็นกุญแจของตัวเอง: การใช้งานชนิดนั้นเรียกว่าชุดแฮชชุดกัญชา

มีวิธีอื่นที่จะใช้ตารางแฮช

ไม่ใช่ตารางแฮชทั้งหมดที่ใช้รายการที่เชื่อมโยง (รู้จักกันในชื่อการโยงแบบแยกต่างหาก ) แต่วัตถุประสงค์ทั่วไปส่วนใหญ่ใช้เป็นตัวเลือกหลักที่ปิดการแฮช (หรือที่อยู่แบบเปิด ) - โดยเฉพาะอย่างยิ่งกับการดำเนินการลบที่สนับสนุน ฟังก์ชันแฮช


คำสองสามคำเกี่ยวกับฟังก์ชันแฮช

คร่ำเครียดที่แข็งแกร่ง ...

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

ปกติแล้วนี่จะประกอบไปด้วยคณิตศาสตร์ที่ซับซ้อนเกินไปสำหรับฉัน ฉันจะพูดถึงวิธีที่ง่ายต่อการเข้าใจ - ไม่ใช่วิธีที่ปรับขนาดได้หรือแคชที่เป็นมิตร แต่สง่างามโดยเนื้อแท้ (เช่นการเข้ารหัสด้วยแผ่นเพียงครั้งเดียว!) เพราะฉันคิดว่ามันช่วยให้บ้านมีคุณสมบัติที่ต้องการดังกล่าวข้างต้น สมมติว่าคุณ hashing 64- บิตdoubles - คุณสามารถสร้าง 8 ตารางแต่ละหมายเลขสุ่ม 256 (รหัสด้านล่าง), จากนั้นใช้ 8-bit / 1-byte slice ของการdoubleเป็นตัวแทนหน่วยความจำเพื่อทำดัชนีลงในตารางที่แตกต่างกัน ตัวเลขสุ่มที่คุณค้นหา ด้วยวิธีนี้มันง่ายที่จะเห็นว่าบิต (ในความหมายเลขฐานสอง) การเปลี่ยนแปลงที่ใดก็ได้ในdoubleผลลัพธ์ในตัวเลขสุ่มที่แตกต่างกันที่ค้นหาในตารางใดตารางหนึ่งและค่าสุดท้ายที่ไม่เกี่ยวข้องทั้งหมด

// note caveats above: cache unfriendly (SLOW) but strong hashing...
size_t random[8][256] = { ...random data... };
const char* p = (const char*)&my_double;
size_t hash = random[0][p[0]] ^ random[1][p[1]] ^ ... ^ random[7][p[7]];

อ่อนล้า แต่คร่ำครึเร็ว ...

ฟังก์ชั่นการแฮชของห้องสมุดหลายแห่งผ่านจำนวนเต็มผ่านการเปลี่ยนแปลง (เรียกว่าฟังก์ชันแฮชเล็กน้อยหรือเอกลักษณ์ ) มันเป็นสุดโต่งอื่น ๆ จากการแฮ็กที่แข็งแกร่งที่อธิบายไว้ข้างต้น แฮชของข้อมูลเฉพาะตัวเป็นอย่างมากการชนกันเกิดขึ้นได้ในกรณีที่เลวร้ายที่สุด แต่ความหวังก็คือในกรณีที่พบได้บ่อยในคีย์จำนวนเต็มที่มีแนวโน้มที่จะเพิ่มขึ้น (อาจจะมีช่องว่างบางส่วน) พวกเขาจะทำแผนที่ลงในถังต่อเนื่อง % ที่ปัจจัยการโหลด 1 กล่าวถึงก่อนหน้านี้) ดังนั้นจึงมีการชนกันน้อยลงและรายการการชนกันขององค์ประกอบการชนที่น้อยลงนั้นทำได้โดยการแมปแบบสุ่ม นอกจากนี้ยังเป็นวิธีที่ดีในการประหยัดเวลาในการสร้างแฮชที่รัดกุมและหากมีการค้นหากุญแจเพื่อที่จะพบปุ่มเหล่านี้ในที่เก็บข้อมูลใกล้เคียงในหน่วยความจำ เมื่อคีย์ไม่ เพิ่มขึ้นอย่างมากหวังว่าพวกเขาจะสุ่มพอที่พวกเขาจะไม่ต้องการฟังก์ชั่นแฮชที่แข็งแกร่งเพื่อสุ่มตำแหน่งของพวกเขาทั้งหมดลงในถัง


6
ให้ฉันพูดว่า: คำตอบที่ยอดเยี่ยม
CRThaze

@Tony Delroy ขอบคุณสำหรับคำตอบที่น่าอัศจรรย์ ฉันยังคงมีจุดเปิดหนึ่งในใจของฉันว่า คุณบอกว่าแม้ว่าจะมีถัง 100 ล้านถัง แต่เวลาในการค้นหาจะเป็น O (1) พร้อมตัวประกอบโหลด 1 และฟังก์ชันแฮ็กการเข้ารหัสลับแบบเข้ารหัส แต่สิ่งที่เกี่ยวกับการหาถังที่เหมาะสมใน 100 ล้าน? แม้ว่าเราจะมีถังทั้งหมดที่เรียงลำดับมันไม่ใช่ O (log100.000.000) ใช่ไหม การค้นหาที่ฝากข้อมูลจะเป็น O (1) ได้อย่างไร
selman

@selman: คำถามของคุณไม่ได้ให้รายละเอียดมากมายที่จะอธิบายว่าทำไมคุณถึงคิดว่าอาจเป็น O (log100,000,000) แต่คุณจะพูดว่า "แม้ว่าเราจะมีถังทั้งหมดที่เรียงลำดับ" - โปรดจำไว้ว่าค่าในถังตารางแฮช จะไม่เคย "เรียง" ในความหมายปกติ: มูลค่าที่ปรากฏในถังซึ่งจะถูกกำหนดโดยการใช้ฟังก์ชั่นแฮชคีย์ การคิดความซับซ้อนคือ O (log100,000,000) บอกเป็นนัยว่าคุณจินตนาการถึงการทำการค้นหาแบบไบนารีผ่านที่จัดเรียงถัง แต่นั่นไม่ใช่วิธีการแปลงแป้นพิมพ์ทำงาน อาจจะอ่านคำตอบอื่น ๆ สองสามข้อแล้วดูว่ามันเหมาะสมหรือไม่
Tony Delroy

@TonyDelroy แน่นอน "การจัดเรียงถัง" เป็นสถานการณ์ที่ดีที่สุดที่ฉันคิด ดังนั้น O (log100,000,000) แต่ถ้าไม่ใช่ในกรณีนี้แอปพลิเคชันจะค้นหากลุ่มที่เกี่ยวข้องได้อย่างไร ฟังก์ชันแฮชสร้างตำแหน่งหน่วยความจำอย่างใดหรือไม่?
selman

1
@selman: เนื่องจากหน่วยความจำคอมพิวเตอร์ให้เวลาคงที่ "การเข้าถึงแบบสุ่ม": หากคุณสามารถคำนวณที่อยู่หน่วยความจำคุณสามารถดึงเนื้อหาหน่วยความจำได้โดยไม่ต้องเข้าถึงหน่วยความจำในส่วนอื่น ๆ ของอาร์เรย์ ดังนั้นไม่ว่าคุณจะเข้าถึงที่ฝากข้อมูลแรกที่ฝากข้อมูลล่าสุดหรือที่ฝากข้อมูลที่ใดก็ตามระหว่างนั้นจะมีลักษณะการทำงานที่เหมือนกัน (หลวมใช้เวลาเท่ากันแม้ว่าจะมีผลกระทบต่อการแคชหน่วยความจำ CPU L1 / L2 / L3 แต่ พวกเขาทำงานเพื่อช่วยให้คุณเข้าถึงที่เข้าถึงล่าสุดหรือถังใกล้เคียงที่เพิ่งเกิดขึ้นอย่างรวดเร็วและสามารถถูกละเว้นสำหรับการวิเคราะห์ O ขนาดใหญ่)
Tony Delroy

24

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

เป็นสิ่งสำคัญที่จะต้องทราบว่าโดยทั่วไปตัว hashvalue ไม่ได้ระบุช่องที่จะใส่ค่า ตัวอย่างเช่น hashvalue อาจเป็นค่าจำนวนเต็มลบ เห็นได้ชัดว่าจำนวนลบไม่สามารถชี้ไปที่ตำแหน่งอาเรย์ นอกจากนี้ค่าแฮจะมีแนวโน้มที่จะมีจำนวนมากกว่าช่องที่มีอยู่หลายครั้ง ดังนั้นการคำนวณอื่นจะต้องดำเนินการโดย hashtable เพื่อคำนวณว่าควรจะใส่ค่าใดลงในสล็อต สิ่งนี้ทำกับการดำเนินการทางคณิตศาสตร์โมดูลัสเช่น:

uint slotIndex = hashValue % hashTableSize;

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

slotIndex = (remainder + 1) % hashTableSize;

ฉันคิดว่าอาจมีวิธีการขั้นสูงอื่น ๆ เพิ่มเติมในการกำหนดดัชนีสล็อต แต่นี่เป็นวิธีการทั่วไปที่ฉันเคยเห็น ... จะสนใจวิธีอื่นที่ทำงานได้ดีกว่า

ด้วยวิธีการโมดูลัสถ้าคุณมีตารางขนาดพูด 1,000, hashvalue ใด ๆ ที่อยู่ระหว่าง 1 ถึง 1,000 จะเข้าไปในช่องที่เกี่ยวข้อง ค่าลบใด ๆ และค่าใด ๆ ที่มากกว่า 1,000 จะเป็นการชนค่าของสล็อต โอกาสที่จะเกิดขึ้นนั้นขึ้นอยู่กับวิธีการแฮชของคุณรวมถึงจำนวนไอเท็มทั้งหมดที่คุณเพิ่มลงในตารางแฮช โดยทั่วไปแล้ววิธีที่ดีที่สุดในการทำให้ขนาดของ hashtable นั้นจำนวนรวมของค่าที่เพิ่มเข้ามานั้นเท่ากับ 70% ของขนาดเท่านั้น หากฟังก์ชันแฮชของคุณทำงานได้ดีแม้ในการแจกจ่ายโดยทั่วไปคุณจะพบว่ามีการชนกันระหว่างช่องเก็บข้อมูลและช่องว่างน้อยมากและจะทำงานได้อย่างรวดเร็วสำหรับการค้นหาและเขียน หากไม่ทราบจำนวนรวมของสิ่งที่จะเพิ่มล่วงหน้าให้ใช้การคาดเดาที่ดีโดยใช้วิธีการใดก็ได้

ฉันหวังว่าสิ่งนี้จะช่วยได้

PS - ใน C # GetHashCode()วิธีนี้ค่อนข้างช้าและทำให้เกิดการชนกันของมูลค่าจริงภายใต้เงื่อนไขที่ฉันทดสอบ เพื่อความสนุกที่แท้จริงสร้าง hashfunction ของคุณเองและพยายามทำให้มันไม่เคยชนกับข้อมูลเฉพาะที่คุณ hashing ทำงานเร็วกว่า GetHashCode และมีการกระจายที่เท่าเทียมกัน ฉันทำสิ่งนี้โดยใช้เวลานานแทนที่จะใช้ค่าแฮชโค้ดขนาด int และมันทำงานได้ดีมากถึง 32 ล้านรายการโดยใช้แฮชค่าใน hashtable ที่มีการชนกัน 0 ครั้ง น่าเสียดายที่ฉันไม่สามารถแบ่งปันรหัสได้เนื่องจากเป็นของนายจ้าง ... แต่ฉันสามารถเปิดเผยได้ว่าเป็นไปได้สำหรับโดเมนข้อมูลบางอย่าง เมื่อคุณสามารถบรรลุสิ่งนี้ hashtable นั้นเร็วมาก :)


ฉันรู้ว่าการโพสต์เก่าสวย แต่คนที่สามารถอธิบายสิ่ง (ที่เหลือ + 1) วิธีการที่นี่
Hari

3
@Hari remainderหมายถึงผลลัพธ์ของการคำนวณแบบโมดูโลดั้งเดิมและเราบวก 1 เข้าไปเพื่อค้นหาสล็อตถัดไปที่มีอยู่
x4nd3r

"อาเรย์นั้นจะมีบางอย่างในแต่ละสล็อตอย่างน้อยที่สุดคุณจะเก็บ hashvalue หรือค่าของมันในสล็อตนี้" - เป็นเรื่องปกติสำหรับ "ช่อง" (ที่เก็บข้อมูล) ที่ไม่มีค่าเลย การใช้การกำหนดแอดเดรสแบบเปิดมักจะเก็บ NULL หรือตัวชี้ไปยังโหนดแรกในรายการที่ลิงก์ - โดยไม่มีค่าโดยตรงในสล็อต / ที่ฝากข้อมูล "จะมีความสนใจในเรื่องอื่นใด" - ที่ "+1" คุณแสดงให้เห็นถึงเรียกว่าเชิงเส้นละเอียด , ผู้ทรงมีประสิทธิภาพดีกว่า: สมการกำลังสองแหย่ "โดยทั่วไปจะพบน้อยมากที่จะไม่มีการชนถัง / สล็อต" - @ ความจุ 70%, ~ 12% สล็อตที่มีค่า 2 ~ ~ 3% 3 ....
โทนี่ Delroy

"ฉันทำสิ่งนี้โดยใช้เวลานานแทนที่จะใช้ค่าแฮชโค้ดขนาด int และมันใช้งานได้ดีมากถึง 32 ล้านรายการโดยใช้แฮชค่าใน hashtable ที่มีการชนกัน 0 ครั้ง" - นี่เป็นไปไม่ได้ในกรณีทั่วไปที่ค่าของคีย์นั้นสุ่มอย่างมีประสิทธิภาพในช่วงที่ใหญ่กว่าจำนวนที่เก็บข้อมูล โปรดทราบว่าการมีค่าแฮชที่แตกต่างนั้นมักจะง่ายพอ (และการที่คุณพูดlongถึงค่าแฮชหมายถึงสิ่งที่คุณประสบความสำเร็จ) แต่ต้องแน่ใจว่าพวกเขาจะไม่ชนกันในตารางแฮชหลังจากการดำเนินการ mod /% ไม่ )
Tony Delroy

(การหลีกเลี่ยงการชนทั้งหมดเรียกว่าhashing ที่สมบูรณ์แบบโดยทั่วไปมันใช้งานได้จริงสำหรับปุ่มสองสามร้อยหรือพันที่รู้ล่วงหน้า gperf เป็นตัวอย่างของเครื่องมือในการคำนวณฟังก์ชัน hash คุณยังสามารถเขียนของคุณเองในจำนวน จำกัด สถานการณ์ - เช่นหากคีย์ของคุณเป็นตัวชี้ไปยังวัตถุจากพูลหน่วยความจำของคุณซึ่งเก็บไว้ค่อนข้างเต็มโดยแต่ละพอยน์เตอร์มีระยะห่างคงที่คุณสามารถแบ่งพอยน์เตอร์ตามระยะทางนั้นและมีดัชนีเป็นอาร์เรย์เบาบางเล็กน้อย การปะทะกัน.)
Tony Delroy

17

นี่คือวิธีการทำงานในความเข้าใจของฉัน:

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

สมมติว่าคุณมีวัตถุ 200 ชิ้น แต่มีเพียง 15 ชิ้นเท่านั้นที่มีรหัสแฮชที่ขึ้นต้นด้วยตัวอักษร 'B. ' ตารางแฮชจะต้องค้นหาและค้นหาวัตถุทั้ง 15 รายการในที่เก็บข้อมูล 'B' แทนที่จะเป็นวัตถุ 200 รายการ

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


13

สั้นและหวาน:

ตารางแฮช wraps internalArrayขึ้นอาร์เรย์ช่วยให้เรียกว่า รายการจะถูกแทรกลงในอาร์เรย์ด้วยวิธีนี้:

let insert key value =
    internalArray[hash(key) % internalArray.Length] <- (key, value)
    //oversimplified for educational purposes

บางครั้งสองคีย์จะแฮชไปยังดัชนีเดียวกันในอาเรย์และคุณต้องการเก็บค่าทั้งสองไว้ ฉันชอบที่จะเก็บค่าทั้งสองไว้ในดัชนีเดียวกันซึ่งง่ายต่อการเขียนโค้ดโดยสร้างinternalArrayอาเรย์ของรายการที่ลิงก์:

let insert key value =
    internalArray[hash(key) % internalArray.Length].AddLast(key, value)

ดังนั้นหากฉันต้องการดึงไอเท็มออกจากตารางแฮชของฉันฉันสามารถเขียน:

let get key =
    let linkedList = internalArray[hash(key) % internalArray.Length]
    for (testKey, value) in linkedList
        if (testKey = key) then return value
    return null

การลบนั้นง่ายเพียงแค่เขียน อย่างที่คุณสามารถบอกแทรกการค้นหาและการลบออกจากรายการที่ลิงก์ของเราเกือบจะ O (1)

เมื่อ internalArray ของเราเต็มเกินไปอาจจะมีความจุประมาณ 85% เราสามารถปรับขนาดอาร์เรย์ภายในและย้ายรายการทั้งหมดจากอาร์เรย์เก่าไปยังอาร์เรย์ใหม่


11

มันง่ายกว่านั้นอีก

Hashtable คืออะไรมากกว่าอาร์เรย์ (มักจะเบาบาง ) ของเวกเตอร์ซึ่งมีคู่ของคีย์ / ค่า ขนาดสูงสุดของอาร์เรย์นี้มักจะมีขนาดเล็กกว่าจำนวนของรายการในชุดของค่าที่เป็นไปได้สำหรับประเภทของข้อมูลที่เก็บไว้ใน hashtable

อัลกอริทึมแฮชจะใช้ในการสร้างดัชนีในอาร์เรย์นั้นขึ้นอยู่กับค่าของรายการที่จะถูกเก็บไว้ในอาร์เรย์

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

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


10

คุณนำสิ่งต่าง ๆ และอาร์เรย์

สำหรับแต่ละสิ่งคุณสร้างดัชนีสำหรับมันเรียกว่าแฮช สิ่งสำคัญเกี่ยวกับการแฮชคือ 'กระจาย' มาก คุณไม่ต้องการให้สองสิ่งที่คล้ายกันมีแฮชคล้ายกัน

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

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

เมื่อการแฮ็คของคุณทำงานได้ดีและอาเรย์ของคุณนั้นใหญ่พอจะมีเพียงไม่กี่อย่างที่ทำดัชนีเฉพาะในอาเรย์ดังนั้นคุณจะไม่ต้องดูมากนัก

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


1
ขอบคุณสำหรับจุดสุดท้ายที่คนอื่นไม่ได้พูดถึง
Sandeep Raju Prabhakar

4

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

ดังที่ simon อธิบายฟังก์ชั่นแฮชจะใช้ในการทำแผนที่จากพื้นที่ขนาดใหญ่ไปยังพื้นที่ขนาดเล็ก การใช้ฟังก์ชันแฮชที่ไร้เดียงสาอย่างไร้เดียงสาสำหรับตัวอย่างของเราอาจใช้ตัวอักษรตัวแรกของสตริงและแมปกับจำนวนเต็มดังนั้น "alligator" มีรหัสแฮชเป็น 0, "bee" มีรหัสแฮชของ 1, " ม้าลาย "จะเป็น 25 เป็นต้น

ต่อไปเรามีอาร์เรย์จำนวน 26 ถัง (อาจเป็น ArrayLists ใน Java) และเราวางรายการลงในที่ฝากข้อมูลที่ตรงกับรหัสแฮชของคีย์ของเรา หากเรามีมากกว่าหนึ่งรายการที่มีคีย์ที่ขึ้นต้นด้วยตัวอักษรเดียวกันพวกเขาจะมีรหัสแฮชเดียวกันดังนั้นทุกคนจะเข้าไปในที่ฝากข้อมูลสำหรับรหัสแฮชนั้นดังนั้นการค้นหาเชิงเส้นจะต้องทำในที่เก็บเพื่อ ค้นหารายการเฉพาะ

ในตัวอย่างของเราหากเรามีรายการไม่กี่โหลที่มีแป้นที่ครอบคลุมตัวอักษรมันจะทำงานได้ดีมาก อย่างไรก็ตามหากเรามีล้านรายการหรือทุกปุ่มเริ่มต้นด้วย 'a' หรือ 'b' ตารางแฮชของเราจะไม่เหมาะ เพื่อให้ได้ประสิทธิภาพที่ดีขึ้นเราจะต้องใช้ฟังก์ชันแฮชและ / หรือที่เก็บข้อมูลเพิ่มเติม


3

นี่เป็นอีกวิธีในการดู

ฉันถือว่าคุณเข้าใจแนวคิดของอาเรย์ A นั่นเป็นสิ่งที่สนับสนุนการทำงานของการจัดทำดัชนีซึ่งคุณสามารถไปที่องค์ประกอบ Ith, A [I] ในขั้นตอนเดียวไม่ว่า A จะใหญ่เพียงใด

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

แต่แน่นอนว่าอาจมีบุคคลมากกว่าหนึ่งคนที่มีอายุเท่ากันดังนั้นสิ่งที่คุณใส่ไว้ในแต่ละรายการนั้นเป็นรายการของคนทั้งหมดที่มีอายุเท่ากัน ดังนั้นคุณสามารถเข้าถึงข้อมูลของบุคคลในขั้นตอนเดียวบวกกับการค้นหาเล็กน้อยในรายการนั้น (เรียกว่า "bucket") มันจะช้าลงถ้ามีคนมากมายที่ถังใหญ่ จากนั้นคุณต้องใช้ชุดที่มีขนาดใหญ่ขึ้นและวิธีอื่น ๆ ในการรับข้อมูลที่ระบุเกี่ยวกับบุคคลเช่นตัวอักษรสองสามตัวแรกของนามสกุลแทนที่จะใช้อายุ

นั่นเป็นแนวคิดพื้นฐาน แทนที่จะใช้อายุฟังก์ชันใด ๆ ของบุคคลที่สร้างค่านิยมที่ดีสามารถนำมาใช้ได้ นั่นคือฟังก์ชั่นแฮช เช่นเดียวกับที่คุณสามารถรับ ASCII ทุก ๆ สามบิตของชื่อของบุคคลนั้น สิ่งที่สำคัญคือคุณไม่ต้องการให้มีคนจำนวนมากเกินไปที่จะแฮ็คไปที่ถังเดียวกันเพราะความเร็วนั้นขึ้นอยู่กับถังที่เหลืออยู่เล็กน้อย


2

วิธีการคำนวณแฮชมักไม่ขึ้นอยู่กับ hashtable แต่ขึ้นอยู่กับรายการที่เพิ่มเข้าไป ในเฟรมเวิร์ก / ไลบรารีคลาสพื้นฐานเช่น. net และ Java แต่ละอ็อบเจ็กต์มีเมธอด GetHashCode () (หรือคล้ายกัน) ที่ส่งคืนรหัสแฮชสำหรับวัตถุนี้ อัลกอริทึมรหัสแฮชที่ดีที่สุดและการใช้งานที่แน่นอนนั้นขึ้นอยู่กับข้อมูลที่แสดงในวัตถุ


2

ตารางแฮชทำงานโดยสิ้นเชิงกับข้อเท็จจริงที่ว่าการคำนวณในทางปฏิบัติเป็นไปตามรูปแบบเครื่องเข้าถึงแบบสุ่มเช่นค่าที่อยู่ใด ๆ ในหน่วยความจำสามารถเข้าถึงได้ในเวลา O (1) หรือเวลาคงที่

ดังนั้นถ้าฉันมีจักรวาลของคีย์ (ชุดของคีย์ที่เป็นไปได้ทั้งหมดที่ฉันสามารถใช้ในแอปพลิเคชันเช่นม้วนหมายเลขสำหรับนักเรียนถ้าเป็นเลข 4 หลักจักรวาลนี้เป็นชุดของตัวเลขตั้งแต่ 1 ถึง 9999) และ a วิธีการจับคู่แผนที่กับจำนวนขนาดที่ จำกัด ฉันสามารถจัดสรรหน่วยความจำในระบบของฉันในทางทฤษฎีแล้วตารางแฮชของฉันก็พร้อมใช้งาน

โดยทั่วไปในแอปพลิเคชันขนาดของจักรวาลของคีย์นั้นมีขนาดใหญ่กว่าจำนวนองค์ประกอบที่ฉันต้องการเพิ่มลงในตารางแฮช (ฉันไม่ต้องการเสียหน่วยความจำ 1 GB ในการแฮชเช่นค่า 10000 หรือ 100000 เพราะเป็น 32 บิตยาวในการตอบกลับแบบไบนารี) ดังนั้นเราจึงใช้การแปลงแป้นพิมพ์นี้ เป็นการเรียงลำดับของการดำเนินการ "ทางคณิตศาสตร์" แบบผสมซึ่งแมปจักรวาลขนาดใหญ่ของฉันกับชุดเล็ก ๆ ของค่าที่ฉันสามารถรองรับในหน่วยความจำ ในกรณีที่ใช้งานจริงพื้นที่ของตารางแฮชมักจะมี "ลำดับ" (big-O) เหมือนกันกับ (จำนวนองค์ประกอบ * ขนาดของแต่ละองค์ประกอบ) ดังนั้นเราจึงไม่เสียความทรงจำมากนัก

ตอนนี้เซ็ตขนาดใหญ่ถูกแม็พกับเซ็ตขนาดเล็กการแม็พต้องเป็นหลายต่อหนึ่ง ดังนั้นคีย์ที่แตกต่างกันจะได้รับการจัดสรรพื้นที่เดียวกัน (ไม่ยุติธรรม) มีสองสามวิธีในการจัดการกับสิ่งนี้ฉันเพิ่งรู้จักสองคนที่เป็นที่นิยม:

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

รู้เบื้องต้นเกี่ยวกับอัลกอริทึมโดย CLRS ให้ข้อมูลเชิงลึกที่ดีมากในหัวข้อ


0

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

(void) addValue : (object) value
{
   int bucket = calculate_bucket_from_val(value);
   if (bucket) 
   {
       //do nothing, just overwrite
   }
   else   //create bucket
   {
      create_extra_space_for_bucket();
   }
   put_value_into_bucket(bucket,value);
}

(bool) exists : (object) value
{
   int bucket = calculate_bucket_from_val(value);
   return bucket;
}

ซึ่งcalculate_bucket_from_val()เป็นฟังก์ชั่นคร่ำเครียดที่ทุกมายากลเอกลักษณ์ที่ต้องเกิดขึ้น

กฏเกณฑ์ง่ายๆคือ: สำหรับการแทรกค่าที่กำหนดไว้ที่ฝากข้อมูลจะต้องไม่ซ้ำกัน & สามารถนำมาใช้ได้จากค่าที่ควรจะเป็นสโตร์

ที่ฝากข้อมูลคือพื้นที่ใด ๆ ที่เก็บค่าไว้ - สำหรับที่นี่ฉันเก็บไว้เป็นดัชนีอาร์เรย์ แต่อาจเป็นตำแหน่งหน่วยความจำเช่นกัน


1
"กฎของหัวแม่มือคือ: สำหรับค่าที่กำหนดที่จะใส่ถังจะต้องไม่ซ้ำกัน & ถอดออกได้จากค่าที่ควรจะเป็นร้านค้า" - สิ่งนี้อธิบายถึงฟังก์ชั่นแฮชที่สมบูรณ์แบบซึ่งมักจะเป็นไปได้สำหรับค่าสองสามร้อยหรือพันค่าที่รู้จักในเวลารวบรวม ส่วนใหญ่ตารางแฮชต้องจับชน นอกจากนี้ตารางแฮชมักจะจัดสรรพื้นที่สำหรับที่เก็บข้อมูลทั้งหมดไม่ว่าจะว่างเปล่าหรือไม่ในขณะที่โค้ดหลอกของคุณเอกสารcreate_extra_space_for_bucket()ขั้นตอนหนึ่งระหว่างการแทรกคีย์ใหม่ ถังอาจเป็นตัวชี้
Tony Delroy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.