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

04

Rangering i lineær tid

Kjernepensum

Merge-Sort bruker Θ(nlg⁡n)\Theta(n \lg n) tid, og ingen algoritme som bare sammenligner elementer parvis, kan gjøre det bedre i verste tilfelle. Grensen gjelder så lenge algoritmen bare sammenligner, og svaret skal være hele den sorterte tabellen. Ber du om mindre enn en sortert tabell, for eksempel bare det ii-ende minste elementet, eller antar du mer om inputen, for eksempel at verdiene er små heltall, går det i lineær tid.

Sorteringsgrensen

Insertion-Sort, Merge-Sort og Quicksort får bare vite noe om elementene gjennom tester som ai≤aja_i \le a_j. De er sammenligningsbaserte. Hver test har to utfall, så alt en slik algoritme kan gjøre på nn elementer, kan tegnes som et binært tre. Hver indre node er en sammenligning, hver løvnode er en ferdig rekkefølge, og én kjøring er stien fra rota til en løvnode.

Et binært tre med tre nivåer av sammenligninger. Rota sammenligner element 1 og 2. Venstre gren (mindre enn eller lik) sammenligner 2 og 3, høyre gren sammenligner 1 og 3. Seks løvnoder nederst er de seks rekkefølgene av tre elementer. En rød sti for inputen 6, 8, 3 går fra rota via 2:3 og 1:3 til løvnoden 3, 1, 2.
Figur 1. Beslutningstreet for Insertion-Sort på tre elementer. Noden i ⁣: ⁣ji\!:\!j tester ai≤aja_i \le a_j, og løvnoden ⟨3,1,2⟩\langle 3, 1, 2 \rangle betyr a3≤a1≤a2a_3 \le a_1 \le a_2. Den røde stien er kjøringen på ⟨6,8,3⟩\langle 6, 8, 3 \rangle.

Tre elementer kan stå i 3!=63! = 6 rekkefølger, og hver av dem må være en løvnode. Et binært tre med høyde 2 har plass til bare fire løvnoder, så minst én input krever tre sammenligninger.

Generelt må treet ha en løvnode for hver av de n!n! permutasjonene av inputen, for hver av dem er riktig svar for en eller annen input. Et binært tre med høyde hh har høyst 2h2^h løvnoder, så 2h≥n!2^h \ge n! og h≥lg⁡n!h \ge \lg n!. Høyden er antall sammenligninger langs den lengste stien, altså i verste tilfelle. Stirlings formel gir n!>(n/e)nn! > (n/e)^n, og dermed

lg⁡n!>nlg⁡n−nlg⁡e=Ω(nlg⁡n)\lg n! > n \lg n - n \lg e = \Omega(n \lg n)

Grensen er en nedre grense for verste tilfelle. Den sier at den dyreste instansen krever minst lg⁡n!\lg n! sammenligninger, og lg⁡n!=Ω(nlg⁡n)\lg n! = \Omega(n \lg n). Mer sier den ikke. En algoritme kan gjerne være verre, og i beste tilfelle kan den være bedre: Insertion-Sort bruker Θ(n)\Theta(n) på sortert input. Beste tilfelle er Ω(n)\Omega(n) uansett, for hvert element må leses. Gjennomsnittet er Ω(nlg⁡n)\Omega(n \lg n) som det verste, for en tilfeldig permutasjon krever i snitt lg⁡n!\lg n! ja/nei-svar å skille fra de andre.

Eksamensoppgave Kjøretiden til Init
Ordinær eksamen, desember 2024, oppgave 10

De følgende to trinnene utgjør tilsammen en sammenligningsbasert sorteringsalgoritme (comparison sort), som sorterer en tabell AA med nn elementer:

1 Init(A, n)
2 Main(A, n)

Kjøretiden til Main er O(n)O(n). Hva er kjøretiden til Init i verste tilfelle?

Om du har en usortert tabell AA av lengde nn og først kjører Init(AA, nn) og deretter Main(AA, nn), vil altså AA ende opp sortert, og både Init og Main baserer seg kun på å sammenligne elementene i AA.

Vis løsning Skjul løsning

Init og Main til sammen er en sammenligningsbasert sortering, så summen av kjøretidene deres er Ω(nlg⁡n)\Omega(n \lg n) i verste tilfelle. Main bidrar bare med O(n)O(n), som er mindre enn det. Resten må Init stå for.

Svar
Ω(nlg⁡n)\Omega(n \lg n)

Randomized-Select

