Beruflich Dokumente
Kultur Dokumente
o
Facom - Faculdade de Computac
a
o
Bacharelado Ciencia da Computac
a
o
Estruturas de Dados e Programac
a
Arvores
Bin
arias de Busca
Neste topico da disciplina, estudaremos conceitos iniciais e algoritmos relativos
a uma estrutura de dados mais complexa do que as estruturas sequenciais vistas ate
o momento, porem com uma grande quantidade de aplicacoes: as arvores. Estas
estruturas admitem, em geral, tratamento computacional simples e eficiente. Mais
especificamente, nosso objetivo e estudar arvores binarias de busca, um caso particular
de arvores, com uma grande variedade de aplicacoes praticas.
Na Secao 1 definimos a estrutura geral do tipo arvore e suas propriedades. Na
Secao 2 estudamos aspectos relacionadas `as arvores binarias, discutindo algoritmos de
percurso nestas estruturas. Finalmente, estudamos na Secao 3 as arvores binarias de
busca e seus algoritmos.
Arvores
A
B
I
Figura 1: Exemplo de uma arvore.
Uma sequencia de nos distintos v1 , ..., vk tal que existe sempre entre os nos consecutivos a relacao e pai de ou e filho de de, e denominada um caminho na arvore
e seu comprimento e igual a k. O nvel de um no v e o comprimento do caminho
da raiz ate o no v. O nvel da raiz e, portanto, igual a 1. A altura de um no v e
o comprimento do maior caminho de v ate um de seus descendentes. As folhas tem
altura 1. A altura de uma arvore e a altura de sua raiz. Representa-se a altura de T
por h(T ), enquanto h(v) denota a altura do no v. A altura da arvore na Figura 1 e 4.
Arvores
Bin
arias
As arvores constituem as estruturas nao sequenciais com maior aplicacao em computacao e dentre estas estruturas de dados, as arvores binarias sao, sem d
uvida, as
mais comuns.
Uma arvore binaria T e um conjunto finito de elementos denominados nos, tal
que
T = , e a arvore e dita vazia, ou
existe um no r chamado raiz de T ; os restantes sao divididos em dois subconjuntos disjuntos TrE e TrD , a subarvore esquerda e direita de r, respectivamente,
as quais tambem sao arvores binarias.
De acordo com a definicao, um no de uma arvore binaria pode possuir 0, 1 ou 2
nos filhos. A raiz da subarvore esquerda de um no v e chamada de filho esquerdo de v
e a raiz da subarvore direita de um no v e chamada de filho direito de v.
2.1
Arvores
Estritamente Bin
arias, Completas e Cheias
Existem alguns tipos especiais de arvores binarias, bastante utilizadas, que sao definidas
a seguir. Um arvore e estritamente binaria se uma arvore binaria em que cada no possui 0 ou 2 filhos. Uma arvore binaria e completa se todos os nos que possuem uma
de suas subarvores vazias encontram-se no u
ltimo ou pen
ultimo nvel da arvore. Uma
arvore binaria e cheia se todos os nos que possuem uma de suas subarvores vazias
encontram-se no u
ltimo nvel da arvore.
A relacao entre a altura de uma arvore binaria e o seu n
umero de nos e um dado
importante para varias aplicacoes. Para um valor fixo de n (n
umero de nos), a arvore
binaria com altura mnima e a arvore binaria completa e a arvore binaria com altura
maxima e aquela onde todos os nos tem no maximo 1 filho. Essa u
ltima e chamada
de arvore ziguezague. Na Figura 2 sao mostrados exemplos destes tipos de arvores
binarias.
(a)
(c)
(b)
Figura 2: Exemplos de tipos especiais de arvores: (a) arvore zigue-zague (b) arvore
completa (c) arvore cheia.
2.2
Definindo um n
o e uma
arvore bin
aria
class Node {
public:
int value;
Node* lft;
Node* rgt;
Node* parent;
Node(int x, Node* p = 0) {
value = x;
parent = p;
lft = rgt = 0;
}
};
class BinaryTree {
public:
BinaryTree() {
root = 0;
numberOfNodes = 0;
}
~BinaryTree() {
clear();
}
void add(int);
bool remove(int);
bool contains(int value) {
Node* t;
return findNode(value) != 0;
}
int size() const {
return numberOfNodes;
}
bool isEmpty() const {
return root == 0;
}
void print();
void suc(int);
void pred(int);
private:
Node* root;
int numberOfNodes;
Node* findNode(int);
bool removeNode(Node*);
void printNode(Node*);
void clear();
void deleteTree(Node*);
int minValue(Node*);
int maxValue(Node*);
int altura(Node*);
void preOrdTree(Node *);
void inOrdTree(Node *);
void posOrdTree(Node *);
void visitNode(Node *);
};
2.3
Percurso em Arvores
Bin
arias
Um percurso consiste em uma visitacao sistematica a cada um dos nos de uma arvore.
Esta e uma das operacoes mais comuns em arvores binarias. O conceito de visita a um
no pode significar manipular, de alguma forma, a(s) informacao(oes) contida(s) nele.
Por exemplo, visitar pode significar imprimir o conte
udo de um ou mais campos de
um no.
Existem diversos tipos de percurso em arvores binarias que se caracterizam,
basicamente, pela ordem com que os nos sao visitados. O percurso em pre-ordem
tambem conhecido como RED, visita primeiramente a raiz e depois segue a visitacao
pela subarvore esquerda e direita, tambem em pre-ordem. O trecho de codigo abaixo
mostra os passos do percurso em pre-ordem, considerando que node e o ponteiro para
um no da arvore onde deseja iniciar o percurso.
void BinaryTree::preOrdTree(Node* node){
if (node != 0){
visitNode(node);
preOrdTree(node->lft);
preOrdTree(node->rgt);
}
}
percurso sao: percorre a subarvore esquerda em ordem simetrica, visita a raiz e percorre a subarvore direita em ordem simetrica. O trecho de codigo abaixo mostra os
passos do percurso em ordem simetrica.
void BinaryTree::inOrdTree(Node * node){
if (node != 0){
inOrdTree(node->lft);
visitNode(node);
inOrdTree(node->rgt);
}
}
No percurso em pos-ordem (DER), primeiramente percorre-se as subarvores esquerda e direita em pos-ordem e posteriormente realiza-se a visitacao da raiz. O trecho
de codigo abaixo mostra os passos do percurso em pos-ordem.
void BinaryTree::posOrdTree(Node * node){
if (node != 0){
posOrdTree(node->lft);
posOrdTree(node->rgt);
visitNode(node);
}
}
Em qualquer dos tres percursos vistos, o metodo correspondente sera executado tantas vezes quantos sao os nos da subarvore cuja raiz e node. Sendo n esse
valor, a complexidade dos percursos, considerando o metodo visitNode de tempo
constante, e O(n). Segue abaixo o exemplo de uma funcao main que instancia um
objeto BinaryTree denominado bt, realiza uma sequencia de insercoes e invocacoes
dos metodos de percurso sobre a arvore, considerando que o metodo visitNode realiza a impressao do campo value de seu no argumento, o metodo retRoot retorna o
ponteiro para o no raiz da arvore e o metodo add insere um novo no na arvore. Esse
u
ltimo metodo sera descrito na proxima secao.
int main(){
BinaryTree* bt = new BinaryTree();
bt->add(50);
bt->add(25);
bt->add(75);
bt->add(12);
bt->add(37);
bt->add(0);
printf("Percurso em pr
e-ordem: ");
bt->preOrdTree(bt->retRoot());
printf("Percurso em ordem sim
etrica: ");
bt->inOrdTree(bt->retRoot());
printf("Percurso em p
os-ordem: ");
bt->posOrdTree(bt->retRoot());
delete bt;
return 0;
}
A sada esperada e:
Percurso em pr
e-ordem: 50 25 12 0 37 75
Percurso em ordem sim
etrica: 0 12 25 37 50 75
Percurso em p
os-ordem: 0 12 37 25 75 50
Arvores
Bin
arias de Busca
Considere um conjunto S = {s1 , ..., sn } de elementos, tal que s1 < s2 < . . . < sn . Se
queremos fazer operacoes de busca, insercao e remocao de elementos em S, podemos
armazenar os elementos de S em um vetor ordenado. As operacoes de busca, insercao
e remocao nessa estrutura podem ser feitas gastando tempo O(log n), O(n) e O(n)
respectivamente.
Nesse topico, estudamos a estrutura de dados arvore para fazer essas operacoes.
Definimos uma arvore T com n nos, onde associamos a cada no, digamos v, uma chave
r(v) que e um elemento de S, digamos sj . Portanto, r(v) = sj . Os elementos do
conjunto sao distribudos pelos nos de uma arvore binaria convenientemente. Organizamos T de tal forma que nao sera necessario realizar um percurso passando por
todos os seus nos para encontrar uma determinada chave ou para garantir que um
dado elemento nao pertence ao conjunto S. No pior caso, sera necessario percorrer
apenas o caminho da raiz ate uma das folhas da arvore.
1
4
3
6
2
2
5
4
7
Figura 3: Duas diferentes arvores binarias de busca para armazenar o mesmo conjunto
de chaves.
3.1
3.1.1
Opera
c
oes B
asicas: busca, inserc
ao e remoc
ao
Busca
A definicao de uma arvore binaria de busca sugere como realizar a busca de um determinado valor de forma eficiente. Para que esta operacao seja aproveitada por outras
operacoes, nosso projeto determina que a busca recebe um valor x e devolva 0 se a
arvore e vazia ou devolva o endereco de um no onde x esta ou devolva o endereco de
uma folha onde deveramos inserir x como um de seus filhos. Entao, determinar se x
e ou nao chave de algum no e computado comparando x com a chave do no devolvido
pelo algoritmo de busca. Segue abaixo o metodo findNode da classe BinaryTree.
3.1.2
Inserc
ao
Remoc
ao
projetado de forma a dar suporte a esta operacao. Para uma entrada x, se o endereco
do no que findNode retorna nao tem como chave o elemento x, nao ha nada o que
fazer na remocao. O metodo booleano responsavel pela remocao retornara false nesse
caso. Caso contrario, um metodo private removeNode sera invocado para efetivar a
remocao.
O metodo removeNode deve levar em consideracao algumas situacoes em particular tais como:
1. se node e folha,
2. se node possui um u
nico filho esquerdo
3. ou outros casos.
A remocao nos dois primeiros casos e simples e possui complexidade constante. No
u
ltimo caso, e necessario buscar o no sucessor de node na arvore para realizar uma
remocao por copia. Os campos do no sucessor serao copiados para o no node.
Ha casos especiais a serem tratados quando, por exemplo, o no a ser removido e
a raiz da arvore. Esse caso especial ficara claro no codigo.
3.2
Enquanto a operacao de insercao nao altera a estrutura dos nos existentes na arvore,
a remocao pode fazer com que varias operacoes de alteracao de ponteiros acontecam,
ja que para remover um no que possui filhos, a estrutura dos nos restantes pode
ser alterada. Seja v o no a ser removido. O metodo geral da remocao consiste em
determinar qual o no que substituira v na arvore, de forma a nao comprometer as
propriedades da arvore binaria de busca. A busca pelo no que substituira v sera feita
sobre seus nos descendentes. Ha dois nos candidatos: o no que armazena o menor valor
na subarvore direita de v ou aquele que armazena o maior valor dentro dos nos da sua
subarvore esquerda (em outras palavras, o menor valor maior do que o que v armazena
ou o maior valor menor do que o que v armazena). Verifique voce mesmo que qualquer
um destes nos possui, no maximo, um filho, o que torna possvel a substituicao de v
por esse no em uma quantidade constante de operacoes. Segue abaixo a descricao de
dois metodos que, em conjunto com o metodo de busca ja visto, realizam a remocao
de um dado no em uma arvore binaria de busca.
bool BinaryTree::remove(int x) {
Node* p = 0;
Node* node = findNode(x,p);
10
if(node != 0)
{
removeNode(x,node);
return true;
}
return false;
}
void BinaryTree::removeNode(int x, Node* node)
{
// node e
o n
o que deve ser removido
// caso onde node possui dois filhos
if (node->lft != 0 && node->rgt !=0 )
{
// nesse caso, vamos fazer a remo
c~
ao por c
opia
Node* tmp = node->rgt;
Node* ant = node;
// busca sucessor de node
while(tmp->lft != 0) {
ant = tmp;
tmp = tmp->lft;
}
// nesse momento, tmp e
o sucessor de node e vamos copiar o campo
// value de tmp para o campo value de node
node->value = tmp->value;
node = tmp;
}
11
else
node->parent->rgt = aux;
}
if (aux !=0) aux->parent = node->parent;
delete node;
numberOfNodes --;
}
3.3
Complexidades
12
de todos os nos, em n
umero de k, do u
ltimo nvel. Logo, T 0 e uma arvore cheia com
n0 = n k nos. Pela hipotese de inducao, h(T 0 ) = 1 + blog2 n0 c. Como T 0 e cheia,
n0 = 2m 1, para glum inteiro m > 0. Isto e, h(T 0 ) = m. Alem disso, 1 k n0 + 1.
Assim, temos
h(T ) = 1 + h(T 0 ) = 1 + m = 1 + log2 (n0 + 1) = 1 + blog2 (n0 + k)c = 1 + blog2 (n)c
Exerccios
1. Escreva o metodo Node*suc(int value) que recebe um valor inteiro e retorna
um ponteiro para o no sucessor do no que armazena o elemento value em um
objeto da classe BinaryTree.
2. Escreva o metodo Node*pred(int value) que recebe um valor inteiro e retorna
um ponteiro para o no predecessor do no que armazena o elemento value em
um objeto da classe BinaryTree.
3. Escreva a versao iterativa dos metodos de percurso (pre-ordem, ordem simetrica
e pos ordem) em um objeto da classe BinaryTree.
13
8
3
2
11
11
5
6
10
10
5
6
(b) Imagemespelho de T
14
class Node {
public:
int value;
Node* lft;
Node* rgt;
Node(int value) {
this->value = value;
lft = rgt = 0;
}
};
class BinaryTree {
public:
// m
etodo construtor
BinaryTree() {
root = 0;
numberOfNodes = 0;
}
// m
etodo destrutor
~BinaryTree() {
clear();
}
// m
etodos
void imageTree(Node *);
void clear();
// ....
private:
Node* root; // apontador para o n
o raiz da a
rvore
int numberOfNodes;
// quantidade de n
os da a
rvore
};
(a) Apos a aplicacao do metodo imageTree usando como argumento a raiz
de uma arvore binaria de busca, a arvore resultante permanece binaria de
busca? Justifique.
(b) Qual a complexidade de tempo do seu metodo? Justifique.
(c) Foi necessario utilizar mais do que uma quantidade constante de espaco
adicional para realizacao da operacao descrita em imageTree? Em caso
afirmativo, ha como evitar esse gasto adicional de memoria?
11. (Questao de prova de 2010) Seja v um no de uma arvore binaria de busca
T . Denominamos Tv a subarvore de T com raiz v. Se um no x 6= v pertence a` subarvore Tv , dizemos que v e ancestral de x. Considere que cada no
da arvore T e implementado como descrito abaixo. Escreva o metodo Node*
15
BinaryTreeParent::ancestral(int u, int v) para encontrar e retornar o ponteiro para o ancestral comum mais pr
oximo de dois nos que armazenam os
valores inteiros u e v (no campo value) passados como argumento.
class Node {
public:
int value;
Node* parent;
Node* lft;
Node* rgt;
Node(int value) {
this->value = value;
lft = rgt = 0;
}
};
class BinaryTreeParent {
public:
// m
etodos
// ...
Node* ancestral(int, int);
// ....
private:
Node* root; // apontador para o n
o raiz da a
rvore
int numberOfNodes;
// quantidade de n
os da a
rvore
};
16