Estruturas De Dados E Algoritmos Com Javascript - Estruturas De Dados E Algoritmos Com Javascript: Groner Loiane ...
Estruturas De Dados E Algoritmos Com Javascript: Groner Loiane ...

Implementando pilhas e filas do zero

A maioria dos cursos começa explicando o que é uma lista encadeada. Ninguém avisa que você vai passar duas horas depurando um ponteiro nulo porque esqueceu de atualizar o next do nó anterior quando remover o último elemento. Eu sei disso porque já perdi uma tarde inteira com isso num projeto real. Vamos pular a teoria básica e ir direto para o código. Uma pilha (stack) em JavaScript pode ser feita com um array simples usando push() e pop(). O problema é que isso esconde o que acontece por baixo. Quando você implementa manualmente, consegue ver exatamente como a memória se comporta.

Estruturas de dados e algoritmos com javascript na prática

Aqui está uma implementação básica de pilha usando nós encadeados: function Node(value) {
this.value = value;}

function Stack() {
Stack.prototype.push = function(value) {
Stack.prototype.pop = function() {};

Parece simples, certo? É. Mas observe o que acontece quando você tenta usar isso em um loop recursivo profundo. A cada chamada de push, você está alocando um novo objeto no heap. Em Node.js, com o V8 gerenciando memória, isso gera garbage collection spikes que podem travar sua aplicação por 50 a 200 milissegundos. Em um servidor que processa milhares de requisições por segundo, esse delay se acumula rápido. Eu tive esse problema num sistema de processamento de filas de eventos onde usávamos uma pilha para fazer rollback de operações. A cada 10 mil itens, o GC pegava. A solução foi trocar para um ArrayBuffer com índices fixos, eliminando a alocação dinâmica completamente. O código ficou menos legível, mas a latência caiu de 120ms para menos de 3ms no pior caso.

Uma fila (queue) segue lógica semelhante, mas com um detalhe importante: precisa manter referência tanto ao head quanto ao tail. Implementar com array usando shift() é um erro comum. O método shift() realoca todo o array na memória, transformando uma operação O(1) em O(n). Sempre use nós encadeados para filas também. Para listas encadeadas duplamente ligadas, o ganho é a capacidade de navegar para trás, mas o custo dobra em alocação de memória. Cada nó precisa de três referências: valor, próximo e anterior. Em JavaScript, onde cada objeto tem overhead adicional do motor V8, isso pode significar 40 a 60 bytes por nó em vez de 20 a 30. Se você está armazenando milhões de registros, considere usar typed arrays como alternativa.

Árvores binárias e a armadilha do desbalanceamento

Uma árvore binária de busca (BST) simples tem complexidade O(log n) para busca, inserção e remoção — desde que esteja balanceada. Na prática, inserindo dados já ordenados, a árvore vira uma lista encadeada e a complexidade degrada para O(n). Eu já vi isso acontecer em produção quando um colega inseriu registros de um banco de dados sem ordenação prévia. Para evitar isso, implementações profissionais usam AVL ou Red-Black. Em JavaScript puro, uma AVL é mais fácil de implementar. Cada nó precisa de um campo balanceFactor. Após cada inserção, você recalcula os fatores e aplica rotações.

function AVLNode(key, value) {
function getHeight(node) {
function updateHeight(node) {br} O problema com árvores em JavaScript é que elas são pesadas em memória. Uma BST com 100 mil nós ocupa aproximadamente 12 a 15 megabytes apenas nos objetos JavaScript, sem contar o heap do V8 e suas estruturas internas. Se o seu cenário permite, considere usar um Map nativo ou até mesmo arrays ordenados com busca binária. Para 95 dos meus projetos, isso foi suficiente e muito mais eficiente.

Gráficos e a escolha entre matriz de adjacência e lista

Esta é a decisão mais importante em problemas de grafos. Matriz de adjacência usa O(V²) de espaço. Lista de adjacência usa O(V + E). Para grafos esparsos, como redes sociais ou rotas de entrega, a diferença é brutal. Uma matriz para 10 mil vértices ocupa 100 megabytes. A lista equivalente ocupa cerca de 2 a 5 megabytes. Eu enfrentei isso num algoritmo de pathfinding para um sistema de logística. Começamos com matriz porque era mais simples de codificar. Quando elevamos o número de nodes de 5 mil para 20 mil, a aplicação simplesmente estourou a memória. Trocar para lista de adjacência reduziu o consumo em 90% e o tempo de construção do grafo de 8 segundos para 0.3 segundos.

Para BFS em JavaScript: function bfs(graph, start) { 0) {br}

👉 Clique no botão abaixo para saber mais sobre o assunto!

Note que novamente usamos shift() na fila. Para grafos grandes, isso vai te penalizar. Uma fila circular com ponteiros head e tail resolve isso, mantendo O(1) para dequeue.

Ordenação: quando quicksort não é a resposta

Quicksort tem complexidade média O(n log n), mas no pior caso cai para O(n²). Isso acontece quando o pivot é consistentemente o menor ou maior elemento. Arrays quase ordenados são o cenário clássico. O Node.js usa introsort, que combina quicksort, heapsort e insertion sort. Você pode replicar isso, mas na maioria das vezes Array.prototype.sort() já é otimizado nativamente pelo V8 em C++. O que a maioria dos desenvolvedores não considera é o custo de comparar objetos complexos. Ordenar 50 mil objetos com uma função customizada de comparação pode levar de 2 a 4 segundos, enquanto o mesmo array de strings leva 50 a 100 milissegundos. Se possível, extraia a chave de ordenação para um campo separado antes de ordenar.

function sortByKey(array, key) { ({key: item[key], index: i})); a.key - b.key); array[item.index]);

br} Isso também serve para merge sort customizado. A complexidade é garantidamente O(n log n), mas o overhead de alocação de arrays temporários pode tornar mais lento que quicksort para conjuntos pequenos, digamos abaixo de 1 mil elementos. A partir de 5 mil, o merge sort se torna previsível e seguro contra o pior caso.

Hash tables: colisões e a Armadilha do Load Factor

Em JavaScript, objetos e Map funcionam como hash tables. A diferença é que Map preserva ordem de inserção e permite qualquer tipo de chave. Objetos convertem todas as chaves para string, o que causa bugs silenciosos quando você usa objetos ou arrays como chaves. Colisões em hash tables são inevitáveis. O V8 usa open addressing com sondagem linear para collision resolution em objetos internos. Map faz algo similar. O load factor crítico é 0.75. Acima disso, a tabela é redimensionada, geralmente dobrando de tamanho. Esse redimensionamento é uma operação O(n) que ocorre de forma assíncrona em muitos casos, mas pode causar jank em threads principais.

Se você está construindo uma hash table customizada, a estratégia de sontagem quadrática ou double hashing reduz a aglomeração em comparação com a sondagem linear. Eu optei por double hashing num sistema de cache distribuído e consegui reduzir colisões de 15% para menos de 2% com carga de 80%, algo que a sondagem linear jamais alcançaria. A profundidade aqui é intencional. Estruturas de dados e algoritmos com javascript exigem entender não apenas a lógica, mas como o motor JavaScript lida com memória e execução. Cada decisão de implementação carrega trade-offs que só ficam evidentes sob carga real.