Det ii-ende minste elementet i en tabell har rang ii. Rang 1 er minimum, rang nn maksimum, og medianen har rang ⌊(n+1)/2⌋\lfloor (n+1)/2 \rfloor, den nederste av de to midterste når nn er partall. Sortering og oppslag finner elementet i Θ(nlg⁡n)\Theta(n \lg n), men du ber om mindre enn en sortert tabell, så sorteringsgrensen gjelder ikke. Verdiene antas å være forskjellige.

Randomized-Select er Quicksort med ett rekursivt kall. Randomized-Partition velger en tilfeldig pivot og setter den på plass qq, og da er pivoten det k=q−p+1k = q - p + 1-ende minste i segmentet. Er i=ki = k, er pivoten svaret. Er i<ki < k, ligger svaret blant de små, og du søker etter rang ii der. Ellers ligger det blant de store, og der har det rang i−ki - k, for kk elementer er allerede mindre. Som i binærsøk stopper du straks du treffer, og søker bare videre i én av delene.

randomized_select.py
from random import randint

def randomized_partition(A, p, r):
  j = randint(p, r)
  A[j], A[r] = A[r], A[j]
  return partition(A, p, r)

def randomized_select(A, p, r, i):
  if p == r:
      return A[p]
  q = randomized_partition(A, p, r)
  k = q - p + 1
  if i == k:
      return A[q]
  elif i < k:
      return randomized_select(A, p, q - 1, i)
  else:
      return randomized_select(A, q + 1, r, i - k)
Randomized-Select
0 / 0

Søylene er tabellen, og målet er medianen, rang ⌊(n+1)/2⌋\lfloor (n+1)/2 \rfloor. Markøren ii står på plassen medianen har i sortert rekkefølge, og qq på pivotens plass. Det skyggelagte feltet er segmentet som er igjen, gule søyler er mindre enn pivoten og oransje større, og grønt er svaret.

Deler pivoten på midten hver gang, er T(n)=T(n/2)+nT(n) = T(n/2) + n: ett kall på halve instansen pluss lineært arbeid til delingen.

Regneeksempel Løs T(n)=T(n/2)+nT(n) = T(n/2) + n

Finn løsningen med iterasjon, og verifiser med substitusjon. T(1)=1T(1) = 1.

Vis løsning Skjul løsning

Iterasjon halverer leddet for hver ekspansjon:

T(n)=n+T(n/2)=n+n/2+T(n/4)=⋯=n+n/2+n/4+⋯+1=2n−1\begin{aligned} T(n) &= n + T(n/2) = n + n/2 + T(n/4) = \cdots \\ &= n + n/2 + n/4 + \cdots + 1 = 2n - 1 \end{aligned}

Substitusjon: anta T(k)=2k−1T(k) = 2k - 1 for alle k<nk < n. Da er T(n)=2(n/2)−1+n=2n−1T(n) = 2(n/2) - 1 + n = 2n - 1, og T(1)=2−1=1T(1) = 2 - 1 = 1 stemmer.

Svar
T(n)=2n−1=Θ(n)T(n) = 2n - 1 = \Theta(n). Summen er geometrisk, så alle de mindre kallene koster til sammen mindre enn det første.

Pivoten deler ikke på midten hver gang. Kall en pivot god når den har rang mellom n/4n/4 og 3n/43n/4 i segmentet på nn elementer. Da har begge sidene høyst 3n/43n/4 elementer, så segmentet du søker videre i, har krympet til høyst 3/43/4 av størrelsen. Halvparten av elementene er gode pivoter, så en tilfeldig pivot er god med sannsynlighet minst 1/21/2. Som når du kaster mynt til første kron, trengs da i forventning høyst to forsøk, altså to delinger, før segmentet krymper slik. Hver deling koster høyst lineær tid i segmentet, så forventet arbeid er høyst

2(n+34n+(34)2n+⋯ )=8n2\left(n + \tfrac34 n + \left(\tfrac34\right)^2 n + \cdots\right) = 8n

Dette betyr at forventet kjøretid er Θ(n)\Theta(n) for enhver input, ettersom regningen bare bruker de tilfeldige valgene og ikke rekkefølgen i inputen. Verste tilfelle er som for Quicksort. Havner pivoten ytterst hver gang, er T(n)=T(n−1)+n=Θ(n2)T(n) = T(n-1) + n = \Theta(n^2), men det krever uflaks med de tilfeldige valgene, og ingen input er mer utsatt enn andre.

Eksamensoppgave Lurviks Randomized-Select
Ordinær eksamen, desember 2022, oppgave 15

Din venn Lurvik har prøvd å skrive ned pseudokode for randomized select etter hukommelsen. Resultatet (algoritme 1) er ikke helt rett. Beskriv hva som må fikses for at algoritmen skal bli korrekt.

