Tópicos Avançados em Estrutura de Dados 6º Período Ciência da Computação Uma Aplicação de Árvores Binárias Árvores Binárias - continuação As árvore binárias são estruturas importantes toda vez que uma decisão binária deve ser tomada em algum ponto de um algoritmo. Vamos agora, antes de passar a algoritmos mais complexos, mostrar uma aplicação simples de árvores binárias. Suponhamos que precisamos descobrir números duplicados em uma lista não ordenada de números. Uma maneira é comparar cada novo número com todos os números já lidos. Isto aumenta em muito a complexidade do algoritmo. Outra possibilidade é manter uma lista ordenada dos números e a cada número lido fazer uma busca na lista. Outra solução é usar uma árvore binária para manter os números. O primeiro número lido é colocado na raiz da árvore. Cada novo número lido é comparado com o elemento raiz, caso seja igual é um valor duplicado e volta-se a ler outro número. Se é menor repete-se o processo com a árvore da direita e se maior com a árvore da esquerda. Este processo continua até que um valor duplicado é encontrado ou uma árvore vazia é achada. Neste caso, o número é inserido na posição devida na árvore. Considere que os números 7 8 2 5 8 3 5 10 4 foram fornecidos pelo usuário, neste caso a árvore binária mostrada na Figura 1 seria construida. Figura 1 O programa abaixo mostra este algoritmo. O programa insere os nós na árvore e imprime uma mensagem caso seja fornecido um número que já foi lido antes. /* programa arv0300.c */ #include<stdio.h> #include<stdlib.h> #include<string.h> typedef struct stno int info; struct stno *esq, *dir; tno; tno *cria_arvore( int ); tno *cria_no( ); void pos_esq (tno *, int );
void pos_dir (tno *, int ); int main() tno *raiz, *p, *q; char linha[80], *numero; int num; printf ( Entre com os elementos da lista, separados por espaço: ); gets(linha); numero = strtok(linha, " "); /* pega o primeiro numero da lista */ num = atoi(numero); raiz = cria_arvore(num); /* insere na raiz */ numero = strtok(null, " "); printf("li numero %d\n", num); /* le novo numero */ while(numero) q = raiz; p = raiz; num = atoi(numero); printf("li numero %d\n", num); /* le novo numero */ while (num!= p->info && q) /* procura na arvore */ p = q; if (num < p->info) q = p->esq; /* passa para arvore esquerda */ q = p->dir; /* passa para direita */ if (num == p->info) printf("o numero %d ja existe na arvore.\n", num); /* vou inserir o numero na arvore */ if (num < p->info) pos_esq(p, num); pos_dir(p, num); numero = strtok(null, " "); /* fim do while (numero) */ getch(); return(0); tno *cria_arvore (int x) tno *p; p = cria_no (); if (p) p->info = x; return p; puts("faltou espaco para alocar no."); exit(1); tno *cria_no() tno *p; if ((p = (tno *) malloc(sizeof(tno))) == NULL) return NULL; p->esq = NULL; p->dir = NULL; return p;
void pos_esq(tno *p, int x) tno *q; if (p->esq) puts("operacao de insercao a esquerda ilegal."); q = cria_arvore(x); p->esq = q; void pos_dir(tno *p, int x) tno *q; if (p->dir) puts("operacao de insercao a direita ilegal."); q = cria_arvore(x); p->dir = q; Programa de Percurso - versão recursiva O programa abaixo captura letras informadas pelo teclado e ao final informa o percurso da árvores nos 3 modos: Pré, In e Pós-ordem. // Exemplo de Percuros em Arvores Binarias #include <conio.h> #include <stdio.h> #include <stdlib.h> struct arvore *coloca(struct arvore *atual,char quem,char ld); void construir(struct arvore *filho,char lado,char letra); void mostrar_preordem (arvore *p); void mostrar_inordem (arvore *p); void mostrar_posordem (arvore *p); struct arvore char dado; struct arvore *esq; struct arvore *dir; *Raiz = NULL; main () // Programa Principal char op, letra; construir(raiz,'r',' '); if (Raiz!= NULL) printf("\n\narvore - Pre-Ordem\n");
mostrar_preordem(raiz); printf("\n\narvore - In-Ordem\n"); mostrar_inordem(raiz); printf("\n\narvore - Pos-Ordem\n"); mostrar_posordem(raiz); printf("\a\n\na Arvore esta vazia\n"); getche(); void construir(struct arvore *filho,char lado,char letra) char entra; struct arvore *onde; if (lado == 'r') printf("\ninforme a raiz: "); if (lado == 'e') printf("\ninforme o filho esquerdo de %c: ",letra); printf("\ninforme o filho direito de %c: ",letra); entra = getche(); if (entra!= 13) onde = coloca(filho,entra,lado); construir(onde,'e',entra); construir(onde,'d',entra); struct arvore *coloca(struct arvore *atual,char quem,char ld) struct arvore *alocou; alocou = (struct arvore*) malloc(sizeof(struct arvore)); (*alocou).dado = quem ; (*alocou).esq = NULL; (*alocou).dir = NULL; if (!Raiz) Raiz = alocou ; if (ld == 'e') (*atual).esq = alocou; (*atual).dir = alocou; return alocou; void mostrar_preordem (arvore *p) if (p == NULL) return; printf("%c ",p->dado); mostrar_preordem(p->esq);
mostrar_preordem(p->dir); void mostrar_inordem (arvore *p) if (p == NULL) return; mostrar_inordem(p->esq); printf("%c ",p->dado); mostrar_inordem(p->dir); void mostrar_posordem (arvore *p) if (p == NULL) return; mostrar_posordem(p->esq); mostrar_posordem(p->dir); printf("%c ",p->dado); A seguir, tem-se dois exemplos de entrada e saída de dados referente ao programa :
Programa Catálogo de clientes Uma empresa que produz catálogos telefônicos precisa, a cada ano, inserir novos nomes de assinantes de forma a manter o catálogo classificado em ordem alfabética. Implemente um programa com funções para: Inserir novos nomes de assinantes, e Mostrar a relação de assinantes em ordem alfabética. // prog3.cpp - Exemplo Arvore Binária #include <conio.h> #include <stdio.h> #include <stdlib.h> #include <string.h> struct arvore char nome[10]; struct arvore *pai; struct arvore *esq; struct arvore *dir; *Raiz = NULL; void incluir_nome (char novo[]) arvore *p, *q, *r; p = ( arvore *) calloc(1,sizeof( arvore)); strcpy(p->nome,novo); p->esq=null; p->dir=null; if (Raiz == NULL)
Raiz = p; q=raiz; while (q!= NULL) r=q; if (strcmp(novo,q->nome)<= 0) q=q->esq; q=q->dir; if (strcmp(novo,r->nome) <= 0) r->esq=p; r->dir=p; p->pai=r; void mostrar_arvore (arvore *p) if (p == NULL) return; mostrar_arvore(p->esq); printf("\n%s",p->nome); mostrar_arvore(p->dir); main () char op, novo[10]; do printf("nome -> "); fflush(stdin); gets(novo); if (strcmp(novo,"fim") == 0) break; incluir_nome(novo); while (1); printf("relacao de assinantes:\n"); mostrar_arvore(raiz); printf("\n"); getche(); Exemplo de entrada e saída de dados referente ao programa:
ÁRVORES BINÁRIAS DE PESQUISA Um tipo especial de árvore binária é a Árvore Binária de Pesquisa (ABP). Neste tipo de árvores cada nodo tem obrigatoriamente um campo denominado chave que permite impor uma ordenação na árvore que satisfaz a seguinte propriedade. Seja x um nodo numa ABP. Se y é um nodo na subárvore esquerda de x então chave[y] <= chave[x] Se y é um nodo na subárvore direita de x então chave[y] >= chave[x] Exemplos: Figura 2 Observe que o campo chave pode ser qualquer tipo de dado que possa ser ordenado. Outra maneira de guardar essa regra é a seguinte: todos os nós das subárvores da esquerda tem valores da chave menor que o valor da chave raiz e todos os nós das subárvores da direita tem valores da chave maior que o valor da chave raiz. Essa regra é válida recursivamente para todas as subárvores da árvore, ou seja, toda as subárvores da esquerda do nó chave em questão deverão ter valores menores ou iguais e todas as chaves das subárvores da direita deverão ter valores maiores ou iguais. Veja os exemplos das Figura 2 e 3. Figura 3 Exercício: 1) Trace ABP(s) de profundidade 2,3,4,5 e 6 sobre o conjunto de chaves 1,4,5,10,16,17,21 2) Das árvores abaixo, qual(is) está(ão) errada(s) e não pode(m) ser considera(s) com ABP(s)? Justifique sua resposta? G G 15 C J C J 6 18 A F N A H N 3 7 17 20 D G M D G M 2 4 13 (A) (B) (C)
Inserção de informação numa ABP O procedimento de inserção numa ABP deve se encarregar de manter a propriedade de ordenação da árvore, ou seja, a inclusão de uma nova informação deverá encontrar a posição correta para inserção de maneira a não violar a ordenação. Outra particularidade é que não podemos inserir informação com chave duplicada, ou seja, caso a chave associada à informação a ser inserida já existir na árvore ela não é inserida. Algoritmo de inserção: função insere(t,v): //insere um valor v numa arvore T y = null x = raiz[t] enquanto x!= nulo //enquanto x não nulo faça y = x //y recebe x se chave[v] < chave[x] então x = esquerda[x] senão x = direita[x] pai[v] = y se y = nulo então raiz[t] = v //arvore estava vazia senão se chave[v] < chave[y] então esquerda[y] = v senão direita[y] = v Pesquisa numa ABP Para encontrarmos se uma determinada informação I está armazenada numa ABP, comparamos o valor da chave da informação com o valor da chave armazenado na raiz da árvore. Se a chave de I é menor sabemos que I só pode estar na subárvore esquerda; se a chave de I é maior, sabemos que I só pode estar na subárvore direita. Algoritmo de busca: função busca(n,v): //n = nó (primeira vez é a raiz), v = valor a pesquisar se n = nulo ou v = chave[n] então retorna n se v < chave[n] então retorna busca(esquerda[n],v) senão retorna busca(direita[n],v) Removendo informação de uma ABP Esta é uma operação que apresenta certa dificuldade, pois pode ser necessário, em alguns casos, efetuar uma reestruturação de grande parte da árvore. Devemos considerar dois casos distintos: O nodo a ser removido possui uma ou ambas as subárvores vazias. Neste caso a remoção é simples. Se ambas as subárvores são vazias o nodo é simplesmente removido. Se apenas uma delas não é vazia, fazemos a raiz do nodo sendo removido ser a nova raiz da subárvore não vazia. Veja as ilustrações abaixo:
Figura 4 Nenhuma das subárvores do nodo é vazia. Uma solução simples é remover o nodo que contém a chave seguinte na ordem para a qual a árvore está organizada e reinseri-lo no lugar do nodo que se deseja realmente remover. O nodo que contém o símbolo seguinte é aquele que contém a menor chave da subárvore da direita. Veja o exemplo abaixo: Figura 5 Algoritmo de remoção função remove(y): //recebe a árvore cuja raiz deseja excluir e retorna a árvore resultante se esquerda[y] = nulo então c = direita[y] senão c = esquerda[y] enquanto direita[c]!= nulo faça c = direita[c] //c é a célula anterior a y na ordem e-r-d fimenquanto a = pai[c] se a!= y então b = esquerda[c] direita[a] = b pai[b] = a esquerda[c] = esquerda[y] pai[esquerda[y]] = c fimse direita[c] = direita[y] pai[direita[y]] = c fimse pai[c] = nulo retorna c
Exercício: 3) Trace uma ABP com o conjunto de chaves 10,30,1,12,45,2 e depois demonstre como fica essa mesma ABP removendo a chave 30. 4) A partir da ABP que é data em pré-ordem pelas chaves 4,1,10,5,17,16,21, trace a nova ABP com a exclusão das chaves 10 e 21. 5) Trace a nova ABP a partir da remoção da chave 12 da ABP ao lado. Referências Bibliográficas: Algoritmos e Estruturas de Dados III UPF: http://www.eduardostefani.eti.br/bennett/algoritmos2/algoritmos-estrutura-dados.pdf WALTER,Marcelo: http://www.inf.unisinos.br/~marcelow/ensino/grad/lab2/ PIMENTEL, Graça: http://www.icmc.sc.usp.br/~sce182/arvbinrb.html CORMEN, Thomas H. et al. Algoritmos: teoria e prática. Rio de Janeiro: Campus, 2002. TENEMBAUM, Aeron. Estruturas de Dados usando C. São Paulo: Makron Books, 1995. Árvores Binárias PUC PR: http://www.ppgia.pucpr.br/~laplima/aulas/materia/arvbin.html CRUZ, Adriano. Árvores. 2000. http://equipe.nce.ufrj.br/adriano/c/apostila/arvore.htm MINETTO, Elton Luis. Apostila de Árvores Binárias. Unochapecó, 2005