Zastanawiałem się tylko, dlaczego w klasach używa się liczb pierwszych hashCode()
metodzie ? Na przykład, gdy używam Eclipse do generowania mojej hashCode()
metody, zawsze 31
używana jest liczba pierwsza :
public int hashCode() {
final int prime = 31;
//...
}
Bibliografia:
Oto dobry podkład na temat Hashcode i artykuł o tym, jak działa haszowanie, który znalazłem (C #, ale koncepcje są przenoszone): Eric Lippert's Guidelines and rules for GetHashCode ()
Odpowiedzi:
Ponieważ chcesz, aby liczba, przez którą mnożysz, i liczba segmentów, do których wstawiasz, miały ortogonalne czynniki pierwsze.
Załóżmy, że jest 8 pojemników do włożenia. Jeśli liczba, której używasz do pomnożenia, jest wielokrotnością 8, to przedział wstawiony do zostanie określony tylko przez najmniej znaczący wpis (ten w ogóle nie został pomnożony). Podobne wpisy będą się zderzać. Nie nadaje się do funkcji skrótu.
31 jest na tyle dużą liczbą pierwszą, że jest mało prawdopodobne, aby liczba zasobników była przez nią podzielna (a w rzeczywistości nowoczesne implementacje HashMap w języku Java utrzymują liczbę zasobników do potęgi 2).
źródło
(x*8 + y) % 8 = (x*8) % 8 + y % 8 = 0 + y % 8 = y % 8
Liczby pierwsze są wybierane tak, aby jak najlepiej rozdzielać dane między zasobniki mieszania. Jeśli rozkład danych wejściowych jest losowy i równomiernie rozłożony, wówczas wybór kodu skrótu / modułu nie ma znaczenia. Ma to wpływ tylko wtedy, gdy na danych wejściowych występuje określony wzorzec.
Dzieje się tak często w przypadku lokalizacji pamięci. Na przykład wszystkie 32-bitowe liczby całkowite są wyrównane do adresów podzielnych przez 4. Zapoznaj się z poniższą tabelą, aby zwizualizować skutki zastosowania modułu pierwszego względem modułu innego niż pierwszy:
Zwróć uwagę na prawie idealny rozkład, gdy używasz modułu podstawowego w porównaniu z modułem innym niż pierwotny.
Jednakże, chociaż powyższy przykład jest w dużej mierze zmyślony, ogólną zasadą jest, że gdy mamy do czynienia z układem danych wejściowych , użycie modułu liczb pierwszych da najlepszy rozkład.
źródło
Co jest warte, Effective Java 2nd Edition rezygnuje z matematyki i po prostu powiedz, że powodem wyboru 31 jest:
Oto pełny cytat z punktu 9: Zawsze zastępuj,
hashCode
gdy zastępujeszequals
:Raczej w uproszczeniu można powiedzieć, że użycie mnożnika z licznymi dzielnikami spowoduje więcej zderzeń z mieszaniem . Ponieważ w celu efektywnego haszowania chcemy zminimalizować liczbę kolizji, staramy się używać mnożnika, który ma mniej dzielników. Liczba pierwsza z definicji ma dokładnie dwa odrębne, dodatnie dzielniki.
Powiązane pytania
źródło
3, 5, 17, 257, 65537
lub 2 ^ N - 1 ( liczb pierwszych Mersenne )3, 7, 31, 127, 8191, 131071, 524287, 2147483647
. Jednak31
(a nie, powiedzmy,127
) jest wybrany.Słyszałem, że wybrano 31 tak, aby kompilator mógł zoptymalizować mnożenie do 5 bitów z przesunięciem w lewo, a następnie odjąć wartość.
źródło
mov reg1, reg2-shl reg1,5-sub reg1,reg2
można wykonać w 2 cyklach. (mov to tylko zmiana nazwy i trwa 0 cykli).Oto cytat nieco bliżej źródła.
Sprowadza się do:
źródło
Najpierw obliczasz wartość skrótu modulo 2 ^ 32 (rozmiar an
int
), więc chcesz, aby coś było względnie pierwsze na 2 ^ 32 (względnie pierwsze oznacza, że nie ma wspólnych dzielników). Każda nieparzysta liczba wystarczyłaby do tego.Następnie dla danej tablicy skrótów indeks jest zwykle obliczany z wartości skrótu modulo rozmiar tablicy mieszającej, więc potrzebujesz czegoś, co jest względnie pierwsze w stosunku do rozmiaru tablicy skrótów. Z tego powodu często rozmiary tabel skrótów są wybierane jako liczby pierwsze. W przypadku Javy implementacja Sun zapewnia, że rozmiar jest zawsze potęgą dwójki, więc i tutaj wystarczyłaby liczba nieparzysta. Istnieje również dodatkowe masowanie kluczy mieszających, aby jeszcze bardziej ograniczyć kolizje.
Zły efekt, jeśli tablica skrótów i mnożnik miały wspólny czynnik,
n
może polegać na tym, że w pewnych okolicznościach zostanie użyty tylko 1 / n wpisów w tablicy skrótów.źródło
Powodem, dla którego używane są liczby pierwsze, jest zminimalizowanie kolizji, gdy dane wykazują określone wzorce.
Po pierwsze: jeśli dane są losowe, nie ma potrzeby stosowania liczby pierwszej, możesz wykonać operację modowania na dowolnej liczbie i będziesz mieć taką samą liczbę kolizji dla każdej możliwej wartości modułu.
Ale kiedy dane nie są przypadkowe, dzieją się dziwne rzeczy. Na przykład rozważ dane liczbowe, które zawsze są wielokrotnością 10.
Jeśli użyjemy mod 4, znajdziemy:
10 mod 4 = 2
20 mod 4 = 0
30 mod 4 = 2
40 mod 4 = 0
50 mod 4 = 2
Zatem z 3 możliwych wartości modułu (0,1,2,3) tylko 0 i 2 będą miały kolizje, czyli źle.
Jeśli użyjemy liczby pierwszej takiej jak 7:
10 mod 7 = 3
20 mod 7 = 6
30 mod 7 = 2
40 mod 7 = 4
50 mod 7 = 1
itp
Zwracamy również uwagę, że 5 nie jest dobrym wyborem, ale 5 jest liczbą pierwszą, ponieważ wszystkie nasze klucze są wielokrotnością 5. Oznacza to, że musimy wybrać liczbę pierwszą, która nie dzieli naszych kluczy, wybór dużej liczby pierwszej to zwykle wystarczy.
Zatem błędem jest powtarzalność, powodem używania liczb pierwszych jest zneutralizowanie wpływu wzorców w kluczach na rozkład kolizji funkcji skrótu.
źródło
31 jest również specyficzne dla Java HashMap, która używa typu danych int jako hash. Zatem maksymalna pojemność 2 ^ 32. Nie ma sensu używać większych liczb pierwszych Fermata lub Mersenne'a.
źródło
Generalnie pomaga to osiągnąć bardziej równomierne rozłożenie danych między zasobnikami mieszania, szczególnie w przypadku kluczy o niskiej entropii.
źródło