Навіщо використовувати просте число в hashCode?


174

Мені було просто цікаво, чому в hashCode()методі класу використовуються праймери? Наприклад, при використанні Eclipse для створення мого hashCode()методу завжди використовується найпросте число 31:

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

Список літератури:

Ось хороший буквар на Hashcode та стаття про те, як працює хеширование ( C #, але поняття можна передати): Керівництво Еріка Ліпперта та правила для GetHashCode ()



Це більш-менш дублікат питання stackoverflow.com/questions/1145217/… .
Ганс-Пітер Штерр

1
Будь ласка, перевірте мою відповідь на сайті stackoverflow.com/questions/1145217/… Це пов'язано з властивостями поліномів над полем (не кільцем!), Отже, прості числа.
TT_

Відповіді:


104

Тому що ви хочете, щоб число, на яке ви помножуєтесь, і кількість відер, які ви вставляєте, мати ортогональні прості множники.

Припустимо, у нього потрібно вставити 8 відер. Якщо число, яке ви використовуєте для множення, є деяким кратним 8, то відро, вставлене в нього, визначатиметься лише найменш значущим записом (тим, що не множиться взагалі). Подібні записи зіткнуться. Не добре для хеш-функції.

31 є достатньо великим прем'єр-мінімом, що кількість відра навряд чи буде поділена на нього (і насправді, сучасні реалізації Java HashMap утримують кількість відра до 2).


9
Тоді хеш-функція, помножена на 31, буде виконувати не оптимально. Однак я вважаю б реалізацію такої хеш-таблиці погано розробленою, враховуючи, наскільки поширений 31 як множник.
ILMTitan

11
Отже, 31 вибирається виходячи з припущення, що виконавці хеш-таблиць знають, що 31 зазвичай використовується в хеш-кодах?
Стів Куо

3
31 вибирається виходячи з ідеї, що більшість реалізацій мають факторизацію щодо невеликих прайменів. Зазвичай 2s, 3s і 5s. Він може початися в 10 і вирости в 3 рази, коли він буде занадто повним. Розмір рідко буває цілком випадковим. І навіть якби це було, 30/31 не є поганими шансами на те, що вони добре синхронізували хеш-алгоритми. Це також може бути легко обчислити, як заявили інші.
ILMTitan

8
Іншими словами ... нам потрібно знати щось про набір вхідних значень та закономірності набору, щоб написати функцію, розроблену для позбавлення їх від цих закономірностей, тому значення в наборі не стикаються однаково відрі хешу Помноження / ділення / модулювання на просте число досягає впливу, тому що якщо у вас є LOOP з X-елементами і ви переходите Y-пробіли в циклі, ви ніколи не повернетесь до того самого місця, поки X не стане фактором Y Оскільки X часто є парним числом або потужністю 2, то вам потрібно Y, щоб бути простим, тому X + X + X ... не є фактором Y, тому 31 yay! : /
Трайнко

3
@FrankQ. Це природа модульної арифметики. (x*8 + y) % 8 = (x*8) % 8 + y % 8 = 0 + y % 8 = y % 8
ILMTitan

135

Прості номери вибираються для найкращого розподілу даних між хеш-відрами. Якщо розподіл входів випадковий і рівномірно розподілений, то вибір хеш-коду / модуля значення не має. Це має вплив лише тоді, коли на входах є певна схема.

Це часто трапляється при роботі з місцями пам'яті. Наприклад, усі 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
Той же принцип. З точки зору вводу / виводу, хеш подається в модульну роботу хеш-таблиці. Думаю, справа в тому, що якщо ви помножите на прайси, ви отримаєте більше випадково розподілених входів до точки, де модуль навіть не має значення. Оскільки хеш-функція сприймає слабкість розподілу входів, роблячи їх менш регулярними, вони рідше стикаються, незалежно від модуля, який використовується для розміщення їх у відро.
Трайнко

9
Така відповідь дуже корисна, оскільки це як навчити когось рибалити, а не ловити одного за них. Це допомагає людям бачити та розуміти основоположний принцип використання праймерів для хешів ... який полягає в тому, щоб розподіляти входи нерегулярно, щоб вони рівномірно потрапляли у відра після модулювання :).
Трайнко

29

Для чого це варто, Ефективна Java 2nd Edition відмовляється від питання математики і просто скаже, що причиною вибору 31 є:

  • Тому що це дивна прем'єр-мінімум, і "традиційно" використовувати праймери
  • Це також одна менша, ніж потужність дві, що дозволяє розрядити оптимізацію

Ось повна цитата з пункту 9: Завжди переосмислюйте, hashCodeколи ви перекриєтеequals :

Значення 31 було вибрано, тому що це незвичайний прайм. Якби це було парне, а множення переповнене, інформація втрачалася б, оскільки множення на 2 еквівалентно зміщенню. Перевага використання прайме менш зрозуміла, але це традиційно.

Приємною властивістю 31 є те, що множення можна замінити зсувом ( §15.19 ) та відніманням для кращої продуктивності:

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

Сучасні віртуальні машини роблять подібну оптимізацію автоматично.


Хоча рецепт у цьому пункті дає досить хороші хеш-функції, він не дає найсучасніших хеш-функцій, а також бібліотеки платформи Java не надають таких хеш-функцій, як випуск 1.6. Написання таких хеш-функцій - тема дослідження, найкраще залишена математикам та теоретикам-комп’ютерам.

