LIFO vs FIFO
LIFO significa Last In, First Out. Assim, o último item colocado na pilha é o primeiro item retirado da pilha.
O que você descreveu com a analogia de sua louça (na primeira revisão ) é uma fila ou FIFO, primeiro a entrar, primeiro a sair.
A principal diferença entre os dois é que o LIFO / pilha empurra (insere) e aparece (remove) do mesmo lado, e uma fila FIFO / faz isso de extremos opostos.
// Both:
Push(a)
-> [a]
Push(b)
-> [a, b]
Push(c)
-> [a, b, c]
// Stack // Queue
Pop() Pop()
-> [a, b] -> [b, c]
O ponteiro da pilha
Vamos dar uma olhada no que está acontecendo sob o capô da pilha. Aqui está um pouco de memória, cada caixa é um endereço:
...[ ][ ][ ][ ]... char* sp;
^- Stack Pointer (SP)
E há um ponteiro de pilha apontando para a parte inferior da pilha atualmente vazia (se a pilha cresce ou diminui não é particularmente relevante aqui, então ignoraremos isso, mas é claro que no mundo real, isso determina qual operação adiciona e que subtrai do SP).
Então, vamos empurrar a, b, and c
novamente. Gráficos à esquerda, operação de "alto nível" no meio, pseudo-código C-ish à direita:
...[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
Como você pode ver, cada vez que push
inserimos o argumento no local que o ponteiro da pilha está apontando no momento e ajusta o ponteiro da pilha para apontar para o próximo local.
Agora vamos aparecer:
...[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
é o oposto de push
, ajusta o ponteiro da pilha para apontar para o local anterior e remove o item que estava lá (geralmente para devolvê-lo a quem chamou pop
).
Você provavelmente percebeu isso b
e c
ainda está na memória. Eu só quero garantir que esses não são erros de digitação. Voltaremos a isso em breve.
Vida sem ponteiro de pilha
Vamos ver o que acontece se não tivermos um ponteiro de pilha. Começando com o envio novamente:
...[ ][ ][ ][ ]...
...[ ][ ][ ][ ]... Push(a) ? = 'a';
Hum, hum ... se não tivermos um ponteiro de pilha, não podemos mover algo para o endereço que ele aponta. Talvez possamos usar um ponteiro que aponte para a base e não para o topo.
...[ ][ ][ ][ ]... 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 Como não podemos alterar o valor fixo da base da pilha, apenas o substituímos a
pressionando b
para o mesmo local.
Bem, por que não acompanhamos quantas vezes pressionamos. E também precisamos acompanhar os horários em que aparecemos.
...[ ][ ][ ][ ]... 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
Bem, funciona, mas na verdade é bastante semelhante a antes, exceto que *pointer
é mais barato do que pointer[offset]
(sem aritmética extra), sem mencionar que é menos digitado. Isso parece uma perda para mim.
Vamos tentar de novo. Em vez de usar o estilo de string Pascal para encontrar o final de uma coleção baseada em array (rastreando quantos itens há na coleção), vamos tentar o estilo de string C (varredura do começo ao fim):
...[ ][ ][ ][ ]... 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 '('
Você já deve ter adivinhado o problema aqui. Não é garantido que a memória não inicializada seja 0. Portanto, quando procuramos o topo para colocar a
, acabamos pulando um monte de locais de memória não utilizados que contêm lixo aleatório. Da mesma forma, quando digitalizamos para o topo, acabamos pulando muito além do a
que acabamos de empurrar até finalmente encontrarmos outro local de memória 0
, e voltar e devolver o lixo aleatório antes disso.
Isso é fácil de corrigir, basta adicionar operações Push
e Pop
garantir que o topo da pilha seja sempre atualizado para ser marcado com a 0
, e precisamos inicializar a pilha com esse terminador. Claro que isso também significa que não podemos ter um 0
(ou qualquer valor que escolhemos como terminador) como um valor realmente na pilha.
Além disso, também alteramos o que eram operações O (1) para operações O (n).
TL; DR
O ponteiro da pilha controla o topo da pilha, onde ocorre toda a ação. Existem maneiras de se livrar dele ( bp[count]
e top
ainda são essencialmente o ponteiro da pilha), mas ambas acabam sendo mais complicadas e lentas do que simplesmente ter o ponteiro da pilha. E não saber onde está o topo da pilha significa que você não pode usá-la.
Nota: O ponteiro da pilha que aponta para a "parte inferior" da pilha de tempo de execução no x86 pode ser um equívoco relacionado a toda a pilha de tempo de execução de cabeça para baixo. Em outras palavras, a base da pilha é colocada em um endereço de memória alto e a ponta da pilha cresce em endereços de memória inferiores. O ponteiro da pilha faz ponto para a ponta da pilha onde toda a ação ocorre, assim que a ponta está em um endereço de memória menor do que a base da pilha.