Estruturas de Dados Marco Antonio Moreira de Carvalho Algoritmos e Estruturas de Dados Bibliografia Básica l Cormen, Leiserson, Rivest. Introduction to Algorithms. 2nd edition. MIT Press, 2001. Capítulos 10, 11, 12, 13, 18, 19, 20, 21. l Aho, Alfred V., Hopcroft, John F., Ullman, Jeffrey D., Data Structure and Algorithms, Massachusetts: Addison-Wesley, 1987. Capítulos 1, 2, 3, 4, 5. 2 Bibliografia Básica l Donald Knuth, The Art of Computer Programming, vols. 3: Sorting and Searching (2nd ed.),Addison Wesley Longman Publishing Co., Inc. 1998. l London, K., Mastering Algorithms with C. O'Reilly & Associates, Inc. 1999. 3 Estruturas de Dados l l São um modo particular de armazenamento e organização de conjuntos de dados; Estes conjuntos de dados manipulados por algoritmos podem crescer, diminuir, serem alterados e também pesquisados l l Por isso são chamados de conjuntos dinâmicos. Estes conjuntos podem ser representados por estruturas de dados simples, que por sua vez podem compor estruturas avançadas l A representação deve ser capaz de suportar as operações em conjuntos dinâmicos. 4 Estruturas de Dados Operações l Operações frequentes sobre conjuntos dinâmicos: l l l l Pesquisar(S, k) – Dado um conjunto S, pesquisar a existência da chave k; Inserir(S, x) – Insere o elemento apontado por x no conjunto S. Deletar(S, x) – Deleta o elemento apontado por x do conjunto S. Mínimo(S) – Pesquisa o conjunto ordenado S e retorna a menor chave encontrada. 5 Estruturas de Dados Operações l l l Máximo(S) – Pesquisa o conjunto ordenado S e retorna a menor chave encontrada; Sucessor(S, x) – Pesquisa o conjunto ordenado S e retorna o elemento após x, ou, caso x seja o último elemento, retorna NULL; Antecessor(S, x) – Pesquisa o conjunto ordenado S e retorna o elemento anterior a x, ou, caso x seja o primeiro elemento, retorna NULL. 6 Listas, Pilhas e Filas l Listas, Pilhas e Filas são formas de conjuntos dinâmicos semelhantes entre si l Basicamente diferem na forma em que os elementos são inseridos e removidos l Para cada operação existe uma política. 7 Lista (Vetores) l Com base nas operações sobre conjuntos dinâmicos, definimos a primeira estrutura de dados: a Lista l l Mantém dados de um mesmo tipo organizados em uma ordem linear; Pode se implementada por um vetor; l Menor flexibilidade para aumentar e diminuir o tamanho. Índice Valor 1 3 2 3 3 4 5 6 7 8 9 5 18 19 23 35 37 42 49 84 8 Exercício l Como realizar operações de inserção, remoção e pesquisa em uma lista implementada com vetores? l l Como manter a ordem sequencial dos elementos? Qual a complexidade do pior caso em cada operação? Índice Valor 1 3 2 3 3 4 5 6 7 8 9 5 18 19 23 35 37 42 49 84 9 Lista - Vetores l Podem ter os tamanhos alterados (em C) dinamicamente, mas isto pode prejudicar o desempenho l Usualmente, define-se um limite máximo para utilização de um vetor estático l l l Na inserção, há um limite para a quantidade de elementos; Na exclusão, é necessário reorganizar os elementos que ainda estão presentes na lista; Permite acesso direto aos elementos. 10 Lista em Vetores Complexidade l Inserção l l l Remoção l l l l No início: O(n); No fim: Θ(1); No início: O(n); No meio: O(n); No fim: Θ(1); Pesquisa l O(n). 11 Ponteiros Ponteiros, apontadores, pointers... l São um tipo de variável que armazena um endereço de memória l l l A partir do ponteiro, é possível acessar o valor armazenado no endereço de memória correspondente, e.g., variáveis, chamadas de funções (em C), etc. Permitem a criação de estruturas dinâmicas, flexíveis. 12 Lista Encadeada l Difere da representação por vetores l l Os elementos são referenciados por ponteiros, ao invés de índices; Flexibilidade para aumentar e diminuir o tamanho l l Alocação dinâmica de memória. Suporta as operações sobre conjuntos dinâmicos l Embora não necessariamente de maneira eficiente. 13 Lista Encadeada l Cada elemento tem um campo com o valor da chave e outro(s) campo(s) com um ponteiro: l l l Aponta para o próximo da lista l Em alguns casos, para o anterior também. Se não houver próximo, aponta para NULL (nulo). Início[L] Para representar o início da lista, temos um ponteiro, chamado início, ou raiz da lista. NU 14 Lista Encadeada l Existem várias formas l Ligação simples ou dupla l l Ordenada ou não l l Cada elemento aponta para o próximo, ou aponta para o próximo e para o anterior. Elementos organizados de acordo com o valor da chave ou não. Circulares l O último elemento aponta para o primeiro, formando um anel de dados. 15 Lista Encadeada Simples l Lista com um único elemento l Lista com dois elementos Início 1 2 Chave Ponteiro Ponteiro Fim da lista para o próximo que indica elemento o início da lista 16 N Lista Encadeada Simples Estrutura struct celula{! ! ! ! ! //chave ! ! !! int chave;! //ponteiro para o próximo! ! struct celula* proximo;! ! };! ! typedef struct{! ! ! ! ! //ponteiro inicial! ! struct celula* inicio;! ! }tipoLista;! ! --------------------------------------! ! Inicializacao(tipoLista *L){! L->inicio = NULL;! }! 17 Lista Encadeada Simples Pesquisa struct celula* Pesquisa(tipoLista *L, int k)! {! struct celula *temp = L->inicio;! ! while(temp != NULL && temp->chave != k) ! ! temp = temp->prox;! ! return temp;! }! k = 16 Início 4 23 15 42 NULL 18 Lista Encadeada Simples Inserção no Início InsercaoInicio(tipoLista *L, int k)! {! struct celula *temp;! ! !temp = (struct celula*)malloc(sizeof(struct celula));! ! !temp->chave = k;! temp->prox = L->inicio;! L->inicio = temp;! }! Exemplo no quadro 19 Lista Encadeada Simples Inserção no Final InsercaoFinal(tipoLista *L, int k){! struct celula *temp = L->inicio;! ! !if(L->inicio == NULL) !{! ! InsercaoInicio(L, k);! !}! !else!{! while(temp->prox != NULL)! ! ! temp = temp->prox;! ! ! temp->prox = (struct celula*)malloc(sizeof(struct celula));! ! temp->prox->chave = k;! ! temp->prox->prox = NULL;! !}! }! Exemplo no quadro 20 Lista Encadeada Simples Remoção no Início ! RemocaoInicio(tipoLista *L)! {! struct celula *temp = L>inicio;! ! if(L->inicio != NULL)! {! temp = L->inicio;! ! L->inicio = L->inicio>prox;! ! free(temp);! }! else! ! printf("Lista Vazia! \n");! }! Exemplo no quadro 21 Lista Encadeada Simples Remoção no Final RemocaoFinal(tipoLista *L){! struct celula *anterior;! struct celula *posterior = L->inicio;! ! if(L->inicio != NULL){! if(L->inicio->prox == NULL){! free(L->inicio);! ! ! L->inicio = NULL;! ! }! ! else{! ! while(posterior->prox != NULL){! ! anterior = posterior;! ! ! posterior = posterior->prox;! ! !}! ! anterior->prox = NULL;! ! free(posterior);! ! }! }! else! printf("Lista Vazia!\n");! }! Exemplo no quadro 22 Lista Encadeada Simples Complexidade l Inserção l l l Remoção l l l l No início: Θ(1); No fim: Θ(1); No início: Θ(1); No meio: O(n); No fim: Θ(n); Pesquisa l O(n). 23 Exercício l O que fazer com uma lista encadeada quando não for mais necessária? E a memória dinâmica alocada? l Criar um procedimento para terminar uma lista encadeada. 24 Início NULL 23 42 16 NULL Lista Duplamente Encadeada l Semelhante à anterior, porém, cada elemento também aponta para o elemento que o antecede; l Requer poucas alterações nos algoritmos anteriores; Permite acesso sequencial em ambas as direções; Torna possível a remoção de elementos a partir de um determinado ponteiro em O(1). l l 25 Lista Encadeada Circular Início l Semelhante às demais, porém, o último elemento 4 aponta para o primeiro; 42 16 8 l Pode ser utilizado para representar buffers e também filas de escalonamento (FIFO). 26 Pilhas l Utiliza a política Last In, First Out – LIFO l l A operação de inserção se chama push (empilha) l l l O elemento é sempre inserido no topo (fim) da pilha, não há alternativa. A operação de remoção se chama pop (desempilha) l l Último a entrar, Primeiro a sair. O elemento removido é sempre o topo da pilha, ou seja, não há como definir outro elemento específico. Pode ser implementada por vetores e ponteiros. Aplicação: ctrl+z. 27 push Pilhas – Representação pop 42 18 4 topo 15 32 8 início 28 Pilhas – Implementação l Com base nos algoritmos para listas: l l Qual alteração necessária na implementação da inicialização? Qual deve ser usado para implementar pop? l l Quais as alterações necessárias? Qual deve ser usado para implementar push? l Quais as alterações necessárias? 29 Push - Implementação void Push(tipoPilha *P, int k)! {! struct celula *temp;! ! !temp = (struct celula*)malloc(sizeof(struct celula));! ! !temp->chave = k;! temp->prox = P->topo;! P->topo = temp;! }! 30 Pop - Implementação void Pop(tipoPilha *P)! {! struct celula *temp = P->topo;! ! if(P->topo != NULL)! {! temp = P->topo;! ! P->topo = P->topo->prox;! ! free(temp);! }! else! ! printf("Pilha Vazia!\n");! }! 31 Exercícios Como uma estrutura de pilha pode ser utilizada para verificar o equilíbrio de símbolos como { }, [ ] e ( )? l Com base nos algoritmos para listas: l l l l Criar um procedimento top, que retorna o valor do elemento do topo da pilha. Criar um procedimento size, que retorna o tamanho da pilha. Criar um procedimento isEmpty, que retorna se a pilha está vazia ou não. 32 Filas l Utiliza a política First In, First Out – FIFO l l A operação de inserção se chama enqueue (enfileira) l l O elemento é sempre inserido no fim da fila, não há como furar . A operação de remoção se chama dequeue (desenfileira) l l Primeiro a entrar, Primeiro a sair. O elemento removido está sempre no início da fila, ou seja, não há como definir outro elemento específico. Pode ser implementada por vetores e ponteiros. 33 Fim Início Filas - Representação 42 05 Enqueue 18 23 26 61 Dequeue 34 Filas - Deques l Deques são filas em que é permitida a remoção tanto no fim quanto no início l Tempo constante. Pode ser utilizado para simular as operações de Fila e Pilha. l Pode ser implementado usando listas duplamente encadeadas. l 35 Filas – Implementação l Com base nos algoritmos para listas: l l Qual alteração necessária na implementação da inicialização? Qual deve ser usado para implementar enqueue? l l Qual deve ser usado para implementar dequeue? l l Quais as alterações necessárias? Quais as alterações necessárias? Implementar as operações de fila em vetores, 36 Enqueue - Algoritmo void Enqueue(tipoFila *F, int k)! {! struct celula *temp = F->inicio;! ! temp = (struct celula*)malloc(sizeof(struct celula));! temp->chave = k;! ! if(F->inicio == NULL)! {! temp->prox = F->inicio;! F->inicio = temp;! }! else! {! while(temp->prox != NULL)! ! temp = temp->prox;! ! temp->prox->prox = NULL;! }! }! 37 Dequeue - Algoritmo void Dequeue(tipoFila *F)! {! struct celula *temp = F->inicio;! ! if(F->inicio != NULL)! {! temp = F->inicio;! ! F->inicio = F->inicio->prox;! ! free(temp);! }! else! ! printf("Fila Vazia!\n");! }! 38 Exercícios l Com base nos algoritmos para listas: l l l l Criar um procedimento first, que retorna o valor do elemento do início da fila. Criar um procedimento size, que retorna o tamanho da fila. Criar um procedimento isEmpty, que retorna se a fila está vazia ou não. Implementar as operações de fila em vetores. 39 Pilha e Fila - Complexidade l A complexidade de todas as operações é mantida: l l l l Push: Θ(1); Pop: Θ(1); Enqueue: O(n); Dequeue: Θ(1). 40 Tabelas Hash l Tabelas Hash são estruturas de dados do tipo dicionário: l l Não permitem l Armazenamento de elementos repetidos; l Ordenação; l Recuperar o elemento sucessor ou antecessor a outro. Possuem as funções l Inserir; l Pesquisar; l Remover (não necessariamente). 41 Tabelas Hash l O endereçamento direto de elementos é especialmente útil para acessarmos um elemento arbitrário em O(1) l O valor da chave é seu endereço em um vetor. Universo de Chaves 2 0 4 1 3 5 0 1 / 3 / 5 0 1 2 3 4 5 42 Tabelas Hash l Todavia, se o universo de chaves é grande, o armazenamento de uma tabela de tamanho idêntico pode ser impraticável l l Além disso, o subconjunto de chaves realmente utilizadas pode ser muito pequeno, causando grande desperdício de espaço. Quando o conjunto K de chaves armazenadas é menor que o universo de todas as chaves possíveis, Tabelas Hash podem reduzir os requisitos de armazenamento a Θ(K). 43 Tabelas Hash l Por exemplo, como armazenar as chaves 0, 100 e 9.999? l Um vetor de 10.000 posições? Em Tabelas Hash, ao invés de inserir o elemento k na posição k, o elemento é inserido na posição h(k); l A chamada Função Hash é utilizada para calcular a posição na Tabela Hash. l l Sua função é diminuir o intervalo de índices necessários. 44 Funções Hash l Devem satisfazer aproximadamente a condição de que cada chave tem igual probabilidade de efetuar hash para cada uma das m posições da tabela l l Nem sempre é possível. Existem diversas técnicas para o cálculo l l Algumas mais simples e rápidas; Outras mais eficientes e mais lentas. 45 Funções Hash l Vamos nos concentrar em dois métodos muito utilizados l l l Método de Divisão; Método de Multiplicação. Para os próximos slides considere uma tabela hash de m posições e k chaves. 46 Método de Divisão l l l l Realiza o mapeamento tomando o resto de k dividido por m h(k) = k mod m Potências de 2 devem ser evitadas para o valor de m; m deve ser um número primo distante de pequenas potências de 2; Por exemplo, k= 1957 e m=701 h(1957) = 1957 mod 701 h(1957) = 555 47 Método de Multiplicação l Opera em duas etapas: l l l l Primeiro, multiplicamos k por uma constante A no intervalo 0<A<1 e mantemos apenas a parte fracionária do resultado; Logo após, multiplicamos esse valor por m e tomamos o piso do resultado. kA mod 1 significa a parte fracionária de kA; O valor de m não é crítico, usualmente uma potência de 2. 48 Método da Multiplicação l Por exemplo, k=123456 e m=16384 e A=0,6108 49 Função Hash l Uma função hash pode gerar o mesmo resultado para duas chaves diferentes, nesse caso, temos uma colisão l l l Existem técnicas eficientes para resolver estes conflitos; Ainda assim, é desejável que o número de colisões seja pequeno; Algumas funções hash produzem menos colisões que outras. 50 Universo de Chaves Função Hash k1 k5 K chaves k3 reais k2 k4 h(k1)= h(k5) h(k3) h(k2)= h(k4) l Funções hash são determinísticas: para cada chave, o mesmo resultado é obtido sempre. 51 Tratamento de Colisões l Existem dois métodos básicos para o tratamento de colisões l l Encadeamento; Endereçamento Aberto. 52 Encadeamento l Cada posição do vetor possui uma lista que armazena as chaves com mesmo valor de função hash. Universo de Chaves k1 k5 K chaves k3 reais k2 k4 NULL 53 Encadeamento l Para realizar operações de dicionário, determina-se a posição de acordo com a função hash e manipulase a lista correspondente l l l Cada chave é inserida no início da lista; Pesquisas e remoções são feitas, obviamente, na lista; A remoção pode ser feita por referência ao invés de valor de chave l Pesquisamos um valor e passamos a referência para o procedimento de remoção. 54 Endereçamento Aberto l l No endereçamento aberto, todas as chaves são mantidas na própria tabela, sem o uso de listas adicionais; Em cada posição da tabela, há uma chave ou VAZIO l l Desta forma, a tabela pode encher e não haver mais espaço para inserção. É realizada uma busca sistemática sucessiva (sondagem) na tabela até que uma chave seja encontrada, ou até que se tenha certeza que o elemento não está presente. 55 Endereçamento Aberto l l l Para inserção, sondamos a tabela até encontrarmos uma posição disponível; A posição inicial da sondagem é definida pela função hash; A sequência em que as posições da tabela são sondadas, portanto, depende da chave a ser inserida l Podemos embutir na função hash a quantidade de sondagens, ou o tamanho dos saltos realizados durante a sondagem, representados por i em h(k, i). 56 Endereçamento Aberto l l A pesquisa na tabela hash usa a mesma política de sondagem da inserção; Especificamente, não se consideram remoções nesta tabela hash l Poderia prejudicar o cálculo da complexidade. l Nestes casos, usa-se encadeamento. 57 Código Inserção HASH_INSERT(int T[], int k)! {! i=0; ! ! ! !//determina o salto! do ! {! j=h(k,i); ! !//determina a sondagem atual ! if(T[j] == VAZIO) !//se está vazio! {! T{j] = k; ! !//insere a chave !! return; ! !//termina! }! i++; ! ! !//senão incrementa o salto! }! while (i!=m); ! !//até que toda tabela tenha sido ! }! ! ! ! !//sondada! 58 Código Pesquisa HASH_INSERT(int T[], int k)! {! i=0; ! ! ! !//determina o salto! do ! {! j=h(k,i); ! !//determina a sondagem atual! ! if(T[j] == k) ! !//se encontrou a chave! return; ! !//termina! ! !! ! !i++; ! ! !//senão incrementa o salto! }! while (i!=m && T[j] != NULL);//até que toda tabela ! ! ! ! ! //tenha sido sondada! }! ! ! ! ! //ou posição vazia ! ! ! ! ! //encontrada! 59 Estratégias de Sondagem l l No passo j=h(k,i), diferentes tipos de sondagens podem ocorrer; Existem basicamente três estratégias: l l l Estratégia Linear; Estratégia Quadrática; Hash Duplo. 60 Estratégia Linear l l l l Utiliza uma função hash auxiliar h h(k,i) = (h (k)+i) mod m Para i=1, ..., m; A primeira posição sondada (para inserção ou pesquisa) é T[h (k)+1] e assim por diante até a posição T[m-1]; A implementação é imediata, porém pode ocorrer agrupamento primário l Longas sequências de posições ocupadas, aumentando o tempo médio de pesquisa. 61 Estratégia Quadrática l l l l Utiliza uma função hash auxiliar h h(k,i) = (h (k)+c1i+c2i2) mod m Para i=1, ..., m e constantes c1 e c2 diferentes de zero; A primeira posição sondada (para inserção ou pesquisa) é T[h (k)] e o deslocamento posterior é definido pela forma quadrática de i; Funciona melhor que a anterior, porém pode ocorrer agrupamento secundário l Duas chaves diferentes com posição inicial igual, possuirão a mesma sequência de sondagem. 62 Hash Duplo l l l l Um dos melhores métodos disponíveis para endereçamento aberto h(k,i) = (h1(k)+ih2(k)) mod m Onde h1 e h2 são funções hash auxiliares; A primeira posição sondada é T[h1(k)], posições posteriores são deslocadas em função da quantidade h2(k) mod m; Neste método, a sequência de sondagem depende de k de duas maneiras diferentes que podem ter valores variáveis. 63 Tabelas Hash - Complexidade Depende da complexidade da função hash e do tempo para encontrar a chave após determinada a posição. l Caso todos os elementos colidam a complexidade é O(n); l Caso nenhum colida, a complexidade é Θ(1); l No caso médio, a complexidade é O(1). l 64 Árvores l Árvores de pesquisa podem ser utilizadas tanto como dicionários quanto como filas de prioridades e conjuntos dinâmicos l l Cada elemento em uma árvore é denominado nó l l Permitem a representação hierárquica de dados. O primeiro deles, é denominado raiz; Ligações entre nós são feitas por arestas. 65 Árvores l A árvore é vista de cabeça para baixo l l A raiz está em cima. De acordo com a hierarquia, um nó pai é ligado a nós filhos, que por sua vez também podem ser pais l l Raiz A raiz não possui pai. 6 Pai Filhos Nós sem filhos são chamados folhas ou nós terminais l Os demais são nós internos. Folhas 66 Árvores l l São estruturas recursivas, pois cada filho também é uma árvore; Cada nó pode ser alcançado a partir da raiz, através de um caminho único de arestas l l O nível de um nó é o número de nós do caminho da raiz até ele l l O comprimento do caminho é sua quantidade de arestas. A raiz não é contada. A altura de uma árvore é o maior caminho até um nó. 67 Árvores l O grau de um nó é sua quantidade de filhos l l l l l Folhas tem grau nulo; O grau de uma árvore é o grau máximo entre seus nós. O fator de ramificação é o número máximo de filhos para cada nó. Uma coleção de árvores é chamada Floresta; O número de filhos por nó e as informações mantidas geram diferentes tipos de árvores l Uma árvore é dita completa se cada nó interno possuir o número máximo de filhos. 68 Árvores de Pesquisa Binária l Cada nó possui no máximo 2 filhos (fator de ramificação = 2) l l l Identificados como filho esquerdo e filho direito. Em uma árvore binária não vazia, o número de folhas é o número de nós internos +1; Em árvores binárias completas l l Existem 2h -1 nós internos e 2h folhas, onde h é a altura; A distância da raiz até qualquer folha é log(n), ou seja, a altura é Θ(logn) l Ganho em complexidade. 69 Árvores de Pesquisa Binária l Satisfazem a propriedade de árvore de pesquisa binária: l Seja x um nó interno da árvore l l l Se y é um nó da subárvore esquerda, então y < x; Se y é um nó da subárvore direita, então y ≥ x. Esta propriedade permite que as chaves de uma árvore sejam percorridas de forma ordenada facilmente. 70 Árvore de Pesquisa Binária Estrutura struct no{! int chave; ! //armazena a chave! struct no* pai;! //ponteiro para o no pai! struct no* direito; //ponteiro para o filho esquerdo! struct no* esquerdo;//ponteiro para o filho direito! };! ! typedef struct{! !struct no* raiz;! //estrutura da árvore ! }tipoArvore;! ! Inicializacao(tipoArvore* T)! {! T->raiz = NULL;! //inicializa a raiz! }! 71 Árvores - Percursos Um percurso em uma árvore é uma sequência de visitação de nós adjacentes, em que cada nó aparece apenas uma vez l Existem dois tipos básicos de percurso em árvores l l l Percurso em Largura Percurso em Profundidade l l l Percurso em Pré-Ordem Percurso em Ordem Percurso em Pós-Ordem 72 Percurso em Largura l l Também conhecido como BFS (Breadth-First Search) Este percurso é realizado no sentido horizontal da árvore l l l Os nós são visitados da esquerda para a direita, ou da direita para a esquerda Os nós são visitados por nível l Uma vez que todos os nós de um nível foram visitados, passa-se ao nível seguinte. Utiliza a estrutura de fila em sua implementação. 73 Percurso em Largura 1 A 2 3 B C 4 5 D E 6 F G 8 H 74 Percurso em Largura - Código DFS(tipoArvore *T, tipoFila *F){! struct no* aux;! ! if(T->raiz != NULL) {! Enfileira(&F, T->raiz);! ! ! ! while(!Vazia(F))! {! ! aux = Desenfileira(&F);! ! ! printf("%d ", aux->chave);! ! ! if(aux->esquerdo != NULL)! ! ! ! Enfileira(&F, aux->esquerdo);! if(aux->direito != NULL)! ! ! ! Enfileira(&F, aux->direito);! ! }! }! }! 75 Percurso em Profundidade l l Também conhecido como DFS (Depth-First Search) Como o próprio nome indica, este percurso é realizado no sentido vertical da árvore l l l l A partir da raiz, percorre-se toda a altura da árvore até uma folha mais à esquerda (ou à direita, de acordo com o critério adotado); Uma vez atingida uma folha, o percurso volta ao penúltimo nó visitado e desce em profundidade novamente; O processo se repete, visitando todos os nós. Utiliza a estrutura de pilha em sua implementação. 76 Percurso em Profundidade 1 A 2 6 B C 3 4 D E 7 F 5 H 77 Percurso em Profundidade l Ainda é possível usar o percurso em profundidade para ordenar os nós linearmente l l l Em ordem: Visita o filho esquerdo, o pai e o filho direito; Pré-ordem: Visita o pai antes dos filhos; Pós-ordem: Visita o filho esquerdo, o filho direito e depois o pai. 78 Percurso em Ordem PercursoEmOrdem(struct no* no)! {! if(no != NULL) ! ! ! //Se o nó existir! {! PercursoEmOrdem(no->esquerdo); //visita o esquerdo! ! printf("%d ", no->chave); //imprime a (sub)raiz! ! PercursoEmOrdem(no->direito); //visita o direito! }! }! ! l A complexidade é linear l l Para cada nó, o procedimento é chamado duas vezes. A partir deste código, os outros percursos podem ser facilmente implementados. 79 Pesquisa - Código l Com base no percurso em ordem e na propriedade de árvore de pesquisa binária, como derivar um método de pesquisa? struct no* Pesquisa(struct no* no, int k)! {! if(no == NULL) ! !//se o nó não existe! ! return no; ! !//retorna NULL! !if(no->chave == k) //se achou a chave! ! return no; ! !//retorna o ponteiro! if(no->chave > k) !//se a chave é menor! ! return Pesquisa(no->esquerdo, k);//procura na ! ! ! ! ! !//esquerda! else return Pesquisa(no->direito, k);//senão! ! ! ! ! ! //procura na direita !! }! 80 Pesquisa l Durante a pesquisa, um caminho é traçado em busca da chave de acordo com o valor de cada nó visitado l l l Somente um nó de cada nível é visitado; Desta forma, visitaremos no máximo h nós, onde h é a altura da árvore. Complexidade: O(h). 81 Mínimo e Máximo l De acordo com a propriedade de árvore de pesquisa binária: l l l A menor chave está no nó mais à esquerda; A maior chave está no nó mais à direita. A complexidade para encontrar cada um deles é O(h) novamente. 82 Sucessor e Antecessor l O sucessor de uma chave x é a menor chave maior que x l l Não confundir com o filho direito. De forma análoga, o antecessor de uma chave x é a maior chave menor que x l Não confundir com o filho esquerdo. 83 Sucessor - Código struct no* Sucessor(struct no* x)! {! struct no* y;! !! !if(x->direito != NULL)! !//se há filho direito! return Minimo(x->direito);//retorne o máximo da! ! ! ! ! ! !//subárvore direita! !y = x->pai; ! ! !//senão, sobe! ! !while(y != NULL && x == y->direito)//até a raiz da! !{ ! ! ! ! ! !//(sub)árvore! ! !x = y;! ! ! !//ou ate não encontrar! ! !y=y->pai; ! ! !//sobe na árvore! !}! ! !return y; ! ! ! !//retorna! }! 84 Antecessor Com base no procedimento anterior, como criar um para o cálculo do Antecessor? l A complexidade de ambos é O(h) l l Percorre-se um caminho para baixo ou para cima na árvore, não mais que isso. 85 Inserção l Semelhantemente à pesquisa, a posição correta de uma chave é determinada pela relação entre seu valor e os dos nós da árvore l Diferentes ordens de inserção geram diferentes árvores. 2 5 2 3 7 3 5 8 7 5 8 5 86 Inserção - Código void Insercao(tipoArvore *T, int k){! struct no* aux = T->raiz;! struct no* pai = NULL;! struct no* novo;! ! novo = (struct no*) malloc(sizeof(struct no));//dados do novo nó! novo->chave = k;! novo->direito = NULL;! novo->esquerdo = NULL;! ! while(aux != NULL){ ! //enquanto não atingir o fim da árvore! pai = aux;! ! !//atualiza o pai do novo no! ! if(k < aux->chave)! !//decide por qual subárvore! ! ! aux = aux->esquerdo; !//descer! ! else! ! ! aux = aux->direito;! }! ! novo->pai = pai; ! !//atribui o novo pai! ! if(pai == NULL) ! !//se não houver! T->raiz = novo; ! !// insere na raiz! else if(k < pai->chave) !//caso contrário determina! pai->esquerdo = novo;!//se será o filho esquerdo! else! pai->direito = novo;//ou o direito! }! 87 Remoção Durante a remoção, é necessário manter a propriedade da árvore binária de pesquisa; Também devemos manter a subárvore (caso exista) da chave removida; Existem três casos para o elemento removido l l l 1. 2. 3. Não possui filhos: apenas o removemos; Possui um filho: ele o substituirá; Possui dois filhos: ele será substituído por seu sucessor. 88 Remoção – Caso 1 15 5 3 16 20 12 10 13 23 18 6 7 89 Remoção – Caso 2 15 5 3 16 20 12 10 13 23 18 6 7 90 Remoção – Caso 3 15 5 3 16 20 12 10 13 23 18 6 7 Sucessor 91 Remoção – Caso 3 15 6 3 16 20 12 10 13 23 18 7 92 Remoção - Código l Tem quatro passos: 1. Testa se possui no máximo um filho l l É verificado se o sucessor (se for o caso) possui filho esquerdo ou direito, que ocupará seu lugar; 2. l l l Se houver filho, o pai dele passa a ser o pai do sucessor. Verifica-se qual o tipo de posição do sucessor a ser ocupada pelo filho 3. 4. Caso positivo, o nó da chave excluída será ocupado pelo filho (caso haja) ou excluído (caso não haja filhos); Caso contrário, busca-se a posição do sucessor. Se raiz, filho esquerdo ou direito. O nó da chave removida recebe o valor do sucessor, se for o caso. A chave é removida. 93 Remoção - Código void Remocao(tipoArvore *T, int k){! struct no* del;! struct no* suc;! struct no* sub;! ! del = Pesquisa(T->raiz, k); //busca o ponteiro para! //a chave a ser removida! if(del->direito == NULL || del->esquerdo == NULL)//maximo de um filho! ! suc = del; //a posição a ser ocupada é a própria! else //da chave removida! ! suc = Sucessor(del); //senão, busca o sucessor! ! if(suc->esquerdo != NULL) //se há filho esquerdo no sucessor! ! sub = suc->esquerdo; //o marca como substituto! else! ! ! //caso contrário! ! sub = suc->direito; //marca o filho direito! ! if(sub != NULL) //se marcou um filho como substituto! ! sub->pai = suc->pai; //atualiza o pai dele como o do sucessor! ! 94 Remoção - Código if(suc->pai == NULL) ! ! ! T->raiz = sub; ! ! else if (suc == suc->pai->esquerdo) ! suc->pai->esquerdo = sub; else ! suc->pai->direito = sub; //se o sucessor não tem pai! //é a raiz, substitui então! //senão, substitui ! //como filho esquerdo! //ou! //como filho direito! ! if(suc != del) ! ! del->chave = suc->chave; ! ! ! ! ! free(pos); ! ! ! }! //se houver sucessor! //copia a chave do sucessor! //para o nó da chave removida! //libera a memória! 95 Inserção e Remoção Complexidade l Ambas as operações são executada em O (h), em que h é a altura da árvore binária l l Na inserção, ocorre no máximo uma pesquisa pela posição adequada; Na remoção l l l l Ocorre apenas o desligamento de um nó; A substituição de um nó ou; Uma pesquisa pelo sucessor e uma substituição. Todas operações O(1) ou O(h). 96 Balanceamento da Altura l Como visto anteriormente, a ordem de inserção das chaves pode gerar árvores de diferentes alturas l l l n chaves inseridas em ordem crescente implicam em altura n-1; Influência direta na complexidade das operações l Uma árvore muito desbalanceada se aproxima de uma lista encadeada. É desejável que a altura da árvore binária seja proporcional ao logaritmo da quantidade de chaves armazenadas l l Pode ser provado que a altura esperada de uma árvore construída aleatoriamente é O(logn) As remoções também podem alterar a altura. 97 Árvores Balanceadas l Existem diferentes esquemas de árvores balanceadas l l l O objetivo é garantir que as operações básicas de conjuntos dinâmicos sejam realizadas em O(logn). Os procedimentos de inserção e remoção são adaptados para garantir que a árvore permaneça balanceada; Veremos dois métodos l l Árvores AVL; Árvores Vermelho e Preto. 98 Árvores AVL l l Propriedade: Para cada nó, a altura das subárvores esquerda e direita diferem por no máximo 1; Cada nó armazena informação sobre seu fator de balanceamento l l l Indica a diferença de altura entre suas subárvores. Garante altura logarítmica; Após cada inserção ou remoção, verifica-se a diferença entre as alturas das subárvores l Caso seja necessário, o balanceamento é realizado por meio de rotações. 99 Árvores AVL Fator de Balanceamento l O fator de balanceamento de um nó é definido como a diferença entre as alturas de suas subárvores esquerda e direita l l l Um nó com fb entre -1 e 1 é considerado balanceado; Se o fb é menor que -1, a subárvore direita o está desbalanceando; Se o fb é maior que 1, a subárvore esquerda o está desbalanceando. 100 Árvores AVL12 +2 +1 2 +2 +1 8 16 +1 0 4 10 0 14 0 6 0 1 2 4 101 Rotações Quando uma inserção ou remoção desbalanceia a árvore, é necessário reorganizar seus nós de forma a balanceá-la l l Ainda, a propriedade de árvore binária deve ser mantida. A rotação é efetuada sobre o nó mais profundo desbalanceado l l É necessário identificar qual subárvore é a origem do desbalanceamento. 102 Rotações Existem 4 casos a serem analisados: l 1. 2. 3. 4. l l A subárvore esquerda do filho esquerdo (LL); A subárvore direita do filho direito (RR); A subárvore direita do filho esquerdo (LR); A subárvore esquerda do filho direito (RL). Os casos 1 e 2, e 3 e 4 são simétricos Há um tipo de rotação para cada um dos casos. 103 k1 8 +1 0 Rotações 4 10 Caso 1 – Rotação Simples LL +1 2 0 6 0 1 l l l k2 é o nó desbalanceado mais profundo e k1 é sua subárvore com diferença de altura; Uma rotação para a direita balanceia a árvore novamente k2 vira filho direito de k1 e o filho direito de k1 vira o filho 104 esquerdo de k2. Rotação LL RR(struct noAVL* k2)! {! struct noAVL* k1; !//raiz da subarvore com ! ! ! ! ! !//diferenca de altura! struct noAVL* fk1; !//filho esquerdo de k1! ! k1 = k2->esquerdo; !//atribui o ponteiro de k1 ! fk1 = k1->direito; !//atribui o ponteiro de fk1! k1->direito = k2; !//k2 vira filho direito de k1! k1->direito->esquerdo = fk1;//fk1 vira filho ! ! ! ! ! !//esquerdo de k2! k2=k1; ! ! ! !//k1 ocupa a posicao ! ! ! ! ! ! !//de k2! }! 105 4 8 Rotações 7 10 Caso 2 – Rotação Simples RR 0 -1 0 12 l l l k2 é o nó desbalanceado mais profundo e k1 é sua subárvore com diferença de altura; Uma rotação para a esquerda balanceia a árvore novamente k2 vira filho de k1, e o filho esquerdo de k1 vira o filho direito 106 de k2. Rotação RR LL(struct noAVL* k2)! {! struct noAVL* k1; !//raiz da subarvore com ! ! ! ! ! !//diferenca de altura! struct noAVL* fk1; !//filho esquerdo de k1! ! k1 = k2->direito; !//atribui o ponteiro de k1! fk1 = k1->esquerdo; !//atribui o ponteiro de fk1! k1->esquerdo = k2; //k2 vira filho esquerdo de k1! k1->esquerdo->direito = fk1;//fk1 vira filho direito! ! ! ! ! ! !//de k2! k2=k1; ! ! !//k1 ocupa a posicao de k2! }! 107 k1 0 -1 4 10 Rotações Caso 3 – Rotação Dupla LR 6 2 k 0 +1 0 2 5 l l Uma das subárvores de k1 está 2 níveis abaixo da outra subárvore de k3: k2. k2 será a nova raiz e k3 se tornará seu filho direito l k1 adota o filho de k2. 108 Rotações Caso 3 – Rotação Dupla LR l O caso anterior equivale a duas rotações simples Entre k3 +2k1 e k2; 8 l -1Entre k3 e k2.0 k1 l 4 0 k2 10 +1 +1 6 2 0 5 +2 k3 k1 k2 4 8 +1 6 0 10 0 5 0 2 109 Rotação LR LR(struct noAVL* k3)! {! RR(k3->direito);//faz a rotação RR em k1! LL(k3); ! !//e a rotação LL em k3! }! 110 Rotação RL RL(struct noAVL* k3)! {! LL(k3->esquerdo); RR(k3); ! ! }! !//faz a rotação LL em k1! !//e a RR em k3! 111 Rotações l Uma forma de identificar o tipo de rotação necessária é comparar os fbs do nó mais profundo desbalanceado e da raiz de sua subárvore com diferença de altura l l Se os sinais forem iguais, a rotação é simples; Se os sinais forem diferentes, a rotação é dupla l l A primeira rotação iguala os sinais; A segunda rotação balanceia a árvore. 112 Complexidade l l As rotações podem ser efetuadas em O(logn); As inserções e remoções são semelhantes às de árvores binárias comuns, porém, incluem testes e rotações l l l l Portanto, podem ser efetuadas em O(logn). Código disponível no site da disciplina Trabalho: Completar as chamadas para rotações e implementar os casos de rotações RL e LR. Extra: Applet de árvores AVL em:http:// www.csi.uottawa.ca/~stan/csi2514/applets/avl/BT.html 113 Árvores Vermelho-Preto Cada nó desta árvore armazena uma informação adicional, sua cor: vermelho ou preto; l A cor que os nós podem ter em cada caminho na árvore é controlada l l l Desta forma, garante-se que nenhum caminho será maior que duas vezes o comprimento de qualquer outro caminho; Como consequência, a árvore é aproximadamente balanceada. 114 Árvores Vermelho-Preto Propriedades: l 1. 2. 3. 4. 5. Todo nó é vermelho ou preto; A raiz é preta; Todo nó nulo é preto; Se um nó é vermelho, então ambos os seus filhos são pretos; Para cada nó, todos os caminhos desde um nó até as folhas descendentes contêm o mesmo número de nós pretos. 115 Árvores Vermelho-Preto 26 17 41 14 10 7 21 16 12 15 19 30 23 20 47 38 28 35 39 3 116 Árvores Vermelho-Preto l A altura de preto ou altura negra de um nó x, denotada por bh(x) é o número de nós pretos no caminho do nó x até uma folha l l Caso o nó x seja preto, não será contado para a altura. Pode ser provado que uma árvore vermelho-preto possui altura no máximo 2log(n+1), em que n denota o número de nós. l l Como consequência, os procedimentos Pesquisa, Mínimo, Máximo, Sucessor e Antecessor podem ser executados em tempo O(logn) em árvores vermelho-preto; Os procedimentos de inserção e remoção precisam ser adaptados para manter o balanceamento, mas também podem ser executados em tempo O(logn). 117 Árvores Vermelho-Preto l A realização de operações de inserção e remoção podem afetar o balanceamento da árvore l l l l Novamente, procedimentos de rotação são utilizados para manter o balanceamento l l l Por isso, alguns nós devem trocar de cor; Para manter as propriedades da árvore vermelho-preto, alguns nós mudam de posição; A propriedade de árvore binária também deve ser mantida; Rotação à esquerda; Rotação à direita. Os procedimentos são simétricos. 118 Rotações l Quando fazemos uma rotação à esquerda em um nó, supomos que seu filho direito não seja nulo; l l Simetricamente, a rotação à direita supõe que o filho esquerdo não seja nulo. No exemplo abaixo, α, β e γ são subárvores arbitrárias Rotação Esquerda(n1) n1 n2 Rotação Direita(n1) α β γ γ n1 n2 α β 119 Rotação à Esquerda RotacaoEsquerda(tipoVP* A, struct noVP* n1)! {! struct noVP* n2;! ! n2 = n1->direito; ! !//n2 é o filho direito de n1! n1->direito = n2->esquerdo; !//o filho direito de n1 é o esquerdo ! ! ! !//de n2! n2->esquerdo->pai = n1; !//atualiza o pai no nó! n2->pai = n1->pai; ! !//o pai de n2 passa a ser o pai de n1! ! if(n1->pai == NULL) ! !//se não há pai! A->raiz = n2; ! !//atribui à raiz! else if(n1 == n1->pai->esquerdo)//senão, se n1 é filho esquerdo! ! n1->pai->esquerdo = n2; //n2 é o novo filho esquerdo! ! !else ! ! ! //senão! ! ! n1->pai->direito = n2; //é o novo filho direito! n2->esquerdo = n1; ! ! //n1 é o filho direito de n2! n1->pai = n2; ! ! //o pai de n1 é n2! }! 120 Rotação à Esquerda 7 4 4 11 n1 3 n2 3 6 9 18 2 19 14 12 2 22 17 20 l Exercício: Implementar a rotação à direita. 121 Inserção l A inserção é semelhante à realizada em árvores binárias, porém, adicionalmente o novo nó é colorido de vermelho l l Após a inserção, é realizado um procedimento de manutenção, que recolore os nós e executa rotações. A inserção pode violar duas das propriedades de árvores vermelho-preto: l l Se o nó inserido for a raiz, ele não poderia ser vermelho l Fácil de corrigir, é só mudar a cor Se o pai do nó inserido for vermelho, o nó inserido não poderia ser vermelho também l Existem três casos possíveis. 122 Violações Caso 1: z (o nó violador) é vermelho, seu pai é vermelho e seu tio é vermelho. Não importa se z é filho direito ou esquerdo; l Caso 2: z é vermelho, seu pai é vermelho, seu tio é preto e z é filho da direita; l Caso 3: z é vermelho, seu pai é vermelho, seu tio é preto e z é filho da esquerda; l O caso 2 recai no caso 3. l 123 Caso 1 C D A z α B β l δ ε novo z γ Neste caso, os nós são recoloridos. O nó C é o novo z, e deve ser verificado quanto a violações. 124 Caso 1 C D A z α B β l δ ε γ Não faz diferença se z é um filho esquerdo ou direito. 125 Casos 2 e 3 A α β l Caso 2, aplica-se uma rotação à esquerda 126 C Casos 2 e 3 A z β l α B Gera um caso 3, recolorimento e rotação à direita. γ 127 Casos 2 e 3 B z α l C A Violação corrigida. Não existem pai e filho vermelhos. β γ 128 Manutenção Exemplo Completo 11 14 2 1 7 15 y 5 z 8 4 l z, seu pai e seu tio são vermelhos: caso 1 (recolorir). 129 Manutenção Exemplo Completo 11 14 2 y z 1 7 5 15 8 4 l z e seu pai são vermelhos, mas seu tio não. z é o filho da direita: caso 2 (rotação à esquerda). 130 Manutenção Exemplo Completo 11 14 7 z 2 y 15 8 5 1 4 l z e seu pai são vermelhos, mas seu tio não. z é o filho da esquerda: caso 2 (rotação à direita). 131 Manutenção Exemplo Completo 7 z 2 11 5 1 8 14 4 l 15 Finalmente, uma árvore vermelho-preto válida. 132 Remoção l l Na remoção de um nó y em uma árvore vermelhopreto, novamente o procedimento para árvores binárias é adaptado para corrigir violações das propriedades; Caso y seja vermelho, não há nenhuma violação: l l l l Nenhuma altura muda; Nenhum nó vermelho se tornou adjacente a outro; A raiz permanece preta. Caso y seja preto, podemos ter 3 tipos de problemas: 1. 2. 3. Se y era raiz, a raiz pode passar a ser vermelha; Se o pai de y era vermelho, e o filho de y era vermelho, termos dois nós vermelhos adjacentes; Qualquer caminho que continha y tem a altura modificada. 133 Remoção l Para resolver o terceiro problema, adicionamos um preto extra a um filho de y, mesmo que esse filho seja NULL l l l Se o filho for preto, se torna preto duplo ; Se o filho for vermelho, se torna vermelho e preto e contribui para as duas contagens; Dessa forma, a altura dos caminhos continua igual; l Porém, viola a propriedade 1 (todo nó é vermelho ou preto). 134 Violações Existem quatro casos para violação da propriedade 1: l 1. 2. 3. 4. O irmão w de x é vermelho; O irmão w de x é preto, e ambos os filhos de w são pretos; O irmão w de x é preto, o filho da esquerda de w é vermelho e o da direita é preto; O irmão w de x é preto e o filho da direita de w é vermelho. 135 Violações l Nos slides a seguir l x denota o nó com preto extra l l l Pode ser um preto duplo ou vermelho e preto. Nós cinza tem o atributo cor definido por c ou c , que podem ser vermelho ou preto; As letras gregas representam subárvores arbitrárias. 136 B Caso 1 x A α β E C γ l w D δ ε ζ O caso 1 é transformado em um dos outros casos. O pai de w se torna vermelho e uma rotação à esquerda é realizada. 137 B c Caso 2x A α β E C γ l w D δ ε ζ No caso 2, o preto extra representado por x é movido para cima (B). w se torna vermelho. 138 B c Caso x3 A α β E C γ l w D δ ε ζ O caso 3 é transformado em caso 4 pela troca de cores entre w e seu filho esquerdo e uma rotação à direita. 139 x A Caso 4 α β C γ l w D c' δ E ε ζ No caso 4, o preto extra representado por x pode ser removido pela troca de cores entre w e c, o filho direito de w se torna preto e executa-se uma rotação à esquerda. 140 Árvores Vermelho-Preto l Como visto anteriormente, todas as operações de dicionário podem ser efetuadas em O(logn) l l Operações de inserção e remoção são adaptadas para manter as propriedades relacionadas. Código das operações na página da disciplina. 141 Conjuntos Um conjunto não possui elementos duplicados; l Os dados podem ser mantidos ordenados ou não; l Operações sobre conjuntos incluem: l l l l l Adição de elementos; Remoção de elementos; Pesquisa; Cardinalidade (tamanho do conjunto). 142 Conjuntos l Sejam S e T dois conjuntos. Em particular, as operações entre conjuntos incluem: l l l l Soma(S, T): retorna o conjunto dos elementos de S e T, sem repetições; Interseção(S, T): retorna o conjunto dos elementos comuns a S e T; Diferença(S, T): retorna o conjunto dos elementos em S mas não em T; Subconjunto(S, T): testa se S é subconjunto de T. 143 Conjuntos l A implementação pode ser realizada através de diferentes estruturas de dados l l l l Conjuntos ordenados são geralmente implementados por árvores balanceadas l Complexidade O(logn) para a maioria das operações. Conjuntos não ordenados são geralmente implementados por tabelas hash l Complexidade O(1) no caso médio e O(n) no pior caso. Conjuntos de inteiros podem ser implementados por vetores; Algumas linguagens de programação fornecem suporte para manipulação de conjuntos l C(template class set), Java (Interface Set), Python (tipo set)... 144 Conjuntos l Trabalho l Implementar as operações de conjuntos de acordo com uma das estruturas de dados apresentadas. 145 Heaps l l l São árvores binárias completas em todos os níveis, exceto o último (possivelmente); O último nível é preenchido da esquerda para a direita; Também pode ser visto como um vetor e possui as seguintes propriedades: l l A raiz da árvore é armazenada em A[1]; Para um dado nó i: l O seu nó pai é ; l Seu filho à esquerda é 2i; l Seu filho à direita é 2i+1; 146 ⎣ Heaps l l Os cálculos de pais e filhos podem ser realizados em uma única instrução; Os heaps podem ser máximos ou mínimos l l l l Heap Máximo (MaxHeap) l Raiz com o maior valor e pais com valor ≥ que os filhos. Heap Mínimo (MinHeap) l Raiz com o menor valor e pais com valor ≤ que os filhos. MaxHeap é utilizado no método de ordenação Heapsort; Ambos os tipos de heaps podem ser utilizados para implementar filas de prioridade. 147 Heaps 1 16 Índice no vetor 2 3 14 10 4 5 6 7 8 7 9 3 8 9 10 2 4 1 1 2 3 4 5 6 7 8 9 10 16 14 10 8 7 9 3 2 4 1 148 Heaps l Como o heap é baseado em árvores binárias, sua altura é Θ(logn) l Portanto, as operações básicas sobre heaps também possuem complexidade Θ(logn). 149 Heaps - Estrutura l A estrutura de um heap deve manter dois atributos l l Comprimento: Número de elementos do vetor; Tamanho do Heap: Número de elemento do heap armazenados no vetor. O tamanho máximo do heap é o comprimento do vetor. 150 Manutenção de um MaxHeap l l l l É necessário manter as propriedades do heap durante as inserções e exclusões. Cada subárvore deve ser um heap máximo, portanto, um nó pai não pode ser menor que os nós filhos; Caso o nó pai seja menor que um dos filhos, ele trocará de posição com o maior deles; É aplicada recursivamente para garantir que uma mudança realizada não viola a propriedade em outras subárvores. 151 2 1 4 MAX-HEAPIFY 4 14 8 9 10 2 8 1 5 6 7 9 152 MAX-HEAPIFY 2 3 14 10 4 5 6 4 7 9 8 9 10 2 8 1 153 MAX-HEAPIFY 2 3 14 10 4 5 6 8 7 9 8 9 10 2 4 1 154 MAX-HEAPIFY - Código void MAX_HEAPIFY(int A[], int i, int n)! ! {! ! int esquerdo;! ! int direito;! ! int maior;! ! ! ! esquerdo = 2*i;! //determina o filho esquerdo! direito = 2*i+1;! //determina o filho direito! ! ! if(esquerdo <= n && A[esquerdo] > A[i])! //se o filho esquerdo for! ! maior = esquerdo;! //maior que o pai, registra! else! //senão! ! maior = i;! //o maior é o pai mesmo! ! ! if (direito <= n && A[direito] > A[maior])!//se o direito é maior que o maior! ! maior = direito;! //registra! ! ! if(maior != i)! //se o maior não é o pai! {! ! Troca(&A[i], &A[maior]);! //troca as posições! ! MAX_HEAPIFY(A, maior, n);! //verifica se a subárvore viola a ! }! //propriedade! 155 }! ! ! MAX-HEAPIFY - Complexidade l l l T( Θ(1) para fazer as trocas em um mesmo nível; Uma subárvore pode ter no máximo tamanho 2n/3; No pior caso então, a complexidade é dada pela recorrência =T (Pelo teorema mestre, caso 2) 156 Construção de um MaxHeap Procedimento BUILD-MAX-HEAP; l Utiliza o procedimento anterior para transformar um vetor em um heap máximo; l É aplicado de baixo para cima na árvore; l Da metade do vetor em diante estão as folhas da árvore, então o procedimento é aplicado deste ponto para trás no vetor; l A propriedade do heap é mantida pelo procedimento anterior. l 157 3 1 4 BUILD-MAX-HEAP2 4 1 3 2 8 9 10 14 8 7 16 9 10 14 8 5 6 16 9 7 158 1 BUILD-MAX-HEAP 4 2 3 3 1 4 5 6 2 16 9 8 9 10 14 8 7 159 1 BUILD-MAX-HEAP 4 2 3 3 1 4 5 6 14 16 9 8 9 10 2 8 7 160 1 BUILD-MAX-HEAP 4 2 3 10 1 4 5 6 14 16 9 8 9 10 2 8 7 161 1 BUILD-MAX-HEAP 4 2 3 10 16 4 5 6 14 7 9 8 9 10 2 8 1 162 1 BUILD-MAX-HEAP 16 2 3 10 14 4 5 6 8 7 9 8 9 10 2 4 1 163 BUILD-MAX-HEAP - Código void BUILD_MAX_HEAP(int A[],int n)! {! int i;! ! ! for(i=n/2; i>0; i--)! //Para cada uma das subárvores,! //verifica corrige a propriedade ! MAX_HEAPIFY(A, i, n);! }! //do heap! //folhas não são verificadas! 164 BUILD-MAX-HEAP Complexidade Aparentemente, a complexidade é O(nlogn); l Porém, analisando-se a quantidade máxima de nós por nível do heap, e a quantidade de níveis, é possível provar que a complexidade do procedimento pode ser limitada por O(n); l Em outras palavras, construir um heap a partir de um vetor aleatório é possível em tempo linear. l 165 Heaps - Inserção A nova chave é inserida na primeira posição livre do vetor; l Após a inserção, é verificada a manutenção das propriedades do heap l l l O pai da nova chave é verificado, e caso necessário, troca de lugar com o filho; Estas trocas podem se estender em efeito cascata semelhante ao que ocorre no método bolha de ordenação. 166 Inserção - MaxHeap MAXHEAP_INSERT(int A[], int k, int *tamanho)! {! int i;! ! (*tamanho)++; ! !//incrementa o tamanho! A[*tamanho] = k; ! !//a chave entra no final! ! i = *tamanho; ! !//posição da nova chave! ! while(i > 1 && A[i/2]< A[i])//enquanto o pai de i for menor! {! Troca(&A[i], &A[i/2]); !//troca as posições de i e seu pai! ! i = i/2; ! ! !//posicao do novo pai de i! }! }! 167 Heaps - Remoção l A remoção em heaps não é realizada sobre um elemento aleatório l l Em Heaps Máximos, o elemento de maior chave é removido; Em Heaps Mínimos, o elemento de menor chave é removido. A operação é chamada extração; l Após a extração, o procedimento MAX_HEAPIFY é chamado para manter as propriedades do heap. l 168 Extração - MaxHeap int MAXHEAP_EXTRACT(int A[], int *tamanho, int comprimento)! {! int max;! ! if(*tamanho < 1) ! !//se não há chaves no heap! { !! ! printf("MaxHeap vazio!");//avisa o erro! ! return -1; ! !! }! else! ! ! !//se houver chaves! {! max = A[1]; ! !//a raiz é a maior! ! A[1] = A[*tamanho]; !//a última chave passa a ser a raiz! ! (*tamanho)--; ! !//o tamanho é decrementado! ! MAX_HEAPIFY(A, 1, comprimento);//restaura o heap! ! ! return max; ! !//retorna a maior chave! }! }! 169 Complexidade l A inserção no pior caso precisa caminhar da chave inserida até o primeiro nível do heap fazendo trocas para manter as propriedades do heap l l Complexidade O(logn); A extração realiza operações constantes e chama o procedimento MAX_HEAPIFY l Complexidade O(logn). 170 MinHeaps Heaps Mínimos são simétricos aos Heaps Máximos; l Trabalho: Implementar os procedimentos para Heaps Mínimos com base nos procedimentos apresentados para Heaps Máximos. l 171