gitweb on Svarog

projekti pod git sistemom za održavanje verzija -- projects under the git version control system
Hash, doterivanje tekstualnog materijala
[spa2-materijali.git] / Hash / primeri / hash.txt
1 % Hash funkcije - pripremni materijal
2 % SPA2, DMI-PMF-UNS, www.dmi.uns.ac.rs
3 % 2019
5 Heš kod i jednakost u programskom jeziku Java
6 =============================================
8 U programskom jeziku Java u svakoj klasi mogu (i često i trebaju) da
9 postoje metodi
11 public int hashCode()
12 public boolean equals(Object o)
14 Oni omogućavaju vrlo efikasne operacije nad velikim skupovima
15 objekata, ali je bitno da uzajamno imaju konzistentno ponašanje.
17 Tačnije, ako su dva objekta jednaka u skladu sa `equals` tada im i heš
18 vrednost koju vraća `hashCode` mora biti ista. Obrnutno ne mora da važi,
19 odnosno može postojati neograničeni broj različitih objekata koji
20 imaju isti heš kod. Isto tako ako dva objekta imaju različih heš, tada
21 oni ne bi smeli biti jednaki kad se pita `equals`. No i ovde ne važi
22 obrnuto, odnosno ako su dva objekta različiti, heš kod im ne mora biti
23 različit.
25 Zbog ovih podrazumevanih odnosa je jako bitno da se ova dva metoda
26 pišu zajedno, odnosno da se ne menja samo jedan bez promena u drugom.
28 U opštem slučaju ako se ovo ponašanje ne promeni reimplementacijom
29 ovih metoda, objekti u Javi će za ova dva metoda da vraćaju vrednosti
30 koje se odnose na sam pokazivač u memoriji. Odnosno dva objekta su
31 isti samo ako se bukvalno isti objekat u memoriji, bez obzira na to da
32 li imaju isti sadržaj. Isto tako će sam heš kod biti generisan od
33 vrednosti samog pokazivača, a ne od sadržaja objekta. Jako često ovo
34 nije poželjno ponašanje, te se zbog toga moraju pisati svoji metodi.
36 Heš kod je uvek poželjno pisati tako da se dobija što više različitih
37 vrednosti za različite instance objekata. U idealnom slučaju bi se za
38 svaki različit objekat dobijao različit heš kod, međutim to u praksi
39 često nije moguće.
41 Klasa `String` ima redefinisana oba ova metoda. Heš kod koji se dobija
42 za ovu klasu je veoma kvalitetan i uglavnom se vredi osloniti na njega
43 za polja ovog tipa.
45 Nažalost, nizovi nemaju nijednu od ove dve funkcije definisane kako
46 treba, pa kada se radi sa njima, mora se uložiti napor da se
47 pojedinačni elementi u nizu porede, odnosno hešuju.
50 Ocene kvaliteta funkcije
51 ========================
53 Kvalitetna heš funkcija ravnomerno raspoređuje elemente po celoj tabeli,
54 odnosno dobijamo podskupove sličnih veličina.
56 Za poređenje kvaliteta heš funkcija se mogu koristi različite mere. Za
57 ove potrebe je napravljena klasa `StatSet` koja implementira klasični
58 Java skup (zapravo u pozadini i koristi `HashSet` za skladištenje podataka),
59 ali osim toga vodi računa i o tome kako su se elementi rasporedili i
60 može da da informacije o tome. Najlakše se dolazi do većine ovih
61 informacija metodom `printStats`.
63 Prva mera kvaliteta je broj različih heš vrednosti elemenata u skupu,
64 a ispisuje se zajedno sa procentom u odnosu na ukupni broj elemenata
65 u skupu. U idealnom slučaju je ovo 100%, odnosno svi elementi imaju
66 različite vrednosti, mada nekad u realnim primenama ovo nije
67 lako izvodljivo.
69 Sledeći bitan uvid u raspodelu elemenata je koliko su veliki podskupovi,
70 odnosno koliko su veliki lanci pretraga pri operaciama nad skupom.
71 Ispisuje se prosečna vrednost dužine lanaca, ali i standarno odstupanje
72 (npr 2.12 +- 1.11). U idealnoj raspodeli svi lanci su dužine tačno 1.
74 Osim ovog podatka se ispisuje i dužina najdužeg lanca u skupu, koja
75 daje predstavu o tome koliko je pretraživanje u najgorem slučaju. Ovaj
76 broj takođe treba da je što manji.
78 Program ispisuje i vrednosti $chi^2$ testa, o čemu postoji još
79 komentara na kraju ovog dokumenta, ali se inače neće dublje
80 razmatrati, nego se ostavlja zainteresovanima da pogledaju.
82 Primeri
83 =======
85 U sledećim primerima će se razmatrati različiti tipovi podataka i kako se oni
86 efikasno mogu skladištiti u heš tabelama. Podaci se smeštaju u instancu `StatSet`
87 koja nam daje statistike o njihovoj raspodeli. Svi dati primeri se mogu pokretati
88 iz odgovarajućih klasa pošto postoje `main` metodi koji pokreću program `TestHash`
89 sa odgovarajućim parametrima.
92 Korišćenje kancelarija
93 ----------------------
95 Treba da čuvamo podatke o korišćenju kancelarija u toku jednog dana i
96 da omogućimo što bržu proveru da li je neka konkretna osoba koristila
97 neku konkretnu kancelariju. Podaci vezuju prezimena za identifikacione
98 brojeve soba. Naravno moguće je da više ljudi koristilo istu
99 kancelariju, a moguće je i da je neko više puta koristio istu -- ovo
100 će se u tabeli skladištiti samo jedan put, pošto nas samo zanima ko je
101 bio unutra, a ne koliko puta.
103 Uzmimo sledeću definiciju klase
105 ```Java
106 class Kancelarija {
107 private int broj;
108 private String prezime;
109 ....
111 ```
113 U datim fajlovima nema više od~100 prostorija, a i postoji samo
114 oko~60 različitih prezimena (tj zaposlenih).
116 Broj sobe tipa `int` i deluje kao "očigledan" kandidat za heš kod.
117 Validno bi bilo da se ova informacija vraća kao heš kod:
119 ```Java
120 public int hashCode() {
121 return broj;
123 ```
125 Pogledajmo primere učitavanja većeg broja podataka u heš tabele sa
126 različitim brojem podskupova, ako imamo tako definisanu heš funkciju:
128 ```
129 Number of elements: 4885
130 Different values: 100 ( 2.05 %)
131 Avg. search chain len.: 48.85 +- 2.99
132 Longest search chain: 56
133 ```
135 Budući da ima samo 100 soba, može biti i samo 100 različitih heš vrednosti, te
136 rezultati nisu zadovoljavajući.
138 Slično važi i za prezimena, budući da ih ima samo
139 šestdesetak, bez obzira na koliko je dobar heš kod u klasi
140 `String`, možemo imati samo šestdesetak različitih
141 rezultata, odnosno dobijamo još gore rezultate. Bitna
142 razlika u odnosu na prethodnu verziju gde smo koristili samo
143 `int` koji je primitivni tip, ovde se mora voditi računa i o
144 tome da samo polje može biti `null`, tj ne sme se desiti da
145 `hashCode` baci izuzetak zbog ovoga.
147 ```Java
148 public int hashCode() {
149 if (prezime != null) {
150 return prezime.hashCode();
151 } else {
152 return 0;
155 ```
157 ```
158 Number of elements: 4885
159 Different values: 61 ( 1.25 %)
160 Avg. search chain len.: 80.08 +- 3.96
161 Longest search chain: 89
162 ```
164 Očigledno je potrebno kombinovati ove dve vrednosti, odnosno
165 iskoristiti sve različitosti u podacima koje postoje..
166 Možemo ih jednostavno samo sabrati:
168 ```Java
169 public int hashCode() {
170 int rez = 0;
171 if (prezime != null) {
172 rez += prezime.hashCode();
174 rez += broj;
175 return rez;
177 ```
179 Dobijamo izuzetne rezultate u testovima, pošto su sve vrednosti različite:
181 ```
182 Number of elements: 4885
183 Different values: 4885 (100.00 %)
184 Avg. search chain len.: 1.00 +- 0.00
185 Longest search chain: 1
186 ```
190 Istovremeno je bitno da se i `equals` definiše da koristi ista polja.
191 Metod zahteva da parametar bude tipa `Object`, pa uvek moramo da krenemo
192 od toga. Najjednostavnija varijanta zahteva da proverimo da li je
193 prosledjeno nešto što je iste klase kao i naš objekat, pa tek
194 onda možemo da menjamo tip i da poredimo polja.
196 ```Java
197 public boolean equals(Object o) {
198 // proveravamo da li je objekat sa
199 // kojim se poredimo Kancelarija
200 if (o instanceof Kancelarija) {
201 // pretvaramo objekat u kancelariju
202 Kancelarija k2 = (Kancelarija) o;
203 // poredimo polja
204 if (prezime.equals(k2.prezime) && broj == k2.broj) {
205 return true;
208 return false;
210 ```
212 Međutim potpuna verzija `equals` metoda treba da proveri da li je u
213 pitanju baš ista klasa kao naša (moguće da je neki naslednik koji
214 tehnički ne mora biti isti), te da tu uzme u obzir i da je moguće da
215 nam je prosleđen `null` što nikako ne može biti isto kao i naš
216 objekat. Dalje se može ubrzati postupak proverom da li su u pitanju
217 pokazivači na isto mesto pošto nema potrebe za proverama onda. Takođe
218 se preporučuje da se polja porede jedno po jedno, pošto je kod
219 pregledniji i lakše se menja, nego kad se kombinuju upiti kao gore.
221 Potpuna verzija bi izgledala ovako:
223 ```Java
224 public boolean equals(Object o) {
225 // Objekat je identican
226 if (this == o) {
227 return true;
229 // Null je uvek razlicit
230 if (o == null) {
231 return false;
233 // Ako su klase razlicite, objekti ne mogu bili jednaki
234 if (getClass() != o.getClass()) {
235 return false;
238 // pretvaramo objekat u kancelariju
239 Kancelarija k2 = (Kancelarija) o;
241 // Prvo proveravamo broj
242 if (broj != k2.broj) {
243 return false;
246 // A potom prezime
247 if (!Objects.equals(prezime, k2.prezime)) {
248 return false;
251 // Proverili smo polja i sva su jednaka
252 return true;
254 ```
256 Razmotrimo sledeću modifikaciju klase: recimo da se i dalje želi samo
257 pratiti u tabeli ko je bio u kojoj kancelariji, ali da su negde već
258 definisani objekti u kojima se skladišti i vreme poslednjeg
259 pristupanja, ili recimo neki dodatni opis zašto je neko bio u
260 kancelariji.
262 ```Java
263 class KancelarijaVreme {
264 private int broj;
265 private String prezime;
267 private int pristupDan, pristupSat;
269 private String komentar;
271 ```
273 Budući da za naše potrebe nije bitno kad je pristupano niti zbog čega,
274 i dalje bi trebali da koristimo iste kodove za `equals` i za
275 `hashCode` pošto nam je dosta da ubacimo samo jedan objekat za svako
276 pojedinačno korišćenje. Negde druge se naravno oni mogu koristiti za
277 detaljije zapise, ali nas to trenutno ne interesuje.
280 Gadjanje mete
281 -------------
283 Potrebno je da čuvamo podatke o rezultatima gadjanja mete. U nizu se
284 nalazi do 20 brojeva izmedju i uključujući 0 i 10.
286 Recimo da je klasa definisana na sledeći način:
288 ```Java
289 public class Gadjanje {
290 private int[] rezultati;
291 private static int MAX_DUZ = 20;
292 ...
294 ```
296 Pošto je moguće (i vrlo verovatno zapravo) da neko nije gađao svih 20
297 puta, u podacima će biti nizovi različitih dužina. U skladu sa tim je
298 i format za unos takav da je prvi broj u redu zapravo broj gađanja,
299 koji određuje veličinu niza.
301 Na početku je već bilo napomenuto da nizovi nemaju `hashCode` i
302 `equals` implementirane na načine koji bi odgovarao ovoj situaciji,
303 tako da se mora implentirati pojedinačno poređenje elemenata, naročito
304 kod ovakvih situacija sa dodatnim značenjima elemenata.
306 Funkcija jednakosti bi izgledala ovako:
308 ```Java
309 public boolean equals(Object o) {
310 // Objekat je identican
311 if (this == o) {
312 return true;
314 // Null je uvek razlicit
315 if (o == null) {
316 return false;
318 // Ako su klase razlicite, objekti ne mogu bili jednaki
319 if (getClass() != o.getClass()) {
320 return false;
323 Gadjanje o2 = (Gadjanje) o;
325 // proveravamo da li su polja null pre dalje provere
326 if (rezultati == null && o2.rezultati != null) {
327 return false;
329 if (rezultati != null && o2.rezultati == null) {
330 return false;
333 // ako u obe instance nije null, poredimo delove
334 if (rezultati != null && o2.rezultati != null) {
335 // proverimo duzinu.
336 if (o2.rezultati.length != rezultati.length) {
337 return false;
339 // ako je ista duzina proveravamo elemente
340 for (int i = 0; i < rezultati.length; i++) {
341 if (o2.rezultati[i] != rezultati[i]) {
342 // cim je nesto razlicito nisu isti
343 return false;
347 // ako nije bilo razlika, vracamo da je sve ok
348 return true;
350 ```
352 “Očigledno” rešenje je samo sabrati sve ove vrednosti i vratiti broj. Međutim
353 ovakvo rešenje u najboljem slučaju daje maksimalno 200 različitih vrednosti,
354 pri čemu bi se očekivala i dosta neravnomerna raspodela (sume pri sredini su
355 daleko verovatnije od najviših i najnižih). Na konkretnim podacima su vrednosti
356 u tabeli nešto gore od očekivanih 200 vrednosti:
358 ```
359 Number of elements: 9049
360 Different values: 139 ( 1.54 %)
361 Avg. search chain len.: 65.10 +- 37.98
362 Longest search chain: 118
363 ```
365 U ovakvoj strukturi, pozicija u nizu bi trebalo da bude značajna za
366 krajnje heš vrednosti. Da bi se to postiglo jedan pristup je množenje
367 brojeva sa svojim pozicijama ili različitim (prostim) brojevima.
369 Za ovaj primer u kome je dužina niza raznolika i ima više "početnih"
370 elemenata je dodatno pogodno da se težine primenjuju unazad, pa recimo
371 možemo uvesti da prvi element ima težinu 21, a poslednji u nizu od 20
372 elemenata težinu 2.
374 ```Java
375 public int hashCode() {
376 int rez = 0;
377 if (rezultati != null) {
378 for (int i = 0; i < rezultati.length; i++) {
379 rez += rezultati[i] * (MAX_DUZ + 1 - i);
382 return rez;
384 ```
386 Vrednosti su dosta bolje, iako još uvek nisu na viskom nivou.
388 ```
389 Number of elements: 9049
390 Different values: 1411 (15.59 %)
391 Avg. search chain len.: 6.41 +- 3.89
392 Longest search chain: 20
393 ```
395 Problem je zapravo što brojevi nisu dovoljno različiti. Pošto znamo da su
396 rezultati najviše 10, možemo to iskoristiti da u suštini sastavimo višecifreni broj
397 na sledeći način:
399 ```Java
400 for (int i = 0; i < rezultati.length; i++) {
401 rez = (rez + rezultati[i]) * 10 ;
403 ```
405 Ovim smo dobili dosta bolje vrednosti:
407 ```
408 Number of elements: 9049
409 Different values: 8911 (98.47 %)
410 Avg. search chain len.: 1.02 +- 0.14
411 Longest search chain: 4
412 ```
414 Možemo uočiti i sledeće - ako na početku nekog niza stoje 0, one će biti ignorisane
415 u ukupnom zbiru, te samim tim možemo imati različite nizove koji vraćaju iste
416 vrednosti. Ovo možemo ispraviti tako što ćemo pojedinačne rezultate povećavati
417 za 1, odnosno pomeriti ih iz opsega 0-10 na 1-11. U skladu sa tim treba da
418 povećamo i broj kojim množimo na 11.
420 ```Java
421 public int hashCode() {
422 int rez = 0;
423 if (rezultati != null) {
424 for (int i = 0; i < rezultati.length; i++) {
425 rez = (rez + rezultati[i] + 1) * 11 ;
428 return rez;
430 ```
432 ```
433 Number of elements: 9049
434 Different values: 9049 (100.00 %)
435 Avg. search chain len.: 1.00 +- 0.00
436 Longest search chain: 1
437 ```
440 Tabla za igru XO
441 ----------------
443 Zadatak nam je da efikasno čuvamo podatke o pozicijama u igri XO,
444 odnosno matrice 3x3, u kojima su vrednosti -1 za "X", 0 za prazno
445 polje i 1 za "O".
447 Npr:
448 ```
449 1 0 -1 O _ X
450 1 1 -1 O O X
451 -1 0 1 X _ O
452 ```
453 predstavlja poziciju u kojoj je ``O'' pobedio/la.
455 Polja klase bi se mogla definisati na sledeći način:
457 ```Java
458 public class XO {
459 public static final int DIM = 3;
460 private final int[][] tabla = new int[DIM][DIM];
461 ....
463 ```
465 Poredjenje ovakvih objekata bi naravno trebalo da pojedinačno ispituje
466 elemente matrice, na primer:
468 ```Java
469 public boolean equals(Object o) {
470 // Objekat je identican
471 if (this == o) {
472 return true;
474 // Null je uvek razlicit
475 if (o == null) {
476 return false;
478 // Ako su klase razlicite, objekti ne mogu bili jednaki
479 if (getClass() != o.getClass()) {
480 return false;
483 // menjamo tip da mozemo da poredimo
484 XO o2 = (XO) o;
485 // posto je u ovoj klasi uvek inicijalizovano polje table
486 // i uvek je DIM x DIM ne moramo proveravati null
487 for (int i = 0; i < DIM; i++) {
488 for (int j = 0; j < DIM; j++) {
489 if (o2.tabla[i][j] != tabla[i][j]) {
490 return false;
494 return true;
496 ```
498 Za ovaj primer vredi prvo razmotriti da neke relativno očigledne
499 tehnike nisu efikasne: sumiranje ovakve matrice daje vrednosti do plus
500 ili minus 9, odnosno ima samo 19 različitih vrednosti, što bi
501 predstavljalo jako lošu pokrivenost tabele. Dodatno, vrlo često će
502 rezultat biti 0 i uopšteno će biti više tablica koje su bliže
503 "sredini" rezultatom, što opet dovodi do loše ravnomernosti
504 raspoređivanja elemenata.
506 Dakle ovako definisan metod za heš će davati dosta loše rezultate:
508 ```Java
509 public int hashCode() {
510 int rez = 0;
511 for (int i = 0; i < DIM; i++) {
512 for (int j = 0; j < DIM; j++) {
513 rez += tabla[i][j];
516 return rez;
518 ```
520 ```
521 Number of elements: 4424
522 Different values: 18 ( 0.41 %)
523 Avg. search chain len.: 245.78 +- 253.74
524 Longest search chain: 674
525 ```
527 Efikasnost se u ovakvoj strukturi dobija davanjem "težine"
528 elementima. Odnosno ne treba gledati jednako ako je "1" u gornjem
529 levom uglu i u sredini. Npr u jednostavnijim varijantama se može zbir
530 iz svakog reda množiti različitim brojem. Još bolji rezultati se
531 dobijaju uvodjenjem još detaljnijih "težina": pri sumiranju reda
532 množiti različito, ili čak imati specijalne proste brojeve za svako
533 polje ili uvoditi dodatne operacije.
535 Takodje treba uzeti u obzir i to da sabiranje pozitivnih i negativnih
536 vrednosti može da dovede do "međusobnog poništavanja", te da nula
537 vrednosti ne pomažu zbiru, naročito kad se uvedu težine koje se množe
538 sa vrednostima polja. Jedan način da se ovo uradi u konkretnom slučaju
539 je da se pri računanju heša vrednosti u matrici sabiraju sa 2, pa time
540 imamo 1,2,3 umesto -1,0,1, što dovodi do veće različitosti.
542 Jedno od mogućih rešenja koje daje dobre rezultate je zapravo jako
543 slično onome viđeno kod niza - ako stalno množimo sa brojem koji je
544 veći od mogućih vrednosti jednog elementa, automatski su sva mesta
545 pomnožena različitim koeficijentom. Ovo možemo kombinovati sa
546 pomeranjem pojedinačnih elemenata na pozitivne vrednosti, mada se
547 dobri rezultati mogu postići i bez toga.
549 ```Java
550 public int hashCode() {
551 int rez = 0;
552 int koef = 3;
553 for (int i = 0; i < DIM; i++) {
554 for (int j = 0; j < DIM; j++) {
555 rez = koef * (rez + (tabla[i][j] + 2));
558 return rez;
560 ```
562 ```
563 Number of elements: 4424
564 Different values: 4424 (100.00 %)
565 Avg. search chain len.: 1.00 +- 0.00
566 Longest search chain: 1
567 ```
570 Dodatne napomene
571 ================
573 Hi kvadrat ocena kvaliteta heš funkcije
574 -----------------------------------
576 Nešto komplikovanija mera je $\chi^2$ \emph{(hi-kvadrat)} test.
577 \begin{equation}
578 \chi ^2 = \sum_{i=1}^{n} \frac{(K_i - E)^2}{E}
579 \end{equation}
581 gde je $K_i$ - broj elemenata u podskupu $i$, a $E$ - očekivan broj
582 elemenata u podskupu, odnosno idealna vrednost koja je jednaka
583 količniku ukupnog broja elemenata i broja skupova.
585 Iako deluje relativno komplikovano, ovo nam zapravo daje dobru ocenu
586 koliko su (ne)ravnomerno raspoređeni elementi. U idealno slučaju bi
587 sve $K_i$ vrednosti bile jednake sa $E$ i imali bi da je vrednost
588 testa nula. U opštem slučaju se naravno javljaju odstupanja od
589 proseka, a pošto kvadriramo udaljenosti, $\chi^2$ brzo raste ukoliko
590 ima većih anomalija.
592 Program daje vrednosti za ovaj test i u odnosu na idealan
593 slučaj kada je broj lanaca/podskupova jednak sa brojem
594 elemenata, kao i za realan broj podskupova (tj različitih
595 vrednosti elemenata).
597 Ispitivanje različitosti dobijenih podataka
598 -------------------------------------------
600 Kada se dobiju nepoznati podaci koje treba obraditi i za njih odrediti
601 heš funkcije, nekad vredi prvo probati saznati više o različitostima
602 dobijenih podataka.
604 Kod primera za korišćenje kancelarija, na primer, definisali smo da je
605 `hashCode` zavistan samo od jednog od polja, i samim tim kad smo
606 pokrenuli program smo saznali tačno koliko ima različitih imena i
607 koliko ima različitih brojeva.
609 Isti pristup se može iskoristiti i u drugim situacijama, što nam može
610 dati bolje ideje kako da postavimo različite koeficijente i na koja
611 polja možda ima smisla staviti više "težine" u samom hešu.
Svarog.pmf.uns.ac.rs/gitweb maintanance Doni Pracner