Estruturas de Dados Prof. Gustavo Willam Pereira Créditos: Profa. Juliana Pinheiro Campos
Árvores Conceitos Árvores binárias Árvores binárias de pesquisa Árvores binárias balanceadas
Árvores ESTRUTURAS DE DADOS Estruturas de dados adequadas para a representação de hierarquias. Exemplo: sistemas de arquivos em um computador (armazenados em diretórios e subdiretórios);
Árvores ESTRUTURAS DE DADOS Uma árvore A é um conjunto finito de 0 ou mais elementos denominados nós. Existe um nó r denominado raiz da árvore.
Árvores ESTRUTURAS DE DADOS Raiz da árvore Nó raiz Nós
Árvores ESTRUTURAS DE DADOS A forma mais natural de definir uma estrutura de árvore é usando a recursividade. O nó r (raiz da árvore) contém 0 ou mais subárvores, cujas raízes são ligadas diretamente a r. Cada nó ligado diretamente a r é raiz de uma subárvore. Todo nó de uma árvore, é raiz de uma subárvore.
Árvores ESTRUTURAS DE DADOS Raiz da árvore Subárvore A1 Nó raiz... Nós
Árvores ESTRUTURAS DE DADOS Raiz da árvore Subárvore A1 Nó raiz... Nós Subárvore A2
Árvores ESTRUTURAS DE DADOS Raiz da árvore Subárvore A1 Nó raiz... Nós Subárvore A2 Subárvore A3
Árvores ESTRUTURAS DE DADOS Estrutura de árvore Nó raiz... Nó raiz... Subárvores
Árvores ESTRUTURAS DE DADOS O número de subárvores de um nó é denominado grau do nó. O grau da árvore é definido como o maior grau dentre todos os seus nós. Nó raiz... Grau do nó raiz = Grau do nó x = 2 Grau da árvore = 3 3
Árvores ESTRUTURAS DE DADOS Uma árvore com 0 (zero) nós é uma árvore vazia. Os nós raízes das subárvores são ditos filhos do nó pai (r); Nós internos: nós que possuem filhos (nós de grau > 0) Nós externos (folhas): nós que não possuem filhos (nós de grau 0).
Árvores ESTRUTURAS DE DADOS Nó raiz Nós pais...
Árvores ESTRUTURAS DE DADOS Nó raiz... Nós filhos
Árvores ESTRUTURAS DE DADOS Nó raiz Nós internos...
Árvores ESTRUTURAS DE DADOS Nó raiz... Nós externos (folhas)
Árvores ESTRUTURAS DE DADOS Em uma árvore nunca pode ocorrer ciclo. O número de filhos permitido por nó e as informações armazenadas em cada nó diferenciam os vários tipos de árvores existentes.
Árvores binárias Em uma árvore binária, cada nó tem 0 (zero), 1 (um) ou 2 (dois) filhos. Todos os nós de uma árvore binária tem grau menor ou igual a 2. Nó raiz
Árvores binárias Definição recursiva de uma árvore binária: Uma árvore vazia; ou Um nó raiz tendo duas subárvores, identificadas como subárvore da direita (SAD) e a subárvore da esquerda (SAE).
Árvores binárias Exemplo de utilização de árvores binárias: avaliação de expressões aritméticas. Os nós folhas representam os operandos. Os nós internos representam os operadores. Expressão: (3 + 6) * (4-1) + 5
Árvores binárias ESTRUTURAS DE DADOS Exemplo de utilização de árvores binárias: avaliação de expressões. Expressão: (3 + 6) * (4-1) + 5 Raiz: Nó + Expressão = (SAE Nó +) + (SAD Nó +) = ((SAE Nó *) * (SAD Nó *)) + 5 = (((SAE Nó +) + (SAD Nó +)) * ((SAE Nó -) (SAD Nó -))) + 5 = (3 + 6) * (4-1) + 5
Árvores binárias Notação textual para descrever árvores binárias: Árvore vazia: < > Árvores não vazias: <raiz SAE SAD> <raiz SAE SAD> a <a <b < > < >> <c < > < >> > b c
Árvores binárias Uma subárvore é especificada como sendo a SAE ou SAD de uma árvore maior, e qualquer uma das duas subárvores pode ser vazia. b a a b As árvores acima são distintas!
Árvores binárias Qual a notação textual descreve a árvore binária abaixo? a b c d e f <a <b <> <d <> <>>> <c <e <> <>> <f <> <> > > >
Árvores binárias Implementação de uma árvore binária em C para armazenar inteiros. As funções que manipulam árvores são, em geral, implementadas de forma recursiva, por meio da definição recursiva da estrutura. Que estrutura podemos usar para representar um nó da árvore?
Árvores binárias Estrutura para representar um nó da árvore binária: typedef struct no { int item; // armazena a informação struct no* esq; // ponteiro para sae struct no* dir; // ponteiro para sad }No; typedef No* NodePtr; //define um apontador para nó A estrutura da árvore é representada por um ponteiro para o nó raiz. typedef struct arvore { No* raiz; }Arvore;
Árvores binárias Operações que serão implementadas: Função para criar uma árvore vazia. Função para inserir e remover elementos da árvore Função que verifica se uma árvore está vazia. Função que verifica se uma determinada informação está armazenada (pertence) à árvore. Função para imprimir as informações contidas na árvore. Função para liberar memória alocada dinamicamente.
Árvores binárias Interface do tipo árvore: Arvore* criararvore() int estavazia(no* p) No* find(int it, No* p) void insert(int it, NodePtr *p) void print(no* p) void remover(int it, NodePtr *p)
Árvores binárias de busca (ou de pesquisa) São árvores projetadas para dar suporte a operações de busca de forma eficiente. Árvores binárias de busca combinam as vantagens de 2 estruturas: Vetor ordenado (busca eficiente); Lista encadeada (inserir e remover elementos de forma eficiente). Armazena elementos que tem uma relação de ordem (>, <, =) entre si.
Árvores binárias de busca (ou de pesquisa) As árvores binárias de busca possuem uma propriedade fundamental: O valor associado à raiz é sempre maior do que o valor associado a qualquer nó da subárvore à esquerda (sae) e é sempre menor do que o valor associado a qualquer nó da subárvore à direita (sad). Essa propriedade garante que, quando a árvore é percorrida em ordem simétrica (sae, raiz, sad), os valores são encontrados em ordem crescente.
Árvores binárias de busca (ou de pesquisa) 6 2 8 Mostre a impressão dos valores da arvore caso fosse utilizada a ordem simétrica. 1 4 1 2 3 4 6 8 3
Árvores binárias de busca (ou de pesquisa) Uma variação possível permite que haja repetição de valores na árvore: o valor associado à raiz é sempre maior que o valor associado a qualquer nó da sae, e é sempre menor ou igual ao valor associado a qualquer nó da sad.
Árvores binárias de busca (ou de pesquisa) 6 1 2 8 4 Como a busca de um valor pode ser feito nessa árvore? Comparamos o valor buscado com a raiz. Se for igual, encontramos o valor buscado. Se for menor, a busca continua na sae; caso contrario, a busca continua na sad.. 3
Árvores binárias de busca (ou de pesquisa) Procedimento Pesquisa para uma árvore binária de busca : Para encontrar um valor x, primeiro compare-o com o valor que está na raiz. Se x é menor do que o valor que está na raiz, vá para a sae; se x é maior, vá para a sad. Repita o processo recursivamente, até que o valor procurado seja encontrado ou então um nó folha seja atingido.
Operações em árvores binárias de busca (ou de pesquisa) As funções que vamos analisar são: Busca: busca um elemento na árvore; Insere: insere um novo elemento na árvore na posição correta para que a propriedade fundamental seja mantida; Retira: retira um elemento da árvore mantendo a propriedade fundamental;
Árvores binárias Função que cria uma árvore vazia: Arvore* criararvore() { Arvore* a; a = (Arvore*) malloc (sizeof(arvore)); a->raiz = NULL; return a; }
Árvores binárias Crie uma estrutura que represente a árvore descrita pela notação textual abaixo: <a <b <> <d <> <>>> <c <e <> <>> <f <> <> > > >
Árvores binárias Função que verifica se a árvore está vazia int estavazia(no* p) { if (p == NULL) return 0; else return 1; }
Árvores binárias A função que imprime a árvore percorre todos os nós imprimindo sua informação. Se a árvore não é vazia, imprimimos a informação associada à raiz e chamamos (recursivamente) a função para imprimir as subárvores.
Árvores binárias Função que imprime a árvore: void imprime(no* p) { if (p!= NULL) { imprime (p->esq); printf("item = %d \n", p->item); imprime (p->dir); } } Como alterar essa função para que ela imprima o conteúdo da árvore na notação textual estudada?
Árvores binárias ESTRUTURAS DE DADOS Função que imprime a árvore: void print_textual(no* p) { printf("<"); if (p!= NULL) { printf(" %d ", p->item); print_textual(p->esq); print_textual(p->dir); } printf(">"); } Imprime usando a notação textual apresentada.
Operações em árvores binárias de busca (ou de pesquisa) Inserção: uma subárvore vazia atingida indica o ponto de inserção. Se a árvore for vazia, deve ser substituída por uma árvore cujo único nó contém o valor v; Se a árvore não for vazia, compara v com a raiz e inserimos v na sae ou na sad, conforme o resultado da comparação. É necessário atualizar os ponteiros para as subarvores à esquerda ou à direita, pois a função pode alterar o valor do ponteiro para a raiz da (sub) árvore.
Árvores binárias ESTRUTURAS DE DADOS Função para inserir um novo nó na árvore: void insert(int it, NodePtr *p) { if (*p == NULL) { No* tmp = (No*) malloc (sizeof(no)); tmp->item = it; tmp->esq = NULL; tmp->dir = NULL; *p = tmp; } else if (it < (*p)->item) insert(it, &(*p)->esq); else if (it > (*p)->item) insert(it, &(*p)->dir); else printf("erro: Registro já existente na } arvore.");
Operações em árvores binárias de busca (ou de pesquisa) Remoção: Se a árvore for vazia, nada deve ser feito; Se a árvore não for vazia, compara v com a raiz e retiramos v na sae ou na sad, conforme o resultado da comparação. Ou retira elemento se for encontrado. Existem 3 situações possíveis: Retirar elemento que é folha Quando elemento possui um único filho Quando o elemento a ser retirado possui 2 filhos
Operações em árvores binárias de busca (ou de pesquisa) Remoção: Retirar elemento que é folha 6 2 8 Como fazer para retirar o nó 3? Basta retirar o elemento da árvore e atualizar o pai. 1 4 X3
Operações em árvores binárias de busca (ou de pesquisa) Remoção: Quando elemento possui um único filho 6 1 2 8 X4 Como fazer para retirar o nó 4? É necessário acertar o ponteiro do pai, fazendo o único neto ser seu filho direto. Em seguida, retira-se o elemento da árvore. 3
Operações em árvores binárias de busca (ou de pesquisa) Remoção: Quando o elemento a ser retirado possui 2 filhos. 6 2 8 Como fazer para retirar o nó 6? 1 4 3
Operações em árvores binárias de busca (ou de pesquisa) Remoção: quando elemento possui 2 filhos Encontramos o elemento que precede o elemento a ser retirado na ordenação (o elemento mais à direita da subárvore a esquerda); Trocamos a informação com o nó encontrado; Retiramos da subárvore à esquerda (recursivamente) o nó encontrado (que agora contém o valor que se quer retirar). 6 2 8 4 2 8 4 2 8 1 4 1 6 1 6 X 3 3 3
Operações em árvores binárias de busca (ou de pesquisa) Remoção: quando elemento possui 2 filhos O elemento pode ser substituído pelo elemento que o precede na ordenação (o elemento mais à direita da subárvore a esquerda); OU O elemento pode ser substituído pelo elemento que o sucede na ordenação (o elemento mais à esquerda da subárvore a direita);
Árvores binárias ESTRUTURAS DE DADOS Função para remover um novo nó da árvore: void remover(int it, NodePtr *p) { No* aux; if (*p == NULL) printf("erro: Registro não encontrado."); else if (it < (*p)->item) remover(it, &(*p)->esq); else if (it > (*p)->item) remover(it, &(*p)->dir); else if ((*p)->dir == NULL) { aux = *p; *p = (*p)->esq; free(aux); }
Árvores binárias ESTRUTURAS DE DADOS Função para remover um novo nó da árvore: void remover(int it, NodePtr *p) { (cont.) } else if ((*p)->esq == NULL ) { aux = *p; *p = (*p)->dir; free(aux); } else antecessor(*p, &(*p)->esq);
Árvores binárias ESTRUTURAS DE DADOS Função auxiliar para remover um novo nó da árvore: void antecessor(no* q, NodePtr *r) { if( (*r)->dir!= NULL) antecessor(q, &(*r)->dir); else { q->item = (*r)->item; q = *r; *r = (*r)->esq; free(q); } }
Árvores binárias ESTRUTURAS DE DADOS Função que verifica se um item pertence à árvore: No* find(int it, No* p) { if (p == NULL) return NULL; else if (it < p->item) find(it, p->esq); else if (it > p->item) find(it, p->dir); else return p; }
Árvores binárias ESTRUTURAS DE DADOS Função que libera a árvore da memória: void liberararvore(nodeptr *p) { if (*p!= NULL) { liberararvore(&(*p)->esq); liberararvore(&(*p)->dir); free(*p); } }
Árvores binárias Ordens de percurso em árvores binárias (métodos de caminhamento): Ordem na qual os nós da árvore serão visitados (para executar alguma operação com a informação). Pré-ordem: visita a raiz, percorre SAE, percorre SAD. Ordem simétrica (In-ordem): percorre SAE, visita raiz, percorre SAD. Pós-ordem: percorre SAE, percorre SAD, visita raiz. Dependendo da aplicação, uma dessas ordens pode ser preferível, ou seja, a ordem de percurso é importante.
Altura de uma árvore A altura da árvore (h) é o comprimento do caminho mais longo da raiz até uma das folhas. Somente nó raiz: h = 0
Altura de uma árvore A altura da árvore (h) é o comprimento do caminho mais longo da raiz até uma das folhas. h = 1
Altura de uma árvore A altura da árvore é o comprimento do caminho mais longo da raiz até uma das folhas. h = 2
Árvores ESTRUTURAS DE DADOS Altura de uma árvore: comprimento do caminho mais longo da raiz até uma das folhas. Nó raiz Altura = 1 Altura = 3
Árvores ESTRUTURAS DE DADOS A altura de uma árvore com um único nó raiz é 0 (zero). Altura = 0 A altura de uma árvore vazia é negativa e vale -1.
Altura de uma árvore Também podemos numerar os níveis em que os nós aparecem na árvore: Nível 0 Nível 1 Nível 2 E assim por diante. O último nível da árvore é o nível h, sendo h a altura da árvore.
Árvores binárias Árvore binária cheia (completa): todos os nós internos, ou seja, exceto as folhas, têm 2 descendentes (2 subárvores associadas). todos os nós folhas estão no último nível. Exemplo:
Árvore binária: Quantidade de nós em uma árvore cheia de altura h: Nível 0: 2º = 1 nó Nível 1: 2¹ = 2 nós Nível 2: 2² = 4 nós E assim por diante: No nível n temos 2ⁿ nós.
Árvore binária: ESTRUTURAS DE DADOS Relação entre o número de nós de uma árvore binária e sua altura: A cada nível, o número de nós vai dobrando, de maneira que uma árvore cheia de altura h pode ter um número de nós dado por: 2 0 + 2 1 + 2 2 + + 2 h-1 + 2 h = h 2 i i = 0 h 2 i i = 0 = 2 h + 1-1
Árvore binária: Observe que o número de nós em um nível n é uma unidade a mais do que a soma de todos os nós dos níveis anteriores:
Árvore binária: Observe que o número de nós em um nível n é uma unidade a mais do que a soma de todos os nós dos níveis anteriores: Então, confirmamos que uma árvore cheia de altura h tem um número de nós dado por: Número de nós do próximo nível
Árvores binárias Árvore degenerada: todos os seus nós internos, têm 1 único descendente (uma única subárvore associada), com exceção da (única) folha. Nível 0: 1 nó Nível 1: 1 nó Nível 2: 1 nó Uma árvore degenerada de altura h tem h + 1 nós.
Árvore binária: ESTRUTURAS DE DADOS A altura de uma árvore é uma medida importante na avaliação da eficiência com que visitamos os seus nós. Uma árvore binária com n nós tem: Altura mínima proporcional a: Altura máxima proporcional a: log n n (caso árvore degenerada) A altura indica o esforço computacional necessário para alcançar qualquer nó da árvore.
Análise de complexidade da operação de busca O número de comparações em função do número n de elementos na árvore em uma pesquisa com sucesso é: Melhor caso: Pior caso: Caso médio: C(n) = O(1) C(n) = O(n) C(n) = O(log n) Quando o elemento procurado se encontra na raiz. Quando os elementos estão em ordem crescente ou decrescente. Quando o número de elementos na sae e na sad é equilibrado. O tempo de execução dos algoritmos para árvores binárias de pesquisa dependem muito do formato das árvores.
Operações em árvores binárias de busca (ou de pesquisa) É importante manter as árvores com altura pequena (próximo da altura mínima log n), isto é, manter as árvores com uma distribuição dos nós próxima à da árvore cheia. Escreva um programa que utilize as funções de árvore binária de busca estudadas.