Tilbake
1.2

1.2 Kjøretidsanalyse fra kode — løkketelling

Å lese O-kjøretiden rett ut av pseudokode ved å telle nøstede løkker og gjenkjenne halveringsmønstre — den mest garanterte Del 1-poengkilden.

50 min
8 oppgaver
Kjøretidsanalyse fra kodeløkketelling
Din fremgang i kapitlet
0 / 8 oppgaver

Forkunnskaper

- kap. 1.1 — hva OO betyr, vekstordningen og regelen om
det dominerende leddet. Alt i dette kapitlet ender med en forenkling som
bruker den.

Er løkker i seg selv ferskt stoff, hjelper disse:

- Løkker: for, while og range — hvordan en løkke teller,
og hva som avgjør hvor mange runder den går.
- Algoritmedefinisjon, pseudokode og kompleksitet (Big-O)
— samme framgangsmåte i et roligere tempo.

Notasjons- og pseudokodeliste

Løkke 1 — én løkke, og løkker inni løkker (ca. 12 min)

Start med det aller enkleste. Denne prosedyren legger sammen alle tallene i et
array:

Procedure Sum(A)
  Input:  array A med n tall, indeksert fra 0
  Output: summen av alle tallene i A
  n = A.length
  sum = 0
  for i = 0 to n-1:
      sum = sum + A[i]
  return sum

Løkka går nn runder. Inni løkka står én linje som gjør en fast mengde arbeid:
ett oppslag og én addisjon, uansett hvor stort arrayet er. Total: nn ganger et
konstant arbeid, altså O(n)O(n).

Det er hele metoden. Alt annet i dette kapitlet er varianter av det samme
spørsmålet: hvor mange ganger kjører den innerste linja?

📜Løkketellingsreglene

Fire regler dekker praktisk talt alle sjanger B-oppgavene.

Regel 1 — nøsting ganger. kk løkker nøstet inni hverandre, der hver går
over nn elementer, gir O(nk)O(n^k). To nøstede løkker er O(n2)O(n^2), tre er
O(n3)O(n^3), fem er O(n5)O(n^5).

Regel 2 — halvering og dobling gir en logaritme. En løkke der
tellevariabelen ganges eller deles på et tall større enn 1 i hver runde,
går ca. log2n\log_2 n runder — ikke nn. Dette gjelder j = j * 2 på vei opp mot
n, og m = m / 2 på vei ned mot 0.

Regel 3 — en konstant indre løkke endrer ingenting. Går en indre løkke til
et fast tall som ikke avhenger av n — 10, 100, 1000 — er den bare en
konstant faktor, og faktorer forsvinner i OO. Da er den nøstede løkka fortsatt
O(n)O(n), ikke O(n2)O(n^2).

Regel 4 — kode etter kode legges sammen. To løkker som står etter
hverandre gir O(f)+O(g)=O(max(f,g))O(f) + O(g) = O(\max(f, g)): du beholder det verste og kaster
det andre.

Framgangsmåten blir da alltid den samme, i fire steg:

1. Marker hver løkke og finn rekkevidden: går den til n, til et fast tall,
eller ganger den seg opp?
2. Gang sammen for nøsting.
3. Legg sammen for sekvens.
4. Behold det dominerende leddet, stryk konstantene, og skriv ett uttrykk.

✏️Eksempel 1: To nøstede løkker

Oppgi kjøretiden til denne prosedyren, som teller hvor mange par av elementer
som er like.

Procedure TellLikePar(A)
  Input:  array A med n tall, indeksert fra 0
  Output: antall par (i, j) der A[i] er lik A[j]
  n = A.length
  antall = 0
  for i = 0 to n-1:
      for j = 0 to n-1:
          if A[i] == A[j]:
              antall = antall + 1
  return antall

Steg 1 — rekkevidder. Den ytre løkka går nn runder. Den indre går også nn
runder, og — dette er poenget — den starter på nytt for hver runde i den ytre.

Steg 2 — nøsting ganger. nn=n2n \cdot n = n^2 runder i den innerste kroppen.

Steg 3 — sekvens. Ingen løkker etter hverandre her.

