Trilha 03 · Algoritmos e complexidade

Estruturas de dados fundamentais

Um algoritmo eficiente começa com a estrutura de dados certa. Pilha, fila e hash table são as três mais usadas no dia a dia — cada uma com operações O(1) para os casos em que foi projetada. Escolher a errada pode transformar uma solução elegante em um gargalo de performance.

① Intuição

Cada estrutura foi projetada para um padrão de acesso

Um array é como uma caixa de ovos numerada: acesso direto pelo índice em O(1), mas inserir no meio exige mover tudo — O(n). Uma pilha é como uma pilha de pratos: só acessa o topo, mas push e pop são sempre O(1). Uma fila é como uma fila de banco: entra pelo fundo, sai pela frente, ambas O(1). Uma hash table é como um armário com etiquetas: dado o nome da etiqueta, vai direto ao compartimento — O(1) em média.

A regra prática: quando precisar de acesso por índice, use array. Quando precisar de LIFO (desfazer ações, parsear expressões), use pilha. Quando precisar de FIFO (fila de tarefas, BFS), use fila. Quando precisar de lookup rápido por chave, use hash table (dict em Python). Cada estrutura tem O(1) para as operações que justificam sua existência.
② Visualização interativa

Pilha, fila e hash table em ação

Interaja com cada estrutura e observe o comportamento de push/pop, enqueue/dequeue e inserção com hash.

LIFO — Last In, First Out. O topo é sempre o último inserido. push/pop são O(1).

42
7
13← topo
Tamanho: 3 / 6
③ Explicação técnica

Tabela de complexidades

# Complexidades de operações das estruturas principais:
#
# Estrutura    | Acesso  | Busca  | Inserção | Remoção
# -------------|---------|--------|----------|--------
# Array        | O(1)    | O(n)   | O(n)     | O(n)
# Linked list  | O(n)    | O(n)   | O(1)*    | O(1)*
# Stack        | O(n)    | O(n)   | O(1)     | O(1)
# Queue        | O(n)    | O(n)   | O(1)     | O(1)
# Hash table   | O(1)*   | O(1)*  | O(1)*    | O(1)*
#
# * caso médio (hash: pior caso O(n) com muitas colisões)
# * lista ligada: O(1) inserção SE já tiver o ponteiro do nó anterior

# Python: as estruturas nativas
lista = [1, 2, 3]           # array dinâmico (como ArrayList do Java)
lista.append(4)             # O(1) amortizado
lista.insert(0, 0)          # O(n) — desloca todos os elementos

pilha = []                  # use lista como pilha
pilha.append("a")           # push — O(1)
top = pilha.pop()           # pop  — O(1)

from collections import deque
fila = deque()              # use deque como fila
fila.append("a")            # enqueue — O(1)
front = fila.popleft()      # dequeue — O(1) (list.pop(0) seria O(n)!)

tabela = {}                  # hash table nativa (dict)
tabela["chave"] = "valor"   # insert — O(1) médio
v = tabela.get("chave")     # lookup — O(1) médio

Como a hash table funciona internamente

# Como funciona uma hash table por dentro
class HashTableSimples:
    def __init__(self, tamanho=8):
        self.buckets = [[] for _ in range(tamanho)]
        self.tamanho = tamanho

    def _hash(self, chave):
        return hash(chave) % self.tamanho   # função hash

    def inserir(self, chave, valor):
        i = self._hash(chave)
        for par in self.buckets[i]:        # colisão: encadeamento
            if par[0] == chave:
                par[1] = valor; return
        self.buckets[i].append([chave, valor])

    def buscar(self, chave):
        i = self._hash(chave)
        for par in self.buckets[i]:
            if par[0] == chave: return par[1]
        return None

# Colisão: duas chaves diferentes mapeiam para o mesmo bucket
# "gato" e "tago" podem ter o mesmo hash % 8
# Resolução por encadeamento: bucket vira uma lista ligada
Fator de carga e rehashing: quando uma hash table fica muito cheia (muitas colisões), fica lenta. A solução é o rehashing: quando o fator de carga (n_elementos / n_buckets) supera ~0.7, cria-se uma tabela maior (geralmente o dobro) e reinserem-se todos os elementos. O custo é O(n), mas acontece raramente — por isso inserção em hash table é O(1) amortizado, não O(1) no pior caso. O dict do Python usa uma tabela que começa com 8 buckets e faz rehash a ~2/3 de carga.
④ Projeto para programar

Implementando do zero

Mini projeto: use uma pilha (lista Python) para verificar se uma expressão tem parênteses balanceados: ((a+b)*c) é válida, ((a+b não é. Para cada ( empilhe; para cada ) desempilhe. Se a pilha estiver vazia ao final e todos os fechamentos encontraram abertura, a expressão é válida. Estenda para {} e [].

Projeto principal: implemente um sistema de cache LRU (Least Recently Used) — quando o cache está cheio, remove o item usado há mais tempo. Use um dict (hash table) para lookup O(1) e um deque (fila duplamente encadeada) para rastrear a ordem de uso. Essa combinação é exatamente como o functools.lru_cache do Python funciona internamente.

Desafio extra: implemente uma fila com dois stacks. A ideia: um stack para enqueue, outro para dequeue. Quando o stack de dequeue estrazia, mova tudo do stack de enqueue para ele (inverte a ordem). Prove que o custo amortizado por operação é O(1). Essa é uma pergunta clássica de entrevista técnica.

⑤ Exercícios rápidos

Teste sua intuição

Qual estrutura permite buscar um elemento por nome (string) em O(1) tempo médio?
Para implementar o botão "Desfazer" (Ctrl+Z) de um editor de texto, qual estrutura é mais adequada?
Qual é a complexidade real de lookup em uma hash table?
⑥ Aplicações no mundo real

Onde você encontra isso

↩️

Ctrl+Z — histórico de ações como pilha

Todo editor (VS Code, Photoshop, Word) usa uma pilha para desfazer ações. Cada operação do usuário é empilhada. Ctrl+Z desempilha a última ação e a desfaz. Ctrl+Y re-empilha (redo stack separada). A limitação de "100 desfazimentos" é um tamanho máximo de pilha para não usar memória infinita.

🌐

Cache de DNS e CDN

Servidores DNS usam hash tables para resolver nomes de domínio para IPs em O(1). CDNs como Cloudflare usam caches LRU — uma combinação de hash table (lookup O(1)) e fila (rastrear recência). Com trilhões de requisições por dia, a diferença entre O(1) e O(n) pode ser a diferença entre latência de 1ms e 1s.

📬

Filas de mensagens (Kafka, RabbitMQ)

Sistemas de mensageria como Kafka são filas distribuídas em escala: producers empurram mensagens no fundo, consumers retiram da frente. Kafka adiciona a ideia de "tópicos" e "offset" — consumidores lembram onde pararam (como um ponteiro em uma fila persistente). Com milhões de mensagens por segundo, a eficiência O(1) de enqueue/dequeue é essencial.

← Anterior: Recursão Próxima: Grafos →