← Back to CoursesStemExpert
StemExpert
Expert · Handboek · 34. Algoritmen en complexiteit
StemExpert · Brecht Corbeel · schoolium.me
StemExpert
E34. Algoritmen en complexiteit
EExpert · deel E7 · Informatica

Handboek · hoofdstuk 34Algoritmen en complexiteit

Een correct programma wordt pas bruikbaar als zijn werk binnen het budget past

De inspectierobot moet een meetpunt bereiken voordat zijn batterij leeg raakt. De Pi moet miljoenen logregels verwerken terwijl de regeling doorloopt. Je onderzoekt welke algoritmen hetzelfde antwoord leveren, hoeveel werk ze vragen en welke aannamen hun correctheid dragen.

8× uitleg3× uitgewerkt voorbeeld1× naslag1× verhaal1× het geheel2× code9 opdrachten in het werkboek± 10 lestijden
De snelste route op een kaart is alleen bruikbaar als elke verbinding werkelijk berijdbaar is.
De snelste route op een kaart is alleen bruikbaar als elke verbinding werkelijk berijdbaar is.
Na dit hoofdstuk
Uitleg · 34.1

Van recept naar bewijs

1/16

Een algoritme beschrijft eindige, ondubbelzinnige stappen voor een klasse problemen. De specificatie legt invoer, uitvoer en uitzonderingen vast. ‘Zoek een temperatuur’ is onvolledig: zoek je een exact tijdstip, de eerste overschrijding of de dichtstbijzijnde waarde? Een correcte implementatie kan toch de verkeerde specificatie uitvoeren.

Drie bewijsstukken horen bij het ontwerp: een preconditie, een invariant tijdens de uitvoering en een postconditie. Lineair zoeken bewaart: alle eerder bekeken posities bevatten de gezochte sleutel niet. Wanneer een overeenkomst verschijnt, is de gevonden index juist; wanneer de lijst eindigt, is de sleutel afwezig. De lus eindigt omdat het aantal ongecontroleerde elementen telkens afneemt.

contractvoorbeeld
invoerlijst met tijdstempels, sleutel t
uitvoerindex of None
foutgevaldubbele tijdstempels: eerste index
budgethoogstens n vergelijkingen
Uitleg · 34.2

Zoeken: elke rij of telkens de helft

2/16

Lineair zoeken vraagt in het slechtste geval vergelijkingen. Binair zoeken vereist een gesorteerde reeks en snelle toegang tot het middelste element. Na een vergelijking blijft alleen een interval over: links als de sleutel kleiner is, rechts als hij groter is. Invariant: als de sleutel bestaat, ligt hij in het overblijvende interval.

Werk met een halfopen interval : begin met , . Kies . Bij een kleinere sleutel wordt , bij een grotere . Voor is het interval leeg. De verminderende intervalbreedte bewijst beëindiging, ook bij een lege lijst. Na halveringen blijven hoogstens elementen over: de groei is logaritmisch. Een gekoppelde lijst kent geen goedkope middentoegang; daar verdwijnt dit voordeel.

binair zoeken in een gesorteerde array
Uitgewerkt voorbeeld · 34.3

Een jaarlog doorzoeken

3/16
Een logger bewaart elke minuut een meting. Het jaar telt in dit rekenvoorbeeld 365 dagen.
Gegeven
  • 525 600 records
  • één sleutelvergelijking kost in het model 0,2 µs
Gevraagd
  • Worst-case zoektijd bij lineair en binair zoeken
Oplossing
  1. 1
    Het aantal records is .
  2. 2
    Lineair: .
  3. 3
    Omdat en , volstaan ten hoogste 20 sleutelvergelijkingen: 4 µs in dit model.
Antwoord
Het verschil is ruim vier ordes van grootte, als de records al geordend zijn.
Klopt dit? De 0,2 µs is een rekenaanname; schijf-I/O, Python-overhead en netwerkvertraging zijn niet inbegrepen.
Uitleg · 34.4

Sorteren: wat kost orde maken?

4/16

