Tilbake
3.2

3.2 Load-faktor, rehashing og hashmap/-set som verktøy

Load-faktor og når man rehasher, og hashmap/-set som Del 2-verktøyet for O(n)-løsninger.

45 min
6 oppgaver
Load-faktorrehashinghashmap/-set som verktøy
Din fremgang i kapitlet
0 / 6 oppgaver

Forkunnskaper

- kap. 3.1 — hashtabellen, h(k)=kmodNh(k) = k \bmod N, lineær
probing og forskjellen på forventet og verste kjøretid. Alt her bygger direkte
på det.
- kap. 2.2 — sortér-og-skann-strategien, som er alternativet
hash-settet skal sammenlignes mot.

Har du brukt en ordbok i et introkurs, kjenner du allerede grensesnittet:
Ordbøker — der er hashmap-en ferdig laget; her ser du hva
den koster.

Notasjons- og pseudokodeliste

Løkke 1 — load-faktoren: hvor full er tabellen? (ca. 12 min)

Klyngedannelsen du så i kap. 3.1 blir raskt verre jo mer
tabellen fylles opp. Med tre nøkler i en tabell på elleve plasser er kollisjoner
sjeldne. Med ti nøkler i den samme tabellen er de nesten uunngåelige, og
probing-sekvensene blir lange.

Målet på hvor full tabellen er, heter load-faktoren:

α=nN\alpha = \frac{n}{N}

der nn er antall lagrede nøkler og NN er antall plasser. Er α=0,5\alpha = 0{,}5,
er halvparten av plassene i bruk. Er α=1\alpha = 1, er tabellen full, og ingen ny
nøkkel får plass.

Rekkefølgen i brøken er verdt å låse fast: nøkler over plasser. Snur du
brøken, får du et tall over 1 så snart tabellen er mindre enn halvfull, og det
gir ingen mening.

Load-faktor
α=n/N\alpha = n/N, der nn er antall lagrede nøkler og NN er tabellstørrelsen.
Andelen av plassene som er i bruk.

For lukket hashing må α1\alpha \le 1 — tabellen kan ikke inneholde flere nøkler
enn den har plasser. Når α\alpha nærmer seg 1, blir probing-sekvensene lange og
den forventede kjøretiden nærmer seg verste tilfelle. Typisk terskel for å utvide
er α\alpha over 0,50{,}5 eller 0,750{,}75.

✏️Eksempel 1: Load-faktor og hva den betyr

En hashtabell har N=7N = 7 plasser. Regn ut load-faktoren etter at 4 nøkler er
satt inn, og etter at 6 er satt inn. Hva sier tallene om kjøretiden?

Med 4 nøkler: α=4/70,571\alpha = 4/7 \approx 0{,}571. Litt over halvparten av
plassene er i bruk.

Med 6 nøkler: α=6/70,857\alpha = 6/7 \approx 0{,}857. Bare én plass er ledig.

Hva tallene betyr. Ved α0,57\alpha \approx 0{,}57 er det fortsatt tre ledige
plasser spredt utover, og en ny nøkkel har god sjanse til å finne plass raskt.
Ved α0,86\alpha \approx 0{,}86 må en ny nøkkel i verste fall probe seg gjennom nesten
hele tabellen for å finne den ene ledige plassen.

Konkret, fra tabellen i kap. 3.1: der ble fem nøkler
satt inn i en tabell med N=7N = 7, altså α=5/70,714\alpha = 5/7 \approx 0{,}714, og
totalkostnaden var 13 prøver for fem innsettinger — 2,6 per innsetting. Hadde
tabellen vært dobbelt så stor, ville de samme fem nøklene hatt langt kortere vei.

Merk hva load-faktoren ikke sier. Den sier ingenting om hvordan nøklene
ligger. En tabell med α=0,5\alpha = 0{,}5 der alle nøklene ligger i én klynge, er
verre enn en med α=0,7\alpha = 0{,}7 der de er spredt. Load-faktoren er et
grovmål — men det er grovmålet implementasjoner faktisk bruker, fordi det er
billig å holde oppdatert.

📝Oppgave 1

