gitweb on Svarog

projekti pod git sistemom za održavanje verzija -- projects under the git version control system
dbad695a2334a77301dd403ecd89214fae269d04
[spa2-materijali.git] / Hash / primeri / hash.txt
1 % Hash funkcije - pripremni materijal
2 % SPA2, DMI-PMF-UNS, www.dmi.rs
3 % 2016
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.
46 Ocene kvaliteta funkcije
47 ========================
49 Kvalitetna heš funkcija ravnomerno raspoređuje elemente po celoj tabeli,
50 odnosno dobijamo podskupove sličnih veličina.
52 Za poređenje kvaliteta heš funkcija se mogu koristi različite mere. Za
53 ove potrebe je napravljena klasa `StatSet` koja implementira klasični
54 Java skup (zapravo u pozadini i koristi `HashSet` za skladištenje podataka),
55 ali osim toga vodi računa i o tome kako su se elementi rasporedili i
56 može da da informacije o tome. Najlakše se dolazi do većine ovih
57 informacija metodom `printStats`.
59 Prva mera kvaliteta je broj različih heš vrednosti elemenata u skupu,
60 a ispisuje se zajedno sa procentom u odnosu na ukupni broj elemenata
61 u skupu. U idealnom slučaju je ovo 100%, odnosno svi elementi imaju
62 različite vrednosti, mada nekad u realnim primenama ovo nije
63 lako izvodljivo.
65 Sledeći bitan uvid u raspodelu elemenata je koliko su veliki podskupovi,
66 odnosno koliko su veliki lanci pretraga pri operaciama nad skupom.
67 Ispisuje se prosečna vrednost dužine lanaca, ali i standarno odstupanje
68 (npr 2.12 +- 1.11). U idealnoj raspodeli svi lanci su dužine tačno 1.
70 Osim ovog podatka se ispisuje i dužina najdužeg lanca u skupu, koja
71 daje predstavu o tome koliko je pretraživanje u najgorem slučaju. Ovaj
72 broj takođe treba da je što manji.
75 Nešto komplikovanija mera je $\chi^2$ \emph{(hi-kvadrat)} test.
76 \begin{equation}
77 \chi ^2 = \sum_{i=1}^{n} \frac{(K_i - E)^2}{E}
78 \end{equation}
80 gde je $K_i$ - broj elemenata u podskupu $i$, a $E$ - očekivan broj
81 elemenata u podskupu, odnosno idealna vrednost koja je jednaka
82 količniku ukupnog broja elemenata i broja skupova.
84 Iako deluje relativno komplikovano, ovo nam zapravo daje dobru ocenu
85 koliko su (ne)ravnomerno raspoređeni elementi. U idealno slučaju bi
86 sve $K_i$ vrednosti bile jednake sa $E$ i imali bi da je vrednost
87 testa nula. U opštem slučaju se naravno javljaju odstupanja od
88 proseka, a pošto kvadriramo udaljenosti, $\chi^2$ brzo raste ukoliko
89 ima većih anomalija.
91 Program daje vrednosti za ovaj test i u odnosu na idealan
92 slučaj kada je broj lanaca/podskupova jednak sa brojem
93 elemenata, kao i za realan broj podskupova (tj različitih
94 vrednosti elemenata).
97 Primeri
98 =======
100 U sledećim primerima će se razmatrati različiti tipovi podataka i kako se oni
101 efikasno mogu skladištiti u heš tabelama. Podaci se smeštaju u instancu `StatSet`
102 koja nam daje statistike o njihovoj raspodeli. Svi dati primeri se mogu pokretati
103 iz odgovarajućih klasa pošto postoje `main` metodi koji pokreću program `TestHash`
104 sa odgovarajućim parametrima.
107 Korišćenje kancelarija
108 ----------------------
110 Treba da čuvamo podatke o korišćenju kancelarija u toku jednog dana i
111 da omogućimo što bržu proveru da li je neka konkretna osoba koristila
112 neku konkretnu kancelariju. Podaci vezuju prezimena za identifikacione
113 brojeve soba. Naravno moguće je da više ljudi koristilo istu
114 kancelariju, a moguće je i da je neko više puta koristio istu -- ovo
115 će se u tabeli skladištiti samo jedan put, pošto nas samo zanima ko je
116 bio unutra, a ne koliko puta.
118 Uzmimo sledeću definiciju klase
120 ```Java
121 class Kancelarija {
122 private int broj;
123 private String prezime;
124 ....
126 ```
128 U datim fajlovima nema više od~100 prostorija, a i postoji samo
129 oko~60 različitih prezimena (tj zaposlenih).
131 Broj sobe tipa `int` i deluje kao "očigledan" kandidat za heš kod.
132 Validno bi bilo da se ova informacija vraća kao heš kod:
134 ```Java
135 public int hashCode() {
136 return broj;
138 ```
140 Pogledajmo primere učitavanja većeg broja podataka u heš tabele sa
141 različitim brojem podskupova, ako imamo tako definisanu heš funkciju:
143 ```
144 Number of elements: 4885
145 Different values: 100 ( 2.05 %)
146 Avg. search chain len.: 48.85 +- 2.99
147 Longest search chain: 56
148 Chi square (no. of el): 229,855.00
149 Chi square (diff el.): 18.28
150 ```
152 Budući da ima samo 100 soba, može biti i samo 100 različitih heš vrednosti, te
153 rezultati nisu zadovoljavajući.
155 Slično važi i za prezimena, budući da ih ima samo
156 šestdesetak, bez obzira na koliko je dobar heš kod u klasi
157 `String`, možemo imati samo šestdesetak različitih
158 rezultata, odnosno dobijamo još gore rezultate. Bitna
159 razlika u odnosu na prethodnu verziju gde smo koristili samo
160 `int` koji je primitivni tip, ovde se mora voditi računa i o
161 tome da samo polje može biti `null`, tj ne sme se desiti da
162 `hashCode` baci izuzetak zbog ovoga.
164 ```Java
165 public int hashCode() {
166 if (prezime != null) {
167 return prezime.hashCode();
168 } else {
169 return 0;
172 ```
174 ```
175 Number of elements: 4885
176 Different values: 61 ( 1.25 %)
177 Avg. search chain len.: 80.08 +- 3.96
178 Longest search chain: 89
179 Chi square (no. of el): 382,448.00
180 Chi square (diff el.): 11.95
181 ```
183 Očigledno je potrebno kombinovati ove dve vrednosti, odnosno
184 iskoristiti sve različitosti u podacima koje postoje..
185 Možemo ih jednostavno samo sabrati:
187 ```Java
188 public int hashCode() {
189 int rez = 0;
190 if (prezime != null) {
191 rez += prezime.hashCode();
193 rez += broj;
194 return rez;
196 ```
198 Dobijamo izuzetne rezultate u testovima, pošto su sve vrednosti različite:
200 ```
201 Number of elements: 4885
202 Different values: 4885 (100.00 %)
203 Avg. search chain len.: 1.00 +- 0.00
204 Longest search chain: 1
205 Chi square (no. of el): 0.00
206 Chi square (diff el.): 0.00
207 ```
211 Istovremeno je bitno da se i `equals` definiše da koristi ista polja.
212 Metod zahteva da parametar bude tipa `Object`, pa uvek moramo da krenemo
213 od toga. Najjednostavnija varijanta zahteva da proverimo da li je
214 prosledjeno nešto što je iste klase kao i naš objekat, pa tek
215 onda možemo da menjamo tip i da poredimo polja.
217 ```Java
218 public boolean equals(Object o) {
219 // proveravamo da li je objekat sa
220 // kojim se poredimo Kancelarija
221 if (o instanceof Kancelarija) {
222 // pretvaramo objekat u kancelariju
223 Kancelarija k2 = (Kancelarija) o;
224 // poredimo polja
225 if (prezime.equals(k2.prezime) && broj == k2.broj) {
226 return true;
229 return false;
231 ```
233 Međutim potpuna verzija `equals` metoda treba da proveri da li je u
234 pitanju baš ista klasa kao naša (moguće da je neki naslednik koji
235 tehnički ne mora biti isti), te da tu uzme u obzir i da je moguće da
236 nam je prosleđen `null` što nikako ne može biti isto kao i naš
237 objekat. Dalje se može ubrzati postupak proverom da li su u pitanju
238 pokazivači na isto mesto pošto nema potrebe za proverama onda.
240 Potpuna verzija bi izgledala ovako:
242 ```Java
243 public boolean equals(Object o) {
244 // Objekat je identican
245 if (this == o) {
246 return true;
248 // Null je uvek razlicit
249 if (o == null) {
250 return false;
252 // Ako su klase razlicite, objekti ne mogu bili jednaki
253 if (getClass() != o.getClass()) {
254 return false;
257 // pretvaramo objekat u kancelariju
258 Kancelarija k2 = (Kancelarija) o;
260 // Prvo proveravamo broj
261 if (broj != k2.broj) {
262 return false;
265 // A potom prezime
266 if (!Objects.equals(prezime, k2.prezime)) {
267 return false;
270 // Proverili smo polja i sva su jednaka
271 return true;
273 ```
275 Razmotrimo sledeću modifikaciju klase: recimo da se i dalje želi samo
276 pratiti u tabeli ko je bio u kojoj kancelariji, ali da su negde već
277 definisani objekti u kojima se skladišti i vreme poslednjeg
278 pristupanja, ili recimo neki dodatni opis zašto je neko bio u
279 kancelariji.
281 ```Java
282 class KancelarijaVreme {
283 private int broj;
284 private String prezime;
286 private int pristupDan, pristupSat;
288 private String komentar;
290 ```
292 Budući da za naše potrebe nije bitno kad je pristupano niti zbog čega,
293 i dalje bi trebali da koristimo iste kodove za `equals` i za
294 `hashCode` pošto nam je dosta da ubacimo samo jedan objekat za svako
295 pojedinačno korišćenje. Negde druge se naravno oni mogu koristiti za
296 detaljije zapise, ali nas to trenutno ne interesuje.
299 Gadjanje mete
300 -------------
302 Potrebno je da čuvamo podatke o rezultatima gadjanja mete. U nizu se
303 nalazi do 20 brojeva izmedju i uključujući 0 i 10.
305 Recimo da je klasa definisana na sledeći način:
307 ```Java
308 public class Gadjanje {
309 private int[] rezultati;
310 private static int MAX_DUZ = 20;
311 ...
313 ```
315 Pošto je moguće (i vrlo verovatno zapravo) da neko nije gađao svih 20
316 puta, u podacima će biti nizovi različitih dužina. U skladu sa tim je
317 i format za unos takav da je prvi broj u redu zapravo broj gađanja,
318 koji određuje veličinu niza.
320 Funkcija jednakosti bi izgledala ovako:
322 ```Java
323 public boolean equals(Object o) {
324 // Objekat je identican
325 if (this == o) {
326 return true;
328 // Null je uvek razlicit
329 if (o == null) {
330 return false;
332 // Ako su klase razlicite, objekti ne mogu bili jednaki
333 if (getClass() != o.getClass()) {
334 return false;
337 Gadjanje o2 = (Gadjanje) o;
338 // proveravamo da li je polje null pre dalje provere
339 if (rezultati != null && o2.rezultati != null) {
340 if (o2.rezultati.length == rezultati.length) {
341 for (int i = 0; i < rezultati.length; i++) {
342 if (o2.rezultati[i] != rezultati[i]){
343 // cim je nesto razlicito nisu isti
344 return false;
347 // ako se sve vrednosti slazu isti su
348 return true;
350 return false;
351 } else {
352 // vracamo da li su oba null, tj da li su jednaki
353 return (rezultati == null && o2.rezultati == null);
356 ```
358 “Očigledno” rešenje je samo sabrati sve ove vrednosti i vratiti broj. Međutim
359 ovakvo rešenje u najboljem slučaju daje maksimalno 200 različitih vrednosti,
360 pri čemu bi se očekivala i dosta neravnomerna raspodela (sume pri sredini su
361 daleko verovatnije od najviših i najnižih). Na konkretnim podacima su vrednosti
362 u tabeli nešto gore od očekivanih 200 vrednosti:
364 ```
365 Number of elements: 9049
366 Different values: 139 ( 1.54 %)
367 Avg. search chain len.: 65.10 +- 37.98
368 Longest search chain: 118
369 Chi square (no. of el): 771,638.00
370 Chi square (diff el.): 3,079.85
371 ```
373 U ovakvoj strukturi, pozicija u nizu bi trebalo da bude značajna za
374 krajnje heš vrednosti. Da bi se to postiglo jedan pristup je množenje
375 brojeva sa svojim pozicijama ili različitim (prostim) brojevima.
377 Za ovaj primer u kome je dužina niza raznolika i ima više "početnih"
378 elemenata je dodatno pogodno da se težine primenjuju unazad, pa recimo
379 možemo uvesti da prvi element ima težinu 21, a poslednji u nizu od 20
380 elemenata težinu 2.
382 ```Java
383 public int hashCode() {
384 int rez = 0;
385 if (rezultati != null) {
386 for (int i = 0; i < rezultati.length; i++) {
387 rez += rezultati[i] * (MAX_DUZ + 1 - i);
390 return rez;
392 ```
394 Vrednosti su dosta bolje, iako još uvek nisu na viskom nivou.
396 ```
397 Number of elements: 9049
398 Different values: 1411 (15.59 %)
399 Avg. search chain len.: 6.41 +- 3.89
400 Longest search chain: 20
401 Chi square (no. of el): 62,736.00
402 Chi square (diff el.): 3,335.34
403 ```
405 Problem je zapravo što brojevi nisu dovoljno različiti. Pošto znamo da su
406 rezultati najviše 10, možemo to iskoristiti da u suštini sastavimo višecifreni broj
407 na sledeći način:
409 ```Java
410 for (int i = 0; i < rezultati.length; i++) {
411 rez = (rez + rezultati[i]) * 10 ;
413 ```
415 Ovim smo dobili dosta bolje vrednosti:
417 ```
418 Number of elements: 9049
419 Different values: 8911 (98.47 %)
420 Avg. search chain len.: 1.02 +- 0.14
421 Longest search chain: 4
422 Chi square (no. of el): 176.00
423 Chi square (diff el.): 171.21
424 ```
426 Možemo uočiti i sledeće - ako na početku nekog niza stoje 0, one će biti ignorisane
427 u ukupnom zbiru, te samim tim možemo imati različite nizove koji vraćaju iste
428 vrednosti. Ovo možemo ispraviti tako što ćemo pojedinačne rezultate povećavati
429 za 1, odnosno pomeriti ih iz opsega 0-10 na 1-11. U skladu sa tim treba da
430 povećamo i broj kojim množimo na 11.
432 ```Java
433 public int hashCode() {
434 int rez = 0;
435 if (rezultati != null) {
436 for (int i = 0; i < rezultati.length; i++) {
437 rez = (rez + rezultati[i] + 1) * 11 ;
440 return rez;
442 ```
444 ```
445 Number of elements: 9049
446 Different values: 9049 (100.00 %)
447 Avg. search chain len.: 1.00 +- 0.00
448 Longest search chain: 1
449 Chi square (no. of el): 0.00
450 Chi square (diff el.): 0.00
451 ```
454 Tabla za igru XO
455 ----------------
457 Zadatak nam je da efikasno čuvamo podatke o pozicijama u igri XO,
458 odnosno matrice 3x3, u kojima su vrednosti -1 za "X", 0 za prazno
459 polje i 1 za "O".
461 Npr:
462 ```
463 1 0 -1 O _ X
464 1 1 -1 O O X
465 -1 0 1 X _ O
466 ```
467 predstavlja poziciju u kojoj je ``O'' pobedio/la.
469 Polja klase bi se mogla definisati na sledeći način:
471 ```Java
472 public class XO {
473 public static final int DIM = 3;
474 private final int[][] tabla = new int[DIM][DIM];
475 ....
477 ```
479 Poredjenje ovakvih objekata bi naravno trebalo da pojedinačno ispituje
480 elemente matrice, na primer:
482 ```Java
483 public boolean equals(Object o) {
484 // Objekat je identican
485 if (this == o) {
486 return true;
488 // Null je uvek razlicit
489 if (o == null) {
490 return false;
492 // Ako su klase razlicite, objekti ne mogu bili jednaki
493 if (getClass() != o.getClass()) {
494 return false;
497 // menjamo tip da mozemo da poredimo
498 XO o2 = (XO) o;
499 // posto je u ovoj klasi uvek inicijalizovano polje table
500 // i uvek je DIM x DIM ne moramo proveravati null
501 for (int i = 0; i < DIM; i++) {
502 for (int j = 0; j < DIM; j++) {
503 if (o2.tabla[i][j] != tabla[i][j]) {
504 return false;
508 return true;
510 ```
512 Za ovaj primer vredi prvo razmotriti da neke relativno očigledne
513 tehnike nisu efikasne: sumiranje ovakve matrice daje vrednosti do plus
514 ili minus 9, odnosno ima samo 19 različitih vrednosti, što bi
515 predstavljalo jako lošu pokrivenost tabele. Dodatno, vrlo često će
516 rezultat biti 0 i uopšteno će biti više tablica koje su bliže
517 "sredini" rezultatom, što opet dovodi do loše ravnomernosti
518 raspoređivanja elemenata.
520 Dakle ovako definisan metod za heš će davati dosta loše rezultate:
522 ```Java
523 public int hashCode() {
524 int rez = 0;
525 for (int i = 0; i < DIM; i++) {
526 for (int j = 0; j < DIM; j++) {
527 rez += tabla[i][j];
530 return rez;
532 ```
534 ```
535 Number of elements: 4424
536 Different values: 18 ( 0.41 %)
537 Avg. search chain len.: 245.78 +- 253.74
538 Longest search chain: 674
539 Chi square (no. of el): 2,237,400.00
540 Chi square (diff el.): 4,715.27
541 ```
543 Efikasnost se u ovakvoj strukturi dobija davanjem "težine"
544 elementima. Odnosno ne treba gledati jednako ako je "1" u gornjem
545 levom uglu i u sredini. Npr u jednostavnijim varijantama se može zbir
546 iz svakog reda množiti različitim brojem. Još bolji rezultati se
547 dobijaju uvodjenjem još detaljnijih "težina": pri sumiranju reda
548 množiti različito, ili čak imati specijalne proste brojeve za svako
549 polje ili uvoditi dodatne operacije.
551 Takodje treba uzeti u obzir i to da sabiranje pozitivnih i negativnih
552 vrednosti može da dovede do "međusobnog poništavanja", te da nula
553 vrednosti ne pomažu zbiru, naročito kad se uvedu težine koje se množe
554 sa vrednostima polja. Jedan način da se ovo uradi u konkretnom slučaju
555 je da se pri računanju heša vrednosti u matrici sabiraju sa 2, pa time
556 imamo 1,2,3 umesto -1,0,1, što dovodi do veće različitosti.
558 Kombinovanjem ovih ideja se mogu napraviti `hashCode` metodi koji će
559 rezultovati sa hi kvadrat vrednostima manjim od broja podskupova
560 ("kolona"), što se ostavlja za samostalnu vežbu.
Svarog.pmf.uns.ac.rs/gitweb maintanance Doni Pracner