Trilha 04 · Estruturas de dados

Pilhas, filas e deques

Estas três estruturas ficam famosas por proibir coisas. Elas não deixam você acessar o elemento do meio, buscar por valor ou inserir onde quiser — e é justamente essa restrição que permite que todas as operações permitidas sejam O(1). Nesta lição você vê como um buffer circular entrega isso sem mover um único byte.

① Intuição

A restrição é a funcionalidade

Uma pilha é uma pilha de pratos: você só alcança o de cima. Isso parece uma limitação até você perceber que é exatamente o comportamento que quer para o botão "desfazer", para o histórico de chamadas de função e para avaliar expressões aninhadas — situações em que "o último que aconteceu" é sempre o que importa primeiro.

Uma fila é a fila do caixa: quem chegou primeiro é atendido primeiro. É a estrutura da justiça e da ordem de chegada — jobs de impressão, requisições num servidor, mensagens entre serviços, e a busca em largura num grafo.

Um deque (pronuncia-se "deck", de double-ended queue) permite as duas pontas. Com ele você tem pilha e fila ao mesmo tempo — e ganha casos que nenhuma das duas resolve sozinha, como uma janela deslizante que cresce por um lado e encolhe pelo outro.

Como escolher: se o padrão do problema é "desfazer / voltar atrás / o mais recente primeiro", é pilha. Se é "ordem de chegada / processar na sequência / nível por nível", é fila. Se você precisa das duas pontas — ou de descartar o mais antigo enquanto adiciona o mais novo — é deque.
② Visualização interativa

O buffer circular e a fila feita de duas pilhas

Na primeira aba, veja os índices head e size darem a volta no array sem que nenhum elemento se mova. Na segunda, o truque clássico de construir uma fila usando somente pilhas — e por que ele é O(1) amortizado.

Um deque (double-ended queue) permite inserir e remover nas duas pontas em O(1). A implementação clássica é um buffer circular: um array de tamanho fixo onde os índices dão a volta com % capacidade.

·0·1A2B3C4·5·6·7size3
head = 2
próximo livre = (head + size) % 8 = 5
Buffer circular com capacidade 8. head aponta para a frente.
Repare: os elementos nunca se movem na memória. Só os índices head e size mudam. É exatamente assim que funcionam o collections.deque do Python, o ArrayDeque do Java e o VecDeque do Rust.
③ Explicação técnica

Três interfaces, não três estruturas

# Pilha, fila e deque não são estruturas — são INTERFACES.
# Cada uma restringe deliberadamente o que você pode fazer,
# e é essa restrição que garante o O(1).

# PILHA (stack) — LIFO: só mexe no topo
push(x)     # adiciona no topo        O(1)
pop()       # remove e devolve o topo O(1)
peek()      # olha o topo sem remover O(1)

# FILA (queue) — FIFO: entra atrás, sai na frente
enqueue(x)  # adiciona no fim         O(1)
dequeue()   # remove da frente        O(1)

# DEQUE — as duas pontas
push_front(x) / push_back(x)    # O(1)
pop_front()   / pop_back()      # O(1)

# Repare no que NÃO existe em nenhuma delas:
#   - acesso por índice
#   - inserção no meio
#   - busca por valor
# Justamente as operações que custariam O(n).

Na prática, em Python

# Em Python
# --- PILHA: list já serve ---
pilha = []
pilha.append("a")     # push — O(1) amortizado
topo = pilha.pop()    # pop  — O(1)

# --- FILA: NUNCA use list ---
fila = []
fila.append("a")
fila.pop(0)           # ⚠️ O(n) — desloca todo o array!

from collections import deque
fila = deque()
fila.append("a")      # enqueue    — O(1)
fila.popleft()        # dequeue    — O(1)
fila.appendleft("z")  # push_front — O(1)
fila.pop()            # pop_back   — O(1)

# deque com tamanho máximo: descarta o mais antigo sozinho.
# Perfeito para "últimas N mensagens" ou média móvel.
ultimos = deque(maxlen=100)

# --- FILA THREAD-SAFE (entre threads) ---
from queue import Queue
q = Queue()           # com locks internos; use esta com threads
A armadilha mais comum: usar list.pop(0) como fila. O código funciona e passa nos testes com 100 itens — e vira um gargalo O(n²) com 100 mil. Regra: se você vai remover do início, use deque. O mesmo vale em Java (ArrayDeque, não ArrayList) e em JavaScript, onde array.shift() tem o mesmo problema.

Por dentro: o buffer circular

