LIFO - FIFO
LIFO означає Last In, First Out. Як і в, Останній елемент, поміщений у стек, - це перший елемент, вийнятий із стека.
Те, що ви описали з аналогією страв (у першій редакції ), - це черга чи FIFO, First In, First Out.
Основна різниця між ними полягає в тому, що 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-ish праворуч:
...[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
щоб переконатися, що верхня частина стека завжди оновлюється, щоб бути позначеною символом a 0
, і ми повинні ініціалізувати стек таким термінатором. Звичайно, це також означає, що ми не можемо мати 0
(або будь-яке значення, яке ми обираємо як термінатор) як фактичне значення в стеку.
Крім того, ми також змінили, що були операціями O (1), на O (n).
TL; DR
Вказівник стека відстежує верхню частину стека, де відбувається вся дія. Є способи їх позбутися ( bp[count]
і top
по суті все ще є вказівником стека), але вони в кінцевому підсумку є складнішими і повільнішими, ніж просто указкою стека. І не знаючи, де знаходиться вершина стека, це означає, що ви не можете використовувати стек.
Примітка: Вказівник стека, що вказує на "дно" стеку виконання в x86, може бути помилковим уявленням, пов'язаним з тим, що весь стек виконання знаходиться внизу. Іншими словами, основа стека розміщується на високій адресі пам'яті, а кінчик стека переростає в нижчі адреси пам'яті. Покажчик стека робить точку на кінчик стека , де відбувається вся дія, тільки що наконечник знаходиться на більш низькому , ніж адреса пам'яті підстави стека.