Steg 4 — forenkling. Kroppen gjør konstant arbeid (én sammenligning, av og
til en økning), så totalen er n2n^2 ganger en konstant.

Svar: O(n2)O(n^2), der nn er antall elementer i A.

Kontrolltelling. Kjører vi den innerste linja og teller, får vi 64 kjøringer
for n=8n = 8, 256 for n=16n = 16 og 1024 for n=32n = 32. Hver dobling av nn
firedobler tallet — nettopp signaturen til n2n^2. Den kontrollen tar fem
sekunder på kladdearket og avslører de fleste tellefeil.

📝Oppgave 1

(Innstegsoppgave, sjanger B — kjøretid fra kode, altså at du leser OO-uttrykket
rett ut av snutten.) Oppgi kjøretiden for hver av disse, uttrykt ved nn.

a) Én løkke for i = 0 to n-1: med en kropp som gjør ett oppslag.
b) Tre løkker nøstet inni hverandre, hver på formen for i = 0 to n-1:.
c) En løkke for i = 0 to n-1: etterfulgt av en helt separat løkke
for j = 0 to n-1:.

Løkke 2 — den konstante indre løkka (ca. 8 min)

Her ligger den fella som koster flest poeng på sjanger B, og den er lett å se
når du først vet om den.

Procedure NullstillNaboer(A)
  Input:  array A med n tall, indeksert fra 0
  Output: A der hvert element er erstattet av summen av seg selv og de ti neste
  n = A.length
  for i = 0 to n-1:
      s = 0
      for j = 0 to 9:
          if i + j < n:
              s = s + A[i + j]
      A[i] = s

Det ser ut som to nøstede løkker, og to nøstede løkker er O(n2)O(n^2). Men se på
den indre: den går fra 0 til 9. Alltid ti runder, uansett om arrayet har
åtte eller åtte millioner elementer.

Ti er en konstant. Den innerste linja kjører derfor 10n10n ganger, og 10n10n er
O(n)O(n).

Konstant indre løkke

En indre løkke som går til et fast tall i stedet for til nn — `for j = 0 to
9, for j = 0 to 99`. Den bidrar bare med en konstant faktor, og endrer derfor
ikke OO-klassen.

Kjennetegnet er at grensen ikke inneholder nn. Er grensen n/2 eller n-1, er
den derimot ikke konstant: n/2 runder er fortsatt O(n)O(n) runder.

✏️Eksempel 2: Konstant indre løkke

Oppgi kjøretiden for NullstillNaboer over, og forklar hvorfor svaret ikke er
O(n2)O(n^2).

Steg 1 — rekkevidder. Ytre løkke: nn runder. Indre løkke: 10 runder, fast,
uavhengig av nn.

Steg 2 — nøsting ganger. n10=10nn \cdot 10 = 10n kjøringer av den innerste
linja.

Steg 4 — forenkling. Faktoren 10 er en konstant og forsvinner.

Svar: O(n)O(n), der nn er antall elementer i A.

Hvorfor ikke O(n2)O(n^2)? Fordi O(n2)O(n^2) ville betydd at arbeidet firedobles
når nn dobles. Her skjer ikke det: telles den innerste linja, får vi 80
kjøringer for n=8n = 8, 160 for n=16n = 16 og 320 for n=32n = 32 — nøyaktig dobling
hver gang, altså lineært.

Prøv formen: «To nøstede løkker, men den indre går til et fast tall (10) og
ikke til nn, så den bidrar bare med en konstant faktor. Kjøretiden er O(n)O(n),
der nn er antall elementer i arrayet.» Det er tre setninger, og de inneholder
alt sensor ser etter: hva du telte, hvorfor den indre ikke teller, og hva nn er.

📝Oppgave 2
Sjanger B

Oppgi kjøretiden for hver snutt.

a)

for i = 0 to n-1:
    for j = 0 to 99:
        x = x + 1

b)

for i = 0 to n-1:
    for j = 0 to n-1:
        for k = 0 to 4:
            x = x + 1

Løkke 3 — når tellevariabelen dobles eller halveres (ca. 12 min)

— naturlig pausepunkt —

Den andre store gjenkjenningsoppgaven er løkker som ikke går ett steg av
gangen, men ganger eller deler.

