Ga naar hoofdinhoud

Ruimtecomplexiteit: hoe algoritmen geheugen gebruiken

Leer hoe je ruimtecomplexiteit berekent met asymptotische notatie, hoe geheugencomponenten zoals recursie, datastructuren en auxiliary space optellen, en hoe je ruimte reduceert met in-place technieken.
· 13 min lezen

Verken met AI

Ruimtecomplexiteit meet het geheugengebruik van algoritmen naarmate de invoergrootte groeit. Daardoor is het begrijpen van ruimtecomplexiteit belangrijk voor resourcebeheer in omgevingen met beperkte middelen.

In dit artikel leg ik in detail uit wat ruimtecomplexiteit is, hoe je die berekent en hoe je die minimaliseert. Daarnaast behandel ik toepassingen in de praktijk en bied ik theoretische fundamenten.

Ruimtecomplexiteit begrijpen

Voordat we in berekeningen en optimalisaties duiken, laten we verduidelijken wat ruimtecomplexiteit precies betekent en waarom je je erom zou moeten bekommeren.

Wat is ruimtecomplexiteit?

Ruimtecomplexiteit is het totale geheugen dat het algoritme nodig heeft relatief tot de invoergrootte. Het is relevant om de worstcases-geheugenvoetafdruk te analyseren voor de schaalbaarheid van het systeem; de totale ruimte omvat invoer, hulpgeheugen en overhead, terwijl auxiliary space het extra geheugen is exclusief invoer en uitvoer. Neem als voorbeeld het sorteren van een array: de totale ruimte omvat de array zelf, terwijl de auxiliary space de tijdelijke ruimte in mergesort kan zijn.

Waarom ruimtecomplexiteit ertoe doet

In de moderne informatica is ruimte-efficiëntie vaak cruciaal. Door ruimtecomplexiteit te optimaliseren zorg je ervoor dat je algoritmen draaien op apparaten met weinig RAM, zoals embedded systemen in IoT of smartphones, of zelfs grote systemen met veeleisende procedures waarbij elke bit RAM telt.

Denk aan mobiele apps: ruimte-optimalisatie helpt crashes te voorkomen tijdens beeldverwerking. Of denk aan kostenbesparing in de cloud op opslag, of aan streamingdata en realtime analytics zonder volledige gegevens in te laden.

Asymptotische analyse en Big O-notatie voor ruimte

In deze sectie leg ik uit hoe asymptotische notaties zoals Big O de groei van ruimte beschrijven, en geef ik je hulpmiddelen om geheugengebruik te begrenzen en te vergelijken.

Introductie tot de Big O-notatie

Wiskundig wordt een functie f(n) gedomineerd door een functie g(n) als er een constante c en k bestaan, zodanig dat voor alle k>n, 0 ≤ |f(n)| ≤ |g(n)|.

In dat geval schrijven we f(n)=O(g(n)), en zeggen we dat f(n) big O van g(n) is.

Eenvoudiger gezegd betekent dit dat f(n) niet sneller groeit dan g(n) vanaf een bepaalde index.

Big O-notatie vertegenwoordigt de bovengrens van de ruimte als functie van invoergrootte n, zodat we ons kunnen richten op dominante termen voor grote n. Deze notatie helpt algoritmen te classificeren; bijvoorbeeld O(n) betekent dat de ruimte verdubbelt bij een verdubbeling van de invoer.

Een veelvoorkomend misverstand onder beginners is dat Big O voor ruimte constanten negeert, zoals 2n is O(n), maar in de praktijk doen constanten ertoe voor kleine n.

Gerelateerde asymptotische notaties

Er zijn verschillende manieren om complexiteit te modelleren en verschillende notaties. Zo geeft Big Omega een ondergrens, wat nuttig is om minimale ruimtelijke vereisten te bewijzen, zoals bijvoorbeeld sorteren dat minstens Ω(n) vereist. Een andere notatie is Big Theta, die een strakke grens geeft wanneer boven- en ondergrens samenvallen; ideaal voor precieze analyse, zoals O(n) = Θ(n) voor lineaire ruimte.

Andere notationele conventies

Er zijn nog enkele andere conventies relevant voor literatuur over ruimtecomplexiteit, zoals de Soft-O-notatie Õ, die polylogfactoren negeert. Die is gangbaar in geavanceerde algoritmen zoals grafverwerking, waar log-termen verwaarloosbaar zijn. Je ziet het in theoretische papers voor benaderende complexiteiten, zoals Õ voor sublineaire ruimte in streamingalgoritmen.

