Forberedelse til kodeintervjuer · leksjon

Rekursjon og metoden med rekursjonstre

Følg rekursive kall i trær, bruk Master Theorem og utled tidskompleksitet for merge sort, fakultet og varianter av Fibonacci.

Leksjon 3 av 413 trinn

Rekursjon og metoden med rekursjonstre er en gratis leksjon i Forberedelse til kodeintervjuer på CoddyKit. Dette er leksjon 3 av 4. Du kan lese hele leksjonen gratis nedenfor – og deretter øve praktisk i nettleseren med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Den er en del av læringsløpet i Forberedelse til kodeintervjuer, og fremdriften din synkroniseres mellom nettet og CoddyKit-appen. Kurset i Forberedelse til kodeintervjuer inneholder totalt 4 leksjoner.

Rekursjon og kallstakken

Når en funksjon kaller seg selv, legger hvert kall til en stakkramme, som hoper seg opp til et basistilfelle nås og rammene avvikles. Å se dette for seg er første steg i analysen av rekursjon.

def factorial(n):
    if n == 0:       # base case
        return 1
    return n * factorial(n - 1)  # recursive call

# Call chain: factorial(4)
#   4 * factorial(3)
#     3 * factorial(2)
#       2 * factorial(1)
#         1 * factorial(0) -> 1
# Unwinds: 1, 2, 6, 24
print(factorial(5))  # 120

Rekursjonstreet for Fibonacci

Et rekursjonstre utvider hvert kall til underkallene sine. Naiv Fibonacci deler seg i to hver gang, og skaper et tre med rundt 2^n noder – det er O(2^n). Se koden.

call_count = [0]

def fib_naive(n):
    call_count[0] += 1
    if n <= 1:
        return n
    return fib_naive(n-1) + fib_naive(n-2)

for n in [5, 10, 15, 20]:
    call_count[0] = 0
    result = fib_naive(n)
    print(f'fib({n})={result}, calls={call_count[0]}')
# Calls roughly double each time n increases by 1

Identifisere gjentatte delproblemer

I dette treet gjentas de samme kallene, for eksempel fib(3), på tvers av grenene. Disse overlappende delproblemene er signalet for memoisering, som reduserer O(2^n) til O(n).

# Memoised: each unique sub-problem computed once
def fib_memo(n, memo={}):
    if n in memo: return memo[n]
    if n <= 1:    return n
    memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
    return memo[n]

call_count2 = [0]
def fib_counted(n, memo={}):
    call_count2[0] += 1
    if n in memo: return memo[n]
    if n <= 1:    return n
    memo[n] = fib_counted(n-1, memo) + fib_counted(n-2, memo)
    return memo[n]

fib_counted(20)
print(f'calls with memo: {call_count2[0]}')  # only 21

Rekursjonstreet for flettesortering

Rekursjonstreet til flettesortering har log n nivåer, og hvert nivå utfører totalt O(n) arbeid – hvert element berøres én gang. Multipliser dem for å få O(n log n). Se koden.

# Merge sort: at each level, n total elements are merged
# Level 0:  1 merge of n elements    -> n work
# Level 1:  2 merges of n/2 each     -> n work
# Level 2:  4 merges of n/4 each     -> n work
# ...log(n) levels...
# Total: n * log(n)

# Verify with operation counter:
def merge_sort_counted(arr):
    ops = [0]
    def _sort(a):
        if len(a) <= 1: return a
        m = len(a) // 2
        l, r = _sort(a[:m]), _sort(a[m:])
        result, i, j = [], 0, 0
        while i < len(l) and j < len(r):
            ops[0] += 1
            if l[i] <= r[j]: result.append(l[i]); i+=1
            else:             result.append(r[j]); j+=1
        return result + l[i:] + r[j:]
    return _sort(arr), ops[0]

_, c = merge_sort_counted(list(range(64, 0, -1)))
print(f'Merge ops: {c}')  # ~384 ~ 64*log2(64)=384

Master-teoremet

