269 lines
11 KiB
Markdown
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
|
||
|
|
||
|
```
|