Pular para o conteúdo

Estruturas avançadas

Heap, união-busca, deque e contador — o que cada uma custa, e o problema que só ela resolve bem.

Cluster e vault resolvem quase tudo. As quatro estruturas desta página existem para os casos em que eles obrigam a pagar O(n) por algo que podia ser O(log n) ou O(1) — e todas já vêm em `Arcane.Collections`.

Heap — o menor primeiro, em `O(log n)`#

Uma fila de prioridade. O que ela dá, e o cluster não: inserir e tirar o menor mantendo a ordem sem ordenar a lista inteira.

dataforge
adopt Arcane.Collections as C

h := C.heap([5, 1, 9, 3])
C.heap_push(h, 2)
out C.heap_peek(h)     // 1   o menor, sem remover
out C.heap_pop(h)      // 1
out C.heap_pop(h)      // 2
OperaçãoHeapCluster ordenadoCluster solto
ver o menorO(1)O(1)O(n)
tirar o menorO(log n)O(n) (desloca)O(n)
inserirO(log n)O(n) (desloca)O(1)
montar de uma listaO(n)O(n log n)O(1)

Onde isso decide: os k maiores#

Ordenar tudo para pegar 10 é O(n log n). Um heap de tamanho 10 é O(n log k) — e com k pequeno, log k é praticamente uma constante:

dataforge
adopt Arcane.Collections as C
adopt Arcane.Time as T

dados := [randint(1, 1000000) cycle i in range(0, 300000)]

inicio := T.monotonic()
maiores_a := sorted(dados)[len(dados) - 10:]
ms_sort := (T.monotonic() - inicio) * 1000

inicio := T.monotonic()
maiores_b := C.top_n(dados, 10)
ms_heap := (T.monotonic() - inicio) * 1000

out $"ordenar tudo e cortar 10:  {round(ms_sort, 1)} ms"
out $"top_n com heap de 10:      {round(ms_heap, 1)} ms"
saída (300 mil itens)
ordenar tudo e cortar 10:  25.7 ms
top_n com heap de 10:      1.7 ms

15x, e a distância cresce com n. C.top_n e C.bottom_n fazem isso; C.priority_queue é o mesmo mecanismo com prioridade explícita, que é como se escreve um Dijkstra ou um escalonador.

União-busca — "estes dois estão no mesmo grupo?"#

O problema: juntar elementos em grupos e perguntar se dois estão juntos. Com listas, cada pergunta varre tudo; a união-busca responde em tempo quase constante.

dataforge
adopt Arcane.Collections as C

uf := C.union_find(["a", "b", "c", "d"])
uf.union("a", "b")
uf.union("c", "d")
out uf.connected("a", "b")     // yes
out uf.connected("a", "c")     // no
out uf.count()                 // 2 grupos
OperaçãoCusto
union(a, b)O(log n) amortizado
connected(a, b)O(log n) amortizado
count()O(n)
groups()O(n)

A compressão de caminho é o que dá o amortizado: cada busca achata a árvore que percorreu, e a próxima passa direto. É o mesmo raciocínio da análise amortizada do append — a operação cara paga pelas baratas que vêm depois.

Serve para componentes conexos de um grafo, para detectar ciclo ao montar uma árvore geradora mínima, e para agrupar duplicatas — "este e-mail e este telefone são da mesma pessoa".

Deque — as duas pontas em `O(1)`#

Um cluster é O(1) no fim e O(n) no começo: inserir na posição 0 empurra todo o resto. O deque é O(1) nas duas pontas.

dataforge
adopt Arcane.Collections as C

d := C.deque([1, 2, 3])
C.push_left(d, 0)
out C.pop_left(d)     // 0
out C.pop(d)          // 3
out len(d)            // 2
OperaçãoDequeCluster
no fim (push/pop)O(1)O(1)
no começo (push/pop)O(1)`O(n)`
por índice, no meioO(n)O(1)

A troca é clara: o deque ganha nas pontas e perde no acesso indexado. Use-o para fila (BFS, produtor-consumidor) e janela deslizante; para acesso aleatório, cluster.

Contador — contar sem laço aninhado#

Contar ocorrências percorrendo e comparando é O(n²). Com um vault de contagem é O(n), e o counter é isso pronto:

dataforge
adopt Arcane.Collections as C

palavras := ["a", "b", "a", "c", "a"]
out C.counter(palavras)                  // {a: 3, b: 1, c: 1}
out C.most_common(palavras, 2)           // [[a, 3], [b, 1]]

most_common(n) usa heap por dentro: O(n log k), não O(n log n). As duas ideias desta página juntas.

Escolhendo em trinta segundos#

A pergunta que você faz muitas vezesA estrutura
"este item está aqui?"vaultO(1)
"qual é o menor/maior agora?"heapO(log n)
"os k maiores de muitos"heap (top_n) — O(n log k)
"estes dois estão no mesmo grupo?"união-busca — quase O(1)
"o primeiro da fila"dequeO(1)
"quantas vezes cada um aparece?"counterO(n)
"o item da posição i"clusterO(1)
"está ordenado? onde entra este?"cluster ordenado + binary_searchO(log n)

Por onde seguir#