# Buffer circular: a implementação clássica de um deque.
class BufferCircular:
    def __init__(self, capacidade):
        self.buf = [None] * capacidade
        self.cap = capacidade
        self.head = 0      # índice do primeiro elemento
        self.tamanho = 0

    def push_back(self, x):
        if self.tamanho == self.cap: raise Exception("cheio")
        self.buf[(self.head + self.tamanho) % self.cap] = x
        self.tamanho += 1

    def push_front(self, x):
        if self.tamanho == self.cap: raise Exception("cheio")
        self.head = (self.head - 1) % self.cap       # anda para trás, dá a volta
        self.buf[self.head] = x
        self.tamanho += 1

    def pop_front(self):
        if not self.tamanho: raise Exception("vazio")
        x = self.buf[self.head]
        self.buf[self.head] = None
        self.head = (self.head + 1) % self.cap
        self.tamanho -= 1
        return x

# O segredo inteiro é o "% cap": os índices dão a volta.
# Nenhum elemento jamais se move na memória — só os índices andam.
# Por isso todas as quatro operações são O(1) de verdade.

Pilha na prática: delimitadores balanceados

# O uso mais clássico de pilha: verificar delimitadores balanceados.
# É literalmente o que seu editor faz para colorir o par de chaves.
def balanceado(texto):
    pares = {")": "(", "]": "[", "}": "{"}
    pilha = []
    for c in texto:
        if c in "([{":
            pilha.append(c)
        elif c in pares:
            if not pilha or pilha.pop() != pares[c]:
                return False          # fechou o que não estava aberto
    return not pilha                # sobrou algo aberto?

balanceado("((a+b)*c)")   # True
balanceado("([)]")        # False — a ordem importa, e a pilha captura isso

# Por que pilha e não um contador?
# Um contador aceitaria "([)]" como válido. A pilha lembra
# QUAL delimitador foi aberto por último — e "por último" é
# exatamente a semântica LIFO.
④ Projeto para programar

Construindo do zero

Mini projeto: implemente uma calculadora de notação polonesa reversa (RPN). A entrada "3 4 + 2 *" deve dar 14. O algoritmo cabe em 10 linhas com uma pilha: número você empilha; operador você desempilha dois, aplica e empilha o resultado. Depois estenda para converter expressões infixas comuns (3 + 4 * 2) para RPN com o algoritmo shunting-yard de Dijkstra — que usa uma segunda pilha para os operadores.

Projeto principal: implemente o BufferCircular acima e use-o como buffer de log de tamanho fixo: as últimas 1000 linhas de log de um servidor, onde escrever a linha 1001 sobrescreve a primeira automaticamente. Meça o tempo de 1 milhão de escritas e compare com uma list que faz pop(0) quando passa de 1000 elementos.

Desafio extra: o problema do máximo em janela deslizante. Dado um array e uma janela de tamanho k, devolva o máximo de cada janela. A solução ingênua é O(n·k). Com um deque que guarda índices em ordem decrescente de valor — removendo pela frente os que saíram da janela e por trás os que ficaram menores que o novo elemento — o custo cai para O(n), com cada elemento entrando e saindo do deque no máximo uma vez.

⑤ Exercícios rápidos

Teste sua intuição

Por que usar list.pop(0) como fila é um problema de performance?
Num buffer circular, por que push_front é O(1) enquanto inserir no início de um array comum é O(n)?
Na fila implementada com duas pilhas, qual é o custo de dequeue?
Qual estrutura resolve a verificação de parênteses balanceados, e por quê?
⑥ Aplicações no mundo real

Onde você encontra isso

📞

A pilha de chamadas do seu programa

Toda linguagem executa funções usando uma pilha: cada chamada empilha um stack frame com argumentos, variáveis locais e o endereço de retorno; cada return desempilha. O StackOverflowError é literalmente essa pilha batendo no limite — geralmente por recursão sem caso base. O rastreamento de erro que você lê num traceback é essa pilha impressa de cima para baixo.

🎧

Buffers circulares em áudio e vídeo

Placas de som e drivers de captura usam ring buffers: o hardware escreve amostras numa ponta enquanto o software lê na outra, sem alocar memória nem copiar nada — o que seria fatal num sistema de tempo real com deadline de milissegundos. O mesmo padrão aparece em drivers de rede (anéis de descritores) e no dmesg do Linux, que é um ring buffer de mensagens do kernel.

🔀

Filas de mensagens e escalonamento

O escalonador do sistema operacional mantém filas de processos prontos para rodar. Sistemas distribuídos como RabbitMQ, SQS e Kafka são filas persistentes: produtores enfileiram, consumidores desenfileiram, e a ordem de chegada dá previsibilidade. Em ambos, o custo O(1) de enqueue/dequeue é o que permite milhões de mensagens por segundo sem que a fila em si vire o gargalo.

← Anterior: Arrays e listas ligadas Próxima: Hash tables por dentro →