Lær hva rekursjon er og forstå grunnleggende algoritmekompleksitet med Big O-notasjon.
Rekursjon og algoritmekompleksitet
Tenk deg at du står foran et speil som reflekterer et annet speil. Du ser deg selv, som ser deg selv, som ser deg selv, i en tilsynelatende uendelig rekke av refleksjoner. Dette er en visuell metafor for rekursjon: noe som refererer til seg selv.
I programmering er rekursjon en kraftig teknikk der en funksjon løser et problem ved å kalle seg selv med et enklere delproblem. Det kan virke magisk, kanskje til og med litt forvirrende, den første gangen du ser det, men det er egentlig et elegant verktøy for å løse problemer som naturlig kan deles inn i like, men enklere delproblemer.
I tillegg til rekursjon skal vi i dette kapittelet se på algoritmekompleksitet, en måte å måle og sammenligne effektiviteten til algoritmer. Vi har allerede sett at binærsøk er raskere enn lineært søk, og at sorteringsalgoritmer har forskjellig ytelse. Big O-notasjonen gir oss et presist språk for å snakke om disse forskjellene.
Rekursjon med fakultet
Det klassiske eksempelet på rekursjon er beregning av fakultet. Fakultet av n (skrevet n!) er produktet av alle positive heltall fra 1 til n:
- 5! = 5 × 4 × 3 × 2 × 1 = 120
- 3! = 3 × 2 × 1 = 6
- 1! = 1
- 0! = 1 (per definisjon)
Det finnes en elegant rekursiv definisjon:
- n! = n × (n-1)! for n > 0
- 0! = 1 (basistilfelle)
Altså: 5! = 5 × 4!, og 4! = 4 × 3!, og så videre til vi når 0! = 1.
Iterativ løsning (med løkke):
def fakultet_iterativ(n):
resultat = 1
for i in range(1, n + 1):
resultat *= i
return resultatRekursiv løsning:
def fakultet(n):
# Basistilfelle
if n == 0:
return 1
# Rekursivt tilfelle
return n * fakultet(n - 1)
print(fakultet(5)) # 120
print(fakultet(0)) # 1Hva skjer når vi kaller 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 kaller seg selv med stadig mindre verdier av n til den når basistilfellet (n == 0). Deretter «rulles» resultatet tilbake opp gjennom alle kallene.
Skriv en rekursiv funksjon som beregner summen av tallene fra 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 fant en 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-tallene
Fibonacci-tallrekken er et berømt eksempel som passer naturlig til rekursjon. Hvert tall er summen av de to foregående:
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 et viktig problem: den er svært ineffektiv! For å beregne fibonacci(5) kalles fibonacci(3) to ganger, fibonacci(2) tre ganger, og så videre. Antall kall vokser eksponentielt. fibonacci(40) kan ta flere sekunder, og fibonacci(100) ville ta lengre enn universets levetid!
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 beregner fibonacci(50) umiddelbart, mens den rekursive versjonen ville bruke ufattelig lang tid. Dette illustrerer at rekursjon ikke alltid er den beste løsningen, selv om den er elegant.
Python har en standard rekursjonsgrense på omtrent 1000 kall. Hvis en rekursiv funksjon kaller seg selv mer enn 1000 ganger uten å nå basistilfellet, krasjer programmet med en RecursionError: maximum recursion depth exceeded. Dette er en sikkerhetsmekanisme for å forhindre at programmet bruker alt minne. For problemer som krever mer enn 1000 rekursive kall, bør du bruke en iterativ løsning i stedet.
Implementer binærsøk som en 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øyre
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 → Funnet!
Den rekursive versjonen er elegant fordi den gjenspeiler algoritmen direkte: halvér, velg side, gjenta. Hvert rekursivt kall reduserer søkeområdet til det halve.
Forstå Big O-notasjon
Big O-notasjon gir oss et språk for å snakke om hvor raskt en algoritme vokser i tid eller plass når inndataen blir større. Vi bryr oss ikke om nøyaktig antall operasjoner, men om veksthastigheten.
De vanligste kompleksitetsklassene (fra raskest til tregest):
| Big O | Navn | Eksempel | 1000 elementer |
|---|---|---|---|
| O(1) | Konstant | Tilgang til listelement via indeks | 1 operasjon |
| O(log n) | Logaritmisk | Binærsøk | ~10 operasjoner |
| O(n) | Lineær | Lineært søk | 1 000 operasjoner |
| O(n log n) | Linearitmisk | Pythons innebygde sortering | ~10 000 operasjoner |
| O(n²) | Kvadratisk | Boblesortering | 1 000 000 operasjoner |
| O(2ⁿ) | Eksponentiell | Naiv Fibonacci-rekursjon | ~10³⁰⁰ operasjoner |
Hvorfor er dette viktig?
Tenk deg at du har en algoritme som fungerer fint for 100 elementer, men brukerne dine har 1 000 000 elementer:
- O(n): 1 000 000 operasjoner – tar millisekunder
- O(n log n): ~20 000 000 operasjoner – tar sekunder
- O(n²): 1 000 000 000 000 operasjoner – tar timer eller dager!
Forskjellen mellom O(n) og O(n²) kan bety forskjellen mellom et program som kjører på et sekund og et som tar en uke.
Big O i praksis: hvordan bestemme kompleksiteten
O(1) – Konstant tid:
Operasjonen tar like lang tid uansett størrelsen 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:
Tiden vokser proporsjonalt med inndataen. Én løkke gjennom n elementer.
# 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østede løkker der begge går gjennom n elementer.
# 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 dataene i hvert 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 -1Tommelfingerregler:
- Én enkel løkke gjennom n elementer → O(n)
- To nøstede løkker gjennom n elementer → O(n²)
- Tre nøstede løkker → O(n³)
- Halvering i hvert steg → O(log n)
- Én løkke med halvering i hvert steg → O(n log n)
Skriv et Python-program som måler tiden det tar å sortere lister av ulike størrelser med boblesortering vs. Pythons innebygde sortering, for å demonstrere forskjellen 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²)) bruker ~4x lenger tid når n dobles, mens sorted() (O(n log n)) vokser mye saktere. For 10 000 elementer er Pythons innebygde sortering omtrent 4700x raskere!
Rekursjon er spesielt nyttig for problemer som naturlig kan deles inn i like delproblemer, som trestrukturer, mappehierarkier, og «del og hersk»-algoritmer. For enkle problemer som summer og fakultet er iterasjon ofte enklere og mer effektivt. En god tommelfingerregel: bruk rekursjon når problemet er naturlig rekursivt (som å traversere et tre), og iterasjon når det er en enkel gjentakelse. I IT 1 er det viktigst å forstå konseptet og kunne implementere enkle eksempler.
Hva er de to nødvendige delene i en rekursiv funksjon?
Hva betyr O(n) i Big O-notasjon?
Hva returnerer denne rekursive funksjonen for kallet mysterium(4)?
def mysterium(n):
if n == 0:
return 0
return n + mysterium(n - 1)Ranger disse Big O-kompleksitetene fra raskest til tregest: O(n²), O(1), O(n log n), O(log n), O(n)
Skriv en rekursiv funksjon potens(base, eksponent) som beregner base opphøyd i eksponent uten å bruke **-operatoren. For eksempel skal potens(2, 5) returnere 32. Identifiser basistilfellet og det rekursive tilfellet.
Skriv en rekursiv funksjon reverser(tekst) som reverserer en streng. For eksempel skal reverser("hallo") returnere "ollah". Hint: basistilfellet er en tom streng eller én bokstav. Det rekursive tilfellet flytter første bokstav til slutten.
Bestem Big O-kompleksiteten for hver av disse Python-funksjonene og forklar hvorfor:
# 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)Hva 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: en funksjon som kaller seg selv.
- Basistilfelle og rekursivt tilfelle: stopp og gjentakelse.
- Eksempler: fakultet, Fibonacci og rekursivt binærsøk.
- Big O-notasjon: beskriver algoritmens ressursbruk.
- Vanlige kompleksiteter: O(1), O(log n), O(n) og O(n^2).
Noekkelbegreper
| Begrep | Forklaring |
|---|---|
| Rekursjon | Funksjon som kaller seg selv |
| Basistilfelle | Tilfellet som stopper rekursjonen |
| Big O | Notasjon for algoritmers effektivitet |
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.