Algoritme 1 Lurviks versjon av randomized select
Randomized-Select(A, p, r, i)
1 if r ⩽ p
2     return A[p]
3 q = Randomized-Partition(A, p, r)
4 k = q − p + 1
5 if i == k
6     return A[q]
7 elseif i < k
8     Randomized-Select(A, p, q − 1, i)
9     Randomized-Select(A, q + 1, r, i − k)
Vis løsning Skjul løsning

De rekursive kallene på linje 8 og 9 kaster bort svaret sitt: verdien som kommer tilbake, må returneres, så begge linjene trenger return foran. Dessuten mangler linje 9 else. Uten else står begge kallene under elseif: når i<ki < k, kjøres begge etter hverandre, og når i>ki > k, kjøres ingen av dem.

8     return Randomized-Select(A, p, q − 1, i)
9 else return Randomized-Select(A, q + 1, r, i − k)

Betingelsen på linje 1 kan også strammes til if p == r, men den er ikke feil slik den står.

Svar
return foran begge de rekursive kallene, og else først på linje 9.

Select

Select fjerner tilfeldigheten og garanterer Θ(n)\Theta(n) i verste tilfelle. Den bruker medianen av medianene til små grupper som pivot, og den pivoten ligger alltid nær midten.

  1. Del i grupper på fem

    Del de nn elementene i n/5n/5 grupper med fem i hver, og sorter hver gruppe. Fem elementer sorteres i konstant tid, så det koster Θ(n)\Theta(n) til sammen. Er nn ikke delelig med fem, flyttes det minste elementet fremst og telles bort først, opptil fire ganger.

  2. Finn medianen av medianene

    Hver gruppe har en median, det midterste av de fem. Kall Select rekursivt på de n/5n/5 medianene og finn medianen av dem, xx.

  3. Del rundt xx

    Bytt xx inn bakerst og kjør Partition. Nå står xx på plass qq, og resten er som i Randomized-Select: svar, eller ett rekursivt kall på riktig side.

Garantien ligger i steg 2. Halvparten av gruppemedianene, n/10n/10 av dem, er større enn eller lik xx. Hver av dem og de to elementene over den i gruppen er større enn eller lik xx, så minst 3⋅n/103 \cdot n/10 elementer er større enn eller lik xx. På samme måte er minst 3n/103n/10 elementer mindre enn eller lik xx. Det rekursive kallet i steg 3 får derfor høyst 7n/107n/10 elementer.

Sju kolonner med fem sirkler i hver, sortert med størst øverst. Den midterste raden er gruppemedianene, tegnet med rød kant, og gruppene er ordnet slik at medianene vokser mot høyre. Sirkelen i midten, x, er rød. Et grønt felt dekker de tre øverste radene i de fire kolonnene til høyre og er merket større enn eller lik x, minst 3n/10. Et rødt felt dekker de tre nederste radene i de fire kolonnene til venstre og er merket mindre enn eller lik x, minst 3n/10.
Figur 2. Gruppene som kolonner, hver sortert med størst øverst, og ordnet etter median. Medianen av medianene xx har minst tre elementer over seg i hver av de høyre kolonnene og tre under seg i hver av de venstre.

Steg 2 er et rekursivt kall på n/5n/5 elementer, steg 3 et kall på høyst 7n/107n/10, og gruppesorteringen og Partition koster Θ(n)\Theta(n):

T(n)≤T(n/5)+T(7n/10)+Θ(n)T(n) \le T(n/5) + T(7n/10) + \Theta(n)

De to kallene får til sammen 9n/109n/10 elementer, mindre enn nn, så arbeidet krymper geometrisk nedover i rekursjonstreet, og rota dominerer.

Verifiser med substitusjon

Anta T(k)≤ckT(k) \le ck for alle k<nk < n, og la det lineære arbeidet være høyst anan. Da er

T(n)≤cn5+7cn10+an=9cn10+an≤cnT(n) \le \frac{cn}{5} + \frac{7cn}{10} + an = \frac{9cn}{10} + an \le cn

så snart c≥10ac \ge 10a. Grunntilfellet ordnes ved å velge cc stor nok for de små verdiene av nn.

Select gir også de kk minste elementene i lineær tid: finn elementet med rang kk og del rundt det. Da står elementet på plass kk med de k−1k - 1 mindre til venstre for seg, og de kk første plassene holder de kk minste. Konstanten i Select er stor, så i praksis brukes Randomized-Select. Select viser likevel at rangering kan gjøres i lineær tid også i verste tilfelle.

