freeCodeCamp/guide/portuguese/algorithms/binary-search-trees/index.md

11 KiB

title localeTitle
Binary Search Trees Árvores de busca binária

Árvores de busca binária

Árvore de busca binária

Uma árvore é uma estrutura de dados composta de nós que possui as seguintes características:

  1. Cada árvore tem um nó raiz (no topo) tendo algum valor.
  2. O nó raiz tem zero ou mais nós filhos.
  3. Cada nó filho possui zero ou mais nós filhos e assim por diante. Isso cria uma subárvore na árvore. Cada nó tem sua própria subárvore, composta de seus filhos e filhos, etc. Isso significa que cada nó sozinho pode ser uma árvore.

Uma árvore de pesquisa binária (BST) adiciona essas duas características:

  1. Cada nó tem um máximo de até dois filhos.
  2. Para cada nó, os valores de seus nós descendentes à esquerda são menores que os do nó atual, que por sua vez é menor que os nós descendentes à direita (se houver).

O BST é construído com base na idéia do algoritmo de busca binária , que permite a rápida pesquisa, inserção e remoção de nós. A maneira como eles são configurados significa que, em média, cada comparação permite que as operações pule cerca de metade da árvore, de modo que cada pesquisa, inserção ou exclusão leve o tempo proporcional ao logaritmo do número de itens armazenados na árvore, O(log n) No entanto, algumas vezes o pior caso pode acontecer, quando a árvore não está balanceada e a complexidade do tempo é O(n) para todas as três dessas funções. É por isso que as árvores de auto-equilíbrio (AVL, vermelho-preto, etc.) são muito mais eficazes do que o BST básico.

Exemplo de cenário de pior caso: Isso pode acontecer quando você continua adicionando nós que são sempre maiores que o nó antes (é pai), o mesmo pode acontecer quando você sempre adiciona nós com valores menores que seus pais.

Operações básicas em um BST

  • Criar: cria uma árvore vazia.
  • Inserir: insira um nó na árvore.
  • Pesquisar: procura um nó na árvore.
  • Apagar: apaga um nó da árvore.

Crio

Inicialmente, uma árvore vazia sem nós é criada. A variável / identificador que deve apontar para o nó raiz é inicializado com um valor NULL .

Pesquisa

Você sempre começa a pesquisar na árvore no nó raiz e desce a partir daí. Você compara os dados em cada nó com o que você está procurando. Se o nó comparado não corresponder, então você vai para o filho direito ou para o filho esquerdo, o que depende do resultado da comparação a seguir: Se o nó que você está procurando for menor do que aquele com o qual você estava comparando, você prossegue para a criança esquerda, caso contrário (se for maior) você vai para a criança certa. Por quê? Como o BST é estruturado (conforme sua definição), o filho certo é sempre maior que o pai e o filho esquerdo é sempre menor.

Inserir

É muito semelhante à função de pesquisa. Você começa novamente na raiz da árvore e desce recursivamente, procurando o lugar certo para inserir nosso novo nó, da mesma forma como explicado na função de busca. Se um nó com o mesmo valor já estiver na árvore, você poderá optar por inserir a duplicata ou não. Algumas árvores permitem duplicatas, outras não. Depende da implementação certa.

Eliminação

Existem 3 casos que podem acontecer quando você está tentando excluir um nó. Se tiver,

  1. Nenhuma subárvore (sem filhos): Esta é a mais fácil. Você pode simplesmente excluir o nó, sem precisar de ações adicionais.
  2. Uma subárvore (um filho): você precisa garantir que depois que o nó for excluído, seu filho será conectado ao pai do nó excluído.
  3. Duas subárvores (dois filhos): você precisa localizar e substituir o nó que deseja excluir com seu sucessor (o nó letfmost na subárvore direita).

A complexidade de tempo para criar uma árvore é O(1) . A complexidade de tempo para procurar, inserir ou excluir um nó depende da altura da árvore h , então o pior caso é O(h) .

Antecessor de um nó

Os predecessores podem ser descritos como o nó que viria logo antes do nó em que você está atualmente. Para localizar o predecessor do nó atual, observe o maior / maior nó de folha na subárvore esquerda.

Sucessor de um nó

Os sucessores podem ser descritos como o nó que viria logo após o nó em que você está atualmente. Para encontrar o sucessor do nó atual, observe o nó da folha mais à esquerda / menor na subárvore direita.

Tipos especiais de BT

  • Pilha
  • Árvore vermelho-preto
  • B-tree
  • Árvore Splay
  • Árvore N-ary
  • Trie (árvore Radix)

Tempo de execução

Estrutura de dados: Array

  • Desempenho de pior caso: O(log n)
  • Melhor desempenho de caso: O(1)
  • Desempenho médio: O(log n)
  • Pobre complexidade do espaço: O(1)

Onde n é o número de nós no BST.

Implementação do BST

Aqui está uma definição para um nó BST com alguns dados, fazendo referência a seus nós filhos esquerdo e direito.

struct node { 
   int data; 
   struct node *leftChild; 
   struct node *rightChild; 
 }; 

Operação de pesquisa

Sempre que um elemento for pesquisado, inicie a pesquisa no nó raiz. Então, se os dados forem menores que o valor da chave, procure o elemento na subárvore esquerda. Caso contrário, procure o elemento na subárvore direita. Siga o mesmo algoritmo para cada nó.