Componenten van ruimtecomplexiteit

Ruimtecomplexiteit kent vele bijdragers en optimalisatiepunten in algoritmeontwerp. Laten we kijken:

Ruimtevereisten voor invoer

Invoergegevens nemen ruimte in proportie tot de grootte, bijvoorbeeld O(n) voor een array of bij het analyseren van een grote dataset zoals genomische sequenties.

Het is gebruikelijk in echte coding interviews om de invoerruimte uit te sluiten bij het berekenen van auxiliary space en alleen te focussen op het extra geheugen dat het algoritme gebruikt. In competitive programming omvat de totale ruimte echter meestal de invoer.

Auxiliary space en tijdelijk geheugen

Auxiliary space is het extra geheugen dat een algoritme nodig heeft om zijn bewerkingen uit te voeren. Dit beïnvloedt direct hoe het algoritme is ontworpen.

Zo kun je bijvoorbeeld O(n) gebruiken voor de tijdelijke array in een mergesort-algoritme. Andere voorbeelden zijn hashtabellen voor duplicaten (O(n)), de recursiestacks voor haakjesmatchen (O(n)), en vele anderen.

Klassieke voorbeelden van auxiliary space zijn twee-pointermethodes (O(1)), sliding-window-algoritmen met een deque (O(k)) en BFS, dat O(V) ruimte vereist voor zijn visited-set en wachtrij.

De volgende code toont een voorbeeld van een bewerking voor mergesort op de tijdelijke array tijdens de merge-stap:

def merge(left_half, right_half):
    merged = [0] * (len(left_half) + len(right_half))  # O(n) auxiliary space
    left_idx = right_idx = merged_idx = 0
    # merging logic...

De call stack en recursieruimte

Elke recursieve aanroep voegt een stackframe toe (returnadres + parameters + locals).

Lineaire recursie, zoals faculteit of DFS op een gelinkte lijst, heeft daarentegen een O(n)-stack.

Binaire recursie, zoals naïeve Fibonacci of boomrecursie, heeft O(n) in het worstcasescenario.

Tail-call-optimalisatie, die wordt ondersteund in Scheme, Rust en soms Python met decorators, reduceert naar O(1). Het echte risico ligt echter bij Python’s standaard recursielimiet, rond de 1000, wat een stack overflow veroorzaakt bij diepe bomen.

Hier is een voorbeeld van naïeve recursieve Fibonacci die de groei van de stack laat zien:

def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)  # O(n) stack depth

Bekijk onze blog over Complexiteit van code analyseren met Python om meer te leren over complexiteitsanalyse in Python.

Overhead van datastructuren

Het fijne aan overhead van datastructuren is de eenvoud, omdat alle structuren vaste/per-element-kosten toevoegen, zoals de minimale overhead van een array, de dubbele ruimte voor de pointers van gelinkte lijsten, en vele andere voorbeelden. 

Hoe dan ook, de programmeertaal heeft enorme invloed. Python-lijsten hebben dynamische overhead, in tegenstelling tot C-arrays—bijvoorbeeld, hashmaps met een load factor van 75% verspillen ruimte aan lege bakken.

Typen ruimtecomplexiteit

Constante ruimtecomplexiteit: O(1)

Constante ruimtecomplexiteit is de fundamentele en optimale oplossing als die gevonden wordt. Veel bekende algoritmen hebben werkelijk constante complexiteit, zoals eenvoudige variabelswaps zoals het onderstaande voorbeeld, een eenvoudige in-place swap met Python tuple-unpacking:

def swap_numbers(first, second):
    first, second = second, first  # O(1) — only two variables
    return first, second

Andere bekende algoritmen met constante complexiteit zijn de Dutch flag-partitionering in quicksort en de Morris-traversal voor bomen. Er is echter een praktische grens waarbij je enkele tientallen variabelen kunt hebben voordat het effectief is, want afhankelijk van de hardware van het systeem kan O(1) soms niet genoeg zijn als het vereiste geheugen de middelen van het systeem overschrijdt.

Lineaire ruimtecomplexiteit: O(n)

Lineaire ruimtecomplexiteit is eenvoudigweg gedefinieerd als evenredig met de invoer. Een paar voorbeelden zijn een extra array om te kopiëren of een hashset voor unieke waarden. Het meest voorkomende gebruik valt binnen gegevensverwerkingspatronen, zoals lijsten filteren.

