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 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
Rekursjon er ein problemløysingsteknikk der ein funksjon løyser eit problem ved å kalle seg sjølv med ein forenkla versjon av det same problemet. Ein rekursiv funksjon har alltid to delar: (1) eit basistilfelle som stoppar rekursjonen og gir eit direkte svar, og (2) eit rekursivt tilfelle der funksjonen kallar seg sjølv med eit mindre problem. Utan eit basistilfelle ville funksjonen kalle seg sjølv uendeleg mange gonger og til slutt krasje med ein «stack overflow»-feil.

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 resultat

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

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

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

✏️Rekursiv summasjon

Skriv ein rekursiv funksjon som reknar ut summen av tala frå 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 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) = 34

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

✏️Rekursivt binærsøk

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

Big O-notasjon
Big O-notasjon er ein matematisk notasjon som skildrar den øvre grensa for ressursbruken (tid eller plass) til ein algoritme som funksjon av inndatastorleiken n. Han fokuserer på korleis ressursbruken veks når n aukar, og ser bort frå konstante faktorar og lågare ordens ledd. Til dømes: ein algoritme som gjer 3n² + 5n + 10 operasjonar har tidskompleksitet O(n²), fordi n²-leddet dominerer for store verdiar av n.

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

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

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

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

✏️Samanlikning av algoritmekompleksitet i praksis

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

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

📝Oppgave 3.5.1

Kva er dei to nødvendige delane i ein rekursiv funksjon?

📝Oppgave 3.5.2

Kva tyder O(n) i Big O-notasjon?

📝Oppgave 3.5.3

Kva 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 desse Big O-kompleksitetane frå raskast til tregast: O(n²), O(1), O(n log n), O(log n), O(n)

📝Oppgave 3.5.5

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.

📝Oppgave 3.5.6

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.

📝Oppgave 3.5.7

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)

📝Oppgave 3.5.8

Kva 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: 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


BegrepForklaring
RekursjonFunksjon som kallar seg sjølv
BasistilfelleTilfellet som stoppar rekursjonen
Big ONotasjon 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.