ทำไมต้องใช้หมายเลขเฉพาะใน hashCode


174

ฉันแค่สงสัยว่าทำไมช่วงเวลาดังกล่าวถูกใช้ในวิธีการเรียนhashCode()? ตัวอย่างเช่นเมื่อใช้ Eclipse เพื่อสร้างhashCode()วิธีการของฉันจะมีจำนวนเฉพาะที่31ใช้เสมอ:

public int hashCode() {
     final int prime = 31;
     //...
}

อ้างอิง:

นี่เป็นไพรเมอร์ที่ดีใน Hashcode และบทความเกี่ยวกับวิธีการแฮ็กที่ทำงานที่ฉันพบ (C # แต่แนวคิดสามารถถ่ายโอนได้): แนวทางและกฎของ Eric Lippert สำหรับ GetHashCode ()



นี้จะมากหรือน้อยซ้ำคำถามstackoverflow.com/questions/1145217/...
Hans-Peter Störr

1
โปรดตรวจสอบคำตอบของฉันได้ที่stackoverflow.com/questions/1145217/ ..มันเกี่ยวข้องกับคุณสมบัติของชื่อพหุนามมากกว่าหนึ่งฟิลด์ (ไม่ใช่เสียงกริ่ง!) ดังนั้นตัวเลขหลัก
TT_

คำตอบ:


104

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

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

31 เป็นนายกที่มีขนาดใหญ่พอที่จำนวนของถังไม่น่าจะแบ่งได้ (และในความเป็นจริงการใช้งาน Java HashMap ที่ทันสมัยทำให้จำนวนของถังเป็นจำนวน 2)


9
จากนั้นฟังก์ชันแฮชที่คูณด้วย 31 จะทำงานได้ไม่ดีที่สุด อย่างไรก็ตามฉันจะพิจารณาการใช้งานตารางแฮชที่ได้รับการออกแบบมาไม่ดีโดยพิจารณาว่า 31 เป็นตัวคูณได้อย่างไร
ILMTitan

11
ดังนั้น 31 ถูกเลือกตามสมมติฐานที่ว่าตัวสร้างตารางแฮชรู้ว่า 31 มักใช้ในรหัสแฮช
Steve Kuo

3
31 ได้รับการคัดเลือกขึ้นอยู่กับความคิดที่ว่าการใช้งานส่วนใหญ่มี factorizations ของจำนวนค่อนข้างน้อย 2s, 3s และ 5s โดยปกติ มันอาจเริ่มต้นที่ 10 และเติบโต 3X เมื่อมันเต็มเกินไป ขนาดนั้นไม่ค่อยสุ่มทั้งหมด และแม้ว่ามันจะเป็น 30/31 ก็ไม่ได้เลวร้ายอะไรสำหรับการมีอัลกอริธึมแฮชที่ซิงค์กัน นอกจากนี้ยังอาจคำนวณได้ง่ายตามที่ผู้อื่นระบุไว้
ILMTitan

8
กล่าวอีกนัยหนึ่ง ... เราจำเป็นต้องรู้อะไรบางอย่างเกี่ยวกับชุดของค่าอินพุตและกฎเกณฑ์ของชุดเพื่อที่จะเขียนฟังก์ชั่นที่ออกแบบมาเพื่อตัดค่าเหล่านั้นออกจากกฎเกณฑ์เหล่านั้นดังนั้นค่าในชุดไม่ชนกัน ถังกัญชา การคูณ / การหาร / การโมดูโลด้วยจำนวนเฉพาะนั้นจะส่งผลกระทบต่อเพราะถ้าคุณมี LOOP กับ X-items และคุณกระโดด Y- ช่องว่างในลูปคุณจะไม่ย้อนกลับไปที่จุดเดิมจนกระทั่ง X กลายเป็น Y เนื่องจาก X มักเป็นเลขคู่หรือกำลังสองดังนั้นคุณต้อง Y เป็นจำนวนเฉพาะดังนั้น X + X + X ... ไม่ใช่ปัจจัยของ Y ดังนั้น 31 ปี! : /
Triynko

3
@FrankQ มันเป็นธรรมชาติของคณิตศาสตร์เลขคณิต (x*8 + y) % 8 = (x*8) % 8 + y % 8 = 0 + y % 8 = y % 8
ILMTitan

135

มีการเลือกหมายเลขเฉพาะเพื่อแจกจ่ายข้อมูลที่ดีที่สุดในกลุ่มถังแฮช หากการกระจายของอินพุตเป็นแบบสุ่มและกระจายอย่างสม่ำเสมอการเลือก hash code / modulus นั้นไม่สำคัญ มันมีผลกระทบเฉพาะเมื่อมีรูปแบบบางอย่างไปยังอินพุต

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

Input       Modulo 8    Modulo 7
0           0           0
4           4           4
8           0           1
12          4           5
16          0           2
20          4           6
24          0           3
28          4           0

สังเกตุการกระจายตัวเกือบสมบูรณ์แบบเมื่อใช้โมดูลัสหลัก

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


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

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

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

29

สำหรับสิ่งที่มีค่าควรมีผลบังคับใช้ Java 2nd Editionยกเว้นปัญหาคณิตศาสตร์และเพียงกล่าวว่าเหตุผลในการเลือก 31 คือ:

  • เพราะมันเป็นนายกที่แปลกและเป็น "ดั้งเดิม" ที่จะใช้เฉพาะช่วงเวลา
  • นอกจากนี้ยังมีพลังงานน้อยกว่าหนึ่งสองซึ่งอนุญาตให้เพิ่มประสิทธิภาพบิต

นี่คือใบเสนอราคาเต็มรูปแบบจากรายการ 9: แทนที่ทุกhashCodeครั้งเมื่อคุณแทนที่equals :

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

คุณสมบัติที่ดีของ 31 คือการคูณสามารถถูกแทนที่ด้วย shift ( §15.19 ) และการลบเพื่อประสิทธิภาพที่ดีขึ้น:

 31 * i == (i << 5) - i

VM สมัยใหม่ทำการเพิ่มประสิทธิภาพประเภทนี้โดยอัตโนมัติ


แม้ว่าสูตรในรายการนี้จะให้ฟังก์ชันแฮชที่ดีพอสมควร แต่ก็ไม่ได้ทำให้เกิดฟังก์ชั่นแฮชที่ล้ำสมัยและไลบรารีแพลตฟอร์ม Java ไม่ได้ให้ฟังก์ชั่นแฮชดังกล่าวในรีลีส 1.6 การเขียนฟังก์ชันแฮชนั้นเป็นหัวข้อการวิจัยที่ดีที่สุดสำหรับนักคณิตศาสตร์และนักวิทยาศาสตร์คอมพิวเตอร์ในเชิงทฤษฎี

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

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

คำถามที่เกี่ยวข้อง


4
เอ๊ะ แต่มีกำลังที่เหมาะสมหลายช่วงเวลาที่มีทั้ง2 ^ 1 + n (เรียกว่าช่วงเวลาที่แฟร์มาต์ ) คือ3, 5, 17, 257, 65537หรือ2 ^ n - 1 ( เซนเนเฉพาะ3, 7, 31, 127, 8191, 131071, 524287, 2147483647 ): อย่างไรก็ตาม31(และไม่ใช่พูด127) ก็เลือกใช้
Dmitry Bychenko

4
"เพราะมันเป็นนายกที่แปลกประหลาด" ... มีเพียงคนเดียวเท่านั้นที่สำคัญ: P
Martin Schneider

ฉันไม่ชอบข้อความที่ว่า "ชัดเจนน้อยลง แต่เป็นแบบดั้งเดิม" ใน "Effective Java" ถ้าเขาไม่ต้องการเข้าไปดูรายละเอียดทางคณิตศาสตร์เขาควรเขียนบางอย่างเช่น "มีเหตุผลทางคณิตศาสตร์ [คล้ายกัน" แทน วิธีการที่เขาเขียนเสียงเหมือนมีพื้นหลังทางประวัติศาสตร์เท่านั้น :(
Qw3ry

5

ฉันได้ยินมาว่ามีการเลือก 31 ตัวเพื่อให้คอมไพเลอร์สามารถปรับการคูณให้เหลือ 5 กะซ้ายจากนั้นจึงลบค่า


คอมไพเลอร์จะปรับวิธีนั้นได้อย่างไร x * 31 == x * 32-1 ไม่เป็นความจริงสำหรับ x afterall ทั้งหมด สิ่งที่คุณหมายถึงถูกเลื่อนไปทางซ้าย 5 (เท่ากับทวีคูณด้วย 32) แล้วลบค่าเดิม (x ในตัวอย่างของฉัน) ในขณะที่สิ่งนี้อาจเร็วกว่าการคูณ (อาจเป็นเพราะโปรเซสเซอร์ซีพียูรุ่นใหม่) แต่ก็มีปัจจัยที่สำคัญกว่าที่ต้องพิจารณาเมื่อเลือกการคูณสำหรับ haschcode (การกระจายตัวของค่าอินพุตไปยังที่เก็บข้อมูลที่เท่ากัน)
Grizzly

ทำการค้นหาเล็กน้อยนี่เป็นความเห็นทั่วไป
Steve Kuo

4
ความคิดเห็นทั่วไปไม่เกี่ยวข้อง
fractor

1
@Grizzly ก็คือเร็วกว่าคูณ IMul ​​มีเวลาหน่วงขั้นต่ำ 3 รอบใน cpu ที่ทันสมัย (ดูคู่มือหมอกของ agner) mov reg1, reg2-shl reg1,5-sub reg1,reg2สามารถทำงานได้ 2 รอบ (mov เป็นเพียงการเปลี่ยนชื่อและใช้เวลา 0 รอบ)
Johan

3

นี่คือการอ้างอิงที่ใกล้กับแหล่งที่มาเล็กน้อย

มันเดือดลงไปที่:

  • 31 เป็นนายกซึ่งช่วยลดการชน
  • 31 สร้างการกระจายที่ดีด้วย
  • การแลกเปลี่ยนที่เหมาะสมในความเร็ว

3

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

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

ผลกระทบที่เลวร้ายหากตารางแฮชและตัวคูณมีปัจจัยร่วมกันnอาจเป็นได้ว่าในบางกรณีจะใช้เพียง 1 / n รายการในตารางแฮช


2

เหตุผลที่ใช้ตัวเลขที่สำคัญคือการลดการชนเมื่อข้อมูลแสดงรูปแบบเฉพาะบางอย่าง

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

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

หากเราใช้ mod 4 เราจะพบ:

10 mod 4 = 2

20 mod 4 = 0

30 mod 4 = 2

40 mod 4 = 0

50 mod 4 = 2

ดังนั้นจากค่าที่เป็นไปได้ 3 ค่าของโมดูลัส (0,1,2,3) มีเพียง 0 และ 2 เท่านั้นที่จะมีการชนกันนั่นเป็นสิ่งที่ไม่ดี

หากเราใช้จำนวนเฉพาะเช่น 7:

10 mod 7 = 3

20 mod 7 = 6

30 mod 7 = 2

40 mod 7 = 4

50 mod 7 = 1

ฯลฯ

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

ดังนั้นการทำซ้ำในด้านของการทำซ้ำจึงมีการใช้จำนวนเฉพาะเพื่อแก้ไขผลกระทบของรูปแบบในคีย์ในการกระจายการชนของฟังก์ชันแฮช


1

31 ยังใช้เฉพาะกับ Java HashMap ซึ่งใช้ int เป็นชนิดข้อมูลแฮช ดังนั้นความจุสูงสุดของ 2 ^ 32 ไม่มีจุดใดในการใช้เฟนต์แฟร์มาต์หรือเซนเซอร์ที่มีขนาดใหญ่ขึ้น


0

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.