LIFO gegen FIFO
LIFO steht für Last In, First Out. Wie in ist der letzte Gegenstand, der in den Stapel gelegt wird, der erste Gegenstand, der aus dem Stapel genommen wird.
Was Sie mit Ihrer Geschirranalogie (in der ersten Überarbeitung ) beschrieben haben, ist eine Warteschlange oder ein FIFO, First In, First Out.
Der Hauptunterschied zwischen den beiden besteht darin, dass der LIFO / Stapel am selben Ende drückt (Einfügungen) und platzt (entfernt) und ein FIFO / eine Warteschlange dies an entgegengesetzten Enden tut.
// Both:
Push(a)
-> [a]
Push(b)
-> [a, b]
Push(c)
-> [a, b, c]
// Stack // Queue
Pop() Pop()
-> [a, b] -> [b, c]
Der Stapelzeiger
Werfen wir einen Blick darauf, was unter der Haube des Stapels passiert. Hier ist etwas Speicher, jede Box ist eine Adresse:
...[ ][ ][ ][ ]... char* sp;
^- Stack Pointer (SP)
Und es gibt einen Stapelzeiger, der auf den unteren Rand des aktuell leeren Stapels zeigt (ob der Stapel wächst oder wächst, ist hier nicht besonders relevant, daher werden wir das ignorieren, aber in der realen Welt bestimmt dies natürlich, welche Operation hinzugefügt wird und die vom SP subtrahiert).
Also lasst uns noch einmal pushen a, b, and c
. Grafik links, "High Level" -Operation in der Mitte, C-ish Pseudocode rechts:
...[a][ ][ ][ ]... Push('a') *sp = 'a';
^- SP
...[a][ ][ ][ ]... ++sp;
^- SP
...[a][b][ ][ ]... Push('b') *sp = 'b';
^- SP
...[a][b][ ][ ]... ++sp;
^- SP
...[a][b][c][ ]... Push('c') *sp = 'c';
^- SP
...[a][b][c][ ]... ++sp;
^- SP
Wie Sie sehen können, wird jedes Mal push
das Argument an der Stelle eingefügt, auf die der Stapelzeiger gerade zeigt, und der Stapelzeiger wird so angepasst, dass er auf die nächste Stelle zeigt.
Jetzt lass uns knallen:
...[a][b][c][ ]... Pop() --sp;
^- SP
...[a][b][c][ ]... return *sp; // returns 'c'
^- SP
...[a][b][c][ ]... Pop() --sp;
^- SP
...[a][b][c][ ]... return *sp; // returns 'b'
^- SP
Pop
ist das Gegenteil von push
, es passt den Stapelzeiger so an, dass er auf die vorherige Position zeigt, und entfernt das Element, das dort war (normalerweise, um es an den Anrufer zurückzugeben pop
).
Sie haben das wahrscheinlich bemerkt b
und c
sind immer noch in Erinnerung. Ich möchte Ihnen nur versichern, dass dies keine Tippfehler sind. Wir werden in Kürze darauf zurückkommen.
Leben ohne Stapelzeiger
Mal sehen, was passiert, wenn wir keinen Stapelzeiger haben. Beginnen Sie erneut mit dem Schieben:
...[ ][ ][ ][ ]...
...[ ][ ][ ][ ]... Push(a) ? = 'a';
Ähm, hmm ... wenn wir keinen Stapelzeiger haben, können wir nichts an die Adresse verschieben, auf die es zeigt. Vielleicht können wir einen Zeiger verwenden, der auf die Basis anstatt auf die Oberseite zeigt.
...[ ][ ][ ][ ]... char* bp; // "base pointer"
^- bp bp = malloc(...);
...[a][ ][ ][ ]... Push(a) *bp = 'a';
^- bp
// No stack pointer, so no need to update it.
...[b][ ][ ][ ]... Push(b) *bp = 'b';
^- bp
Oh oh. Da wir den festen Wert der Stapelbasis nicht ändern können, haben wir den Wert einfach überschrieben, a
indem wir ihn b
an dieselbe Stelle verschoben haben .
Warum verfolgen wir nicht, wie oft wir gepusht haben? Und wir müssen auch die Zeiten verfolgen, in denen wir aufgetaucht sind.
...[ ][ ][ ][ ]... char* bp; // "base pointer"
^- bp bp = malloc(...);
int count = 0;
...[a][ ][ ][ ]... Push(a) bp[count] = 'a';
^- bp
...[a][ ][ ][ ]... ++count;
^- bp
...[a][b][ ][ ]... Push(a) bp[count] = 'b';
^- bp
...[a][b][ ][ ]... ++count;
^- bp
...[a][b][ ][ ]... Pop() --count;
^- bp
...[a][b][ ][ ]... return bp[count]; //returns b
^- bp
Nun, es funktioniert, aber es ist eigentlich ziemlich ähnlich wie zuvor, außer dass *pointer
es billiger ist als pointer[offset]
(keine zusätzliche Arithmetik), ganz zu schweigen davon, dass es weniger zu tippen ist. Das scheint mir ein Verlust zu sein.
Lass es uns erneut versuchen. Anstatt den Pascal-Zeichenfolgenstil zu verwenden, um das Ende einer Array-basierten Sammlung zu finden (Verfolgen der Anzahl der Elemente in der Sammlung), versuchen wir den C-Zeichenfolgenstil (Scannen vom Anfang bis zum Ende):
...[ ][ ][ ][ ]... char* bp; // "base pointer"
^- bp bp = malloc(...);
...[ ][ ][ ][ ]... Push(a) char* top = bp;
^- bp, top
while(*top != 0) { ++top; }
...[ ][ ][ ][a]... *top = 'a';
^- bp ^- top
...[ ][ ][ ][ ]... Pop() char* top = bp;
^- bp, top
while(*top != 0) { ++top; }
...[ ][ ][ ][a]... --top;
^- bp ^- top return *top; // returns '('
Möglicherweise haben Sie das Problem hier bereits erraten. Es ist nicht garantiert, dass der nicht initialisierte Speicher 0 ist. Wenn wir also nach der obersten Position suchen a
, überspringen wir eine Reihe nicht verwendeter Speicherorte, in denen sich zufälliger Müll befindet. Wenn wir nach oben scannen, überspringen wir in ähnlicher Weise weit über das hinaus, was a
wir gerade gedrückt haben, bis wir endlich einen anderen Speicherort finden, der sich gerade befindet 0
, und gehen zurück und geben den zufälligen Müll kurz davor zurück.
Das ist einfach zu beheben. Wir müssen nur Operationen hinzufügen Push
und Pop
sicherstellen, dass die Oberseite des Stapels immer aktualisiert wird, um mit einem gekennzeichnet zu werden 0
, und wir müssen den Stapel mit einem solchen Abschlusszeichen initialisieren. Das bedeutet natürlich auch, dass wir keinen 0
(oder einen beliebigen Wert, den wir als Terminator auswählen) als tatsächlichen Wert im Stapel haben können.
Darüber hinaus haben wir O (1) -Operationen in O (n) -Operationen geändert.
TL; DR
Der Stapelzeiger verfolgt den oberen Rand des Stapels, in dem die gesamte Aktion ausgeführt wird. Es gibt Möglichkeiten, es loszuwerden ( bp[count]
und top
sind im Wesentlichen immer noch der Stapelzeiger), aber beide sind komplizierter und langsamer als nur der Stapelzeiger. Und wenn Sie nicht wissen, wo sich die Oberseite des Stapels befindet, können Sie den Stapel nicht verwenden.
Hinweis: Der Stapelzeiger, der in x86 auf den "unteren Rand" des Laufzeitstapels zeigt, kann ein Missverständnis sein, das damit zusammenhängt, dass der gesamte Laufzeitstapel auf dem Kopf steht. Mit anderen Worten, die Basis des Stapels befindet sich an einer hohen Speicheradresse, und die Spitze des Stapels wächst zu niedrigeren Speicheradressen herab. Der Stapelzeiger zeigt auf die Spitze des Stapels, an der die gesamte Aktion ausgeführt wird. Nur diese Spitze befindet sich an einer niedrigeren Speicheradresse als die Basis des Stapels.