Na aula de hoje, exploramos duas estruturas de dados fundamentais na ciência da computação: pilhas (stacks) e filas (queues). Ambas são estruturas lineares que organizam elementos de forma sequencial, mas diferem na política de acesso. Pilhas seguem o princípio LIFO (Last In, First Out), enquanto filas seguem FIFO (First In, First Out). Discutimos suas implementações, operações básicas e principais aplicações.
O que é uma Pilha?
Uma pilha é uma coleção de elementos onde a inserção e remoção ocorrem em uma única extremidade chamada topo. Imagine uma pilha de pratos: o último prato colocado é o primeiro a ser retirado. Isso caracteriza o comportamento LIFO.
As operações fundamentais são:
- push: insere um elemento no topo.
- pop: remove o elemento do topo.
- top (ou peek): acessa o elemento do topo sem removê-lo.
- is_empty: verifica se a pilha está vazia.
Implementação em Python:
class Pilha:
def __init__(self):
self.itens = []
def push(self, item):
self.itens.append(item)
def pop(self):
if not self.is_empty():
return self.itens.pop()
raise IndexError("pop de pilha vazia")
def top(self):
if not self.is_empty():
return self.itens[-1]
raise IndexError("top de pilha vazia")
def is_empty(self):
return len(self.itens) == 0
Complexidade: push e pop são O(1) quando usamos lista dinâmica.
Implementação com lista encadeada
Outra forma comum de implementar uma pilha é usando uma lista encadeada (linked list). Cada nó armazena o valor e um ponteiro para o nó anterior (ou próximo, dependendo da orientação). A operação push insere um novo nó no topo e pop remove o nó do topo. Ambas as operações são O(1), sem necessidade de realocação de memória como em listas dinâmicas.
class Node:
def __init__(self, valor):
self.valor = valor
self.proximo = None
class PilhaEncadeada:
def __init__(self):
self.topo = None
def push(self, item):
novo = Node(item)
novo.proximo = self.topo
self.topo = novo
def pop(self):
if self.is_empty():
raise IndexError("pop de pilha vazia")
valor = self.topo.valor
self.topo = self.topo.proximo
return valor
def top(self):
if self.is_empty():
raise IndexError("top de pilha vazia")
return self.topo.valor
def is_empty(self):
return self.topo is None
Essa implementação consome um pouco mais de memória por nó (armazenamento do ponteiro), mas é adequada quando o tamanho máximo da pilha é desconhecido ou varia muito.
O que é uma Fila?
Uma fila é uma estrutura em que a inserção ocorre em uma extremidade (fim) e a remoção na outra (início), seguindo o princípio FIFO. É como uma fila de banco: o primeiro a chegar é o primeiro a ser atendido.
Operações básicas:
- enqueue: insere no fim.
- dequeue: remove do início.
- front: acessa o primeiro elemento.
- is_empty: verifica se a fila vazia.
Implementação simples:
class Fila:
def __init__(self):
self.itens = []
def enqueue(self, item):
self.itens.append(item)
def dequeue(self):
if not self.is_empty():
return self.itens.pop(0)
raise IndexError("dequeue de fila vazia")
def front(self):
if not self.is_empty():
return self.itens[0]
raise IndexError("fila vazia")
def is_empty(self):
return len(self.itens) == 0
Nota: usar pop(0) tem custo O(n) para listas Python; em implementações reais, usa-se deque (collections.deque) para operações O(1) em ambas extremidades.
Implementação eficiente com deque
A biblioteca padrão do Python oferece a classe collections.deque, uma fila duplamente terminada que suporta operações O(1) de inserção e remoção em ambas as extremidades. Para usar como fila, basta fazer:
from collections import deque
class FilaDeque:
def __init__(self):
self.itens = deque()
def enqueue(self, item):
self.itens.append(item)
def dequeue(self):
if self.is_empty():
raise IndexError("dequeue de fila vazia")
return self.itens.popleft()
def front(self):
if self.is_empty():
raise IndexError("fila vazia")
return self.itens[0]
def is_empty(self):
return len(self.itens) == 0
O uso de deque é recomendado para a maioria dos casos, pois combina desempenho e simplicidade. Além disso, ela também pode ser usada como pilha (append/pop) com a mesma eficiência.
Implementação com lista encadeada
Assim como a pilha, a fila também pode ser implementada com lista encadeada. A diferença é que precisamos manter referências tanto para o início quanto para o fim da fila, para que enqueue e dequeue sejam O(1). Veja o exemplo:
class Node:
def __init__(self, valor):
self.valor = valor
self.proximo = None
class FilaEncadeada:
def __init__(self):
self.inicio = None
self.fim = None
def enqueue(self, item):
novo = Node(item)
if self.fim:
self.fim.proximo = novo
self.fim = novo
if not self.inicio:
self.inicio = novo
def dequeue(self):
if self.is_empty():
raise IndexError("dequeue de fila vazia")
valor = self.inicio.valor
self.inicio = self.inicio.proximo
if not self.inicio:
self.fim = None
return valor
def front(self):
if self.is_empty():
raise IndexError("fila vazia")
return self.inicio.valor
def is_empty(self):
return self.inicio is None
A implementação encadeada é útil em cenários onde o tamanho da fila é imprevisível e não queremos desperdiçar memória com capacidade ociosa.
Aplicações
Pilhas são usadas em:
- Expressões aritméticas e avaliação (notação polonesa reversa);
- Gerenciamento de chamadas de função (call stack);
- Desfazer/refazer (undo/redo);
- Algoritmos de backtracking (como em busca em profundidade).
Filas são usadas em:
- Gerenciamento de tarefas (scheduling);
- Filas de impressão;
- BFS (busca em largura) em grafos;
- Buffers de dados (produtor-consumidor).
Exemplos práticos
Verificação de parênteses com pilha
Um uso clássico de pilha é verificar se uma expressão matemática tem os parênteses balanceados. Veja a implementação:
def parenteses_balanceados(expr):
pilha = Pilha() # usando a classe Pilha definida anteriormente
for ch in expr:
if ch == '(':
pilha.push(ch)
elif ch == ')':
if pilha.is_empty():
return False
pilha.pop()
return pilha.is_empty()
# Exemplos
print(parenteses_balanceados("(())")) # True
print(parenteses_balanceados("(()")) # False
Esse mesmo princípio pode ser estendido para colchetes e chaves, e é a base para parsers de linguagens de programação.
Busca em largura (BFS) com fila
A busca em largura em grafos utiliza uma fila para percorrer os vértices nível a nível. Exemplo simplificado:
from collections import deque
def bfs(grafo, inicio):
visitados = set()
fila = deque([inicio])
visitados.add(inicio)
while fila:
vertice = fila.popleft()
print(f"Visitando: {vertice}")
for vizinho in grafo[vertice]:
if vizinho not in visitados:
visitados.add(vizinho)
fila.append(vizinho)
# Exemplo de grafo
grafo = {
1: [2, 3],
2: [4],
3: [5],
4: [],
5: []
}
bfs(grafo, 1)
A fila garante que os vértices sejam explorados na ordem correta, gerando uma busca por níveis.
Comparação
| Característica | Pilha | Fila |
|---|---|---|
| Princípio | LIFO | FIFO |
| Inserção/Remoção | mesmo lado | lados opostos |
| Exemplo típico | pilha de pratos | fila de pessoas |
Variações
Deque (Double-Ended Queue)
O deque (fila duplamente terminada) permite inserção e remoção tanto no início quanto no fim. Em Python, a classe collections.deque implementa essa estrutura. Deques são úteis em problemas que exigem acesso eficiente a ambas as extremidades, como em algoritmos de janela deslizante ou na implementação de filas com prioridade dupla.
Fila de Prioridade (Priority Queue)
Uma fila de prioridade é uma fila onde cada elemento possui uma prioridade e o desenfileiramento ocorre na ordem de maior (ou menor) prioridade, não na ordem de chegada. Em Python, o módulo heapq implementa uma heap mínima, que pode ser usada como fila de prioridade:
import heapq
class FilaPrioridade:
def __init__(self):
self.itens = []
def enqueue(self, item, prioridade):
heapq.heappush(self.itens, (prioridade, item))
def dequeue(self):
if self.itens:
return heapq.heappop(self.itens)[1]
raise IndexError("fila de prioridade vazia")
def is_empty(self):
return len(self.itens) == 0
Filas de prioridade são amplamente usadas em algoritmos como Dijkstra, scheduling de processos e sistemas de mensageria.
Perguntas Frequentes
Qual a diferença fundamental entre pilha e fila?
A diferença está na ordem de remoção: pilha remove o elemento mais recente (LIFO); fila remove o mais antigo (FIFO).
Onde são usadas pilhas e filas no dia a dia da programação?
Pilhas aparecem em avaliação de expressões, call stack e undo; filas aparecem em processamento assíncrono, filas de mensagens e BFS.
Posso implementar pilha e fila com listas encadeadas?
Sim, ambas podem ser implementadas com listas ligadas, com vantagens de não ter realocação de memória e operações O(1) para inserção/remoção quando feitas nas extremidades corretas.
Como implementar uma fila circular?
Uma fila circular utiliza um array de tamanho fixo e dois ponteiros (início e fim) que se movem circularmente. Quando um ponteiro atinge o final do array, ele volta ao início, otimizando o uso do espaço. Essa abordagem é comum em sistemas embarcados e buffers de baixo nível. Implementar uma fila circular em Python pode ser feito com uma lista de tamanho fixo e controle manual de índices.
Qual a relação entre pilha e recursão?
A recursão utiliza implicitamente a pilha de chamadas (call stack) do sistema. Cada chamada recursiva empilha um novo quadro (frame) na pilha, contendo variáveis locais e o ponto de retorno. Quando a condição de base é atingida, os quadros começam a ser desempilhados. Por isso, recursões muito profundas podem causar estouro de pilha (stack overflow). Compreender pilhas ajuda a escrever funções recursivas eficientes e, quando necessário, convertê-las em iteração usando uma pilha explícita.
Resumo
- Pilhas e filas são estruturas lineares simples e poderosas.
- Implementá-las bem é essencial para resolver problemas clássicos.
- Python oferece listas e deque como ferramentas práticas.
Confira outros artigos sobre ciência da computação em nossa listagem de posts.
Dominar pilhas e filas é um passo fundamental para avançar em estruturas de dados mais complexas, como árvores, heaps e grafos, bem como para entender algoritmos clássicos de ordenação e busca.