Invoegsorteren houdt links een gesorteerd prefix bij. Elk nieuw element schuift naar zijn plaats. Bij omgekeerde invoer zijn er verschuivingen: . Bij reeds gesorteerde invoer is het lineair. Het algoritme kan stabiel zijn: records met dezelfde sleutel behouden hun volgorde.

Mergesort splitst de lijst in tweeën, sorteert beide helften en voegt ze samen. Samenvoegen kost ; op elke van ongeveer niveaus wordt in totaal werk gedaan. Dus . Een gewone arrayimplementatie gebruikt tijdelijk geheugen. Quicksort verdeelt rond een pivot: gemiddeld , maar bij slechte verdelingen. Een bibliotheeksortering is gewoonlijk verstandiger dan een eigen sorteerroutine, tenzij een meetbaar geheugen- of real-time contract anders vereist.

algoritmeslechtste tijdextra geheugen
invoegsorterenO(n²)O(1)
mergesort arraysO(n log n)O(n)
quicksort, basisvormO(n²)O(n) recursiestapel in slechtste geval
Code · 34.5

Zoeken, sorteren en recursie echt uitvoeren

5/16

De uitvoer telt drie verschillende soorten werk expliciet. Invoegsorteren geeft bij omgekeerde data 28, 120 en 496 verschuivingen. Mergesort geeft voor deze invoervorm 12, 32 en 80 sleutelvergelijkingen; dit is niet het slechtste geval. Naïeve Fibonacci doet voor n=10 precies 177 aanroepen; memoïsatie berekent slechts 11 verschillende deelproblemen. De uitvoer van zoeken toont elk interval en middenpunt, inclusief de afwezige sleutel.

Pythonzoek_sorteer_recursie.py56 regelsDownload
from functools import cache def zoek(a, sleutel):    l,r = 0,len(a)    spoor = []    while l < r:        m = (l+r)//2        spoor.append((l,r,m,a[m]))        if a[m] == sleutel: return m,spoor        if a[m] < sleutel: l=m+1        else: r=m    return None,spoor def invoeg(a):    a = list(a)    verschuivingen = 0    for i in range(1,len(a)):        sleutel = a[i]        j = i        while j>0 and a[j-1]>sleutel:            a[j]=a[j-1]; j-=1; verschuivingen+=1        a[j]=sleutel    return a,verschuivingen def merge(a):    if len(a)<2: return list(a),0    m=len(a)//2    links,nl=merge(a[:m]); rechts,nr=merge(a[m:])    i=j=0; resultaat=[]; vergelijkingen=nl+nr    while i<len(links) and j<len(rechts):        vergelijkingen+=1        if links[i]<=rechts[j]: resultaat.append(links[i]); i+=1        else: resultaat.append(rechts[j]); j+=1    return resultaat+links[i:]+rechts[j:],vergelijkingen for sleutel in (13,8):    print("zoeken",sleutel,zoek([2,4,7,9,13,18,25,31],sleutel))for n in (8,16,32):    data=list(range(n,0,-1))    a,ni=invoeg(data); b,nm=merge(data)    assert a==b==sorted(data)    print("n",n,"verschuivingen",ni,"mergevergelijkingen",nm) aanroepen=0def fib(n):    global aanroepen    aanroepen+=1    return n if n<2 else fib(n-1)+fib(n-2)print("fib(10)",fib(10),"aanroepen",aanroepen)aanroepen=0@cachedef fib_geheugen(n):    global aanroepen    aanroepen+=1    return n if n<2 else fib_geheugen(n-1)+fib_geheugen(n-2)print("fib memo",fib_geheugen(10),"berekende deelproblemen",aanroepen)
zoeken 13 (4, [(0, 8, 4, 13)])
zoeken 8 (None, [(0, 8, 4, 13), (0, 4, 2, 7), (3, 4, 3, 9)])
n 8 verschuivingen 28 mergevergelijkingen 12
n 16 verschuivingen 120 mergevergelijkingen 32
n 32 verschuivingen 496 mergevergelijkingen 80
fib(10) 55 aanroepen 177
fib memo 55 berekende deelproblemen 11
Uitleg · 34.6

O is een bovengrens, geen stopwatch

6/16

