Stapels en wachtrijen uitgelegd: LIFO, FIFO en gelinkte lijsten
Stack en queue stap voor stap: push, pop, enqueue en dequeue, implementatie met array en gelinkte lijst, haakjes controleren en code in Python en Java.
Een stapel (stack) en een wachtrij (queue) zijn de twee eenvoudigste datastructuren die er bestaan, en tegelijk twee van de meest gebruikte. Elke keer dat je op Ctrl+Z drukt, gebruikt je editor een stapel. Elke keer dat je iets naar de printer stuurt, komt het in een wachtrij. En elke functieaanroep in je programma belandt op de aanroepstapel (call stack).
Op het tentamen krijg je meestal een reeks operaties en moet je de toestand erna opschrijven, of moet je uitleggen welke structuur bij een probleem past. In sollicitatiegesprekken is "controleer of de haakjes kloppen" een klassieker. Allebei komen hieronder aan bod.
Stapel: last in, first out
Een stapel werkt als een stapel borden: je legt een nieuw bord bovenop en je pakt ook altijd het bovenste weer weg. Wat het laatst erop kwam, gaat er het eerst af: LIFO (last in, first out).
De operaties:
push(x): legxbovenop de stapel.pop(): haal het bovenste element weg en geef het terug.peek()(oftop()): bekijk het bovenste element zonder het weg te halen.isEmpty(): is de stapel leeg?
We noteren de toestand van een stapel altijd van onder naar boven: het meest rechtse element is de bovenkant.
| Stap | Operatie | Teruggegeven | Stapel (onder → boven) |
|---|---|---|---|
| 1 | push(4) | – | 4 |
| 2 | push(7) | – | 4, 7 |
| 3 | push(1) | – | 4, 7, 1 |
| 4 | pop() | 1 | 4, 7 |
| 5 | push(9) | – | 4, 7, 9 |
| 6 | pop() | 9 | 4, 7 |
| 7 | pop() | 7 | 4 |
| 8 | push(5) | – | 4, 5 |
Wachtrij: first in, first out
Een wachtrij werkt als de rij bij de kassa: wie het eerst aansluit, wordt het eerst geholpen: FIFO (first in, first out).
enqueue(x): zetxachteraan in de rij.dequeue(): haal het voorste element weg en geef het terug.front(): bekijk het voorste element.
De toestand noteren we van voor naar achter: het meest linkse element is als eerste aan de beurt. Dezelfde reeks als hierboven, maar nu met een wachtrij:
| Stap | Operatie | Teruggegeven | Wachtrij (voor → achter) |
|---|---|---|---|
| 1 | enqueue(4) | – | 4 |
| 2 | enqueue(7) | – | 4, 7 |
| 3 | enqueue(1) | – | 4, 7, 1 |
| 4 | dequeue() | 4 | 7, 1 |
| 5 | enqueue(9) | – | 7, 1, 9 |
| 6 | dequeue() | 7 | 1, 9 |
| 7 | dequeue() | 1 | 9 |
| 8 | enqueue(5) | – | 9, 5 |
Zelfde invoer, totaal ander resultaat: de stapel eindigt met 4, 5, de wachtrij met 9, 5.
Een stapel met een array of lijst
Een stapel is eenvoudig te bouwen op een dynamische array (Python list, Java ArrayList). De
bovenkant is het einde van de lijst: toevoegen en weghalen aan het einde kost O(1) (bij een
dynamische array gemiddeld, omdat de array af en toe moet groeien).
class Stapel:
def __init__(self):
self.data = []
def push(self, x):
self.data.append(x) # bovenkant = einde van de lijst
def pop(self):
if not self.data:
raise IndexError("pop op lege stapel")
return self.data.pop()
def peek(self):
return self.data[-1]
def is_empty(self):
return len(self.data) == 0
Voor een wachtrij werkt een gewone lijst slecht: dequeue haalt het eerste element weg, en dan
moeten alle andere elementen een plek opschuiven. In Python is list.pop(0) daarom O(n). Gebruik
voor een wachtrij collections.deque (Python) of ArrayDeque (Java), of bouw er een met een
gelinkte lijst.
Gelinkte lijsten
Een gelinkte lijst (linked list) bestaat uit losse knopen (nodes). Elke knoop bevat een
waarde en een verwijzing naar de volgende knoop. In een enkel gelinkte lijst (singly linked
list) wijst elke knoop alleen naar zijn opvolger; de lijst zelf onthoudt alleen de eerste knoop, de
head.
head → [4 | •] → [7 | •] → [1 | •] → null
Het grote voordeel: vooraan invoegen of verwijderen kost O(1), want je verandert alleen een
paar verwijzingen. Niets hoeft op te schuiven.
- Vooraan invoegen: maak een nieuwe knoop, laat hem naar de oude
headwijzen, maak hem de nieuwehead. - Vooraan verwijderen: maak
head.nextde nieuwehead.
Het nadeel: je kunt niet direct naar index i springen. Om het i-de element te vinden, loop je
vanaf head de verwijzingen af: O(n). Daarom werkt binair zoeken niet
op een gelinkte lijst.
| Operatie | Array | Enkel gelinkte lijst |
|---|---|---|
element op index i | O(1) | O(n) |
| vooraan invoegen/verwijderen | O(n) | O(1) |
| achteraan invoegen | O(1) gemiddeld | O(1) met tail-verwijzing, anders O(n) |
| achteraan verwijderen | O(1) | O(n) (je moet de voorlaatste knoop vinden) |
Stapel en wachtrij met een gelinkte lijst
Een stapel is een gelinkte lijst waarbij je alleen vooraan werkt: push voegt vooraan in, pop
verwijdert vooraan. De head is de bovenkant van de stapel.
Een wachtrij houdt twee verwijzingen bij: head (voorkant) en tail (achterkant).
enqueue hangt een knoop achter tail, dequeue haalt head weg. Allebei O(1).
class Knoop:
def __init__(self, waarde, volgende=None):
self.waarde = waarde
self.volgende = volgende
class Wachtrij:
def __init__(self):
self.head = None # voorkant: hier gaat het eruit
self.tail = None # achterkant: hier komt het erbij
def enqueue(self, x):
knoop = Knoop(x)
if self.tail is None: # lege wachtrij
self.head = self.tail = knoop
else:
self.tail.volgende = knoop
self.tail = knoop
def dequeue(self):
if self.head is None:
raise IndexError("dequeue op lege wachtrij")
x = self.head.waarde
self.head = self.head.volgende
if self.head is None: # wachtrij is nu leeg
self.tail = None
return x
Let op de randgevallen: bij de eerste enqueue moeten head én tail naar de nieuwe knoop wijzen,
en als de laatste knoop eruit gaat moet tail weer null worden. Vergeet je dat, dan wijst tail
naar een knoop die niet meer in de lijst zit.
Toepassing: haakjes controleren
Klopt {[()()]}? En ([)]? Elk sluithaakje moet passen bij het laatst geopende haakje dat nog
open staat. "Laatst geopend, eerst gesloten": dat is LIFO, dus een stapel.
Het algoritme: openingshaakje → push. Sluithaakje → pop en controleer of het bijbehorende
openingshaakje eruit komt. Aan het eind moet de stapel leeg zijn.
| Teken | Actie | Stapel (onder → boven) |
|---|---|---|
{ | push | { |
[ | push | { [ |
( | push | { [ ( |
) | pop (, past | { [ |
( | push | { [ ( |
) | pop (, past | { [ |
] | pop [, past | { |
} | pop {, past | leeg |
De stapel is leeg aan het einde: {[()()]} klopt. Bij ([)] gaat het mis bij het derde teken:
) komt binnen, maar pop levert [ op. Fout. En bij (() blijft er aan het eind een ( op de
stapel staan: ook fout.
def haakjes_kloppen(s):
paren = {")": "(", "]": "[", "}": "{"}
stapel = []
for c in s:
if c in "([{":
stapel.append(c) # openingshaakje: push
elif c in paren:
# sluithaakje: de bovenkant moet het juiste openingshaakje zijn
if not stapel or stapel.pop() != paren[c]:
return False
return not stapel # alles moet gesloten zijn
Meer toepassingen
| Toepassing | Structuur | Waarom |
|---|---|---|
| Undo in een editor | stapel | de laatste wijziging maak je als eerste ongedaan |
| Functieaanroepen (call stack) | stapel | de laatst aangeroepen functie keert als eerste terug |
| DFS in een graaf | stapel (of recursie) | ga zo diep mogelijk, keer terug naar de laatste splitsing |
| Printerwachtrij | wachtrij | opdrachten in volgorde van binnenkomst |
| BFS in een graaf | wachtrij | knopen per laag verwerken, dichtstbijzijnde eerst |
| Verzoeken op een webserver | wachtrij | eerlijk: wie eerst komt, wordt eerst bediend |
Wil je in plaats van "eerst binnen" juist "kleinste eerst" verwerken, dan heb je een prioriteitswachtrij nodig. Die bouw je met een heap.
Complexiteit
| Operatie | Stapel (array) | Stapel (gelinkte lijst) | Wachtrij (deque/ArrayDeque) | Wachtrij (gelinkte lijst met tail) |
|---|---|---|---|---|
push / enqueue | O(1) gemiddeld | O(1) | O(1) gemiddeld | O(1) |
pop / dequeue | O(1) | O(1) | O(1) | O(1) |
peek / front | O(1) | O(1) | O(1) | O(1) |
| zoeken naar een waarde | O(n) | O(n) | O(n) | O(n) |
"Gemiddeld" (amortized) bij de array: meestal is er nog plek, maar af en toe moet de array
verdubbelen en wordt alles gekopieerd. Verdeeld over alle operaties is dat nog steeds O(1) per
operatie.
Veelgemaakte fouten op het tentamen
- De stapel in de verkeerde richting opschrijven. Wij noteren van onder naar boven: na
push(4), push(7)is het4, 7, niet7, 4. Het meest rechtse element gaat er als eerste af. - Bij de wachtrij aan de verkeerde kant weghalen.
dequeuehaalt het element weg dat het langst wacht, dus het meest linkse in de notatie voor → achter. - Een gewone lijst als wachtrij gebruiken en denken dat
pop(0)ofremove(0)O(1)is. Het isO(n), want alles schuift op. - Bij haakjes controleren vergeten te kijken of de stapel aan het eind leeg is. Dan keur je
(()ten onrechte goed. tailniet bijwerken in een gelinkte wachtrij als de laatste knoop verdwijnt, ofheadvergeten bij de eersteenqueue.
Samenvatting
- Stapel = LIFO:
pushenpopaan dezelfde kant; noteer van onder naar boven. - Wachtrij = FIFO:
enqueueachteraan,dequeuevooraan; noteer van voor naar achter. - Alle kernoperaties zijn
O(1), mits je de juiste implementatie kiest (geenpop(0)op een lijst). - Een enkel gelinkte lijst kan vooraan invoegen en verwijderen in
O(1), maar indexeren kostO(n). - Stapel: haakjes, undo, call stack, DFS. Wachtrij: printer, BFS, verzoeken verwerken.
