Stack, Queue und verkettete Liste
Kurz gesagt: Ein Stack gibt das zuletzt hinzugefügte Element zuerst zurück (LIFO), eine Queue das zuerst hinzugefügte (FIFO), und eine verkettete Liste hält jedes Element in einem Knoten, der auf den nächsten zeigt.
Problem
„Rückgängig“ macht das Falsche rückgängig
Ein Texteditor hält die Aktionen in einer Array. Strg+Z soll die letzte rückgängig machen, macht aber die erste rückgängig:
const actions = [];actions.push("Text getippt");actions.push("Titel fett gemacht");actions.push("Foto eingefügt");
// Strg+Z: die letzte Aktion rückgängig machen.const undone = actions.shift();console.log(`Rückgängig: ${undone}`);console.log("Übrig:", actions);Rückgängig: Text getipptÜbrig: [ 'Titel fett gemacht', 'Foto eingefügt' ]Die Korrektur ist actions.pop(). Aber jeder, der diesen Code anfasst, muss sich merken, an welchem Ende er entfernt. Und an der Kasse im Laden ist es umgekehrt: Wer zuerst kam, wird zuerst bedient. Gibt es eine Collection, die das Ende selbst kennt?
Die Idee
Stack: der Letzte liegt oben
Ein Stack ist wie ein Stapel Teller. Du legst oben drauf und nimmst von oben. Was zuletzt hineinkam, kommt zuerst heraus. Das heißt LIFO: last in, first out.
Der Button „Zurück“ im Browser ist ein Stack:
const history = [];history.push("Startseite");history.push("Laptops");history.push("Laptop 14 Zoll");console.log(`Jetzt: ${history.at(-1)}`);
history.pop();console.log(`Zurück: ${history.at(-1)}`);console.log(history);Jetzt: Laptop 14 ZollZurück: Laptops[ 'Startseite', 'Laptops' ]JavaScript hat keine eigene Klasse für einen Stack: Ein Array ist ein Stack, wenn du nur am Ende hineinlegst und herausnimmst.
pushlegt oben drauf,pop()entfernt das oberste und gibt es zurück, undat(-1)schaut das oberste nur an.- Oben ist das Ende des Arrays. Beide Operationen sind schnell, weil sich am Ende nichts verschiebt.
console.loggibt den Stack von unten nach oben aus: Das oberste steht zuletzt.
Genauer, für Neugierige
pop()undat(-1)auf einem leeren Array brechen das Programm nicht ab. Sie gebenundefined:
const history = [];console.log(history.pop());console.log(history.at(-1));undefinedundefinedPrüf also history.length, bevor du etwas nimmst.
Probier es aus
Was bleibt rückgängig zu machen?
Was gibt das Programm aus?
const undo = [];undo.push("Schriftart");undo.push("Farbe");undo.push("Größe");undo.pop();undo.push("Unterstreichung");while (undo.length > 0) { console.log(undo.pop());}pop entfernt „Größe“, weil es oben liegt. Dann kommt „Unterstreichung“ nach oben. Die Schleife nimmt sie von oben nach unten heraus: Unterstreichung, Farbe, Schriftart.
Die Idee
Queue: Wer zuerst kommt, wird zuerst bedient
Eine Queue (Warteschlange) ist wie die Schlange an der Kasse. Du stellst dich hinten an, und der Erste wird bedient. Das heißt FIFO: first in, first out.
Druckaufträge warten in einer Queue:
const printer = [];printer.push("rechnung.pdf");printer.push("vertrag.docx");printer.push("foto.jpg");console.log(`Nächster: ${printer[0]}`);
while (printer.length > 0) { console.log(`Drucke ${printer.shift()}`);}console.log("Warteschlange:", printer);Nächster: rechnung.pdfDrucke rechnung.pdfDrucke vertrag.docxDrucke foto.jpgWarteschlange: []JavaScript hat keine eigene Klasse für eine Queue. Ein Array wird zur Queue, wenn du hinten anstellst und vorne herausnimmst:
pushstellt hinten an,shiftentfernt den ersten und gibt ihn zurück, undprinter[0]schaut den ersten an.shiftauf einem leeren Array gibtundefined. Es bricht das Programm nicht ab.
Aber shift hat seinen Preis. Es rückt alle anderen einen Platz nach vorne, wie das Einfügen vorne in der Lektion über Arrays. Bei ein paar Druckaufträgen merkst du das nicht. Bei einer Queue mit Zehntausenden Elementen ist es um ein Vielfaches langsamer als einfaches Lesen der Reihe nach. Dann nimmst du die verkettete Liste aus der nächsten Karte, die sowohl den ersten als auch den letzten Knoten hält.
Genauer, für Neugierige
shiftauf einer leeren Queue:
const clients = ["Kunde 1"];console.log(`Bedient: ${clients.shift()}`);console.log(clients.shift());Bedient: Kunde 1undefined- Die Engine V8 macht
shiftmanchmal schnell, indem sie den Anfang des Arrays verschiebt statt der Elemente. Bei großen Arrays passiert das nicht. Verlass dich nicht darauf.
Probier es aus
Der Nächste an der Kasse
Die Kassiererin ruft den nächsten Kunden. Welche Methode nimmt ihn aus der Queue?
const checkout = [];
checkout.push("Jonas");
checkout.push("Maria");
const client = checkout.();
console.log(`Du bist dran, ${client}`);shift entfernt den Ersten in der Queue und gibt ihn zurück, hier Jonas. checkout[0] würde nur schauen, ohne ihn zu entfernen, und beim nächsten Mal wäre es wieder Jonas. Und pop() würde die Letzte nehmen, Maria.
Sieh es dir an
Eine verkettete Liste von innen
Ein Array hält die Elemente nebeneinander in einem Array. Darum verschiebt das Einfügen vorne alle anderen. Es geht auch anders: Jedes Element steckt in einem eigenen Objekt, das den Wert und einen Pfeil zum nächsten hält. So ein Objekt heißt Knoten:
class Node { constructor(value, next) { this.value = value; this.next = next; }}Eine Liste aus Knoten heißt verkettete Liste. Es reicht, den ersten Knoten zu halten, head. Jeden nächsten findest du, indem du dem Pfeil next folgst. Der letzte zeigt auf null.
let head = null;head = new Node("Kaffee", head);head = new Node("Milch", head);head = new Node("Brot", head);
for (let n = head; n !== null; n = n.next) { console.log(n.value);}Schritt für Schritt, in Worten
- Die Liste ist leer: head zeigt auf null.
- Ein neuer Knoten: next ist der alte head (null), und head rückt auf den Knoten.
- Der zweite Knoten zeigt auf den ersten. Im Speicher bewegt sich nichts: Nur die Pfeile ändern sich.
- Auch der dritte Knoten kommt nach vorne: Der zuletzt hinzugefügte ist der erste.
- n beginnt bei head und gibt jeden Knoten aus.
- n erreicht null: Die Schleife endet.
- Einfügen vorne: ein neuer Knoten, dessen
nextauf den alten ersten zeigt, undheadzeigt auf den neuen. Zwei Pfeile, und du verschiebst nichts. - Durchlaufen: Du beginnst bei
headund folgstnext, bis du beinullankommst. - Der Preis: Knoten haben keine Indizes. Zum fünften Element kommst du, indem du durch die ersten vier gehst.
Die Idee
Das Array, und was wann
JavaScript hat keine fertige verkettete Liste. Ein Array kann an beiden Enden hinzufügen und entfernen: push und pop hinten, unshift und shift vorne:
const route = [];route.push("Nürnberg");route.push("Leipzig");route.unshift("München");route.push("Berlin");console.log(route);
route.splice(route.indexOf("Leipzig"), 1);console.log("Nach der Änderung:", route);console.log(`Von ${route[0]} nach ${route.at(-1)}`);[ 'München', 'Nürnberg', 'Leipzig', 'Berlin' ]Nach der Änderung: [ 'München', 'Nürnberg', 'Berlin' ]Von München nach BerlinWas wann:
- Eine Liste nach Nummer: ein Array. Das ist die übliche Wahl.
- Ein Stack: ein Array mit
pushundpop. - Eine Queue: ein Array mit
pushundshift, solange sie klein ist. Eine große Queue mit Tausenden Elementen: eine verkettete Liste wie die aus der vorigen Karte, die auch den letzten Knoten hält.
Genauer, für Neugierige
unshiftist wieshift: Es verschiebt alle Elemente um einen Platz, ist also vorne an einem großen Array langsam.- Warum ist ein Array meist schneller als eine verkettete Liste, sogar beim Einfügen? Jeder Knoten ist ein eigenes Objekt irgendwo im Speicher, und der Prozessor liest Daten am schnellsten, wenn sie nebeneinander liegen. Ein Array ist genau das.
- Der Knoten aus dieser Karte hat nur
next. So eine Liste heißt einfach verkettet.
Probier es aus
Finde den Fehler
Die Funktion soll die Knoten zählen. Für die Liste Brot → Milch → Kaffee gibt sie 2 statt 3 zurück. Und bei einer leeren Liste bricht sie mit TypeError ab. In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
n.next !== null hört auf, wenn n auf dem letzten Knoten steht, bevor er gezählt ist. Und ist head null, gibt es nichts, wovon man n.next lesen könnte. Die Bedingung sollte n !== null sein: weiter, solange es einen Knoten gibt.
Die Meldung bei einer leeren Liste ist TypeError: Cannot read properties of null (reading 'next'): null hat kein next.
Übung
Stacks und Queues im Einsatz
# vorne.1. Ein Spielzeugzug. Eine Klasse Train mit den Methoden addWagon(wagon) und removeWagon(). Der Zug ist ein Stack: Der zuletzt angekoppelte Wagen wird zuerst abgekoppelt. removeWagon() gibt den abgekoppelten Wagen zurück, oder null, wenn keiner da ist. Füge count() hinzu. Kopple drei Wagen an und zwei ab.
Abgekoppelt: Wagen 3Abgekoppelt: Wagen 2Wagen: 12. Eine Buslinie. Eine Klasse BusRoute mit einer Queue von Haltestellen und den Methoden addStop(stop) und getNextStop(), die die nächste Haltestelle zurückgibt, oder null, wenn die Strecke zu Ende ist. Füge „Hauptbahnhof“, „Rathaus“ und „Universität“ hinzu und fahr die Strecke ab.
Nächster Halt: HauptbahnhofNächster Halt: RathausNächster Halt: UniversitätEnde der Strecke.3. Ein Börsenticker. Eine Klasse StockTicker hält die letzten N Preise einer Aktie. Der Konstruktor bekommt das Symbol und N. updatePrice(price) fügt einen Preis hinzu, und wenn es mehr als N Preise sind, entfernt es den ältesten. Füge getLastPrice() und average() für die gespeicherten Preise hinzu. Für SAP mit N = 3 übergib die Preise 150, 152, 149 und 155. Tipp: at(-1) ist der letzte Preis. Gib die Preise mit zwei Nachkommastellen aus, mit toFixed(2).
SAP: letzter Preis 155.00 €Durchschnitt der letzten 3: 152.00 €4. Ausgeglichene Klammern. Ein Code-Editor prüft, ob die Klammern (), [] und {} in der richtigen Reihenfolge geschlossen sind. Schreib isBalanced(text) mit einem Stack: Eine öffnende Klammer legst du auf den Stack, bei einer schließenden nimmst du die oberste herunter und prüfst, dass sie von derselben Art ist. Am Ende muss der Stack leer sein. Lies drei Zeilen ein.
(1 + [2 * 3])(1 + 2]((5)(1 + [2 * 3]) -> ja(1 + 2] -> nein((5) -> nein5. Deine eigene verkettete Liste. Eine Klasse MyLinkedList mit den Knoten aus der Lektion und den Methoden add(value) (am Ende), remove(value), contains(value), size() und toString(). In remove findest du den Knoten vor dem gesuchten und lenkst sein next um. Prüf den Fall, dass der erste entfernt wird, gesondert.
[Rechnung, Bericht, Besprechung]Enthält Bericht: trueNach dem Entfernen: [Rechnung, Besprechung]Größe: 2Weitere Aufgaben
- Rückgängig und Wiederholen. Halte zwei Stacks:
undoundredo. Eine neue Aktion kommt inundound leertredo. „Rückgängig“ verschiebt die oberste vonundonachredo, und „Wiederholen“ verschiebt sie zurück. - Wörter rückwärts. Lies einen Satz ein und gib ihn mit den Wörtern in umgekehrter Reihenfolge aus, über einen Stack: „Heute ist Freitag“ wird zu „Freitag ist Heute“.
- Zwei Kassen. Kunden kommen nacheinander und gehen zu der Kasse mit der kürzeren Schlange. Gib aus, wer an welcher Kasse bedient wurde.
- Die verkettete Liste umdrehen. Füge
MyLinkedListeine Methodereverse()hinzu, die die Pfeile umdreht, ohne neue Knoten anzulegen.
- Eine schnelle Queue. Füge
MyLinkedListein Feld für den letzten Knoten hinzu, damit auchaddam Ende schnell ist. Jetzt ist sie eine Queue ohneshift:addhinten, den ersten vorne entfernen.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Polymorphie · Interfaces ohne interface · Exceptions · Arrays: push, splice und sort
- Geht weiter in: Hashcodes und Hashfunktionen · Klassen und Daten in Klassen · Command · Speicherkomplexität
- Aufgaben zum Thema: Collections