Logaritmische en sublineaire ruimtecomplexiteit

Een logaritmische ruimtecomplexiteit betekent dat de hoeveelheid geheugen die een algoritme gebruikt zeer langzaam groeit naarmate de invoergrootte toeneemt. Concreet is die evenredig met de logaritme van de invoergrootte.

Zo heeft een binaire zoek-recursie een O(log(n)) stackdiepte, net als divide-and-conquer met tail-recursie of de streamingalgoritmen, die resultaten benaderen in O(log(n)) of O(sqrt(n)) ruimte.

Logaritmische en sublineaire ruimtecomplexiteiten zijn zeldzaam, maar zeer krachtig voor enorme datasets. Je ziet een voorbeeld van logaritmische complexiteit bij de iteratieve binaire zoek (constante ruimte) zoals de voorbeeldcode hieronder laat zien:

def binary_search(numbers, target):
    left = 0
    right = len(numbers) - 1
    while left <= right:                 # O(1) space total
        mid = (left + right) // 2
        # comparison logic...

Kwadratische ruimtecomplexiteit: O(n²)

Kwadratische ruimtecomplexiteit treedt op wanneer de geheugenvraag schaalt met het kwadraat van de invoer. Een veelvoorkomend voorbeeld is de 2D dynamic programming-tabel, met complexiteit O(n²), of een adjacentiematrix voor grafen met complexiteit O(V²). Het is echter belangrijk om te beseffen dat deze complexiteit onpraktisch wordt voorbij duizenden elementen invoer.

Exponentiële en factoriële ruimtecomplexiteit

Exponentiële complexiteit, zoals de naam suggereert, laat de benodigde geheugen­groei evenredig zijn met de invoer tot de macht, O(2^n), en dit gebeurt bijvoorbeeld wanneer je alle deelverzamelingen of paden in een beslisboom opslaat.

Evenzo is bij factoriële complexiteit het vereiste geheugen evenredig met de faculteit van de invoergrootte O(n!), en dit gebeurt wanneer je alle permutaties van de invoer opsomt. Deze complexiteiten zijn echter doorgaans zeer kostbaar en vrijwel zeker onhaalbaar voor n>20–30 in de meeste systemen. Als je hiermee werkt, heb je waarschijnlijk snoeien, benadering of een andere aanpak nodig.

Hoe ruimtecomplexiteit te berekenen

Systematische analyse identificeert alle geheugenallocaties en hun groeipatronen:

Aanpak met regel-voor-regel-analyse

De eerste stap is je code regel voor regel te bekijken en elke variabele-, array- en objectallocatie te identificeren. Bepaal vervolgens of de grootte van elke allocatie afhangt van de invoerparameters. Tel daarna het geheugen op voor onafhankelijke allocaties en neem het maximum voor wederzijds exclusieve takken. Houd tot slot zowel stack-allocaties voor lokale variabelen als heap-allocaties voor dynamisch geheugen bij. Hier is een voorbeeld:

def linear_search(arr, target):
    n = len(arr)           # O(1) - single integer
    for i in range(n):     # O(1) - loop variable
        if arr[i] == target:
            return i
    return -1
# Total: O(1) auxiliary space

Recursieve complexiteitsanalyse

De ruimtecomplexiteit voor recursie is gelijk aan de maximale recursiediepte vermenigvuldigd met de ruimte per aanroepframe; tel daar dan eventuele auxiliary datastructuren bij op die tijdens de recursie worden gealloceerd, identificeer de basisgeval en analyseer hoe de recursiediepte zich verhoudt tot de invoergrootte. Zo creëert naïeve Fibonacci O(n) diepte met O(1) per frame, wat O(n) stackruimte oplevert. Hier is een voorbeeld voor faculteitsruimteberekening:

def factorial(n):
    if n <= 1:             # Base case
        return 1
    return n * factorial(n - 1)
# Space: O(n)

Complexiteitsanalyse van dynamic programming

Standaard DP-tabellen hebben afmetingen die overeenkomen met de subprobleemparameters; een eendimensionaal probleem vereist dus O(n) ruimte, en tweedimensionale problemen hebben O(nxm) nodig. Optimalisatietechnieken zoals rollende arrays reduceren één dimensie en transformeren bijvoorbeeld O(n²) naar O(n). Staatcompressie benut afhankelijkheden, dus als rij i alleen i-1 nodig heeft, sla je slechts twee rijen op. Hier is een voorbeeld van ruimtegeoptimaliseerde Fibonacci met slechts twee variabelen:

