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.
Dette er den billigste poengkilden i hele faget.
Sjanger B — kjøretid fra kode, altså at du får en kodesnutt og skal svare med
ett -uttrykk, har kommet i alle sju settene (100 %), ofte med én til to
oppgaver per sett. Det er ren mekanikk: ingen algoritme skal gjenkjennes, ingen
struktur skal håndkjøres. Du teller løkker.
Tre ting fra sensorveiledningene er verdt å vite på forhånd:
- Poeng gis for riktig orden selv om koden har en syntaksfeil. Er du usikker
på om snutten «går», svarer du likevel med ordenen.
- Svaret skal være ett uttrykk, det strammeste. Ikke , men
.
- Rekurrensligninger er ikke IN2010-pensum. Kjøretid forenkles her ved
løkketelling, også for rekursive prosedyrer. Du skal aldri sette opp en
rekurrensligning eller bruke et generelt teorem for å løse den.
Prioritet: høyeste. Dette må sitte, og det tar en kveld å få det til å sitte.
Forkunnskaper
- kap. 1.1 — hva 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.
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 sumLøkka går 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: ganger et
konstant arbeid, altså .
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?
Fire regler dekker praktisk talt alle sjanger B-oppgavene.
Regel 1 — nøsting ganger. løkker nøstet inni hverandre, der hver går
over elementer, gir . To nøstede løkker er , tre er
, fem er .
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. runder — ikke . Dette gjelder j = j * 2 på vei opp motn, 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 . Da er den nøstede løkka fortsatt
, ikke .
Regel 4 — kode etter kode legges sammen. To løkker som står etter
hverandre gir : 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.
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 antallrunder, og — dette er poenget — den starter på nytt for hver runde i den ytre.
Steg 2 — nøsting ganger. 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 ganger en konstant.
Svar: , der er antall elementer i A.
Kontrolltelling. Kjører vi den innerste linja og teller, får vi 64 kjøringer
for , 256 for og 1024 for . Hver dobling av
firedobler tallet — nettopp signaturen til . Den kontrollen tar fem
sekunder på kladdearket og avslører de fleste tellefeil.
(Innstegsoppgave, sjanger B — kjøretid fra kode, altså at du leser -uttrykket
rett ut av snutten.) Oppgi kjøretiden for hver av disse, uttrykt ved .
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økkefor 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] = sDet ser ut som to nøstede løkker, og to nøstede løkker er . 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 ganger, og er
.
En indre løkke som går til et fast tall i stedet for til — `for j = 0 to
9, for j = 0 to 99`. Den bidrar bare med en konstant faktor, og endrer derfor
ikke -klassen.
Kjennetegnet er at grensen ikke inneholder . Er grensen n/2 eller n-1, er
den derimot ikke konstant: n/2 runder er fortsatt runder.
Oppgi kjøretiden for NullstillNaboer over, og forklar hvorfor svaret ikke er
.
uavhengig av .
Steg 2 — nøsting ganger. kjøringer av den innerste
linja.
Steg 4 — forenkling. Faktoren 10 er en konstant og forsvinner.
Svar: , der er antall elementer i A.
Hvorfor ikke ? Fordi ville betydd at arbeidet firedobles
når dobles. Her skjer ikke det: telles den innerste linja, får vi 80
kjøringer for , 160 for og 320 for — 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 , så den bidrar bare med en konstant faktor. Kjøretiden er ,
der 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 er.
Oppgi kjøretiden for hver snutt.
a)
for i = 0 to n-1:
for j = 0 to 99:
x = x + 1b)
for i = 0 to n-1:
for j = 0 to n-1:
for k = 0 to 4:
x = x + 1Lø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 * 2Med tar j verdiene 1, 2, 4 — tre runder, for ved er ikke
lenger j < n. Med blir det fire runder, med fem, med
sju.
Legg merke til hva som skjedde: vi doblet fra 8 til 16, og antall runder
økte med én. Doblet vi igjen, økte det med én til. Det er nøyaktig
definisjonen av en logaritme: antall doblinger fra 1 opp til er
.
Løkka er altså — og det gjelder like mye den motsatte veien:
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 .
Den går ca. runder, altså — ikke . Kjennetegnet er
at oppdateringen er en multiplikasjon, ikke en addisjon.
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. runder, altså . Dette er mønsteret bak
binærsøk og bak sift-operasjonene i en heap: hvert steg kaster halvparten av
det som er igjen.
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 sm starter påog halveres til den er nede i 1, altså ca. runder. Viktig: den
indre løkka starter på nytt med
m = n i hver runde av den ytre, så antallrunder er det samme hver gang.
Steg 2 — nøsting ganger. .
Svar: , der er antall elementer i A.
Kontrolltelling. For kjører den innerste linja 24 ganger
(, siden m går 8, 4, 2), for blir det 64 (),
og for blir det 160 (). Forholdstallet mellom nabotallene
er 2,67 og 2,5 — litt over dobling, som er nøyaktig hva skal gi. Var
svaret , hadde tallene firedoblet seg; var det , hadde de doblet
seg eksakt.
Merk hvor lett det er å bomme her. Ser du bare «to nøstede løkker», svarer
du 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.
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 * 2b)
i = 1
while i < n:
j = 1
while j < n:
x = x + 1
j = j * 2
i = i * 2Lø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 + 1Den første blokken er , den andre er . Til sammen , og
det dominerende leddet er . Svaret er .
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 + 1Den indre løkka går runder når i er 0, runder når i er 1, og til
slutt bare 1 runde. Summen er , som
er — halvparten av arbeidet til to fulle løkker, men samme orden,
fordi er en konstant.
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 eller runder totalt, altså
. Halvparten av arbeidet til to fulle løkker, men samme -klasse —
konstantfaktoren forsvinner.
kjøretidene sammen, og det raskest voksende leddet overlever alene.
Praktisk: en -blokk fulgt av tusen -blokker er fortsatt .
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 runder ganger med , ikke med .
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, brunder, og summen over alle er . Det er .
Kontrolltelling: 36 kjøringer for (og ),
136 for , 528 for . Forholdet er ca. 3,9 per dobling, som
nærmer seg 4 — kvadratisk.
Steg 1 og 2 — den andre blokken. Én løkke over : .
Steg 3 — sekvens. De to blokkene står etter hverandre:
.
Steg 4 — forenkling. Det dominerende leddet er , og
konstanten forsvinner.
Svar: , der er antall elementer i A.
Poengtrapp-notat. Sensor ser først etter om du har riktig orden. En
kandidat som skriver « fordi den doble løkka dominerer den enkle» får
full uttelling; en som skriver «» har rett i regnestykket, men feil
i formen og mister som regel et poeng. Forenkl alltid til ett ledd.
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 + 1b)
for i = 0 to n-1:
j = 0
while j < n:
x = x + 1
j = j + 3Lø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.
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- Innerst: for k = 0 to n-1 går runder.
- Midten: while j < n med j = j * 2 er en doblingsløkke, ca.
runder. Den kjører den innerste løkka i hver runde: .
- Ytterst: for i = 0 to n-1 går runder, og gjentar hele midten hver
gang: .
Svar: , der er antall elementer i A.
Kontrolltelling. Den innerste linja kjører 192 ganger for
(), 1024 for () og 5120 for
(). Forholdstallet er 5,33 og 5,0 per dobling —
mer enn 4 (som ville vært ) og mindre enn 8 (som ville vært ).
Nettopp der skal ligge.
Svarformen sensor vil ha, i tre setninger: «Den ytre løkka går runder.
Den midterste dobler j og går derfor ca. runder. Den innerste går
runder. Til sammen , der 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 nivåer. Totalt blir
det kall — for er det 2047, for over to
millioner. Kjøretiden er .
Kontrolltelling: for 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?
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.
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 som en med ett.
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 (gir runder), til et fast tall (gir
konstant), og med multiplikasjon eller divisjon av tellevariabelen (gir
ca. runder).
Det minste -uttrykket som fortsatt er en gyldig øvre grense. Svaret sensor
ber om.
En -algoritme er teknisk sett også , men svarer du ,
mister du poeng. Forenkl til ett ledd uten konstanter: ikke , men
.
Mønsteret der en prosedyre kaller seg selv to ganger med argumentet redusert
med 1. Antall kall dobles per nivå, så totalen blir kall, altså
.
Ikke å forveksle med rekursjon som halverer problemet i hvert kall — da får
du bare nivåer, og kjøretiden blir langt lavere.
En linje uten løkke og uten prosedyrekall er : 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 er ikke ,
men .
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 . Det som ikke forsvinner, er faktorer som
vokser med — og det er hele forskjellen mellom j = j + 3 (ingen effekt på
ordenen) og j = j * 2 (gir en -faktor).
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.
Alle disse er dokumentert som poengtap på sjanger B.
- Å telle en konstant indre løkke som . for j = 0 to 9 er ti runder,
ikke runder. Sjekk alltid om øvre grense inneholder .
- Å overse at en løkke dobler eller halverer. j = j * 2 gir
runder, ikke . Oppdateringslinja står ofte nederst i løkka, der øyet hopper
over den — les den før du bestemmer deg.
- Å gange sekvensielle løkker i stedet for å legge dem sammen. To løkker
etter hverandre er , ikke . Se på innrykket.
- Å svare med et usimplifisert uttrykk. er ikke feil
regnestykke, men det er feil form. Svaret skal være .
- Å la være å svare fordi snutten har en syntaksfeil. Sensorveiledningene er
eksplisitte: riktig orden gir poeng selv om koden ikke ville kompilert. Svar
alltid.
- Å bruke uten å si hva det er. Dette er felle #10 i bokas register
— å oppgi kjøretid eller drøfte uten å definere problemstørrelsen. Én setning
holder: « er antall elementer i A».
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 cDenne 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 cHer er IsSorted(B) prosedyren fra kap. 1.1, som sjekker
om et array er sortert.
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 / 2b)
i = n
while i > 0:
for j = 0 to n-1:
x = x + 1
i = i - 1Oppgi 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 cDette 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.