LIFO vs FIFO
LIFO adalah singkatan dari Last In, First Out. Seperti dalam, item terakhir yang dimasukkan ke dalam tumpukan adalah item pertama yang dikeluarkan dari tumpukan.
Apa yang Anda gambarkan dengan analogi hidangan Anda (dalam revisi pertama ), adalah antrian atau FIFO, First In, First Out.
Perbedaan utama antara keduanya, adalah bahwa LIFO / stack mendorong (menyisipkan) dan muncul (menghapus) dari ujung yang sama, dan FIFO / antrian melakukannya dari ujung yang berlawanan.
// Both:
Push(a)
-> [a]
Push(b)
-> [a, b]
Push(c)
-> [a, b, c]
// Stack // Queue
Pop() Pop()
-> [a, b] -> [b, c]
Penunjuk tumpukan
Mari kita lihat apa yang terjadi di bawah kap tumpukan. Berikut adalah beberapa memori, setiap kotak adalah alamat:
...[ ][ ][ ][ ]... char* sp;
^- Stack Pointer (SP)
Dan ada penunjuk tumpukan yang menunjuk ke bagian bawah tumpukan yang saat ini kosong (apakah tumpukan tumbuh atau tumbuh tidak terlalu relevan di sini sehingga kita akan mengabaikan itu, tetapi tentu saja di dunia nyata, yang menentukan operasi mana yang ditambahkan , dan yang mengurangi dari SP).
Jadi mari kita dorong a, b, and c
lagi. Grafik di sebelah kiri, operasi "level tinggi" di tengah, kode semu C-ish di kanan:
...[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
Seperti yang dapat Anda lihat, setiap kali kita push
, itu memasukkan argumen di lokasi penunjuk tumpukan saat ini menunjuk, dan menyesuaikan penunjuk tumpukan untuk menunjuk ke lokasi berikutnya.
Sekarang mari kita pop:
...[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
adalah kebalikan dari push
, itu menyesuaikan penunjuk tumpukan untuk menunjuk pada lokasi sebelumnya dan menghapus item yang ada di sana (biasanya untuk mengembalikannya kepada siapa pun yang dipanggil pop
).
Anda mungkin memperhatikan itu b
dan c
masih dalam memori. Saya hanya ingin meyakinkan Anda bahwa itu bukan kesalahan ketik. Kami akan segera kembali ke situ.
Hidup tanpa stack pointer
Mari kita lihat apa yang terjadi jika kita tidak memiliki stack pointer. Mulai dengan mendorong lagi:
...[ ][ ][ ][ ]...
...[ ][ ][ ][ ]... Push(a) ? = 'a';
Eh, hmm ... jika kita tidak memiliki stack pointer, maka kita tidak bisa memindahkan sesuatu ke alamat yang ditunjuknya. Mungkin kita bisa menggunakan pointer yang menunjuk ke pangkalan alih-alih bagian atas.
...[ ][ ][ ][ ]... 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
Uh oh. Karena kami tidak dapat mengubah nilai tetap dari basis tumpukan, kami hanya menimpa a
dengan mendorong b
ke lokasi yang sama.
Nah, mengapa kita tidak melacak berapa kali kita telah mendorong. Dan kita juga harus melacak waktu kita muncul.
...[ ][ ][ ][ ]... 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
Yah itu berfungsi, tetapi sebenarnya sangat mirip dengan sebelumnya, kecuali *pointer
lebih murah daripada pointer[offset]
(tidak ada aritmatika tambahan), belum lagi kurang mengetik. Ini seperti kehilangan bagi saya.
Mari coba lagi. Alih-alih menggunakan gaya string Pascal untuk menemukan akhir koleksi berbasis array (melacak berapa banyak item dalam koleksi), mari kita coba gaya string C (memindai dari awal hingga akhir):
...[ ][ ][ ][ ]... 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 '('
Anda mungkin sudah menebak masalahnya di sini. Memori yang tidak diinisialisasi tidak dijamin menjadi 0. Jadi ketika kita mencari bagian atas ke tempat a
, kita akhirnya melewatkan sekelompok lokasi memori yang tidak digunakan yang memiliki sampah acak di dalamnya. Demikian pula, ketika kita memindai ke atas, kita akan melompati jauh melampaui a
kita hanya mendorong sampai kita akhirnya menemukan lokasi memori lain yang kebetulan 0
, dan bergerak kembali dan mengembalikan sampah acak tepat sebelum itu.
Itu cukup mudah untuk diperbaiki, kita hanya perlu menambahkan operasi Push
dan Pop
untuk memastikan bagian atas tumpukan selalu diperbarui untuk ditandai dengan 0
, dan kita harus menginisialisasi tumpukan dengan terminator semacam itu. Tentu saja itu juga berarti kita tidak dapat memiliki 0
(atau nilai apa pun yang kita pilih sebagai terminator) sebagai nilai sebenarnya dalam tumpukan.
Selain itu, kami juga mengubah operasi O (1) menjadi operasi O (n).
TL; DR
Penunjuk tumpukan melacak bagian atas tumpukan, tempat semua tindakan terjadi. Ada beberapa cara untuk menghilangkannya ( bp[count]
dan top
pada dasarnya masih stack pointer), tetapi keduanya akhirnya menjadi lebih rumit dan lebih lambat daripada hanya memiliki stack pointer. Dan tidak tahu di mana bagian atas tumpukan berarti Anda tidak dapat menggunakan tumpukan.
Catatan: Penunjuk tumpukan menunjuk ke "bawah" dari tumpukan runtime di x86 mungkin kesalahpahaman terkait dengan seluruh tumpukan runtime terbalik. Dengan kata lain, dasar tumpukan ditempatkan pada alamat memori tinggi, dan ujung tumpukan tumbuh menjadi alamat memori yang lebih rendah. Stack pointer tidak menunjuk ke ujung tumpukan mana semua tindakan terjadi, hanya saja tip di alamat memori lebih rendah dari dasar tumpukan.