Ein Stack<T> ist ein Stapel: Du legst Elemente mit Push oben ab und nimmst sie mit Pop oben wieder weg, das zuletzt hineingelegte Element kommt also zuerst heraus (LIFO). Nur das oberste Element ist erreichbar, und jede Operation darauf braucht konstante Zeit.
Push, Pop und Peek
Ausgabe:
On top: green
Count: 3
Took green
On top: red
Took red
Took blue
Count: 0
green wurde zuletzt abgelegt und kommt daher zuerst heraus. Peek gibt das oberste Element zurück, ohne den Stack zu ändern. So siehst du nach, was Pop liefern würde, bevor du dich entscheidest, es zu nehmen.
Die Exception bei leerem Stack und TryPop
Pop oder Peek auf einem leeren Stack wirft InvalidOperationException. Das passiert am häufigsten in Parsern und Algorithmen, deren Eingabe mehr schließende als öffnende Elemente hat.
Ausgabe:
Caught InvalidOperationException
True 10
False 0
TryPop und TryPeek (ab .NET Core 2.0) geben bei leerem Stack false zurück und setzen die out-Variable auf den Standardwert, hier 0. Unter .NET Framework prüfe vorher Count > 0.
Reihenfolge beim Durchlaufen: oben zuerst
Einen Stack zu durchlaufen entfernt nichts, und es geht von oben nach unten, in der Reihenfolge, in der Pop die Elemente zurückgeben würde:
Ausgabe:
checkout products home
checkout > products > home
True
home
checkout
Die umgekehrte Kopie erwischt Leute: Der Konstruktor nimmt ein beliebiges IEnumerable<T> und legt seine Elemente der Reihe nach ab, und ein Stack wird oben zuerst durchlaufen, das alte oberste Element landet in der Kopie also unten. Die Sequenz vorher umzukehren (Reverse() aus LINQ liefert die Elemente von unten zuerst) ergibt eine Kopie mit demselben obersten Element.
Eine Liste von Elementen auf einen neuen Stack zu legen kehrt sie ebenfalls um, was ein schneller Weg ist, eine Sequenz umzukehren: new Stack<char>("hello") gibt per Pop o, l, l, e, h zurück.
Beispiel: eine Undo-Historie
Editoren legen jede Änderung auf einen Stack. Undo nimmt die letzte Änderung per Pop herunter und macht sie rückgängig; Redo führt einen zweiten Stack mit rückgängig gemachten Änderungen.
Ausgabe:
Hello, world!
Hello, world
Hello
Hello, world
Ganze Momentaufnahmen zu speichern ist die einfachste Version. Echte Editoren legen stattdessen kleine Befehlsobjekte ab (was wo eingefügt wurde), jedes mit einer Methode, sich selbst rückgängig zu machen, aber die beiden Stacks funktionieren genauso.
Beispiel: ausgeglichene Klammern
Zu prüfen, dass (, [ und { in der richtigen Reihenfolge geschlossen werden, ist die Standardübung für Stacks, und dieselbe Logik steckt in jedem Compiler und JSON-Parser.
Ausgabe:
"f(a[i], {x: 1})" -> True
"(]" -> False
"((a)" -> False
"a)b(" -> False
"" -> True
Die drei Fehlerprüfungen entsprechen den drei Arten, wie Klammern falsch sein können: eine schließende Klammer ohne offene (a)b(, erkannt durch Count == 0 statt durch eine Exception von Pop), eine schließende Klammer der falschen Art ((]) und öffnende Klammern, die nie geschlossen werden (((a), erkannt durch die abschließende Prüfung).
Weitere Einsätze
- Tiefensuche. Ersetze die Queue einer Breitensuche durch einen Stack, und die Durchquerung geht erst in die Tiefe, dann in die Breite. Ein expliziter Stack ersetzt auch Rekursion, wenn die Eingabe tief genug ist, um eine
StackOverflowExceptionzu riskieren, die sich nicht abfangen lässt. - Ausdrücke auswerten. Postfix-Notation (
3 4 + 2 *) wird ausgewertet, indem Zahlen abgelegt und für jeden Operator zwei per Pop genommen werden. - Backtracking. Navigationsverlauf, Labyrinthlösung und Parserzustände legen eine Position ab und kehren in einer Sackgasse per Pop zu ihr zurück.
Das Gegenstück nach dem Prinzip first in, first out findest du unter Queue.
Stack gegenüber Queue gegenüber List
Stack<T> | Queue<T> | List<T> | |
|---|---|---|---|
| Reihenfolge beim Herausnehmen | Neuestes zuerst | Ältestes zuerst | Beliebig, per Index |
| Hinzufügen | Push | Enqueue | Add, Insert |
| Entfernen | Pop (oben) | Dequeue (vorn) | Remove, RemoveAt |
| Ansehen | Peek | Peek | list[i] |
| Sichere Varianten | TryPop, TryPeek | TryDequeue, TryPeek | nicht nötig |
Für mehrere Threads bietet ConcurrentStack<T> in System.Collections.Concurrent die Methoden Push, TryPop und TryPeek ohne Locks.
Häufige Fehler
- Pop ohne Prüfung. Ein leerer Stack wirft
InvalidOperationException; prüfeCountoder nimmTryPop. - Erwarten, dass
foreachbeim zuerst abgelegten Element beginnt. Es beginnt oben. - Mit
new Stack<T>(stack)kopieren. Die Kopie ist umgekehrt. - In
foreachüber denselben Stack ablegen. Wirft; nimm eine Schleifewhile (stack.Count > 0).
Häufig gestellte Fragen
Was ist ein Stack in C#?
Stack<T> in System.Collections.Generic ist eine Collection nach dem Prinzip last in, first out (LIFO). Push legt ein Element oben ab, Pop entfernt das oberste Element und gibt es zurück, und Peek gibt das oberste Element zurück, ohne es zu entfernen. Alle drei laufen in konstanter Zeit.
Was passiert in C# bei Pop auf einem leeren Stack?
Pop und Peek werfen InvalidOperationException, wenn der Stack leer ist. Prüfe vorher stack.Count > 0 oder nimm TryPop(out var item) und TryPeek(out var item), die stattdessen false zurückgeben (ab .NET Core 2.0).
In welcher Reihenfolge durchläuft foreach einen Stack?
Von oben nach unten: Das zuletzt abgelegte Element kommt zuerst, in derselben Reihenfolge, in der Pop sie zurückgeben würde. ToArray() verwendet dieselbe Reihenfolge. Eine Folge davon: new Stack<T>(otherStack) erzeugt eine umgekehrte Kopie, weil der Konstruktor die Elemente in der Reihenfolge ablegt, in der er sie durchläuft.
Was ist der Unterschied zwischen einem Stack und einer Queue in C#?
Ein Stack<T> gibt das neueste Element zuerst zurück (last in, first out), eine Queue<T> das älteste (first in, first out). Nimm einen Stack für Undo-Historien, verschachtelte Strukturen und Tiefensuche; nimm eine Queue, um Arbeit in der Reihenfolge des Eintreffens abzuarbeiten, und für die Breitensuche.
Wie prüfe ich in C# ausgeglichene Klammern?
Durchlaufe den String einmal. Lege jede öffnende Klammer auf einen Stack<char>. Bei jeder schließenden Klammer muss der Stack nicht leer sein, und sein oberstes Element muss die passende öffnende Klammer sein, die du dann mit Pop entfernst. Der String ist ausgeglichen, wenn der Durchlauf mit leerem Stack endet.