Stabilitet og Counting-Sort

Select og Randomized-Select går under sorteringsgrensen ved å be om mindre enn en sortert tabell. Den andre veien er å anta mer om inputen: verdiene er heltall i et lite område 0..k0 .. k. Da trenger du ikke sammenligne dem. Du kan bruke dem som indekser.

Elementer bærer ofte mer enn nøkkelen de sorteres på. Sorterer du en alfabetisk studentliste på karakter, kan studentene med samme karakter komme ut i en annen rekkefølge enn de sto i. En sorteringsalgoritme er stabil når like nøkler står i samme innbyrdes rekkefølge i output som i input, og da står studentene med samme karakter fortsatt alfabetisk. Når nøkkelen er hele elementet, spiller stabiliteten ingen rolle. Insertion-Sort og Merge-Sort er stabile når de skrives slik at et element flyttes bare forbi strengt større, og flettingen tar fra venstre ved likhet. Quicksort er det ikke, for Partition bytter elementer forbi hverandre.

Counting-Sort teller først hvor mange ganger hver verdi forekommer, C[v]C[v]. Så gjøres tellingen kumulativ, slik at C[v]C[v] er antall elementer som er mindre enn eller lik vv. I den sorterte tabellen BB står de C[v−1]C[v-1] elementene som er mindre enn vv, først, og vv-ene fyller plassene etter dem, til og med plass nummer C[v]C[v]. Til slutt går du baklengs gjennom AA: legg A[j]A[j] på plass nummer C[A[j]]C[A[j]] i BB, og trekk én fra C[A[j]]C[A[j]]. Neste like verdi havner ett hakk til venstre.

counting_sort.py
def counting_sort(A, n, k):
  B = [None] * n
  C = [0] * (k + 1)
  for j in range(n):
      C[A[j]] = C[A[j]] + 1
  for i in range(1, k + 1):
      C[i] = C[i] + C[i - 1]
  for j in range(n - 1, -1, -1):
      C[A[j]] = C[A[j]] - 1  # plass nummer c har indeks c - 1
      B[C[A[j]]] = A[j]
  return B
Counting-Sort
0 / 0

Radene er inputen AA med heltall i 0..50 .. 5, tellingene CC og outputen BB. Like verdier har merker (2, 2′, 2″) så rekkefølgen kan følges, og de uthevede cellene er dem steget leser eller skriver.

Den baklengse løkka gjør algoritmen stabil. Den siste vv-en i AA behandles først og får den siste plassen i sin blokk, og hver tidligere vv legges til venstre for den. Rekkefølgen fra AA er bevart. Kjøretiden er Θ(n+k)\Theta(n + k), to løkker over AA og én over CC. Er k=O(n)k = O(n), er kjøretiden lineær. Sorteringsgrensen brytes ikke, for ingen elementer sammenlignes. Verdiene brukes som indekser, og indekseringen krever at de er små heltall.

Eksamensoppgave Snu løkka i Counting-Sort
Kontinuasjonseksamen, august 2024, oppgave 3

En av løkkene i Counting-Sort går fra nn ned til 1 (for j=nj = n downto 1). Hva er konsekvensen av å skifte retning på løkka (for j=1j = 1 to nn)?

Merk: Her kreves ingen forklaring.

Vis løsning Skjul løsning

Den siste løkka plasserer elementene bakfra, og det er det som gjør at like nøkler beholder rekkefølgen sin fra AA. Snur du løkka, plasseres like nøkler i motsatt rekkefølge av den de kom i.

Svar
Sorteringen blir ustabil.

Radix-Sort

Er verdiområdet stort, blir CC for stor. Men et tall med dd siffer har dd små nøkler. Radix-Sort sorterer stabilt på ett siffer om gangen, det minst signifikante først, med Counting-Sort med k=9k = 9 som subrutine for desimale siffer.

radix_sort.py
def radix_sort(A, n, d):
  for i in range(d):
      A = stable_sort_on_digit(A, n, i)  # Counting-Sort med k = 9
  return A
Radix-Sort
0 / 0

Venstre kolonne er tallene før passet, høyre kolonne outputen fra passet. Sifrene telles fra 0 bakfra som i koden, sifferet passet sorterer på er uthevet, og grønne siffer er ferdig behandlet.

Etter ii pass er tallene sortert på de ii siste sifrene. Denne invarianten holder ved induksjon, og induksjonssteget er stabiliteten. Pass nummer ii ordner tallene etter det ii-ende sifferet bakfra. To tall med ulikt siffer der havner dermed i riktig rekkefølge, og to tall med likt siffer beholder rekkefølgen fra før, som var sortert på de i−1i - 1 siste sifrene. Til sammen gir passet nøyaktig den rekkefølgen de ii siste sifrene gir.