Master-teoremet løser T(n) = a*T(n/b) + O(n^d) med tre tilfeller. For flettesortering (a=2, b=2, d=1) gir det O(n log n). Lær de tre tilfellene utenat til eksamen.

# Merge sort: T(n) = 2*T(n/2) + O(n)
# a=2, b=2, d=1, log_b(a)=log2(2)=1=d  => O(n log n)

# Binary search: T(n) = 1*T(n/2) + O(1)
# a=1, b=2, d=0, log2(1)=0=d  => O(log n)

# Strassen matrix mult: T(n) = 7*T(n/2) + O(n^2)
# a=7, b=2, d=2, log2(7)~2.81 > 2 => O(n^log2(7)) ~ O(n^2.81)

import math
print('log2(7) =', math.log2(7))  # 2.807...

Tegne rekursjonstrær: trinn for trinn

Slik tegner De et rekursjonstre: plasser T(n) øverst, utvid hvert kall, summer arbeidet på hvert nivå og multipliser deretter med antallet nivåer. Øv til det går automatisk.

# Factorial: T(n) = T(n-1) + O(1)
# Tree is a chain: n levels, O(1) each -> O(n)

# Fibonacci: T(n) = T(n-1) + T(n-2) + O(1)
# Binary tree of depth n, ~2^n nodes -> O(2^n)

# Merge sort: T(n) = 2*T(n/2) + O(n)
# Log levels, n work each -> O(n log n)

def count_recursive_calls(n, results=[]):
    if n <= 1:
        results.append(n)
        return n
    return count_recursive_calls(n-1, results) + count_recursive_calls(n-2, results)

results = []
count_recursive_calls(8, results)
print(f'fib(8) leaf calls: {len(results)}')

Eksponentiell rekursjon: delmengder

Å generere alle delmengder er O(2^n) – det finnes nøyaktig 2^n av dem, så det er umulig å gjøre det raskere. Hvert element er enten med eller ute, og slik bygges et binærtre av valg. Se koden.

def subsets(nums):
    result = []
    def backtrack(start, current):
        result.append(list(current))  # O(n) copy
        for i in range(start, len(nums)):
            current.append(nums[i])
            backtrack(i + 1, current)
            current.pop()
    backtrack(0, [])
    return result

nums = [1, 2, 3]
ss = subsets(nums)
print(len(ss))  # 8 = 2^3
print(ss)

Halerekursjon og optimalisering

Halerekursjon er når det rekursive kallet er det aller siste steget. Noen språk gjenbruker stakkrammen i slike tilfeller, men Python gjør ikke det – derfor fører dyp rekursjon fortsatt til overflow. Bruk en løkke i stedet.

# Tail-recursive factorial (accumulator pattern)
def fact_tail(n, acc=1):
    if n == 0:
        return acc
    return fact_tail(n - 1, n * acc)  # tail call

# Python does NOT TCO, so this overflows for large n
# Instead, convert to iterative:
def fact_iter(n):
    acc = 1
    while n > 0:
        acc *= n
        n -= 1
    return acc

print(fact_tail(10))  # 3628800
print(fact_iter(10))  # 3628800

Plasskompleksitet for rekursjon

Hvert rekursivt kall holder på en stakkramme, så rekursjon bruker O(depth) plass. Lineær rekursjon er O(n); DFS i et balansert tre er O(log n). Gå for dypt, så får De RecursionError.

import sys
print(sys.getrecursionlimit())  # default 1000

# Increase limit for deep problems
sys.setrecursionlimit(10000)

# Track max depth manually
def max_depth_tracker(n, depth=0, max_seen=[0]):
    max_seen[0] = max(max_seen[0], depth)
    if n <= 0:
        return
    max_depth_tracker(n - 1, depth + 1, max_seen)
    return max_seen[0]

print(max_depth_tracker(50))  # 50  => O(n) stack frames

Rekursjonstreet for quicksort

Quicksort er O(n log n) med en god pivot, men en dårlig pivot på sortert inndata gjør at den degraderes til O(n^2). Derfor er det viktig å randomisere pivoten. Se koden.

