3.4 Spredetabeller

Når nøglerne er små heltal, findes der en implemention af symboltabeller som er overordentlig enkel: Datastrukturen er en række af samme størrelse som den størst mulige nøgle; nøglerne tolkes som rækkeindices sådan at værdien knyttet til nøgle \(i\) gemmes i rækkens \(i\)’te plads. I dette afsnit betragter vi en metode kaldt spredning, som bruger denne idé for mere komplekse typer af nøgler ved at transformere nølger til rækkeindices gennem aritmiske operationer.

crux of hashing

Spredningsbaserede søgealgoritmer består af to dele. Den først del er en spredefunktion. Denne bruges til beregning af søgenøglens spredeværdi, som vil blive brugt som rækkeindeks. Idealet er at forskellige nøgler resulterer i forskellige indices. Dette ideal kan generelt ikke opnås, fordi der er flere mulige nøgler en spredeværdier. Derfor må vi tage højde for, at to eller flere søgenøgler kan spredes til samme rækkeindeks. Spredesøgningens anden del er derfor en konvention for kollisionshåndtering. Modular hashing

Spredefunktioner.

Hvis vores række har plads til \(M\) nøgle-værdi-par, behøver vi en funktion, der kan transformere en given nøgle til et indeks i rækken, dvs. et heltal mellem \(0\) og \(M-1\). Vores ønske til en spredefunktion er, at den kan beregnes effektivt og at den spreder nøglerne uniformt i intervallet \(\{0,\ldots, M-1\}\). Der er hovedsagelig tre krav til implementationen af en god spredefunktion for en given datatype: For at kunne analysere spredningsbaserede algoritmer og opstille hypoteser om deres opførsel gør vi følgende idealiserende antagelse om spredefunktionen:

Antagelse J (uniform spredning).

Spredefunktionen fordeler nøgler uniformt og uafhængigt i \(\{0,\ldots,M-1\}\).

Spredning med separat hægtning.

Spredefunktionen omvandler nøgljer til rækkeindices. Spredningsalgoritmens anden komponent er kollisionshåndteringen: en strategi der angiver, hvad der sker, når to nøgler spreded til samme indeks. En enkel løsning er at bygge en hægtet liste af nøgle-værdi-par i hver rækkeindgang, hvor der opstår en kollision. Vi vælger rækkestørrelsen \(M\) til at være tilstrækkelig stor for at listerne bliver tilstrækkelig korte til at blive gennemsøgt effektiv. For at slå op i datastrukturen beregner vi først søgennøglens spredeværdi, betragter den tilsvarende rækkeindgang og gennemløber den hægtede liste sekventielt, til vi finder nøglen.

hashing with separate chaining

Programmet SeparateChainingHashST.java implementerer en symboltabel ved brug af en spredetabel med separat hægtning. Den opretholder en række, hvis indgange er symboltabeller implementeret som uordnede hægtet lister SequentialSearchST. Opslagsmetoden get() og indsættelsesmetoden put() beregner først en spredeværdi til at bestemme den symboltabel, der potentielt indeholder søgenøglen, og bruger så metoderne get() og put() fra SequentialSearchST for at udføre operationen.

Påstand K. Betragt en spredetabel med separat hægtning af \(M\) lister. Hvis tabellen indeholder \(N\) nøgler under antagelse J, så er sandsynligheden for at længden af en liste er inden for en lille konstant faktor af \(N/M\) meget tæt på 1.

Dette klassiske matematiske resultat virker besnærende, men det afhænger fuldstændigt af antagelse J. I praksis ser man dog ofte samme opførsel.

Egenskab L. Betragt en spredetabel med separat hægtning af \(M\) lister, som indeholder \(N\) nøgler. Antallet af sammenligninger ved indsættelse og forgæves søgning er proportional med \(N/M\).

Spredning med lineær probering.

Et alternativ til separat hægtning er at gemme \(N\) nøgle-værdi-par i en række af størrelse \(M> N\) og stole på, at der er tilstrækkelig mange tomme rækkeindgange til at håndtere kollisioner. Denne ide kaldes åben adressering. Den enkleste implementation af åben adressering er lineær probering: Hvis en søgenøgles spredeværdi angiver en rækkeindgang, som allerede er optaget af en anden nøgle, anbringer vi den givne nøgle i rækkens næste ledige plads ved lægge 1 til det indeks, det gives af spredeværdien. Der er tre mulige scenarier:

hashing with linear probing

Programmet LinearProbingHashST.java implementerer en symboltabel ved brug af en spredetabel med lineær probering.

Som ved separat hægtning afhænger køretiden af metoder baseret på åben adressing af forholdet \(\alpha = N/M\), men med en anden fortolkningen. Ved separat hægtning angiver \(\alpha\) den gennemsnitlige listelængde og er generelt større end 1. Ved åben adressering angiver \(\alpha\) andelen af optagede rækkeindgange; den skal være mindre end 1. Vi kalder \(\alpha\) for spredetabellens belastning.

Påstand M. Betragt en spredetabel af størrelse \(M\), der bruger lineær probering for \(N = \alpha M\) nøgler. Under antagelse J er det gennemsnitlige antal prober \(\sim\frac{1}{2} (1 + 1 / (1 - \alpha))\) for vellykket søgning og \(\sim\frac{1}{2} (1 + 1 / (1 - \alpha)^2)\) for indsættelse og forgæves søgning.

Gøgespredning.

En anden implementation af åben adressering er gøgespredning: I gøgespredning bruges to spredefunktioner \(h_1\)og \(h_2\) i stedet for én. Hver søgenøgle har altså to spredeværdier; som invariant fastholder vi, at hver søgenøgle \(x\) befinder sig i en af de to rækkeindgange, som indiceres af henholdsvis \(h_1(x)\) og \(h_2(x)\). Søgning kræver altså blot to opslag i værste fald. En ny søgenøgle indsættes i den rækkeindgang, der er angivet af nøglens første spredeværdi. Hvis denne rækkeindgang allerede er optaget af en anden søgenøgle, skubbes denne ud af indgangen og indsættes på sin alternative plads. Her kan den selv skubbe en tredje søgenøgle ud, og så videre, til en ledig plads er fundet eller processen går i ring. I det senere tilfælde vælges to nye spredeværdier og hele tabellen bygges på ny.

cuckoo hashing

Påstand. Betragt en spredetabel af størrelse \(M\), der bruger gøgespredning for \(N = \alpha M\) nøgler og antag at begge spredefunktioner opfylder antagelse J om uniform spredning og er uafhængige. Køretiden for søgning er konstant i værste fald. Køretiden for indsættelse er forventet konstant hvis \(\alpha < \frac{1}{2}\).