struct node* search(int data){ 
   struct node *current = root; 
   printf("Visiting elements: "); 
 
   while(current->data != data){ 
 
      if(current != NULL) { 
         printf("%d ",current->data); 
 
         //go to left tree 
         if(current->data > data){ 
            current = current->leftChild; 
         }//else go to right tree 
         else { 
            current = current->rightChild; 
         } 
 
         //not found 
         if(current == NULL){ 
            return NULL; 
         } 
      } 
   } 
   return current; 
 } 

Inserir operação

Sempre que um elemento for inserido, primeiro localize seu local apropriado. Comece a pesquisar no nó raiz e, se os dados forem menores que o valor da chave, procure o local vazio na subárvore esquerda e insira os dados. Caso contrário, procure o local vazio na subárvore direita e insira os dados.

void insert(int data) { 
   struct node *tempNode = (struct node*) malloc(sizeof(struct node)); 
   struct node *current; 
   struct node *parent; 
 
   tempNode->data = data; 
   tempNode->leftChild = NULL; 
   tempNode->rightChild = NULL; 
 
   //if tree is empty 
   if(root == NULL) { 
      root = tempNode; 
   } else { 
      current = root; 
      parent = NULL; 
 
      while(1) { 
         parent = current; 
 
         //go to left of the tree 
         if(data < parent->data) { 
            current = current->leftChild; 
            //insert to the left 
 
            if(current == NULL) { 
               parent->leftChild = tempNode; 
               return; 
            } 
         }//go to right of the tree 
         else { 
            current = current->rightChild; 
 
            //insert to the right 
            if(current == NULL) { 
               parent->rightChild = tempNode; 
               return; 
            } 
         } 
      } 
   } 
 } 

Árvores de busca binária (BSTs) também nos dão acesso rápido a antecessores e sucessores. Os predecessores podem ser descritos como o nó que viria logo antes do nó em que você está atualmente.

  • Para localizar o predecessor do nó atual, observe o nó da folha mais à direita / maior na subárvore esquerda. Os sucessores podem ser descritos como o nó que viria logo após o nó em que você está atualmente.
  • Para localizar o sucessor do nó atual, observe o nó da folha mais à esquerda / menor na subárvore direita.

Vamos dar uma olhada em alguns procedimentos operando em árvores.

Como as árvores são definidas recursivamente, é muito comum escrever rotinas que operam em árvores que são elas próprias recursivas.

Então, por exemplo, se quisermos calcular a altura de uma árvore, essa é a altura de um nó raiz, podemos ir em frente e recursivamente fazer isso, passando pela árvore. Então podemos dizer:

  • Por exemplo, se tivermos uma árvore nula, sua altura será 0.
  • Caso contrário, somos 1 mais o máximo da árvore de filhos à esquerda e a árvore de filhos certa.
  • Então, se olharmos para uma folha, por exemplo, essa altura seria 1, porque a altura da criança esquerda é nula, é 0, e a altura da criança nula direita também é 0. Então, o máximo disso é 0, então 1 mais 0

Algoritmo de altura (árvore)

if tree = nil: 
    return 0 
 return 1 + Max(Height(tree.left),Height(tree.right)) 

Aqui está o código em C ++

int maxDepth(struct node* node) 
 { 
    if (node==NULL) 
        return 0; 
   else 
   { 
       int rDepth = maxDepth(node->right); 
       int lDepth = maxDepth(node->left); 
 
       if (lDepth > rDepth) 
       { 
           return(lDepth+1); 
       } 
       else 
       { 
            return(rDepth+1); 
       } 
   } 
 } 

Também poderíamos ver o cálculo do tamanho de uma árvore que é o número de nós.

  • Novamente, se tivermos uma árvore nula, temos zero nós.
  • Caso contrário, temos o número de nós no filho à esquerda mais 1 para nós mesmos mais o número de nós no filho certo. Então 1 mais o tamanho da árvore esquerda mais o tamanho da árvore certa.

Algoritmo de tamanho (árvore)

if tree = nil 
    return 0 
 return 1 + Size(tree.left) + Size(tree.right) 

Aqui está o código em C ++

int treeSize(struct node* node) 
 { 
    if (node==NULL) 
        return 0; 
    else 
        return 1+(treeSize(node->left) + treeSize(node->right)); 
 } 

Vídeos relevantes no canal do YouTube freeCodeCamp

A seguir estão os tipos comuns de árvores binárias:

Árvore Binária Completa / Árvore Binária Restrita: Uma Árvore Binária é completa ou estrita se cada nó tiver exatamente 0 ou 2 filhos.

           18 
       /       \ 
     15         30 
    /  \        /  \ 
  40    50    100   40 

Na Árvore Binária Completa, o número de nós de folha é igual ao número de nós internos mais um.

Árvore Binária Completa: Uma Árvore Binária é uma Árvore Binária completa se todos os níveis estiverem completamente preenchidos, exceto possivelmente o último nível e o último nível tiver todas as chaves o mais possível

           18 
       /       \ 
     15         30 
    /  \        /  \ 
  40    50    100   40 
 /  \   / 
 8   7  9