(Innstegsoppgave, sjanger C — kjøretids- og teorifakta, altså rene
sant/usant-påstander.) En hashtabell har N=16N = 16 plasser og inneholder 12
nøkler.

a) Hva er load-faktoren?
b) Kan load-faktoren i en tabell med lukket hashing bli større enn 1?
c) Sant eller usant: en lav load-faktor garanterer korte probing-sekvenser.

Løkke 2 — rehashing: hele tabellen bygges på nytt (ca. 16 min)

— naturlig pausepunkt —

Når load-faktoren blir for høy, må tabellen utvides. Og her ligger poenget som
har vært et sant/usant-punkt i sett etter sett.

Du kan ikke bare lage et større array og flytte nøklene over på de samme
indeksene.

Grunnen står i hashfunksjonen: h(k)=kmodNh(k) = k \bmod N. Den avhenger av NN. Dobler du
tabellen fra 7 til 14, endres alle hashverdier. Nøkkelen 22 hashet til plass
1 da N=7N = 7; med N=14N = 14 hasher den til plass 8. En nøkkel som ligger igjen på
plass 1, vil aldri bli funnet av et søk som starter på plass 8.

Derfor må hele tabellen bygges på nytt: opprett et nytt array, gå gjennom det
gamle, og sett inn hver nøkkel på nytt med den nye hashfunksjonen.

Rehashing

Å opprette et nytt, større array — typisk dobbelt så stort — og sette inn alle
nøklene på nytt
med en hashfunksjon som bruker den nye tabellstørrelsen.

Nødvendig fordi h(k)=kmodNh(k) = k \bmod N avhenger av NN: når NN endres, endres alle
hashverdier. De gamle indeksene kan ikke gjenbrukes, og det er hele poenget som
testes.

Kostnaden er O(n+N)O(n + N) for én rehashing: O(N)O(N) for å gå gjennom det gamle
arrayet og O(n)O(n) for å sette inn nøklene på nytt.

📜Pseudokode-kontrakt: `Rehash`
Antagelser om representasjon. T er et array med N plasser indeksert fra 0;
hver plass er tom eller inneholder én nøkkel. Insert er prosedyren fra
kap. 3.1, som bruker k mod T.length som hashfunksjon —
altså tabellens egen lengde.

Prebetingelse: ingen. Postbetingelse: returverdien er en ny tabell med
dobbelt så mange plasser, som inneholder nøyaktig de samme nøklene, hver på sin
nye plass.

Procedure Rehash(T)
  Input:  hashtabell T som array med N plasser (indeks fra 0)
  Output: en ny hashtabell med 2N plasser og de samme noeklene
  N = T.length
  ny = nytt array med 2N plasser, alle tomme
  for i = 0 to N-1:
      if T[i] er ikke tom:
          Insert(ny, T[i])
  return ny

Grunnideen i én setning: fordi Insert regner ut hashverdien fra tabellens
egen lengde, får hver nøkkel automatisk sin nye plass — du trenger ikke gjøre noe
mer enn å sette dem inn på nytt.

Rekkefølgen nøklene settes inn i er venstre mot høyre i det gamle arrayet.
Det er en antagelse du bør oppgi, for den avgjør hvor nøkler som kolliderer i den
nye tabellen, havner. En annen rekkefølge gir en like korrekt tabell, men en annen
plassering — så på en håndkjøring skal du si hvilken rekkefølge du brukte.

Kjøretid: løkka går gjennom alle NN plassene, og for hver av de nn nøklene
gjøres én innsetting på O(1)O(1) forventet. Totalt O(n+N)O(n + N) for én rehashing.

✏️Eksempel 2: Rehashing fra $N = 7$ til $N = 14$

Tabellen _, 15, 22, 8, 29, _, _ har N=7N = 7 og inneholder fire nøkler, altså
α=4/70,571\alpha = 4/7 \approx 0{,}571. Terskelen for utvidelse er α>0,5\alpha > 0{,}5, så
tabellen skal rehashes til N=14N = 14. Vis hele prosessen.

Reinnsettingsrekkefølge: venstre mot høyre i den gamle tabellen, altså 15,
22, 8, 29.

