TDT4120 Algoritmer og datastrukturer Interaktivt kompendium · H2026
Fremgang
0/15

02a

Datastrukturer

Kjernepensum

En tabell slår opp element nummer ii på konstant tid, men å sette inn et element forrest krever at alle de andre flyttes. En lenket liste setter inn forrest på konstant tid, men må følge pekere for å finne element nummer ii. Begge lagrer de samme elementene; det som er billig i den ene, er dyrt i den andre.

Minne, pekere og tabeller

Minnet er én lang rekke celler med hver sin adresse. En peker er en variabel som inneholder en slik adresse, altså bare et heltall som tolkes som en posisjon i minnet.

En endimensjonal tabell ligger som en sammenhengende blokk. Har blokken startadresse aa, elementbredde bb og startindeks ss, ligger element ss på adressen aa, og hvert steg i indeksen flytter adressen bb celler. Element ii ligger i−si - s steg etter element ss, altså på adressen a+b(i−s)a + b(i - s). Oppslaget er ett gangestykke og én addisjon uansett hvor stor tabellen er, og derfor koster indeksering konstant tid i RAM-modellen.

En matrise må også ned i det samme endimensjonale minnet. Radvis lagring legger rad 1, så rad 2, og så videre. Foran rad ii ligger da i−1i - 1 hele rader med cc elementer hver, og foran M[i,j]M[i, j] i rad ii ligger j−1j - 1 elementer, så M[i,j]M[i, j] i en matrise med cc kolonner ligger på plass c(i−1)+(j−1)+1c(i - 1) + (j - 1) + 1. Kolonnevis lagring legger kolonnene etter hverandre i stedet. Et tredje alternativ er én tabell per rad, med en tabell av pekere øverst; da kan radene ha ulik lengde, såkalte jagged arrays. Alle strategiene kan utvides til flere dimensjoner.

En to ganger tre-matrise med verdiene 1 til 6, og de samme verdiene lagt ut i to endimensjonale tabeller: radvis som 1 2 3 4 5 6, kolonnevis som 1 4 2 5 3 6. Rad 1 og rad 2 har hver sin farge, og kolonnevis lagring fletter fargene.
Figur 1. Samme matrise i minnet på to vis. Fargen følger raden: radvis lagring holder radene samlet, kolonnevis lagring fletter dem.

Stakker og køer

En stakk er sist inn, først ut: alt skjer på toppen. Strukturen er en tabell og en toppindeks, der Push legger et element på toppen og Pop tar av det øverste, begge i konstant tid. Push på en full tabell gir overflyt, Pop på en tom gir underflyt. Kallstakken i et kjørende program er nettopp en slik stakk.

En kø er først inn, først ut, med innsetting bakerst og uttak forrest. Legger du køen rett inn i en tabell, må alt flyttes hver gang noen går ut forrest. Ringbufferen unngår det med to indekser som rykker fremover: head peker på den første i køen og tail på neste ledige plass, og når en av dem passerer enden, går den rundt til starten igjen.

ko.py
class Queue:
  def __init__(self, m):
      self.A = [None] * m
      self.head = 0  # først i køen
      self.tail = 0  # neste ledige plass

  def enqueue(self, x):
      self.A[self.tail] = x
      self.tail = (self.tail + 1) % len(self.A)  # rundt til 0 etter siste plass

  def dequeue(self):
      x = self.A[self.head]
      self.head = (self.head + 1) % len(self.A)
      return x
Kø som ringbuffer
0 / 0

Grønn pil er head, første i køen; rød pil er tail, neste ledige plass. En verdi som er tatt ut, blir stående nedtonet til plassen brukes igjen.

Testene for over- og underflyt er sløyfet her, som i læreboka. De trengs i praksis: med bare head og tail ser en helt full kø ellers lik ut som en tom.

Eksamensoppgave Den sensurerte linja i Enqueue
Kontinuasjonseksamen, august 2024, oppgave 1

Det følgende er hentet fra Enqueue:

1 Q[Q.tail] = x
2 if Q.tail == Q.size
3
4 else Q.tail = Q.tail + 1

Hva skal den sensurerte biten være?

Vis løsning Skjul løsning

Linje 2 tester om tail står på siste plass i tabellen. Da er neste ledige plass den første, så tail skal gå rundt til starten. Boka bruker 1-indeksering, så starten er 1.

Svar
Q.tail = 1

Dynamiske mengder

Mange algoritmer trenger bare å holde styr på en samling elementer med nøkler: sett inn, slett og søk, og når nøklene har en ordning, også minste, største, forgjenger og etterfølger. Dette kalles en dynamisk mengde, og det er funksjonalitet, ikke en bestemt datastruktur. Finnes ikke elementet du spør etter, returneres NIL. Problemstørrelsen er vanligvis antallet elementer i mengden.