j = 1
while j < n:
    x = x + 1
    j = j * 2

Med n=8n = 8 tar j verdiene 1, 2, 4 — tre runder, for ved j=8j = 8 er ikke
lenger j < n. Med n=16n = 16 blir det fire runder, med n=32n = 32 fem, med
n=128n = 128 sju.

Legg merke til hva som skjedde: vi doblet nn fra 8 til 16, og antall runder
økte med én. Doblet vi nn igjen, økte det med én til. Det er nøyaktig
definisjonen av en logaritme: antall doblinger fra 1 opp til nn er
log2n+1\lfloor \log_2 n \rfloor + 1.

Løkka er altså O(logn)O(\log n) — og det gjelder like mye den motsatte veien:

Doblingsløkke

En løkke der tellevariabelen ganges med en faktor større enn 1 i hver runde,
typisk j = j * 2, og som stopper når den passerer nn.

Den går ca. log2n\log_2 n runder, altså O(logn)O(\log n) — ikke O(n)O(n). Kjennetegnet er
at oppdateringen er en multiplikasjon, ikke en addisjon.

Halveringsløkke

En løkke der tellevariabelen deles på et tall større enn 1 i hver runde,
typisk m = m / 2, og som stopper når den når 0 eller 1.

Går også ca. log2n\log_2 n runder, altså O(logn)O(\log n). Dette er mønsteret bak
binærsøk og bak sift-operasjonene i en heap: hvert steg kaster halvparten av
det som er igjen.

✏️Eksempel 3: Løkke over n med en halvering inni

Oppgi kjøretiden:

Procedure Merkelig(A)
  Input:  array A med n tall, indeksert fra 0
  Output: en sum
  n = A.length
  s = 0
  for i = 0 to n-1:
      m = n
      while m > 1:
          s = s + 1
          m = m / 2
  return s

Steg 1 — rekkevidder. Ytre løkke: nn runder. Indre løkke: m starter på
nn og halveres til den er nede i 1, altså ca. log2n\log_2 n runder. Viktig: den
indre løkka starter på nytt med m = n i hver runde av den ytre, så antall
runder er det samme hver gang.

Steg 2 — nøsting ganger. nlog2nn \cdot \log_2 n.

Svar: O(nlogn)O(n \log n), der nn er antall elementer i A.

Kontrolltelling. For n=8n = 8 kjører den innerste linja 24 ganger
(838 \cdot 3, siden m går 8, 4, 2), for n=16n = 16 blir det 64 (16416 \cdot 4),
og for n=32n = 32 blir det 160 (32532 \cdot 5). Forholdstallet mellom nabotallene
er 2,67 og 2,5 — litt over dobling, som er nøyaktig hva nlognn \log n skal gi. Var
svaret O(n2)O(n^2), hadde tallene firedoblet seg; var det O(n)O(n), hadde de doblet
seg eksakt.

Merk hvor lett det er å bomme her. Ser du bare «to nøstede løkker», svarer
du O(n2)O(n^2) og taper poenget. Det er oppdateringslinja m = m / 2 som avgjør
alt, og den står nederst i løkka der øyet lett hopper over den. Les alltid
oppdateringslinja før du bestemmer deg.

📝Oppgave 3
Sjanger B

Oppgi kjøretiden for hver snutt, og skriv én setning om hva som
avgjør svaret.

a)

j = 1
while j < n:
    for i = 0 to n-1:
        x = x + 1
    j = j * 2

b)

i = 1
while i < n:
    j = 1
    while j < n:
        x = x + 1
        j = j * 2
    i = i * 2

Løkke 4 — sekvens, og løkker som starter der den ytre står (ca. 10 min)

To siste mønstre, og så har du hele repertoaret.

Sekvens. Står to løkker etter hverandre, legges de sammen — og den verste
overlever:

for i = 0 to n-1:
    for j = 0 to n-1:
        x = x + 1

for i = 0 to n-1:
    y = y + 1

Den første blokken er O(n2)O(n^2), den andre er O(n)O(n). Til sammen n2+nn^2 + n, og
det dominerende leddet er n2n^2. Svaret er O(n2)O(n^2).

Trekantløkker. Denne varianten dukker opp hver gang koden skal se på alle
par uten å telle samme par to ganger:

