Lær hva rekursjon er og forstå grunnleggende algoritmekompleksitet med Big O-notasjon.
Rekursjon og algoritmekompleksitet
Tenk deg at du står framfor eit spegel som reflekterer eit anna spegel. Du ser deg sjølv, som ser deg sjølv, som ser deg sjølv, i ei tilsynelatande uendeleg rekkje av refleksjonar. Dette er ein visuell metafor for rekursjon: noko som refererer til seg sjølv.
I programmering er rekursjon ein kraftig teknikk der ein funksjon løyser eit problem ved å kalle seg sjølv med eit enklare delproblem. Det kan verke magisk, kanskje til og med litt forvirrande, den fyrste gongen du ser det, men det er eigentleg eit elegant verktøy for å løyse problem som naturleg kan delast inn i like, men enklare delproblem.
I tillegg til rekursjon skal vi i dette kapittelet sjå på algoritmekompleksitet, ein måte å måle og samanlikne effektiviteten til algoritmar på. Vi har allereie sett at binærsøk er raskare enn lineært søk, og at sorteringsalgoritmar har ulik yting. Big O-notasjonen gir oss eit presist språk for å snakke om desse skilnadene.
Rekursjon med fakultet
Det klassiske dømet på rekursjon er utrekning av fakultet. Fakultet av n (skrive n!) er produktet av alle positive heiltal frå 1 til n:
- 5! = 5 × 4 × 3 × 2 × 1 = 120
- 3! = 3 × 2 × 1 = 6
- 1! = 1
- 0! = 1 (per definisjon)
Det finst ein elegant rekursiv definisjon:
- n! = n × (n-1)! for n > 0
- 0! = 1 (basistilfelle)
Altså: 5! = 5 × 4!, og 4! = 4 × 3!, og så vidare til vi når 0! = 1.
Iterativ løysing (med løkke):
def fakultet_iterativ(n):
resultat = 1
for i in range(1, n + 1):
resultat *= i
return resultatRekursiv løysing:
def fakultet(n):
# Basistilfelle
if n == 0:
return 1
# Rekursivt tilfelle
return n * fakultet(n - 1)
print(fakultet(5)) # 120
print(fakultet(0)) # 1Kva skjer når vi kallar fakultet(4)?
fakultet(4) = 4 * fakultet(3)
= 3 * fakultet(2)
= 2 * fakultet(1)
= 1 * fakultet(0)
= 1 ← basistilfelle!
= 1 * 1 = 1
= 2 * 1 = 2
= 3 * 2 = 6
= 4 * 6 = 24Funksjonen kallar seg sjølv med stadig mindre verdiar av n til han når basistilfellet (n == 0). Deretter vert resultatet «rulla» tilbake opp gjennom alle kalla.
Skriv ein rekursiv funksjon som reknar ut summen av tala frå 1 til n.
- sum(n) = n + sum(n-1) for n > 0
- sum(0) = 0 (basistilfelle)
def sum_rekursiv(n):
# Basistilfelle
if n == 0:
return 0
# Rekursivt tilfelle
return n + sum_rekursiv(n - 1)
print(sum_rekursiv(5)) # 15 (1+2+3+4+5)
print(sum_rekursiv(100)) # 5050Oppruling av sum_rekursiv(5):
sum_rekursiv(5) = 5 + sum_rekursiv(4)
= 5 + 4 + sum_rekursiv(3)
= 5 + 4 + 3 + sum_rekursiv(2)
= 5 + 4 + 3 + 2 + sum_rekursiv(1)
= 5 + 4 + 3 + 2 + 1 + sum_rekursiv(0)
= 5 + 4 + 3 + 2 + 1 + 0
= 15Merk: Gauss fann ein formel for denne summen: n × (n+1) / 2. For n = 100 gir det 100 × 101 / 2 = 5050. Men den rekursive versjonen illustrerer prinsippet godt.
Fibonacci-tala
Fibonacci-tallrekkja er eit berømt døme som passar naturleg til rekursjon. Kvart tal er summen av dei to føregåande:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...
Rekursiv definisjon:
- fib(0) = 0
- fib(1) = 1
- fib(n) = fib(n-1) + fib(n-2) for n > 1
def fibonacci(n):
# Basistilfeller
if n == 0:
return 0
if n == 1:
return 1
# Rekursivt tilfelle
return fibonacci(n - 1) + fibonacci(n - 2)
# Skriv ut de 10 første Fibonacci-tallene
for i in range(10):
print(f"fib({i}) = {fibonacci(i)}")Kjøring:
fib(0) = 0
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34Fibonacci-rekursjonen har eit viktig problem: han er svært ineffektiv! For å rekne ut fibonacci(5) vert fibonacci(3) kalla to gonger, fibonacci(2) tre gonger, og så vidare. Talet på kall veks eksponentielt. fibonacci(40) kan ta fleire sekund, og fibonacci(100) ville ta lengre tid enn levetida til universet!
Effektiv iterativ versjon:
def fibonacci_effektiv(n):
if n <= 1:
return n
forrige = 0
naavaerende = 1
for _ in range(2, n + 1):
forrige, naavaerende = naavaerende, forrige + naavaerende
return naavaerende
print(fibonacci_effektiv(50)) # 12586269025 (umiddelbart!)Denne iterative versjonen reknar ut fibonacci(50) umiddelbart, medan den rekursive versjonen ville bruke ufatteleg lang tid. Dette illustrerer at rekursjon ikkje alltid er den beste løysinga, sjølv om han er elegant.
Python har ei standard rekursjonsgrense på omtrent 1000 kall. Dersom ein rekursiv funksjon kallar seg sjølv meir enn 1000 gonger utan å nå basistilfellet, krasjar programmet med ein RecursionError: maximum recursion depth exceeded. Dette er ein tryggleiksmekanisme for å hindre at programmet brukar alt minne. For problem som krev meir enn 1000 rekursive kall, bør du bruke ei iterativ løysing i staden.
Implementer binærsøk som ein rekursiv funksjon i Python.
def binaersok_rekursiv(liste, sokeord, lav, hoy):
"""Rekursivt binærsøk."""
# Basistilfelle: elementet finnes ikke
if lav > hoy:
return -1
midt = (lav + hoy) // 2
# Basistilfelle: elementet er funnet
if liste[midt] == sokeord:
return midt
# Rekursivt tilfelle: søk i riktig halvdel
if liste[midt] < sokeord:
return binaersok_rekursiv(liste, sokeord, midt + 1, hoy)
else:
return binaersok_rekursiv(liste, sokeord, lav, midt - 1)
# Bruk
sortert = [2, 5, 8, 13, 17, 21, 25, 30]
resultat = binaersok_rekursiv(sortert, 17, 0, len(sortert) - 1)
print(f"Fant 17 på posisjon {resultat}") # 4
resultat = binaersok_rekursiv(sortert, 10, 0, len(sortert) - 1)
print(f"Fant 10 på posisjon {resultat}") # -1Gjennomgang for å finne 17 i [2, 5, 8, 13, 17, 21, 25, 30]:
1. Kall 1: lav=0, hoy=7, midt=3, liste[3]=13 < 17 → søk høgre
2. Kall 2: lav=4, hoy=7, midt=5, liste[5]=21 > 17 → søk venstre
3. Kall 3: lav=4, hoy=4, midt=4, liste[4]=17 == 17 → Funne!
Den rekursive versjonen er elegant fordi han gjenspeglar algoritmen direkte: halvér, vel side, gjenta. Kvart rekursivt kall reduserer søkeområdet til det halve.
Forstå Big O-notasjon
Big O-notasjon gir oss eit språk for å snakke om kor raskt ein algoritme veks i tid eller plass når inndataen vert større. Vi bryr oss ikkje om nøyaktig tal på operasjonar, men om veksthastigheita.
Dei vanlegaste kompleksitetsklassane (frå raskast til tregast):
| Big O | Namn | Døme | 1000 element |
|---|---|---|---|
| O(1) | Konstant | Tilgang til listeelement via indeks | 1 operasjon |
| O(log n) | Logaritmisk | Binærsøk | ~10 operasjonar |
| O(n) | Lineær | Lineært søk | 1 000 operasjonar |
| O(n log n) | Linearitmisk | Pythons innebygde sortering | ~10 000 operasjonar |
| O(n²) | Kvadratisk | Boblesortering | 1 000 000 operasjonar |
| O(2ⁿ) | Eksponentiell | Naiv Fibonacci-rekursjon | ~10³⁰⁰ operasjonar |
Kvifor er dette viktig?
Tenk deg at du har ein algoritme som fungerer fint for 100 element, men brukarane dine har 1 000 000 element:
- O(n): 1 000 000 operasjonar – tek millisekund
- O(n log n): ~20 000 000 operasjonar – tek sekund
- O(n²): 1 000 000 000 000 operasjonar – tek timar eller dagar!
Skilnaden mellom O(n) og O(n²) kan tyde skilnaden mellom eit program som køyrer på eit sekund og eitt som tek ei veke.
Big O i praksis: korleis bestemme kompleksiteten
O(1) – Konstant tid:
Operasjonen tek like lang tid uansett storleiken på inndataen.
# O(1) – å hente et element fra en liste via indeks
def hent_forste(liste):
return liste[0] # Alltid 1 operasjonO(n) – Lineær tid:
Tida veks proporsjonalt med inndataen. Éi løkke gjennom n element.
# O(n) – lineært søk
def finn_maks(liste):
storst = liste[0]
for tall in liste: # n gjennomløp
if tall > storst:
storst = tall
return storstO(n²) – Kvadratisk tid:
Nøsta løkker der begge går gjennom n element.
# O(n²) – boblesortering
def boblesortering(liste):
n = len(liste)
for i in range(n): # n gjennomløp
for j in range(n - 1): # n gjennomløp for hvert i
if liste[j] > liste[j + 1]:
liste[j], liste[j + 1] = liste[j + 1], liste[j]O(log n) – Logaritmisk tid:
Halverer dataa i kvart steg.
# O(log n) – binærsøk halverer søkeområdet
def binaersok(liste, maal):
lav, hoy = 0, len(liste) - 1
while lav <= hoy: # Halverer for hvert steg
midt = (lav + hoy) // 2
if liste[midt] == maal:
return midt
elif liste[midt] < maal:
lav = midt + 1
else:
hoy = midt - 1
return -1Tommelfingerreglar:
- Éi enkel løkke gjennom n element → O(n)
- To nøsta løkker gjennom n element → O(n²)
- Tre nøsta løkker → O(n³)
- Halvering i kvart steg → O(log n)
- Éi løkke med halvering i kvart steg → O(n log n)
Skriv eit Python-program som måler tida det tek å sortere lister av ulike storleikar med boblesortering vs. Pythons innebygde sortering, for å demonstrere skilnaden mellom O(n²) og O(n log n).
import time
import random
def boblesortering(liste):
n = len(liste)
for i in range(n - 1):
for j in range(n - 1 - i):
if liste[j] > liste[j + 1]:
liste[j], liste[j + 1] = liste[j + 1], liste[j]
storrelser = [1000, 2000, 5000, 10000]
print(f"{'Størrelse':<12} {'Boblesortering':<18} {'sorted()':<12}")
print("-" * 42)
for n in storrelser:
data = [random.randint(0, 100000) for _ in range(n)]
# Boblesortering
kopi1 = data.copy()
start = time.time()
boblesortering(kopi1)
tid_boble = time.time() - start
# Pythons innebygde sortering
kopi2 = data.copy()
start = time.time()
sorted(kopi2)
tid_python = time.time() - start
print(f"{n:<12} {tid_boble:<18.4f} {tid_python:<12.6f}")Typisk kjøring:
Størrelse Boblesortering sorted()
------------------------------------------
1000 0.0450 0.000080
2000 0.1780 0.000170
5000 1.1200 0.000450
10000 4.4800 0.000950Legg merke til at boblesortering (O(n²)) brukar ~4x lengre tid når n vert dobla, medan sorted() (O(n log n)) veks mykje saktare. For 10 000 element er Pythons innebygde sortering omtrent 4700x raskare!
Rekursjon er spesielt nyttig for problem som naturleg kan delast inn i like delproblem, som trestrukturar, mappehierarki, og «del og hersk»-algoritmar. For enkle problem som summar og fakultet er iterasjon ofte enklare og meir effektivt. Ein god tommelfingerregel: bruk rekursjon når problemet er naturleg rekursivt (som å traversere eit tre), og iterasjon når det er ei enkel gjentaking. I IT 1 er det viktigast å forstå konseptet og kunne implementere enkle døme.
Kva er dei to nødvendige delane i ein rekursiv funksjon?
Kva tyder O(n) i Big O-notasjon?
Kva returnerer denne rekursive funksjonen for kallet mysterium(4)?
def mysterium(n):
if n == 0:
return 0
return n + mysterium(n - 1)Ranger desse Big O-kompleksitetane frå raskast til tregast: O(n²), O(1), O(n log n), O(log n), O(n)
Skriv ein rekursiv funksjon potens(base, eksponent) som reknar ut base opphøgd i eksponent utan å bruke **-operatoren. Til dømes skal potens(2, 5) returnere 32. Identifiser basistilfellet og det rekursive tilfellet.
Skriv ein rekursiv funksjon reverser(tekst) som reverserer ein streng. Til dømes skal reverser("hallo") returnere "ollah". Hint: basistilfellet er ein tom streng eller éin bokstav. Det rekursive tilfellet flyttar fyrste bokstav til slutten.
Bestem Big O-kompleksiteten for kvar av desse Python-funksjonane og forklar kvifor:
# Funksjon A
def funk_a(n):
return n * (n + 1) / 2
# Funksjon B
def funk_b(liste):
for element in liste:
print(element)
# Funksjon C
def funk_c(liste):
for i in liste:
for j in liste:
print(i, j)Kva er tidskompleksiteten til denne funksjonen?
def mysterie(n):
i = n
teller = 0
while i > 1:
i = i // 2
teller += 1
return tellerOppsummering
I dette kapittelet har du lært:
- Rekursjon: ein funksjon som kallar seg sjølv.
- Basistilfelle og rekursivt tilfelle: stopp og gjentaking.
- Døme: fakultet, Fibonacci og rekursivt binærsøk.
- Big O-notasjon: skildrar ressursbruken til algoritmen.
- Vanlege kompleksitetar: O(1), O(log n), O(n) og O(n^2).
Nøkkelbegrep
| Begrep | Forklaring |
|---|---|
| Rekursjon | Funksjon som kallar seg sjølv |
| Basistilfelle | Tilfellet som stoppar rekursjonen |
| Big O | Notasjon for effektiviteten til algoritmar |
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.