En vanlig tabell kan levere funksjonaliteten, med priser som avhenger av om tabellen holdes sortert. Usortert er innsetting og sletting konstant tid, mens søk og minimum må gjennom alt. Sortert blir søket logaritmisk med binærsøk og minimum konstant, men innsetting og sletting må flytte elementer og blir lineære. En lenket liste setter inn og sletter midt i en rekkefølge uten å flytte noe, og har ingen fast størrelse. En hashtabell gjør søket forventet konstant. For de ordnede operasjonene er søketrær verktøyet.

Lenkede lister

En lenket liste består av noder som peker på neste node, og i en dobbeltlenket liste også på forrige. Lista selv er bare en peker til hodet.

Søk følger pekerne fra hodet til nøkkelen dukker opp eller lista tar slutt, og koster Θ(n)\Theta(n) i verste fall. Til gjengjeld er innsetting og sletting konstant tid, for nodene blir liggende der de er, og bare pekerne endres. Å sette inn en node etter en annen er fire pekertilordninger; å slette en node er to, der naboene settes til å peke på hverandre og noden hoppes over. Konstant tid forutsetter at du allerede står ved noden. Skal den først finnes, betaler du det lineære søket i tillegg.

liste.py
def insert_after(a, x):
  x.prev = a
  x.next = a.next
  a.next.prev = x
  a.next = x

def delete(x):
  x.prev.next = x.next
  x.next.prev = x.prev
Pekerne i en dobbeltlenket liste
0 / 0

Rød peker settes i steget, oransje leses.

Grensetilfellene er sløyfet her også: står noden først eller sist i lista, finnes bare den ene naboen.

Eksamensoppgave Lenkede lister mot tabeller
Kontinuasjonseksamen, august 2025, oppgave 1

Sammenlign lenkede lister (linked lists) med tabeller (arrays). Hvilke fordeler og ulemper har de, sammenlignet med hverandre? Forklar.

Vis løsning Skjul løsning

En tabell ligger sammenhengende i minnet, så element nummer ii finnes med et regnestykke på adressen, i konstant tid. Prisen er at innsetting og sletting inne i tabellen må flytte alle elementene bak, altså Θ(n)\Theta(n) i verste tilfelle, og at størrelsen er fast: blir tabellen full, må en større allokeres og alt kopieres.

En lenket liste holder elementene som noder med pekere. Har du en peker til noden, setter du inn eller sletter i konstant tid uten å flytte noe, og lista vokser fritt. Til gjengjeld finnes ikke element nummer ii uten å følge ii pekere, så oppslag og søk er Θ(n)\Theta(n), og hver node koster ekstra minne til pekerne.

Svar
Tabell: konstant oppslag, men dyr innsetting midt i og fast størrelse. Lenket liste: konstant innsetting og sletting der du står, men lineært oppslag og pekere som koster minne.

Hashtabeller

Den enkleste formen for oppslag er direkte adressering: bruk nøkkelen selv som indeks, slik at T[k]T[k] inneholder elementet med nøkkel kk. Søk, innsetting og sletting er én tabelloperasjon hver. Prisen er at tabellen må være like stor som mengden av mulige nøkler, ikke antallet du faktisk lagrer.

En hashtabell beholder ideen, men kjører nøkkelen gjennom en hashfunksjon hh som krymper den til en gyldig indeks 0,…,m−10, \ldots, m-1. Den enkleste er h(k)=k mod mh(k) = k \bmod m. I praksis brukes gjerne funksjoner som blander bitene bedre, som multipliser-og-skift, og strenger hashes ved å kombinere tegnene aritmetisk. En god hashfunksjon gir alltid samme indeks for samme nøkkel, sprer nøklene så indeksene ser tilfeldig fordelt ut og regnes ut i konstant tid.

Med flere mulige nøkler enn plasser må to nøkler før eller siden dele indeks, en kollisjon. Kjeding løser det ved å la hver plass i tabellen holde en lenket liste. Innsetting setter elementet forrest i lista på plass h(k)h(k) og koster Θ(1)\Theta(1); søk hasher nøkkelen og leter lineært gjennom lista på plassen.

Å regne ut h(k)h(k) og slå opp plassen tar konstant tid, så resten av søket er letingen gjennom lista på plassen. Fordeler hashfunksjonen nøklene jevnt og uavhengig, får hver av de mm listene i snitt n/mn/m elementer. Det tallet er belastningsfaktoren α=n/m\alpha = n/m. Et søk som ikke finner nøkkelen, går gjennom en hel liste, forventet α\alpha elementer, så søket tar forventet Θ(1+α)\Theta(1 + \alpha) tid. Et søk som finner nøkkelen, stopper i snitt omtrent halvveis i lista og har samme grense.