for i = 0 to n-1:
    for j = i to n-1:
        x = x + 1

Den indre løkka går nn runder når i er 0, n1n-1 runder når i er 1, og til
slutt bare 1 runde. Summen er n+(n1)++1=n(n+1)2\displaystyle n + (n-1) + \cdots + 1 = \frac{n(n+1)}{2}, som
er O(n2)O(n^2) — halvparten av arbeidet til to fulle løkker, men samme orden,
fordi 12\displaystyle \frac{1}{2} er en konstant.

Trekantløkke

En nøstet løkke der den indre starter (eller stopper) på den ytre løkkevariabelen:
for j = i to n-1 eller for j = 0 to i-1.

Den kjører n(n+1)2\displaystyle \frac{n(n+1)}{2} eller n(n1)2\displaystyle \frac{n(n-1)}{2} runder totalt, altså
O(n2)O(n^2). Halvparten av arbeidet til to fulle løkker, men samme OO-klasse —
konstantfaktoren 12\displaystyle \frac{1}{2} forsvinner.

Sumregelen for løkker etter hverandre
O(f)+O(g)=O(max(f,g))O(f) + O(g) = O(\max(f, g)): står to kodeblokker etter hverandre, legges
kjøretidene sammen, og det raskest voksende leddet overlever alene.

Praktisk: en O(n2)O(n^2)-blokk fulgt av tusen O(n)O(n)-blokker er fortsatt O(n2)O(n^2).

Produktregelen for nøstede løkker
kk løkker nøstet inni hverandre, som hver går over nn elementer, gir O(nk)O(n^k).
Antall runder ganges sammen, ett nivå om gangen innenfra og ut.

Merk at regelen gjelder antall runder, ikke antall løkker: en indre løkke som
går logn\log n runder ganger med logn\log n, ikke med nn.

✏️Eksempel 4: Sekvens og trekant i samme snutt

Oppgi kjøretiden:

Procedure Analyser(A)
  Input:  array A med n tall, indeksert fra 0
  Output: to tellinger
  n = A.length
  a = 0
  b = 0
  for i = 0 to n-1:
      for j = i to n-1:
          a = a + 1
  for i = 0 to n-1:
      b = b + 1
  return a, b

Steg 1 og 2 — den første blokken. Trekantløkke: den indre går nin - i
runder, og summen over alle ii er n(n+1)2\displaystyle \frac{n(n+1)}{2}. Det er O(n2)O(n^2).

Kontrolltelling: 36 kjøringer for n=8n = 8 (og 892=36\displaystyle \frac{8 \cdot 9}{2} = 36),
136 for n=16n = 16, 528 for n=32n = 32. Forholdet er ca. 3,9 per dobling, som
nærmer seg 4 — kvadratisk.

Steg 1 og 2 — den andre blokken. Én løkke over nn: O(n)O(n).

Steg 3 — sekvens. De to blokkene står etter hverandre:
n(n+1)2+n\displaystyle \frac{n(n+1)}{2} + n.

Steg 4 — forenkling. Det dominerende leddet er n22\displaystyle \frac{n^2}{2}, og
konstanten 12\displaystyle \frac{1}{2} forsvinner.

Svar: O(n2)O(n^2), der nn er antall elementer i A.

Poengtrapp-notat. Sensor ser først etter om du har riktig orden. En
kandidat som skriver «O(n2)O(n^2) fordi den doble løkka dominerer den enkle» får
full uttelling; en som skriver «O(n2+n)O(n^2 + n)» har rett i regnestykket, men feil
i formen og mister som regel et poeng. Forenkl alltid til ett ledd.

📝Oppgave 4
Sjanger B

Oppgi kjøretiden for hver snutt.

a)

for i = 0 to n-1:
    for j = 0 to n-1:
        x = x + 1
for i = 0 to n-1:
    for j = 0 to n-1:
        for k = 0 to n-1:
            y = y + 1

b)

for i = 0 to n-1:
    j = 0
    while j < n:
        x = x + 1
        j = j + 3

Løkke 5 — sammensatte snutter og et blikk på rekursjon (ca. 10 min)

