São requisitos para essa aula:
Agradecimentos especiais ao prof. Fabiano Oliveira e prof. Fábio Protti, cujo conteúdo didático forma a base desses slides
Consideramos o Problema da Busca em que, dados:
Responda: $x$ pertence a $S$?
Em caso positivo, encontrar $s_i$ tal que $s_i = x$.
Desafio: Como organizar os dados de forma a facilitar a operação de busca?
Podemos utilizar uma Árvore Binária rotulada $T$, tal que:
{width=30%}
$T$ é uma Árvore Binária de Busca (ABB)
Exemplos de ABB, contendo nós D, E, F, L, M, N e O.
::::::::::{.columns}
:::::{.column width=45%}
{height=40%}
:::::
:::::{.column width=50%}
{height=40%}
:::::
::::::::::
Relembrando (aula de Árvores) a estrutura de árvore binária considerada:
struct NoEnc3 {
char chave; // dado armazenado
NoEnc3* esq; // filho esquerdo
NoEnc3* dir; // filho direito
};
struct ArvoreEnc3 {
NoEnc3* raiz; // raiz da árvore
};
Podemos resolver o Problema da Busca, com chave de busca $c$, através de uma ABB.
Ideia Geral:
v->chave == cc < v->chave: refaça o algoritmo na subárvore esquerdac > v->chave: refaça o algoritmo na subárvore direita
{height=30%}
Avalie se as árvores abaixo são árvores binárias de busca:
::::::::::{.columns}
:::::{.column width=45%}
{height=40%}
:::::
:::::{.column width=50%}
{height=40%}
:::::
::::::::::
. . .
Solução: nenhuma delas é! Erros: $B < A$ (na A1); $H > K$ (na A4).
buscaABBImplementação da busca em árvores binárias de busca:
auto* buscaABB(auto* no, char c) {
if(!no) return no; // chave não encontrada
if(no->chave == c) return no; // chave encontrada
if(c < no->chave)
return buscaABB(no->esq, c); // recursão esquerda
else
return buscaABB(no->dir, c); // recursão direita
}
Pergunta: Quantos chamadas recursivas esse algoritmo pode precisar?
. . .
Resposta: Em uma árvore degenerada com $N$ nós, até $N$ passos (observe que, nesse caso, $N$ também é a altura da árvore)
Encontre o pior caso (pior chave de busca) para a execução do algoritmo buscaABB nas quatro árvores abaixo (avalie primeiro se são ou não árvores binárias de busca):
::::::::::{.columns}
:::::{.column width=70%}
{width=90%}
:::::
:::::{.column width=30%}
{height=40%}
:::::
::::::::::
. . .
Solução: 1. N/A, 2. N/A, 3. D, 4. N/A, Fig.7 L
Como a buscaABB depende a altura da árvore, qual o melhor caso possível para a busca (menor altura possível) em uma árvore binária com $N$ nós?
Relembrando: uma árvore binária completa (ou cheia/perfeita) possui $\lceil \log_2 (N+1) \rceil$ níveis.
::::::::::{.columns}
:::::{.column width=70%}
{width=80%}
:::::
:::::{.column width=30%}
{height=40%}
:::::
::::::::::
. . .
Solução: 1. N/A, 2. N/A, 3. Falso, 4. $N=7$ e $\log_2 8 = 3$ (Fig.7 tem $\log_2 9 = 4$)
Dado um nó do tipo NoEnc5, calcule o menor elemento da árvore, com método:
NoEnc5* minimo(NoEnc5* const no) { ... }
| :::::::: | |
| ::::: | {30%} |
struct NoEnc5 {
char chave;
NoEnc5* esq;
NoEnc5* dir;
NoEnc5* pai;
};
:::::
. . .
| ::::: | {55%} |
NoEnc5* minimo(NoEnc5* const no)
// pre(no) // C++26
// post(out : !out->esq) // C++26
{
auto* atual = no;
while(atual->esq) atual = atual->esq;
return atual;
};
::::: ::::::::
Dado um nó do tipo NoEnc5, calcule o elemento sucessor na árvore, com método:
NoEnc5* sucessor(NoEnc5* const no) { ... }
| :::::::: | |
| ::::: | {30%} |
struct NoEnc5 {
char chave;
NoEnc5* esq;
NoEnc5* dir;
NoEnc5* pai;
};
:::::
. . .
| ::::: | {55%} |
NoEnc5* sucessor(NoEnc5* const no)
// pre(no) // C++26
// post(out : implica(no->dir, !out->esq))
{
if(no->dir) return minimo(no->dir);
auto* atual = no;
auto* suc = atual->pai;
while(suc && eh_filho_direito(atual)) {
atual = suc;
suc = atual->pai;
}
return suc;
}
::::: ::::::::
Dado um nó do tipo NoEnc6, com chave char e dado complementar float,
implemente a operação upsert: um update, caso chave exista, ou um insert, caso chave não exista:
void upsertABB(char c, float v, NoEnc6* no) { ... }
::: - :::::::: || ::::: |{15%}
struct NoEnc6 {
char chave;
float dado;
NoEnc6* esq;
NoEnc6* dir;
NoEnc6* pai;
};
:::::
. . .
| ::::: | {70%} |
void upsertABB(char c, float v, NoEnc6* no) { // pre(no)
if (c == no->chave) { no->dado = v; return; }
if (c < no->chave) {
if (no->esq) upsertABB(c, v, no->esq);
else no->esq =
new NoEnc6{.chave=c,.dado=v,.esq=0,.dir=0,.pai=no};
} else {
if (no->dir) upsertABB(c, v, no->dir);
else no->dir =
new NoEnc6{.chave=c,.dado=v,.esq=0,.dir=0,.pai=no};
}
}
::::: :::::::: :::
Relembrando (aula de Árvores) a estrutura de árvore binária com pai considerada:
struct NoEnc5 {
char chave; // dado armazenado
NoEnc5* esq; // filho esquerdo
NoEnc5* dir; // filho direito
NoEnc5* pai; // pai
};
struct ABB {
NoEnc5* raiz; // raiz da árvore
int N; // número de nós
... // métodos típicos: busca, insere, remove, ...
};
insereABBImplementação de inserção em árvore binária de busca não-vazia:
void insereABB(char c, NoEnc5* no)
// pre(no) // C++26
{
if(c <= no->chave) {
if(no->esq) insereABB(c, no->esq);
else no->esq = new NoEnc5{.chave=c, .esq=0, .dir=0, .pai=no};
}
else {
if(no->dir) insereABB(c, no->dir);
else no->dir = new NoEnc5{.chave=c, .esq=0, .dir=0, .pai=no};
}
}
Pergunta: Quantos chamadas recursivas esse algoritmo pode precisar? R: O(N).
removeABBImplementação de remoção em árvore binária de busca (retorna nova raiz):
::: -
auto removeABB(char c, auto* raiz) {
if(!raiz) return std::tuple{false, raiz};
auto* no = ::buscaABB(raiz, c); if(!no) return std::tuple{false, raiz};
if(tem_dois_filhos(no)) {
auto* removido = sucessor(no);
extrai(removido); // 'removido' não tem filho esquerdo
no->chave = removido->chave;
delete removido;
return std::tuple{true, raiz};
} else {
auto [pai, filho] = extrai(no); // 'no' tem no máximo um filho
delete no;
if (!pai) return std::tuple{true, filho};
return std::tuple{true, raiz};
}
} // Exercicio: entenda cada caso e cada tipo de retorno!
:::
Finalmente, temos uma ABB completa:
::: -
export struct ABB {
NoEnc5* raiz; // raiz da árvore
int N;
void cria() { N = 0; raiz = 0; }
void libera() { if(raiz) ::destroi_bin(raiz); raiz = 0; }
NoEnc5* busca(char c) { return ::buscaABB(raiz, c); }
void insere(char c) {
if(!raiz) raiz = new NoEnc5{.chave = c, .esq=0, .dir=0, .pai=0};
else ::insereABB(c, raiz);
N++;
}
bool remove(char c) {
auto [b, nraiz] = ::removeABB(c, raiz); raiz = nraiz; if(b) N--;
return b;
}
}; // fim
:::
Conclusão:
Árvores Binárias de Busca (ABB) são muito úteis para busca e organização da informação.
Porém, elas podem se degenerar! Para isso precisaremos estudar as Árvores Balanceadas.
Além da bibliografia do curso, recomendamos para esse tópico:
Em especial, agradeço aos colegas que elaboraram bons materiais, como o prof. Fabiano Oliveira (IME-UERJ), e o prof. Jayme Szwarcfiter cujos conceitos formam o cerne desses slides.
Estendo os agradecimentos aos demais colegas que colaboraram com a elaboração do material do curso de Pesquisa Operacional, que abriu caminho para verificação prática dessa tecnologia de slides.
Esse material de curso só é possível graças aos inúmeros projetos de código-aberto que são necessários a ele, incluindo:
Agradecimento especial a empresas que suportam projetos livres envolvidos nesse curso:
Esses slides foram escritos utilizando pandoc, segundo o tutorial ilectures:
Exceto expressamente mencionado (com as devidas ressalvas ao material cedido por colegas), a licença será Creative Commons.
Licença: CC-BY 4.0 2020
Igor Machado Coelho