Forventningen er ingen garanti. Ingenting hindrer at alle nn nøklene havner på samme plass, og da er søket et lineært søk i én liste, Θ(n)\Theta(n).

Regneeksempel Kjeding med h(k)=k mod 7h(k) = k \bmod 7

Sett inn nøklene 19, 36, 5, 21 og 12, i den rekkefølgen, i en tom hashtabell med m=7m = 7 plasser og kjeding. Hvor havner nøklene, og hvor mange nøkler må et søk etter 19 sammenligne?

Vis løsning Skjul løsning

19 mod 7=519 \bmod 7 = 5, 36 mod 7=136 \bmod 7 = 1, 5 mod 7=55 \bmod 7 = 5, 21 mod 7=021 \bmod 7 = 0 og 12 mod 7=512 \bmod 7 = 5. Plass 0 får 21 og plass 1 får 36, mens 19, 5 og 12 alle havner på plass 5. Innsetting skjer forrest i lista, så kjeden på plass 5 blir 12→5→1912 \to 5 \to 19, med den sist innsatte nøkkelen først. Et søk etter 19 hasher til plass 5 og må gjennom hele kjeden: først 12, så 5, så 19.

Hashtabellen med sju plasser etter innsettingene: 21 på plass 0, 36 på plass 1, og på plass 5 kjeden 12, 5, 19 med den sist innsatte forrest. Plassene 2, 3, 4 og 6 er tomme. Til høyre står utregningene, blant annet 19 mod 7 = 5, og røde tall 1, 2, 3 over kjeden viser søkerekkefølgen for nøkkel 19.
Tabellen etter de fem innsettingene. De røde tallene er sammenligningene i søket etter 19.
Svar
21 på plass 0, 36 på plass 1 og kjeden 12→5→1912 \to 5 \to 19 på plass 5. Søket etter 19 sammenligner tre nøkler.

Er nøkkelmengden statisk, kjent på forhånd og uforanderlig, kan du bygge en tabell der ingen nøkler kolliderer. Det kalles perfekt hashing og gir søk i garantert konstant tid, ikke bare forventet. Et typisk bruksområde er de reserverte ordene i en kompilator.

Tabellen har to nivåer. Først hashes de nn nøklene inn i m=nm = n plasser, med kollisjoner som vanlig. Plass jj, som fikk njn_j nøkler, får så sin egen sekundærtabell med nj2n_j^2 plasser og sin egen hashfunksjon, valgt slik at ingen av de njn_j nøklene kolliderer der. En slik funksjon er lett å finne. De njn_j nøklene danner færre enn nj2/2n_j^2/2 par. Trekkes funksjonen tilfeldig fra en familie der hvert par kolliderer med sannsynlighet høyst 1/nj21/n_j^2, er forventet antall kollisjoner derfor under 12\tfrac12. Da er sannsynligheten for minst én kollisjon også under 12\tfrac12, siden hver trekning med kollisjon bidrar med minst 1 til forventningen. Du trekker på nytt til funksjonen er kollisjonsfri, i snitt færre enn to ganger.

Et søk regner ut to hashverdier og sammenligner med én nøkkel, så det tar konstant tid uansett input. Sekundærtabeller på nj2n_j^2 plasser ser dyre ut, men med en jevn fordeling på første nivå er summen av alle nj2n_j^2 forventet under 2n2n, så tabellene tar O(n)O(n) plass til sammen.

Kostnadene så langt, samlet:

Verste tilfelle der ikke annet er sagt. Sletting forutsetter at du står ved elementet; å finne det først koster et søk. Hashtabellens søk er Θ(n)\Theta(n) i verste tilfelle.
StrukturSøkInnsettingSlettingMinimum
Usortert tabellΘ(n)\Theta(n)Θ(1)\Theta(1)Θ(1)\Theta(1)Θ(n)\Theta(n)
Sortert tabellΘ(lg⁡n)\Theta(\lg n)Θ(n)\Theta(n)Θ(n)\Theta(n)Θ(1)\Theta(1)
Lenket listeΘ(n)\Theta(n)Θ(1)\Theta(1)Θ(1)\Theta(1)Θ(n)\Theta(n)
Hashtabell med kjedingΘ(1+α)\Theta(1+\alpha) forventetΘ(1)\Theta(1)Θ(1)\Theta(1)Θ(n)\Theta(n)
Eksamensoppgave Vurder en hashfunksjon
Ordinær eksamen, desember 2024, oppgave 3

Hvordan vurderer du følgende hashfunksjon? Forklar kort.

h(k)=min⁡{m,2k}h(k) = \min\{m, 2^k\}

Spørsmålet er altså hvor god eller dårlig hashfunksjonen er, og hvorfor. Her er kk et positivt heltall og mm er størrelsen på hashtabellen.

Vis løsning Skjul løsning

