3.4 Hashing
Hashtabeller med kjeding, kollisjoner, lastfaktor og hva som gjør en hashfunksjon god — et voksende tema (3/3 i 2022–23).
grunnlaget (41 %) — men det er med i alle tre settene fra 2022 og 2023.
Prioriteten i pensumoversikten er «kjenne til», og det står ved lag, men gitt
trenden bør du prioritere det høyere enn prosenten alene tilsier. Dette er 45
minutter som med god margin kan gi poeng.
Temaet testes i tre sjangre, og alle tre trenes her:
- Sjanger D — definisjon «med egne ord», altså at du skriver én presis
setning med hovedpoenget først. «Hva er lastfaktoren i en hashtabell?»
- Sjanger E — kjøretidskunnskap, altså at du oppgir det strammeste
uttrykket, med \Theta der grensen er tett og O der bare den øvre er vist.
- Sjanger F — «stemmer dette?», altså at du svarer ja eller nei først, og
så begrunner med én setning. Dette er hashingens hovedsjanger, og fella er
nesten alltid den samme: er søk i en hashtabell «alltid raskt»?
Det ene du må ha helt presist: søk i en hashtabell med kjeding er
forventet under enkel uniform hashing, og i
verste tilfelle. Skriver du «garantert », har du svart feil på det
spørsmålet som stilles oftest.
Slik er kapitlet lagt opp (45 min):
| Del | Innhold | Tid |
|---|---|---|
| 1 | Hashtabellen, kollisjoner og kjeding | ca. 15 min |
| 2 | Lastfaktoren og hva søk faktisk koster | ca. 15 min |
| 3 | Hva som gjør en hashfunksjon god | ca. 15 min |
Forkunnskaper
- kap. 1.1 — de asymptotiske symbolene. Du trenger
særlig skillet mellom (tett grense, både over og under) og (bare
øvre grense), og hva det vil si at noe er en forventet kjøretid framfor en
garantert.
Dette sto der: brukes når du har vist at kjøretiden både er minst
og høyst av størrelsesorden , mens bare lover et tak. En
«forventet» kjøretid er et gjennomsnitt over noe tilfeldig — her over hvordan
nøklene fordeler seg — og sier ingenting om hva som skjer i det verste tilfellet.
Trenger du en mykere inngang til -notasjonen først, ligger den i
Algoritmedefinisjon, pseudokode og kompleksitet (Big-O).
Du trenger ikke hauger eller søketrær for dette kapitlet. Hashtabellen er en
helt egen struktur, og den sammenlignes med søketreet først helt til slutt.
Hashtabellen, kollisjoner og kjeding (~15 min)
Et idrettslag låner ut nøkkelbrikker til klubbhuset. Hver brikke har et
femsifret nummer, og i løpet av et år er det kanskje tre hundre brikker i
omløp. Skal du kunne slå opp «hvem har brikke 40 231?» raskt, har du to
åpenbare, dårlige alternativer.
Du kan lage én plass for hvert mulige brikkenummer. Det gir oppslag på ett
øyeblikk, men krever hundre tusen plasser for tre hundre brikker. Eller du kan
legge brikkene i en liste og lete gjennom den. Det tar minimalt med plass, men
oppslaget koster .
Hashtabellen er kompromisset: du velger et håndterbart antall plasser, og
lar en regel bestemme hvilken plass hver nøkkel hører hjemme i. Regelen er
hashfunksjonen. Med tre hundre brikker holder det med for eksempel 401 plasser —
og oppslaget blir raskt likevel.
en tabell T med plasser der plassen til en nøkkel bestemmes av en regel i
stedet for av innsettingsrekkefølgen. Plassene kalles bøtter. Tabellen bruker
plass, altså langt mindre enn én plass per mulige nøkkel, og
støtter innsetting, søk og sletting.
regelen som avgjør hvilken bøtte en nøkkel havner i: er et tall
mellom og . Den må være deterministisk (samme nøkkel gir samme bøtte
hver gang), rask å regne ut, og den bør spre nøklene jevnt over bøttene.
En hashfunksjon oversetter altså fra et stort nøkkelunivers til et lite
tabellområde.
at to ulike nøkler får samme bøtte, altså for
. Kollisjoner er uunngåelige når nøkkeluniverset er større enn
antall bøtter, og en hashtabell er derfor alltid en struktur pluss en
kollisjonsstrategi. Spørsmålet er aldri om kollisjoner oppstår, men hva du gjør
når de gjør det.
kollisjonsstrategien der hver bøtte holder en lenket liste av alle nøklene
som hashet dit. Innsetting legger den nye nøkkelen fremst i lista og koster
; søk må gå gjennom lista i den ene bøtta. Alle nøklene ligger altså inne i
tabellen, og antall nøkler kan overstige antall bøtter.
T er en tabell med plasser, indeksertT[0] til T[m-1]. Hver plass peker til en lenket liste, som er tom iutgangspunktet. Hashfunksjonen
h gir et heltall i 0..m-1 for enhver nøkkel.Prebetingelse: T er en gyldig hashtabell med kjeding. Postbetingelse for
innsetting: x ligger fremst i lista T[h(x.key)], og ingen andre elementer
er flyttet.
Chained-Hash-Insert(T, x)
Input: hashtabell T med kjeding, element x med noekkel x.key
Output: x lagt inn fremst i lista T[h(x.key)]
sett x fremst i den lenkede lista T[h(x.key)]
Kjoretid: O(1)Chained-Hash-Search(T, k)
Input: hashtabell T med kjeding, noekkel k
Output: elementet med noekkel k, eller NIL
i = h(k)
for hvert element y i den lenkede lista T[i]
if y.key == k
return y
return NIL
Kjoretid: Theta(1 + alfa) forventet, Theta(n) versteGrunnideen i én setning: hashfunksjonen gjør om et søk i hele datamengden til
et søk i én enkelt kjede, og alt henger på hvor korte de kjedene er.
Kjøretid: innsetting er fordi den ikke leter — den legger elementet
fremst uten å sjekke om nøkkelen finnes fra før. Søk koster én hashutregning
pluss gjennomgangen av kjeden, altså forventet under enkel
uniform hashing, og når alle nøklene ligger i samme bøtte.
En hashtabell har bøtter og bruker divisjonsmetoden .
Sett inn nøklene 26, 13, 40, 9, 18, 31 i denne rekkefølgen.
a) Hvordan ser tabellen ut etterpå?
b) Hva er lastfaktoren?
c) Hvor mange sammenligninger koster et mislykket søk etter nøkkelen 5?
| Steg | Nøkkel k | h(k) = k mod 7 | Kollisjon? | Bøtta etter innsettingen |
|---|---|---|---|---|
| 1 | 26 | 26 mod 7 = 5 | nei | 26 |
| 2 | 13 | 13 mod 7 = 6 | nei | 13 |
| 3 | 40 | 40 mod 7 = 5 | ja (1 element der fra før) | 40 -> 26 |
| 4 | 9 | 9 mod 7 = 2 | nei | 9 |
| 5 | 18 | 18 mod 7 = 4 | nei | 18 |
| 6 | 31 | 31 mod 7 = 3 | nei | 31 |
Tabellen til slutt:
T[0]: tom
T[1]: tom
T[2]: 9
T[3]: 31
T[4]: 18
T[5]: 40 -> 26
T[6]: 13b) .c) , så søket går til bøtte 5. Der ligger 40 og 26. Begge må
sammenlignes med 5 før søket kan svare at nøkkelen ikke finnes: to
sammenligninger.
Legg merke til rekkefølgen i bøtte 5. Nøkkelen 40 kom sist, men ligger
først. Chained-Hash-Insert legger alltid det nye elementet fremst, fordi det er
— å legge det bakerst ville krevd at man gikk gjennom hele lista.
Legg merke til hva et mislykket søk koster. Det er dyrere enn et vellykket
søk i gjennomsnitt, fordi det alltid må gå gjennom hele kjeden. Et vellykket
søk kan stoppe underveis.
En hashtabell med kjeding har bøtter og bruker . Sett
inn nøklene 14, 29, 7, 20, 33, 11 i denne rekkefølgen.
a) Oppgi innholdet i hver av de fem bøttene.
b) Hva er lastfaktoren?
Lastfaktoren og hva et søk faktisk koster (~15 min)
Hashtabellens hastighet er ikke en egenskap ved strukturen alene. Den avhenger
av hvor full tabellen er, og av hvor jevnt nøklene har fordelt seg. Det første
måles av lastfaktoren.
forholdet mellom antall lagrede nøkler og antall bøtter, altså
gjennomsnittlig kjedelengde. Med kjeding kan være både mindre og større
enn 1. Lastfaktoren er den ene knappen du kan skru på: holder du
konstant ved å øke når vokser, holder du også den forventede søketiden
konstant.
å hashe til hver av de bøttene, uavhengig av hvor de andre nøklene havner.
Dette er en antakelse om nøklene og hashfunksjonen sammen — ikke noe algoritmen
kan garantere.
Under den antakelsen er den forventede lengden på en kjede lik lastfaktoren
. Et søk koster da
der leddet 1 er hashutregningen og oppslaget i tabellen, som du betaler uansett,
og leddet er gjennomgangen av kjeden.
Konsekvensen er den viktigste setningen i kapitlet: holder du
begrenset av en konstant — for eksempel ved å velge slik at
— blir forventet søketid . Det er nettopp derfor
hashtabeller er raske i praksis.
Verste tilfelle er noe helt annet. Havner alle nøklene i samme bøtte, er
tabellen redusert til én lenket liste, og et søk koster . Det er ikke
et teoretisk grensetilfelle: velger du og alle nøklene er delelige med 6,
skjer det.
Derfor: søk i en hashtabell med kjeding er forventet
og i verste tilfelle. Uttrykket «garantert » er galt, og
det er den feilen dette temaet oftest tester.
En hashtabell har bøtter og . Sett inn nøklene18, 27, 45, 13, 36, 22, 31.
a) Hvordan ser tabellen ut?
b) Hva er lastfaktoren, og hva koster et mislykket søk etter nøkkelen 40?
c) Hva forteller dette om forskjellen mellom forventet og verste tilfelle?
| Steg | Nøkkel k | h(k) = k mod 9 | Kollisjon? | Bøtta etter innsettingen |
|---|---|---|---|---|
| 1 | 18 | 18 mod 9 = 0 | nei | 18 |
| 2 | 27 | 27 mod 9 = 0 | ja (1 element der fra før) | 27 -> 18 |
| 3 | 45 | 45 mod 9 = 0 | ja (2 elementer der fra før) | 45 -> 27 -> 18 |
| 4 | 13 | 13 mod 9 = 4 | nei | 13 |
| 5 | 36 | 36 mod 9 = 0 | ja (3 elementer der fra før) | 36 -> 45 -> 27 -> 18 |
| 6 | 22 | 22 mod 9 = 4 | ja (1 element der fra før) | 22 -> 13 |
| 7 | 31 | 31 mod 9 = 4 | ja (2 elementer der fra før) | 31 -> 22 -> 13 |
T[0]: 36 -> 45 -> 27 -> 18
T[1]: tom
T[2]: tom
T[3]: tom
T[4]: 31 -> 22 -> 13
T[5]: tom
T[6]: tom
T[7]: tom
T[8]: tomb) . Et mislykket søk etter 40 går til bøtte, og må gjennom hele kjeden der: tre sammenligninger.
c) Lastfaktoren er lavere enn i Eksempel 1 ( mot ), men
tabellen er klart dårligere. Sju av ni bøtter står tomme, og de to kjedene er
fire og tre lange. Lastfaktoren er et gjennomsnitt, og gjennomsnittet sier
ingenting om hvordan nøklene faktisk har spredt seg.
Hvorfor det gikk galt her. Fire av nøklene er delelige med 9, så de gir alle
. Med blir hashfunksjonen dermed helt avhengig av om nøklene
tilfeldigvis har en felles faktor med 9. Det er ikke tilfeldig at anbefalingen er
å velge som et primtall som ikke ligger nær en potens av 2: da er det
langt færre nøkkelmønstre som kan kollapse på denne måten.
En hashtabell med kjeding har bøtter og . Sett inn
nøklene 41, 17, 25, 33, 6, 19 i denne rekkefølgen.
a) Oppgi innholdet i hver bøtte.
b) Hva er lastfaktoren, og hva er den lengste kjeden?
c) Hvor mange sammenligninger koster et mislykket søk etter nøkkelen 12?
En hashtabell med kjeding har bøtter og . Sett inn
nøklene 12, 18, 24, 30, 7.
a) Oppgi innholdet i hver bøtte.
b) Hva koster et mislykket søk etter nøkkelen 42, målt i sammenligninger?
c) Hva er kjøretiden for søk i denne tabellen i verste tilfelle, uttrykt i
?
En hashtabell med kjeding har bøtter og . Sett inn
nøklene 52, 19, 41, 30, 63, 8.
a) Oppgi innholdet i hver bøtte.
b) Er dette verstetilfellet for en hashtabell med kjeding og ? Svar ja
eller nei, og begrunn med én setning.
c) Betyr resultatet at er et dårlig valg? Begrunn med én setning.
Hva gjør en hashfunksjon god? (~15 min)
Kravene er tre, og de kan alle formuleres i én setning hver. Dette er
definisjonssjangeren i praksis: du blir bedt om å nevne kravene, ikke om å bevise
noe.
Uniform fordeling. Nøklene skal spres jevnt over bøttene, slik at ingen
kjede blir mye lengre enn de andre. Dette er det som gjør antakelsen om enkel
uniform hashing rimelig, og dermed det som gjør til noe du kan
regne med i praksis.
Determinisme. Samme nøkkel må gi samme bøtte hver gang. Uten det kan du
sette inn en nøkkel og aldri finne den igjen. Kravet virker banalt, men det
utelukker enhver hashfunksjon som bruker tilfeldighet eller tid.
Hastighet. Utregningen av må koste . Er hashfunksjonen dyrere
enn å gå gjennom en kort kjede, har du tapt det du kom for.
hashfunksjonen , altså resten når nøkkelen deles på antall
bøtter. Den er rask og enkel, og den er den du regner med for hånd på eksamen.
Kvaliteten avhenger helt av valget av : et primtall som ikke ligger nær en
potens av 2 er anbefalingen, fordi som potens av 2 gjør at bare de nederste
bitene i nøkkelen teller.
en alternativ hashfunksjon der nøkkelen først multipliseres med en konstant
mellom 0 og 1, og desimaldelen av produktet deretter skaleres opp til
. Fordelen er at valget av ikke er kritisk — metoden fungerer også
når er en potens av 2. Du trenger å kjenne den som begrep og vite hva den
løser; det er divisjonsmetoden du regner med.
- Å påstå at søk i en hashtabell er garantert . Dette er kapitlets
hovedfelle, og den kommer igjen og igjen i «stemmer dette?»-oppgaver. Riktig
svar er at søk er forventet under enkel uniform
hashing, og i verste tilfelle. Kontrollen: har du skrevet ordet
«forventet» eller «i gjennomsnitt» i svaret ditt? Hvis ikke, mangler det noe.
- Å blande kjeding med åpen adressering. Med kjeding ligger nøklene i
lenkede lister utenfor selve tabellplassen, og kan være større enn 1.
Med åpen adressering — den andre hovedstrategien, der en kolliderende
nøkkel i stedet plasseres i en annen ledig bøtte i tabellen — ligger alle
nøklene inne i tabellen, og da må . Sier en oppgave «kjeding»,
gjelder det første.
- Å glemme lastfaktorens rolle i svaret. Et svar som bare sier «søk er raskt»
får ikke uttelling. Det er som styrer, og det er derfor tabellen må
vokse når vokser.
- Å tro at en lav lastfaktor garanterer korte kjeder. Lastfaktoren er et
gjennomsnitt. Med og nøklene 12, 18, 24 og 30 er under 1, mens
den lengste kjeden er fire.
- Å tro at enhver hashfunksjon gir uniform fordeling. Uniformitet er en
antakelse vi gjør for å kunne regne, og et mål når vi designer
funksjonen — ikke noe som følger av at funksjonen er en hashfunksjon.
Stemmer det at søk i en hashtabell med kjeding alltid er ?
Svar ja eller nei på hver av påstandene, og begrunn hver med én setning.
a) Lastfaktoren i en hashtabell med kjeding kan ikke være større enn 1.
b) En hashfunksjon må være deterministisk.
c) Kollisjoner kan unngås helt hvis hashfunksjonen er god nok.
b) Oppgi forventet søketid for en hashtabell med kjeding, med den antakelsen
som kreves, og verste søketid.
To hashtabeller bruker begge kjeding, har begge bøtter og lagrer begge
fem nøkler. Tabell 1 har nøklene 12, 18, 24, 30, 7; tabell 2 har nøklene1, 2, 3, 4, 5. Begge bruker .
a) Hva er lastfaktoren i hver av tabellene?
b) Hva er lengste kjede i hver av dem?
c) Stemmer det at to hashtabeller med samme lastfaktor har samme søketid i
verste tilfelle? Svar ja eller nei, og begrunn med én setning.
Kjøretidene, samlet
| Operasjon | Forventet | Verste | Krav / egenskap |
|---|---|---|---|
Chained-Hash-Insert | legger fremst i kjeden, sjekker ikke om nøkkelen finnes fra før | ||
Chained-Hash-Search | forventningen krever enkel uniform hashing | ||
Chained-Hash-Delete | forutsetter dobbeltlenket liste og at elementet allerede er funnet | ||
| Plassbruk | bøtter pluss lagrede nøkler |
Til sammenligning: et binært søketre gir på søk, der høyden er
forventet for et tilfeldig bygd tre og i verste
tilfelle — se kap. 3.2. Hashtabellen er raskere i det
typiske tilfellet, men søketreet gir deg noe hashtabellen ikke gir: nøklene i
sortert rekkefølge, i , via
Inorder-Tree-Walk. Trenger du orden,hjelper ingen hashfunksjon deg.
Begrepsbank
Begrepsbanken er flashcard- og repetisjonsstoff — den gjentar det du nettopp har
lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder
kjernestoffet.
én av de plassene i hashtabellen. Med kjeding holder hver bøtte en lenket
liste over alle nøklene som hashet dit, og en bøtte kan være tom, ha ett element
eller ha mange. Antall bøtter er , og det er den ene størrelsen du styrer selv
når du dimensjonerer tabellen.
antakelsen om at hver nøkkel er like sannsynlig å hashe til hver av de
bøttene, uavhengig av hvor de andre nøklene havner. Det er denne antakelsen som
gjør forventet søketid . Den er en antakelse om nøklene og
hashfunksjonen sammen, ikke en garanti algoritmen kan gi.
tabelloppslaget, og ett ledd for gjennomgangen av kjeden. Holdes
begrenset av en konstant, er forventet søketid — men det er en
forventning, ikke en garanti.
tabellen dermed er redusert til én lenket liste. Dette skjer ikke bare i teorien:
med nøkler som ligger 8 fra hverandre holder.
den andre hovedstrategien for kollisjoner, der en kolliderende nøkkel plasseres
i en annen ledig bøtte i selve tabellen i stedet for i en liste utenfor. Alle
nøklene ligger da inne i tabellen, og lastfaktoren kan derfor ikke overstige 1.
Kjeding og åpen adressering skal ikke blandes i et svar.
et søk etter en nøkkel som ikke finnes. Det koster alltid gjennomgang av
hele kjeden i den bøtta peker på, mens et vellykket søk kan stoppe
underveis. Blir du bedt om å telle sammenligninger, er det derfor kjedelengden
som er svaret.
tommelfingerregelen er å velge som et primtall som ikke ligger nær en potens
av 2. Er en potens av 2, avgjøres bøtta bare av de nederste bitene i
nøkkelen, og all annen informasjon i tallet kastes bort. Valget av er den ene
designbeslutningen som avgjør om divisjonsmetoden sprer godt.
hashtabellen gir forventet på søk, men ingen orden:
nøklene kan ikke leses ut sortert, og det finnes ingen naturlig «neste nøkkel».
Søketreet gir på søk og sortert utskrift i . Trenger du
rekkefølge eller intervallsøk, velger du treet.
fordi nøkkeluniverset nesten alltid er mye større enn antall bøtter: med flere
mulige nøkler enn plasser må minst to nøkler dele plass. En hashtabell er derfor
alltid en tabell pluss en kollisjonsstrategi, og påstanden «med en god nok
hashfunksjon slipper man kollisjoner» er gal.
strategien der tabellen forstørres når vokser, slik at holder
seg under en fast grense. Da forblir forventet søketid uansett hvor
mange nøkler som kommer inn. Forstørringen koster å gjøre — det er samme
mekanikk som i den dynamiske tabellen i kap. 3.5.
oppgavetypen der du får en påstand og skal avgjøre om den holder. Svarformen er
fast: ja eller nei først, deretter én presis setning som begrunner. Hashing
er et av temaene som oftest kommer i denne formen, og påstanden handler nesten
alltid om at søk skulle være garantert konstant.
Dette kapitlet er skrevet av Anthropics toppmodeller (Claude Opus og Claude Fable) og er foreløpig ikke manuelt gjennomgått — kvalitetskontrollen gjøres av uavhengige KI-agenter, og innmeldte feil rettes fortløpende. Funnet en feil? Meld fra, så retter vi den. Les mer om hvordan innholdet lages.
Skolesaga er en uavhengig læringsressurs og er ikke tilknyttet eller godkjent av Norges teknisk-naturvitenskapelige universitet. Dette er ikke offisielt studiemateriell. Les mer.