def fibonacci_optimized(n):
    previous = 0
    current = 1
    for _ in range(2, n + 1):            # O(1) space
        previous, current = current, previous + current
    return current

Als je nieuwsgierig bent naar dimensionaliteit en de reductie ervan, raad ik je aan onze tutorials te bekijken over Mastering Slowly Changing Dimensions (SCD) en Understanding Cardinality: Challenges and Solutions for Data-Heavy Workflows.

Stappen om in de praktijk ruimtecomplexiteit te berekenen

Volg deze stappen om in de praktijk de ruimtecomplexiteit te berekenen:

  1. Verduidelijk of de invoerruimte is inbegrepen, afhankelijk van de context
  2. Tel variabelen en constanten met vaste grootte. Ze dragen O(1) bij
  3. Meet auxiliary structuren zoals arrays en hashmaps relatief tot invoergrootte
  4. Bereken de maximale recursiediepte voor de bijdrage aan stackruimte
  5. Combineer alle componenten en druk uit met de strakste asymptotische grens.

De ruimtecomplexiteit van gangbare algoritmen

Ruimtevereisten van sorteeralgoritmen

Het mergesort-algoritme vereist O(n) auxiliary space voor tijdelijke arrays tijdens het mergen. Quicksort gebruikt daarentegen gemiddeld slechts O(log n) stackruimte, maar O(n) in het slechtste geval zonder optimalisatie. Heapsort bereikt O(1) auxiliary space door in-place te sorteren, zoals de volgende code laat zien:

def heapify(array, heap_size, root_index):
    largest = root_index
    left_child = 2 * root_index + 1
    right_child = 2 * root_index + 2
    # comparison & swap logic...       # O(1) auxiliary space

Tot slot heeft counting sort O(k) ruimte nodig, waarbij k het waardebereik is, maar dat is onpraktisch wanneer k>>n.

Ruimtecomplexiteit van graafalgoritmen

Graaftraversatie kent vele algoritmen. Zo onderhoudt breadth-first search (BFS) een wachtrij die O(V) ruimte vereist voor knopen, terwijl depth-first search (DFS) in het slechtste geval O(V) stackruimte gebruikt voor maximale diepte. Dijkstra’s algoritme heeft daarentegen O(V) nodig voor de prioriteitswachtrij en het bijhouden van afstanden per knoop. De representatie van de graaf is van belang: de adjacentiematrix gebruikt O(V²) terwijl de adjacentielijst O(V+E) gebruikt. 

Ruimtekenmerken van datastructuren

Er zijn verschillende datastructuren en elk vereist andere geheugenallocaties. Bijvoorbeeld: arrays en ArrayLists slaan n elementen op in O(n) aaneengesloten geheugen, gelinkte lijsten vereisen O(n) ruimte plus pointer-overhead per knoop, binaire bomen gebruiken O(n) ruimte met 2–3 pointers per knoop wat aanzienlijke overhead toevoegt, hashtabellen verbruiken O(n) ruimte vermenigvuldigd met de load factor die typisch varieert tussen 1,5 en 3 keer, en adjacentielijsten bieden O(V+E) ruimte-efficiëntie voor spaarzame grafen.

Voorbeelden van berekening van ruimtecomplexiteit

Hier zijn voorbeelden voor de berekening van ruimtecomplexiteit

# O(1): In-place string reversal
left = 0
right = len(text) - 1
while left < right:
    text[left], text[right] = text[right], text[left]  # O(1)
    left += 1
    right -= 1

# O(n): Detect duplicate using hash set
seen_numbers = set()                                      # O(n)
for num in array:
    if num in seen_numbers:
        return True
    seen_numbers.add(num)

# O(log n): Recursive binary tree height
def tree_height(root):
    if not root:
        return 0
    return 1 + max(tree_height(root.left), tree_height(root.right))  # O(log n) stack

# O(n²): Floyd-Warshall distance matrix
distance = [[float('inf')] * vertices for _ in range(vertices)]  # O(n²)
for i in range(vertices):
    distance[i][i] = 0

Ruimtecomplexiteit vs. tijdcomplexiteit

Tijd en ruimte vertegenwoordigen verschillende resource-dimensies die in balans moeten worden gebracht op basis van de systeembeperkingen:

