INF1010 Hashing Marit Nybakken marnybak@ifi.uio.no 8. mars 2004 Til nå har vi trodd at en HashMap var en mystisk uendelig stor samleeske der vi på magisk vis kan putte inn objekter og ta ut objekter ved hjelp av nøkler. Men alle datastrukturer på implementeres på en eller annen måte ved hjelp av grunnleggende javakode. Nå skal vi se hvordan det er gjort når de smarte folka i Sun laga HashMap. Hashing En hashtabell kan implementeres ved hjelp av to arrayer. I den ene arrayen lagrer vi nøklene, i den andre lagrer vi de tilhørende objektene. Ligger nøkkelen til et objekt på indeks 21, ligger objektet også på indeks 21. For å bestemme hvilken indeks som skal brukes, tar vi nøkkelen, som som oftest er et String-objekt, og kjører den gjennom en hashfunksjon som returnerer et heltall. En slik hashfunksjon kan lages på mange forskjellige måter. Vi kan summere ascii-verdien til bokstavene. Vi kan bruke minneadressen til String-objektet. Vi kan ta solas posisjon i forhold til Merkur i det objektet ble opprettet og multiplisere dette med antall fødte barn i Jugoslavia i etterkrigsårene. Uansett så søker vi en funksjon som gir oss indekser spredt utover hele tabellen og der to ulike nøkler gir to ulike indekser så langt det lar seg gjøre. 1
Figur 1: En hashtabell er to arrayer hashcode I java er det slik at alle objekter har sin egen hashkode som vi kan få tak i ved å kalle funksjonen hashcode i objektet. Derfor slipper vi å stusse på hvordan vi skal skrive vår egen hashfunksjon før i INF1020. hashcode funker slik: gir alltid det samme heltallet for det samme objektet gir alltid det samme heltallet for to forskjellige objekter som er like i følge equals-metoden. Det er derfor tekststrenger fungerer så fint som nøkler, equals gir true hvis den sammenligner to String-objekter med den samme teksten inni, selv om objektene er forskjellige. Det samme gjelder objekter av klassen Integer, de ser på tallet inni objektet og ikke pekerlikhet. Hashkoden kan være et kjempestort tall, og til og med et negativt tall. class TesteHashkode { 2
Figur 2: Ta et objekt og få ut en arrayindeks public static void main(string [ ] args) { String a = "banantre"; String b = "salte bjørner"; String c = "banantre"; System.out.println(a + " sin hashkode er " + a.hashcode()); System.out.println(b + " sin hashkode er " + b.hashcode()); 10 System.out.println(c + " sin hashkode er " + c.hashcode()); } } D:\komp\hashing>java TesteHashkode banantre sin hashkode er 1867554901 salte bj rner sin hashkode er 250890568 banantre sin hashkode er 1867554901 20 Det er derfor vanlig å bruke indeksen hashkode % tabellstørrelse. Tabellstørrelse bør være primtall, det gir best spredning. Kollisjonshåndtering Det kan godt hende at teksten snømus og teksten salte bjørner gir nøyaktig den samme hashkoden og derfor den samme indeksen i objektarrayen. Hvis 3
Figur 3: Separate chaining nøkkelen snømus kommer først og tar opp plassen, hvor skal da objektet med nøkkelen salte bjørner puttes hen? Plassen er jo opptatt. Dette kalles en kollisjon, og metodene for å håndtere slik styggedom kalles kollisjonshåndtering. Det virkelig vriene med å lage gode hashtabeller er å finne ut hva man skal gjøre ved kollisjoner. Her er noen strategier Separate chaining Denne teknikken går ut på at hver tabellplass har en liste over alle objektene som hashet hit (fikk denne indeksen ut fra nøkkelen). Dette kan typisk være en linket liste, der hvert objekt har en peker til det neste objektet i listen (linkede lister kommer litt senere i pensum, men det er bare objekter som hektes sammen vha pekere til en liste, omtrent som når du kjeder deg og hekter sammen binderser til en kjede). Se figur 3. Når objektet skal hentes ut søker vi bare gjennom alle objektene i listen på tabellplassen. Ganske greit å gjennomføre, men det går tregere fordi vi må opprette plass til neste element i lista hele tiden. Vi må også implementere en ekstra datastruktur. 4
Open adressing Figur 4: Linear probing Open adressing går ut på at vi prøver nye celler hvis den vi ønsket var opptatt. Å prøve ut nye celler må selvfølgelig foregå etter et bestemt mønster, slik at det er mulig å finne igjen objektet. Linear probing Her finner vi neste celle ut i fra en lineær funksjon. Ofte er dette bare å hoppe nedover tabellen steg for steg til vi finner en som ikke er opptatt. Se figur 4. Denne teknikken skaper noe som kalles for primary clustering. Dette er store blokker med opptatte celler etter hverandre, selv når tabellen er realtivt tom. Vi får altså da ikke den spredningen i tabellen vi er ute etter. Hvis enda 5
Figur 5: Objektene har hopa seg sammen 6
Figur 6: Quadratic probing et objekt hasher inn i blokken, må den bruke mange forsøk på å finne en ledig celle og blokken blir i tillegg større. Se figur 5. Quadratic probing Her søker vi etter en ledig celle ut i fra en kvadratisk funksjon. Funksjonen er gjerne i 2, hvilket vil si at vi først sjekker cellen 1 plass unna, deretter cellen 4 plasser unna (2 2 ), så 9 (3 2 ), osv, helt til vi finner en ledig. Se figur 6. Her slipper man blokk-fenomenet, men hashtabellen bør maksimalt bli halvfull før vi må gjøre den større. Vi er faktisk ikke garantert å finne en ledig celle lenger når den er over halvfull. Dette er jo ikke spesielt god utnyttig av plassen. 7
Double hashing Med double hashing finner vi en ny plass ut i fra en ny hashfunksjon hvis plassen vi hashet til var opptatt. Det kalles dobbelhashing fordi vi hasher en hashkode. Hvis den første tabellplassen vi kom frem til var x, prøver vi først på plass nyhashfunksjon(x), deretter 2*nyHashfunksjon(x), helt til vi finner en ledig plass. Rehashing Når tabellen blir for full må vi lage oss en ny og større tabell. Da lager vi først en tabell omtrent 2 ganger så stor som den gamle. Deretter går vi igjennom alle objektene i den gamle tabellen, beregner ny tabellplass i den nye tabellen og flytter objektene over. Dette kalles rehashing fordi vi må beregne hashkode på nytt for alle objektene i den gamle tabellen, siden indeksen er avhengig av tabellstørrelsen. Dette tar selvfølgelig ganske lang tid, men heldigvis så skjer det ikke så ofte. Spørsmålet er når vi skal rehashe. Følgende strategier er mulige: Når tabellen er halvfull (quadratic probing kan feile, men kanskje litt vel tidlig?) Når vi feiler når vi prøver å putte inn et objekt (kanskje litt sent?) Når vi når en bestemt load factor. Det siste blir en gylden middelvei. Vi kan for eksempel velge å rehashe når bare 20% av plassene er ledige. 8