Estruturas de Dados - DECOM-UFOP

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