devly
Voltar para Algoritmos

Navegando em Hierarquias: Árvores

Foco em Árvores de Busca Binária (BSTs), travessias, Heaps (Priority Queues) e Tries (para strings).

Atualizado em: 10 de Junho, 2025
Notas do Autor

Quando vi meu primeiro problema de árvore, tentei resolvê-lo com loops e um monte de \`if/else\` para controlar se ia para a esquerda ou para a direita. Foi um desastre. O código ficou complexo, ilegível e cheio de bugs.

O segredo, que aprendemos na aula passada, é que a recursão é a linguagem nativa das árvores. Cada nó de uma árvore é, em si, a raiz de uma sub-árvore menor. Uma vez que você enxerga um problema de árvore como um problema recursivo, tudo "clica".

Nesta aula, vamos aprender a "falar o idioma das árvores", com foco nas estruturas que mais aparecem nas entrevistas: Árvores de Busca Binária (BSTs), Heaps e Tries.


1. O Básico: Anatomia de uma Árvore

Em computação, uma Árvore é uma estrutura de dados hierárquica que consiste em nós (nodes) conectados por arestas (edges). Diferente das estruturas lineares, ela não tem um "começo" e "fim" sequenciais, mas sim uma estrutura de pais e filhos.

Root (Raiz): O nó do topo da árvore, o único sem um "pai".
Parent (Pai): Um nó que tem conexões com nós abaixo dele.
Child (Filho): Um nó que tem uma conexão de um nó acima dele.
Leaf (Folha): Um nó sem filhos.
Height (Altura): O comprimento do caminho mais longo da raiz até uma folha.

A Peça Fundamental: O Nó

Tudo começa com a definição do nó. Em JavaScript, geralmente é uma classe simples:

javascript
class TreeNode {
  constructor(val, left = null, right = null) {
    this.val = val;     // O valor do nó
    this.left = left;   // Referência para o filho da esquerda (outro TreeNode)
    this.right = right; // Referência para o filho da direita (outro TreeNode)
  }
}

2. Árvores de Busca Binária (BSTs): A Árvore Ordenada 🌳

A BST é o tipo de árvore mais comum em entrevistas. Ela tem uma regra fundamental que a torna especial: para qualquer nó, todos os valores na sua sub-árvore esquerda são menores que o valor do nó, e todos os valores na sua sub-árvore direita são maiores.

Essa propriedade permite operações de busca, inserção e remoção muito eficientes, com complexidade de tempo média de O(log n) para uma árvore balanceada.

Implementação Simplificada de uma BST (Insert e Search)
class BinarySearchTree {
  constructor() {
    this.root = null; // A BST começa vazia
  }

  insert(val) {
    const newNode = new TreeNode(val);
    if (!this.root) {
      this.root = newNode;
      return this;
    }

    let current = this.root;
    while (true) {
      if (val === current.val) return undefined; // Não permite valores duplicados
      if (val < current.val) {
        if (current.left === null) {
          current.left = newNode;
          return this;
        }
        current = current.left;
      } else {
        if (current.right === null) {
          current.right = newNode;
          return this;
        }
        current = current.right;
      }
    }
  }

  search(val) {
    if (!this.root) return false;

    let current = this.root;
    while (current) {
      if (val === current.val) return true; // Encontrou!
      if (val < current.val) {
        current = current.left;
      } else {
        current = current.right;
      }
    }

    return false; // Não encontrou
  }
}

Travessias (Traversals): Como Visitar os Nós

Como você visita todos os nós de uma árvore? Existem dois estilos principais: Busca em Profundidade (DFS) e Busca em Largura (BFS).

DFS vs. BFS: Qual Usar?

Use DFS (Pre, In, Post-order) quando quiser explorar um caminho até o fim. É mais simples de implementar com recursão e gasta menos memória. Ótimo para problemas de busca de caminho ou quando a ordem da travessia importa (como obter valores ordenados de uma BST).

Use BFS (Level-order) quando quiser encontrar o caminho mais curto entre dois nós. Ele explora "vizinhos primeiro", nível por nível. É a escolha certa para problemas do tipo "menor número de passos para chegar a X".

Os 4 Tipos de Travessia
// --- DFS: Depth-First Search ---

// 1. In-Order (Esquerda, Raiz, Direita) -> Para BSTs, resulta em ordem crescente.
function inOrder(node) {
  if (!node) return;
  inOrder(node.left);
  console.log(node.val);
  inOrder(node.right);
}

// 2. Pre-Order (Raiz, Esquerda, Direita) -> Útil para copiar a árvore.
function preOrder(node) {
  if (!node) return;
  console.log(node.val);
  preOrder(node.left);
  preOrder(node.right);
}

// 3. Post-Order (Esquerda, Direita, Raiz) -> Útil para deletar nós.
function postOrder(node) {
  if (!node) return;
  postOrder(node.left);
  postOrder(node.right);
  console.log(node.val);
}

// --- BFS: Breadth-First Search ---

// 4. Level-Order -> Visita a árvore nível por nível, usando uma Fila.
function levelOrder(root) {
  if (!root) return;

  const queue = [root]; // Usa uma fila para controlar os nós a visitar

  while (queue.length > 0) {
    const node = queue.shift(); // Pega o primeiro da fila
    console.log(node.val);

    if (node.left) queue.push(node.left);
    if (node.right) queue.push(node.right);
  }
}

3. Heaps: A Árvore de Prioridades (Priority Queue) 🏆

Um Heap é um tipo especial de árvore (geralmente binária) onde os dados têm uma propriedade de ordem. Ele não serve para buscas gerais, mas é extremamente eficiente para uma tarefa: encontrar e remover o elemento de maior (ou menor) prioridade rapidamente.

Max-Heap: O nó pai é sempre maior ou igual aos seus filhos. O elemento máximo está sempre na raiz.
Min-Heap: O nó pai é sempre menor ou igual aos seus filhos. O elemento mínimo está sempre na raiz.

As operações principais, como inserir um elemento ou extrair o mínimo/máximo, são O(log n). É a estrutura de dados por trás do que chamamos de Fila de Prioridade (Priority Queue).

Caso de Uso Clássico: Top 'K' Elementos

Um problema comum em entrevistas é "Encontre os K maiores elementos de uma coleção". Uma solução eficiente usa um Min-Heap de tamanho K. Você itera pela coleção: se o Heap tem menos de K elementos, adicione. Se já tem K e o elemento atual é maior que o topo do Heap (o menor dos K maiores), remova o topo e insira o novo. No final, o Heap contém os K maiores elementos.

4. Tries (Árvores de Prefixo): A Árvore para Strings 🔡

Uma Trie (lê-se "try") é uma árvore otimizada para armazenar e buscar strings. Cada nó representa um caractere, e um caminho da raiz até um nó específico forma um prefixo ou uma palavra completa. Elas são a base para funcionalidades de dicionário e autocomplete.

Implementação de uma Trie (Insert, Search, StartsWith)
class TrieNode {
  constructor() {
    this.children = {};    // Mapeia caracteres para outros TrieNodes
    this.isEndOfWord = false; // Flag para marcar o fim de uma palavra
  }
}

class Trie {
  constructor() {
    this.root = new TrieNode();
  }

  // Insere uma palavra na Trie
  insert(word) {
    let current = this.root;
    for (const char of word) {
      if (!current.children[char]) {
        current.children[char] = new TrieNode();
      }
      current = current.children[char];
    }
    current.isEndOfWord = true;
  }

  // Procura por uma palavra completa na Trie
  search(word) {
    let current = this.root;
    for (const char of word) {
      if (!current.children[char]) {
        return false; // Caminho não existe
      }
      current = current.children[char];
    }
    return current.isEndOfWord; // Retorna true apenas se for uma palavra completa
  }

  // Verifica se existe alguma palavra com um dado prefixo
  startsWith(prefix) {
    let current = this.root;
    for (const char of prefix) {
      if (!current.children[char]) {
        return false;
      }
      current = current.children[char];
    }
    return true;
  }
}

Caso de Uso Clássico: Autocomplete

Pense em como o Google funciona: você digita "dev" e ele sugere "devly", "desenvolvedor", "devops". Uma Trie é a estrutura de dados perfeita para esse tipo of funcionalidade baseada em prefixos. Ela permite encontrar todas as palavras que começam com um dado prefixo de forma muito eficiente.

5. Uma Palavra sobre Árvores Balanceadas (AVL, Red-Black)

O "sonho" do O(log n) de uma BST depende dela ser balanceada. Se você inserir elementos já ordenados (ex: 1, 2, 3, 4, 5) em uma BST simples, ela se tornará uma "árvore degenerada", que na prática é uma Linked List, e as operações caem para O(n).

Fica a Dica 💡

Árvores auto-balanceáveis como AVL e Red-Black Trees são estruturas mais complexas que realizam rotações para garantir que a árvore permaneça balanceada, mantendo a performance de O(log n) garantida. Você quase nunca precisará implementar uma do zero em uma entrevista, mas saber que elas existem e o problema que resolvem é um grande sinal de conhecimento.

6. A Mágica do Balanceamento: Rotações de Árvore (Nível Hardcore) 🧠

Mencionamos que árvores como AVL e Red-Black se auto-balanceiam. Mas como? A operação fundamental por trás dessa "mágica" é a rotação. Uma rotação é uma operação local que altera a estrutura da árvore para diminuir sua altura, mas — e isso é crucial — preserva a propriedade de uma Árvore de Busca Binária.

Entender o conceito de rotações, mesmo que você não decore o código, é um diferencial gigantesco.

Rotação para a Direita (Right Rotation)

Usada quando uma sub-árvore esquerda se torna muito "pesada" (alta). O filho da esquerda de um nó "sobe" para se tornar o novo pai, e o pai antigo "desce" para se tornar o filho da direita.


    y           (Rotaciona para a Direita em 'y')           x
   / \                                                     / \
  x   C         ---------------------------------->        A   y
 / \                                                         / \
A   B                                                       B   C
Rotação para a Direita
function rightRotate(y) {
  const x = y.left;
  const B = x.right;

  // Realiza a rotação
  x.right = y;
  y.left = B;

  // Retorna a nova raiz da sub-árvore
  return x;
}

Rotação para a Esquerda (Left Rotation)

É a operação inversa. Usada quando uma sub-árvore direita fica muito pesada. O filho da direita sobe para se tornar o novo pai.


  x             (Rotaciona para a Esquerda em 'x')          y
 / \                                                       / \
A   y           ---------------------------------->        x   C
   / \                                                     / \
  B   C                                                   A   B
Rotação para a Esquerda
function leftRotate(x) {
  const y = x.right;
  const B = y.left;

  // Realiza a rotação
  y.left = x;
  x.right = B;

  // Retorna a nova raiz da sub-árvore
  return y;
}

Quando isso é perguntado?

Em uma entrevista, o cenário pode ser: "Dado esta BST, a inserção de um novo nó a desbalanceou. Desenhe no quadro branco como uma rotação corrigiria a estrutura para manter a performance O(log n)". Saber fazer esses diagramas e explicar a lógica é o que eles procuram.

Mão na Massa: Resolvendo o Cenário da Entrevista

Vamos simular um cenário clássico de entrevista para solidificar o conceito.

Passo 1: A Árvore Inicial.


(30)

Passo 2: Inserindo 20 e 10 (Criando o Desbalanceamento).


    (30) <-- Fator: -2 (Desbalanceado!)
   /
 (20)
 /
(10)

A inserção de 10 na sub-árvore esquerda do filho esquerdo do nó 30 causou o desbalanceamento. Este é o caso "Esquerda-Esquerda" (Left-Left).

Passo 3: A Solução (Rotação para a Direita). Para consertar, aplicamos uma Rotação para a Direita no nó desbalanceado (30).


      (20) <-- Nova Raiz
     /   \
   (10)  (30)

Resultado: O nó 20 "sobe" e se torna a nova raiz. O 30 se torna seu filho direito. A árvore está balanceada e a propriedade da BST continua válida!

Fica a Dica 💡

Você não precisa saber todos os quatro casos de rotação (Left-Left, Left-Right, etc.) de cor. Mas entender e ser capaz de explicar UM caso como este demonstra um conhecimento profundo que impressiona qualquer entrevistador.

💊 Pílula Devly

Árvores são sobre hierarquia e relacionamentos. Use uma BST para manter dados ordenados e buscá-los rapidamente. Use um Heap quando a prioridade máxima/mínima é tudo que importa. Use uma Trie para problemas de prefixo com strings. E lembre-se: a linguagem nativa para "falar" com árvores é a recursão.

Anterior

Pensando em Recursão (e o Labirinto do Backtracking)

Voltar

Próximo

Desvendando Conexões: Grafos

Continuar