Belangrijkste verschillen en overeenkomsten

Op het eerste gezicht lijken ruimte- en tijdcomplexiteit op elkaar, maar ze verschillen ook: tijdcomplexiteit meet computationele operaties, terwijl ruimtecomplexiteit het geheugengebruik meet. Hoewel ze identieke asymptotische notaties en wiskundige analyse­kaders gebruiken, moet je weten dat algoritmen optimaal kunnen zijn in de ene dimensie maar inefficiënt in de andere. Optimalisatie omvat vaak het inruilen van meer ruimte voor minder tijd of andersom.

Het fundamentele trade-off begrijpen

Als je ruimte voor tijd inruilt, sla je vooraf berekende resultaten op die overbodige berekeningen elimineren. Tijd voor ruimte inruilen vereist daarentegen het opnieuw berekenen van waarden op aanvraag in plaats van ze op te slaan. Lookup-tabellen ruilen een eenmalige ruimte-investering in voor herhaalde O(1)-toegang. Er kunnen specifieke beperkingen zijn, zoals geheugenlimieten en prestatie-eisen, die dicteren welke resource prioriteit krijgt.

Memoization en optimalisatie met dynamic programming

Memoization werkt door waarden op te slaan in plaats van ze opnieuw te berekenen; het is efficiënt in tijd maar slecht voor ruimte. Het voegt bijvoorbeeld O(n) cache toe aan recursieve Fibonacci, maar reduceert de tijd van O(2^n) naar O(n), zoals het onderstaande voorbeeld van gememoizeerde Fibonacci, dat herberekening vermijdt:

memory = {}
def fibonacci_memory(n):
    if n in memo:
        return memory[n] # O(n) space, O(n) time
    if n <= 1:
        return n
    memory[n] = fibonacci_memory(n-1) + fibonacci_memory(n-2)
    return memory[n]

Bottom-up dynamic programming daarentegen rekent iteratief om de overhead van de recursiestack te elimineren. Om ruimte te optimaliseren, behoud je alleen de vorige toestanden die nodig zijn voor de huidige berekening; zo kan de edit distance worden teruggebracht van een O(nxm)-tabel naar O(min(n,m)) door één rij te bewaren.

In-place algoritmetechnieken

In-place algoritmen passen de invoer direct aan in plaats van nieuwe geheugenstructuren te alloceren, wat, indien iteratief, het geheugenverbruik aanzienlijk helpt en zo de ruimtecomplexiteit verlaagt, zoals het onderstaande voorbeeld van in-place arrayrotatie (omkeer-algoritme):

def rotate_array(nums, k):
    n = len(nums)
    k = k % n
    reverse(nums, 0, k - 1)  # all operations O(1) auxiliary
    reverse(nums, k, n - 1)
    reverse(nums, 0, n - 1)

Andere in-place technieken reduceren het geheugenverbruik drastisch. Zo manipuleert de twee-pointertechniek arrays met O(1) auxiliary space, en lost cyclic sort permutatieproblemen op in O(1) ruimte door elementen naar de juiste posities te swappen. Let wel: in-place aanpassing kan invoergegevens vernietigen, wat herbruikbaarheid en code-duidelijkheid beïnvloedt.

Datastructuurkeuze voor ruimte-efficiëntie

Er zijn veel verschillende datastructuren, elk met een andere trade-off; geef daarom bijvoorbeeld de voorkeur aan arrays boven gelinkte lijsten wanneer de grootte bekend is, elimineer per-element pointer-overhead, en manipuleer bits om booleaanse vlaggen op te slaan met 8 keer compressie ten opzichte van byte-arrays. Tries delen gemeenschappelijke prefixen en bieden ruimte-efficiëntie voor grote verzamelingen strings. Kies voor spaarzame grafen adjacentielijsten boven matrices om O(V+E) in plaats van O(V²) ruimte te krijgen.

Praktische overwegingen en implementatierealiteiten

Ruimtecomplexiteit in de praktijk hangt af van programmeertalen, hardware-architectuur en runtime-omgevingen.

Taal- en runtimefactoren

Hogere programmeertalen zoals Python en Java voegen per-objectmetadata en -overhead toe, in tegenstelling tot C en C++, die handmatig geheugenbeheer bieden voor precieze controle, ten koste van complexiteit. Daarom gebruiken machine learning-optimalisatieframeworks zoals PyTorch vaak C++ voor berekeningen.