Nye hashverdier med N=14N = 14: 15mod14=115 \bmod 14 = 1, 22mod14=822 \bmod 14 = 8,
8mod14=88 \bmod 14 = 8, 29mod14=129 \bmod 14 = 1.

Legg merke til hvor mye som endret seg. I den gamle tabellen hashet alle fire
til plass 1. Nå fordeler de seg på to ulike startplasser.

StegNøkkel kkgammel hhny h(k)h(k)Prøvde indekserTabell etter steget
115111_, 15, _, _, _, _, _, _, _, _, _, _, _, _
222188_, 15, _, _, _, _, _, _, 22, _, _, _, _, _
38188 -> 9_, 15, _, _, _, _, _, _, 22, 8, _, _, _, _
429111 -> 2_, 15, 29, _, _, _, _, _, 22, 8, _, _, _, _

Sluttilstand:
indeks:  0   1   2   3   4   5   6   7   8   9   10  11  12  13
T:       _   15  29  _   _   _   _   _   22  8   _   _   _   _
Ny load-faktor: α=4/140,286\alpha = 4/14 \approx 0{,}286.
Se på kolonnen «gammel hh». Alle fire var 1. Etter doblingen er to av dem 1
og to er 8. Det er nøyaktig dette som gjør at de gamle indeksene ikke kan

gjenbrukes: nøkkelen 22 lå på plass 2 i den gamle tabellen og skal på plass 8 i

den nye. Hadde vi bare kopiert arrayet over, ville et søk etter 22 startet på

plass 8, funnet den tom, og meldt «ikke funnet».

Hva vi vant. Den gamle tabellen hadde én sammenhengende klynge på fire
plasser; totalkostnaden for de fire innsettingene var 10 prøver. Den nye har to
små klynger, og de samme fire innsettingene kostet 6 prøver. Neste innsetting blir
billigere, og det er hele hensikten.
Fellenote. Fella her er å tro at rehashing er en flytteoperasjon. Den er en

reinnsetting: hver nøkkel går gjennom Insert på nytt, med den nye
tabellstørrelsen i hashfunksjonen.

📝Oppgave 2
Sjanger E og C

En hashtabell med N=5N = 5 inneholder nøklene 12, 17 og 3 slik:
_, _, 12, 17, 3.

a) Hva er load-faktoren?
b) Tabellen rehashes til N=10N = 10. Regn ut de nye hashverdiene, og vis
reinnsettingen med rekkefølge venstre mot høyre i den gamle tabellen.
c) Oppgi den nye tabellen og den nye load-faktoren.

📜Hvorfor doblingen er billig i lengden
Én rehashing koster O(n+N)O(n + N), og det ser dyrt ut. Men den skjer sjelden, og
regnestykket over mange innsettinger er verdt å kunne:

Starter du med tabellstørrelse N0N_0 og dobler hver gang tabellen blir for full,
har du etter nn innsettinger gjort rehashinger med samlet kostnad omtrent

N0+2N0+4N0++n.N_0 + 2N_0 + 4N_0 + \cdots + n.

Dette er en doblingsrekke, og summen er mindre enn det dobbelte av siste ledd
— altså under 2n2n. Fordelt på nn innsettinger blir det en konstant per
innsetting.

Konklusjonen: nn innsettinger i en hashtabell med dobling koster O(n)O(n)
totalt
, altså O(1)O(1) per innsetting i snitt, selv om enkelte innsettinger
utløser en dyr rehashing.

Om ordet «amortisert». Denne typen analyse — å fordele kostnaden av en sjelden
dyr operasjon utover mange billige — kalles amortisert analyse. Begrepet nevnes
her bare for å avgrense det: amortisert analyse er ikke IN2010-pensum, og du
blir ikke bedt om å gjøre en slik analyse. Det du skal kunne, er konklusjonen: at
doblingen ikke ødelegger den forventede O(1)O(1)-kostnaden per operasjon.

Løkke 3 — hashmap og hash-set som Del 2-verktøy (ca. 15 min)

Nå til den delen som gir poeng på Del 2.