betekent: er bestaan en zodat voor alle . Constanten en lagere machten verdwijnen uit de groeiklasse: . Met zeg je ook dat de orde van onderen begrensd is. Een algoritme is formeel tevens ; daarom vermeld je de scherpste nuttige grens.

Een werkmodel moet benoemen wat telt: vergelijkingen, vermenigvuldigingen, bytes of berichten. Op de Mega kan geheugengebruik de doorslag geven; op de Pi kan netwerk-I/O belangrijker zijn dan rekenen. Dezelfde asymptotische klasse kan een factor honderd verschillen. Meet met herhalingen, geef invoergrootte en omstandigheden en gebruik de mediaan; een kleine proef bewijst geen worst-case grens.

81624324048566401024204830724096nmodelaantal bewerkingennn log₂ nn²
Bij n = 64 telt het kwadratische model 4096 bewerkingen; het n log₂ n-model 384.
Uitgewerkt voorbeeld · 34.7

Wanneer het kwadratische model vastloopt

7/16
Een controle vergelijkt elk record met elk ander record.
Gegeven
  • Modeltijd T = 0,5 µs · n²
  • tijdlimiet 1 s
Gevraagd
  • Grootste n binnen het modelbudget
Oplossing
  1. 1
    Los op: .
  2. 2
    Een geheel aantal records geeft n ≤ 1414; n = 1415 overschrijdt het budget.
  3. 3
    Een gesorteerde controle van opeenvolgende sleutels vraagt slechts n−1 vergelijkingen na het sorteren.
Antwoord
Ongeveer 1400 records: een jaarlog is vele malen te groot.
Klopt dit? De grens betreft dit werkmodel; een echt systeem moet ook I/O en piekbelasting meten.
Uitleg · 34.8

Recursie: een kleinere kopie van het probleem

8/16

Recursie heeft een basisgeval en een stap die aantoonbaar dichterbij dat geval komt. Voor faculteit: en voor geheel . De invoer moet gevalideerd worden; een negatieve n bereikt 0 nooit. Elke actieve aanroep bewaart een frame: bij faculteit zijn tijd en stapel beide . Een iteratieve lus gebruikt extra geheugen.

De naïeve Fibonacci-recursie herberekent dezelfde deelproblemen. De aanroepboom groeit exponentieel. Memoïsatie bewaart elk resultaat eenmaal; dan worden tijd en geheugen . Een iteratieve oplossing bewaart alleen de vorige twee waarden. Deel-en-heers zoals mergesort is nuttige recursie, omdat de deelproblemen zonder deze explosieve overlap ontstaan.

overlappende Fibonacci-deelproblemen
Uitleg · 34.9

De gang wordt een graaf

9/16

Een kruising of meetplaats is een knoop; een berijdbare verbinding is een kant. De afstand is een gewicht. Eenrichtingsverkeer vraagt gerichte kanten. Verboden doorgangen haal je uit de graaf; een grote straf is ongeschikt als de route echt verboden is. Bij vaste snelheid is afstand een bruikbare doelfunctie; bij draaien, wachten en hellingen modelleer je reistijd of energie.

Een adjacentielijst bewaart per knoop de buren: geheugen. Een adjacentiematrix kost en biedt directe kantopzoeking. Niet elk paar knopen is verbonden; onbereikbaarheid hoort als oneindige afstand in de uitvoer. Het voorbeeld breidt de lusrobot van i33 uit met keuzeroutes, zonder de eerdere ganggeometrie als echte stationskaart te presenteren.

241525142LaadABCDE
Kanten zijn berijdbare verbindingen; getallen zijn modelafstanden in meter, geen schaaltekening van de gang.
Uitgewerkt voorbeeld · 34.10

Voorlopig is nog niet definitief

10/16
Dijkstra begint bij Laad en houdt een wachtrij met kandidaatparen (afstand,knoop) bij.
241525142LaadABCDE
Kanten zijn berijdbare verbindingen; getallen zijn modelafstanden in meter, geen schaaltekening van de gang.
Gegeven
  • Alle kantgewichten staan in de voorbeeldgraaf
Gevraagd
  • De eerste drie extracties en relaxaties
