Post on 12-Nov-2018
2
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.
3
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.
4
Estruturas de Dados
l São um modo particular de armazenamento e organização de conjuntos de dados;
l Estes conjuntos de dados manipulados por algoritmos podem crescer, diminuir, serem alterados e também pesquisados l Por isso são chamados de conjuntos dinâmicos.
l 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.
5
Estruturas de Dados - Operações l Operações frequentes sobre conjuntos
dinâmicos: l Pesquisar(S, k) – Dado um conjunto S, pesquisar
a existência da chave k; l Inserir(S, x) – Insere o elemento apontado por x
no conjunto S. l Deletar(S, x) – Deleta o elemento apontado por x
do conjunto S. l Mínimo(S) – Pesquisa o conjunto ordenado S e
retorna a menor chave encontrada.
6
Estruturas de Dados - Operações
l Máximo(S) – Pesquisa o conjunto ordenado S e retorna a menor chave encontrada;
l Sucessor(S, x) – Pesquisa o conjunto ordenado S e retorna o elemento após x, ou, caso x seja o último elemento, retorna NULL;
l Antecessor(S, x) – Pesquisa o conjunto ordenado S e retorna o elemento anterior a x, ou, caso x seja o primeiro elemento, retorna NULL.
7
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.
8
Lista (Vetores)
l Com base nas operações sobre conjuntos dinâmicos, definimos a primeira estrutura de dados: a Lista l Mantém dados de um mesmo tipo organizados em uma
ordem linear; l Pode se implementada por um vetor;
l Menor flexibilidade para aumentar e diminuir o tamanho.
1 2 3 3 4 5 6 7 8 9 3 5 18 19 23 35 37 42 49 84
Índice
Valor
9
Exercício
l Como realizar operações de inserção, remoção e pesquisa em uma lista implementada com vetores? l Como manter a ordem sequencial dos elementos? l Qual a complexidade do pior caso em cada operação?
1 2 3 3 4 5 6 7 8 9 3 5 18 19 23 35 37 42 49 84
Índice
Valor
10
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 Na inserção, há um limite para a quantidade de
elementos; l Na exclusão, é necessário reorganizar os elementos
que ainda estão presentes na lista; l Permite acesso direto aos elementos.
11
Lista em Vetores Complexidade l Inserção
l No início: O(n); l No fim: Θ(1);
l Remoção l No início: O(n); l No meio: O(n); l No fim: Θ(1);
l Pesquisa l O(n).
12
Ponteiros
l Ponteiros, apontadores, pointers... l São um tipo de variável que armazena um
endereço de memória 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.
l Permitem a criação de estruturas dinâmicas, flexíveis.
13
Lista Encadeada
l Difere da representação por vetores l Os elementos são referenciados por ponteiros, ao
invés de índices; l Flexibilidade para aumentar e diminuir o tamanho
l Alocação dinâmica de memória. l Suporta as operações sobre conjuntos dinâmicos
l Embora não necessariamente de maneira eficiente.
14
Lista Encadeada
l Cada elemento tem um campo com o valor da chave e outro(s) campo(s) com um ponteiro: l Aponta para o próximo da lista
l Em alguns casos, para o anterior também. l Se não houver próximo, aponta para NULL (nulo).
l Para representar o início da lista, temos um ponteiro, chamado início, ou raiz da lista.
Início[L] NULL
15
Lista Encadeada
l Existem várias formas l Ligação simples ou dupla
l Cada elemento aponta para o próximo, ou aponta para o próximo e para o anterior.
l Ordenada ou não l Elementos organizados de acordo com o valor da
chave ou não. l Circulares
l O último elemento aponta para o primeiro, formando um “anel” de dados.
16
Lista Encadeada Simples
l Lista com um único elemento
l Lista com dois elementos
Ponteiro que indica
o início da lista
Chave Ponteiro para o próximo
elemento
Fim da lista
Início 1 NULL
Início 1 2 NULL
17
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;!}!!
1 NULL
Início
NULLInício
18
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;!}!
Início 4 42 NULL23 15k = 16
19
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
20
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
21
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
22
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
23
Lista Encadeada Simples Complexidade l Inserção
l No início: Θ(1); l No fim: Θ(1);
l Remoção l No início: Θ(1); l No meio: O(n); l No fim: Θ(n);
l Pesquisa l O(n).
24
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.
25
Lista Duplamente Encadeada
l Semelhante à anterior, porém, cada elemento também aponta para o elemento que o antecede;
Início 23 42 16 NULLNULL
l Requer poucas alterações nos algoritmos anteriores;
l Permite acesso sequencial em ambas as direções; l Torna possível a remoção de elementos a partir de
um determinado ponteiro em O(1).
26
Lista Encadeada Circular
l Semelhante às demais, porém, o último elemento aponta para o primeiro;
Início
4
8
1642
l Pode ser utilizado para representar buffers e também filas de escalonamento (FIFO).
27
Pilhas
l Utiliza a política Last In, First Out – LIFO l Último a entrar, Primeiro a sair.
l A operação de inserção se chama push (empilha) l O elemento é sempre inserido no topo (fim) da pilha, não há
alternativa. l A operação de remoção se chama pop (desempilha)
l O elemento removido é sempre o topo da pilha, ou seja, não há como definir outro elemento específico.
l Pode ser implementada por vetores e ponteiros. l Aplicação: ctrl+z.
29
Pilhas – Implementação
l Com base nos algoritmos para listas: l Qual alteração necessária na implementação da
inicialização? l Qual deve ser usado para implementar pop?
l Quais as alterações necessárias? l Qual deve ser usado para implementar push?
l Quais as alterações necessárias?
30
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;!}!
31
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");!
}!
32
Exercícios
l 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 Criar um procedimento top, que retorna o valor
do elemento do topo da pilha. l Criar um procedimento size, que retorna o
tamanho da pilha. l Criar um procedimento isEmpty, que retorna se a
pilha está vazia ou não.
33
Filas
l Utiliza a política First In, First Out – FIFO l Primeiro a entrar, Primeiro a sair.
l A operação de inserção se chama enqueue (enfileira) l O elemento é sempre inserido no fim da fila, não há como “furar”.
l A operação de remoção se chama dequeue (desenfileira) l O elemento removido está sempre no início da fila, ou seja,
não há como definir outro elemento específico.
l Pode ser implementada por vetores e ponteiros.
35
Filas - Deques
l Deques são filas em que é permitida a remoção tanto no fim quanto no início l Tempo constante.
l Pode ser utilizado para simular as operações de Fila e Pilha.
l Pode ser implementado usando listas duplamente encadeadas.
36
Filas – Implementação
l Com base nos algoritmos para listas: l Qual alteração necessária na implementação da
inicialização? l Qual deve ser usado para implementar enqueue?
l Quais as alterações necessárias? l Qual deve ser usado para implementar dequeue?
l Quais as alterações necessárias? l Implementar as operações de fila em vetores,
37
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;! }!}!
38
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");!
}!
39
Exercícios
l Com base nos algoritmos para listas: l Criar um procedimento first, que retorna o valor
do elemento do início da fila. l Criar um procedimento size, que retorna o
tamanho da fila. l Criar um procedimento isEmpty, que retorna se a
fila está vazia ou não. l Implementar as operações de fila em vetores.
40
Pilha e Fila - Complexidade
l A complexidade de todas as operações é mantida: l Push: Θ(1); l Pop: Θ(1); l Enqueue: O(n); l Dequeue: Θ(1).
41
Tabelas Hash
l Tabelas Hash são estruturas de dados do tipo dicionário: l Não permitem
l Armazenamento de elementos repetidos; l Ordenação; l Recuperar o elemento sucessor ou antecessor a outro.
l Possuem as funções l Inserir; l Pesquisar; l Remover (não necessariamente).
42
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.
0 0 1 1 / 2 3 3 / 4 5 5
Universo de Chaves
10
2
34
5
43
Tabelas Hash
l Todavia, se o universo de chaves é grande, o armazenamento de uma tabela de tamanho idêntico pode ser impraticável l Além disso, o subconjunto de chaves realmente utilizadas
pode ser muito pequeno, causando grande desperdício de espaço.
l 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).
44
Tabelas Hash
l Por exemplo, como armazenar as chaves 0, 100 e 9.999? l Um vetor de 10.000 posições?
l 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 Sua função é diminuir o intervalo de índices
necessários.
45
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 Nem sempre é possível.
l Existem diversas técnicas para o cálculo l Algumas mais simples e rápidas; l Outras mais eficientes e mais lentas.
46
Funções Hash
l Vamos nos concentrar em dois métodos muito utilizados l Método de Divisão; l Método de Multiplicação.
l Para os próximos slides considere uma tabela hash de m posições e k chaves.
47
Método de Divisão
l Realiza o mapeamento tomando o resto de k dividido por m
h(k) = k mod m l Potências de 2 devem ser evitadas para o valor de
m; l m deve ser um número primo distante de pequenas
potências de 2; l Por exemplo, k= 1957 e m=701
h(1957) = 1957 mod 701 h(1957) = 555
48
Método de Multiplicação l Opera em duas etapas:
l Primeiro, multiplicamos k por uma constante A no intervalo 0<A<1 e mantemos apenas a parte fracionária do resultado;
l Logo após, multiplicamos esse valor por m e tomamos o piso do resultado.
l kA mod 1 significa a parte fracionária de kA; l O valor de m não é crítico, usualmente uma
potência de 2.
⎣ ⎦)1mod()( kAmnh =
49
Método da Multiplicação
l Por exemplo, k=123456 e m=16384 e A=0,6108
⎣ ⎦⎣ ⎦15151)123456(
9248,016384)123456()1mod6108,0123456(16384)123456(
=
×=
××=
hhh
50
Função Hash
l Uma função hash pode gerar o mesmo resultado para duas chaves diferentes, nesse caso, temos uma colisão l Existem técnicas eficientes para resolver estes
conflitos; l Ainda assim, é desejável que o número de
colisões seja pequeno; l Algumas funções hash produzem menos colisões
que outras.
51
Função Hash
K chaves reais
k5k1
k2k4
Universo de Chaves
k3h(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.
52
Tratamento de Colisões
l Existem dois métodos básicos para o tratamento de colisões l Encadeamento; l Endereçamento Aberto.
53
Encadeamento
l Cada posição do vetor possui uma lista que armazena as chaves com mesmo valor de função hash.
k1 k5 NULL
K chaves reais
k5k1
k2k4
Universo de Chaves
k3
NULL
k3 k2 NULL
k4 NULL
54
Encadeamento
l Para realizar operações de dicionário, determina-se a posição de acordo com a função hash e manipula-se a lista correspondente l Cada chave é inserida no início da lista; l Pesquisas e remoções são feitas, obviamente, na lista; l 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.
55
Endereçamento Aberto
l No endereçamento aberto, todas as chaves são mantidas na própria tabela, sem o uso de listas adicionais;
l Em cada posição da tabela, há uma chave ou VAZIO l Desta forma, a tabela pode “encher” e não haver mais
espaço para inserção. l É 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.
56
Endereçamento Aberto
l Para inserção, sondamos a tabela até encontrarmos uma posição disponível;
l A posição inicial da sondagem é definida pela função hash;
l 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).
57
Endereçamento Aberto
l A pesquisa na tabela hash usa a mesma política de sondagem da inserção;
l Especificamente, não se consideram remoções nesta tabela hash l Poderia prejudicar o cálculo da complexidade.
l Nestes casos, usa-se encadeamento.
58
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!
59
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!
60
Estratégias de Sondagem
l No passo j=h(k,i), diferentes tipos de sondagens podem ocorrer;
l Existem basicamente três estratégias: l Estratégia Linear; l Estratégia Quadrática; l Hash Duplo.
61
Estratégia Linear
l Utiliza uma função hash auxiliar h’ h(k,i) = (h’(k)+i) mod m
l Para i=1, ..., m; l 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];
l 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.
62
Estratégia Quadrática
l Utiliza uma função hash auxiliar h’ h(k,i) = (h’(k)+c1i+c2i2) mod m
l Para i=1, ..., m e constantes c1 e c2 diferentes de zero;
l A primeira posição sondada (para inserção ou pesquisa) é T[h’(k)] e o deslocamento posterior é definido pela forma quadrática de i;
l 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.
63
Hash Duplo
l Um dos melhores métodos disponíveis para endereçamento aberto
h(k,i) = (h1(k)+ih2(k)) mod m l Onde h1 e h2 são funções hash auxiliares; l A primeira posição sondada é T[h1(k)], posições
posteriores são deslocadas em função da quantidade h2(k) mod m;
l Neste método, a sequência de sondagem depende de k de duas maneiras diferentes que podem ter valores variáveis.
64
Tabelas Hash - Complexidade
l 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).
65
Árvores
l Árvores de pesquisa podem ser utilizadas tanto como dicionários quanto como filas de prioridades e conjuntos dinâmicos l Permitem a representação hierárquica de dados.
l Cada elemento em uma árvore é denominado nó l O primeiro deles, é denominado raiz;
l Ligações entre nós são feitas por arestas.
66
Árvores
l A árvore é vista de cabeça para baixo l A raiz está em cima.
l De acordo com a hierarquia, um nó pai é ligado a nós filhos, que por sua vez também podem ser pais l A raiz não possui pai.
l Nós sem filhos são chamados folhas ou nós terminais l Os demais são nós internos.
5
3 7
2 5 8Raiz
Folhas
Pai
Filhos
6
67
Árvores
l São estruturas recursivas, pois cada filho também é uma árvore;
l Cada nó pode ser alcançado a partir da raiz, através de um caminho único de arestas l O comprimento do caminho é sua quantidade de
arestas. l O nível de um nó é o número de nós do caminho da
raiz até ele l A raiz não é contada.
l A altura de uma árvore é o maior caminho até um nó.
68
Árvores
l O grau de um nó é sua quantidade de filhos l Folhas tem grau nulo; l O grau de uma árvore é o grau máximo entre seus nós.
l O fator de ramificação é o número máximo de filhos para cada nó.
l Uma coleção de árvores é chamada Floresta; l 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.
69
Árvores de Pesquisa Binária
l Cada nó possui no máximo 2 filhos (fator de ramificação = 2) l Identificados como filho esquerdo e filho direito.
l Em uma árvore binária não vazia, o número de folhas é o número de nós internos +1;
l Em árvores binárias completas l Existem 2h -1 nós internos e 2h folhas, onde h é a altura; l A distância da raiz até qualquer folha é log(n), ou seja, a
altura é Θ(logn) l Ganho em complexidade.
70
Árvores de Pesquisa Binária
l Satisfazem a propriedade de árvore de pesquisa binária: l Seja x um nó interno da árvore
l Se y é um nó da subárvore esquerda, então y < x; l Se y é um nó da subárvore direita, então y ≥ x.
l Esta propriedade permite que as chaves de uma árvore sejam percorridas de forma ordenada facilmente.
71
Á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!}!
72
Árvores - Percursos
l 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 Percurso em Largura l Percurso em Profundidade
l Percurso em Pré-Ordem l Percurso em Ordem l Percurso em Pós-Ordem
73
Percurso em Largura
l Também conhecido como BFS (Breadth-First Search)
l Este percurso é realizado no sentido horizontal da árvore l Os nós são visitados da esquerda para a direita, ou da
direita para a esquerda l 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.
l Utiliza a estrutura de fila em sua implementação.
75
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);!! }!
}!}!
76
Percurso em Profundidade
l Também conhecido como DFS (Depth-First Search) l Como o próprio nome indica, este percurso é
realizado no sentido vertical da árvore 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);
l Uma vez atingida uma folha, o percurso volta ao penúltimo nó visitado e desce em profundidade novamente;
l O processo se repete, visitando todos os nós.
l Utiliza a estrutura de pilha em sua implementação.
78
Percurso em Profundidade
l Ainda é possível usar o percurso em profundidade para ordenar os nós linearmente l Em ordem: Visita o filho esquerdo, o pai e o filho
direito; l Pré-ordem: Visita o pai antes dos filhos; l Pós-ordem: Visita o filho esquerdo, o filho direito
e depois o pai.
79
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 Para cada nó, o procedimento é chamado duas vezes.
l A partir deste código, os outros percursos podem ser facilmente implementados.
80
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 !!
}!
81
Pesquisa
l Durante a pesquisa, um caminho é traçado em busca da chave de acordo com o valor de cada nó visitado l Somente um nó de cada nível é visitado; l Desta forma, visitaremos no máximo h nós, onde
h é a altura da árvore.
l Complexidade: O(h).
82
Mínimo e Máximo
l De acordo com a propriedade de árvore de pesquisa binária: l A menor chave está no nó mais à esquerda; l A maior chave está no nó mais à direita.
l A complexidade para encontrar cada um deles é O(h) novamente.
83
Sucessor e Antecessor
l O sucessor de uma chave x é a menor chave maior que x l Não confundir com o filho
direito. l De forma análoga, o
antecessor de uma chave x é a maior chave menor que x l Não confundir com o filho
esquerdo.
8
3 15
2 9 11
84
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!
}!
85
Antecessor
l Com base no procedimento anterior, como criar um para o cálculo do Antecessor?
l A complexidade de ambos é O(h) l Percorre-se um caminho para baixo ou para cima
na árvore, não mais que isso.
86
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.
5
3 7
2 5 8
5
3
7
2
5
8
87
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!}!
88
Remoção
l Durante a remoção, é necessário manter a propriedade da árvore binária de pesquisa;
l Também devemos manter a subárvore (caso exista) da chave removida;
l Existem três casos para o elemento removido
1. Não possui filhos: apenas o removemos; 2. Possui um filho: ele o substituirá; 3. Possui dois filhos: ele será substituído por seu
sucessor.
93
Remoção - Código
l Tem quatro passos: 1. Testa se possui no máximo um filho
l Caso positivo, o nó da chave excluída será ocupado pelo filho (caso haja) ou excluído (caso não haja filhos);
l Caso contrário, busca-se a posição do sucessor. 2. É verificado se o sucessor (se for o caso) possui filho esquerdo
ou direito, que ocupará seu lugar; l Se houver filho, o pai dele passa a ser o pai do sucessor.
3. Verifica-se qual o tipo de posição do sucessor a ser ocupada pelo filho
l Se raiz, filho esquerdo ou direito. 4. O nó da chave removida recebe o valor do sucessor, se for o
caso. l A chave é removida.
94
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!
!
95
Remoção - Código
if(suc->pai == NULL) ! ! //se o sucessor não tem pai! ! T->raiz = sub; ! ! //é a raiz, substitui então! else if (suc == suc->pai->esquerdo) //senão, substitui !! suc->pai->esquerdo = sub; //como filho esquerdo!
else //ou!! suc->pai->direito = sub; //como filho direito!
! if(suc != del) ! ! //se houver sucessor! del->chave = suc->chave; //copia a chave do sucessor!! ! ! ! ! //para o nó da chave removida!
free(pos); ! ! ! //libera a memória!}!
96
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 Na inserção, ocorre no máximo uma pesquisa
pela posição adequada; l Na remoção
l Ocorre apenas o desligamento de um nó; l A substituição de um nó ou; l Uma pesquisa pelo sucessor e uma substituição.
l Todas operações O(1) ou O(h).
97
Balanceamento da Altura
l Como visto anteriormente, a ordem de inserção das chaves pode gerar árvores de diferentes alturas l n chaves inseridas em ordem crescente implicam em
altura n-1; l Influência direta na complexidade das operações
l Uma árvore muito desbalanceada se aproxima de uma lista encadeada.
l É desejável que a altura da árvore binária seja proporcional ao logaritmo da quantidade de chaves armazenadas l Pode ser provado que a altura esperada de uma árvore
construída aleatoriamente é O(logn) l As remoções também podem alterar a altura.
98
Árvores Balanceadas
l Existem diferentes esquemas de árvores balanceadas l O objetivo é garantir que as operações básicas de
conjuntos dinâmicos sejam realizadas em O(logn). l Os procedimentos de inserção e remoção são
adaptados para garantir que a árvore permaneça balanceada;
l Veremos dois métodos l Árvores AVL; l Árvores Vermelho e Preto.
99
Árvores AVL
l Propriedade: Para cada nó, a altura das subárvores esquerda e direita diferem por no máximo 1;
l Cada nó armazena informação sobre seu fator de balanceamento l Indica a diferença de altura entre suas subárvores.
l Garante altura logarítmica; l 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.
100
Á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 Um nó com fb entre -1 e 1 é considerado
balanceado; l Se o fb é menor que -1, a subárvore direita o está
desbalanceando; l Se o fb é maior que 1, a subárvore esquerda o
está desbalanceando.
101
12
8
1
4 10
62
14
16
+2
+1
00
+2
+1
0+1
0
Árvores AVL
2
4
12
4
10
2 8
61
14
16
+1
+1
00
0
0
00 0
102
Rotações
l Quando uma inserção ou remoção desbalanceia a árvore, é necessário reorganizar seus nós de forma a balanceá-la
l Ainda, a propriedade de árvore binária deve ser mantida.
l A rotação é efetuada sobre o nó mais profundo desbalanceado
l É necessário identificar qual subárvore é a origem do desbalanceamento.
103
Rotações
l Existem 4 casos a serem analisados: 1. A subárvore esquerda do filho esquerdo (LL); 2. A subárvore direita do filho direito (RR); 3. A subárvore direita do filho esquerdo (LR); 4. A subárvore esquerda do filho direito (RL).
l Os casos 1 e 2, e 3 e 4 são simétricos l Há um tipo de rotação para cada um dos
casos.
104
Rotações Caso 1 – Rotação Simples LL
l k2 é o nó desbalanceado mais profundo e k1 é sua subárvore com diferença de altura;
l Uma rotação para a direita balanceia a árvore novamente l k2 vira filho direito de k1 e o filho direito de k1 vira o filho
esquerdo de k2.
4
2
1 6
8
0
0
0
+1
0
10
0
k2
k1
8
4
2 6
10
+2
0
0
+1
+1
1
0
k2
k1
105
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!
}!
106
Rotações Caso 2 – Rotação Simples RR
6
4
7 10
8
-2
-1
-1
0
0
12
0
k2
k1
l k2 é o nó desbalanceado mais profundo e k1 é sua subárvore com diferença de altura;
l Uma rotação para a esquerda balanceia a árvore novamente l k2 vira filho de k1, e o filho esquerdo de k1 vira o filho direito
de k2.
8
6
7 12
10
0
-1
0
0
0
4
0
k2
k1
107
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!}!
108
Rotações Caso 3 – Rotação Dupla LR
8
4
2 6
10
+2
0
+1
-1
0
5
0
k3
k1
k2
l Uma das subárvores de k1 está 2 níveis abaixo da outra subárvore de k3: k2.
l k2 será a nova raiz e k3 se tornará seu filho direito l k1 adota o filho de k2.
6
4
2 5
8
0
0
0
0
0
10
0
k3k1
k2
109
Rotações Caso 3 – Rotação Dupla LR l O caso anterior equivale a duas rotações simples
l Entre k1 e k2;
l Entre k3 e k2. 8
4
2 6
10
+2
0
+1
-1
0
5
0
k3
k1
k2
6
4
2 5
8
0
0
0
0
0
10
0
k3k1
k2
8
6
4 5
10
+2
0
0
+1
+1
2
0
k3
k1
k2
110
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!}!
111
Rotação RL
RL(struct noAVL* k3)!{! LL(k3->esquerdo); !//faz a rotação LL em k1! RR(k3); ! ! !//e a RR em k3!}!
112
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 Se os sinais forem iguais, a rotação é simples; l Se os sinais forem diferentes, a rotação é dupla
l A primeira rotação iguala os sinais; l A segunda rotação balanceia a árvore.
113
Complexidade
l As rotações podem ser efetuadas em O(logn); l As inserções e remoções são semelhantes às de
árvores binárias comuns, porém, incluem testes e rotações l Portanto, podem ser efetuadas em O(logn). l Código disponível no site da disciplina
l Trabalho: Completar as chamadas para rotações e implementar os casos de rotações RL e LR.
l Extra: Applet de árvores AVL em:http://www.csi.uottawa.ca/~stan/csi2514/applets/avl/BT.html
114
Árvores Vermelho-Preto
l 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 Desta forma, garante-se que nenhum caminho
será maior que duas vezes o comprimento de qualquer outro caminho;
l Como consequência, a árvore é aproximadamente balanceada.
115
Árvores Vermelho-Preto
l Propriedades: 1. Todo nó é vermelho ou preto; 2. A raiz é preta; 3. Todo nó nulo é preto; 4. Se um nó é vermelho, então ambos os seus
filhos são pretos; 5. Para cada nó, todos os caminhos desde um nó
até as folhas descendentes contêm o mesmo número de nós pretos.
117
Á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 Caso o nó x seja preto, não será contado para a altura.
l 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 Como consequência, os procedimentos Pesquisa, Mínimo,
Máximo, Sucessor e Antecessor podem ser executados em tempo O(logn) em árvores vermelho-preto;
l 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).
118
Árvores Vermelho-Preto
l A realização de operações de inserção e remoção podem afetar o balanceamento da árvore l Por isso, alguns nós devem trocar de cor; l Para manter as propriedades da árvore vermelho-preto,
alguns nós mudam de posição; l A propriedade de árvore binária também deve ser mantida;
l Novamente, procedimentos de rotação são utilizados para manter o balanceamento l Rotação à esquerda; l Rotação à direita.
l Os procedimentos são simétricos.
119
Rotações
l Quando fazemos uma rotação à esquerda em um nó, supomos que seu filho direito não seja nulo; l Simetricamente, a rotação à direita supõe que o filho
esquerdo não seja nulo. l No exemplo abaixo, α, β e γ são subárvores
arbitrárias
n2
n1
α
β γ
n1
n2
α β
γ
Rotação Esquerda(n1)
Rotação Direita(n1)
120
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!}!
121
Rotação à Esquerda 7
4
17
63 18
11
9
14 19
22
20
12
2
n1
n2
7
4
17
63
18
11
9 14
19
22
2012
2
n1
n2
l Exercício: Implementar a rotação à direita.
122
Inserção
l A inserção é semelhante à realizada em árvores binárias, porém, adicionalmente o novo nó é colorido de vermelho l Após a inserção, é realizado um procedimento de
manutenção, que recolore os nós e executa rotações. l A inserção pode violar duas das propriedades de
árvores vermelho-preto: l Se o nó inserido for a raiz, ele não poderia ser vermelho
l Fácil de corrigir, é só mudar a cor l 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.
123
Violações
l 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.
124
Caso 1
A
C
α
γ
B
β
D
δ εz
l Neste caso, os nós são recoloridos. O nó C é o novo z, e deve ser verificado quanto a violações.
A
C
α
γ
B
β
D
δ εznovo z
125
Caso 1
A
C
α
γ
B
β
D
δ εz
A
C
α
γ
B
β
D
δ εz
l Não faz diferença se z é um filho esquerdo ou direito.
129
Manutenção Exemplo Completo
85
4
11
2
71
14
15
z
y
l z, seu pai e seu tio são vermelhos: caso 1 (recolorir).
130
Manutenção Exemplo Completo
85
4
11
2
71
14
15z
y
l z e seu pai são vermelhos, mas seu tio não. z é o filho da direita: caso 2 (rotação à esquerda).
131
Manutenção Exemplo Completo
8
5
4
11
2
7
1
14
15
y
z
l z e seu pai são vermelhos, mas seu tio não. z é o filho da esquerda: caso 2 (rotação à direita).
132
Manutenção Exemplo Completo
85
4
112
7
1 14
15
z
l Finalmente, uma árvore vermelho-preto válida.
133
Remoção
l Na remoção de um nó y em uma árvore vermelho-preto, novamente o procedimento para árvores binárias é adaptado para corrigir violações das propriedades;
l Caso y seja vermelho, não há nenhuma violação: l Nenhuma altura muda; l Nenhum nó vermelho se tornou adjacente a outro; l A raiz permanece preta.
l Caso y seja preto, podemos ter 3 tipos de problemas:
1. Se y era raiz, a raiz pode passar a ser vermelha; 2. Se o pai de y era vermelho, e o filho de y era vermelho,
termos dois nós vermelhos adjacentes; 3. Qualquer caminho que continha y tem a altura modificada.
134
Remoção
l Para resolver o terceiro problema, adicionamos um “preto extra” a um filho de y, mesmo que esse filho seja NULL l Se o filho for preto, se torna “preto duplo”; l Se o filho for vermelho, se torna “vermelho e
preto” e contribui para as duas contagens; l Dessa forma, a altura dos caminhos continua
igual; l Porém, viola a propriedade 1 (todo nó é vermelho ou
preto).
135
Violações
l Existem quatro casos para violação da propriedade 1:
1. O irmão w de x é vermelho; 2. O irmão w de x é preto, e ambos os filhos de w
são pretos; 3. O irmão w de x é preto, o filho da esquerda de w
é vermelho e o da direita é preto; 4. O irmão w de x é preto e o filho da direita de w é
vermelho.
136
Violações
l Nos slides a seguir l x denota o nó com preto extra
l Pode ser um preto duplo ou vermelho e preto. l Nós cinza tem o atributo cor definido por c ou c’,
que podem ser vermelho ou preto; l As letras gregas representam subárvores
arbitrárias.
137
Caso 1 A
B
α
γ
Cβ
D
δ ε
x
ζ
E
w
A
B
α γ
C
β
D
δ
εx ζ
Enovo w
l O caso 1 é transformado em um dos outros casos. O pai de w se torna vermelho e uma rotação à esquerda é realizada.
138
Caso 2 AB
α
γ
Cβ
D
δ ε
x
ζ
E
w
c
A
B
α
γ
Cβ
D
δ ε
novo x
ζ
E
c
l No caso 2, o preto extra representado por x é movido para cima (B). w se torna vermelho.
139
Caso 3 AB
α
γ
Cβ
D
δ ε
x
ζ
E
w
c
A
B
α γ
C
β D
δ
ε
x
ζ
E
novo w
c
l O caso 3 é transformado em caso 4 pela troca de cores entre w e seu filho esquerdo e uma rotação à direita.
140
Caso 4 A
B
α
γ
Cβ
D
δ ε
x
ζ
E
w
c
c' A
B
γ
C
β
D
δ
ε ζ
Ec'
c
novo x: raiz da árvore
l 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.
141
Árvores Vermelho-Preto
l Como visto anteriormente, todas as operações de dicionário podem ser efetuadas em O(logn) l Operações de inserção e remoção são adaptadas
para manter as propriedades relacionadas. l Código das operações na página da
disciplina.
142
Conjuntos
l Um conjunto não possui elementos duplicados;
l Os dados podem ser mantidos ordenados ou não;
l Operações sobre conjuntos incluem: l Adição de elementos; l Remoção de elementos; l Pesquisa; l Cardinalidade (tamanho do conjunto).
143
Conjuntos
l Sejam S e T dois conjuntos. Em particular, as operações entre conjuntos incluem: l Soma(S, T): retorna o conjunto dos elementos de
S e T, sem repetições; l Interseção(S, T): retorna o conjunto dos
elementos comuns a S e T; l Diferença(S, T): retorna o conjunto dos elementos
em S mas não em T; l Subconjunto(S, T): testa se S é subconjunto de T.
144
Conjuntos
l A implementação pode ser realizada através de diferentes estruturas de dados l Conjuntos ordenados são geralmente implementados por
árvores balanceadas l Complexidade O(logn) para a maioria das operações.
l 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.
l Conjuntos de inteiros podem ser implementados por vetores;
l Algumas linguagens de programação fornecem suporte para manipulação de conjuntos l C(template class set), Java (Interface Set), Python (tipo
set)...
145
Conjuntos
l Trabalho l Implementar as operações de conjuntos de
acordo com uma das estruturas de dados apresentadas.
146
Heaps
l São árvores binárias completas em todos os níveis, exceto o último (possivelmente);
l O último nível é preenchido da esquerda para a direita;
l Também pode ser visto como um vetor e possui as seguintes propriedades: l A raiz da árvore é armazenada em A[1]; l Para um dado nó i:
l O seu nó pai é ; l Seu filho à esquerda é 2i; l Seu filho à direita é 2i+1;
⎣ ⎦2/i
147
Heaps
l Os cálculos de pais e filhos podem ser realizados em uma única instrução;
l Os heaps podem ser máximos ou mínimos l Heap Máximo (MaxHeap)
l Raiz com o maior valor e pais com valor ≥ que os filhos. l Heap Mínimo (MinHeap)
l Raiz com o menor valor e pais com valor ≤ que os filhos.
l MaxHeap é utilizado no método de ordenação Heapsort;
l Ambos os tipos de heaps podem ser utilizados para implementar filas de prioridade.
148
Heaps 16
10
9 3
14
8 7
2 4 1
1
2 3
4 5 6 7
8 9 10
1423978101416 1423978101416
1 2 3 4 5 6 7 8 9 10
Θ(lgn)
Índice no vetor
149
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).
150
Heaps - Estrutura
l A estrutura de um heap deve manter dois atributos l Comprimento: Número de elementos do vetor; l Tamanho do Heap: Número de elemento do
heap armazenados no vetor. O tamanho máximo do heap é o comprimento do vetor.
151
Manutenção de um MaxHeap
l É necessário manter as propriedades do heap durante as inserções e exclusões.
l Cada subárvore deve ser um heap máximo, portanto, um nó pai não pode ser menor que os nós filhos;
l Caso o nó pai seja menor que um dos filhos, ele trocará de posição com o maior deles;
l É aplicada recursivamente para garantir que uma mudança realizada não viola a propriedade em outras subárvores.
155
MAX-HEAPIFY - Código
void MAX_HEAPIFY(int A[], int i, int n)!{! int esquerdo;! int direito;! int maior;!! esquerdo = 2*i;! direito = 2*i+1;!! if(esquerdo <= n && A[esquerdo] > A[i])!
! maior = esquerdo;! else!
! maior = i;!! if (direito <= n && A[direito] > A[maior])!
! maior = direito;!! if(maior != i)! {! Troca(&A[i], &A[maior]);!
! MAX_HEAPIFY(A, maior, n);! }!}!
!!!!!!//determina o filho esquerdo!//determina o filho direito!!//se o filho esquerdo for!//maior que o pai, registra!//senão!//o maior é o pai mesmo!!//se o direito é maior que o maior!//registra!!//se o maior não é o pai!!//troca as posições!//verifica se a subárvore viola a !//propriedade!!!!
156
MAX-HEAPIFY - Complexidade
l Θ(1) para fazer as trocas em um mesmo nível; l Uma subárvore pode ter no máximo tamanho 2n/3; l No pior caso então, a complexidade é dada pela
recorrência )(lg)(
)1(32)(
nOnT
nTnT
==
Θ+⎟⎠
⎞⎜⎝
⎛=
(Pelo teorema mestre, caso 2)
157
Construção de um MaxHeap
l 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.
164
BUILD-MAX-HEAP - Código
void BUILD_MAX_HEAP(int A[],int n)!{! int i;!! for(i=n/2; i>0; i--)! MAX_HEAPIFY(A, i, n);!}!
!//Para cada uma das subárvores,!//verifica corrige a propriedade !//do heap!//folhas não são verificadas!
165
BUILD-MAX-HEAP Complexidade l 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.
166
Heaps - Inserção
l 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 O pai da nova chave é verificado, e caso
necessário, troca de lugar com o filho; l Estas trocas podem se estender em efeito
cascata semelhante ao que ocorre no método bolha de ordenação.
167
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!
}!}!
168
Heaps - Remoção
l A remoção em heaps não é realizada sobre um elemento aleatório l Em Heaps Máximos, o elemento de maior chave
é removido; l Em Heaps Mínimos, o elemento de menor chave
é removido. l A operação é chamada extração; l Após a extração, o procedimento
MAX_HEAPIFY é chamado para manter as propriedades do heap.
169
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!
}!}!
170
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 Complexidade O(logn);
l A extração realiza operações constantes e chama o procedimento MAX_HEAPIFY l Complexidade O(logn).