Hashtabellen du har håndkjørt, er maskineriet. To grensesnitt bygger på det, og
begge har du sannsynligvis brukt før uten å tenke på kostnaden:

- Et hashmap lagrer par av nøkkel og verdi. Du slår opp på nøkkelen og får
verdien. «Hvor mange ganger forekommer hvert ord i teksten?» er en
hashmap-oppgave.
- Et hash-set lagrer bare nøkler, og svarer på ett spørsmål: finnes denne
her? «Har jeg sett dette elementet før?» er en hash-set-oppgave.

Begge gir O(1)O(1) forventet for innsetting, oppslag og medlemskapstest, og
O(n)O(n) i verste tilfelle. Og det er nettopp den forventede O(1)O(1)-en som gjør at
en hel klasse Del 2-oppgaver kan løses i O(n)O(n) i stedet for O(nlogn)O(n \log n).

Hashmap

En struktur som lagrer par av nøkkel og verdi, og som slår opp verdien
direkte fra nøkkelen ved hjelp av hashing.

Put(M, k, v), Get(M, k) og medlemskapstest er alle O(1)O(1) forventet,
O(n)O(n) i verste tilfelle. Typisk bruk på Del 2: telle forekomster, gruppere
elementer, eller huske hvilken verdi som hørte til hvilken nøkkel.

Hash-set

En struktur som lagrer nøkler uten verdier, og som svarer på ett spørsmål:
finnes denne nøkkelen her?

Add(S, k) og Contains(S, k) er O(1)O(1) forventet, O(n)O(n) i verste tilfelle.
Typisk bruk på Del 2: «har jeg sett dette elementet før?» — altså duplikatsøk,
eller å sjekke om komplementet til et tall finnes.

📜Broen til Del 2: når hash-set gir O(n)O(n)

Mønsteret er alltid det samme, og det er verdt å kunne som en form:

Går du gjennom elementene én gang, og trenger du for hvert element å vite om
du har sett noe bestemt før, kan et hash-set gjøre hvert oppslag O(1)O(1)
forventet — og hele algoritmen O(n)O(n) forventet.

Sammenligningen sensor forventer:

StrategiKjøretidForutsetning
Dobbel løkke over alle parO(n2)O(n^2)ingen
Sortér og skannO(nlogn)O(n \log n)elementene kan sammenlignes
Hash-setO(n)O(n) forventet, O(n2)O(n^2) versteelementene kan hashes

Tre presiseringer som hver er verdt et delpoeng:
1. «Forventet» må stå. Hash-set-løsningen er O(n)O(n) forventet. I verste
tilfelle, når alt kolliderer, koster hvert oppslag O(n)O(n) og totalen blir
O(n2)O(n^2) — like dårlig som den naive løsningen.

2. Sortering er ikke feil. O(nlogn)O(n \log n) er en helt korrekt besvarelse og gir

uttelling. Den står bare ett trinn lavere i poengtrappen.
3. Bucket og radix er ikke lov som «rask sortering» med mindre verdiområdet er
kjent og begrenset — felle #5 i bokas feilregister, se
kap. 2.3.
Selve algoritmene skrives ut i kap. 3.4.

✏️Eksempel 3: Samme problem, tre kjøretider

Du har nn heltall i et usortert array og skal avgjøre om to av dem er like. Sett
opp de tre lovlige strategiene med kjøretid, og si hvilken som står øverst i
poengtrappen.

Problemet navngitt: duplikatsøk i en usortert mengde.

Antagelser om representasjon: A er et array med nn heltall indeksert fra 0.
Heltall kan hashes, og vi kan opprette nye strukturer.

Strategi 1 — dobbel løkke. Sammenlign hvert par:

for i = 0 to n-1:
    for j = i+1 to n-1:
        if A[i] == A[j]:
            return sant
return usant

To nøstede løkker over nn gir n(n1)2\displaystyle \frac{n(n-1)}{2} sammenligninger, altså
O(n2)O(n^2). Korrekt, men nederst i trappen.

