Leer Algoritmen & complexiteit vanaf nul
1Wat complexiteit meet
Een algoritme is een eindige lijst van precieze stappen die een probleem oplost, zoals een lijst sorteren of een naam in een telefoonboek zoeken.
Twee algoritmen kunnen hetzelfde probleem oplossen maar heel verschillend veel tijd kosten. Complexiteit beschrijft hoe het werk groeit als de invoer groter wordt.
De grootte van de invoer heet n: het aantal items om te sorteren, de lengte van een lijst, het aantal knopen in een graaf.
Denk aan het zoeken van een naam in een telefoonboek van 1000 pagina’s. Elke pagina lezen kost maximaal 1000 stappen. Het boek in het midden openen en telkens halveren kost ongeveer 10 stappen.
Tijdcomplexiteit telt de stappen. Ruimtecomplexiteit telt het extra geheugen. Beide worden geschreven als functie van n.
Meestal kijk je naar het slechtste geval: het meeste werk over alle invoeren van grootte n. Soms wordt in plaats daarvan naar het gemiddelde of het beste geval gevraagd.
Zelftoets Een algoritme waarvan het werk groeit als n² doet er 1 seconde over voor n = 1000. Hoe lang duurt het ongeveer voor n = 2000?
- 1 seconde
- 2 seconden
- 4 seconden
- 8 seconden
Antwoord: 4 seconden. Een verdubbeling van n vermenigvuldigt n² met 2² = 4, dus de tijd gaat van ongeveer 1 naar ongeveer 4 seconden. 2 seconden zou het antwoord zijn bij lineaire groei, en 8 bij n³.
2Big O en de groeiklassen
De Big O-notatie houdt alleen over wat telt voor grote n. Je laat constante factoren weg en houdt alleen de snelst groeiende term.
3n² + 5n + 2 ⇒ O(n²)
Voor n = 1000 is de n²-term 3 000 000, terwijl 5n maar 5000 is. De kleinere termen veranderen het totaal nauwelijks.
Deze klassen komen steeds terug, van langzaamste naar snelste groei:
- O(1), constant: een array-element lezen via zijn index.
- O(log n), logaritmisch: binair zoeken, dat het probleem in elke stap halveert.
- O(n), lineair: één keer langs alle items, zoals het zoeken van de grootste waarde.
- O(n log n): goede sorteeralgoritmen zoals merge sort.
- O(n²), kwadratisch: twee geneste lussen over de items, zoals bubble sort.
- O(2ⁿ), exponentieel: elke deelverzameling proberen. Het wordt snel onbruikbaar.
Big O is een bovengrens: het werk groeit hoogstens zo snel. Ω (Big Omega) is een ondergrens en Θ (Theta) is een scherpe grens, zowel boven als onder.
Het grondtal van een logaritme doet er in Big O niet toe, omdat logaritmen met verschillende grondtallen alleen door een constante factor verschillen.
Zelftoets Wat is de Big O van 4n + 2n² + 100?
- O(100)
- O(2n²)
- O(n)
- O(n²)
Antwoord: O(n²). Voor grote n domineert de n²-term en wordt de constante factor 2 weggelaten. O(n) houdt de verkeerde term, en O(2n²) vergeet de constante weg te laten.
3Zoeken, sorteren en recursie
Een paar algoritmen komen in bijna elke cursus voor. Leer hun complexiteiten als feiten.
- Lineair zoeken controleert items één voor één: O(n). Binair zoeken heeft een gesorteerde array nodig en halveert het bereik in elke stap: O(log n).
- Bubble sort, insertion sort en selection sort vergelijken en verwisselen items in geneste lussen: O(n²). Insertion sort is O(n) op een array die al gesorteerd is.
- Merge sort splitst de array in helften, sorteert ze en voegt ze samen: O(n log n) in elk geval, met O(n) extra geheugen.
- Quicksort splitst rond een pivot: O(n log n) gemiddeld, maar O(n²) in het slechtste geval, bijvoorbeeld een gesorteerde array met het eerste element als pivot.
- Geen sortering die alleen items vergelijkt, kan in het slechtste geval beter zijn dan Ω(n log n). Counting sort is sneller omdat het niet vergelijkt.
Een recursieve functie roept zichzelf aan op een kleinere invoer totdat hij een basisgeval bereikt dat hij direct beantwoordt. Zijn rekentijd wordt geschreven als een recurrente betrekking:
- T(n) = T(n/2) + O(1) geeft O(log n), zoals binair zoeken.
- T(n) = 2T(n/2) + O(n) geeft O(n log n), zoals merge sort.
- T(n) = T(n − 1) + O(n) geeft O(n²).
- T(n) = 2T(n − 1) + O(1) geeft O(2ⁿ).
Drie ontwerpideeën zijn het kennen waard. Verdeel en heers splitst, lost op en combineert. Greedy kiest in elke stap de lokaal beste keuze. Dynamisch programmeren slaat de antwoorden op van overlappende deelproblemen, zodat ze maar één keer worden berekend.
Zelftoets Wat is de tijdcomplexiteit van quicksort in het slechtste geval?
- O(log n)
- O(n log n)
- O(n)
- O(n²)
Antwoord: O(n²). Bij slechte pivots laat elke splitsing n − 1 items aan één kant over, wat O(n²) geeft. O(n log n) is het gemiddelde geval, en het slechtste geval van merge sort.
4De methode stap voor stap
Zo vind je de tijdcomplexiteit van een stuk code:
- Bepaal wat n is: de grootte van de invoer.
- Tel hoe vaak elke lus draait. Een lus die elke keer 1 optelt, draait ongeveer n keer. Een lus die verdubbelt of halveert, draait ongeveer log₂ n keer.
- Lussen binnen elkaar vermenigvuldig je. Lussen na elkaar tel je op.
- Schrijf bij een recursieve functie de recurrente betrekking op: hoeveel aanroepen, op welke grootte, plus het werk per aanroep.
- Houd de snelst groeiende term en laat de constanten weg.
- Controleer met een klein getal, bijvoorbeeld n = 8 of n = 1024.
Noem bij een theorievraag eerst het algoritme of idee dat wordt beschreven, en haal dan zijn complexiteit en voorwaarden op (gesorteerde invoer, niet-negatieve gewichten, overlappende deelproblemen).
5Een uitgewerkt voorbeeld
Volg elke stap. Probeer de volgende stap te voorspellen voordat je hem opent.
Uitgewerkt voorbeeldWat is de tijdcomplexiteit van deze code? for i = 1 to n: (j = 1; while j < n: j = j × 2)
1n is het getal waartot de lussen lopen.
2i loopt van 1 tot n in stappen van 1, dus de buitenste lus draait n keer.
3j neemt de waarden 1, 2, 4, 8, … aan en stopt als het n bereikt. Na k rondes is j = 2k, dus de lus stopt na ongeveer log₂ n rondes.
4De binnenste lus zit in de buitenste lus, dus de aantallen vermenigvuldigen.
n · log₂ n ⇒ O(n log n)
5Voor n = 1024 draait de binnenste lus log₂ 1024 = 10 keer, dus de body draait 1024 · 10 = 10 240 keer. Dat is veel minder dan n² = 1 048 576.
6Veelgemaakte fouten
- Lussen vermenigvuldigen die na elkaar draaien. n + n = 2n is nog steeds O(n).
- Constanten houden, zoals in O(2n) of O(n/2). Beide zijn O(n).
- Een verdubbelingslus tellen als n rondes. Hij draait ongeveer log₂ n keer.
- Binair zoeken gebruiken op ongesorteerde gegevens, of op een gelinkte lijst waar het midden niet in één stap bereikt kan worden.
- Het gemiddelde geval van quicksort geven terwijl naar het slechtste geval wordt gevraagd.
- Denken dat grote O “precies” betekent. O(n²) is een bovengrens; Θ(n²) is de exacte groei.
Zelftoets Een functie voert een lus van n iteraties uit en daarna, los daarvan, nog een lus van n iteraties. Wat is de tijdcomplexiteit?
- O(2n)
- O(log n)
- O(n)
- O(n²)
Antwoord: O(n). Lussen die na elkaar komen, tel je op: n + n = 2n, en de constante laat je weg, dus O(n). O(n²) zou alleen juist zijn als de ene lus binnen de andere zat.
7Hoe tentamenvragen eruitzien
- Makkelijk: kernbegrippen en feiten. De complexiteit van binair zoeken, merge sort of toegang tot een array, de betekenis van O, Ω en Θ, wat recursie is.
- Gemiddeld: concepten toepassen. De complexiteit van een stuk code, een recurrente betrekking oplossen, hoe de tijd verandert als n verdubbelt, welk algoritme bij een taak past.
- Moeilijk: gevallen en analyse. De hoofdstelling (master theorem), slechtste gevallen van quicksort, greedy versus dynamisch programmeren, en de klassen P, NP en NP-volledig.
P bevat de problemen die in polynomiale tijd op te lossen zijn. NP bevat de problemen waarvan de oplossingen in polynomiale tijd gecontroleerd kunnen worden. Elk probleem in P zit in NP; of P = NP is nog onbekend.
Leer het stap voor stap in de appLeer het stap voor stap in de app