En god hashfunksjon sprer nøklene jevnt over alle mm posisjonene. Denne gjør det motsatte. 2k2^k vokser så fort at bare nøklene med k<lg⁡mk < \lg m får hver sin posisjon, og det er færre enn lg⁡m\lg m av dem. Alle nøkler med k≥lg⁡mk \ge \lg m får h(k)=mh(k) = m og kolliderer i samme posisjon. Nesten hele tabellen står tom mens én lenke får nesten alle elementene, og søk blir Θ(n)\Theta(n) i stedet for forventet konstant tid.

Svar
Dårlig: høyst lg⁡m\lg m posisjoner brukes, og alle nøkler med k≥lg⁡mk \ge \lg m havner i posisjon mm.

Dynamiske tabeller

Blir en stakk eller en kø full, må den få mer plass: alloker en større tabell, kopier alt over, og fortsett der. En hashtabell med kjeding blir aldri full, men α=n/m\alpha = n/m vokser med nn, så den trenger også en større tabell for at søket skal holde seg forventet konstant. Ved utvidelsen hashes hver nøkkel på nytt med en funksjon for den nye størrelsen, i konstant tid per nøkkel, så den koster like mye som en kopiering. Kopieringen tar lineær tid, så den bør skje sjelden. Med tabelldobling allokeres dobbelt så stor plass hver gang tabellen er full, eller for hashtabellen hver gang α\alpha når en fast grense.

Én enkelt innsetting kan da koste lineær tid, men de dyre innsettingene blir sjeldnere jo større tabellen blir. Tell derfor det totale arbeidet over mange innsettinger.

Regneeksempel Seksten innsettinger med tabelldobling

En dynamisk tabell starter med plass til ett element og dobles hver gang den er full. Tell operasjonene for n=16n = 16 innsettinger, med én operasjon per innsetting og én per kopiert element.

Vis løsning Skjul løsning

Hver innsetting skriver ett element: 16 operasjoner. Kopiering skjer når tabellen er full, ved størrelsene 1, 2, 4 og 8, og koster like mange operasjoner som størrelsen:

1+2+4+8=151 + 2 + 4 + 8 = 15

Summen er toerpotens-summen, som alltid er én mindre enn neste toerpotens, her 16−116 - 1. Totalt blir det 16+15=3116 + 15 = 31 operasjoner.

Svar
31=2⋅16−131 = 2 \cdot 16 - 1 operasjoner, i snitt under 2 per innsetting.

For nn innsettinger skjer kopieringen ved størrelsene 1,2,4,…,2j1, 2, 4, \ldots, 2^j, der 2j2^j er den største toerpotensen under nn. Kopieringsarbeidet er da 1+2+⋯+2j=2j+1−11 + 2 + \cdots + 2^j = 2^{j+1} - 1, og det er mindre enn 2n2n, ettersom 2j<n2^j < n. Med de nn skrivingene i tillegg blir totalen under 3n3n, så hver innsetting koster i snitt færre enn 3 operasjoner. For n=16n = 16 gir grensen 4848, og tellingen over ga 3131.

Stolpediagram med én stolpe per innsetting fra 1 til 16. Hver stolpe har en grønnblå enhet nederst for selve skrivingen. Innsettingene 2, 3, 5 og 9 har i tillegg røde enheter for kopieringen, 1, 2, 4 og 8 av dem, så toppene blir høyere, men sjeldnere. En stiplet linje ligger på 3 operasjoner.
Figur 2. Kostnaden per innsetting med tabelldobling. Grønnblått er selve skrivingen, rødt er kopieringen når tabellen dobles, og den stiplede linjen ligger på 3 operasjoner.

Å regne ut det totale arbeidet for en hel operasjonsfølge og dele på antallet operasjoner heter aggregert analyse. Det er den enkleste formen for amortisert analyse, og den viser at tabelldobling koster amortisert konstant tid per innsetting. Analysen antok ingen sannsynlighet og ingen snill input: amortisert kjøretid er et snitt over operasjonene i én følge, i verste fall. Gjennomsnittstilfellet er noe annet, et snitt over instanser med en sannsynlighetsfordeling.

Multipop på en stakk

Multipop popper inntil kk elementer av en stakk. Én Multipop kan koste Θ(n)\Theta(n), så nn operasjoner ser ut som O(n2)O(n^2). Men hvert element må pushes før det kan poppes, så nn Push og Multipop gjør totalt Θ(n)\Theta(n) arbeid, amortisert Θ(1)\Theta(1) per operasjon.

Sjekk deg selv

En hashtabell med kjeding har fast m=100m = 100 plasser og blir aldri utvidet. Hashfunksjonen fordeler jevnt. Hvordan vokser forventet søketid når du setter inn nn nøkler?
Oppdatert 27. september 2026 Foreslå endring