1 Hashtabellen Datastructuren
2 Dit onderwerp Direct-access-tabellen (vorige keer) Hashtabellen –Oplossen van botsingen met “ketens” (chaining) Vorige keer (inclusief analyse) –Oplossen van botsingen door “open addressing” Hashfuncties Hash: “mix”, “schud” …
3 Datastructuren voor Dictionaries Operaties op een verzameling: –Zoek (Search) –VoegToe (Insert) –LaatWeg (Delete) Veel toepassingen Andere datastructuur: Zoekbomen –Bieden meer operaties (successor, minimum, etc.) –Zijn in de praktijk wat langzamer als we alleen zoeken, toevoegen, en weglaten –Komen later
4 Direct-access tabellen Array met positie (slot) voor elke mogelijke key Grootte array dus even groot als aantal mogelijke keys Nadeel: veel geheugen als er veel verschillende keys zijn Soms nuttig: bij weinig keys –Bijv. Key is lengte persoon in mm (< 3000 verschillende keys) extra info 6 + extra info 11 + extra info leeg
5 Hash-tabellen Als er veel keys zijn –Is een direct-access-table te groot –Maar kunnen we iets anders doen: hashtabel Neem een functie f, die iedere mogelijke key afbeeldt op een positie in de tabel (hashfunctie) Iets doen als er meer keys op dezelfde plek komen extra info leeg 40 + extra info 28 + extra info f(x) = x mod 17
6 Hash-functies Stel iedere key komt uit een universum U (eindige of oneindige verzameling) We hebben een functie h: U {0, 1, …, m-1} –m is de grootte van de tabel / het array Vaak gebruikte hashfuncties: U = N = {0, 1, 2, …} h(k) = k mod m Goede waardes voor m: priemgetallen, niet in de buurt van 2-macht Slechte waardes voor m: machten van 2, machten van 10, …
7 Oplossen van botsingen: “ketens” Botsingen (collisions): als we twee keys x en y willen hashen met h(x) == h(y) Oplossing 1: op elke positie van de hashtabel een pointer naar een gelinkte lijst (chaining) Oplossing 2: slim op een andere plek in de tabel zetten (open addressing) komt later leeg
8 Aanname bij analyse van Hashing met chaining “Simple uniform hashing” aanname: elk element heeft kans 1/m voor elk van de slots, onafhankelijk van de andere elementen –Versimpeling van de werkelijkheid –Helpt wel om een redelijk inzicht te krijgen m: tabelgrootte n: aantal elementen opgeslagen in tabel m/n = = load factor
9 Analyse Schrijf n(j) is aantal keys op positie j = lengte gelinkte lijst T(j) Onder de simple uniform hashing aanname is de verwachte waarde van n(j): –E(n(j)) = n/m Stelling. In een hashtabel waarbij botsingen door chaining worden opgelost, onder de simple uniform hashing aanname, kost een onsuccesvolle zoekactie gemiddeld (1+n/m) tijd Stelling. In een hashtabel waarbij botsingen door chaining worden opgelost, onder de simple uniform hashing aanname, kost een succesvolle zoekactie gemiddeld (1+n/m) tijd
10 Hashfuncties: maak van elke key een integer Keys kan je zien als bitstrings, en die kan je zien als integers Dus, we nemen aan dat de keys integers zijn Slechte hashfunctie: –Eerste r bits van de key –Want dan botsen keys met hetzelfde beginstuk
11 Hash-functies: 2 methoden Division method: (“deel-methode”) –h(k) = k mod m Multiplication method: (“vermenigvuldig-methode”) –h(k) = m( kA – kA ) –Voor constant A met 0 < A < 1 –Dus: vermenigvuldig met geschikt getal tussen 0 en 1, neem het deel “achter de komma”, en vermenigvuldig met m en rond naar beneden af –Voordeel: hangt niet zo erg af van keuze van m, en dus kan je je tabelgrootte zelf kiezen –Beste keuze van A??? Knuth: neem A = ( 5 – 1)/ 2 = … Geval: registratie nummers en moduli …
12 Open addressing Alles in dezelfde tabel
13 Open addressing Alle elementen opslaan in de tabel zelf Idee: –Hashfunctie heeft twee argumenten: Key Aantal “eerdere pogingen” Methode: –Reken eerst h(k,0) uit –Als h(k,0) leeg is, zetten we k op positie h(k,0), –Als er wel wat op h(k,0) staat, dan rekenen we h(k,1) uit –Als h(k,1) leeg is, zetten we k op positie h(k,1) –Anders, kijk naar h(k,2), etc.
14 Open addressing pseudocode en aanname Aanname: voor elke key k geldt: elke positie in {0, 1, 2, …, m-1} komt precies 1 keer voor in de “probe sequence” {h(k,0), h(k,1), h(k,2), …, h(k,m)} Goede “schudding” van de permutatie gewenst Hash-Insert(T,k) i = 0 while h(k,i) niet leeg and i<m do –i++; if i<m then –Zet k op positie h(k,i) else –Return foutmelding: Tabel te vol!!!!
15 Zoeken pseudocode Hash-Search(T,k) i=0; repeat –positie = h(k,i); –if k staat in T(positie) then Return extra gegevens op T(positie) etc. Gevonden! –i++; until T[positie] == NIL (lege plek) or i == m return Element niet gevonden (NIL)
16 Weglaten – het probleem Probleem: –“Gewoon” weglaten kan misgaan: Stel h(x,0) = h(y,0) = 10 We slaan x op, op positie 10 We slaan y op. h(y,0) is vol, dus y gaat naar h(y,1), zeg 20 Nu laten we x weg Nu zoeken we y, en we kijken eerst op 10. Maar… daar staat niks meer!
17 Weglaten – gedeeltelijke oplossing Oplossing: –Na weglaten krijg je niet een “gewone” positie, maar een positie met een speciale DELETED waarde –Zoeken: doorzoeken als je DELETED ziet, totdat je ‘t element vindt of LEGE positie tegenkomt –Invoegen: zet op de eerst gevonden plaats met LEEG of met DELETED –Nadeel: weggelaten elementen beïnvloeden nog steeds de tijd van zoekacties –Techniek: herbouw de hele hash-tabel als er “teveel” deletions gedaan zijn: Maak een nieuwe lege tabel Zet alle “echte” elementen over naar de nieuwe tabel Gooi de oude tabel weg Of gebruik Chaining als je deletions wil doen
18 Keuze van hashfuncties voor open addressing Stel h’ is “gewone” hashfunctie (met 1 argument) Overzicht (details komen): –Linear probing h(k,i) = (h’(k)+i) mod m –Quadratic probing h(k,i) = (h’(k) + c 1 i + c 2 i 2 ) mod m –Double hashing h(k,i) = (h’(k) + i* h’’(k)) mod m
19 Linear probing h(k,i) = (h’(k)+i) mod m Nadeel: primary clustering (“primaire klontvorming”): ophopen van keys bij elkaar als hun hashwaarden in elkaars buurt zitten: lange zoek- en invoegtijden
20 Quadratic probing h(k,i) = (h’(k) + c 1 i + c 2 i 2 ) mod m Voor geschikte constantes c 1 en c 2 Geen primaire klontvorming, maar wel secundaire klontvorming: alleen gevolgen voor keys met dezelfde hashwaarde –O(1) verwacht…
21 Double hashing Stel h’ is een “gewone” hashfunctie (met 1 argument) Stel h’’ is een functie die altijd elementen in 1 … m-1 geeft die relatief priem tov m zijn Double hashing –h(k,i) = (h’(k) + i* h’’(k)) mod m Bijvoorbeeld: –m is macht van 2 –h’’ geeft oneven getal Of: –Neem m priem, en getal m’ die iets kleiner is dan m, bijv m’=m-1 –h’(k) = k mod m; –h’’(k) = 1+ (k mod m’) Double hashing benadert de simple uniform hashing aanname het beste
22 Over de simple uniform hashing aanname Simple uniform hashing voor open addressing: –“Elk element en elke poging heeft evenveel kans”: 1/m
23 Analyse Neem weer uniform hashing aan Stelling. In een hash-tabel waarbij botsingen door open addressing worden opgelost, onder de simple uniform hashing aanname, kost een onsuccesvolle zoekactie gemiddeld (1+1/(1- )) tijd, waarbij = n/m < 1. Bewijs De kans dat er een element op positie h(k,0) staat is . De kans dat we daarna de tweede keer een bezette positie zien is (n-1)/(m-1), want de 2 e plek heeft m-1 mogelijkheden waar nog n-1 elementen zitten. Zo is de kans dat we minstens i keer een positie bekijken met een element erop:
24 Analyse - vervolg Kans dat je ‘n i-de positie bekijkt is hooguit i-1. Dus verwachtte aantal posities dat je bekijkt is hooguit Verwachtte tijd is O(1+ aantal bekeken posities) = O(1 + 1/(1-n/m)). QED Als loadfactor constant is, betekent dit dat de verwachtte tijd (onder aanname over hashfunctie) O(1) is: constant. Bijvoorbeeld: als tabel voor 50% vol: 1/(1-0.5) = 2 posities; als tabel voor 90% vol: 1/(1-0.9) = 10 posities die gemiddeld bekeken worden.
25 Toevoegen: analyse Stelling. In een hashtabel waarbij botsingen door open addressing worden opgelost, onder de simple uniform hashing aanname, kost een insertion gemiddeld (1+1/(1- )) tijd, waarbij = n/m < 1. Bewijs: we doen eerst een onsuccesvolle zoekactie op het in te voegen element, en dan plaatsen we het element. Dus, de vorige analyse geldt.
26 Analyse van succesvolle zoekactie Stelling. In een hashtabel waarbij botsingen door open addressing worden opgelost, onder de simple uniform hashing aanname, kost een succesvolle zoekactie gemiddeld (1+(1/ ) ln (1/(1- )) tijd, waarbij =n/m < 1. Bewijs. Stel k is het (i+1) e element dat we invoegen. Als we ‘m zoeken, kunnen we alleen het 1 e, 2 e, …, i e element dat ingevoegd is tegenkomen, want k kan alleen met hun botsen tijdens de insertion van k. Verwachtte aantal bekeken posities bij zoeken van k is daarom hooguit 1/(1-i/m) = m / (m-i). Gemiddeld over 1 e, 2 e, …, n e element is: Hierbij nemen we aan dat elk element dezelfde kans heeft om gezocht te worden; details zie boek.
27 Succesvolle zoekacties Voor constante loadfactor: succesvol zoeken kost O(1) tijd Bijvoorbeeld: als tabel 50% vol, dan posities bekeken; 90% vol geeft Dus: neem bij hashtabellen de tabel groot genoeg, om e.e.a. snel genoeg te laten verlopen.
28 Techniek in praktijk We beginnen met een niet al te grote tabel Als de load factor te groot wordt (bijv. meer dan 0.8) dan verdubbelen we de tabelgrootte –Neem een twee keer zo groot array –Zet alle elementen uit de hashtabel in de nieuwe array –Gebruik vanaf nu het nieuwe array –Herhalen als de load factor weer te groot wordt
29 Voordelen en nadelen Hashtabellen hebben een slecht “slechtste geval”, maar zijn verwacht, en in de praktijk, heel snel Je moet een hashfunctie maken, iets doen met deletions als je open addressing gebruikt, iets doen met volle tabellen (herbouw), … Zoekbomen hebben meer functies: minimum, opvolger, … Als je dit soort functies wilt, is een zoekboom beter. Zo niet, dan is hashing in de praktijk meestal sneller.
30 Conclusies Hashtabellen: snel en praktisch Chaining Open adressing: het probleem zijn de deletions Je moet goede hashfuncties maken De tabel moet niet te vol worden, want dan wordt de hashing te langzaam In Java: HashMap –Iteration over alle elementen gaat alle tabelentries langs en slaat de lege over. –Je kan de initiële capaciteit zetten (beïnvloedt efficiëntie) In C#: Hashtable