LIFO vs FIFO
LIFOは、Last In、First Outの略です。同様に、スタックに入れられた最後のアイテムは、スタックから取り出された最初のアイテムです。
(最初のリビジョンで)料理の類推で説明したのは、キューまたはFIFO、先入れ先出しです。
2つの主な違いは、LIFO /スタックが同じ端からプッシュ(挿入)とポップ(削除)を行い、FIFO /キューが反対端からプッシュすることです。
// Both:
Push(a)
-> [a]
Push(b)
-> [a, b]
Push(c)
-> [a, b, c]
// Stack // Queue
Pop() Pop()
-> [a, b] -> [b, c]
スタックポインタ
スタックのフードの下で何が起こっているのか見てみましょう。ここにいくつかのメモリがあります、各ボックスはアドレスです:
...[ ][ ][ ][ ]... char* sp;
^- Stack Pointer (SP)
そして、現在空のスタックの一番下を指すスタックポインターがあります(スタックが大きくなっても小さくなっても、ここでは特に関係がないので無視しますが、実際には、どの操作が追加されるかを決定します、およびSPから差し引く)。
a, b, and c
もう一度プッシュしましょう。左側のグラフィック、中央の「高レベル」操作、右側のC風の擬似コード:
...[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
ご覧のとおり、を実行するたびにpush
、スタックポインターが現在指している場所に引数が挿入され、スタックポインターが次の場所を指すように調整されます。
今ポップしましょう:
...[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
push
はの逆で、スタックポインタを調整して前の場所をポイントし、そこにあったアイテムを削除します(通常はを呼び出した人に返しますpop
)。
あなたはおそらくそれに気づいていてb
、c
まだ記憶に残っています。私はそれらがタイプミスではないことをあなたに保証したいだけです。すぐに戻ります。
スタックポインターのない生活
スタックポインターがない場合はどうなるか見てみましょう。もう一度押すことから始めます:
...[ ][ ][ ][ ]...
...[ ][ ][ ][ ]... Push(a) ? = 'a';
えーと、うーん...スタックポインターがない場合、それが指しているアドレスに何かを移動することはできません。トップではなくベースを指すポインターを使用できるかもしれません。
...[ ][ ][ ][ ]... 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
ええとああ。スタックのベースの固定値を変更することはできないため、同じ場所にa
プッシュb
することで上書きしました。
ええと、何回プッシュしたかを追跡してみませんか。また、ポップした時間を追跡する必要もあります。
...[ ][ ][ ][ ]... 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
うまくいきますが、タイプするのが少ないことは言うまでもなく、(余分な計算なしで)*pointer
より安価であることを除いて、実際には以前とかなり似ていpointer[offset]
ます。これは私にとっては損失のようです。
もう一度やってみましょう。配列ベースのコレクションの終わりを見つけるPascal文字列スタイル(コレクション内のアイテム数の追跡)を使用する代わりに、C文字列スタイル(最初から最後までスキャン)を試してみましょう。
...[ ][ ][ ][ ]... 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 '('
あなたはすでにここで問題を推測したかもしれません。初期化されていないメモリは0であるとは限りません。そのため、配置する上位を探すと、a
ランダムなガベージが含まれている未使用のメモリの場所をスキップしてしまいます。同様に、一番上までスキャンすると、a
最終的にはたまたま別のメモリロケーションが見つかるまで、プッシュした0
だけでなく、前に戻ってランダムなガベージを返すまでスキップします。
それは修正に簡単に十分です、私達はちょうどに操作を追加する必要がありますPush
し、Pop
必ずスタックの最上位は、常にでマークするように更新されていることを確認するために0
、私たちは、このようなターミネータでスタックを初期化する必要があります。もちろん、これは0
スタックの実際の値として(またはターミネーターとして選択した任意の値)を持つことができないことも意味します。
さらに、O(1)オペレーションをO(n)オペレーションに変更しました。
TL; DR
スタックポインターは、すべてのアクションが発生するスタックの先頭を追跡します。そこそれを取り除くの一種の方法があります(bp[count]
とtop
、まだ基本的にスタックポインタです)が、それら両端より複雑にアップし、単にスタックポインタを持つよりも遅いです。スタックのトップがどこにあるかわからないということは、スタックを使用できないことを意味します。
注:x86のランタイムスタックの「ボトム」を指すスタックポインターは、ランタイムスタック全体が上下逆になっていることに関連する誤解である可能性があります。つまり、スタックのベースが高いメモリアドレスに配置され、スタックの先端が低いメモリアドレスに成長します。スタックポインタは、すべてのアクションが発生するスタックの先端を指します。その先端は、スタックのベースよりも低いメモリアドレスにあります。