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

269 lines
11 KiB
Markdown

---
title: Binary Search Trees
localeTitle: Árvores de busca binária
---
## Árvores de busca binária
![Árvore de busca binária](https://cdn-images-1.medium.com/max/1320/0*x5o1G1UpM1RfLpyx.png)
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](https://guide.freecodecamp.org/algorithms/search-algorithms/binary-search) , 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.
```c
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ó.
```c
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.
```c
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
* [Árvore de busca binária](https://youtu.be/5cU1ILGy6dM)
* [Árvore de busca binária: Traversal e Altura](https://youtu.be/Aagf3RyK3Lw)
### 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
```