Oplossing
  1. 1
    Laad met 0 wordt definitief. Relaxaties leveren A=2 en B=4; wachtrij (2,A),(4,B).
  2. 2
    A met 2 wordt genomen. B verbetert van 4 naar 3 via A; C krijgt 7 via A. De heap bevat (3,B),(4,B),(7,C).
  3. 3
    B met 3 wordt genomen. C verbetert naar 5 en D krijgt 8. De oude (4,B) wordt later verworpen, omdat afstand[B] inmiddels 3 is. De invariant betreft de beste afstand per knoop, niet elke heapentry.
Antwoord
Definitief: Laad 0, A 2, B 3. Kandidaten C 5 en D 8.
Klopt dit? B=4 was ooit plausibel maar nooit definitief; voorganger[B] moet bij de verbetering ook veranderen.
Uitleg · 34.11

Dijkstra: waarom de kleinste kandidaat definitief is

11/16

Begin met afstand 0 bij de bron en oneindig elders. Neem telkens de kleinste voorlopige afstand uit de prioriteitswachtrij. Relaxeer alle uitgaande kanten: . Bewaar bij een verbetering de voorganger. Het pad lees je achterwaarts terug.

Het correctheidsargument gebruikt niet-negatieve gewichten: een omweg door nog onbekende knopen kan een reeds kleinste afstand niet verkleinen. Met een negatieve kant breekt dat argument. Een heap biedt per extractie of invoeging; met een gebruikelijke efficiënte implementatie is de tijd . Deze lescode laat oude heapkandidaten staan: bij verwerking worden ze verworpen. De heap kan dan entries bewaren, met tijd ; voor eenvoudige grafen is dit dezelfde gebruikelijke logaritmische orde.

definitiefafstand (m)voorganger
Laad0—
A2Laad
B3A
C5B
D6C
E8D
Code · 34.12

Kortste pad en een kleine meetronde

12/16

De uitvoer wordt bij het bouwen opnieuw berekend. De meetronde bezoekt A, C en E en keert terug; we testen voor slechts drie meetpunten alle zes volgordes. Dijkstra berekent de afstanden tussen meetpunten, maar kiest zelf geen optimale rondreis.

Pythonrobot_route.py46 regelsDownload
from heapq import heappush, heappopfrom itertools import permutations G = {k: {} for k in ("Laad", "A", "B", "C", "D", "E")}for a, b, w in [("Laad","A",2), ("Laad","B",4), ("A","B",1),                ("A","C",5), ("B","C",2), ("B","D",5),                ("C","D",1), ("C","E",4), ("D","E",2)]:    G[a][b] = G[b][a] = w def dijkstra(g, start):    afstand = {v: float("inf") for v in g}    vorig = {}    afstand[start] = 0    wachtrij = [(0, start)]    while wachtrij:        d, v = heappop(wachtrij)        if d != afstand[v]:  # oude kandidaat na een betere route            continue        for buur, w in g[v].items():            if w < 0:                raise ValueError("Dijkstra vereist niet-negatieve gewichten")            nieuw = d + w            if nieuw < afstand[buur]:                afstand[buur] = nieuw                vorig[buur] = v                heappush(wachtrij, (nieuw, buur))    return afstand, vorig def pad(vorig, doel):    route = [doel]    while route[-1] in vorig:        route.append(vorig[route[-1]])    return route[::-1] d, v = dijkstra(G, "Laad")print("afstanden", d)print("route", " -> ".join(pad(v, "E")))print("rijtijd", d["E"] / 0.20, "s")stations = ("A", "C", "E")alle = {k: dijkstra(G, k)[0] for k in ("Laad",)+stations}def rondelengte(volgorde):    p = ("Laad",)+volgorde+("Laad",)    return sum(alle[a][b] for a,b in zip(p,p[1:]))best = min(permutations(stations), key=rondelengte)print("meetronde", best, rondelengte(best), "m")assert d["E"] == 8 and rondelengte(best) == 16
afstanden {'Laad': 0, 'A': 2, 'B': 3, 'C': 5, 'D': 6, 'E': 8}
route Laad -> A -> B -> C -> D -> E
rijtijd 40.0 s
meetronde ('A', 'C', 'E') 16 m
241525142LaadABCDE
Kanten zijn berijdbare verbindingen; getallen zijn modelafstanden in meter, geen schaaltekening van de gang.
Uitleg · 34.13

