ЛИФО против ФИФО
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)
И есть указатель стека, указывающий на нижнюю часть в настоящее время пустого стека (растёт или растёт стек, здесь не особо важно, поэтому мы проигнорируем это, но, конечно, в реальном мире, это определяет, какая операция добавляет и который вычитает из СП).
Итак, давайте подтолкнем 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
убедиться, что вершина стека всегда обновляется, чтобы быть помеченной знаком 0
, и мы должны инициализировать стек с таким терминатором. Конечно, это также означает, что мы не можем иметь 0
(или любое другое значение, которое мы выбираем в качестве терминатора) в качестве фактического значения в стеке.
Кроме того, мы также изменили операции O (1) на операции O (n).
TL; DR
Указатель стека отслеживает вершину стека, где происходит все действие. Есть способы как-то избавиться от этого ( bp[count]
и top
по сути все еще остаются указателем стека), но оба они оказываются более сложными и медленными, чем просто наличие указателя стека. А незнание того, где находится вершина стека, означает, что вы не можете использовать этот стек.
Примечание. Указатель стека, указывающий на «нижнюю часть» стека времени выполнения в x86, может быть ошибочным, поскольку весь стек времени выполнения переворачивается вверх ногами. Другими словами, основание стека размещается по высокому адресу памяти, а верхушка стека переходит в более низкие адреса памяти. Указатель стека делает точку на кончик стека , где происходит все действие, только что наконечник находится на более низком , чем адрес памяти основания стека.