På eksamen kommer sjanger B som regel med to eller tre nivåer der minst ett er
en dobling. Da er framgangsmåten uendret: ta ett nivå om gangen, innenfra og ut.

✏️Eksempel 5: Eksamensnivå — tre nivåer

Oppgi kjøretiden:

Procedure Rapport(A)
  Input:  array A med n tall, indeksert fra 0
  Output: en telling
  n = A.length
  c = 0
  for i = 0 to n-1:
      j = 1
      while j < n:
          for k = 0 to n-1:
              c = c + 1
          j = j * 2
  return c

Innenfra og ut, ett nivå om gangen.

- Innerst: for k = 0 to n-1 går nn runder.
- Midten: while j < n med j = j * 2 er en doblingsløkke, ca. log2n\log_2 n
runder. Den kjører den innerste løkka i hver runde: nlognn \log n.
- Ytterst: for i = 0 to n-1 går nn runder, og gjentar hele midten hver
gang: nnlognn \cdot n \log n.

Svar: O(n2logn)O(n^2 \log n), der nn er antall elementer i A.

Kontrolltelling. Den innerste linja kjører 192 ganger for n=8n = 8
(8388 \cdot 3 \cdot 8), 1024 for n=16n = 16 (1641616 \cdot 4 \cdot 16) og 5120 for
n=32n = 32 (3253232 \cdot 5 \cdot 32). Forholdstallet er 5,33 og 5,0 per dobling —
mer enn 4 (som ville vært n2n^2) og mindre enn 8 (som ville vært n3n^3).
Nettopp der n2lognn^2 \log n skal ligge.

Svarformen sensor vil ha, i tre setninger: «Den ytre løkka går nn runder.
Den midterste dobler j og går derfor ca. logn\log n runder. Den innerste går nn
runder. Til sammen O(n2logn)O(n^2 \log n), der nn er antall elementer i arrayet.»

Et sideblikk: rekursjon

Rekursjon — at en prosedyre kaller seg selv — er pensum i IN2010, og du
kommer til å skrive mye av den i tre- og grafkapitlene. Til kjøretidsanalyse
gjør vi likevel akkurat det samme som med løkker: teller arbeid, ikke setter
opp ligninger.

Ett mønster er verdt å kjenne igjen, mest fordi det har dukket opp i eldre sett:

Procedure Proc(n)
  Input:  et heltall n
  Output: ingenting; utfører et fast stykke arbeid per kall
  if n <= 0:
      return
  gjør en fast mengde arbeid
  Proc(n-1)
  Proc(n-1)

Hvert kall lager to nye kall med argumentet redusert med bare 1. Antall kall
dobles for hvert nivå: 1, 2, 4, 8 og så videre nedover nn nivåer. Totalt blir
det 2n+112^{n+1} - 1 kall — for n=10n = 10 er det 2047, for n=20n = 20 over to
millioner. Kjøretiden er O(2n)O(2^n).

Kontrolltelling: for n=1n = 1 til 5 er antall kall 3, 7, 15, 31 og 63 — hvert
tall er ett mindre enn en toerpotens, akkurat som formelen sier.

Avgrensning, og den er verdt penger: rekurrensligninger, substitusjonsmetoden
og de generelle teoremene for å løse slike ligninger er ikke IN2010-pensum. Møter du dem i en generell
algoritmebok, kan du hoppe over kapitlet. I dette faget analyserer du rekursjon
ved å telle: hvor mange kall blir det, og hvor mye arbeid gjør hvert kall?

Løkketelling

Fagets metode for kjøretidsanalyse: tell hvor mange ganger den innerste linja
kjører, gang for nøsting, legg sammen for sekvens, og behold det dominerende
leddet.

Metoden erstatter rekurrensligninger og de generelle teoremene for å løse dem,
som ikke er IN2010-pensum.

Grunnsteg

En operasjon som tar konstant tid uansett inputstørrelse: en tilordning, en
sammenligning, et array-oppslag, en aritmetisk operasjon.

Kjøretidsanalyse teller grunnsteg, ikke sekunder. Derfor er en løkkekropp med
tre grunnsteg like mye O(1)O(1) som en med ett.

Løkkas rekkevidde

Hvor mange runder løkka faktisk går. Det er dette du teller — ikke hvor mange
løkker det står i koden.