import random

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = random.choice(arr)  # randomised -> O(n log n) expected
    less    = [x for x in arr if x < pivot]
    equal   = [x for x in arr if x == pivot]
    greater = [x for x in arr if x > pivot]
    return quick_sort(less) + equal + quick_sort(greater)

print(quick_sort([3, 6, 8, 10, 1, 2, 1]))  # sorted

Potensfunksjon: rekursjon med log n

Naiv x^n krever O(n) multiplikasjoner, men kvadrering halverer arbeidet i hvert steg: x^n = (x^(n/2))^2. Det gir en ren O(log n) – halvering i praksis. Se koden.

def fast_pow(x, n):
    if n == 0: return 1
    if n < 0:  return 1 / fast_pow(x, -n)
    if n % 2 == 0:
        half = fast_pow(x, n // 2)
        return half * half          # O(log n) calls
    return x * fast_pow(x, n - 1)

print(fast_pow(2, 10))   # 1024
print(fast_pow(3, 5))    # 243
# Only log2(10)=3-4 recursive calls for n=10

Hurtigsjekk

Hurtigsjekk – vis hva metoden med rekursjonstre har lært Dem. Ett spørsmål, ta Dem god tid. 🌳

Oppsummering av leksjonen

Oppsummering: et rekursjonstre viser det totale arbeidet, Master-teoremet løser rekurrenser for splitt-og-hersk, og rekursjon bruker O(dybde) plass på stakken.

Gratis å komme i gang

Lær deg Forberedelse til kodeintervjuer med en AI-veileder – gratis

Skriv og kjør ekte kode i nettleseren, få umiddelbar hjelp fra en AI-veileder som er tilgjengelig døgnet rundt, og fortsett der du slapp – på nettet eller i appen.

Kurs
90
Leksjoner
360

Ofte stilte spørsmål

Er leksjonen «Rekursjon og metoden med rekursjonstre» gratis?

Ja – hele teksten i «Rekursjon og metoden med rekursjonstre» er gratis å lese her på nettet. For å øve interaktivt med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt, og for å låse opp resten av Forberedelse til kodeintervjuer-kurset, kan du oppgradere til CoddyKit PRO. Kurset i Forberedelse til kodeintervjuer inneholder totalt 4 leksjoner.

Hva lærer jeg i «Rekursjon og metoden med rekursjonstre»?

Følg rekursive kall i trær, bruk Master Theorem og utled tidskompleksitet for merge sort, fakultet og varianter av Fibonacci. Du øver på Forberedelse til kodeintervjuer med praktisk kode som du kjører direkte i nettleseren, mens en AI-veileder som er tilgjengelig døgnet rundt, svarer på spørsmålene dine mens du jobber deg gjennom leksjonen.

Trenger jeg erfaring for å begynne med Forberedelse til kodeintervjuer?

Ingen tidligere erfaring er nødvendig. Forberedelse til kodeintervjuer på CoddyKit er lagt opp for både nybegynnere og viderekomne, så De kan begynne her eller helt fra start og lære i Deres eget tempo. Dette er leksjon 3 av 4.

Hvor lang tid tar leksjonen «Rekursjon og metoden med rekursjonstre»?

De fleste CoddyKit-leksjoner tar omtrent 5–10 minutter. Hver leksjon er kort og interaktiv, slik at De gjør jevne fremskritt og kan fortsette akkurat der De slapp – både på nettet og i appen.

Kan jeg skrive og kjøre kode i denne Forberedelse til kodeintervjuer-leksjonen?

Ja. Alle Forberedelse til kodeintervjuer-leksjoner har en innebygd kodeeditor, slik at De kan skrive og kjøre ekte kode direkte i nettleseren og få umiddelbar tilbakemelding fra AI – uten lokal konfigurering.

Alle leksjonene i dette kurset

  1. Big-O-notasjon fra grunnen av
  2. Analyse av løkker og nøstede løkker
  3. Rekursjon og metoden med rekursjonstre
  4. Plasskompleksitet og avveininger
← Tilbake til Forberedelse til kodeintervjuer