Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Estruturas de Dados
Lição 1
Conceitos Básicos e Notações
Requerimentos de Software
NetBeans Enterprise Pack 5.5 executando sobre Java 2 Platform Standard Edition
Development Kit 5.0 ou superior (JDK 5.0, versão 1.5.0_01 ou superior), contemplando a
Java Runtime Environment, ferramentas de desenvolvimento para compilar, depurar, e
executar aplicações escritas em linguagem Java. Sun Java System Application Server
Platform Edition 9.
• Para Solaris, Windows, e Linux, os arquivos da JDK podem ser obtidos para sua
plataforma em http://java.sun.com/j2se/1.5.0/download.html
• Para Mac OS X, Java 2 Plataform Standard Edition (J2SE) 5.0 Release 4, pode ser
obtida diretamente da Apple's Developer Connection, no endereço:
http://developer.apple.com/java (é necessário registrar o download da JDK).
Estruturas de Dados 2
JEDITM
Auxiliadores especiais
Coordenação do DFJUG
Agradecimento Especial
John Paul Petines – Criador da Iniciativa JEDITM
Rommel Feria – Criador da Iniciativa JEDITM
Estruturas de Dados 3
JEDITM
1. Objetivos
Na criação de solução para os processos de resolução de problemas, existe a necessidade de
representação dos dados em nível mais alto a partir de informações básicas e estruturas disponíveis
em nível de máquina. Existe também a necessidade de se sintetizar o algoritmo a partir das
operações básicas disponíveis em nível de máquina para manipular as representações em alto nível.
Estas duas características são muito importantes para obtenção do resultado desejado. Estruturas
de dados (Data Structures) são necessárias para a representação dos dados, enquanto que os
algoritmos precisam operar no dado para obter a saída correta.
Nesta lição iremos discutir os conceitos básicos por detrás do Processo Resolução de Problemas
(Problem Solving), tipos de dados, tipos de dados abstratos, algoritmos e suas propriedades,
métodos de endereçamento, funções matemáticas e complexidade dos algoritmos.
Estruturas de Dados 4
JEDITM
Estruturas de Dados 5
JEDITM
Palavra-chave Descrição
byte Inteiro do tamanho de um byte
short Inteiro curto
int Inteiro
long Inteiro longo
float Ponto flutuante de precisão simples
double Ponto flutuante de precisão dupla
char Um único caractere
boolean Valor booleano (verdadeiro ou falso)
Tabela 1: Dados primitivos
Tipo de dado abstrato (Abstract Data Type – ADT) é um modelo matemático contendo uma
coleção de operadores. Ele especifica um tipo de dado armazenado, o que a operação faz, mas não
como é feito. Um ADT pode ser expresso por uma interface que contém apenas uma lista de
métodos. Por exemplo, esta é uma interface para a stack ADT:
Abstração e representação ajudam-nos a entender os princípios por detrás dos grandes sistemas de
software. Encapsulamento de informação pode ser utilizada junto com abstração para particionar um
sistema grande em subsistemas menores com interfaces simples que são mais fáceis de entender e
utilizar.
Estruturas de Dados 6
JEDITM
4. Algoritmo
Algoritmo é um conjunto finito de instruções que, se seguidas corretamente, completam uma
determinada tarefa. Possui cinco importantes propriedades: finito, definido, entrada, saída e efetivo.
Finito quer dizer que um algoritmo sempre terá um fim após um número finito de passos. Definido
é a garantia de que todos os passos do algoritmo foram precisamente definidos. Por exemplo:
“dividir por um número x” não é suficiente. O número x deve ser precisamente definido, ou seja, x
deve ser inteiro e positivo. Entrada é o domínio do algoritmo que pode ser nenhum (zero) ou vários.
Saída é o conjunto de um ou mais resultados que também é chamado de alcance do algoritmo.
Efetivo é a garantia de que todas as operações do algoritmo são suficientemente simples de
maneira que também possam, a princípio, ser executadas, em um tempo exato e finito, por uma
pessoa utilizando papel e caneta.
A classe acima obtém o valor mínimo de um array de inteiros. Não há entrada de dados por parte do
usuário uma vez que o array já está pronto dentro da classe. Para cada propriedade de entrada e
saída cada passo da classe é precisamente definido; neste ponto esta poderá ser definida. A
declaração do laço for e suas respectivas saídas terão um número finito de execução. Logo, a
propriedade finito é satisfeita. E quando executado, a classe retornará o valor mínimo entre os
valores do array, e por isso é dito efetivo.
Estruturas de Dados 7
JEDITM
5. Métodos de Endereçamento
Na criação de uma estrutura de dados é importante definir como acessar estes dados. Isto é
determinado pelo método de acesso a dados. Existem dois tipos de métodos de endereçamento –
método calculado e método de endereçamento – computado e por link.
Um item de dado pode ser acessado diretamente pelo índice de onde o dado está armazenado.
public Node(Object o) {
info = o;
}
public Node(Object o, Node n) {
info = o;
link = n;
}
}
O pool de memória é a fonte dos nodes, onde são construídas as estruturas de ligação. Também são
conhecidas como lista de espaços disponíveis (ou nodes) ou simplesmente lista disponível:
Estruturas de Dados 8
JEDITM
public AvailList() {
head = null;
}
enquanto o método a seguir na classe Avail retorna um node para a lista disponível:
Estruturas de Dados 9
JEDITM
Os dois métodos poderiam ser usados por estruturas de dados que usam alocação de ligação para
pegar os nodes e retorná-los para o pool de memória. E como teste final teremos a seguinte classe:
System.out.println(avail.getNode().info);
System.out.println(avail.getNode().link.info);
System.out.println(avail.getNode().link.link.info);
System.out.println(avail.getNode().link.link.link.info);
}
}
Estruturas de Dados 10
JEDITM
6. Funções Matemáticas
Funções matemáticas são úteis na criação e na análise de algoritmos. Nesta seção, algumas das
funções mais básicas e mais comumente usadas e suas propriedades serão mostradas.
• Floor de x – o maior inteiro menor que ou igual a x, onde x é um número real qualquer.
Notação: x
• Ceil de x – é o menor inteiro maior que ou igual a x, onde x é um número real qualquer.
Notação : x
x mod y =x se y = 0
=x-y* x/y se y <> 0
6.1. Identidades
Estruturas de Dados 11
JEDITM
7. Complexidade de Algoritmos
Diversos algoritmos podem ser criados para resolver um único problema. Estes algoritmos podem
variar no modo de obter, processar e dar saída nos dados. Com isso, eles podem ter diferença
significativa em termos de performance e utilização de memória. É importante saber como analisar
os algoritmos, e saber como medir a eficiência dos algoritmos ajuda bastante no processo de análise.
A eficiência dos algoritmos é medida através de dois critérios: utilização de espaço e eficiência de
tempo. Utilização de espaço é a quantidade de memória requerida para armazenar dados enquanto
eficiência de tempo é a quantidade de tempo gasta para processar os dados.
Antes de podermos medir a eficiência de tempo de um algoritmo, temos que obter o tempo de
execução. O tempo de execução é a quantidade de tempo gasto para se executar as instruções de
um dado algoritmo. Ele depende do computador (hardware) sendo usado. Para exibir o tempo de
execução, usamos a seguinte notação:
T(n), onde T é a função e n o tamanho da entrada.
Existem vários fatores que afetam o tempo de execução. Eles são:
• Tamanho da entrada
• Tipo da instrução
• Velocidade da máquina
• Qualidade do código-fonte da implementação do algoritmo
• Qualidade do código de máquina gerado pelo compilador
Embora T(n) forneça a quantidade real de tempo na execução de um algoritmo, é mais fácil
classificar as complexidades de algoritmos utilizando uma notação mais abrangente, a notação Big-O
(ou simplesmente O). T(n) cresce a uma taxa proporcional a n e dessa forma T(n) é dita como
tendo “ordem de magnitude n” denotada pela notação O:
T(n) = O(n)
Esta notação é usada para descrever a complexidade de tempo ou espaço de um algoritmo. Ela
fornece uma medida aproximada do tempo de computação de um algoritmo para um grande número
de dados de entrada. Formalmente, a notação O é definida como:
g(n) = O (f(n)) se existem duas constantes c e n0 tais que
| g(n) | <= c * | f(n) | para todo n >= n0
A seguir temos exemplos de tempos de computação sobre análise de algoritmos:
Estruturas de Dados 12
JEDITM
Para tornar clara a diferença, vamos efetuar a comparação baseada no tempo de execução onde
n=100000 e a unidade de tempo = 1 mseg:
Estruturas de Dados 13
JEDITM
então
Já que as iterações no laço mais interno serão executadas n + n-1 + n-2 + ... + 2 + 1 vezes,
então o tempo de execução será:
T(n) = 2n +4 = O(n)
Estruturas de Dados 14
JEDITM
2n + 4 <= cn
2n + 4 <= c
---------
n
2 + 4/n <= c
Assim c = 4 e n0 = 3.
Seguem abaixo as regras gerais para se determinar o tempo de execução de um algoritmo:
• Laços FOR
• A Análise é feita a partir do laço mais interior para fora. O tempo total de execução de
uma declaração dentro de um grupo de laço FOR é o tempo de execução da declaração,
multiplicado pelo produto dos tamanhos de todos os laços FOR.
• DECLARAÇÕES CONSECUTIVAS
• Condicional IF/ELSE
• Quanto maior o tempo de execução do teste, maior será o tempo de execução do bloco
condicional.
Estruturas de Dados 15
JEDITM
8. Exercícios
a) Funções Piso, Teto e Módulo. Compute para os valores resultantes:
a) -5.3
b) 6.14
c) 8 mod 7
d) 3 mod –4
e) –5 mod 2
f) 10 mod 11
g) (15 mod –9) + 4.3
a) 3n5 + 2n3 + 3n +1
b) n3/2+n2/5+n+1
c) n5+n2+n
d) n3 + lg n + 34
c) Imagine que tenhamos duas partes em um algoritmo, sendo que a primeira parte toma
T(n1)=n3+n+1, tempo para executar e a segunda parte toma T(n2) = n5+n2+n. Qual é a
complexidade do algoritmo, se a parte 1 e a parte 2 forem executadas uma de cada vez?
Estruturas de Dados 16
Módulo 3
Estruturas de Dados
Lição 2
Stack
1. Objetivos
Uma stack (pilha) é uma ordem linear de elementos obedecendo a regra: “o último que entrar é o
primeiro a sair”, é conhecida como listas LIFO (Last In First Out).
É semelhante a um conjunto de caixas em um armazém, onde só a caixa de topo pode ser retirada e
não há acesso às outras caixas. Quando adicionamos uma caixa, é sempre é colocada no topo.
Estruturas de Dados 4
JEDITM
2. Operações em Stack
Como já mencionado, Interfaces (Application Programming Interface ou API) são usadas para
implementar ADT em Java. Esta é a interface Java para stack:
public interface Stack {
public int size(); // retorna o tamanho da stack
public boolean isEmpty(); // verifica se está vazia
public Object top() throws StackException;
public Object pop() throws StackException;
public void push(Object item) throws StackException;
}
• Verificar o tamanho
• Verificar se está vazia
• Pegar o elemento do topo sem excluí-lo
• Inserir um novo elemento (push)
• Apagar um elemento do topo (pop)
Estruturas de Dados 5
JEDITM
3. Representação seqüencial
A alocação seqüencial de uma stack faz uso de arrays, conseqüentemente o tamanho é estático. A
stack está vazia se o topo=-1 e cheia se o topo=n-1. Tentar apagar um elemento de uma stack vazia
causa um underflow enquanto a inserção de um elemento em uma stack cheia causa um overflow.
A figura a seguir mostra um exemplo de uma stack ADT:
Estruturas de Dados 6
JEDITM
S[top--] = null;
return item;
}
Como resultado, lembre-se que a stack armazena “Empilhando” os dados, e retirá-os do último ao
primeiro elemento armazenado.
Estruturas de Dados 7
JEDITM
4. Representação Encadeada
Uma lista acoplada de nodes poderia ser utilizada para implementar uma stack. Na representação
acoplada, um node com a estrutura definida a seguir será usada:
class Node {
private Object info;
private Node link;
public Node(Object o, Node n) {
info = o;
link = n;
}
}
A figura seguinte mostra uma stack representada como uma lista linear encadeada:
Estruturas de Dados 8
JEDITM
top = top.link;
return temp.info;
}
Estruturas de Dados 9
JEDITM
1. Pegue o próximo caractere a ou b da string de entrada e insira na stack; repita até o símbolo
c ser encontrado.
2. Pegue o próximo caractere a ou b da string de entrada, abra a stack e compare. Se os dois
símbolos batem, continue, de outra maneira, pare – a string não está em L.
A seguir estão os estados adicionais nos quais a string de entrada pode ser encontrada para não
estar em L:
Estruturas de Dados 10
JEDITM
A seguir temos uma classe Java utilizada para implementar o padrão recognizer:
public class PatternRecognizer{
ArrayStack S = new ArrayStack(100);
// O ultimo character é c
if (i == input.length()) return false;
while (!S.isEmpty()) {
// Se o character de entrada e o outro no topo da stack não baterem
if (!(input.substring(i,i+1)).equals(S.pop()))
return false;
i++;
}
// A stack está vazia mas o fim da string não foi alcançada ainda
if (i < input.length()) return false;
Estruturas de Dados 11
JEDITM
}
}
Teorema: uma expressão postfix é bem moldada se o rank de todos os topos são maiores ou iguais
a 1 e o rank da expressão é 1.
Exemplos:
a^b^c-d abc^^d-
a*(b+(c+d)/e)-f a b c d + e /+* f -
a*b/c+f*(g+d)/(f–h)^i ab*c/fgd+*fh–i^/+
1. A ordem dos operandos nas duas formas são as mesmas se os parênteses estiverem ou não
presentes na expressão infix.
2. Se a expressão infix não contém parênteses, então a ordem dos operadores na expressão
postfix está de acordo com sua prioridade.
3. Se a expressão infix contém sub expressões em parênteses, a regra 2 se aplica do mesmo
Estruturas de Dados 12
JEDITM
Agora o algoritmo:
Estruturas de Dados 13
JEDITM
f - abc*d++f sai f
/ -/ abc*d++f icp(/)>isp(-), push /
g -/ abc*d++fg sai g
^ -/^ abc*d++fg icp(^)>isp(/), push ^
h -/^ abc*d++fgh sai h
- abc*d++fgh^/- retira ^, retira /, retira -
Tabela 6: Forma postfix
Estruturas de Dados 14
JEDITM
Se duas stack compartilham determinado array S, elas crescem e diminuem dentro do array S,
sendo que os finais estão frente a frente separados por um intervalo definido. A figura a seguir
mostra o comportamento de duas stack quando compartilham um mesmo array S:
Na inicialização, a stack 1 está com o topo marcado como sendo -1, desta forma, top1=-1 e para a
stack 2 será top2 = n.
B[m] = n-1
Os pontos B[i] determinam o espaço de uma stack. Para inicializar a stack, os topos serão marcados
como sendo a ponto base da outra stack formando células,
Por exemplo:
Estruturas de Dados 15
JEDITM
Figura 7. Três estados das stack (vazia, não vazia mas não cheia, cheia)
a parte do código Java a seguir mostra a implementação das operações de push e pop (inserir e
remover) para a classe Mstack:
class MStack {
int m = 3; // número de stacks, por padrão 3
int n = 300; // tamanho do array S
Object[] S;
int B[], T[], oldT[];
// Construtor padrão
public MStack() {
S = new Object[n];
B = new int[m+1];
T = new int[m];
oldT = new int[m];
}
Estruturas de Dados 16
JEDITM
Para fazer isto, procuramos nas stacks sobre a stack i (endereço-primário) pela mais próxima stack
com células disponíveis, chamamos stack k, e então trocaremos a stack i+1 acima da stack k uma
célula, até a célula esteja disponível para a stack i. Se todas as stacks sobre a stack i estão cheias,
então procuramos as stacks abaixo até a mais próxima stack com espaço livre, chamamos de stack
k, e então trocamos as células uma unidade para baixo. Isto é conhecido como o método de unit-
shift. Se k possui um valor inicial igual a -1, então as seguintes implementações de código para o
método unitShift que será chamado pelo método MStackFull:
/* capturando estouro de stack (stack overflow) usando o policiamento unidade
de troca(Unit-Shift) e retorna true se obteve sucesso, caso contrário false */
Estruturas de Dados 17
JEDITM
Estruturas de Dados 18
JEDITM
α = 10% * freecells / m
onde:
- m= número de stack
- α é o número de células que cada stack possui dos 10% do espaço válido alocado
- β é o número de células que a stack terá por unidade incrementada do uso da stack dos
90% do espaço livre restantes.
inicialmente, (new)B[0] = -1 e σ = 0
for j = 1 to m-1:
τ = σ + α + diff[j-1]*β
σ=τ
5. Atribuindo oldT = T
Considere o seguinte exemplo. 5 stack coexistindo num vector de 500 posições. O estado das stacks
é mostrado na figura abaixo:
Estruturas de Dados 19
JEDITM
fator Valor
Tamanho das stack Tamanho = (80, 120+1, 60, 35, 23)
Diferenças Diferença = (0, 15+1, 16, 0, 8)
freecells 500 - (80 + 120+1 + 60 + 35 + 23) = 181
incr 0 + 15+1 + 16 + 0 + 8 = 40
Tabela 7: Fator e valor
B[0] = -1 and σ = 0
for j = 1 to m:
τ = σ + α + diff(j-1)*β
σ=τ
= -1 + 80 + 3.62 – 0 = 82
Estruturas de Dados 20
JEDITM
σ = 3.62
σ = 72.4
σ = 141.18
σ = 144.8
T[i] = B[i] + size [i] ==> T = (0+80, 83+121, 273+60, 402+35, 440+23)
Existem algumas técnicas para melhorar a utilização das stack. Primeiro, saiba qual stack é a maior,
faça isso primeiro. Segundo, o algoritmo pode emitir um comando de parada quando o espaço livre
se tornar menor que um valor mínimo especificado que não 0, quer dizer um minfree (mínimo livre),
onde o usuário possa especificar esse valor mínimo.
Além de Stacks, o algoritmo pode ser capacitado para realocar espaço para mais dados em outros
Estruturas de Dados 21
JEDITM
O método a seguir implementa um algoritmo Garwick's que será chamado pelo método MStackFull:
// Método Garwick's
public void garwicks(int i) throws StackException {
int diff[] = new int[m];
int size[] = new int[m];
int totalSize = 0;
double freecells, incr = 0;
double alpha, beta, sigma=0, tau=0;
diff[i]++;
size[i]++;
totalSize++;
incr++;
freecells = n - totalSize;
alpha = 0.10 * freecells / m;
beta = 0.90 * freecells / incr;
// Restabeleça tamanho da stack que teve overflowed para o seu antigo valor
size[i]--;
Estruturas de Dados 22
JEDITM
Estruturas de Dados 23
JEDITM
7. Exercícios
1. Converta as expressões seguintes da forma infix para postfix e mostre a stack resultante.
a) a+(b*c+d)-f/g^h
b) 1/2-5*7^3*(8+11)/4+2
5. Três stack coexistem em um array de tamanho 300. Uma inserção é tentada na stack 3. O estado
de computação está definido por:
OLDT(0:2) = (65, 180, 245)
T(0:2) = (80, 140, 299)
B(0:3) = (-1, 101, 215, 299)
2. Crie uma classe Java que implementa a conversão bem-formada de uma expressão infix e seu
postfix equivalente.
3. Implemente a conversão de infix para postfix usando uma implementação encadeada de stack. A
classe solicitará uma entrada do usuário e verificará se a entrada está correta. Mostre a produção e
conteúdo da stack em toda interação.
4. Crie uma definição de classe Java de uma stack múltipla em um array dimensional. Implemente
as operações básicas em stack (push e pop, etc) para ser aplicável em múltipla Stack. O nome da
classe será MStack.
5. Uma loja de livro tem estantes com divisórias ajustáveis. Quando uma divisória fica cheia, a
divisória será ajustada para obter mais espaço. Crie uma classe Java que relocará o espaço da
estante usando o algoritmo de Garwick.
Estruturas de Dados 24
Módulo 3
Estruturas de Dados
Lição 3
Queue
1. Objetivos
Uma queue (fila) é um conjunto de elementos ordenado linearmente que têm as características
First-In (Primeiro a entrar) e First-Out (Primeiro a sair). Conhecido também como lista FIFO.
Existem duas operações básicas para queue: (1) inserção no final, e (2) remoção no início.
Estruturas de Dados 4
JEDITM
2. Representação de Queue
Para definir uma queue em Java, devemos usar a seguinte interface:
interface Queue{
// Insere um item
void enqueue(Object item) throws QueueException;
// Remove um item
Object dequeue() throws QueueException;
}
Assim como na stack, devemos usar o seguinte código de Exception a fim de tratarmos as exceções:
front = 0;
rear = 0;
Estruturas de Dados 5
JEDITM
Q [rear] = item;
Rear ++;
x = Q[front];
front ++;
Sempre que uma remoção é feita, um espaço vago é criado na frente da queue. Portanto, existe a
necessidade de mover os itens de forma que o espaço vazio fique no fim da queue para futuras
inserções. O método moveQueue executa esse procedimento. Esse método é chamado pelo código
abaixo:
Estruturas de Dados 6
JEDITM
front = 0;
}
Como resultado, lembre-se que a queue armazena “Enfileirando” os dados, e retirá-os do primeiro ao
último elemento armazenado.
Representação encadeada também pode ser utilizada para uma queue. Da mesma forma que para
stacks, utilizará nodes com os campos INFO e LINK. Na representação acoplada, um node com a
estrutura definida a seguir será utilizado:
class Node {
private Object info;
private Node link;
public Node(Object o, Node n) {
info = o;
link = n;
}
}
A figura a seguir mostra uma queue implementada como uma queue encadeada:
Estruturas de Dados 7
JEDITM
Estruturas de Dados 8
JEDITM
3. Queue Circular
Uma desvantagem da implementação seqüencial anterior é a necessidade de se mover os elementos,
no caso de rear ser igual a n e front ser maior que 0, para abrir espaço a fim de inserir um novo
elemento. Se a queue fosse vista como um círculo não haveria necessidade de executar esse
movimento. Em uma queue circular, os elementos são considerados como se estivessem organizados
dentro de um círculo. O front aponta para o elemento atual no início da queue, enquanto o rear
aponta para o elemento à direita do último, no momento (sentido horário). A figura a seguir mostra
uma queue circular:
Para apagar:
x = Q[front];
front = (front + 1) mod n;
Utilizamos a função MOD (módulo) para realizar um teste no início e final da queue. Quando
inserções e remoções são feitas, a queue é movimentada em sentido horário. Se o início alcançar o
final, isto é, se front é igual a rear, então teremos uma queue vazia. Se o final alcançar o início, uma
condição também indicada por front igual a rear, então todos os elementos estão em uso e teremos
uma queue cheia.
Para evitarmos ter a mesma relação significando duas condições diferentes, não permitiremos que o
final alcance o início considerando que a queue está cheia quando existir apenas uma célula livre.
Portanto a queue cheia é indicada por:
front == (rear + 1) mod n
Estruturas de Dados 9
JEDITM
Estruturas de Dados 10
JEDITM
É definida como uma relação entre os elementos de um conjunto S, caracterizada pelo símbolo ≼
(que é lido como 'precede ou igual a'). A seguir estão as propriedades da condição parcial ≼:
• Transitividade: se x ≼ y e y ≼ z, então x ≼ z
• Anti-simetria: se x ≼ y e y ≼ x, então x = y
• Reflexividade: x ≼ x
Resultado. Se x ≼ y e x ≠ y então x ≺ y.
• Transitividade: se x ≺ y e y ≺ z, então x ≺ z
• Simetria: se x ≺ y então y ≺ x
• Não-Reflexividade: x ≺ x
4.2. Algoritmo
Estruturas de Dados 11
JEDITM
• Input (Entrada): um conjunto de pares com a forma (i, j) para cada relação i ≼ j que poderia
representar o ordenamento parcial dos elementos. Os pares de entrada podem estar em
qualquer ordem;
• Output (Saída): O algoritmo se torna uma seqüência linear de itens, de modo que nenhum
item aparece na seqüência antes de seu antecessor direto;
• Algoritmo apropriado: Uma condição da Classificação Topológica é não visualizar/imprimir os
itens cujos seus antecessores ainda não tenham executado esta tarefa. Para fazer isso, é
necessário manter os números dos antecessores em cada item. Um array pode ser usado
para esse propósito. Chamaremos esse array de COUNT (contador). Quando um item é
enviado à saída o count de cada sucessor do item é decrementado. Se o count de um item
é zero, ou se torna zero como resultado de todos os seus antecessores terem sido enviados à
saída, esse seria o tempo em que os itens estão prontos para a visualização/impressão. Para
manter-se a par dos sucessores, uma lista de ligações, chamada SUC, com a estrutura
(INFO, LINK), será usada, onde INFO contém o rótulo do sucessor direto enquanto LINK
aponta para o próximo sucessor, se existir.
class Node {
int info;
Node link;
}
O COUNT (array contador) é inicialmente definido como 0 e o array SUC como null para cada
entrada do par (i, j),
COUNT[j]++;
Estruturas de Dados 12
JEDITM
Segue exemplo:
Para gerar a saída, que é uma classificação linear de objetos de modo que nenhum objeto apareça
na seqüência antes de seu antecessor direto, procedemos da seguinte maneira:
1. Procuramos por um item, digamos k, com o contador dos antecessores diretos igual a zero,
ex., COUNT[k]==0. Coloque k na saída;
2. Buscamos a lista de sucessores diretos de k, e decrementamos 1 do contador de cada
sucessor;
3. Repetir passos 1 e 2 até que todos os itens estejam na saída.
Para evitar percorrer todo o array COUNT repetidamente enquanto procuramos por objetos com o
contador igual a zero, iremos constituir todos os objetos em uma queue encadeada. Inicialmente, a
queue irá consistir de itens sem antecessor direto (sempre haverá ao menos um item).
Subseqüentemente, cada vez que o contador de antecessores diretos de um item cair para zero, este
será inserido na queue, pronto para a saída. Desde que para cada item, digamos j, com seu
contador igual a 0, podemos reutilizar COUNT [j] como um campo de ligação de modo que:
Se a entrada para o algoritmo estiver correta, isto é, se as relações de entrada satisfazem a condição
parcial, o algoritmo termina quando a fila estiver vazia e com todos os objetos colocados na saída.
Se, por outro lado, a condição parcial é violada de modo que existam objetos que constituem um ou
mais laços de repetição (por exemplo, 1≺2; 2≺3; 3≺4; 4≺1), ainda assim este algoritmo termina,
mas objetos incluídos nos laços de repetição não serão colocados na saída.
Estruturas de Dados 13
JEDITM
Essa abordagem da Classificação Topológica usa tanto técnicas seqüenciais quanto técnicas
encadeamento de alocamento, e o uso de uma queue encadeada inserida em um array seqüencial.
Estruturas de Dados 14
JEDITM
5. Exercícios
a) Ordenação Topológica. Dado o ordenamento parcial de sete elementos, como eles podem ser
arranjados de forma que nenhum elemento apareça na seqüência antes de seu antecessor direto?
1. (1,3), (2,4), (1,4), (3,5), (3,6), (3,7), (4,5), (4,7), (5,7), (6,7), (0,0)
2. (1,2), (2,3), (3,6), (4,5), (4,7), ( 5,6), (5,7), (6,2), (0,0)
1. Crie uma execução de múltiplas queue que coexista num array simples. Use o algoritmo de
Garwick's para realocação de memória durante o overflow.
Execute uma relação de matérias utilizando o algoritmo de ordenação topológica. A classe deve
solicitar um arquivo que contenha o conjunto de matérias e a ordenação parcial destas. As matérias
devem estar na seguinte forma, no arquivo, (número, matéria) onde número é um inteiro
atribuído a matéria e matéria é o identificador do curso [ex.: (1, CS 1)]. Cada par (número,
matéria) deve estar em linhas separadas no arquivo. Para terminar a inclusão deve-se usar o par
(0, 0). Os pré-requisitos das matérias devem ser obtidos a partir do mesmo arquivo. A definição de
um pré-requisito deve estar na forma (i, j), uma linha por par, onde i é um número atribuído ao
pré-requisito da matéria de número j. O último pré-requisito deve ser o par (0, 0).
A saída deve ser também em um arquivo, e seu nome deve ser solicitado ao usuário. A saída deve
ser em uma tabela contendo o número do semestre (um número auto-incrementável de 1 a n) junto
com as matérias do mesmo.
Estruturas de Dados 15
Módulo 3
Estruturas de Dados
Lição 4
Árvores Binárias
1. Objetivos
Uma árvore binária é um tipo de dado abstrato que é estruturalmente hierárquico. É uma coleção de
nodes que pode estar vazia ou pode consistir de uma raiz e duas árvores binárias distintas chamadas
de sub-árvores à esquerda e à direita. É semelhante a uma árvore no sentido de que existe o
conceito de uma raiz, galhos e folhas. Entretanto, diferem na orientação já que a raiz de uma árvore
binária está no topo como primeiro elemento, ao contrário do que ocorre com uma árvore real na
qual a raiz localiza-se no final da árvore como último elemento.
Árvores Binárias são mais utilizadas em pesquisa, classificação, localização eficiente em cadeias de
caracteres, listas de prioridades, tabelas de decisão e tabelas de símbolos.
Estruturas de Dados 4
JEDITM
O nível de um node se refere à distância do node à raiz. Portanto, a raiz da árvore tem nível 0, as
suas sub-árvores têm nível 1 e assim por diante. A altura ou profundidade de uma árvore é o nível
dos seus nodes mais inferiores, que também é o tamanho do maior caminho da raiz para qualquer
folha. Por exemplo, a árvore binária a seguir possui altura 3:
Um node é externo, se não tiver children, caso contrário ele é interno. Se dois nodes tiverem o
mesmo pai, são irmãos. O ancestral de um node é ele próprio ou um ancestral de seu pai.
Inversamente, o node u é um descendente do node v se v é um ancestral do node u.
Uma árvore binária pode estar vazia. Se a árvore binária tiver zero ou dois children, é classificada
Estruturas de Dados 5
JEDITM
como uma árvore binária equilibrada ou balanceada. Deste modo, cada árvore binária
equilibrada possui nodes internos com dois children ou nenhum.
A figura abaixo mostra os diferentes tipos de árvores binárias: (a) mostra uma árvore binária vazia;
(b) mostra uma árvore binária com apenas um node, a raiz; (c) e (d) mostram árvores sem children
à direita e à esquerda respectivamente; (e) mostra uma árvore binária inclinada à esquerda
enquanto (f) mostra uma árvore binária completa.
Uma árvore binária pode ser classificada como degenerada, estritamente binária, cheia ou completa.
Uma árvore binária degenerada à direita (esquerda) é uma árvore em que os nodes não têm
sub-árvores à esquerda (direita). Dado um número de nodes, uma árvore binária degenerada à
esquerda ou à direita tem profundidade máxima.
Estruturas de Dados 6
JEDITM
Uma árvore estritamente binária é uma árvore em que todos os nodes têm duas sub-árvores ou
nenhuma sub-árvore.
Uma árvore binária cheia é uma árvore estritamente binária em que todos os nodes terminais
estão no nível mais baixo. Dada uma profundidade, esta árvore tem o número máximo de nodes.
Uma árvore binária completa é uma árvore que resulta quando zero ou mais nodes são deletados
de uma árvore binária cheia em ordem reversa de nível, isto é, da direita para a esquerda e de baixo
para cima.
Estruturas de Dados 7
JEDITM
Estruturas de Dados 8
JEDITM
Estruturas de Dados 9
JEDITM
Na busca pré-ordem de uma árvore binária, a raiz é primeiro node a ser visitado. Depois os children
são percorridos recursivamente da mesma maneira. Este algoritmo é útil nas aplicações que
requerem a listagem de elementos onde os pais sempre devem aparecer antes de seus children.
Método:
Se a árvore binária estiver vazia, não faça nada (busca finalizada).
Caso contrário:
Visite a raiz.
Percorra a sub-árvore esquerda em pré-ordem.
Percorra a sub-árvore direita em pré-ordem.
Em Java, adicione o seguinte método à classe BinaryTree:
// Listagem pré-ordem dos elementos da árvore
public void preorder() {
if (root != null) {
System.out.println(root.getInfo().toString());
new BinaryTree(root.getLeft()).preorder();
new BinaryTree(root.getRight()).preorder();
}
}
A busca em ordem de uma árvore binária pode ser definida, informalmente, como a pesquisa “da
esquerda para a direita” de uma árvore binária. Isto é, a sub-árvore da esquerda é percorrida
recursivamente em ordem, seguida por uma visita ao seu node pai, e finalizando com a pesquisa
recursiva também em ordem da sub-árvore direita.
Estruturas de Dados 10
JEDITM
Método:
Se a árvore binária estiver vazia, não faça nada (busca finalizada).
Caso contrário:
Percorra a sub-árvore esquerda em ordem.
Visite a raiz.
Percorra a sub-árvore direita em ordem.
Em Java, adicione o seguinte método à classe BinaryTree:
// Listagem em ordem dos elementos da árvore
public void inorder() {
if (root != null) {
new BinaryTree(root.getLeft()).inorder();
System.out.println(root.getInfo().toString());
new BinaryTree(root.getRight()).inorder();
}
}
Método:
Se a árvore binária estiver vazia, não faça nada (percorrimento finalizado).
Caso contrário:
Estruturas de Dados 11
JEDITM
Estruturas de Dados 12
JEDITM
Duas árvores binárias são equivalentes se uma é cópia exata da outra. O seguinte algoritmo verifica
a equivalência entre duas árvores binárias:
1. Verifique se o node α e o node β contêm os mesmos dados
2. Percorra as sub-árvores da esquerda, nodes α e β em pré-ordem e verifique se são equivalentes
3. Percorra as sub-árvores da direita, nodes α e β em pré-ordem e verifique se são equivalentes
Estruturas de Dados 13
JEDITM
System.out.println("Preorder(bt1): ");
bt1.preorder();
System.out.println("Inorder(bt1): ");
bt1.inorder();
System.out.println("Postorder(bt1): ");
bt1.postorder();
System.out.println(bt1.equivalent(bt2));
System.out.println(bt1.equivalent(bt3));
}
Estruturas de Dados 14
JEDITM
6.1. Shift-Up
Uma árvore binária completa pode ser convertida em uma stack por meio da aplicação de um
processo chamado shift-up. Neste processo, chaves maiores “sobem” a árvore para satisfazer a
propriedade de ordenamento de stacks. Este é um processo que se dá de baixo para cima e da
direita para a esquerda, durante o qual as sub-árvores menores de uma árvore binária completa são
convertidas em stacks, seguido pela conversão das sub-árvores que as contêm, e assim por diante,
até que toda a árvore binária seja convertida em uma stack.
Estruturas de Dados 15
JEDITM
Observe que quando uma sub-árvore com raiz em algum node, por exemplo α, é convertida em uma
stack, as sub-árvores da esquerda e da direita, vinculadas à mesma, já são stacks. Este tipo de sub-
árvore é denominada quase-stack. Quando uma quase-stack é convertida em uma stack, pode
ocorrer de uma de suas sub-árvores deixar de ser uma stack (ex. Pode se tornar uma quase-stack).
A mesma, porém, pode ser convertida em uma stack e o processo continua com sub-árvores
pequenas e outras menores ainda perdendo e reconquistando a propriedade de stack, com chaves
MAIORES migradas para o topo.
Para testarmos os exemplos, criamos uma nova classe denominada SequentialHeap e a iniciamos
com o seguinte código:
public class SequentialHeap {
int key[];
public SequentialHeap(){
key = new int[10];
}
public SequentialHeap(int size){
key = new int[size];
}
public SequentialHeap(int k[]){
key = k;
}
}
Uma árvore binária completa pode ser representada seqüencialmente em um vetor unidimensional
de tamanho n em ordem de nível. Se os nodes de uma árvore são numerados, como ilustrado
abaixo,
pode então ser representada seqüencialmente por meio de um vetor de nome CHAVE, conforme
demonstrado abaixo:
Estruturas de Dados 16
JEDITM
A representação seqüencial de uma árvore binária completa possibilita a localização dos children (se
houver), do pai (se existir) e do pai de um node em tempo constante, por meio da utilização da
seguinte fórmula:
• se 2i ≤ n, o child à esquerda do node i é 2i; senão, o node i não possui nenhum child à
esquerda
• se 2i + 1 ≤ n, o child à direita do node i é 2i + 1; senão, o node i não possui nenhum child
à direita
• se 1 < i ≤ n, o pai do node i é (i)/2
Heapsort é um algoritmo elegante de ordenação que foi desenvolvido em 1964 por R. W. Floyd e J.
W. J. Williams. O heap é a base deste algoritmo de ordenação elegante:
1. Atribua as chaves a serem ordenadas aos nodes de uma árvore binária completa
2. Converta esta árvore binária em um heap aplicando o método shift-up aos seus nodes em
ordem reversa de nível
3. Repita o seguinte até que o heap esteja vazio:
(a) Remova a chave na raiz do heap (o menor valor no heap) e coloque-o na saída
(b) Extraía do heap o node-folha mais à direita no nível mais baixo, obtenha sua chave
e armazene-a na raiz do heap
(c) Aplique shift-up à raiz para converter a árvore binária em um heap novamente
Estruturas de Dados 17
JEDITM
Estruturas de Dados 18
JEDITM
Estruturas de Dados 19
JEDITM
Estruturas de Dados 20
JEDITM
Estruturas de Dados 21
JEDITM
Estruturas de Dados 22
JEDITM
Estruturas de Dados 23
JEDITM
7. Exercícios
1. Varredura. Percorra a seguinte árvore em pré-ordem, em ordem e pós-ordem.
a) b)
c)
a) C G A H F E D J B I
b) 1 6 3 4 9 7 5 8 2 12 10 14
1. Heaps podem ser usadas para avaliação de expressões. Um método alternativo é a utilização de
árvores binárias. Os usuários vêem as expressões na forma pré-fixada, mas a forma pós-fixada é
a mais adequada para que computadores avaliem as expressões. Neste exercício de programação,
crie um algoritmo que faça a conversão da expressão pré-fixada para sua forma pós-fixada,
utilizando árvore binária. Cinco operações binárias são apresentadas e estão listadas aqui de
acordo com a precedência:
Estruturas de Dados 24
JEDITM
Operação Descrição
^ Exponenciação (maior precedência)
*/ Multiplicação e Divisão
+- Adição e Subtração
Estruturas de Dados 25
Módulo 3
Estruturas de Dados
Lição 5
Árvores
1. Objetivos
Árvores podem ser ordenadas, orientadas ou livres. A alocação de ponteiros pode ser usada para
representar árvores e estas podem ser representadas seqüencialmente usando a representação
aritmética da árvore. Zero ou mais árvores separadas fazem juntas uma floresta e esta ordenada
pode ser convertida em uma árvore binária única e vice-versa usando correspondência natural.
Ao final desta lição, o estudante será capaz de:
• Discutir os conceitos básicos e definições de árvores
• Identificar os tipos de árvores: ordenadas, orientadas e árvores livres
• Usar a representação de árvores com ponteiros
• Explanar os conceitos básicos e definições sobre florestas
• Converter uma floresta na sua representação de árvore binária e vice-versa usando a
correspondência natural
• Percorrer uma floresta usando o processo pré-ordem, pós-ordem, por nível e por família
• Criar representações de árvores usando a alocação seqüencial
• Utilizar a representação aritmética de árvores
• Utilizar árvores em uma aplicação: O problema da equivalência
Estruturas de Dados 4
JEDITM
Uma árvore ordenada é um conjunto finito, chamado T, de um ou mais nodes de modo que há um
node especialmente chamado de raiz, e os demais nodes raiz são particionados em n ≥ 0 conjuntos
disjuntos (sem elementos em comum) T1, T2, ... , Tn, onde cada um desses conjuntos é por sua vez
uma árvore ordenada. Em uma árvore ordenada, a ordem de cada node na árvore é importante.
O grau de uma árvore é definido como o grau do(s) node(s) com o maior grau. Por essa razão, a
árvore acima tem o grau 3.
Uma árvore orientada é uma árvore em que a ordem de cada subárvore de cada node da árvore é
secundário.
No exemplo acima, as duas árvores são duas árvores orientadas diferentes, mas são a mesma
árvore.
Estruturas de Dados 5
JEDITM
Uma árvore livre não tem um node designado como raiz e a orientação de um node para outro é sem
importância.
Quando em uma árvore livre é designado um node raiz, ela torna-se uma árvore orientada. Quando
a ordem dos nodes é definida em uma árvore orientada, ela torna-se uma árvore ordenada. O
seguinte exemplo demonstra essa progressão.
Estruturas de Dados 6
JEDITM
Na estrutura de nodes, k é o grau da árvore. SON 1, SON2, ..., SONk são ponteiros para os possíveis k
filhos de um node.
Aqui temos algumas propriedades de uma árvore com n nodes e com grau k:
• O número de campos ponteiros é igual a n*k
• O número de ponteiros não vazios é igual a n-1 (Número de ramos)
• O número de ponteiros vazios é igual a n*k – (n-1), ou seja, n(k-1) + 1
A representação ligada é a maneira mais natural de representar uma árvore. Porém, devido às
propriedades acima, uma árvore com grau 3 terá 67% de ponteiros nulos, enquanto que em uma
árvore com grau 10, o espaço vazio será de 90%. Uma perda considerável de espaço é introduzida
por essa abordagem. Caso a utilização de espaço seja um assunto importante, podemos optar por
usar a estrutura alternativa.
Com essa estrutura, LEFT aponta para o filho à esquerda do node enquanto RIGHT aponta para o
próximo irmão mais novo.
Estruturas de Dados 7
JEDITM
4. Florestas
Quando zero ou mais árvores disjuntas são associadas, são conhecidas como florestas. A seguir um
exemplo de floresta:
Figura 7: Floresta F
Se as árvores que compreendem a floresta são árvores ordenadas e se a sua ordem na floresta é
essencial, ela é conhecida como floresta ordenada.
Uma floresta ordenada, digamos F, pode ser convertida em uma árvore binária única, digamos B(F),
e vice-versa, usando um processo bem definido conhecido como correspondência natural.
Formalmente:
Seja F = (T1, T2, ..., Tn) uma floresta ordenada de árvores ordenadas. A árvore binária
B(F) correspondente a F é obtida da seguinte maneira:
2. Remover ligações de um pai para todos os seus filhos exceto o filho mais velho (ou
mais à esquerda).
O exemplo a seguir ilustra a transformação de uma floresta em sua árvore binária equivalente:
Estruturas de Dados 8
JEDITM
Em correspondência natural, a raiz é trocada pela raiz da primeira árvore, a subárvore esquerda é
trocada pelas subárvores da primeira árvore e a subárvore direita é trocada pelas árvores
remanescentes.
Como em árvores binárias, florestas podem ser atravessadas (percorridas). Contudo, uma vez que o
conceito de node intermediário não esteja definido, uma floresta somente pode ser atravessada em
pré-ordem e pós-ordem.
Se a floresta estiver vazia, o atravessar é considerado executado; senão:
Estruturas de Dados 9
JEDITM
• Atravessar em Pré-Ordem
• Visitar a raiz da primeira árvore
• Atravessar as subárvores da primeira árvore em pré-ordem
• Atravessar as árvores restantes em pré-ordem
• Atravessar em Pós-Ordem
• Atravessar as subárvores da primeira árvore em pós-ordem
• Visitar a raiz da primeira árvore
• Atravessar as árvores remanescentes em pós-ordem
Figura 9: Floresta F
Floresta pré-ordem : A B C D E F G H I K J L M N
Floresta pós-ordem : C D E F B A H K I L J G N M
A árvore binária equivalente da floresta resultará na seguinte listagem para pré-ordem em-ordem e
pós-ordem
B(F) pré-ordem :ABCDEFGHIKJLMN
B(F) em-ordem :CDEFBAHKILJGNM
B(F) pós-ordem :FEDCBKLJIHNMGA
Observe que a floresta pós-ordem produz o mesmo resultado que na floresta em-ordem B(F). Não é
coincidência.
Estruturas de Dados 10
JEDITM
Florestas podem ser implementadas usando representação seqüencial. Considere a floresta F e sua
árvore binária correspondente. Os exemplos a seguir mostram a árvore usando representações
seqüenciais de pré-ordem, família-ordem e nível-ordem.
Nesta representação seqüencial, os elementos são listados nas suas seqüências pré-ordem e dois
arrays adicionais são mantidos – RLINK e LTAG. RLINK é um ponteiro de um irmão mais velho para
um irmão mais novo na floresta, ou de um pai para o seu filho da direita na sua representação de
árvore binária. LTAG indica se um node é terminal ( node folha) e o símbolo ')' é usado como
indicativo.
RLINK contém o node apontado pelo node atual e LTAG tem um valor de 1 para cada ')' na
representação.
Uma vez que o node final sempre precede imediatamente um node apontado por uma seta, exceto o
último node na seqüência, o uso do dado pode ser diminuído pela
(1) Eliminação de LTAG; ou
(2) Substituição de RLINK com RTAG que simplesmente identifica os nodes onde uma seta procede
Usando a segunda opção, será necessária a utilização de uma stack para estabelecer a relação entre
os nodes, já que as setas têm estrutura “último a entrar, primeiro a sair”, e usando-a levará a
seguinte representação:
Estruturas de Dados 11
JEDITM
Tendo valores de bit para RTAG e LTAG, a última opção mostra claramente o menor espaço
requerido para armazenamento. Porém, acarreta mais processamento na recuperação da floresta.
Nesta representação seqüencial, a listagem por família de elementos é usada. Atravessar por família,
a primeira família a ser listada consiste de nodes raízes de todas as árvores na floresta e
subseqüentemente, as famílias são listadas com base em primeiro-a-entrar primeiro-a-sair
(FIFO). Esta representação faz uso de LLINK e RTAG. LLINK é um ponteiro para o filho mais à
esquerda de um node ou o filho à esquerda numa representação de árvore binária. RTAG identifica o
irmão mais novo na linhagem ou o último membro da família.
Assim como na representação seqüencial pré-ordem, uma vez que um valor RTAG sempre precede
imediatamente uma seta, exceto para o último node da seqüência, uma estrutura alternativa é a
substituição de LLINK com LTAG, que é determinado se uma seta provier dele:
Estruturas de Dados 12
JEDITM
A terceira opção de representação seqüencial é o percurso por nível. Nesta representação, a floresta
é atravessada por nível, por exemplo, de-cima-para-baixo, da-esquerda-para-direita, para obter a
listagem de elementos por nível. Assim como no percurso por família, irmãos (os quais constituem
uma família) são listados consecutivamente. LLINK e RTAG são usados na representação. LLINK é
um ponteiro para o filho mais velho na floresta ou o filho da esquerda na sua representação em
árvore binária. RTAG identifica o irmão mais novo na descendência (ou o último membro da família).
Usando a representação seqüencial de percurso por nível, temos o seguinte para a floresta F:
Observe que ao contrário da pré-ordem ou do percurso por família, as setas se cruzam nesta
representação. Todavia, é possível ser observado que a primeira cruz a começar é também a
primeira a finalizar. Tendo a estrutura FIFO (first-in, first-out), uma queue poderia ser utilizada para
estabelecer o relacionamento entre os nodes. Conseqüentemente, assim como nos métodos
anteriores, poderia ser representada como:
Em termos de espaço, a representação seqüencial é ideal para florestas. Todavia, uma vez que a
representação por link é mais natural para florestas, existem instâncias em que teremos que utilizar
esta última. A classe a seguir implementa um método para conversão de representação seqüencial
para a representação por link:
Estruturas de Dados 13
JEDITM
if (LTAG[i] == 1){
alpha.setLeft(null);
sigma = (BTNode) stack.pop();
sigma.setRight(beta);
} else
alpha.setLeft(beta);
alpha.setInfo(INFO[i]);
alpha = beta;
}
// Preenche os campos do node mais a direita
alpha.setInfo(INFO[n-1]);
return t;
}
public static void main(String args[]) {
int[] RTAG = {1,0,1,1,1,0,1,1,1,0,0,0,0,0};
int[] INFO = {'A','B','C','D','E','F','G','H','I','K','J','L','M','O'};
int[] LTAG = {0,0,1,1,1,1,0,1,0,1,0,1,0,1};
SeqForest f = new SeqForest(RTAG, INFO, LTAG);
BinaryTree b = f.convert();
b.preorder();
}
}
Estruturas de Dados 14
JEDITM
Estruturas de Dados 15
JEDITM
O problema de equivalência é uma outra aplicação que faz uso de uma árvore internamente
representada como uma árvore aritmética.
Uma relação de equivalência é uma relação entre os elementos de uma coleção de objetos S que
satisfaçam as 3 propriedades para qualquer objeto x, y e z (não necessariamente distintos) em S:
Estruturas de Dados 16
JEDITM
São dadas as relações de equivalência i ≡ j onde i e j são raízes. Para unir as duas classes, dizemos
que a raiz de i é um novo child (tronco) da raiz de j. Este é o processo de união, e o código que o
implementa é:
while (FATHER[i] > 0)
i = FATHER[i];
while (FATHER[j] > 0)
j = FATHER[j];
if (i != j)
FATHER[j] = k;
Entrada Floresta
1 ≡ 10
1 2 3 4 5 6 7 8 9 10 11 12 13
10 0 0 0 0 0 0 0 0 0 0 0 0
9 ≡ 12
Estruturas de Dados 17
JEDITM
1 2 3 4 5 6 7 8 9 10 11 12 13
10 0 0 0 0 0 0 0 12 0 0 0 0
9≡3
1 2 3 4 5 6 7 8 9 10 11 12 13
10 0 0 0 0 0 0 0 12 0 0 3 0
6 ≡ 10
1 2 3 4 5 6 7 8 9 10 11 12 13
10 0 0 0 0 10 0 0 12 0 0 3 0
10 ≡ 12
1 2 3 4 5 6 7 8 9 10 11 12 13
10 0 0 0 0 10 0 0 12 3 0 3 0
2≡5
1 2 3 4 5 6 7 8 9 10 11 12 13
10 5 0 0 0 10 0 0 12 3 0 3 0
7≡8
1 2 3 4 5 6 7 8 9 10 11 12 13
10 5 0 0 0 10 8 0 12 3 0 3 0
4 ≡ 11
1 2 3 4 5 6 7 8 9 10 11 12 13
10 5 0 11 0 10 8 0 12 3 0 3 0
Estruturas de Dados 18
JEDITM
6 ≡ 13
1 2 3 4 5 6 7 8 9 10 11 12 13
10 5 13 11 0 10 8 0 12 3 0 3 0
1≡9
1 2 3 4 5 6 7 8 9 10 11 12 13
10 5 13 11 0 10 8 0 12 3 0 3 0
public Equivalence() {
}
// Pega a raiz de j e k
while (FATHER[j] > 0)
j = FATHER[j];
while (FATHER[k] > 0)
k = FATHER[k];
Estruturas de Dados 19
JEDITM
}
/* Aceita dois elementos j e k.
Retorna verdadeiro se equivalente, senão returna falso */
public boolean test(int j, int k) {
// Obtém as raízes de j e k
while (FATHER[j] > 0)
j = FATHER[j];
while (FATHER[k] > 0)
k = FATHER[k];
O problema com o algoritmo anterior similar ao problema resolvido pelo enfraquecimento, i.e.,
criando uma árvore que possua o maior número de níveis em profundidade possível, feito isso é
criado um prejuízo de performance, i.e., tempo de complexidade para O(n). Para esta ilustração,
considere o conjunto S = { 1, 2, 3, ..., n } e a relação de equivalência 1 ≡ 2, 1 ≡ 3, 1≡ 4, ..., 1 ≡ n. A
seguinte figura mostra como a árvore é montada:
Estruturas de Dados 20
JEDITM
public Equivalence2(){
}
public Equivalence2(int size){
n = size+1;
FATHER = new int[n];
}
public void setEquivalence(int a[], int b[]){
int j, k;
for (int i=0; i<FATHER.length; i++) FATHER[i] = -1;
for (int i=0; i<a.length; i++){
j = a[i];
k = b[i];
j = find(j);
k = find(k);
if (j != k) union(j, k);
}
}
}
Para resolver este problema, utilizaremos uma técnica conhecida como a balanceamento por
união. Definida a seguir:
primeiro o node i e o node j são raízes. Se o número de nodes da árvore cuja raiz i for
maior que o número de nodes com raiz j, faz-se o node i pai do node j; senão, faz o node j
pai do node i.
No algoritmo, Um array COUNT pode ser usado para contar o número de nodes de cada árvore da
floresta. Porém, se o node é raiz de classes equivalentes, eles não entram no array PAI não tem
importância e a raiz não possui pai. Para entender a vantagem sobre isto, podemos usar esta
abertura como array PAI no lugar de usar outro. Para diferenciar entre contadores e rótulos no PAI,
um sinal de menos é adicionado aos contadores. O seguinte método adicionado na classe
Sequence2, implementa o balanceamento por união:
// Implementa o balanceamento por união
public void union(int i, int j) {
int count = FATHER[i] + FATHER[j];
if (Math.abs(FATHER[i]) > Math.abs(FATHER[j])) {
Estruturas de Dados 21
JEDITM
FATHER[j] = i;
FATHER[i] = count;
} else {
FATHER[i] = j;
FATHER[j] = count;
}
}
A operação de UNIÃO possui tempo de complexidade O(1). Se o balanceamento por união não for
aplicado, a ordem é O(n) pesquisa-união na operação de inserção, no pior caso, O(n 2). Fora isso, se
aplicado, o tempo de tempo de complexidade é O(n log2n).
Outra observação em usar árvores em problemas semelhantes é mostrado para o pior caso até
quando a criação balanceamento por união é aplicada. Considere a seguinte ilustração:
A figura mostra como a árvore pode crescer logaritmicamente apesar do balanceamento por união
ser aplicado. Isto é, o pior caso das árvores com n nodes é log2 n. podemos prevenir, uma árvore
possui n-1 nodes filhos apenas um node pode ser usado para representear a classe de equivalência.
Neste caso, a profundidade para a árvore de pior caso pode ser reduzido por aplicar outra técnica, e
esta é a desmontar por ordem de pesquisa. Com este, o caminho pode ser feito buscando o
caminho percorrido da raiz ao node p. Isto é , em um processo de busca, Se o atual caminho
descoberto não for o ótimo, ele é “desmontada” para conseguir o ótimo. A ilustração anterior mostra
isso, considere a figura seguinte:
Estruturas de Dados 22
JEDITM
// Procura raiz
while (FATHER[k] > 0)
k = FATHER[k];
Estruturas de Dados 23
JEDITM
l = FATHER[j];
FATHER[j] = k;
j = l;
}
return k;
}
// Retorna as raizes de j e k
j = find(j);
k = find(k);
A seguir é mostrado o estado de classes equivalentes após esta solução final de equivalência o
problema é resolvido:
1 2 3 4 5 6 7 8 9 10 11 12 13
10 0 0 0 0 0 0 0 0 0 0 0 0
Estruturas de Dados 24
JEDITM
1 2 3 4 5 6 7 8 9 10 11 12 13
10 0 0 0 0 0 0 0 12 0 0 0 0
9 ≡ 3,
balanceando count(12) > count(3)
1 2 3 4 5 6 7 8 9 10 11 12 13
10 0 12 0 0 0 0 0 12 0 0 0 0
6 ≡ 10
1 2 3 4 5 6 7 8 9 10 11 12 13
10 0 12 0 0 10 0 0 12 0 0 0 0
1 2 3 4 5 6 7 8 9 10 11 12 13
12 0 12 0 0 12 0 0 12 12 0 0 0
2≡5
1 2 3 4 5 6 7 8 9 10 11 12 13
10 5 12 0 0 12 0 0 12 12 0 0 0
7≡8
1 2 3 4 5 6 7 8 9 10 11 12 13
10 5 12 0 0 12 8 0 12 12 0 0 0
4 ≡ 11
1 2 3 4 5 6 7 8 9 10 11 12 13
12 5 12 11 0 12 8 0 12 12 0 0 0
Estruturas de Dados 25
JEDITM
6 ≡ 13
1 2 3 4 5 6 7 8 9 10 11 12 13
10 5 12 11 0 12 8 0 12 12 0 0 0
1≡9
1 2 3 4 5 6 7 8 9 10 11 12 13
12 5 12 11 0 12 8 0 12 12 0 0 0
Figura 22: Um Exemplo Usando Balanceamento por União e Desmontar por Ordem de Pesquisa
Estruturas de Dados 26
JEDITM
6. Exercícios
1. Para cada uma das florestas abaixo,
FLORESTA 1
FLORESTA 2
FLORESTA 3
a b c d e f g h i j k l m n o
5 2 0 0 1 0 0 2 1 0 4 1 0 0 0
3. Classes de Equivalência
Estruturas de Dados 27
JEDITM
b) Desenhe a floresta correspondente e o array pai resultante das classes equivalentes obtidas dos
elementos de S = {1,2,3,4,5,6,7,8,9,10,11,12} na base das relações equivalentes 1≡2, 3≡5, 5≡7,
9≡10, 11≡12, 2≡5, 8≡4, e 4≡6. Use a regra dos pesos para a união.
Estruturas de Dados 28
Módulo 3
Estruturas de Dados
Lição 6
Grafos
1. Objetivos
Essa lição cobre a nomenclatura para o Grafo ADT. Discute diferentes modos para representar um
grafo. Os dois algoritmos de grafos transversais também são discutidos, assim como o problema de
árvores geradoras de custo mínimo e o problema do caminho mais curto.
Ao final dessa lição, o estudante deverá ser capaz de:
• Explicar conceitos básicos e definições de grafos
• Discutir métodos de representação de grafos: matriz de adjacência e lista de adjacência
• Grafos Transversais usando os algoritmos depth-first search (busca em primeira-
profundidade) e breadth-first search (busca em primeira-largura)
• Entender árvores geradoras de custo mínimo para grafos não-dirigidos usando o algoritmo
de Prim e de Kruskal
• Resolver o problema de menor caminho com início único usando o algoritmo de Dijkstra
• Resolver o problema de menor caminho para todos os pares usando o algoritmo de Floyd
Estruturas de Dados 4
JEDITM
Dois vértices i e j são adjacentes se aresta(i, j) é uma aresta em E. A aresta(i, j) é conhecida com
sendo incidente nos vértices i e j.
Um grafo não-dirigido completo é um grafo no qual uma aresta conecta todos os pares de
vértices. Se um grafo não-dirigido complete tem n vértices, existirão n(n -1)/2 arestas nele.
Estruturas de Dados 5
JEDITM
Um grafo dirigido ou dígrafo é um grafo no qual cada aresta é representada por um par ordenado
<i, j>, onde i é o vértice principal e j é o vértice secundário da aresta. As arestas <i,j> e <j,i> são
duas arestas distintas.
Um grafo dirigido completo é um grafo no qual todos os pares de vértices i e j são conectados por
duas arestas <i,j> e <j,i>. Existem n(n-1) arestas nele.
Estruturas de Dados 6
JEDITM
Uma árvore geradora é um subgrafo que conecta todos os vértices de um grafo. O custo de uma
árvore geradora, se for ponderada, é a soma dos pesos dos galhos (arestas) da árvore geradora.
Uma árvore geradora que tem o custo mínimo é conhecida como árvore geradora de custo
mínimo. Isso não é necessariamente único para um determinado grafo.
Estruturas de Dados 7
JEDITM
3. Representação de Grafos
Existem diversas maneiras de se representar um grafo, e existem alguns fatores que devem ser
considerados:
• Operações nos grafos
• Número de arestas relativas ao número de vértices no grafo
A matriz de adjacência pode ser declarada como uma matriz de lógicos se o grafo não for ponderado.
Se o grafo for ponderado, A(i, j) é configurado para conter o custo da aresta <i, j>, mas se não
existir aresta <i, j> no grafo, A(i, j) é configurado para um valor muito grande. A matriz é então
chamada de matriz custo-adjacência.
Por exemplo, a representação de custo-adjacência do seguinte grafo é mostrada abaixo:
1 2 3 4 5
1 0 1 ∞ 9 ∞
2 ∞ 0 2 5 10
3 ∞ ∞ 0 ∞ 3
4 ∞ ∞ 4 0 8
5 6 ∞ ∞ ∞ 0
Não é permitida referência a si própria, portanto, os elementos diagonais são sempre zeros. O
número de elementos não-zero em A é menor ou igual a n(n-1), o qual é o limite se o dígrafo é
completo. O grau de saída do vértice i, ou seja, o número de setas derivadas dele, é o mesmo de
números de elementos não-zero na linha i. O caso é parecido para o grau de entrada do vértice j,
em que o número de setas apontando para ele é o mesmo do número de elementos não-zero na
coluna j.
Com essa representação, saber se existe uma aresta <i, j> leva O(1) tempo. No entanto, mesmo se
o dígrafo tiver menos que n2 arestas, a representação implica em requerimento de espaço de O(n2).
Uma tabela seqüencial ou lista pode também ser usada para representar um dígrafo G em n vértices,
digamos LIST. A lista é mantida como se para qualquer vértice i em G, LIST(i) aponta para a lista de
vértices adjacentes de i.
Por exemplo, segue a representação da lista de adjacência o grafo anterior:
Estruturas de Dados 8
JEDITM
1 0 * * * *
2 0 0 * * *
3 0 1 0 * *
4 1 0 1 0 *
5 1 1 0 0 0
Figura 12: Representação dos custos em uma Matriz de Adjacência para Grafos Não Direcionados
A representação é similar a lista de adjacência para Grafos Direcionados. Contudo, para Grafos Não
Direcionados, existem duas entradas na lista para uma aresta (i, j).
Por exemplo, veja a seguir a representação da Lista de Adjacência do Grafo anterior:
Figura 13: Representação dos custos em Lista de Adjacência para Grafos Não Direcionados
Estruturas de Dados 9
JEDITM
4. Percurso em Grafos
Um grafo, diferentemente de uma árvore, não tem o conceito de um node raiz no qual o método de
percurso pode ser iniciado. Também não há uma ordem natural entre vértices de ou para o vértice
mais recentemente visitado que indica o próximo a ser visitado. No percurso em grafos, também é
importante perceber que desde que um vértice possa ser adjacente de ou para muitos vértices, há a
possibilidade de encontrá-lo novamente. Por essa razão, existe a necessidade de indicar se um
vértice já foi visitado.
Nessa seção, cobriremos dois algoritmos de percurso em grafos: busca em profundidade e busca em
largura.
Sempre que houver ambigüidade em relação a qual deve ser o próximo vértice a ser visitado, no
caso de existirem muitos vértices adjacentes, aquele com o menor número deve ser escolhido. Por
exemplo, iniciando no vértice 1, a busca em profundidade percorrerá os elementos conforme o
seguinte grafo:
Estruturas de Dados 10
JEDITM
Estruturas de Dados 11
JEDITM
A Busca em Primeira Largura (Breadth First Search - BFS) percorre o grafo da forma mais ampla o
possível. Dado um grafo com vértices marcados como não visitados, o percurso é executado da
seguinte forma:
1. Selecione um vértice não visitado, por exemplo i, e marque-o como visitado. Então,
busque o mais amplamente possível partindo de i e visitando seus vértices adjacentes.
Assim como na busca em profundidade, no caso de dúvida sobre qual node será o próximo a ser
visitado, deve-se considerar a ordem numérica crescente dos elementos.
Por exemplo, iniciando no vértice 1, a busca em largura percorrerá os elementos conforme o
seguinte grafo:
Estruturas de Dados 12
JEDITM
Estruturas de Dados 13
JEDITM
Considere G = (V, E) um grafo não-dirigido, ponderado e conexo. Considere U seja algum conjunto
apropriado de V e (u, v) seja uma aresta de menor custo tal que u ∈ U e v ∈ (V – U). Existe uma
árvore geradora de custo mínimo T tal que (u, v) é uma aresta em T.
3. Repita (2) até U = V, em que, T é uma árvore geradora de custo mínimo para G
Por exemplo,
Estruturas de Dados 14
JEDITM
Tomando a como o vértice de partida, a figura a seguir mostra a execução do algoritmo Prim para
resolver o problema MST:
Por exemplo,
A tabela a seguir mostra a execução do algoritmo Kruskal para resolver o problema MST do grafo
acima:
Estruturas de Dados 15
JEDITM
Estruturas de Dados 16
JEDITM
Desde que todos os vértices estejam prontos em U, a MST deve ser obtida. O algoritmo resultante
para a MST possui o custo 33.
Nesse algoritmo, o maior fator em custo computacional é a ordenação de arestas em ordem
crescente.
Estruturas de Dados 17
JEDITM
• Problema de Menor Caminho para Todos os Pares (All-Pairs Shortest Paths (APSP)) que
determina o custo do menor caminho de cada vértice para todos os vértices de V.
Vamos discutir o algoritmo criado por Dijkstra para resolver o Problema SSSP, e, para o Problema
APSP, vamos usar o algoritmo desenvolvido por Floyd.
Assim como os algoritmos de Prim e Kruskal, o algoritmo de Dijkstra usa o caminho "ganancioso".
Neste algoritmo, para cada vértice é atribuída uma classe e um valor, onde:
• Um vértice de Classe 1 é um vértice o qual a sua menor distância para o vértice
inicial, digamos k, já foi encontrada; seu valor é igual a sua distância do vértice k
pelo menor caminho.
Seja u o vértice inicial e v o vértice final. Seja pivô o vértice que foi mais recentemente considerado
parte do caminho. Seja caminho de um vértice seu início direto no menor caminho. Agora o
algoritmo:
1. Coloque o vértice u na classe 1 e todos os outros vértices na classe 2
2. Defina o valor de vértice u para zero e o valor de todos os outros vértices para
infinito
Por exemplo, dado o seguinte grafo ponderado e direcionado, encontre o menor caminho do vértice
1 ao vértice 7.
Estruturas de Dados 18
JEDITM
Estruturas de Dados 19
JEDITM
O caminho do vértice inicial 1 até o vértice final 7 pode ser obtido recuperando o valor do
caminho(7) na ordem inversa, que é,
caminho(7) = 6
caminho(6) = 5
caminho(5) = 2
caminho(2) = 1
PATH (i,j) = 0 inicialmente, indica que o caminho mas curto entre i e j. É a aresta (i,j) se a
Estruturas de Dados 20
JEDITM
mesma existir
Para k = 1, 2, 3, ..., n
Por exemplo, resolva o problema APSP do grafo a seguir usando o algoritmo de Floyd:
Como auto-referência não é permitida, não é necessário computar A(j, j), para 1≤j≤n. Também, para
a k-ésima iteração, não haverá mudanças para as k-ésimas linhas e colunas em A e no PATH, uma
vez que só somará 0 ao valor atual. Por exemplo, se k = 2:
A(2, 1) = mínimo( A(2, 1), A(2,2) + A(2,1) )
1 0 2 ∞ 12 1 0 0 0 0
2 8 0 ∞ 7 2 0 0 0 0
3 5 10 0 7 3 0 0 0 0
4 ∞ ∞ 1 0 4 0 0 0 0
A PATH
Estruturas de Dados 21
JEDITM
1 2 3 4 1 2 3 4
1 0 2 ∞ 12 1 0 0 0 0
k=1 2 8 0 ∞ 7 2 0 0 0 0
3 5 7 0 7 3 0 1 0 0
4 ∞ ∞ 1 0 4 0 0 0 0
A PATH
Para k=2:
A(1, 3) = mínimo( A(1, 3) , A(1, 2) + A(2, 3) ) = mínimo(∞, ∞) = ∞
A(1, 4) = mínimo( A(1, 4) , A(1, 2) + A(2, 4) ) = mínimo(12, 9) = 9
A(3, 1) = mínimo( A(3, 1) , A(3, 2) + A(2, 1) ) = mínimo(5, 15) = 5
A(3, 4) = mínimo( A(3, 4) , A(3, 2) + A(2, 4) ) = mínimo(7, 12) = 7
A(4, 1) = mínimo( A(4, 1) , A(4, 2) + A(2, 1) ) = mínimo(∞, ∞) = ∞
A(4, 3) = mínimo( A(4, 3) , A(4, 2) + A(2, 3) ) = mínimo(1, ∞) = ∞
1 2 3 4 1 2 3 4
1 0 2 ∞ 9 1 0 0 0 2
k=2 2 8 0 ∞ 7 2 0 0 0 0
3 5 7 0 7 3 0 1 0 0
4 ∞ ∞ 1 0 4 0 0 0 0
A PATH
Para k=3:
A(1, 2) = mínimo ( A(1, 2) , A(1, 3) + A(3, 2) ) = mínimo (2, ∞) = ∞
A(1, 4) = mínimo ( A(1, 4) , A(1, 3) + A(3, 4) ) = mínimo (9, ∞) = ∞
A(2, 1) = mínimo ( A(2, 1) , A(2, 3) + A(3, 1) ) = mínimo (8, ∞) = ∞
A(2, 4) = mínimo ( A(2, 4) , A(2, 3) + A(3, 4) ) = mínimo (7, ∞) = ∞
A(4, 1) = mínimo ( A(4, 1) , A(4, 3) + A(3, 1) ) = mínimo (∞, 6) = 6
A(4, 2) = mínimo ( A(4, 2) , A(4, 3) + A(3, 2) ) = mínimo (∞, 8) = 8
1 2 3 4 1 2 3 4
1 0 2 ∞ 9 1 0 0 0 2
k=3 2 8 0 ∞ 7 2 0 0 0 0
3 5 7 0 7 3 0 1 0 0
4 6 8 1 0 4 3 3 0 0
A PATH
Para k=3:
Estruturas de Dados 22
JEDITM
1 0 2 10 9 1 0 0 4 2
k=4 2 8 0 8 7 2 0 0 4 0
3 5 7 0 7 3 0 1 0 0
4 6 8 1 0 4 3 3 0 0
A PATH
Após a nth interação, A contém o menor custo enquanto PATH contém o caminho de menor custo.
Para ilustrar como usar o resultado das matrizes, vamos encontrar o caminho mais curto do vértice 1
para o vértice 4:
A (1, 4) = 9
PATH (1, 4) = 2 --> Desde que não seja 0, temos que pegar PATH(2, 4):
PATH (2, 4) = 0
Por essa razão, o caminho mais curto do vértice 1 para o vértice 4 é 1 --> 2 --> 4 com custo 9. Até
mesmo se existe uma aresta direta de 1 ao 4 (com custo 12), o algoritmo retornou outro caminho.
Esse exemplo mostra que ele não é sempre a conexão direta que é retornada ao obter o caminho
mais curto em um ponderado, grafo direcionado.
Estruturas de Dados 23
JEDITM
7. Exercícios
1. O que é DFS e BFS listando elementos dos seguintes grafos com 1 como vértice de partida?
a)
b)
2. Encontre a árvore geradora de custo mínimo dos seguintes grafos usando os algoritmos de
Kruskal e Prim. Dê o custo do MST.
a)
b)
3. Resolva o problema SSSP do seguinte grafo, usando o algoritmo Dijkstra. Mostre o valor, a
classe e o caminho dos vértices para cada interação:
Estruturas de Dados 24
JEDITM
a)
b)
4. Resolva o APSP dos grafos a seguir dando as matrizes A e Path usando o algoritmo Floyd:
1. Crie uma definição de classe Java para grafos direcionados ponderados usando representação de
matriz de adjacência.
Implemente um caminho mais curto dado um mapa e o custo de cada aresta. O programa pedirá um
arquivo de entrada contendo as origens ou destinos (vértices) e as conexões entre as localizações
(aresta) com o custo. As localizações seguem a forma (número, localização), onde número é um
inteiro determinado para o vértice e localização é o nome do vértice. Todo par (número,
localização) tem que ser localizado em uma linha separada no arquivo de entrada. Para terminar a
entrada, use (0, 0). Conexões serão restritas também de um mesmo arquivo. Uma definição de tem
que se da forma (i, j, k), uma linha por aresta, onde i é o número determinado para a origem, j o
Estruturas de Dados 25
JEDITM
Estruturas de Dados 26
Módulo 3
Estruturas de Dados
Lição 7
Listas
1. Objetivos
Lista é uma estrutura de dado que é baseada numa seqüência de itens. Nessa lição, iremos cobrir os
dois tipos de listas – linear e generalizada – e suas diferentes representações. Listas encadeadas
simples, circulares e encadeadas duplas também serão exploradas. Além disso, duas aplicações
irão ser apresentadas – aritmética polinomial e alocação dinâmica de memória. Aritmética polinomial
inclui a representação de operações polinomiais e aritméticas definidas nela. Alocação dinâmica de
memória cobre ponteiros e estratégias de alocação. Também haverá uma breve discussão sobre
conceitos de fragmentação.
Ao final desta lição, o estudante será capaz de:
• Explicar definições e conceitos básicos de listas
• Usar as diferentes representações de lista: seqüencial e encadeada
• Diferenciar lista encadeada simples, lista encadeada dupla, lista circular e lista com
header nodes
• Explicar como as listas são aplicadas na aritmética polinomial
• Discutir as estruturas de dados usadas na alocação dinâmica de memória usando
métodos sequential-fit e métodos buddy-system
Estruturas de Dados 4
JEDITM
O ordenamento linear dos átomos é a propriedade essencial de uma lista linear. A seguir a notação
para esse tipo de lista:
L = ( i1, i2, i3, ..., in )
Várias operações podem ser feitas com uma lista linear. Inserção pode ser feita em qualquer
posição. Similarmente, qualquer elemento pode ser deletado de qualquer posição. A seguir estão as
operações que podem ser feitas nas listas lineares:
• Inicialização (a lista é igual a NULL)
• Determinar se a lista é vazia (checando se L != NULL)
• Achar o tamanho (obtendo o número de elementos)
• Acessar o j-ésimo elemento, 0 ≤ j ≤ n-1
• Atualizar o j-ésimo elemento
• Deletar o j-ésimo elemento
• Inserir um novo elemento
• Combinar duas ou mais listas em uma única lista
• Dividir uma lista em duas ou mais listas
• Duplicar uma lista
• Apagar uma lista
• Buscar por um valor
• Ordenar a lista
Uma lista generalizada pode conter elementos átomo e lista. Ela tem profundidade e tamanho. A lista
generalizada é também conhecida como lista estruturada, ou simplesmente lista. A seguir um
exemplo:
L = ((a, b, ( c, ( ))), d, ( ), ( e,( ), (f, (g, (a))), d ))
Estruturas de Dados 5
JEDITM
No exemplo, a lista L possui quatro elementos. O primeiro elemento é a lista (a, b, (c, ( ) )), o
segundo é o átomo d, o terceiro é o conjunto null () e o quarto é a lista (e, ( ), (f, (g, (a))), d).
Estruturas de Dados 6
JEDITM
3. Representações de lista
Uma forma de representar uma lista é organizar os elementos um após o outro em uma estrutura
seqüencial como um array. Outra forma para implementar isso, é o encadeamento de nodes
contendo os elementos da lista usando uma representação encadeada.
Com essa representação, poderá ser tomado O(1) tempo para acessar e atualizar o j-ésimo
elemento na lista. Por fazer o caminho até o último item, poder ser tomado O(1) tempo para dizer se
a lista é vazia e para achar o tamanho da lista. Há uma condição, no entanto, em que o primeiro
elemento deve ser sempre armazenado no primeiro índice L(0). Nesse caso, inserir e deletar podem
requerer desvio de elementos para assegurar que a lista satisfaz essa condição. No pior caso, isso
pode obrigar desvio de todos os elementos no array, resultando na complexidade de tempo de O(n)
para inserir e excluir n elementos. Ao combinar duas listas, um array mais largo é requerido se o
tamanho combinado não couber em alguma das duas listas. Isso pode obrigar a copiar todos os
elementos das duas listas na nova lista. Duplicar uma lista pode requerer atravessar a lista inteira,
portanto, uma complexidade de tempo de O(n). Buscar um valor em particular pode tomar tempo de
O(1) se o elemento for o primeiro na lista; de outra forma, o pior caso é quando o elemento
pesquisado é o último, onde a passagem da lista inteira é necessária. Nesse caso, a complexidade
de tempo é O(n).
A alocação seqüencial, sendo estática por natureza, é uma desvantagem para listas de tamanho
incerto, por exemplo, o tamanho não é sabido no momento da inicialização, e com várias inserções e
remoções, pode eventualmente precisar crescer ou encolher. Copiando a lista transbordada em um
array mais largo e descartando o antigo pode funcionar, mas isso pode ser um desperdício de tempo.
Nesse caso, é melhor usar a representação encadeada.
Uma corrente de nodes encadeados pode ser usada para representar uma lista.
Para acessar o j-ésimo elemento com alocação encadeada, a lista tem que ser percorrida do primeiro
elemento até o j-ésimo elemento. O pior caso é quando i = n, onde n é o número de elementos.
Portanto, a complexidade de tempo para acessar o j-ésimo elemento é O(n). Similarmente,
encontrando o tamanho pode obrigar a percorrer a lista inteira, sendo a complexidade de O(n). Se
inserções forem feitas no início da lista, isso pode levar ao tempo O(1). De outra forma, com
Estruturas de Dados 7
JEDITM
remoção e atualização, a busca tem que ser realizada resultando na complexidade de tempo de
O(n). Para dizer se a lista é vazia, pode ser tomado um tempo constante, como na representação
seqüencial. Para copiar uma lista, cada node é copiado enquanto a lista original é percorrida.
A tabela seguinte resume as complexidades de tempo das operações realizadas em listas com os
dois tipos de alocação:
Representação Representação
Operação
Seqüencial Encadeada
Determinar se uma lista é vazia O(1) O(1)
Encontrar o tamanho O(1) O(n)
Acessar o j-ésimo elemento O(1) O(n)
Atualizar o j-ésimo elemento O(1) O(n)
Deletar o j-ésimo elemento O(n) O(n)
Inserir um novo elemento O(n) O(1)
Tabela 1: Representação Seqüencial x Encadeada
A representação seqüencial é apropriada para listas que são estáticas por natureza. Se o tamanho é
desconhecido, o uso de alocação encadeada é recomendado.
Ainda no encadeamento simples linear, há mais variedades de representações encadeadas de listas.
Encadeamento simples circular, encadeamento duplo e lista com header nodes são as variedades
mais comuns.
Uma lista circular encadeada simples é formada pelo envio do link do último node e apontar de volta
ao primeiro node. Isso é ilustrado na seguinte figura:
Observe que o ponteiro para a lista nessa representação aponta para o último elemento na lista.
Com lista circular, existe a vantagem de ser possível acessar um node de qualquer outro node.
Iniciamos a classe que exemplificará a lista circular encadeada, com dois nodes de referência, para o
primeiro e o último elemento:
public class CircularList {
private Node F;
private Node L;
}
Lista circular pode ser usada para implementar uma stack. Nesse caso, inserção (push) pode ser
feita na ponta esquerda da lista, e remoção (pop) na mesma ponta. Similarmente, queue também
pode ser implementada permitindo inserção na ponta direita da lista e remoção na ponta esquerda.
Estruturas de Dados 8
JEDITM
Estruturas de Dados 9
JEDITM
}
L = alpha;
}
O seguinte método inserido na classe realiza o procedimento de eliminar o elemento mais à esquerda
da lista circular L:
public void deleteLeft() throws Exception {
Node alpha;
if (L == null)
throw new Exception("CList empty.");
else {
F.link = L.link;
alpha = L.link;
if (alpha == L)
L = null;
else
L = alpha;
}
}
Estruturas de Dados 10
JEDITM
Terminamos esta classe com um método para mostrar o conteúdo sequencial da lista (lembrando
que a mesma é circular, então devemos listar uma determinada quantidade de elementos, pois não é
possível localizar o fim da lista).
public void showList(Node n, int t) {
if (t-- == 0)
return;
System.out.println(n.info);
showList(n.link, t);
}
Estruturas de Dados 11
JEDITM
observe que sempre listamos um elemento a mais para mostrar a circularidade da lista.
Um header node pode ser usado para armazenar informação adicional sobre a lista, como o número
de elementos. O header node é também conhecido como list header. Para uma lista circular, ele
serve como sentinela para indicar passagem completa pela lista. Também pode indicar tanto o
começo como o ponto final. Para uma lista encadeada dupla, ele é usado para apontar para ambos
Estruturas de Dados 12
JEDITM
Listas encadeadas duplas são formadas de nodes que possuem ponteiros para ambos vizinhos
(esquerdo e direito) na lista. A próxima figura mostra a estrutura de nodes para uma lista encadeada
dupla.
Com lista encadeada dupla, cada node tem dois campos de ponteiro – LLINK e RLINK que apontam
para os vizinhos da esquerda e direita respectivamente. A lista pode ser percorrida em ambas as
direções. Um node i pode ser deletado sabendo apenas a informação sobre o node i. Ainda, um node
j pode ser inserido tanto antes quanto depois de um node i sabendo a informação sobre i. A figura
seguinte ilustra a remoção do node i:
Uma lista encadeada dupla pode ser constituída das seguintes formas:
• Lista linear encadeada dupla
• Lista circular encadeada dupla
• Lista circular encadeada dupla com cabeçalho de lista
Estruturas de Dados 13
JEDITM
Estruturas de Dados 14
JEDITM
Para tratar estes assuntos, lista circular simplesmente ligada com cabeça de lista pode ser usada,
com uma estrutura de node como ilustrado abaixo:
onde,
• O campo EXPO é dividido em um subcampo de sinal (S) e três subcampos de expoentes para
as variáveis x, y, z.
• S é negativo (-) se for a cabeça de lista, caso contrário é positivo
• ex, ey, ez são para as potências das variáveis x, y e z respectivamente
• O campo COEF pode conter qualquer número real, com ou sem sinal.
Por exemplo, a figura seguinte é a representação da lista do polinômio P(x,y,z) = 20x 5y4 – 5x2yz +
13yz3:
Neste aplicativo, há uma regra em que os nodes devem ser arranjados em valor decrescente do
triplo (exeyez). Um polinômio satisfazendo esta propriedade é dito estar em forma canônica. Esta
regra torna o desempenho de executar as operações aritméticas do polinômio mais rápida, em
comparação aos termos estarem organizados em nenhuma ordem particular.
Desde que a estrutura de lista tem uma cabeça de lista, para representar o zero polinomial, temos o
seguinte:
Estruturas de Dados 15
JEDITM
class PolyTerm {
int expo;
int coef;
PolyTerm link;
Estruturas de Dados 16
JEDITM
Esta seção cobre como adição polinomial, subtração e multiplicação podem ser implementadas,
usando a estrutura há pouco descrita.
4.1.1. Adição Polinomial
Somando dois polinômios P e Q, a soma é retida em Q. Três ponteiros de execução são necessário:
• α para apontar para o termo corrente (node) em P polinomial
• β para apontar para o termo corrente em Q polinomial
• σ apontar para o node atrás de β. Isto é usado durante a inserção e a deleção em Q para
obter a complexidade do tempo O(1).
• Se EXPO (α) < 0, ambos os ponteiros α e β têm uma volta completa no círculo e
estão agora apontando para as cabeças de lista
• Senão, α e β estão apontando para dois termos que podem ser adicionados
Estruturas de Dados 17
JEDITM
Adicionar os dois,
Expo(α) = Expo(β)
adicionar α e β:
Expo(α) = Expo(β)
adicionar α e β, resultados para excluir o node apontado por β:
Estruturas de Dados 18
JEDITM
Desde que ambos P e Q estejam em forma canônica, uma passagem é suficiente. Se os operando
não estiverem em forma canônica, o procedimento não produzirá o resultado correto. Se P tem m
termos e Q tem n termos, a complexidade de tempo do algoritmo é O(m+n).
Com este algoritmo, não há necessidade para manipulação especial do zero polinomial. Ele trabalha
com zero P e/ou Q. Porém, desde que a soma é retida em Q, ele tem que ser duplicado, se a
necessidade de usar Q depois da adição poderá surgir. Poderia ser feito chamando o método
adicionar(Q, P) da classe Polinomial, onde P é inicialmente o zero polinomial e contém a duplicata de
Q.
O seguinte é a implementação de Java deste Procedimento:
// Executa a operaçãoQ = P + Q, Q é [this] polinômio
public void add(Polynomial P) {
// Ponteiro móvel em P
PolyTerm alpha = P.head.link;
// Ponteiro móvel em Q
PolyTerm beta = head.link;
Estruturas de Dados 19
JEDITM
Subtração de um polinomial Q de P, i.e., Q = P-Q, é simplesmente uma adição polinomial com cada
termo em Q negado: Q = P + (-Q). Isto pode ser feito percorrendo Q e negando os coeficientes no
processo antes de chamar polyAdd.
Estruturas de Dados 20
JEDITM
Estruturas de Dados 21
JEDITM
É necessário gerenciar o memory pool para que os blocos sejam alocados e desalocados conforme
a necessidade. Problemas aparecem após uma seqüência de alocar e desalocar blocos. Isto é,
quando o memory pool consiste de blocos livres e dispersos todos entre blocos reservados do pool.
Nestes casos, as linked lists podem ser utilizadas para organizar blocos livres tornando mais
eficientes os processos de reserva e liberação.
No método sequential-fit, todos os blocos livres estão constituídos em uma lista singly-linked
chamada de lista disponível. No método buddy-system, blocos são alocados em tamanhos
quantum apenas, i.e. 1, 2, 4, 2k apenas words. Assim, algumas listas disponíveis são mantidas, uma
para cada tamanho permitido.
Uma forma de constituir listas disponíveis é usar a primeira word de cada bloco livre como uma
control word (palavra de controle). Consiste de dois campos: SIZE e LINK. SIZE contém o tamanho
do bloco livre, enquanto LINK aponta para o próximo bloco livre no memory pool. A lista deve ser
ordenada de acordo com o tamanho, endereço, ou deve estar left unsorted.
Para satisfazer uma necessidade de n words, a lista disponível é escaneada por blocos que reúnem
um critério apropriado:
• first fit – o primeiro bloco com words m ≥ n
• best fit – o bloco best-fitting (mais apropriado), i.e. o menor bloco com words m≥n
• worst fit – o maior bloco
Após encontrar um bloco, n words das que estão reservadas e as restantes m-n são mantidas na
lista disponível. Todavia, se as words restantes m-n forem muito pequenas para satisfazer
qualquer pedido, devemos optar por alocar o bloco inteiro.
Estruturas de Dados 22
JEDITM
A abordagem acima é simples mas sofre de dois problemas. Primeiro, retorna para a lista
disponível o que quer que esteja à esquerda do bloco após a reserva. Isto remete a longas buscas
e muito do espaço livre não usado é dispersado na lista disponível. Segundo, a busca sempre
começa no início da lista disponível. Daqui, pequenos blocos a esquerda da parte conduzida da
lista resultando em buscas muito grandes.
Para resolver o primeiro problema, podemos alocar o bloco inteiro, se o que restar for pequeno para
satisfazer o pedido. Podemos definir um valor mínimo como minsize. Usando esta solução, será
necessário armazenar o tamanho do bloco desde que o tamanho reservado não coincida com o
tamanho atual do bloco alocado. Isto deverá ser feito adicionando-se um campo size para o bloco
reservado, que será usado durante a liberação.
Para resolver o segundo problema, poderemos manter a trilha do final da última busca e começar a
próxima busca na conexão do mesmo bloco atual, i.e. se chegarmos ao fina do bloco A, poderemos
iniciar a próxima busca no LINK(A). Um ponto sem destino, dizemos rover, é necessário para
manter a trilha deste bloco. O seguinte método implementa estas soluções.
Buscas curtas na lista disponível fazem a segunda abordagem mais rápida que a primeira. A última
abordagem é a que iremos usar para o primeiro método de ajuste da reserva de ajuste seqüencial.
Por exemplo, dado o estado do memory pool com um tamanho de 64K como ilustrado abaixo,
reserve espaço para o seguinte pedido retornando o que quer que esteja a esquerda da alocação.
Task Request
A 2K
B 8K
C 9K
D 5K
Estruturas de Dados 23
JEDITM
Task Request
E 7K
O método best-fit reserva largos blocos para futuras requisições enquanto que na worst-fit, um
largo bloco é alocado para retornar uma grande disponibilidade de blocos para a lista disponível. No
método best-fit, há uma necessidade em procurar a lista inteira para achar o melhor bloco ajustado.
Há pouco semelhança com o primeiro ajuste, e também existe uma tendência em se acumular blocos
muito pequenos. Entretanto, isso pode ser minimizado utilizando minsize. Best-fit não significa
necessariamente que produzirá resultados melhores no primeiro ajuste. Algumas pesquisas mostram
que há muitos casos na qual há um resultado melhor que supera o primeiro ajuste. Em algumas
aplicações, produto de pior ajuste produz os melhores resultados entre os três métodos.
Quando um bloco reservado não é necessário, deve ser liberado imediatamente. Durante a liberação,
deve-se desmontar os blocos livres adjacentes para se formar um bloco maior (problema de
desmontagem). Esta é uma consideração importante em liberação de sequential-fit.
Estruturas de Dados 24
JEDITM
Na figura, o bloco B está liberado. (a) mostra dois blocos adjacentes liberados, (b) e (c) mostram um
bloco adjacente liberado e (d) mostra dois blocos adjacentes liberados. Para liberar, o bloco liberado
deve estar mesclado com o bloco adjacente livre, se existir algum.
Existem dois métodos para mesclar na liberação: a técnica sorted-list e a técnica boundary-tag.
5.3.1. A técnica Sorted-List
Para saber se um bloco liberado está adjacente a quaisquer dos blocos livres na lista disponível,
usamos o tamanho do bloco. Para mesclar dois blocos, o campo SIZE do bloco mais baixo, que seu
endereço conhecido, é simplesmente atualizado para conter a soma dos tamanhos dos blocos
combinados.
Estruturas de Dados 25
JEDITM
A técnica Boundary-Tag utiliza duas words de controle e uma dupla ligação. A primeira e a última
palavra contém detalhes de controle. A figura seguinte mostra a estrutura de ligação e os dois
estados de um bloco (reservado e livre):
O valor da TAG é 0 se o bloco está livre, de outra maneira será 1. Ambos os campos TAG e SIZE
estão presentes para mesclar blocos livres executados no tempo O(1).
A lista disponível é concebida como uma lista doubly-linked como a lista principal (de cabeçalho).
Inicialmente, a lista disponível é formada por apenas um bloco, o memory pool inteiro, limitado
abaixo e acima pelos blocos de memória não disponíveis para DMA. A figura seguinte mostra o
estado inicial de uma lista disponível:
Estruturas de Dados 26
JEDITM
Após usar a memória por algum tempo, ficará com segmentos descontínuos, assim teremos a
seguinte lista disponível:
A técnica Sorted-list está em O(m), onde m é o número de blocos na lista disponível. A técnica
Boundary-tag tem tempo de complexidade O(1).
Nos métodos buddy-system, blocos estão alocados em tamanhos quantum. Algumas listas
disponíveis são mantidas, uma para cada tamanho permitido. Existem dois métodos buddy-system:
• O método binary buddy-system – os blocos são alocados em tamanhos baseados nas
potências de 2: 1, 2, 4, 8, …, 2k words
• o método Fibonacci buddy-system – os blocos são alocados em tamanhos baseados na
seqüência numérica Fibonacci: 1, 2, 3, 5, 8, 13, … palavras (k-1)+(k-2)
5.4.2. Reserva
Dado um bloco com tamanho 2k, o que segue é o algoritmo para reservar um bloco para um pedido
para n words:
1. Se o tamanho do bloco atual é < n:
• Se o bloco atual é o de maior tamanho, retorna: um bloco não suficientemente grande está
disponível
• Caso contrário vai para a lista disponível do próximo bloco no tamanho. Vai para 1
• Caso contrário, vai para 2
Estruturas de Dados 27
JEDITM
3. Divide o bloco em duas partes. Estas duas partes são chamados buddies. Vai para 2, tendo a
metade superior dos novos limite de corte como o bloco corrente
Por exemplo, reserve espaço para os pedidos A (7K), B (3K), C (16K), D (8K), e E (15K) a partir de
um memory pool não usado de tamanho 64K.
Estruturas de Dados 28
JEDITM
Estruturas de Dados 29
JEDITM
5.4.3. Liberação
Quando um bloco está liberado de uma tarefa e se o buddy do bloco inicialmente liberado está livre,
é necessário mesclar os buddies. Quando o buddy dos blocos recentemente mesclados também está
livre, executa-se novamente uma mescla. Isto é feito repetidamente até que mais nenhum buddy
possa ser mesclado.
Se o buddy localizado estiver livre, ele pode ser mesclado com o bloco mais recentemente liberado.
Para o método buddy-system ser eficiente, é necessário manter uma lista disponível para cada
Estruturas de Dados 30
JEDITM
tamanho alocável. A seguir temos o algoritmo para a reserva usando o método binary buddy
system:
1. Se um pedido para n words é feito e a lista disponível para blocos do tamanho 2k, onde k = log2n
, não está vazio, então temos um bloco da lista disponível. De outra forma, vá para 2
2. Obtenha um bloco da lista disponível de tamanho 2p onde p é o menor inteiro maior que k para
que a lista não seja vazia
3. Dividir o bloco p-k vezes, inserindo blocos não usados em suas respectivas listas disponíveis
Usando a alocação anterior como nosso exemplo, liberar os blocos com reserva B (3K), D (8K), e A
(7K) nesta ordem.
Estruturas de Dados 31
JEDITM
Na implementação, a seguinte estrutura, uma lista doubly-linked, será usada para as listas
disponíveis:
LLINK TAG KVAL RLINK
TAG = 0 se o bloco estiver livre
1 se o bloco estiver liberado
KVAL = k se o bloco tem tamanho 2k
Para inicializar o memory pool para o método buddy-system, assumimos que seu tamanho é 2m. É
necessário manter m+1 listas. Os ponteiros avail(0:m) para as listas são armazenados em um
array de tamanho m+1.
Após as séries de reserva e divisão, blocos de tamanho muito pequeno para satisfazer qualquer
pedido irão sobrar na lista disponível. Sendo muito pequenos, eles terão pouca chance de serem
reservados, e, eventualmente serão dispersados na lista disponível. Isto irá resultar em buscas
muito longas. Além disso, mesmo que a soma dos tamanhos resulte em um valor que possa
satisfazer um pedido, eles não poderão ser utilizados se estiverem dispersos no memory pool. Isto
é o que chamamos de external fragmentation. Resolvemos este problema com métodos
sequential-fit usando minsize, onde um bloco inteiro é reservado se o que sobrar na alocação for
menor que o valor especificado. Durante a liberação, os blocos são dirigidos à uma mescla livre com
blocos adjacentes.
A abordagem de usar minsize em método sequential-fit ou arredondar os pedidos para 2log2n no
método binary buddy-system, ligações para 'overallocation' de espaço. Esta alocação de espaço
maior que a necessidade para tarefas é o que chamamos de internal fragmentation.
Estruturas de Dados 32
JEDITM
6. Exercícios
1. Mostre a representação de ligação circular do polinômio
3. Usando métodos first fit, best-fit e worst-fit aloque 15k, 20k, 8k, 5k, e 10k no memory pool
ilustrado abaixo, tendo minsize = 2:
4. Usando método binary buddy-system e dado um memory pool vazio de tamanho 128K, reserve
espaço para os seguintes pedidos:
Tarefa Pedido
A 30K
B 21K
C 13K
D 7K
E 14K
Mostre o estado do memory pool após cada allocation. Não é necessário mostrar as listas
disponíveis. Libere C, E e D nesta ordem. Execute a mescla se necessário. Mostre como os buddies
são obtidos
Estruturas de Dados 33
JEDITM
1. Crie uma interface Java contendo as operações insert, delete, isEmpty, size, update, append two
lists e search.
2. Escreva uma definição de classe Java que implemente a interface criada no exercício 1 utilizando
uma lista linear doubly-linked.
3. Escreva uma interface Java que faça uso de um node para criar uma lista generalizada.
b) uma circular-list
Estruturas de Dados 34
Módulo 3
Estruturas de Dados
Lição 8
Tabelas
1. Objetivos
Uma das operações mais comuns no processo de solução de problemas é a busca. Esta refere-se ao
problema de encontrar dados que estão em algum lugar da memória do computador. Algumas
informações identificadas como dados desejados são alimentados para mostrar resultados desejados.
Tabelas são mais comuns em estruturas de armazenamento de dados para buscas.
Ao final desta lição, o estudante será capaz de:
• Discutir os conceitos básicos e as definições sobre tabelas: chaves, operações e
implementação
• Explicar as organizações de tabelas – ordenadas e não ordenadas
• Executar buscas usando uma tabela sequencial, indexação sequencial, binária e busca por
Fibonacci
Estruturas de Dados 4
JEDITM
2.2. Operações
Do lado da busca, muitas outras operações podem ser feitas numa tabela. A seguir uma lista das
operações possíveis:
Estruturas de Dados 5
JEDITM
2.3. Implementação
Uma tabela pode ser implementada usando alocação sequencial, alocação por link ou uma
combinação de ambas. Na Implementação da árvore ADT, existem diversos fatores a considerar:
Se o espaço de chave é fixo, por exemplo m, não tão grande, então a tabela pode simplesmente ser
implementada como um array de m células. Com isto toda chave no conjunto é associada a um
campo na tabela. Se a chave é a mesma que o índice do array, ela é conhecida como tabela de
endereço direto.
Fatores de Implementação
Ao implementar uma tabela de endereçamento direto, as seguintes coisas devem ser consideradas:
• Desde que os índices identifiquem registros unicamente, não é necessário armazenar a chave ki
explicitamente.
• Os dados podem ser armazenados em qualquer lugar. Se não há espaço bastante para os dados Xi
com a chave Ki, utiliza-se uma estrutura externa à tabela, um ponteiro para o dado atual é então
armazenado como Xi. Neste caso, a tabela serve como um índice para o dado atual.
• É necessário para indicar células em desuso correspondentes a chaves em desuso.
Vantagens
Com as tabelas de endereços diretos, a busca é eliminada pois a célula X i que contém o dado ou um
ponteiro para o dado é apontado para a chave Ki. Da mesma forma, operações de inserção e deleção
são relativamente diretas.
Estruturas de Dados 6
JEDITM
3. Tabelas e Busca
Um algoritmo de busca aceita um argumento e tenta encontrar um registro ao qual a chave é igual
à especificada. Se a busca for executada com sucesso, um ponteiro é retornado. Recuperação
ocorre quando a busca é realizada com sucesso. Esta seção discute as maneiras de organizar uma
tabela, bem como as operações de busca nas diferentes organizações de tabelas.
O algoritmo
Dado: Uma tabela de registros R0, R1, ..., Rn-1 com chaves K0, K1, ... Kn-1 respectivamente, onde n ≥
0. Procurar por um valor K:
1. Inicialize: faça i = 0
2. Compare: se K = Ki, pare – busca com sucesso
3. Avance: Incremente i por 1
4. Fim do arquivo?: se i < n, vá para o passo 2. então pare: Busca sem sucesso
class Search {
final static int notFound = -1;
public int sequentialSearch(int k, int key[]) {
for (int i=0; i<key.length; i++)
if (k == key[i])
return i; // busca com sucesso
return -1; // busca sem sucesso
}
}
A busca sequencial realiza n comparações no pior caso, com uma complexidade de tempo O(n). Este
algoritmo trabalha bem quando a tabela é relativamente pequena ou é mal percorrida. A vantagem
sobre este algoritmo é que ele trabalha uniformemente se a tabela está desordenada.
Estruturas de Dados 7
JEDITM
Na busca sequencial indexada, uma tabela auxiliar, chamada índice, aponta para a tabela ordenada.
As seguintes são características do algoritmo de busca sequencial indexada:
• Cada elemento no índice consiste de uma chave e um ponteiro para o registro no arquivo
que corresponde a Kindex
• Elementos no índice devem ser ordenados baseados na chave
• O arquivo de dados atual deve ou não ser ordenado
Com este algoritmo, o tempo de busca para um item particular é reduzido. Da mesma forma, um
índice poderá ser usado para apontar para uma tabela ordenada implementada como um array ou
com uma lista linkada. A última implementação implica em grande sobrecarga de espaço para
ponteiros mas inserções e deleções podem ser realizadas imediatamente.
Busca Binária
Busca binária começa com um intervalo ocupando a tabela inteira, este é o valor médio. Se o valor
procurado é menor que o item no meio do intervalo, o intervalo é encurtado para menos que a
metade. Senão, é encurtado para mais que a metade. Este processo de redução do tamanho da
busca pela metade é repetidamente realizado até que o valor seja encontrado ou o intervalo fique
vazio. O algoritmo para a busca binária faz uso das seguintes relações na busca pela chave K:
• K = Ki : pare, o registro desejado foi encontrado
• K < Ki : procura a menos metade – registros com as chaves K1 to Ki-1
• K > Ki : procura a maior metade – registros com as chaves Ki+1 to Kn
O Algoritmo
// Retorna o índice da chave k se encontrado, senão -1
public int binarySearch(int k, Table t) {
int lower = 0;
Estruturas de Dados 8
JEDITM
Então a área de procura é reduzida logaritmicamente, isto é, cada vez que o tamanho é reduzido, a
complexidade de tempo do algoritmo é O(log2n). O algoritmo pode se usado se a tabela usa
organização sequencial indexada. Entretanto, pode apenas ser usado com tabelas ordenadas
armazenadas como um array.
É similar ao algoritmo anterior, mas evita a divisão necessária para se encontrar a chave média. Para
fazer isto, é necessário reorganizar os registros na tabela:
1. Atribua chaves K1 < K2 < K3 < …< Kn aos nós de uma árvore binária completa na sequência
in order
Algoritmo de Busca
A comparação começa na raiz da árvore binária , j = 0, que é a chave média na tabela original.
/*
* Recebe um conjunto de chaves representado como uma árvore binária completa
*/
public int multiplicativeBinarySearch(int k, int key[]) {
int i = 0;
while (i < key.length) {
if (k == key[i])
return i; // busca bem sucedida
else if (k < key[i])
Estruturas de Dados 9
JEDITM
i = 2 * i + 1; // vá para esquerda
else
i = 2 * i + 2; // vá para direita
}
return -1; // busca mal sucedida
}
Como a computação do elemento médio é eliminada, a busca binária multiplicativa é mais rápida que
a busca binária tradicional. Entretanto, há necessidade de uma reorganização de um dado conjunto
de chaves antes que o algoritmo possa ser aplicado.
Busca de Fibonacci
Busca de Fibonacci utiliza as propriedades simples da sequência numérica de Fibonacci definida pela
seguinte relação de recorrência:
F0 = 0
F1 = 1
Fj = Fi-2 + Fi-1 ,i≥2
Em Java,
Kj é escolhida inicialmente de tal forma que j = F i, onde Fi é o maior número da série de Fibonacci
que é menor ou igual ao tamanho da tabela (n).
• Se K < Kj
• Descarte todas as chaves com índices maiores que j
• Faça j = j – q
• Desloque p e q uma posição para a esquerda na sequência numérica
• Se K > Kj,
• Descarte todas as chaves com índices menores que j
• Faça j = j + q
Estruturas de Dados 10
JEDITM
0 1 2 3 4 5 6
0 1 1 2 3 5 8 13 F
01234567 i
i = 5; Fi = 5; (Suposição) table size = Fi+1 - 1 = 7
j = 5, p = 3, q = 2: k < key[j]
j = 3, p = 2, q = 1: k < key[j]
j = 2, p = 1, q = 1: k = key[j] Bem sucedido
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Este é o algorítimo:
Estruturas de Dados 11
JEDITM
Podemos testar estas pesquisas através do seguinte método principal inserido na classe:
public static void main(String args[]) {
int size = 2000;
int key[] = new int[size];
for (int i = 0; i < size; i++)
key[i] = i;
Search s = new Search();
SimpleDateFormat sdT = new SimpleDateFormat("hh:mm:ss:SSSS");
SimpleDateFormat sdR = new SimpleDateFormat("ssSSS");
Estruturas de Dados 12
JEDITM
Este método criará um array de 2000 posicões, contendo o valor em cada posição. O que faremos
com ele será analizar os tempos iniciais e finais com cada método. O resultado final pode variar pois
depende do computador utilizado, eis um exemplo de saída:
Time: 05:19:43:0593
Time: 05:19:43:0609
Result Sequential: 16
Time: 05:19:43:0609
Time: 05:19:43:0734
Result Binary: 125
Time: 05:19:43:0734
Time: 05:19:43:0750
Result Multiplicative Binary: 16
Time: 05:19:43:0750
Time: 05:19:44:0187
Result Fibonacci: 437
Realize alguns testes modificando os valores e descubra qual o método de pesquisa ideal para as
suas necessidades.
Estruturas de Dados 13
JEDITM
4. Exercícios
Utilizando os métodos de pequisa abaixo,
a) Pesquisa Sequencial
b) Pesquisa Binária Multiplicativa
c) Pesquisa Binária
d) Pesquisa Fibonacci
Pesquisar por
1. A em S E A R C H I N G
2. D em W R T A D E Y S B
3. T em C O M P U T E R S
1. Estenda a classe Java definida neste capítulo para que a mesma contenha métodos para a
inserção em um local específico e exclusão de um item com chave K. Utilize a representação
sequencial.
Estruturas de Dados 14
Módulo 3
Estruturas de Dados
Lição 9
Árvores de Pesquisa Binária
1. Objetivos
Outras aplicações das árvores binárias (ADT1) são a pesquisa e a classificação através da
aplicação de certas regras aos valores dos elementos armazenados em uma árvore binária.
Considere como exemplo a árvore binária apresentada.
O valor de cada node na árvore é maior que o valor do node à sua esquerda (se existir) e é menor
que o valor do node à sua direita (se existir). Em árvores de Pesquisa binária, esta propriedade
sempre deverá ser satisfeita.
Ao final desta lição, o estudante será capaz de:
• Discutir as propriedades de uma árvore de pesquisa binária
• Realizar as operações em árvores de pesquisa binária
• Aprimorar a pesquisa, inserção e remoção em árvores de Pesquisa binária mantendo o
balanceamento utilizando árvores AVL
1
ADT – sigla em inglês Abstract Data Type, Tipo de Dado Abstrato
Estrutura de Dados 4
JEDITM
Figura 2. BSTNode
Em Java,
class BSTNode {
int info;
BSTNode left, right;
public BSTNode(){
}
public BSTNode(int i) {
info = i;
}
public BSTNode(int i, BSTNode l, BSTNode r) {
info = i;
left = l;
right = r;
}
}
A árvore de pesquisa binária utilizada nesta lição tem uma cabeça de lista (list head) como ilustrado
na figura abaixo:
Se a árvore de pesquisa binária está vazia, os ponteiros da esquerda e da direita do header da lista
apontam para si mesmos; senão, o ponteiro da direita apontará para a raiz da árvore. Na sequência
está a definição da classe de uma BST usando esta estrutura:
public class BST {
Estrutura de Dados 5
JEDITM
2.1. Pesquisando
O mesmo processo é repetido até que uma correspondência seja encontrada ou que se atinja um
node folha. Neste caso, a pesquisa não teve sucesso.
A seguir está a implementação Java para o algoritmo acima:
// Pesquisa por k, retorna o node que contém k se encontrado
public BSTNode search(int k) {
BSTNode p = bstHead.right; // node raiz
// Compare
while (true) {
if (k == p.info)
return p; // sucesso na Pesquisa
else if (k < p.info) // vá pela esquerda
if (p.left != null)
p = p.left;
else
return null; // não encontrou
else // vá pela direita
if (p.right != null)
p = p.right;
else
return null; // não encontrou
}
}
2.2. Inserção
Na inserção de um valor na árvore será realizada uma pesquisa para encontrar o local apropriado
para o novo valor. A seguir está o algoritmo:
Estrutura de Dados 6
JEDITM
1. Comece a Pesquisa no node da raiz. Declare um node p e faça-o apontar para a raiz.
2. Faça a comparação:
if (k == p.info) return false // se encontrou a chave, não permite inserção
else if (k < p.info) p = p.left // pela esquerda
else p = p.right // se (k > p.info) pela direita
3. Insira o node (p agora aponta para o novo pai do node para inserir):
newNode.info = k
newNode.left = null
newNode.right = null
if (k < p.info) p.left = newNode
else p.right = newNode
Em Java,
// Insere k na árvore de pesquisa binária
public boolean insert(int k) {
BSTNode p = bstHead.right; // node raiz
BSTNode newNode = new BSTNode();
2.3. Eliminação
Eliminar uma chave da árvore de pesquisa binária é um pouco mais complexo que as outras duas
operações discutidas. A operação inicia por encontrar a chave para deletar. Se não encontrar, o
algoritmo simplesmente retorna dizendo que a exclusão falhou. Se a Pesquisa retornar uma chave
encontrada, então existe necessidade de eliminar o node que contém a chave que procuramos.
Estrutura de Dados 7
JEDITM
Entretanto, excluir não é tão simples como remover um node encontrado que tenha um parente
apontando pra ele. Existe também a possibilidade que ele seja o parente de alguns outros nodes na
BST. Neste caso, existe a necessidade desses filhos serem “adotados” por outros nodes, e também
ajustar os ponteiros que apontavam para o node removido. E, no processo de atribuir apontadores, a
propriedade BST da ordem dos valores das chaves deve ser mantida.
Existem dois casos gerais para se considerar na exclusão de um node d:
1. node d é externo (folha):
1. Obtenha o node predecessor ip. Node predecessor é definido como o node mais à direita
na subárvore a esquerda do node corrente, o qual neste caso é d. Ele contém a chave
precedente se o BST é atravessado node
2. Obtenha o parente de ip, diga p_ip
3. Substitua d com ip
4. Remova ip de sua antiga localização para ajustar os apontadores:
a. Se ip não é uma folha node:
1. Ajuste o filho da direita de p_ip para apontar para o filho da esquerda de ip
2. Ajuste o filho da esquerda de ip para apontar para o filho da esquerda de d
b. Ajuste o filho da direita de ip para apontar para o filho da direita de d
Estrutura de Dados 8
JEDITM
Caso 2: Se d não tem filho a esquerda, mas possui uma subárvore à direita.
1. Obtenha o sucessor inorder, is. O sucessor Inorder é definido como o node mais à
esquerda na subárvore da direita do node atual. Ele contém a próxima chave se a APB
estiver sendo varrida em ordem direta.
2. Obtenha o pai de is, digamos p_is.
3. Substitua d por is.
4. Remova is de sua antiga locação pelo ajuste dos ponteiros:
a. Se is não é um node folha:
1. Ajuste o filho da esquerda de p_is para apontar para o filho da direita de is
2. Ajuste o filho da direita de is para apontar para o filho da direita de d
b. Ajuste o filho da esquerda de is para apontar para o filho da esquerda de d
Estrutura de Dados 9
JEDITM
Estrutura de Dados 10
JEDITM
inPreParent.right = inPre.left;
inPre.left = delNode.left;
}
inPre.right = delNode.right;
}
// Case 2.2: Se delNode é interno e não tem filho a esquerda, mas a direita
else {
BSTNode inSuc = delNode.right; // successor inorder
BSTNode inSucParent = null; // pai do sucessor inorder
As três operações discutidas fazem pesquisas pelo node que contém uma chave determinada. Por
isso, concentraremos a atenção em relação a pesquisa que possui o melhor desempenho. O
algoritmo de pesquisa em uma BST gasta O(log2 n) vezes em média, quando a árvore de pesquisa
binária está balanceada, quando a altura está em O(log2 n). Entretanto, este nem sempre é o caso
num processo de pesquisa. Considere, por exemplo, o caso onde os elementos inseridos na BST
estão ordenados (ou em ordem inversa), então a BST resultante terá todo seus nodes da esquerda
(ou direita) apontando para NULL, resultando numa árvore degenerada. Neste caso, o tempo de
pesquisa deteriora para O(n), equivalendo a uma pesquisa seqüencial, como no caso da árvore
seguinte:
Estrutura de Dados 11
JEDITM
Uma das árvores de Pesquisa binárias mais comumente utilizadas é a árvore AVL. Foi criada por G.
Adel’son-Vel’skii e E. Landis, de onde advém o nome AVL. Uma árvore AVL é balanceada quando a
diferença de altura entre as subárvores de um node, para cada node da árvore, é no máximo 1.
Considere as árvores abaixo onde os nodes estão identificados com os fatores de balanceamento:
As árvores binárias A, C e D todas têm nodes com balanceamentos na faixa [-1, 1], i.e., -1, 0 e +1.
Portanto, são árvores AVL. A árvore B tem um node com balanceamento +2, e está além da faixa,
não sendo uma árvore AVL.
Além de ter uma complexidade temporal O(log2 n) para Pesquisas, as seguintes operações terão a
mesma complexidade se a árvore AVL for usada:
• Encontrar o nº item, dado n
• Inserir um item em um lugar específico
• Excluir um item específico
Estrutura de Dados 12
JEDITM
Para manter o balanceamento de uma árvore AVL, as rotações devem ocorrer durante a inserção e a
exclusão. As seguintes rotações são usadas:
• Rotação simples à direita (Simple right rotation - RR) – Usada quando o novo item C esta
na árvore à esquerda do node filho B do ancestral mais próximo A com fator de
balanceamento +2
• Rotação simples à esquerda (Simple left rotation - LR) – Utilizada quando o novo item C
está na subárvores à direita do node filho B do ancestral mais próximo A com fator de
balanceamento -2.
• Rotação esquerda direita (Left right rotation - LRR) – utilizado quando o novo item C está
na subárvore da direita do filho a esquerda do ancestral mais próximo A com fator de
balanceamento +2.
Estrutura de Dados 13
JEDITM
• Rotação direita esquerda (Right left rotation - RLR) – usado quando o novo item C estiver
na subárvore à esquerda do filho direita B do antecessor mais próximo A, com fator de
balanceamento -2.
A inserção em uma árvore de AVL e o mesmo que na BST. Entretanto, o equilíbrio do resultado tem
que ser verificado dentro da árvore de AVL. Para introduzir um novo valor:
Estrutura de Dados 14
JEDITM
Estrutura de Dados 15
JEDITM
4. Exercícios
1. Insira as seguintes chaves em uma árvore AVL, baseada na ordem informada:
a) 1674258903
b) ARFGEYBXCSTIOPLV
Realize um override nos métodos insert e delete para implementar as rotações para a árvore AVL
para manter o balanceamento.
Estrutura de Dados 16
Módulo 3
Estruturas de Dados
Lição 10
Hash Table e Técnicas de Hashing
1. Objetivos
Hashing é a aplicação de uma função matemática (chamada função hash) em valores de chave que
resultam no mapeamento dos possíveis valores de chave para uma faixa menor de endereços
relativos. A função hash é algo como uma caixa trancada que tem necessidade de uma chave para
se obter a saída que, neste caso, é o endereço onde a chave está armazenada:
No hashing não existe conexão óbvia entre a chave e o endereço gerado, pois a função “seleciona
randomicamente” um endereço para um valor específico de chave, sem se preocupar com a
seqüência física dos registros no arquivo. Por essa razão, hashing também é conhecido como
esquema de randomização.
Ao fim da lição, o estudante deve ser capaz de:
• Definir hashing e explicar como o hashing funciona
• Implementar técnicas simples de hashing
• Discutir como colisões são evitadas/minimizadas através da utilização de técnicas de
resolução de colisão
• Explicar os conceitos por trás de arquivos dinâmicos e hashing
Estruturas de Dados 4
JEDITM
Este é um dos métodos mais comuns de randomização. Se o valor chave é dividido por um número
n, a faixa de endereços gerados vai variar entre 0 e n-1.
A fórmula é:
h(k) = k mod n
Valor da Chave k Valor Hash h(k) Valor da Chave k Valor Hash h(k)
125 8 234 0
845 0 431 2
444 2 947 11
256 9 981 6
345 7 792 12
745 4 459 4
902 5 725 10
569 10 652 2
254 7 421 5
382 5 458 3
Estruturas de Dados 5
JEDITM
2.2. Desdobramento
Outra técnica simples de hashing é o desdobramento. Nessa técnica, o valor chave é dividido em
duas ou mais partes e então passam por uma operação de adição, AND ou XOR para se obter o
endereço hash. Se o resultado obtido tiver mais dígitos que o maior endereço no arquivo, os dígitos
excedentes de maior ordem são eliminados.
Existem diferentes formas de desdobramento. O valor chave pode ser desdobrado ao meio. Isso é
ideal para valores de chave relativamente pequenos já que eles poderiam facilmente caber nos
endereços disponíveis. Se, por qualquer motivo, a chave for desdobrada de forma desigual, a parte
da esquerda deve ser maior que a parte da direita. A chave também pode ser desdobrada em
terços. Isso é ideal para valores de chave um pouco maiores. Também podemos ter
desdobramento por dígitos alternados. Os dígitos das posições ímpares formam uma parte e os
dígitos das posições pares formam outra. Desdobrar ao meio e em terços pode ser feito ainda de
duas formas. Uma é o desdobramento pelos extremos onde algumas partes da chave desdobrada
são invertidas (imitando o jeito em que dobramos papel) e então somadas. Por último, há o
desdobramento por substituição onde nenhuma parte desdobrada das chaves é invertida.
A seguir, alguns exemplos de desdobramento por substituição:
1. Dígitos pares, desdobrando ao meio
125758 => 125+758 => 883
2. Desdobrando em terços
125758 => 12+57+58 => 127
6. Alternando dígitos
125758 => 155+278 => 433
2. Desdobrando em terços
125758 => 21+57+85 => 163
Estruturas de Dados 6
JEDITM
6. Alternando dígitos
125758 => 155+872 => 1027
Este método é útil para converter chaves com grande número de dígitos em chaves com menos
dígitos de forma que o endereço caiba em uma palavra de memória. É também mais fácil de
armazenar, pois as chaves não precisam de muito espaço para serem guardadas.
Estruturas de Dados 7
JEDITM
Existem várias técnicas de resolução de colisões e nessa seção iremos cobrir encadeamento,
utilização de buckets e endereçamento aberto.
3.1. Encadeamento
No encadeamento, m listas ligadas são mantidas, uma para cada possível endereço na tabela hash.
Utilizando encadeamento para resolver colisão no armazenamento do exemplo de hashing do Método
de Divisão por Número Primo:
Valor Chave k Valor Hash h(k) Valor Chave k Valor Hash h(k)
125 8 234 0
845 0 431 2
444 2 947 11
256 9 981 6
345 7 792 12
745 4 459 4
902 5 725 10
569 10 652 2
254 7 421 5
382 5 458 3
Estruturas de Dados 8
JEDITM
As chaves 845 e 234 têm hash para o endereço 0, então estão conectadas ao endereço. É o mesmo
caso para os endereços 2, 4, 5, 7 e 10, enquanto que os demais endereços não têm colisão. O
método de encadeamento resolve a colisão fornecendo nodes de conexão adicionais para cada um
dos valores.
Assim como no encadeamento, este método divide a tabela hash em m grupos de registros onde
cada grupo contém exatamente b registros, sendo cada endereço considerado um bucket.
Por exemplo:
A colisão é redefinida nessa abordagem. Ela acontece quando um bucket estoura – ou seja, quando
se tenta uma inserção em um bucket cheio. Por esse motivo, existe uma redução significativa no
número de colisões. Entretanto, este método desperdiça espaço e não está livre de ficar cheio
futuramente, caso em que uma regra para estouro deve ser criada. No exemplo acima, existem três
vagas em cada endereço. Por ter tamanho estático, surgirão problemas quando mais de três valores
tiverem hash para um mesmo endereço.
Estruturas de Dados 9
JEDITM
No endereçamento aberto, quando o endereço produzido por uma função hash h(k) não tem mais
espaço para inserções, um slot diferente de h(k) é alocado na tabela hash. Este processo é chamado
de verificação. Neste slot vazio, o novo registro que colidiu com o anterior, conhecido como chave
de colisão, pode ser seguramente alocado. Neste método, introduzimos a permutação da tabela de
endereços, digamos β0, β1, ... βm-1, esta permutação é chamada seqüência de verificação.
Nesta lição, iremos cobrir duas técnicas: verificação linear e hashing duplo.
Verificação linear é uma das técnicas mais simples para lidar com colisões em que o arquivo é lido ou
verificado seqüencialmente, como um arquivo circular, e a chave de colisão é armazenada no espaço
disponível mais próximo ao endereço. Isso é usado nos sistemas em que o esquema “primeiro a
chegar, primeiro a sair” é utilizado. Um exemplo é o sistema de reservas de uma empresa de linhas
aéreas em que os lugares para os passageiros na lista de espera são oferecidos quando os
passageiros aos quais os lugares estavam reservados não aparecem. Sempre que uma colisão ocorre
em um certo endereço, os endereços seguintes são testados, ou seja, procurados seqüencialmente
até que um vago seja encontrado. A chave utiliza então esse endereço. Um array deve ser
considerado circular, de forma que quando a última localização é alcançada, a pesquisa continua na
primeira posição do array.
Neste método, é possível que o arquivo inteiro seja pesquisado, a partir da posição i+1, e as chaves
de colisão sejam distribuídas por todo o arquivo. Se uma chave tem hash para a posição i, que está
ocupada, as posições i+1, …,n são pesquisadas procurando-se por uma que esteja vaga.
Os slots em uma tabela hash podem conter somente uma chave ou também podem conter um
bucket. Nesse exemplo, buckets de capacidade 2 são usados para armazenar as seguintes chaves:
Valor Chave k Valor Hash h(k) Valor Chave k Valor Hash h(k)
125 8 234 0
845 0 431 2
444 2 947 11
256 9 981 6
345 7 792 12
745 4 459 4
902 5 725 10
569 10 652 2
254 7 421 5
382 5 458 3
CHAVE1 CHAVE2
0 845 234
1
2 444 431
3 652 458
Estruturas de Dados 10
JEDITM
CHAVE1 CHAVE2
4 745 459
5 902 382
6 981 421
7 345 254
8 125
9 256
10 569 725
11 947
12 792
Nessa técnica, a chave 652 indicou endereço de hash 2, mas já está cheio. Verificar o próximo
endereço disponível nos leva ao endereço 3, onde a chave é armazenada. Prosseguindo no processo
de inserção, a chave 458 tem endereço de hash 3 e é armazenada no segundo slot do endereço.
Com a chave 421 que tem hash para o endereço cheio 5, o espaço disponível seguinte é o endereço
6, onde a chave é armazenada.
Esta abordagem resolve o problema de estouro no endereçamento do bucket. Além disso, procurar
por espaço disponível faz com que as chaves excedentes sejam armazenadas próximas de seus
endereços originais, na maioria dos casos. Entretanto, este método sofre com o problema de
deslocamento onde as chaves que por direito detém um endereço podem ser deslocadas por outras
chaves que simplesmente encontraram aquele endereço vago. Além disso, verificar uma tabela hash
cheia levará um tempo de complexidade de O(n).
O hashing duplo faz uso de uma segunda função hash, digamos h2(k), sempre que houver colisão. O
registro é inicialmente indicado para um endereço de hash utilizando-se a primeira função. Se o
endereço de hash não estiver disponível, é aplicada uma segunda função hash e acrescentada ao
primeiro valor hash, e a chave de colisão é levada ao novo endereço hash se houver espaço
disponível. Se não houver, o processo é repetido. A seguir o algoritmo:
1. Utilize a função hash primária h1(k) para determinar a posição i onde colocar o valor.
2. Se houver colisão, utilize a função de rehash rh(i, k) sucessivamente até que um slot vago
seja encontrado:
Utilizando a segunda função hash h2(k)= k mod 11 no armazenamento das seguintes chaves:
Valor Chave k Valor Hash h(k) Valor Chave k Valor Hash h(k)
125 8 234 0
845 0 431 2
444 2 947 11
256 9 981 6
Estruturas de Dados 11
JEDITM
Valor Chave k Valor Hash h(k) Valor Chave k Valor Hash h(k)
345 7 792 12
745 4 459 4
902 5 725 10
569 10 652 2
254 7 421 5
382 5 458 3
Para as chaves 125, 845, 444, 256, 345, 745, 902, 569, 254, 382, 234, 431, 947, 981, 792, 459 e
725, o armazenamento é direto – não houve estouro.
CHAVE1 CHAVE2
0 845 234
1
2 444 431
3
4 745 459
5 902 382
6 981
7 345 254
8 125
9 256
10 569 725
11 947
12 792
Inserir 652 na tabela hash resulta em estouro no endereço 2, então fazemos rehash:
CHAVE1 CHAVE2
0 845 234
1
2 444 431
3
4 745 459
Estruturas de Dados 12
JEDITM
CHAVE1 CHAVE2
5 902 382
6 981
7 345 254
8 125 652
9 256
10 569 725
11 947
12 792
Fazendo hash para 421 também resulta em colisão, então fazemos o rehash:
h2(421) = 421 mod 11 = 3
CHAVE1 CHAVE2
0 845 234
1
2 444 431
3 458
4 745 459
5 902 382
6 981
7 345 254
8 125 652
9 256
10 569 725
11 947 421
12 792
Por último, a chave 458 é armazenada no endereço 3. Este método é uma evolução da verificação
linear em termos de performance, ou seja, o novo endereço é computado utilizando-se outra função
hash ao invés de percorrer a tabela hash seqüencialmente por um espaço vago. Entretanto, assim
como a pesquisa linear, este método também sofre com o problema de deslocamento.
Estruturas de Dados 13
JEDITM
Hashing extensível utiliza uma estrutura auto-ajustável com tamanho de bucket ilimitado. Este
método de hashing é construído sobre o conceito de árvore.
4.1.1. Árvore
A idéia básica é construir um índice baseado na representação numérica binária do valor hash.
Utilizamos um conjunto mínimo de dígitos binários e acrescentamos mais dígitos se necessário. Por
exemplo, considere que cada chave em hash é uma seqüência de três dígitos, e no início, precisamos
de apenas três buckets:
BUCKET ENDEREÇO
A ,00
B 10
C 11
A figura na esquerda mostra a árvore. A figura na direita mostra a tabela hash e os ponteiros para os
buckets reais. Para o bucket A, já que somente um dígito é considerado, ambos endereços 00 e 01
apontam para ele. O bucket tem a estrutura (PROFUNDIDADE, DADO) onde PROFUNDIDADE é a
quantidade de dígitos considerados no endereçamento ou a quantidade de dígitos considerados na
árvore. No exemplo, a profundidade do bucket A é 1, enquanto que para os buckets B e C, é 2.
Quando A estoura, um novo bucket é criado, digamos D. Isso irá criar mais espaço para inserção.
Estruturas de Dados 14
JEDITM
Quando B estoura, um novo bucket E é criado e o endereço de B é expandido para três dígitos:
Normalmente oito buckets são necessários para o conjunto de todas as chaves com três dígitos,
mas, nesta técnica, utilizamos o conjunto mínimo de buckets necessário. Note que quando o espaço
de endereçamento é aumentado, seu tamanho original é dobrado.
A exclusão de chaves leva a buckets vazios e resulta na necessidade de colapsar buckets vizinhos.
Dois buckets são vizinhos se eles estão na mesma profundidade e seus bits iniciais são os mesmos.
Por exemplo, na figura anterior, os buckets A e D têm a mesma profundidade e seus bits iniciais são
os mesmos. O caso é similar com os buckets B e E, pois ambos têm a mesma profundidade e seus
dois bits iniciais são iguais.
O hashing dinâmico é muito semelhante ao hashing extensível, pois também aumenta em tamanho à
medida que novos registros são acrescentados. Eles diferem somente na forma em que o tamanho
cresce dinamicamente. Se no hashing extensível a tabela hash tem seu tamanho duplicado cada vez
que é expandida, no hashing dinâmico, o crescimento é lento e incremental. O hashing dinâmico
inicia com um tamanho de endereçamento fixo semelhante ao hashing estático, e então cresce
conforme necessário. Normalmente, duas funções hash são usadas. A primeira é para verificar se o
bucket está no espaço de endereçamento original e a segunda é usada caso não esteja. A segunda
função hash é usada para guiar a pesquisa através da árvore.
Estruturas de Dados 15
JEDITM
Estruturas de Dados 16
JEDITM
5. Exercícios de Fixação
1. Considere as chaves a seguir:
12345 21453 22414 25411 45324 13541 21534 54231 41254 25411
a) Qual deve ser o valor de n se o método de hashing usado for Divisão por Número Primo?
b) Com o n encontrado em (a), faça o hash das chaves em uma tabela hash com tamanho n e
endereçamento de 0 a n-1. No caso de colisão, utilize verificação linear.
c) Utilizando desdobramento ao meio pelos extremos que resulte em endereços de três dígitos,
quais são os valores hash?
2. Usando Hashing Extensível, armazene as chaves abaixo em uma tabela hash na ordem
apresentada. Utilize primeiro os dígitos mais à esquerda. Utilize mais dígitos quando necessário.
Inicie a tabela hash com tamanho 2. Apresente a tabela a cada nova extensão.
1. Crie um programa Java que implemente o desdobramento ao meio por substituição. Este
programa recebe os parâmetros k, dk e da, onde k é a chave para fazer hash, dk é o número de
dígitos na chave e da é o número de dígitos no endereço. O programa deve retornar o endereço
com da dígitos.
2. Escreva um programa Java completo que usa o método da divisão como método de hash e
verificação linear como técnica de resolução de colisão.
Estruturas de Dados 17
JEDITM
Instituto CTS
Patrocinador do DFJUG.
Sun Microsystems
Fornecimento de servidor de dados para o armazenamento dos vídeo-aulas.
DFJUG
Detentor dos direitos do JEDITM nos países de língua portuguesa.
Banco do Brasil
Disponibilização de seus telecentros para abrigar e difundir a Iniciativa JEDITM.
Politec
Suporte e apoio financeiro e logístico a todo o processo.
Borland
Apoio internacional para que possamos alcançar os outros países de língua
portuguesa.
Instituto Gaudium/CNBB
Fornecimento da sua infra-estrutura de hardware de seus servidores para que os
milhares de alunos possam acessar o material do curso simultaneamente.
Estruturas de Dados 18