Radix-Sort trenger begge deler: det minst signifikante sifferet først og en stabil subrutine. Starter du med det mest signifikante sifferet, ødelegger neste pass ordningen fra det forrige, for passet sorterer på et mindre viktig siffer uten å ta hensyn til det viktigere. Sorterer du 13 og 21 først på det fremste sifferet og så stabilt på det bakerste, ender du med 21, 13. Og er subrutinen ustabil, mister to tall med likt siffer ordningen fra de lavere sifrene.

Kjøretiden er dd pass med Θ(n+k)\Theta(n + k) hver, altså Θ(d(n+k))\Theta(d(n + k)). Er dd og kk konstanter, som for sekssifrede desimaltall, er kjøretiden lineær. Et siffer trenger ikke være et desimalsiffer. Datoer sorteres stabilt på dag, så måned, så år. Etter hvert pass er datoene sortert på feltene som er behandlet, med samme induksjonssteg som for sifrene.

Bucket-Sort

Bucket-Sort antar at inputen er tilfeldige tall, uniformt fordelt i [0,1)[0, 1). Del intervallet i nn like bøtter, og legg A[j]A[j] i bøtte nummer ⌊n⋅A[j]⌋\lfloor n \cdot A[j] \rfloor. Det er hashing med kjeding, men hashfunksjonen bevarer ordningen: alt i bøtte ii er mindre enn alt i bøtte i+1i + 1. Sorter hver bøtte med Insertion-Sort, og sett bøttene etter hverandre.

bucket_sort.py
def bucket_sort(A, n):
  B = [[] for _ in range(n)]
  for j in range(n):
      B[int(n * A[j])].append(A[j])
  for i in range(n):
      insertion_sort(B[i], len(B[i]))
  return [x for bucket in B for x in bucket]

Insertion-Sort på en bøtte med nin_i elementer koster O(ni2)O(n_i^2), så det er forventningen av ni2n_i^2 som avgjør, ikke at bøtta i snitt får ett element. Med uniform fordeling havner hvert element i bøtte ii med sannsynlighet 1/n1/n, uavhengig av de andre, og da er E[ni2]=2−1/nE[n_i^2] = 2 - 1/n. Summert over de nn bøttene er forventet arbeid i Insertion-Sort O(n)O(n), så hele sorteringen tar forventet Θ(n)\Theta(n).

Forventningen til ni2n_i^2

Skriv ni=∑jXjn_i = \sum_j X_j, der Xj=1X_j = 1 når element jj havner i bøtte ii, og ellers 00. Da er Xj2=XjX_j^2 = X_j, og XjX_j og XkX_k er uavhengige for j≠kj \ne k, så

E[ni2]=∑jE[Xj2]+∑j≠kE[XjXk]=n⋅1n+n(n−1)⋅1n2=2−1nE[n_i^2] = \sum_j E[X_j^2] + \sum_{j \ne k} E[X_j X_k] = n \cdot \tfrac1n + n(n-1) \cdot \tfrac1{n^2} = 2 - \tfrac1n

Forventningen her er et snitt over tilfeldige inputer, altså gjennomsnittstilfellet. Bucket-Sort gjør ingen tilfeldige valg selv, så en input der alt havner i én bøtte, gir alltid Insertion-Sort på hele inputen, Θ(n2)\Theta(n^2). Randomized-Select og Randomized-Quicksort har forventet kjøretid over sine egne tilfeldige valg, for enhver input, og den garantien er sterkere.

Fem veier under sorteringsgrensen. De to første svekker kravet til output, de tre siste styrker kravet til input.
AlgoritmeForutsetningKjøretid
Randomized-Selectbare rang iiforventet Θ(n)\Theta(n), verste Θ(n2)\Theta(n^2)
Selectbare rang iiΘ(n)\Theta(n)
Counting-Sortheltall i 0..k0 .. kΘ(n+k)\Theta(n + k)
Radix-Sortdd siffer i 0..k0 .. kΘ(d(n+k))\Theta(d(n + k))
Bucket-Sortuniformt fordelt i [0,1)[0, 1)forventet Θ(n)\Theta(n), verste Θ(n2)\Theta(n^2)

Sjekk deg selv

Du skal sortere nn heltall i området 0..n3−10 .. n^3 - 1. Hvilken algoritme gjør det i O(n)O(n) tid i verste tilfelle?
Oppdatert 27. september 2026 Foreslå endring