Можливо, наступний випуск платформи забезпечить сучасні хеш-функції для своїх класів та корисних методів, що дозволить середнім програмістам побудувати такі хеш-функції. Тим часом методи, описані в цьому пункті, повинні бути адекватними для більшості застосувань.

Скоріше спрощено, можна сказати, що використання множника з численними дільниками призведе до більшого хеш-зіткнення . Оскільки для ефективного хешування ми хочемо мінімізувати кількість зіткнень, ми намагаємось використовувати множник, який має менше дільників. Просте число за визначенням має рівно два чітких, позитивних дільника.

Пов'язані питання


4
Ех, але ви багато відповідних простих чисел , які є або 2 ^ п + 1 (так звані Ферма простих чисел ), тобто 3, 5, 17, 257, 65537або 2 ^ N - 1 ( простих чисел Мерсенна ): 3, 7, 31, 127, 8191, 131071, 524287, 2147483647. Однак 31(а не, скажімо, 127) вибрано.
Дмитро Биченко

4
"тому що це дивна прем'єра" ... є лише одна непарна прем'єра : P
Мартін Шнайдер

Мені не подобається формулювання "менш чітке, але воно є традиційним" в "Ефективній Java". Якщо він не хоче вникати в математичні деталі, він повинен написати щось на зразок "має [подібні] математичні причини". Те, як він пише, звучить так, що воно мало лише історичну історію :(
Qw3ry

5

Я чув, що 31 був обраний таким чином, щоб компілятор міг оптимізувати множення на ліворуч на 5 біт, а потім відняти значення.


як компілятор міг оптимізувати такий спосіб? x * 31 == x * 32-1 не відповідає всім x afterall. То, що ви мали на увазі, було зсувом ліворуч 5 (дорівнює множенню на 32), а потім відніманням вихідного значення (x у моєму прикладі). Хоча це може бути швидше, ніж множення (це, до речі, не для сучасних процесорів процесора), є більш важливі фактори, які слід враховувати при виборі множення на хеш-код (на думку спадає рівний розподіл вхідних значень на відра)
Grizzly

Трохи пошукайте, це досить поширена думка.
Стів Куо

4
Загальна думка не має значення.
фрактор

1
@Grizzly, то є швидше , ніж множення. IMul ​​має мінімальну затримку в 3 цикли на будь-якому сучасному процесорі. (див. посібники з туману на агнер) mov reg1, reg2-shl reg1,5-sub reg1,reg2можна виконати за два цикли. (mov - це лише перейменування та займає 0 циклів).
Йоган

3

Ось цитування трохи ближче до джерела.

Він зводиться до:

  • 31 є простим, що зменшує зіткнення
  • 31 дає хороший розподіл, с
  • розумний компроміс у швидкості

3

Спочатку ви обчислюєте значення хеш-модуля 2 ^ 32 (розмір an int), тож ви хочете щось порівняно просте до 2 ^ 32 (відносно простий означає, що немає загальних дільників). Будь-яке непарне число зробило б для цього.

Тоді для даної хеш-таблиці індекс зазвичай обчислюється з хеш-значення по модулю розміру хеш-таблиці, тому ви хочете щось, що є відносно простим до розміру хеш-таблиці. Часто з цієї причини розміри хеш-таблиць вибирають як прості числа. У випадку з Java реалізація Sun гарантує, що розмір завжди є двома, так що і непарне число також буде достатньо. Існує також додаткове масаж хеш-клавіш для подальшого обмеження зіткнень.

Поганий ефект, якби хеш-таблиця та множник мали спільний фактор, nможуть полягати в тому, що за певних обставин використовуються лише 1 / n записів у хеш-таблиці.


2

Причина застосування простих чисел - мінімізація зіткнень, коли дані демонструють певні закономірності.

Спочатку спочатку: Якщо дані випадкові, тоді не потрібно простого числа, ви можете виконати операцію з модом проти будь-якого числа, і у вас буде однакова кількість зіткнень для кожного можливого значення модуля.

Але коли дані не випадкові, то трапляються дивні речі. Наприклад, розглянемо числові дані, які завжди кратні 10.

Якщо ми використовуємо mod 4, ми знаходимо:

10 мод 4 = 2

20 мод 4 = 0

30 мод 4 = 2

40 мод 4 = 0

50 мод 4 = 2

Тож із 3 можливих значень модуля (0,1,2,3) зіткнення мають лише 0 і 2, що погано.

Якщо ми будемо використовувати просте число типу 7:

10 мод 7 = 3

20 мод 7 = 6

30 мод 7 = 2

40 мод 7 = 4

50 мод 7 = 1

тощо

Ми також зауважимо, що 5 не є вдалим вибором, але 5 - це головна причина, тому що всі наші ключі кратні 5. Це означає, що ми повинні вибрати просте число, яке не ділить наші ключі, вибираючи велике просте число. зазвичай достатньо.

Тому помилка на стороні повторюваності причин, що використовуються простими числами, - це нейтралізувати дію шаблонів у клавішах на розподіл зіткнень хеш-функції.


1

31 також характерний для Java HashMap, який використовує int як тип даних хеша. Таким чином, максимальна ємність 2 ^ 32. Немає сенсу використовувати більші прайми Ферма чи Мерсенна.


0

Зазвичай це допомагає досягти більш рівномірного розповсюдження ваших даних між хеш-відрами, особливо для ключів із низькою ентропією.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.