Tre typiske rekkevidder: til nn (gir nn runder), til et fast tall (gir
konstant), og med multiplikasjon eller divisjon av tellevariabelen (gir
ca. logn\log n runder).

Strammeste O-uttrykk

Det minste OO-uttrykket som fortsatt er en gyldig øvre grense. Svaret sensor
ber om.

En O(n)O(n)-algoritme er teknisk sett også O(n2)O(n^2), men svarer du O(n2)O(n^2),
mister du poeng. Forenkl til ett ledd uten konstanter: ikke O(3n2+n)O(3n^2 + n), men
O(n2)O(n^2).

Rekursjon med to kall per nivå

Mønsteret der en prosedyre kaller seg selv to ganger med argumentet redusert
med 1. Antall kall dobles per nivå, så totalen blir 2n+112^{n+1} - 1 kall, altså
O(2n)O(2^n).

Ikke å forveksle med rekursjon som halverer problemet i hvert kall — da får
du bare logn\log n nivåer, og kjøretiden blir langt lavere.

Kjøretid for én linje

En linje uten løkke og uten prosedyrekall er O(1)O(1): en tilordning, en
sammenligning, et oppslag A[i].

Unntaket du må se etter: en linje som kaller en annen prosedyre arver den
prosedyrens kjøretid. if IsSorted(A) inne i en løkke over nn er ikke O(n)O(n),
men O(n2)O(n^2).

Konstantfaktorer i kodeanalyse

Alt som ganger uttrykket med et fast tall: en indre løkke til 10, et steg på 3 i
stedet for 1, tre grunnsteg i løkkekroppen i stedet for ett.

Slike faktorer forsvinner i OO. Det som ikke forsvinner, er faktorer som
vokser med nn — og det er hele forskjellen mellom j = j + 3 (ingen effekt på
ordenen) og j = j * 2 (gir en logn\log n-faktor).

Analyse innenfra og ut

Framgangsmåten for nøstede løkker: finn antall runder for den innerste løkka
først, gang deretter med antall runder i nivået over, og fortsett utover.

Fordelen er at du aldri trenger å holde mer enn ett tall i hodet om gangen, og
at snutter med tre eller fire nivåer blir like enkle som snutter med to.

📝Oppgave 5
Sjanger B, eksamensnivå

Oppgi kjøretiden, og skriv den korte begrunnelsen
sensor ber om.

Procedure Bearbeid(A)
  Input:  array A med n tall, indeksert fra 0
  Output: en telling
  n = A.length
  c = 0
  for i = 0 to n-1:
      for j = 0 to n-1:
          k = 1
          while k < n:
              c = c + 1
              k = k * 2
  return c

📝Oppgave 6
Sjanger B

Denne snutten kaller en annen prosedyre. Oppgi kjøretiden, og si
hva du antar om den prosedyren.

Procedure Sjekk(A)
  Input:  array A med n tall, indeksert fra 0
  Output: antall prefikser av A som er sortert
  n = A.length
  c = 0
  for i = 0 to n-1:
      if IsSorted(A[0..i]):
          c = c + 1
  return c

Her er IsSorted(B) prosedyren fra kap. 1.1, som sjekker
om et array er sortert.

📝Oppgave 7
Sjanger B, eksamensnivå

Oppgi kjøretiden for hver av de to snuttene, og
forklar med én setning hvorfor de får ulikt svar selv om de ser nesten like ut.

a)

i = n
while i > 0:
    for j = 0 to n-1:
        x = x + 1
    i = i / 2

b)

i = n
while i > 0:
    for j = 0 to n-1:
        x = x + 1
    i = i - 1

📝Oppgave 8
Sjanger B, sammensatt

Oppgi kjøretiden for hele prosedyren.

Procedure Blandet(A)
  Input:  array A med n tall, indeksert fra 0
  Output: en telling
  n = A.length
  c = 0

  for i = 0 to n-1:
      for j = 0 to 49:
          c = c + 1

  for i = 0 to n-1:
      for j = i to n-1:
          c = c + 1

  p = 1
  while p < n:
      c = c + 1
      p = p * 2

  return c

Repetisjon — framgangsmåten 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.