Na aula de hoje, número 165 da série, exploramos duas estruturas de dados lineares fundamentais: pilhas e filas. Essas estruturas são a base para muitos algoritmos e sistemas, desde o gerenciamento de chamadas de funções até o escalonamento de processos. Vamos detalhar seus conceitos, operações e aplicações.

Pilhas (Stacks)

Uma pilha (stack) é uma estrutura que segue o princípio LIFO (Last In, First Out). O último elemento inserido é o primeiro a ser removido. Imagine uma pilha de livros: para pegar o livro do meio, é necessário retirar os de cima primeiro.

Operações Básicas

  • Push: insere um elemento no topo da pilha.
  • Pop: remove o elemento do topo.
  • Top (ou Peek): consulta o elemento do topo sem removê-lo.
  • isEmpty: verifica se a pilha está vazia.

Aplicações

  • Mecanismo "desfazer" (undo) em editores de texto.
  • Navegação entre páginas web (botão voltar).
  • Avaliação de expressões matemáticas (notação polonesa reversa).
  • Pilha de chamadas de funções (call stack) em linguagens de programação.

Um exemplo clássico do uso de pilhas é a verificação de parênteses balanceados em expressões matemáticas e código fonte. Ao percorrer a expressão, cada parêntese aberto é empilhado; ao encontrar um parêntese fechado, o topo da pilha é desempilhado e comparado. Se houver correspondência, continua; caso contrário, a expressão está mal formada. Ao final, se a pilha estiver vazia, a expressão é válida.

Implementação Simples em Python

class Pilha:
    def __init__(self):
        self.itens = []
    def push(self, item):
        self.itens.append(item)
    def pop(self):
        if not self.esta_vazia():
            return self.itens.pop()
    def top(self):
        if not self.esta_vazia():
            return self.itens[-1]
    def esta_vazia(self):
        return len(self.itens) == 0

Filas (Queues)

Uma fila (queue) segue o princípio FIFO (First In, First Out). O primeiro elemento inserido é o primeiro a ser removido. É análogo a uma fila de banco: quem chega primeiro é atendido primeiro.

Operações Básicas

  • Enqueue: insere um elemento no final da fila.
  • Dequeue: remove o elemento do início da fila.
  • Front: consulta o elemento do início sem removê-lo.
  • isEmpty: verifica se a fila está vazia.

Aplicações

  • Filas de impressão.
  • Gerenciamento de processos em sistemas operacionais (escalonamento).
  • Algoritmo de busca em largura (BFS) em grafos.
  • Buffers em comunicação de dados.

Uma implementação mais eficiente de fila utiliza o conceito de fila circular, onde o início e o fim são índices que se movem dentro de um array fixo, reaproveitando posições. A fila circular evita o deslocamento de elementos e garante operações O(1) no pior caso. Em Python, a classe collections.deque implementa uma deque (fila de duas pontas) que pode ser usada como fila ou pilha com desempenho O(1) para inserções e remoções em ambas as extremidades.

Implementação Simples em Python

class Fila:
    def __init__(self):
        self.itens = []
    def enqueue(self, item):
        self.itens.append(item)
    def dequeue(self):
        if not self.esta_vazia():
            return self.itens.pop(0)
    def front(self):
        if not self.esta_vazia():
            return self.itens[0]
    def esta_vazia(self):
        return len(self.itens) == 0

Nota: usar pop(0) em listas Python é ineficiente (O(n)); para uso real prefira collections.deque.

Comparação entre Pilhas e Filas

CaracterísticaPilhaFila
PrincípioLIFOFIFO
InserçãoNo topo (push)No final (enqueue)
RemoçãoDo topo (pop)Do início (dequeue)
Complexidade típicaO(1) (push/pop)O(1) (enqueue/dequeue)
Aplicações comunsUndo, call stack, parserEscalonamento, BFS, buffers

Deques (Double-Ended Queues)

Um deque é uma generalização de pilha e fila que permite inserção e remoção tanto no início quanto no final, com complexidade O(1) para ambas as operações (em implementações adequadas). Em Python, collections.deque é a implementação recomendada para uso como fila ou pilha quando o desempenho é crítico. Deques são úteis em algoritmos que exigem acesso rápido a ambas as extremidades, como na implementação de janelas deslizantes (sliding window) ou em algoritmos de cache.

Perguntas Frequentes (FAQ)

1. Qual a principal diferença entre pilha e fila?
Pilha utiliza LIFO (último a entrar, primeiro a sair), enquanto fila utiliza FIFO (primeiro a entrar, primeiro a sair). Na pilha, a inserção e remoção ocorrem na mesma extremidade (topo); na fila, a inserção ocorre no final e a remoção no início.

2. É possível implementar uma fila usando pilhas?
Sim, usando duas pilhas: uma para enfileirar (enqueue) e outra para desenfileirar (dequeue). Quando a pilha de dequeue estiver vazia, transferimos todos os elementos da pilha de enqueue para ela, invertendo a ordem. Essa implementação tem complexidade amortizada O(1) por operação.

3. O que é uma fila circular e por que é útil?
Uma fila circular reaproveita os espaços liberados ao remover elementos do início, evitando desperdício de memória. Em vez de deslocar todos os elementos, mantém índices de início e fim que avançam circularmente.

4. Qual a importância de pilhas em compiladores?
Pilhas são usadas para analisar a sintaxe da linguagem (parsing), avaliar expressões (notação polonesa reversa) e gerenciar escopos de variáveis durante a compilação.

5. O que causa um stack overflow?
Stack overflow ocorre quando a pilha de chamadas de funções ultrapassa o limite de memória alocada para ela. Isso pode acontecer devido a recursão infinita, recursão muito profunda sem otimização, ou alocação excessiva de variáveis locais grandes na pilha. Quando ocorre, o programa geralmente é encerrado com um erro de segmentação ou estouro de pilha.

6. Qual a relação entre pilhas e recursão?
A recursão depende intrinsecamente da pilha de chamadas (call stack). Cada chamada recursiva empilha um novo quadro (stack frame) contendo variáveis locais e o ponto de retorno. Quando a condição de base é atingida, as chamadas começam a retornar e os quadros são desempilhados. Por isso, recursões muito profundas podem causar stack overflow. Linguagens como Scheme otimizam a recursão em cauda (tail call optimization) para reaproveitar o quadro atual e não aumentar a pilha.

Conclusão

Pilhas e filas são estruturas de dados linear essenciais na computação. Dominar seus conceitos, operações e implementações permite resolver problemas clássicos de forma eficiente e compreender sistemas mais complexos, como escalonadores, navegadores e interpretadores de linguagens. Continuaremos explorando variações como deques e filas de prioridade nas próximas aulas.