Stackgroottelimieten variëren doorgaans van 1 tot 8 MB, wat de maximale recursiediepte beperkt. Let op dat 64-bit systemen 8-bytepointers gebruiken terwijl 32-bit systemen 4-bytepointers gebruiken, wat de pointerstructuurgroottes verdubbelt. Wil je meer leren over hoe hoog-dimensionale data de algoritme­prestatie beïnvloedt, bekijk dan onze tutorial: The Curse of Dimensionality in Machine Learning.

Geheugenhiërarchie en cache-effecten

Het ruimtegebruik en de prestaties van algoritmen hangen sterk af van de geheugenhiërarchie. RAM-toegang duurt rond 100 nanoseconden en CPU-cachetoegang 1 tot 10 nanoseconden, wat 100 keer sneller is. Daarnaast benut sequentiële geheugentoegang cachelijnen waarbij typisch 64 bytes samen worden geladen.

Algoritmen met goede ruimtelijke localiteit behalen betere prestaties door cache-efficiëntie. Wanneer je met grote datasets werkt die de capaciteit overschrijden, veroorzaakt dat thrashing, wat de prestaties sterk verslechtert.

Garbage collection en geheugenbeheer

Een nuttig hulpmiddel voor geheugenbeheer is garbage collection, waarmee je data kunt opruimen die niet meer nuttig is. Met andere woorden, garbage collectors voegen geheugenoverhead toe voor objecttracking en generatiebeheer.

Piekgeheugengebruik overschrijdt de grootte van live data door allocaties die plaatsvinden vóór collectierondes. Bovendien optimaliseert Generationele GC voor kortlevende objecten door jonge generaties in kleinere ruimtes te houden. Wil je echter meer deterministische ruimtelijke controle, dan kun je handmatig geheugenbeheer gebruiken, maar wees je bewust van de risico’s, zoals geheugenlekken en fragmentatie.

Conclusie en aanbevelingen

Bedankt voor het lezen van mijn artikel over ruimtecomplexiteit. Ik laat je achter met een paar laatste tips:

  • Verduidelijk altijd of de ruimte-analyse de invoergrootte omvat om miscommunicatie in interviews en code reviews te voorkomen.
  • Prioriteer eerst correctheid, optimaliseer daarna pas ruimte wanneer profilering echte knelpunten aantoont.
  • Houd vroeg in systeemontwerp rekening met hardware- en geheugenbeperkingen om kostbare refactors te voorkomen.
  • Kies altijd de taalspecifieke profilingtools om theoretische analyses te valideren tegen daadwerkelijk gedrag en weeg ruimte-optimalisatie af tegen codeleesbaarheid. Te slimme optimalisaties kunnen ook de onderhoudbaarheid schaden. 
Iheb Gafsi

Auteur

Iheb Gafsi

Ik werk aan versnelde AI-systemen die edge-intelligentie mogelijk maken met gefedereerde ML-pijplijnen op gedecentraliseerde data en gedistribueerde workloads.  Mijn werk richt zich op Large Models, spraakverwerking, computervisie, reinforcement learning en geavanceerde ML-topologieën.

Veelgestelde vragen

Als schaarse data slechts 10% van de gealloceerde O(n)-ruimte gebruikt, is de complexiteit dan nog steeds O(n)?

Ja. Ruimtecomplexiteit gaat uit van het worstcasescenario. Schaarse garanties zijn een ander probleemgeval en worden niet weerspiegeld in Big O-analyse.

Kan ruimtecomplexiteit beter zijn dan tijdcomplexiteit?

Ja. Lineair zoeken is O(1) ruimte maar O(n) tijd. Je itereert zonder iets op te slaan.

Waarom verslechtert memoization de ruimtecomplexiteit ondanks dat het de tijd versnelt?

Het ruilt tijd in voor ruimte. Fibonacci verbetert van O(2^n) tijd naar O(n), maar voegt O(n) ruimte toe voor caching.

Vermindert het elimineren van tail-recursie de ruimtecomplexiteit?

Ja, echt. Het zet recursie om in lussen en elimineert stackframes. Faculteit gaat van O(n) → O(1) ruimte wanneer geoptimaliseerd.

Als hashtabellen 3× zoveel geheugen gebruiken door de load factor, is de complexiteit dan O(3n) of O(n)?

O(n). Big O negeert constante factoren. De 3x-vermenigvuldiger is overhead die onafhankelijk is van de invoergrootte.

Onderwerpen