Strategi 2 — sortér og skann. Sortér med flettesortering i O(nlogn)O(n \log n), og
gå deretter gjennom naboparene i O(n)O(n). Sumregelen gir O(nlogn)O(n \log n).

Strategi 3 — hash-set. Gå gjennom arrayet én gang. For hvert element: finnes
det allerede i settet? Da har du duplikatet. Ellers legg det inn.

S = tomt hash-set
for i = 0 to n-1:
    if Contains(S, A[i]):
        return sant
    Add(S, A[i])
return usant

Én løkke over nn elementer, og hvert oppslag er O(1)O(1) forventet. Totalt
O(n)O(n) forventet, der nn er antall elementer i A.

Poengtrappen:

KjøretidStrategiUttelling
O(n)O(n) forventethash-setfull pott
O(nlogn)O(n \log n)sortér og skannnoe mindre
O(n2)O(n^2)dobbel løkkeminst

Kan dette gjøres raskere enn O(n)O(n)? Nei. Du må i verste fall se på hvert
element minst én gang for å kunne konkludere, så O(n)O(n) er nedre grense for
problemet. Den setningen er verdt å skrive: den viser at du vet at du har truffet
bunnen, og den er en del av det sensor ser etter i et Del 2-svar.

Og det ærlige forbeholdet: i verste tilfelle, når alle nøklene kolliderer, er
hash-set-løsningen O(n2)O(n^2) — nøyaktig like dårlig som den doble løkka. Det er
derfor ordet «forventet» ikke kan utelates.

📝Oppgave 3
Sjanger C

Avgjør sant eller usant, og begrunn hver med én setning.

a) Rehashing kan gjenbruke de gamle indeksene direkte etter en dobling.
b) Et hashmap har O(1)O(1) oppslagstid i verste tilfelle.
c) Load-faktoren i en tabell med lukket hashing kan være 1,5.
d) Å sette inn nn nøkler i en hashtabell med dobling koster O(n)O(n) totalt.

📝Oppgave 4
Sjanger I

Skriv Rehash(T), som dobler tabellen og setter inn alle nøklene på
nytt.

a) Oppgi antagelser om representasjon.
b) Skriv prosedyren.
c) Oppgi kjøretiden for én rehashing, og for nn innsettinger med dobling
totalt.

📝Oppgave 5
Sjanger I, krevende

Du får et usortert array A med nn heltall og et tall
x, og skal avgjøre om to elementer i A summerer til x.

a) Skriv en løsning med hash-set. Oppgi antagelser og kjøretid.
b) Hvorfor kan du ikke bare sjekke om x - A[i] finnes i settet før du
har lagt inn noe?
c) Hva blir kjøretiden hvis alle nøklene kolliderer?

📝Oppgave 6
Sjanger I, krevende

Du får en liste med nn ord og skal finne det ordet som
forekommer flest ganger.

a) Skriv en løsning med hashmap. Oppgi antagelser og kjøretid.
b) Hva ville en løsning uten hashmap kostet?
c) Hva skjer med kjøretiden hvis alle ordene er forskjellige?

Begrepsbank

Begrepsbanken er flashcard-/repetisjonsstoff — den gjentar det du nettopp har
lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder
kjernestoffet.

Terskelen for rehashing

Grensen for load-faktoren der tabellen utvides — typisk α\alpha over 0,50{,}5
eller 0,750{,}75.

Tallet er et implementasjonsvalg, ikke en naturlov, og eksamensoppgaver oppgir
det. Poenget er hva som skjer når terskelen krysses, ikke hvilket tall den har.

Hvorfor hashverdiene endres ved dobling

Fordi h(k)=kmodNh(k) = k \bmod N har NN i seg. Nøkkelen 22 gir plass 1 når N=7N = 7 og
plass 8 når N=14N = 14.

Konsekvensen: de gamle indeksene er verdiløse i den nye tabellen, og hver nøkkel
må settes inn på nytt. Påstanden «rehashing kan gjenbruke de gamle indeksene» er
usann, og den er kapitlets faste distraktor.

Kostnaden ved én rehashing
O(n+N)O(n + N): O(N)O(N) for å gå gjennom alle plassene i det gamle arrayet, og O(n)O(n)
for de nn innsettingene på O(1)O(1) forventet hver.

