Centro Federal de Educação Tecnológica do Espírito Santo
Unidade de Ensino Descentralizada de Colatina
Tecnólogo em Redes de Computadores
PROGRAMAÇÃO II
Prof. Jean Eduardo Glazar
4. ÁRVORE
Uma árvore impõe uma estrutura hierárquica em uma coleção de itens. Um exemplo familiar
é a árvore genealógica. Árvores despontam de forma natural em várias áreas da ciência da
computação. Por exemplo, árvores são usadas para organizar informações em sistemas de
banco de dados e para representar a estrutura sintática de programas fontes em
compiladores.
FUNDAMENTOS
Uma árvore é uma coleção finita de n 0 nodos. Se n = 0, dizemos que a árvore é nula.
Caso contrário, uma árvore apresenta as seguintes características:
•
existe um nodo especial denominado raiz;
•
os demais nodos são particionados em T1, T2, ..., Tk estruturas disjuntas de árvores;
•
as estruturas T1, T2, ..., Tk denominam-se subárvores.
OBS: todo nodo de uma árvore é a raiz de uma subárvore.
A exigência de que as estruturas T1, T2, ...,Tk sejam coleções disjuntas, garante que um
mesmo nodo não aparecerá em mais de uma subárvore ao mesmo tempo, ou seja, nunca
teremos subárvores interligadas. Observe que cada uma das estruturas Ti é organizada na
forma de árvore, isto caracteriza uma definição recursiva.
Abaixo é dado um exemplo de representação gráfica de uma árvore.
1
Centro Federal de Educação Tecnológica do Espírito Santo
Unidade de Ensino Descentralizada de Colatina
Tecnólogo em Redes de Computadores
TERMOS MAIS USADOS
• Grau de um nodo: número de subárvores de um nodo. Por exemplo, na figura acima, o
nodo a tem grau 3, o nodo d tem grau 2 e o nodo c tem grau 1.
• Grau de uma árvore: o grau de uma árvore (n-áridade) é definido como sendo igual ao
máximo dos graus de todos os seus nodos. Na ilustração, podemos constatar que o grau
da árvore é 3, pois nenhum nodo tem mais de 3 subárvores.
• Folhas de uma árvore: é um nodo que possui grau 0, ou seja, que não possui
subárvores. Também conhecido como nodo terminal. No exemplo dado, são folhas os
nodos e, k, g, l, m, i e j.
• Filhos de um nodo X: são as raízes das subárvores do nodo X. Neste caso, o nodo X é o
pai. Os filhos do nodo b são e, f e g; h é o pai de l e m; os filhos do mesmo pai
denominam-se irmãos.
• Ancestrais de um nodo: são todos os nodos ao longo do caminho, a partir da raiz até o
nodo.
• Descendentes de um nodo: são todos os nodos abaixo deste nodo até as folhas.
• Nível de um nó: por definição dizemos que a raiz de uma árvore encontra-se no nível 1.
Estando um nodo no nível n, seus filhos estarão no nível n+1.
• Altura ou profundidade: é definida como sendo o máximo dos níveis de todos os seus
nodos. Uma árvore nula tem altura 0. A árvore da figura mostrada tem altura igual a 4. A
subárvore com raiz d tem altura 2.
4.1.
ÁRVORES BINÁRIAS
Uma árvore binária é uma árvore que pode ser nula, ou então tem as seguintes
características:
• existe um nodo especial denominado raiz;
• os demais nodos são particionados em T1, T2 estruturas disjuntas de árvores binárias;
• T1 é denominada subárvore esquerda e T2 subárvore direita da raiz.
Árvore binária é um caso especial de árvore em que nenhum nodo tem grau superior a 2,
isto é, nenhum nodo tem mais que dois filhos. Adicionalmente, para árvores binárias existe
um “senso de posição”, ou seja, distingue-se entre uma subárvore esquerda e uma direita.
Por exemplo, as árvores binárias abaixo são consideradas distintas: a primeira tem a
subárvore direita nula e a segunda, a esquerda.
2
Centro Federal de Educação Tecnológica do Espírito Santo
Unidade de Ensino Descentralizada de Colatina
Tecnólogo em Redes de Computadores
Propriedades
1. Número máximo de nodos no nível i (i > 1) de uma árvore binária é 2(i –1).
2. Número máximo de nodos em um árvore binária de altura k é 2 k – 1.
A terminologia básica utilizada para árvores binárias é a mesma definida para árvores de
grau genérico. Podemos dizer agora filho esquerdo, filho direito, etc.
4.2.
ÁRVORES DE BUSCA BINÁRIA
Uma árvore binária, cuja raiz armazena o elemento R, é denominada árvore de busca
binária se:
•
todo elemento armazenado na subárvore esquerda é menor que R;
•
nenhum elemento armazenado na subárvore direita é menor que R;
•
as subárvores esquerda e direita também são árvores de busca binária.
A árvore abaixo é um exemplo de árvore binária.
O próximo exemplo tem o elemento d na raiz, todos os elementos na subárvore esquerda (a,
b e c) são menores que a raiz, nenhum elemento da subárvore direita (d, e, f) é menor que a
raiz e, no entanto, ela não é uma árvore de busca binária. Isto é devido ao fato de a
subárvore esquerda não ser uma árvore de busca binária. Note que a subárvore esquerda,
cuja raiz é a, tem uma subárvore esquerda que armazena um elemento maior que a raiz.
Logo, a árvore como um todo não está de acordo com a definição e não é considerada,
portanto, uma árvore de busca binária.
3
Centro Federal de Educação Tecnológica do Espírito Santo
Unidade de Ensino Descentralizada de Colatina
Tecnólogo em Redes de Computadores
Operações Básicas em Árvores de Busca Binária
Ao implementarmos árvores binárias, precisamos definir o formato dos nodos a serem
empregados na organização dos dados. Como cada nodo precisa armarzenar um elemento
e referenciar duas subárvores, podemos defini-lo como a seguir. Note que estaremos
implementando uma árvore de caracteres, por alocação dinâmica. É possível implementar
utilizando vetores, mas isto é muito pouco utilizado na prática, pelas limitações que já se
conhece na implementação por vetores e porque não é uma programação muito intuitiva.
PRINCIPAIS FUNÇÕES
CriarArvore – Inicia a árvore vazia.
ArvoreVazia – Retorna verdadeiro se a árvore estiver vazia e falso caso contrário.
CriarNodoArvore – Aloca memória para um novo nodo e retorna o seu apontador.
Inserir – Insere um elemento na árvore.
Remover – Remove um elemento na árvore.
Localizar – Localiza um elemento em uma árvore.
Imprimir – Imprime a árvore. Existem várias estratégias para imprimir uma árvore.
Destruir – Destroi a árvore, liberando a memória dos nodos.
DEFINIÇÃO
TYPE Informacao = RECORD
chave : integer;
{ ou string ou outro tipo para pesquisa }
{ demais elementos de cada nodo da árvore, por exemplo:
nome, cpf, telefone, etc ... }
END;
Arvore = ^NodoArvore;
NodoArvore = RECORD
info : Informacao;
filhoEsq : Arvore;
filhoDir : Arvore;
{ Árvore da esquerda }
{ Árvore da direita }
END;
Árvore binária (quando dissermos árvore binária estaremos sempre nos referindo a árvore
de busca binária) para nós será um ponteiro para a raiz da árvore. Como para muita
estruturas definidas anteriormente, uma árvore binária vazia será representada por uma
variável ponteiro nula. Então, se tivermos em um programa uma variável arv do tipo Arvore,
que represente uma árvore binária e os seguintes elementos nesta árvore: a, b, d, e, f e g,
teremos em memória a seguinte situação.
4
Centro Federal de Educação Tecnológica do Espírito Santo
Unidade de Ensino Descentralizada de Colatina
Tecnólogo em Redes de Computadores
4.3.
INSERÇÃO
Com relação à inserção de um elemento numa árvore binária, os novos elementos inseridos
entrarão sempre na condição de folhas, isto é, um nodo não pode entrar numa árvore e já
assumir filhos.
Para inserir o elemento c na árvore da figura acima, começamos pelo nodo raiz apontado
pela variável arv (do tipo Arvore). Como c é menor que e, tomamos a subárvore da
esquerda. Comparando com a nova raiz, temos c > b e concluímos que o elemento c deverá
ser armazenado na subárvore direita. O processo se repete até chegarmos a uma subárvore
nula. Neste ponto, uma folha é alocada (para armazenar o novo elemento) e entra como raiz
da subárvore nula. No nosso exemplo, o elemento c entraria como sub-árvore esquerda do
nodo que armazena o elemento d.
Inserir um elemento numa árvore vazia é trivial. Se a árvore não se encontra vazia e o
elemento a inserir é menor que a raiz, o inserimos na subárvore esquerda; caso contrário,
devemos inseri-lo na subárvore direita.
Antes de definirmos uma função para inserir um elemento na árvore, vamos definir as
funções CriarArvore, ArvoreVazia e CriarNodoArvore.
ALGUMAS IMPLEMENTAÇÕES:
{######
Inicializar a árvore vazia
######}
PROCEDURE CriarArvore ( VAR A : Arvore);
BEGIN
A := NIL;
{ Árvore vazia. Apontador aponta para nada}
END;
5
Centro Federal de Educação Tecnológica do Espírito Santo
Unidade de Ensino Descentralizada de Colatina
Tecnólogo em Redes de Computadores
{###### Retorna TRUE se a árvore estiver vazia
#######
e FALSE caso contrario
#####
##### }
FUNCTION ArvoreVazia ( VAR A : Arvore ) : BOOLEAN;
BEGIN
IF ( A = NIL )
THEN ArvoreVazia := TRUE
ELSE ArvoreVazia := FALSE;
END;
{######
{######
{######
Aloca memória para um determinado NODO e
######}
retorna o apontador para este NODO. Se não for possível }
alocar memória, retorna NIL
######}
FUNCTION CriarNodoArvore ( e : informacao ) : Arvore;
VAR aux : Arvore;
BEGIN
IF MaxAvail < SIZEOF(NodoArvore) { ** Testa se existe memória *}
THEN BEGIN
WRITELN(‘Não existe memória suficiente’);
CriarNodoArvore := NIL;
END
ELSE BEGIN
NEW(aux);
aux^.info := e;
{** Copiar todo o registro de E para INFO}
CriarNodoArvore := aux;
END;
END;
Passemos agora a uma função para inserir um elemento em uma árvore binária.
{###### Insere um elemento ‘novo’ em um árvore binária ‘arv’ ######}
PROCEDURE Inserir ( VAR arv : Arvore; novo : Arvore );
BEGIN
{ se arvore for VAZIA, então a inserção é direta }
IF ( ArvoreVazia(arv) )
THEN BEGIN
arv := novo;
novo^.filhoEsq := NIL;
novo^.filhoDir := NIL;
END
ELSE BEGIN { se nao for vazia, então insere em um dos filhos }
{ verifica em qual arvore será inserido: na subarvore }
{ esquerda ou direita }
IF (novo^.info.chave < arv^.info.chave)
THEN Inserir(arv^.filhoEsq,novo)
ELSE Inserir(arv^.filhoDir,novo);
END;
END;
6
Centro Federal de Educação Tecnológica do Espírito Santo
Unidade de Ensino Descentralizada de Colatina
Tecnólogo em Redes de Computadores
4.4.
ATRAVESSAMENTO EM ÁRVORES BINÁRIAS
Atravessar uma árvore binária significa passar de forma sistemática, por cada um de seus
nodos, realizando um certo processamento. Os tipos mais conhecidos de atravessamento
são três:
•
em-ordem
•
pré-ordem
•
pós-ordem
No atravessamento em-ordem visitamos primeiro o lado esquerdo, depois a raiz e
finalmente o lado direito. No pré-ordem, visitamos a raiz e então o lado esquerdo e direito
repectivamente. Finalmente, no pós-ordem, o lado esquerdo e direito são, nesta ordem,
visitados e só então visitamos a raiz.
Para entender melhor cada tipo, vamos a um exemplo simples. Seja a árvore abaixo.
De acordo com o explicado acima, os elementos desta árvore seriam acessados na seguinte
ordem (de acordo com os três tipos de atravessamento):
•
em-ordem: a, b, c
•
pré-ordem: b, a, c
•
pós-ordem: a, c, b
Note que estes tipos de atravessamento servem para árvores binárias de uma forma geral.
No exemplo dado, temos uma árvore de busca binária. Nestes tipos de árvore binária, o
atravessamento em-ordem corresponde a um acesso na ordem crescente dos elementos.
Vamos a um exemplo um pouco maior.
7
Centro Federal de Educação Tecnológica do Espírito Santo
Unidade de Ensino Descentralizada de Colatina
Tecnólogo em Redes de Computadores
Caso fosse aplicado o atravessamento em-ordem para a árvore acima, os nodos seriam
visitados na ordem: a, b, c, d, e, f. Para obter esta sequência, é fácil perceber que primeiro
aplicamos o atravessamento em-ordem na subárvore esquerda (da raiz de d), visitando
portanto os elementos a, b, c. Depois, visitamos o elemento d (a raiz) e finalmente,
aplicamos o atravessamento em-ordem na subárvore direita, visitando portanto, os elemento
e e f.
Percemos então que, mais uma vez, podemos produzir um algoritmo recursivo para realizar
atravessamentos. No caso do algoritmo para atravessamento em-ordem, seja uma árvore
arv, teremos então o seguinte:
•
arv é uma árvore nula: não há nada a fazer;
•
Realizamos o atravessamento em-ordem da subárvore esquerda da raiz de arv;
•
Visitamos o elemento da raiz;
•
Realizamos o atravessamento em-ordem da subárvore direita da raiz de arv;
Os algoritmos para atravessamento pré-ordem e pós-ordem podem ser generalizados
segundo o mesmo raciocínio, só muda a ordem das ações.
Um uso muito comum de atravessamento é para imprimir os elementos de uma árvore.
Vamos então codificar funções para imprimir os elementos de uma árvore de busca binária.
Vejamos as três funções abaixo, uma para cada tipo de atravessamento.
{###### Imprime a árvore EM-ORDEM ######}
PROCEDURE ImprimirEmOrdem ( VAR arv : Arvore );
BEGIN
IF ( NOT ArvoreVazia(arv) )
THEN BEGIN
ImprimirEmOrdem(arv^.filhoEsq);
write(arv^.info.chave);
{ Imprimir todos os campos}
ImprimirEmOrdem(arv^.filhoDir);
END;
END;
{###### Imprime a árvore PRE-ORDEM ######}
PROCEDURE ImprimirPreOrdem ( VAR arv : Arvore );
BEGIN
IF ( NOT ArvoreVazia(arv) )
THEN BEGIN
write(arv^.info.chave);
{ Imprimir todos os campos }
ImprimirPreOrdem(arv^.filhoEsq);
ImprimirPreOrdem(arv^.filhoDir);
END;
END;
8
Centro Federal de Educação Tecnológica do Espírito Santo
Unidade de Ensino Descentralizada de Colatina
Tecnólogo em Redes de Computadores
{###### Imprime a árvore POS-ORDEM ######}
PROCEDURE ImprimirPosOrdem ( VAR arv : Arvore );
BEGIN
IF ( NOT ArvoreVazia(arv) )
THEN BEGIN
ImprimirPosOrdem(arv^.filhoEsq);
ImprimirPosOrdem(arv^.filhoDir);
write(arv^.info.chave);
{ Imprimir todos os campos }
END;
END;
Começamos a falar de atravessamento, dizendo que "atravessar uma árvore é passar
(visitar) por todos os seus nodos realizando, para cada um deles, um certo processamento".
Vimos funções que utilizam esses atravessamentos para imprimir elementos de uma árvore
de busca binária, entretanto qualquer outra tarefa, que se aplique a uma determinada
necessidade, pode ser feita. Por exemplo, poderíamos criar uma função para desalocar
todos os nodos de uma árvore de busca binária, quando essa não fosse mais necessária no
programa. Esta função utilizará um atravessamento pós-ordem. Vejamos abaixo.
{###### Destrói a árvore desalocando todos os seus nodos ######}
PROCEDURE Destruir( VAR arv : Arvore );
BEGIN
IF ( NOT ArvoreVazia(arv) )
THEN BEGIN
Destruir(arv^.filhoEsq);
Destruir(arv^.filhoDir);
DISPOSE(arv);
END;
END;
4.5.
LOCALIZAR UM ELEMENTO EM ÁRVORES BINÁRIAS
Se o elemento a ser localizado não se encontra na raiz da árvore, procuraremos este
elemento na sub-árvore esquerda caso ele seja menor que a raiz, e na sub-árvore direita
caso contrário. Portanto a nossa função seria:
{##### Localizar um elemnto em uma árvore. Retorna o ponteiro #####}
{##### para o elemento se encontrar e NIL caso contrário
#####}
FUNCTION Localizar( VAR arv : Arvore; e : TipoDaChave) : Arvore;
BEGIN
IF (ArvoreVazia(arv) )
THEN Localizar := NIL
ELSE BEGIN
IF (e = arv^.info.chave)
THEN Localizar := arv
ELSE IF (e < arv^.info.chave )
THEN Localizar := Localizar(arv^.filhoEsq,e)
ELSE Localizar := Localizar(arv^.filhoDir,e);
END;
END;
9
Centro Federal de Educação Tecnológica do Espírito Santo
Unidade de Ensino Descentralizada de Colatina
Tecnólogo em Redes de Computadores
4.6.
REMOÇÃO
O algoritmo para remoção em árvore de busca binária é talvez o mais trabalhoso que temos.
Ainda sim, após analisarmos todos os possíveis casos, ele parecerá bastante simples. Para
facilitar o entendimento, vamos admitir inicialmente que o elemento a ser removido
encontra-se na raiz da árvore de busca binária arv. Nestas condições, temos três
possibilidades:
1. a raiz não possui filhos: a solução é trivial. Podemos remover a raiz e anular arv;
2. a raiz possui um único filho: podemos remover o nodo raiz, substituindo-o pelo seu
nodo-filho;
3. a raiz possui dois filhos: não é possível que os dois filhos assumam o lugar do pai.
Escolhemos então o nodo que armazena o menor elemento na subárvore direita de arv.
Para entender a solução adotada para o terceiro caso, precisamos lembrar da definição de
árvore de busca binária. Segundo esta definição, não podemos ter elementos maiores nem
iguais à raiz numa subárvore esquerda. Também não podemos ter elementos menores que
a raiz numa subárvore direita. Ora, se tomamos o menor elemento da subárvore direita e o
posicionamos na raiz, então continua valendo a definição.
Sabemos que o menor elemento numa árvore de busca binária encontra-se no nodo que
ocupa a posição mais à esquerda possível. Este nodo certamente não terá um filho
esquerdo pois se o tivesse não seria o menor da árvore. Pode, entretanto, ter um filho direito
(maior ou igual).
Primeiro, desenvolveremos uma função que procura na árvore, o nodo que armazena o seu
menor elemento, desliga-o da árvore e retorna um poteiro para este nodo. Lembrando que
se este nodo tiver um filho direito, este tomará o lugar do pai.
{##### Funcao que procura o menor nodo e o retira da arvore #####}
FUNCTION TiraMenor(VAR arv : Arvore) : Arvore;
VAR aux : Arvore;
BEGIN
IF ( ArvoreVazia(arv^.filhoEsq) )
THEN BEGIN
aux := arv;
arv := arv^.filhoDir;
aux^.filhoDir := NIL;
TiraMenor := aux;
END
ELSE TiraMenor := TiraMenor(arv^.filhoEsq));
END;
Tendo implementado a função TiraMenor, podemos remover facilmente um nodo que tenha
dois filhos. Observe a árvore do desenho abaixo:
10
Centro Federal de Educação Tecnológica do Espírito Santo
Unidade de Ensino Descentralizada de Colatina
Tecnólogo em Redes de Computadores
Se quiséssemos remover o nodo raiz desta árvore bastariam as linhas de código abaixo:
VAR pn : Arvore;
BEGIN
. . .
pn := TiraMenor(arv^.filhoDir);
arv^.info = pn^.info;
DISPOSE(pn);
. . .
END;
Para entender o que a linha aux := TiraMenor(arv^.filhoDir); faz, observe o
desenho abaixo:
Abaixo mostramos a função para realizar a remoção de um elemento da árvore. Sempre
testamos se a raiz da árvore é o elemento a ser removido. Se for, basta abranger as três
11
Centro Federal de Educação Tecnológica do Espírito Santo
Unidade de Ensino Descentralizada de Colatina
Tecnólogo em Redes de Computadores
possibilidades mostradas em parágrafos anteriores. Se não, chamamos a mesma função
recursivamente, até encontrar o elemento como sendo raiz de alguma subárvore.
{ ##### Procedimeno para remover um nodo da arvore. ##### }
PROCEDURE Remover ( VAR arv : Arvore; e : informacao);
VAR pn : Arvore;
BEGIN
IF ( NOT ArvoreVazia(arv) )
THEN BEGIN
IF ( arv^.info.chave = e.chave)
THEN BEGIN
pn := arv;
IF ( ArvoreVazia(arv^.filhoEsq) )
THEN arv := arv^.filhoDir
ELSE IF ( ArvoreVazia(arv^.filhoDir) )
THEN arv := arv^.filhoEsq
ELSE BEGIN
pn := TiraMenor(arv^.filhoDir);
arv^.info := pn^.info;
END;
DISPOSE(pn);
END
ELSE IF ( e.chave < arv^.info.chave )
THEN Remover(arv^.filhoEsq, e)
ELSE Remover(arv^.filhoDir, e);
END;
END;
Com todas as funções vistas até agora já podemos realizar testes. Abaixo é dado o progrma
principal de exemplo que utiliza tudo que falamos até agora. Junte todas as funções que
vimos até agora e experimente executar o programa exemplo abaixo.
VAR arv, aux : Arvore;
e : Informacao;
BEGIN
ClrScr;
CriarArvore(arv);
e.chave := 'H';
Inserir(arv,CriarNodoArvore(e));
e.chave := 'B';
Inserir(arv,CriarNodoArvore(e));
e.chave := 'A';
Inserir(arv,CriarNodoArvore(e));
IF ( localizar(arv,'C') = NIL )
THEN writeln('Nao achou');
imprimiremordem(arv);
writeln;
imprimirpreordem(arv);
12
Centro Federal de Educação Tecnológica do Espírito Santo
Unidade de Ensino Descentralizada de Colatina
Tecnólogo em Redes de Computadores
writeln;
imprimirposordem(arv);
writeln;
e.chave := 'B';
Remover(arv,e);
imprimiremordem(arv);
Destruir(arv);
readln;
end.
4.7.
EXERCÍCIOS
1. Considere a árvore esquematizada a seguir, determine:
a) O grau de cada nodo;
b) O grau da árvore;
c) Quais são os nodos folhas;
d) Os filhos de C;
e) Os ancestrais de B, G e I;
f) O nível e altura do nodo F;
g) O nível e altura do nodo A;
h) A altura da árvore.
2. Seja uma árvore de busca binária inicialmente vazia, esquematize o seu estado final,
após terem sido inseridos os elementos 4, 1, 0, 5, 3, 7, 2, 6, 9 e 8, nesta ordem.
3. Informe o resultado após utilizar os seguintes atravessamentos na árvore do exercício 1.
a) em-ordem;
b) pós-ordem;
c) pré-ordem.
4. Descreva como seria o algoritmo para contar o número de nodos não-terminais numa
árvore de busca binária.
5. Escrever uma função que calcule a altura de uma árvore binária dada. A altura de uma
árvore é igual ao máximo nível de seus nós.
6. Faça uma função que retire o maior elemento de uma árvore de busca binária.
13
Centro Federal de Educação Tecnológica do Espírito Santo
Unidade de Ensino Descentralizada de Colatina
Tecnólogo em Redes de Computadores
Respostas:
5)
FUNCTION CalculaAltura (VAR arv : Arvore);
VAR x,y : INTEGER;
BEGIN
IF ( ArvoreVazia(arv) )
THEN CalculaAltura := 0
ELSE BEGIN
x := CalculaAltura(arv^.filhoEsq);
y := CalculaAltura(arv^.filhoDir);
IF ( x > y )
THEN CalcularAltura := 1 + x
ELSE CalcularAltura := 1 + y;
END;
END;
14
Download

PROGRAMAÇÃO II 4. ÁRVORE