estruturas de dados - decom-ufop · dinâmicos, definimos a primeira estrutura de dados: a lista !...

171
Estruturas de Dados Marco Antonio Moreira de Carvalho Algoritmos e Estruturas de Dados

Upload: truongdan

Post on 12-Nov-2018

214 views

Category:

Documents


0 download

TRANSCRIPT

Estruturas de Dados

Marco Antonio Moreira de Carvalho Algoritmos e Estruturas de Dados

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.

28

Pilhas – Representação

8

32

15

442 18

push pop

início

topo

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.

34

Filas - Representação 42 05 18 23 26 61

InícioFim

Enqueue Dequeue

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.

74

Percurso em Largura A

B C

D E F G

H

1

2 3

4 5 6 7

8

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.

77

Percurso em Profundidade A

B C

D E F G

H

1

2 6

3 4 7 8

5

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.

89

Remoção – Caso 1 15

5

13

3 12

10

6

20

23

16

7

18

90

Remoção – Caso 2 15

5

13

3 12

10

6

20

23

16

7

18

91

Remoção – Caso 3 15

5

13

3 12

10

6

20

23

16

7

18

Sucessor

92

Remoção – Caso 3 15

6

13

3 12

10

7

20

23

16

18

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.

116

Árvores Vermelho-Preto 26

19

17

23

2114 47

41

30

28 38

393520

16

15

10

127

3

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.

126

Casos 2 e 3 A

C

α

γ

B

β

z

l  Caso 2, aplica-se uma rotação à esquerda

127

Casos 2 e 3 A

C

α

γ

B

β

z

l  Gera um caso 3, recolorimento e rotação à direita.

128

Casos 2 e 3 B

α γ

A

β

zC

δ

l  Violação corrigida. Não existem pai e filho vermelhos.

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

α

γ

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

α

γ

D

δ ε

x

ζ

E

w

c

A

B

α

γ

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

α

γ

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

α

γ

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.

152

MAX-HEAPIFY

16

10

9 3

4

14 7

2 8 1

1

2 3

4 5 6 7

8 9 10

153

MAX-HEAPIFY

16

10

9 3

14

4 7

2 8 1

1

2 3

4 5 6 7

8 9 10

154

MAX-HEAPIFY

16

10

9 3

14

8 7

2 4 1

1

2 3

4 5 6 7

8 9 10

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.

158

BUILD-MAX-HEAP

4

3

9 10

1

2 16

14 8 7

1

2 3

4 5 6 7

8 9 10

4 1 3 2 16 9 10 14 8 7

159

BUILD-MAX-HEAP 4

3

9 10

1

2 16

14 8 7

1

2 3

4 5 6 7

8 9 10

160

BUILD-MAX-HEAP 4

3

9 10

1

14 16

2 8 7

1

2 3

4 5 6 7

8 9 10

161

BUILD-MAX-HEAP 4

10

9 3

1

14 16

2 8 7

1

2 3

4 5 6 7

8 9 10

162

BUILD-MAX-HEAP 4

10

9 3

16

14 7

2 8 1

1

2 3

4 5 6 7

8 9 10

163

BUILD-MAX-HEAP 16

10

9 3

14

8 7

2 4 1

1

2 3

4 5 6 7

8 9 10

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).

171

MinHeaps

l  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.