Én rehashing er altså en dyr operasjon. Den skjer sjelden nok til at snittet over
mange innsettinger likevel blir konstant.

Doblingsrekkens sum
N0+2N0+4N0++nN_0 + 2N_0 + 4N_0 + \cdots + n er mindre enn 2n2n: i en doblingsrekke er summen
av alle leddene under det dobbelte av det siste.

Derfor koster nn innsettinger med dobling O(n)O(n) totalt, altså O(1)O(1) per
innsetting i snitt. Analysemetoden bak dette kalles amortisert analyse og er
ikke IN2010-pensum — bare konklusjonen er det.

Reinnsettingsrekkefølge

Rekkefølgen nøklene settes inn i under en rehashing — vanligvis venstre mot høyre
i det gamle arrayet.

Den avgjør hvor nøkler som kolliderer i den nye tabellen havner, så den er en
antagelse du skal oppgi i en håndkjøring. En annen rekkefølge gir en like korrekt
tabell med andre plasseringer.

Hashmap-operasjonene
Put(M, k, v) lagrer verdien v under nøkkelen k og overskriver hvis k
finnes. Get(M, k) henter verdien.

Begge er O(1)O(1) forventet, O(n)O(n) i verste tilfelle. Bruk på Del 2: telle
forekomster, gruppere elementer, huske hvilken verdi som hørte til hvilken
nøkkel.

Hash-set-operasjonene
Add(S, k) legger k i settet uten effekt hvis den allerede er der.
Contains(S, k) avgjør medlemskap.

Begge er O(1)O(1) forventet, O(n)O(n) i verste tilfelle. Bruk på Del 2: «har jeg
sett dette før?» — duplikatsøk og komplementsøk.

Hash-set-mønsteret for O(n)O(n)

Går du gjennom elementene én gang, og trenger for hvert element å vite om du har
sett noe bestemt før, gir et hash-set O(1)O(1) forventet per oppslag og O(n)O(n)
forventet totalt.

Det er dette mønsteret som løfter en Del 2-besvarelse fra O(nlogn)O(n \log n) til
O(n)O(n), og det gjelder duplikatsøk, parsøk og telleoppgaver.

Poengtrappen for søkeoppgaver
O(n)O(n) forventet med hash-set gir full pott; O(nlogn)O(n \log n) med sortér-og-skann gir
noe mindre; O(n2)O(n^2) med dobbel løkke gir minst.

Alle tre er korrekte. Sensorveiledningene sier eksplisitt at lavere kjøretid er mer
poenggivende på samme oppgave, så å velge struktur er å velge poeng.

«Forventet» er ikke pynt

En hash-basert løsning er O(n)O(n) forventet og O(n2)O(n^2) i verste tilfelle,
når alle nøklene kolliderer.

Skriver du bare «O(n)O(n)», har du oppgitt en kjøretid som ikke matcher algoritmen —
og det trekkes det eksplisitt for. Ett ord, ett delpoeng.

Hash-set kontra sortering

Hash-set: O(n)O(n) forventet, O(n2)O(n^2) verste, krever at elementene kan hashes.
Sortér-og-skann: O(nlogn)O(n \log n) garantert, krever at elementene kan
sammenlignes.

Valget mellom dem er en avveining mellom forventet fart og garanti. Ber oppgaven
om en garantert kjøretid, er sorteringen det riktige svaret selv om den er
tregere.

Amortisert analyse — avgrenset bort

Metoden for å fordele kostnaden av en sjelden dyr operasjon utover mange billige.
Den forklarer hvorfor doblingen ikke ødelegger O(1)O(1) per innsetting.

Ikke IN2010-pensum. Begrepet nevnes her kun for å avgrense det: du blir aldri
bedt om å gjøre en amortisert analyse. Konklusjonen — at nn innsettinger med
dobling er O(n)O(n) totalt — er derimot verdt å kunne.

Repetisjon — kapitlet på ett kort

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 Universitetet i Oslo. Dette er ikke offisielt studiemateriell. Les mer.