Hashtabeller TDA416, lp3 2016
Mängder och avbildningar (Sets and Maps) I den abstrakta datatypen avbildning/uppslagstabell (Map) lagras nyckelvärde-par. Grundläggande operationerna är insättning, borttagning och sökning (uppslag). Ett nyckel-värde-par identifieras av sin nyckel. Varje nyckel måste alltså vara unik i avbildningen, medan olika nycklar kan höra ihop med samma värde. Slår man upp en nyckel får man det associerade värdet som svar (eller inget, om nyckeln inte finns). Mängder kan ses som ett specialfall av avbildningar där värdena inte innehåller någon data. Informationen som datastrukturen lagrar är då enbart vilka nycklar/element som finns i mängden. k " v " k # k $ v # k % v $ x x $ " x# x % x (
Avbildningar implementeringar För naiv implementering (osorterad lista) tar operationerna O(n). Vi har sett att balanserade sökträd förbättrar detta till O(log n). I många tillämpningar krävs ännu bättre prestanda. Med hash-tabeller kan man få O(1).
Hashtabell Idén med hashtabellär att använda en arrayoch en funktion, f(k), som baserat på nyckeln snabbt beräknar ett index där nyckel-värde-paret ska lagras / återfinns. På det viset undviker man att skapa en array med en plats för varje tänkbar nyckel. Det är avgörandeför oftastär mängden tänkbara nycklar enorm. (T ex en sökmotor som har en avbildningmellan sökord och URLer. Det finns enormt många bokstavskombinationer, men bara en bråkdelutgör riktiga ord.) När man söker efter en nyckel och hittar ett nyckelvärde-par på hashfunktionens index måsteman kontrollera att det är sammanyckel. [0] [1] k3 v3 [2] k1 v1 [3] [4] k2 v2 [5] [6] k4 v4 f(k3)=1 f(k1)=2 f(k2)=4 f(k4)=6
Hashkod Hashkoden, h(k), utgör grunden för hashfunktionen. Normalt är f(k)=h(k)%n (där % beräknar divisionsresten och n är arrayens storlek) Hashtabeller är mycket vanliga och därför är hashcode() en metod i Javas Object, d.v.s. man uppmuntras att implementera detta för alla klasser. Att skriva bra hashcode()-implementeringar är utmanande, viktigt för hashtabellers prestandaoch fortfarande ett aktivt forskningsområde. Hashkoden bör ha god spridning (alla hashkoder bör förekomma lika ofta). Små skillnader i nyckeln bör ge förändrad hashkod. (Hashkoden för strängar bör alltså inte vara de fyra första tecknen omvandlade till en int32.) Detta för att få en god spridning av data i hashtabellen och undvika hopklumpning.
Exempel på hashkod En grundmetod för att beräkna hashkod används i java för strängar. 31 +," c. + 31 +,$ c " + + 31 " c +,$ + 31. c +," där c. c +," är ascii-koden för tecknen i strängen. En term för varje del i nyckeltypen. Varje tal multipliceras med en potens av ett primtal.
Kollisioner Det kan förstås hända att två olika nycklar, k1 och k2, sätts in i en tabell för vilka f(k1)=f(k2) Dessa skaalltså lagras påsamma index så detuppstår en kollision. Kollisioner hanteras på olika sätt. De två standardsätten är open addressing och chaining (closed addressing).
Open addressing, insättning När kollision uppstårförsöker man lagra nyckel-värde-paretpå ett annat index, oftast relativt ursprungsindexet. Man söker igenom en sekvens av index enligt ett givet mönster tills man finneren ledig plats. Ett sekvensmönster är linjär sökning (linear probing). Man söker då helt enkelt på nästkommande index i varje steg. Når man slutet av arrayen så fortsätter man i början. [0] [1] [2] k3 v3 [3] k1 v1 [4] k2 v2 [5] sätt in (k5,v5) f(k5)=3 [6] k4 v4
Open addressing, uppslagning slå upp k4, f(k4)=2 Om nyckeln på det index som hashfunktionenger inte innehåller samma nyckel, leta på nästa plats i sekvensen. Fortsätt jämföra med nyckeln på varje plats i sekvensen tills antingen nyckeln hittas eller platsen är tom. Om man når en tom plats såfinnsinte nyckeln i tabellen. [0] [1] k2 v2 [2] k1 v1 [3] k4 v4 [4] [5] k3 v3 [6] slå upp k5, f(k5)=5
tag bort k1, f(k1)=3 Open addressing, borttagning [0] När man ska ta bort ett nyckel-värde-par letar man upp det som vid sökning. Man kan inte bara ta bort paret utan måste markera att platsenhar varit upptagen. Detta för att framtida sökningar inte ska ge upp i förtid. [1] [2] k3 v3 [3] k1 v1 [4] k2 v2 [5] [6] k4 v4 [0] [1] [2] k3 v3 [3] * * [4] k2 v2 [5] [6] k4 v4 slå upp k2, f(k2)=2
Open addressing, kvadratisk sökning Linjär sökning har en tendens att bilda kluster där operationer tar lång tid (på grund av långa söksekvenser). Andra sekvenser förbättrar detta, t.ex. kvadratisk sökning (quadratic probing) där man söker på plats i+1, i+4, i+9, i+16 etc.
Chaining Vid chaining har varje element i arrayen en länkad lista innehållande nyckel-värde-par. På så vis kan alla par vars nyckel har samma index lagras på sin rätta plats. Vid sökning måste man traversera hela länkade listan tills man hittar nyckeln eller man når slutet. [0] [1] [2] [3] [4] [5] [6] k8 va k1 vb k3 vc k20 vd k13 ve
Rehashing När antalet nyckel-värde-par närmar sig arrayens kapacitet blir den genomsnittsliga tiden för operationer längre och längre. Detta p.g.a. detökadeantalet kollisioner. Man behöver då allokera en ny, större array. Detta kallas rehashing för när man fyller den nya arrayen räknas indexen för varje nyckel om. Denna är ju h(k)%n där n ändrat sig. Nyckel-värde-paren fördelar sig jämnt över den nya arrayen och kollisionerna blirfärre.
Prestenda Genomsnittsliga antalet jämförelser, c, för en operation vid open addressing är c = 1 2 1 + 1 1 L där L är lasten, d.v.s. antalet nyckel-värde-par dividerat med arrayens kapacitet. För chaining är sambandet c = 1 + L 2
Prestandajämförelse L open addressing chaining 0 1 1 0,5 1,5 1,25 0,75 2,5 1,38 0,9 5,5 1,45 0,95 10,5 1,48
Nackdelar med hashtabell Så länge lasten hålls under ett visst värde så är förväntade tiden för operationer O(1). Varför kan man föredra balanserade sökträd som tar O(log n)? Hashtabeller behöverrehashas. Detta tar mycket tid då ochdå. Enda garanterade värstafallskomplexiteten är O(n) för varje enskild operation. I binära sökträd kan man snabbt traversera elementen i sorteringsordning.