Tilbake
3.5
Rekursjon og algoritmekompleksitet

3.5 Rekursjon og algoritmekompleksitet

Lær hva rekursjon er og forstå grunnleggende algoritmekompleksitet med Big O-notasjon.

60 min
8 oppgaver
RekursjonBasistilfelleBig O-notasjonTidskompleksitet
Du leser den tradisjonelle versjonen
Din fremgang i kapitlet
0 / 8 oppgaver

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
Rekursjon er en problemløsningsteknikk der en funksjon løser et problem ved å kalle seg selv med en forenklet versjon av det samme problemet. En rekursiv funksjon har alltid to deler: (1) et basistilfelle som stopper rekursjonen og gir et direkte svar, og (2) et rekursivt tilfelle der funksjonen kaller seg selv med et mindre problem. Uten et basistilfelle ville funksjonen kalle seg selv uendelig mange ganger og til slutt krasje med en «stack overflow»-feil.

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 resultat

Rekursiv 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))  # 1

Hva 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 = 24

Funksjonen kaller seg selv med stadig mindre verdier av n til den når basistilfellet (n == 0). Deretter «rulles» resultatet tilbake opp gjennom alle kallene.

✏️Rekursiv summasjon

Skriv en rekursiv funksjon som beregner summen av tallene fra 1 til n.

Rekursiv definisjon:
- 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)) # 5050

Oppruling 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
                = 15

Merk: 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) = 34

Fibonacci-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.

✏️Rekursivt binærsøk

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}")  # -1

Gjennomgang 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.

Big O-notasjon
Big O-notasjon er en matematisk notasjon som beskriver den øvre grensen for en algoritmes ressursbruk (tid eller plass) som funksjon av inndatastørrelsen n. Den fokuserer på hvordan ressursbruken vokser når n øker, og ignorerer konstante faktorer og lavere ordens ledd. For eksempel: en algoritme som gjør 3n² + 5n + 10 operasjoner har tidskompleksitet O(n²), fordi n²-leddet dominerer for store verdier av n.

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 ONavnEksempel1000 elementer
O(1)KonstantTilgang til listelement via indeks1 operasjon
O(log n)LogaritmiskBinærsøk~10 operasjoner
O(n)LineærLineært søk1 000 operasjoner
O(n log n)LinearitmiskPythons innebygde sortering~10 000 operasjoner
O(n²)KvadratiskBoblesortering1 000 000 operasjoner
O(2ⁿ)EksponentiellNaiv 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 operasjon

O(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 storst

O(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 -1

Tommelfingerregler:
- É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)

✏️Sammenligning av algoritmekompleksitet i praksis

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.000950

Legg 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!

📝Oppgave 3.5.1

Hva er de to nødvendige delene i en rekursiv funksjon?

📝Oppgave 3.5.2

Hva betyr O(n) i Big O-notasjon?

📝Oppgave 3.5.3

Hva returnerer denne rekursive funksjonen for kallet mysterium(4)?

def mysterium(n):
    if n == 0:
        return 0
    return n + mysterium(n - 1)

📝Oppgave 3.5.4

Ranger disse Big O-kompleksitetene fra raskest til tregest: O(n²), O(1), O(n log n), O(log n), O(n)

📝Oppgave 3.5.5

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.

📝Oppgave 3.5.6

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.

📝Oppgave 3.5.7

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)

📝Oppgave 3.5.8

Hva er tidskompleksiteten til denne funksjonen?

def mysterie(n):
    i = n
    teller = 0
    while i > 1:
        i = i // 2
        teller += 1
    return teller

Oppsummering

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


BegrepForklaring
RekursjonFunksjon som kaller seg selv
BasistilfelleTilfellet som stopper rekursjonen
Big ONotasjon 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.