Eén bestemming verschilt van alle bezoeken

13/16

Een kortste pad tussen twee knopen is een ander probleem dan een rondreis die alle inspectiepunten bezoekt. Het handelsreizigersprobleem heeft in een brute-force aanpak volgordes; symmetrie kan dat aantal verkleinen, maar de groei blijft explosief. Voor drie punten zijn zes volgordes klein, voor twintig zijn het circa .

Een heuristiek zoals telkens de dichtstbijzijnde onbezochte plek geeft snel een route, zonder garantie op het optimum. A* gebruikt een ondergrens op de resterende afstand: met een geschikte admissibele heuristiek blijft een optimale oplossing mogelijk. Bij veranderende obstakels moet de robot opnieuw plannen. Lokale obstakeldetectie en de noodstop blijven afzonderlijke functies: een optimale kaart mag nooit een actuele blokkade overrulen.

brute force groeit sneller dan de robot kan wachten
Verhaal · 34.14

Een echte softwareles: bibliotheken hebben voorwaarden

14/16

De Python-documentatie beschrijft bisect als invoegposities zoeken in een gesorteerde lijst. De zoekstap is logaritmisch, maar invoegen in een array verschuift elementen en kost lineaire tijd. Een naam zoals ‘binaire invoeging’ maakt dus de volledige bewerking niet logaritmisch. Voor de stationslogger, die bijna altijd in tijdsvolgorde schrijft, is achteraan toevoegen eenvoudiger.

De documentatie van heapq beschrijft de min-heap: het kleinste element ligt bovenaan, terwijl de overige lijst niet volledig gesorteerd is. Een heap en een geordende lijst hebben verschillende invarianten. Bron: Python bisect en heapq, geraadpleegd 30-09-2026.

structuursnelvoorwaarde
bisectpositie zoekenlijst blijft gesorteerd
heapminimum verwijderenheapinvariant
hash-tabelsleutel opzoeken, gemiddeldhash en gelijkheid correct
Naslag · 34.15

Algoritmen kiezen met een contract

15/16
invoegsorteren, omgekeerde invoer
symboolbetekeniseenheid
aantal records—
situatiekeuzecontrole
één sleutel, ongesorteerdlineaireerste/laatste/afwezig
veel sleutels, geordendbinairdubbele sleutels en intervalgrenzen
niet-negatieve routekostenDijkstraonbereikbaar, nullen, duplicaten
harde deadlinebegrensd algoritmeworst-case geheugen en uitvoering
Het geheel · 34.16

Rekenen, bewegen en plannen

16/16

Een algoritme verplaatst geen robot zonder correcte kaart, eenheidsafspraken en een actuatorlaag. De routeplanner produceert een lijst van knopen; de bestaande lijnvolger voert de verbindingen uit. Het meetbudget omvat rijden, stilstand, controleren en terugkeren.

De bestaande robot voert de route uit.

Planner en bestuurder worden aparte modules.

Een deadline vraagt een begrensde uitvoering.

Een afhankelijkheidsgraaf geeft ook een kritisch pad.

Habitat

Habitat krijgt een routeplanner op de Pi; de microcontroller behoudt afstandsbewaking en stoppen. De gewogen graaf is een expliciet voorbeeldmodel, te vervangen door een opgemeten kaart.

Samenvatting

In het kort

Begrippen

Wat je nu kent

tijdscomplexiteit
De groei van het aantal elementaire bewerkingen als functie van de invoergrootte.
invariant
Een eigenschap die vóór en na elke lusiteratie waar blijft.
recursie
Een functie die kleinere exemplaren van hetzelfde probleem oplost door zichzelf aan te roepen.
graaf
Een verzameling knopen met verbindingen, eventueel met richting en gewicht.
relaxatie
Een voorlopige padafstand vervangen wanneer een kortere route wordt gevonden.
Aan de slag in het werkboek9 opdrachten, van oefenen tot uitdagen