projeto jedi - estruturas de dados - java - 198 páginas

198
Módulo 3 Estruturas de Dados Lição 1 Conceitos Básicos e Notações Versão 1.0 - Mai/2007

Upload: augustonunes

Post on 15-Jun-2015

1.473 views

Category:

Documents


6 download

TRANSCRIPT

Page 1: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

Módulo 3Estruturas de Dados

Lição 1Conceitos Básicos e Notações

Versão 1.0 - Mai/2007

Page 2: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

AutorJoyce Avestro

EquipeJoyce AvestroFlorence BalagtasRommel FeriaReginald HutchersonRebecca OngJohn Paul PetinesSang ShinRaghavan SrinivasMatthew Thompson

Necessidades para os ExercíciosSistemas Operacionais SuportadosNetBeans IDE 5.5 para os seguintes sistemas operacionais:

• Microsoft Windows XP Profissional SP2 ou superior• Mac OS X 10.4.5 ou superior• Red Hat Fedora Core 3 • Solaris™ 10 Operating System (SPARC® e x86/x64 Platform Edition)

NetBeans Enterprise Pack, poderá ser executado nas seguintes plataformas:• Microsoft Windows 2000 Profissional SP4• Solaris™ 8 OS (SPARC e x86/x64 Platform Edition) e Solaris 9 OS (SPARC e

x86/x64 Platform Edition) • Várias outras distribuições Linux

Configuração Mínima de HardwareNota: IDE NetBeans com resolução de tela em 1024x768 pixel

Sistema Operacional Processador Memória HD Livre

Microsoft Windows 500 MHz Intel Pentium III workstation ou equivalente

512 MB 850 MB

Linux 500 MHz Intel Pentium III workstation ou equivalente

512 MB 450 MB

Solaris OS (SPARC) UltraSPARC II 450 MHz 512 MB 450 MB

Solaris OS (x86/x64 Platform Edition)

AMD Opteron 100 Série 1.8 GHz 512 MB 450 MB

Mac OS X PowerPC G4 512 MB 450 MB

Configuração Recomendada de Hardware

Sistema Operacional Processador Memória HD Livre

Microsoft Windows 1.4 GHz Intel Pentium III workstation ou equivalente

1 GB 1 GB

Linux 1.4 GHz Intel Pentium III workstation ou equivalente

1 GB 850 MB

Solaris OS (SPARC) UltraSPARC IIIi 1 GHz 1 GB 850 MB

Solaris OS (x86/x64 Platform Edition)

AMD Opteron 100 Series 1.8 GHz 1 GB 850 MB

Mac OS X PowerPC G5 1 GB 850 MB

Requerimentos de SoftwareNetBeans 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).

Para mais informações: http://www.netbeans.org/community/releases/55/relnotes.html

Estruturas de Dados 2

Page 3: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Colaboradores que auxiliaram no processo de tradução e revisãoAlexandre MoriAlexis da Rocha SilvaAline Sabbatini da Silva AlvesAllan Wojcik da SilvaAndré Luiz MoreiraAnna Carolina Ferreira da RochaAntonio Jose R. Alves RamosAurélio Soares NetoBárbara Angélica de Jesus BarbosaBruno da Silva BonfimBruno dos Santos MirandaBruno Ferreira RodriguesCarlos Alexandre de SeneCarlos Eduardo Veras NevesCleber Ferreira de SousaEveraldo de Souza SantosFabrício Ribeiro BrigagãoFernando Antonio Mota TrintaFrederico DubielGivailson de Souza Neves

Jacqueline Susann BarbosaJoão Paulo Cirino Silva de NovaisJoão Vianney Barrozo CostaJosé Augusto Martins NieviadonskiJosé Ricardo CarneiroKleberth Bezerra G. dos SantosKefreen Ryenz Batista LacerdaLeonardo Leopoldo do NascimentoLucas Vinícius Bibiano ThoméLuciana Rocha de OliveiraLuís Carlos AndréLuiz Fernandes de Oliveira Junior Luiz Victor de Andrade LimaMarco Aurélio Martins BessaMarcos Vinicius de ToledoMarcus Borges de S. Ramos de PáduaMaria Carolina Ferreira da SilvaMassimiliano GiroldiMauricio da Silva MarinhoMauro Cardoso Mortoni

Mauro Regis de Sousa LimaNamor de Sá e SilvaNolyanne Peixoto Brasil VieiraPaulo Afonso CorrêaPaulo Oliveira Sampaio ReisPedro Antonio Pereira MirandaRenato Alves FélixRenê César PereiraReyderson Magela dos ReisRicardo Ulrich BomfimRobson de Oliveira CunhaRodrigo Fernandes SuguiuraRodrigo Vaez Ronie DotzlawRosely Moreira de JesusSeire ParejaSilvio SzniferTiago Gimenez RibeiroVanderlei Carvalho Rodrigues PintoVanessa dos Santos Almeida

Auxiliadores especiais

Revisão Geral do texto para os seguintes Países:

• Brasil – Tiago Flach• Guiné Bissau – Alfredo Cá, Bunene Sisse e Buon Olossato Quebi – ONG Asas de Socorro

Coordenação do DFJUG

• Daniel deOliveira – JUGLeader responsável pelos acordos de parcerias• Luci Campos - Idealizadora do DFJUG responsável pelo apoio social• Fernando Anselmo - Coordenador responsável pelo processo de tradução e revisão,

disponibilização dos materiais e inserção de novos módulos• Rodrigo Nunes - Coordenador responsável pela parte multimídia• Sérgio Gomes Veloso - Coordenador responsável pelo ambiente JEDITM (Moodle)

Agradecimento Especial

John Paul Petines – Criador da Iniciativa JEDITM

Rommel Feria – Criador da Iniciativa JEDITM

Estruturas de Dados 3

Page 4: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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.

Ao final desta lição, o estudante será capaz de:

• Explicar os processos de resolução de problemas• Definir tipos de dados (data type), tipos de dados abstratos (abstract data type) e

estrutura de dados (data structure)• Identificar as propriedades de um algoritmo• Diferenciar os dois métodos de endereçamento – endereçamento computado e

endereçamento por link• Utilizar as funções matemáticas básicas para analisar algoritmos• Mensurar a complexidade dos algoritmos expressando a eficiência em termos de

complexidade de tempo e notação Big-O

Estruturas de Dados 4

Page 5: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

2. Processo de resolução de problemas

Programação é um processo de resolução de problemas. Por exemplo, o problema é identificado, o dado a ser manipulado e trabalhado é distinguido e o resultado esperado é determinado. Isso é implementado em uma máquina chamada computador e as informações fornecidas para ela são utilizadas para solucionar um dado problema. O processo de resolução de problemas pode ser visto em termos de Domínio de Problema (Domain Problem), máquina e solução.

Domínio do problema inclui a entrada (input), ou os dados brutos, em um processo, e a saída (output), ou os dados processados. Por exemplo, na classificação de um conjunto de números aleatórios, o dado bruto é um conjunto de números na ordem original, aleatórios, e o dado processado são os números em ordem classificada, crescente, por exemplo.

O domínio de máquina (machine domain) consiste em meios de armazenamento (storage medium) e unidades processadas. Os meios de armazenamento – bits, bytes, etc – consistem na combinação de bits em seqüências que são endereçáveis como unidade. As unidades processadas nos permitem melhorar o desempenho de operações básicas que incluem a aritmética, a comparação e assim por diante.

O domínio de solução (solution domain), em outras palavras, liga os domínios de problema e de máquina. É no domínio de solução que as estruturas de dados de alto nível e os algoritmos são afetados.

Estruturas de Dados 5

Page 6: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

3. Tipo de Dado, Tipo de Dado Abstrato e Estrutura de Dados

Tipo de dado (data type) refere-se à classificação do dado que um atributo pode assumir, armazenar ou receber em uma linguagem de programação e para a qual as operações são automaticamente fornecidas. Em Java, os dados primitivos são:

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:

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;}

Estrutura de dados é a implementação de um TDA em termos de tipos de dados ou outras estruturas de dados. Uma estrutura de dados é modelada através de classes. Classes especificam como as operações são executadas. Para implementar um TDA como uma estrutura de dados, uma interface é implementada através de uma classe.

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

Page 7: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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.

Considere o seguinte exemplo:

public class Minimum { public static void main(String[] args) { int a[] = { 23, 45, 71, 12, 87, 66, 20, 33, 15, 69 }; int min = a[0]; for (int i = 1; i < a.length; i++) { if (a[i] < min) min = a[i]; } System.out.println("The minimun value is: " + min); }}

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.

Todas as propriedades devem ser garantidas na construção de um algoritmo.

Estruturas de Dados 7

Page 8: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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.

5.1. Método de Endereçamento Computado

O método de endereçamento computado é utilizado para acessar os elementos de uma estrutura em um espaço pré-alocado. É essencialmente estático. Um array, por exemplo:

int x[] = new int[10];

Um item de dado pode ser acessado diretamente pelo índice de onde o dado está armazenado.

5.2. Método de Endereçamento por Link

Este método de endereçamento fornece um mecanismo de manipulação dinâmica de estruturas, onde o tamanho e a forma não são conhecidos de antemão, ou que são alterados durante a execução. O importante para este método é o conceito de node contido nestes dois campos: INFO e LINK.

Figura 1: Estrutura de nodeEm Java:

public class Node { public Object info; public Node link;

public Node(Object o) { info = o; }

public Node(Object o, Node n) { info = o; link = n; }}

5.2.1. Alocação de ligação: O Pool de Memória

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

Page 9: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 2: Lista Disponível

A seguir uma classe Java chamada AvailList:

public class AvailList { private Node head;

public AvailList() { head = null; }

public AvailList(Node n){ head = n; }}

Criando uma lista disponível através de uma simples declaração:

AvailList avail = new AvailList();

5.2.2. Dois Procedimentos Básicos

Os dois procedimentos básicos que manipulam a lista disponível são getNode e setNode, que obtém e retornam um node, respectivamente.

O método seguinte na classe AvailList obtém um node da lista disponível:

public Node getNode() { return head;}

Figura 3: Obtém um node

enquanto o método a seguir na classe Avail retorna um node para a lista disponível:

public void setNode(Node n) { n.link = head.link; // Adiciona o novo node no início da lista disponível head.link = n;}

Estruturas de Dados 9

Page 10: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 4: Retorna um node

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:

public class TestNodes { public static void main(String [] args) { Node n1 = new Node("1"); Node n2 = new Node("2", n1); Node n3 = new Node("3", n2); Node n4 = new Node("4", n3); AvailList avail = new AvailList(n4); 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

Page 11: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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

ex. 3.14 = 3 1/2 = 0 -1/2 = - 1

• Ceil de x – é o menor inteiro maior que ou igual a x, onde x é um número real qualquer.

Notação : x

ex. 3.14 = 4 1/2 = 1 -1/2 = 0

• Módulo - Dados quaisquer dois números reais x e y, x mod y é definido como

x mod y = x se y = 0 = x - y * x / y se y <> 0

ex. 10 mod 3 = 124 mod 8 = 0-5 mod 7 = 2

6.1. Identidades

O que segue são identidades relacionadas às funções matemáticas definidas acima:

• x = x se e somente se x é um inteiro• x > x se e somente se x não é um inteiro• - x = - x • x + y <= x + y • x = x + x mod 1• z ( x mod y ) = zx mod zy

Estruturas de Dados 11

Page 12: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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.

7.1. Eficiência de Algoritmos

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

7.2. A Notação Big-O

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:

Big-O Descrição Algoritmo

O(1) Constante

O(log2n) Logarítmica Busca Binária

O(n) Linear Busca Seqüencial

O(n log2n) Heapsort

O(n2) Quadrática Inserção Ordenada

O(n3) Cúbica Algoritmo de Floyd

Estruturas de Dados 12

Page 13: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Big-O Descrição Algoritmo

O( 2n ) Exponencial

Tabela 2: Tempos de computação sobre análise de algoritmos

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:

F(n) Tempo de Execução

log2n 19.93 microssegundos

n 1.00 segundos

n log2n 19.93 segundos

n2 11.57 dias

n3 317.10 séculos

2n Eternidade

Tabela 3: Tempo de execução

7.3. Operações sobre a Notação O

1. Regra para Adição

Suponha que T1(n) = O( f(n) ) e T2(n) = O( g(n) ). Então, t(n) = T1(n) + T2(n) = O( max( f(n), g(n) ) ).

Prova: Por definição da notação O,T1(n) ≤ c1 f(n) para n ≥ n1 e T2(n) ≤ c2 g(n) para n ≥ n2.

Seja n0 = max(n2, n2). EntãoT1(n) + T2(n) ≤ c1 f(n) + c2 g(n) n ≥ n0.

≤ (c1 + c2) max(f(n),g(n)) n ≥ n0.≤ c max ( f(n), g(n) ) n ≥ n0.

Sendo assim, T(n) = T1(n) + T2(n) = O( max( f(n), g(n) ) ).

Por exemplo, 1. T(n) = 3n3 + 5n2 = O(n3)2. T(n) = 2n + n4 + nlog2n = O(2n)

2. Regra para Multiplicação

Suponha que T1(n) = O( f(n) ) e T2(n) = O( g(n) ). Então, T(n) = T1(n) * T2(n) = O( f(n) * g(n) ).

Por exemplo, considere o algoritmo abaixo:

for (int i=1; i < n-1; i++) for (int i=1; i <= n; i++) // as iterações são executadas O(1) vezes

Já que as iterações no laço mais interno são executadas:

Estruturas de Dados 13

Page 14: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

n + n-1 + n-2 + ... + 2 + 1 vezes,

então

n(n+1)/2 = n2/2 + n/2 = O(n2)

Exemplo: Considere o trecho de código abaixo:

for (i=1; i <= n, i++) for (j=1; j <= n, j++) // iterações que são executados O(1) vezes

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á:

n(n+1)/2 = n2/2 + n/2 = O(n2)

7.4. Análise de Algoritmos

Exemplo 1: Revisitação Mínima

public class Minimum { public static void main(String [] args) { int a[] = {23, 45, 71, 12, 87, 66, 20, 33, 15, 69}; int min = a[0]; for (int i = 0; i < a.length; i++) { if (a[i] < min) min = a[i]; } System.out.println("Minimun value is: " + min); } }

No algoritmo, as declarações de a e min terão tempos constantes. O tempo constante da sentença if no loop for serão executadas n vezes, onde n é o número de elementos do array a. A última linha também será executada em tempo constante.

Linha #Vezes executada

4 1

5 1

6 n+1

7 n

9 1

Tabela 4: Quantidade de execuções por linha

Usando a regra para adição, temos:

T(n) = 2n +4 = O(n)

Estruturas de Dados 14

Page 15: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Já que g(n) <= c f(n) para n >= n0, então

2n + 4 <= cn2n + 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

• Tempo de execução da declaração dentro do laço FOR vezes o número de iterações.

• Laços FOR ANINHADOS

• 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

• A declaração com o maior tempo de execução.

• 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

Page 16: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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 7d) 3 mod –4e) –5 mod 2f) 10 mod 11 g) (15 mod –9) + 4.3

b) Qual é a complexidade de tempo do algoritmo com os seguintes tempos de execução?

a) 3n5 + 2n3 + 3n +1b) n3/2+n2/5+n+1c) n5+n2+nd) 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?

d) Ordene as seguintes complexidades de tempo em ordem ascendente.

0(n log2 n) 0(n2) 0(n) 0(log2 n) 0(n2 log2 n)

0(1) 0(n3) 0(nn) 0(2n) 0(log2 log2 n)

e) Qual é o tempo de execução e a complexidade de tempo do algoritmo abaixo?

void warshall(int A[][], int C[][], int n){ for (int i=1; i<=n; i++) for (int j=1; j<=n; j++) A[i][j] = C[i][j]; for (int i=1; i<=n; i++) for (int j=1; j<=n; j++) for (int k=1; k<=n; k++) if (A[i][j] == 0) A[i][j] = A[i][k] & A[k][j]; }

Estruturas de Dados 16

Page 17: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

Módulo 3Estruturas de Dados

Lição 2Stack

Versão 1.0 - Mai/2007

Page 18: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

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.

Stack é utilizada em reconhecimentos de padrões, listas e árvores transversais, avaliação de expressões, soluções de recursividade e muito mais. As duas operações básicas para manipulação de dados em uma stack são “push” e “pop”, ou seja, inserção e retirada de elementos do topo da stack respectivamente.

Ao final desta lição, o estudante será capaz de:

• Explicar os conceitos básicos e operações em uma stack ADT• Implementar uma stack ADT usando representação seqüencial e de ligação• Discutir aplicações de stack: Os problemas de reconhecimento de padrões e conversões do

tipo infix para postfix• Explicar como múltiplas stacks podem ser armazenadas utilizando array de uma dimensão• Realocação de memória durante um transbordamento (estouro) de um array com múltiplas

stacks utilizando algoritmos unit-shift policy e Garwick's

Estruturas de Dados 4

Page 19: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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;}

StackException é uma extensão de RuntimeException:

class StackException extends RuntimeException{ public StackException(String err){ super(err); }}

As stacks possuem duas implementações possíveis - array unidimensional seqüencialmente alocado ou uma lista linear encadeada. Entretanto, a implementação que será usada é uma interface Stack.

A seguir as operações de uma stack:

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

Figura 1. Operação PUSH

Figura 2. Operação POP

Estruturas de Dados 5

Page 20: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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:

Figura 3. Retirar e inserir

A seguir, a implementação de uma stack usando a representação seqüencial:

public class ArrayStack implements Stack { // Array usado para implementar a stack Object S[];

// Inicializa a stack em vazio int top = -1;

// Inicializa a stack para o padrão 0 public ArrayStack() { this(0); }

// Inicializa a stack para ser o comprimento recebido public ArrayStack(int c) { S = new Object[c]; }

// Implementação do método size public int size() { return (top+1); }

// Implementação do método isEmpty public boolean isEmpty() { return (top < 0); }

// Implementação do método top public Object top() { if (isEmpty()) throw new StackException("Stack empty."); return S[top]; }

// Implementação do método pop public Object pop() { Object item; if (isEmpty()) throw new StackException("Stack underflow."); item = S[top];

Estruturas de Dados 6

Page 21: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

S[top--] = null; return item; }

// Implementação do método push public void push(Object item) { if (size() == s.length) throw new StackException("Stack overflow."); S[++top]=item; }}

Podemos testar esta representação com a seguinte classe:

public class TestArrayStack { public static void main(String [] args) { ArrayStack stack = new ArrayStack(4); stack.push("1"); stack.push("2"); stack.push("3"); stack.push("4"); System.out.println(stack.pop()); System.out.println(stack.pop()); System.out.println(stack.pop()); System.out.println(stack.pop()); } }

Como resultado, lembre-se que a stack armazena “Empilhando” os dados, e retirá-os do último ao primeiro elemento armazenado.

Estruturas de Dados 7

Page 22: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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:

Figura 4. Representação Encadeada

O código Java a seguir implementa a stack utilizando representação encadeada:

public class LinkedStack implements Stack { private Node top;

// O número de elementos na stack private int numElements = 0;

// Implementaçao do método size public int size() { return (numElements); }

// Implementaçao do método isEmpty public boolean isEmpty() { return (top == null); }

// Implementaçao do método top public Object top() { if (isEmpty()) throw new StackException("Stack empty."); return top.info; }

// Implementaçao do método pop public Object pop() { Node temp; if (isEmpty()) throw new StackException("Stack underflow."); temp = top;

Estruturas de Dados 8

Page 23: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

top = top.link; return temp.info; }

// Implementaçao do método push public void push(Object item) { Node newNode = new Node(item); newNode.link = top; top = newNode; }}

Podemos testar esta representação com a seguinte classe:

public class TestArrayStack { public static void main(String [] args) { LinkedStack stack = new LinkedStack(); stack.push("1"); stack.push("2"); stack.push("3"); stack.push("4"); System.out.println(stack.pop()); System.out.println(stack.pop()); System.out.println(stack.pop()); System.out.println(stack.pop()); } }

Estruturas de Dados 9

Page 24: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

5. Aplicação Exemplo: Problema do Reconhecimento de Padrão

Dado o conjunto L = { wcwR | w ⊂ { a, b }+ }, onde wR é o reverso de w. L define uma linguagem que contém um conjunto infinito de strings palíndromos. w não pode ser uma string vazia. Exemplos são aca, abacaba, bacab, bcb e aacaa.

O algoritmo seguinte pode ser usado para resolver o problema:

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:

1. O fim da string foi atingida mas o c não foi encontrado.2. O fim da string foi atingido mas a stack não está vazia.3. A stack está vazia mas o fim da string não foi atingido ainda.

Os exemplos a seguir ilustram como o algoritmo trabalha:

Entrada Ação Stack

abbabcbabba ------ (bottom) --> (top)

abbabcbabba Entre a a

bbabcbabba Entre b ab

babcbabba Entre b abb

abcbabba Entre a abba

bcbabba Entre b abbab

cbabba Descarte c abbab

babba Abra, compare b e b --> ok abba

abba Abra, compare a e a --> ok abb

bba Abra, compare b e b --> ok ab

ba Abra, compare b e b --> ok a

a Abra, compare a e a --> ok -

- Sucesso

Tabela 1: Execução do algoritmo

Entrada Ação Stack

abacbab ------ (bottom) --> (top)

abacbab Entre a a

bacbab Entre b ab

acbab Entre a aba

cbab Descarte c aba

Estruturas de Dados 10

Page 25: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

bab Abra, compare a e b

--> não batem, na string

ba

Tabela 2: Execução do algoritmo

No primeiro exemplo, a string será aceita enquanto que no segundo não.

A seguir temos uma classe Java utilizada para implementar o padrão recognizer:

public class PatternRecognizer{ ArrayStack S = new ArrayStack(100);

public static void main(String[] args) { PatternRecognizer pr = new PatternRecognizer(); if (args.length < 1) System.out.println("Usage: PatternRecognizer <input string>"); else { boolean inL = pr.recognize(args[0]); if (inL) System.out.println(args[0] + " is in the language."); else System.out.println(args[0] + " is not in the language."); } } boolean recognize(String input) { int i=0; // Indicador de caractere corrente

// Enquanto c não é encontrado, entre com um caractere na stack while ((i < input.length()) && (input.charAt(i) != 'c')) { S.push(input.substring(i, i+1)); i++; }

// O fim da string foi atingido mas o c não foi encontrado if (i == input.length()) return false;

// Descarte o c, mova para o próximo caractere i++;

// 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;

// O fim da string foi alcançado mas a stack não está vazia else if ((i == input.length()) && (!S.isEmpty())) return false;

else return true;

Estruturas de Dados 11

Page 26: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

}}

5.1. Aplicação: Infix to Postfix

Uma expressão está na forma infix se toda sub-expressão a ser avaliada está no formato operando-operador-operando. Por outro lado, a forma postfix é aquela em que a sub-expressão a ser avaliada está na forma operando-operando-operador. Estamos acostumados a avaliar expressões infix, porém é mais apropriado para os computadores avaliarem expressões na forma postfix.

Existem algumas propriedades que precisamos tomar nota neste problema:

• O grau de um operador é o número de operandos que ele tem.• O rank de um operando é 1. O rank de um operando é 1 menos seu grau. O rank de uma

seqüência arbitrária de operandos e operadores é as somas dos ranks dos operandos e operadores individuais.

• Se z = x | y é uma string, então x é o topo de z. E x é um próprio topo se y não é uma string nula.

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.

A tabela a seguir mostra a ordem de precedência dos operadores:

Operador Prioridade Propriedade Exemplo

^ 3 Associação a direita a^b^c = a^(b^c)

* / 2 Associação a esquerda a*b*c = (a*b)*c

+ - 1 Associação a esquerda a+b+c = (a+b)+c

Tabela 3: Ordem de precedência

Exemplos:

Expressão Infix Expressão Postfix

a * b + c / d a b * c d / -

a ^ b ^ c - d a b c ^ ^ d -

a * ( b + ( c + d ) / e ) - f a b c d + e /+* f -

a * b / c + f * ( g + d ) / ( f – h ) ^ i a b * c / f g d + * f h – i ^ / +

Tabela 4: Expressão Infix e Postfix

Regras para conversão de infix para postfix:

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

Page 27: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

modo para as sub-expressões.

E a seguir estão os números prioritários:

• Icp (x de ·) - número da prioridade quando o simbólico (token) x é um símbolo de entrada (prioridade de entrada)

• Isp (x de ·) - número de prioridade quando o simbólico (token) x está na stack (prioridade de entrada na stack)

Token, x icp(x) isp(x) Rank

Operando 0 - +1

+ - 1 2 -1

* / 3 4 -1

^ 6 5 -1

( 7 0 -

Tabela 5: Números prioritários

Agora o algoritmo:

1. Pega o próximo símbolo (token) x.2. Se x é operando então sai x3. Se x é (, então insere x na stack.4. Se x é ), então retira elementos da stack até que ( seja encontrado, mais uma vez apagar o

(, Se topo = 0, o algoritmo termina.5. Se x é um operador, então, enquanto icp(x) < isp(stack(top)), remove os elementos da

stack; caso contrário; se icp(x) > isp(stack(top)), então insere x na stack.6. Retorna ao passo 1.

Como no exemplo, vamos fazer a conversão de a + ( b * c + d ) - f / g ^ h usando a forma postfix:

Símbolo de entrada

Stack saída Observações

a a sai a

+ + a entra +

( +( a entra (

b +( ab sai b

* +(* ab icp(*) > isp(()

c +(* abc sai c

+ +(+ abc* icp(+) < isp(*), sai *

icp(+) > isp((), insere +

d +(+ abc*d sai d

) + abc*d+ sai +, sai (

- - abc*d++ icp(-) < isp(+), pop +, push -

Estruturas de Dados 13

Page 28: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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

Page 29: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

6. Tópicos avançados de stacks

6.1. Múltiplas stacks usando um array unidimensional

Duas ou mais stack podem coexistir em um array S comum de tamanho n. Nesta abordagem temos uma melhora na utilização da memória.

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:

Figura 5. Duas stack coexistindo em um mesmo array

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.

6.2. Três ou mais stacks no mesmo array S:

Se três ou mais stack compartilham um mesmo array, elas precisarão de um indicador do endereço para os vários topos e bases destas, os apontadores de bases definem o inicio das stack m dentro do array S com tamanho igual a n, a notação disto será determinada por B[i]:

B[i] = n/m * i - 1 0 ≤ i < m

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,

T[i] = B[i] , 0 ≤ i ≤ m

Por exemplo:

Figura 6. Três stacks no mesmo array

Estruturas de Dados 15

Page 30: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

6.3. Três possíveis estados

O diagrama abaixo mostra as três possibilidades possíveis: stack vazia, cheia, cheia mas não lotada. Stack i está cheia se T[i] = B[i+1]. Não está cheia se T[i] < B[i+1] e está cheia se T[i] = B[i].

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[]; //int B[] = {-1,90,210,310,400,499}; //int T[] = {79,210,270,345,423}; //int oldT[] = {85,195,254,360,415}; // Construtor padrão public MStack() { S = new Object[n]; B = new int[m+1]; T = new int[m]; oldT = new int[m]; } // Construtor com número de stacks e tamanho public MStack(int numStacks, int s) { m = numStacks; n = s; S = new Object[n]; B = new int[m+1]; T = new int[m]; oldT = new int[m]; } // Returna o tamanho da stack i public int size(int i) { return (T[i]-B[i]); } // Checa se a stack está vazia public boolean isEmpty(int i) { return ((T[i]-B[i])==0); } // Returna o topo da stack i public Object top(int i) throws StackException {

Estruturas de Dados 16

Page 31: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

if (isEmpty(i)) throw new StackException("Stack empty."); return S[T[i]]; } // inserindo elementos na stack i public void push(int i, Object item) { if (T[i]==B[i+1]) MStackFull(i); S[++T[i]]=item; }

// retirando elementos na stack i public Object pop(int i) throws StackException { Object item; if (isEmpty(i)) throw new StackException("Stack underflow."); item = S[T[i]]; S[T[i]--] = null; return item; }

}

O método MStackFull captura uma possível condição de overflow.

public void MStackFull(int i) { // garwicks(i); // unitShift(i); }

Veremos os métodos descritos a seguir.

6.4. Realocação de memória no caso de estouro (Stack Overflow)

Quando as stack coexistem em um mesmo array é possível que uma determinada stack fique cheia enquanto a stack adjacente ainda esteja vazia ou “não cheia”. Neste cenário será preciso realocar memória para que seja possível disponibilizar mais espaço para a stack cheia. Para fazer isso procuramos a stack que possui endereços vazios.

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 */

public void unitShift(int i) throws StackException { int k=-1; // Pontos da mais proxima stack com espaço livre

// Procura a stack acima (endereço) for (int j=i+1; j<m; j++) if (T[j] < B[j+1]) { k = j; break; }

Estruturas de Dados 17

Page 32: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

// Troca os itens da stack k para fazer frente a stack i if (k > i) { for (int j=T[k]; j>T[i]; j--) S[j+1] = S[j];

// Ajusta os pontos de topo e base for (int j=i+1; j<=k; j++) {

T[j]++;B[j]++;

} } else if (k > 0) { // Procura a stack abaixo se nenhum for achado for (int j=i-1; j>=0; j--) if (T[j] < B[j+1]) { k = j+1; break; } for (int j=B[k]; j<=T[i]; j++) S[j-1] = S[j];

// Ajusta os ponteiros da base e to topo for (int j=i; j>k; j--) {

T[j]--; B[j]--; } } else // Não obteve sucesso, todas as stack estão cheias

throw new StackException("Stack overflow.");}

6.5. Realocação de Memória usando o algoritmo de Garwick

O algoritmo de Garwick é uma maneira mais eficaz de se realocar os espaços quando a stack se torna cheia. Ele realoca a memória em dois passos: primeiro, um tamanho fixo de espaço é dividido entre todas as stack; e, segundo, o espaço restante é distribuído nas stack de acordo com a necessidade. A seguir vemos o algoritmo:

1. Elimina todas as células que não estão sendo usadas de todas as stack e considera esse espaço não-usado como sendo um espaço válido para a realocação.

2. Realoca de 1 a 10% do espaço livre válido igualmente entre as stack.

3. Realoca o espaço restante disponível nas stack em proporção com o recente crescimento, onde o recente crescimento será medido como sendo a diferença entre T[j] – oldT[j], onde oldT[j] é o valor de T[j] ao final da ultima realocação. Uma diferença negativa (positiva) significa que a stack j realmente foi diminuída (aumentada) em tamanho desde a última realocação.

6.6. Implementação de Knuth para o Algoritmo de Garwick

A implementação de Knuth organiza os espaços para que sejam distribuídos igualmente entre as stacks em 10%. Os 90% restantes serão particionados de acordo com o crescimento recente. O tamanho da stack (crescimento cumulativo) também é usado como medida de necessidade para a distribuição dos 90%. Quanto maior a stack, maior a quantidade de espaço que será alocada.

A seguir vemos o algoritmo:

1. Reunindo estatísticas sobre o uso da stack:

Estruturas de Dados 18

Page 33: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Tamanho da stack = T[j] - B[j]

Nota: +1 se a stack estiver sobrecarregada

differences = T[j] – oldT[j] if T[j] – oldT[j] >0

else 0 [a diferença negativa será substituída por 0]

Nota: +1 se a stack estiver sobrecarregada

freecells = tamanho total – (soma dos tamanhos)

incr = (soma das diferenças)

Nota: contador recebe +1 para a célula que sobrecarregou a stack.

2. Calculo da alocação de fatores

α = 10% * freecells / m

β = 90%* freecells / incr

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.

3. Calculando o novo endereço

σ - espaço livre teoricamente alocado para as stack 0, 1, 2, ..., j - 1

τ - espaço livre teoricamente alocada para as stack 0, 1, 2, ..., j

Número real total de células livres alocadas na stack j = τ - σ

inicialmente, (new)B[0] = -1 e σ = 0

for j = 1 to m-1:

τ = σ + α + diff[j-1]*β

B[j] = B[j-1] + size[j-1] + τ - σ

σ = τ

4. Trocando as stack para suas novas coordenadas.

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

Page 34: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 8. Estado das stack antes da re-alocação

1. Reunindo estatísticas sobre o uso da stack

Tamanho das stack = T[j] - B[j]

Nota: +1 se a stack estiver sobrecarregada

Diferenças = T[j] – OLDT[j] if T[j] – OLDT[j] >0

else 0 [a diferença negativa é substituída por 0]

Nota: +1 se a stack estiver sobrecarregada

freecells = tamanho total – (soma dos tamanhos)

incr = (soma das diferenças)

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

2. Calculando a alocação dos fatores

α = 10% * freecells / m = 0.10 * 181 / 5 = 3.62

β = 90%* freecells / incr = 0.90 * 181 / 40 = 4.0725

3. Calcula o novo endereço

B[0] = -1 and σ = 0

for j = 1 to m:

τ = σ + α + diff(j-1)*β

B[j] = B[j-1] + size[j-1] + τ - σ

σ = τ

j = 1: τ = 0 + 3.62 + (0*4.0725) = 3.62

B[1] = B[0] + size[0] + τ - σ

= -1 + 80 + 3.62 – 0 = 82

Estruturas de Dados 20

Page 35: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

σ = 3.62

j = 2: τ = 3.62 + 3.62 + (16*4.0725) = 72.4

B[2] = B[1] + size[1] + τ - σ

= 82 + 121 + 72.4 – 3.62 = 272

σ = 72.4

j = 3: τ = 72.4 + 3.62 + (16*4.0725) = 141.18

B[3] = B[2] + size[2] + τ - σ

= 272 + 60 + 141.18 – 72.4 = 401

σ = 141.18

j = 4: τ = 141.18 + 3.62 + (0*4.0725) = 144.8

B[4] = B[3] + size[3] + τ - σ

= 401 + 35 + 144.8 – 141.18 = 439

σ = 144.8

Para checar, NEWB(5) deverá ser igual a 499:

j = 5: τ = 144.8 + 3.62 + (8*4.0725) = 181

B[5] = B[4] + size[4] + τ - σ

= 439 + 23 + 181 – 144.8 = 499 [OK]

4. Troca as stack para suas novas coordenadas.

B = (-1, 82, 272, 401, 439, 499)

T[i] = B[i] + size [i] ==> T = (0+80, 83+121, 273+60, 402+35, 440+23)

T = (80, 204, 333, 437, 463)

oldT = T = (80, 204, 333, 437, 463)

Figura 9. Estado das stacks após uma realocação

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

Page 36: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

tipos de estruturas (por exemplo queue, tabelas seqüenciais ou a combinação destas).

O método a seguir implementa um algoritmo Garwick's que será chamado pelo método MStackFull:

// Método Garwick'spublic 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;

// calculo de fatores de distribuição for (int j=0; j<m; j++) { size[j] = T[j]-B[j]; if ((T[j]-oldT[j]) > 0)

diff[j] = T[j]-oldT[j]; else

diff[j] = 0; totalSize += size[j]; incr += diff[j]; }

diff[i]++; size[i]++; totalSize++; incr++; freecells = n - totalSize; alpha = 0.10 * freecells / m; beta = 0.90 * freecells / incr;

// se todas as stack estiverem cheias if (freecells < 1) throw new StackException("Stack overflow.");

// cálculo para novas bases for (int j=1; j<m; j++) { tau = sigma + alpha + diff[j-1] * beta;

B[j] = B[j-1] + size[j-1] + (int)Math.floor(tau) - (int)Math.floor(sigma); sigma = tau;

}

// Restabeleça tamanho da stack que teve overflowed para o seu antigo valor size[i]--;

// cálculo para o novo endereço de topo for (int j=0; j<m; j++)

T[j] = B[j] + size[j]; oldT = T;}

Para testar esta classe insira o seguinte método principal:

public static void main(String args[]) { MStack stack = new MStack(5, 500); System.out.println(stack.n); for (int i=0; i<stack.m; i++){ System.out.println("B: " + stack.B[i] +

Estruturas de Dados 22

Page 37: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

" T: " + stack.T[i] + " oldT: " + stack.oldT[i]); } stack.push(1, "a"); for (int i=0; i<stack.m; i++){ System.out.println("B: " + stack.B[i] + " T: " + stack.T[i] + " oldT: " + stack.oldT[i]); } }

Estruturas de Dados 23

Page 38: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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

2. Converta as expressões seguintes para a forma postfix:

a) a+b/c*d*(e+f)-g/hb) (a-b)*c/d^e*f^(g+h)-ic) 4^(2+1)/5*6-(3+7/4)*8-2d) (m+n)/o*p^q^r*(s/t+u)-v

3. Estratégias de realocação para overflow de stack para os números 3 e 4:

a) Desenhe um diagrama mostrando o estado atual da stack.b) Desenhe um diagrama mostrando o estado da stack depois da implementação do unit-shift policy.c) Desenhe um diagrama mostrando o estado da stack depois de usar o algoritmo Garwick's mostrando como as novas bases foram computadas.

4. Cinco stack coexistindo em um array de 500 posições. Uma inserção é tentada na stack 2. O estado de computação está definido por:

OLDT(0:4) = (89, 178, 249, 365, 425)T(0:4) = (80, 220, 285, 334, 433)B(0:5) = (-1, 99, 220, 315, 410, 499)

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)

7.1. Exercícios para Programar

1. Escreva uma classe de Java que verifica se os parênteses e chaves estão equilibrados em uma expressão aritmética.

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

Page 39: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

Módulo 3Estruturas de Dados

Lição 3Queue

Versão 1.0 - Mai/2007

Page 40: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

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.

Ao final desta lição, o estudante será capaz de:

• Definir os conceitos básicos e operações com queue ADT• Implementar uma queue ADT usando representação seqüencial e encadeada• Realizar operações em queues circulares• Usar a ordenação topológica para produzir uma organização dos elementos que satisfaça a

um padrão estabelecido

Estruturas de Dados 4

Page 41: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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:

class QueueException extends RuntimeException{ public QueueException(String err){ super(err); }}

Como a stack, a queue deve ser implementada utilizando a representação seqüencial ou encadeada.

2.1. Representação Seqüencial

Se a execução utiliza uma representação seqüencial, um array unidimensional é usado, portanto o tamanho é estático. Se a queue tem dados, front aponta para seu primeiro elemento, enquanto que rear aponta para a célula seguinte à última ocupada. A queue está vazia quando front é igual a rear e cheia quando front é igual a zero e rear é igual a n. Tentar remover um item de uma queue vazia causa um underflow, enquanto que tentar inserir em uma queue cheia causa um overflow. A figura a seguir mostra um exemplo de queue:

Figura 1: Operações em uma fila

Para iniciar, igualamos front e rear a 0:

front = 0;rear = 0;

Para inserir um item, digamos x, fazemos o seguinte:

Estruturas de Dados 5

Page 42: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Q [rear] = item;Rear ++;

e para remover um item, fazemos o seguinte:

x = Q[front];front ++;

Para implementar uma queue utilizando a representação seqüencial:

class SequentialQueue implements Queue{ Object Q[]; int n = 100 ; // tamando da fila , padrão 100 int front = 0; // inicio e fim iniciar como 0 int rear = 0;

// Cria uma queue com tamanho definido de 100 elementos public SequentialQueue() { Q = new Object[n]; }

// Cria uma queue com tamanho a definir public SequentialQueue(int size) { n = size; Q = new Object[n]; }

// Insere um item na queue public void enqueue(Object item) throws QueueException { if (rear == n) throw new QueueException("Inserting into a full queue."); Q[rear] = item; rear++; }

// Remove um item da queue public Object dequeue() throws QueueException { if (front == rear) throw new QueueException("Deleting from an empty queue."); Object x = Q[front]; front++; return x; }}

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:

public void moveQueue() throws QueueException { if (front==0) throw new QueueException("Inserting into a full queue"); for (int i=front; i<n; i++) Q[i-front] = Q[i]; rear = rear - front;

Estruturas de Dados 6

Page 43: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

front = 0;}

É preciso modificar a execução do método enqueue para se utilizar do método moveQueue:

public void enqueue(Object item) { // se rear está no fim do array if (rear == n) moveQueue(); Q[rear] = item; rear++;}

Podemos testar esta representação com a seguinte classe:

public class TestQueue { public static void main(String [] args) { SequentialQueue queue = new SequentialQueue(4); queue.enqueue("1"); queue.enqueue("2"); queue.enqueue("3"); queue.enqueue("4"); System.out.println(queue.dequeue()); System.out.println(queue.dequeue()); System.out.println(queue.dequeue()); System.out.println(queue.dequeue()); } }

Como resultado, lembre-se que a queue armazena “Enfileirando” os dados, e retirá-os do primeiro ao último elemento armazenado.

2.2. Representação Encadeada

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:

Figura 2: Representação encadeada de uma Queue

Estruturas de Dados 7

Page 44: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

As definições já vistas sobre node serão utilizadas aqui.

A queue está vazia se front é igual a null. Na representação encadeada, desde que a queue cresça dinamicamente, o overflow acontecerá somente quando a classe ficar sem espaço para novas inserções e tratar disso está fora do escopo desse tópico.

O seguinte classe executa a representação encadeada de uma queue ADT:

class LinkedQueue implements Queue { private Node front; private Node rear;

// Cria uma queue vazia public LinkedQueue() { } // Cria uma queue com n NODES inicialmente public LinkedQueue(Node n) { front = n; rear = n; } // Insere um item na queue public void enqueue(Object item) { Node n = new Node(item, null); if (front == null) { front = n; rear = n; } else { rear.link = n; rear = n; } } // Remove um item da queue public Object dequeue() throws QueueException { Object x; if (front == null) throw new QueueException("Deleting from an empty queue."); x = front.info; front = front.link; return x; }}

Podemos testar esta representação com a seguinte classe:

public class TestQueue { public static void main(String [] args) { LinkedQueue queue = new LinkedQueue(); queue.enqueue("1"); queue.enqueue("2"); queue.enqueue("3"); queue.enqueue("4"); System.out.println(queue.dequeue()); System.out.println(queue.dequeue()); System.out.println(queue.dequeue()); System.out.println(queue.dequeue()); } }

Estruturas de Dados 8

Page 45: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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:

Figura 3: Queue Circular

Para iniciar uma queue circular:

front = 0; rear = 0;

Para inserir um item, por exemplo, x:

Q[rear] = x;rear = (rear + 1) mod n;

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

Page 46: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Este é um exemplo completo para uma implementação de uma queue circular:

public class CircularQueue implements Queue{ Object Q[]; int n = 20; // tamanho da queue, por padrão 20 int front = 0; int rear = 0; // Cria a circular queue de tamanho padrão public CircularQueue(){ Q = new Object[n]; } // Cria a circular queue de tamanho n public CircularQueue(int size){ n = size; Q = new Object[n]; } public void enqueue(Object item) throws QueueException { if (front == (rear % n) + 1) throw new QueueException("Inserting into a full queue."); Q[rear] = item; rear = (rear % n) + 1; } public Object dequeue() throws QueueException { Object x; if (front == rear) throw new QueueException("Deleting from an empty queue."); x = Q[front]; front = (front % n) + 1; return x; } // Método principal para testar a queue public static void main(String args[]) { CircularQueue q = new CircularQueue(5); for (int i=1; i < 7; i++) { q.enqueue(new Integer(i)); System.out.println(i +" inserted"); System.out.println(q.dequeue() + " retrieved"); System.out.println("front:"+q.front+" rear:"+q.rear); } }}

Estruturas de Dados 10

Page 47: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

4. Aplicação: Classificação Topológica

A Classificação Topológica é um problema característico de redes ativas. Utiliza ambas as técnicas de alocação, seqüencial e encadeada, na qual a queue encadeada está inserida em um array seqüencial.

É um processo aplicado em elementos parcialmente ordenados. A entrada é um conjunto de pares de condicionamento parcial e a saída é a lista de elementos, onde não existe nenhum elemento cujo antecessor já não esteja na saída.

4.1. Ordenação Parcial

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

Propriedades equivalentes são:

• Transitividade: se x ≺ y e y ≺ z, então x ≺ z

• Simetria: se x ≺ y então y ≺ x

• Não-Reflexividade: x ≺ x

Um exemplo familiar de condição parcial na matemática é a relação u ⊆ v entre os conjuntos u e v. A seguir temos outro exemplo onde a lista de condição parcial é mostrada à esquerda; o gráfico que ilustra a condição parcial é mostrado no centro, e a saída esperada é mostrada à direita.

0,10,30,51,21,52,43,23,45,46,56,77,17,5

Exemplo de Classificação Topológica

Saída:0 6 3 7 1 2 5 4

4.2. Algoritmo

Juntamente com a execução do algoritmo, devemos considerar alguns componentes discutidos no capítulo 1- input (entrada), output (saída), e o algoritmo apropriado.

Estruturas de Dados 11

Page 48: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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.

Segue a definição de node:

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]++;

e um newNODE (nó) é adicionado à SUC(i):

Node newNode = new Node();newNode.info = j;newNode.link = SUC[i];SUC[i] = newNode;

Figura 4: Adicionando um newNODE

Estruturas de Dados 12

Page 49: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Segue exemplo:

Figura 5: Representação de um exemplo de Classificação Topológica

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:

COUNT [j] = k se k é o próximo item na queue = 0 se j for o último elemento na queue

Conseqüentemente temos uma embedded linked queue em um array seqüencial.

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

Page 50: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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

Page 51: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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)

5.1. Exercícios para Programar

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.

2. Escreva uma classe que execute o algoritmo de ordenação topológica.

3. Relação de matérias (escolares) utilizando Ordenação Topológica.

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.

Para simplificar, consideraremos apenas matérias semestrais.

Exemplo de arquivo de entrada Exemplo de arquivo de saída(1, CS 1)(2, CS 2)...(0, 0)(1, 2)(2, 3)...(0, 0)

Início da definição de ordem parcial.

Sem 1 Sem 2CS 1 CS 12

Sem 2 Sem 4CS 2 CS 135CS 3 CS 140

CS 150

Estruturas de Dados 15

Page 52: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

Módulo 3Estruturas de Dados

Lição 4Árvores Binárias

Versão 1.0 - Mai/2007

Page 53: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

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.

Ao final desta lição, o estudante será capaz de:

• Explicar os conceitos básicos e definições relacionadas a árvores binárias

• Identificar as propriedades de uma árvore binária

• Enumerar os diferentes tipos de árvores binárias

• Discutir como as árvores binárias são representadas na memória dos computadores

• Percorrer árvores binárias usando três algoritmos de varredura: pré-ordem, em ordem, pós-ordem

• Discutir aplicações da varredura em árvores binárias

• Usar heaps e o algoritmo heapsort para classificar um conjunto de elementos

Estruturas de Dados 4

Page 54: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

2. Definições e Conceitos Relacionados

Uma árvore binária T tem um node especial, chamado r, que é o node raiz. Cada node v de T, que é diferente de r, possui um node pai p. O node v é chamado de child (ou filho) do node p. Um node pode ter no mínimo zero (0) e no máximo dois (2) children que são classificados como child esquerdo ou child direito. As sub-árvores de T cuja raiz é v são consideradas filhas de v. É uma sub-árvore à esquerda se for o child esquerdo do node v ou uma sub-árvore à direita se estiver ligada ao child direito do node v. O grau de um node é o número de sub-árvores não-nulas deste node. Se um node tiver grau zero, ele é classificado como folha ou um node terminal.

Figura 1: Uma Árvore Binária

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:

Figura 2: Níveis de uma Árvore Binária

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

Page 55: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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.

Figura 3: Diferentes Tipos de Árvore Binária

2.1. Propriedades de uma Árvore Binária

Para uma árvore binária (equilibrada ou balanceada) de profundidade k,

• O número máximo de nodes no nível i é 2i , i ≥ 0.• O número de nodes é no mínimo 2k + 1 e no máximo 2k+1 – 1.• O número de nodes externos é no mínimo h+1 e no máximo 2k.• O número de nodes internos é no mínimo h e no máximo 2k – 1.• Se no é o número de nodes folhas e n2 é o número de nodes de grau 2 numa árvore binária,

então no = n2 + 1.

2.2. Tipos de Árvores Binárias

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

Page 56: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 4: Árvores Binárias Degeneradas à Esquerda e à Direita

Uma árvore estritamente binária é uma árvore em que todos os nodes têm duas sub-árvores ou nenhuma sub-árvore.

Figura 5: Árvore Estritamente Binária

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.

Figura 6: Árvore Binária Cheia

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.

Figura 7: Árvore Binária Completa

Estruturas de Dados 7

Page 57: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

3. Representação das Árvores Binárias

A maneira mais ‘natural’ para se representar uma árvore binária na memória do computador é utilizando a representação por links. A seguinte figura mostra a estrutura do node de uma árvore binária utilizando esta representação:

Figura 8: Nodes de Árvores Binárias

A seguinte classe Java implementa a representação acima:

public class BTNode { private Object info; private BTNode left, right; public BTNode(Object info) { this.setInfo(info); } public BTNode(Object info, BTNode left, BTNode right) { this.setInfo(info); this.setLeft(left); this.setRight(right); } public void setLeft(BTNode left) { this.left = left; } public BTNode getLeft() { return left; } public void setRight(BTNode right) { this.right = right; } public BTNode getRight() { return right; } public Object getInfo() { return info; } public void setInfo(Object info) { this.info = info; }}

O exemplo abaixo mostra a representação com links de uma árvore binária:

Estruturas de Dados 8

Page 58: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 9: Representação com Links de uma Árvore Binária

Em Java, a seguinte classe define uma árvore binária:

public class BinaryTree { private BTNode root; public BinaryTree(BTNode node) { this.root = node; } public BinaryTree(BTNode node, BTNode left, BTNode right) { this.root = node; this.root.setLeft(left); this.root.setRight(right); } }

Estruturas de Dados 9

Page 59: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

4. Percorrendo Árvores Binárias

Pesquisas em árvores binárias geralmente envolvem uma busca ou varredura. Busca é um procedimento que percorre os nodes de uma árvore binária de maneira linear de modo que cada node é visitado apenas uma única vez. Visitar pode ser definido como a realização de computações locais no node.

Há três maneiras de se percorrer uma árvore: pré-ordem, em ordem e pós-ordem. Os prefixos (pré, em e pós) referem-se à ordem em que a raiz de cada sub-árvore é visitada.

4.1. Busca Pré-Ordem

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.

Figura 10: Percorrimento Pré-ordem

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 árvorepublic void preorder() { if (root != null) { System.out.println(root.getInfo().toString()); new BinaryTree(root.getLeft()).preorder(); new BinaryTree(root.getRight()).preorder(); }}

4.2. Busca em Ordem

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

Page 60: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 11: Percorrimento em Ordem

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 árvorepublic void inorder() { if (root != null) { new BinaryTree(root.getLeft()).inorder(); System.out.println(root.getInfo().toString()); new BinaryTree(root.getRight()).inorder(); }}

4.3. Busca Pós-ordem

A busca pós-ordem é o contrário da pré-ordem, isto é, recursivamente percorrem-se primeiro os children e depois os pais.

Figura 12: Percorrimento Pós-ordem

Método:

Se a árvore binária estiver vazia, não faça nada (percorrimento finalizado).Caso contrário:

Estruturas de Dados 11

Page 61: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Percorra a sub-árvore esquerda em pós-ordem.Percorra a sub-árvore direita em pós-ordem.Visite a raiz.

Em Java, adicione o seguinte método à classe BinaryTree:

// Listagem pós-ordem dos elementos da árvorepublic void postorder() { if (root != null) { new BinaryTree(root.getLeft()).postorder(); new BinaryTree(root.getRight()).postorder(); System.out.println(root.getInfo().toString()); }}

A seguir temos alguns exemplos:

Figura 13: Exemplo 1

Figura 14: Exemplo 2

Estruturas de Dados 12

Page 62: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

5. Aplicações de Busca em Árvores Binárias

A busca em árvores binárias tem várias aplicações. Nesta seção, duas aplicações serão abordadas: a duplicação de uma árvore binária e a verificação da equivalência entre duas árvores binárias.

5.1. Duplicando uma Árvore Binária

Existem momentos em que a duplicação de uma árvore binária é necessária para um processamento. Para isso, pode ser utilizado o seguinte algoritmo para duplicar uma árvore binária existente:

1. Percorra a sub-árvore esquerda do node α em pós-ordem e faça uma cópia dela 2. Percorra a sub-árvore da direita do node α em pós-ordem e faça uma cópia dela3. Faça uma cópia do node α e anexe as cópias de suas sub-árvores da esquerda e da direita

O seguinte método da classe BinaryTree implementa este algoritmo:

public BTNode copy() { BTNode newRoot; BTNode newLeft; BTNode newRight; if (root != null) { newLeft = new BinaryTree(root.getLeft()).copy(); newRight = new BinaryTree(root.getRight()).copy(); newRoot = new BTNode(root.getInfo(), newLeft, newRight); return newRoot; } return null;}

5.2. Equivalência entre Duas Árvores Binárias

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 dados2. Percorra as sub-árvores da esquerda, nodes α e β em pré-ordem e verifique se são equivalentes3. Percorra as sub-árvores da direita, nodes α e β em pré-ordem e verifique se são equivalentes

O seguinte método da classe BinaryTree implementa este algoritmo:

public boolean equivalent(BinaryTree t2) { boolean answer = false; if ((root == null) && (t2.root == null)) answer = true; else { answer = (root.getInfo().equals(t2.root.getInfo())); if (answer) answer = new BinaryTree(root.getLeft()).equivalent( new BinaryTree(t2.root.getLeft())); if (answer) answer = new BinaryTree(root.getRight()).equivalent( new BinaryTree(t2.root.getRight())); } return answer;}

É possível testar esta classe implementando o seguinte método main:

Estruturas de Dados 13

Page 63: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

public static void main(String args[]) { BinaryTree bt1 = new BinaryTree( new BTNode("Root1"), new BTNode("Left1"), new BTNode("Right1")); BinaryTree bt2 = new BinaryTree( new BTNode("Root2"), new BTNode("Left2"), new BTNode("Right2")); BinaryTree bt3 = new BinaryTree( new BTNode("Root1"), new BTNode("Left1"), new BTNode("Right1")); System.out.println("Preorder(bt1): "); bt1.preorder(); System.out.println("Inorder(bt1): "); bt1.inorder(); System.out.println("Postorder(bt1): "); bt1.postorder();

BinaryTree bt4 = new BinaryTree(bt1.copy()); System.out.println("Preorder (bt4): "); bt4.preorder();

System.out.println(bt1.equivalent(bt2)); System.out.println(bt1.equivalent(bt3));}

E na execução desta classe, como resultado teremos:

Preorder(bt1): Root1Left1Right1Inorder(bt1): Left1Root1Right1Postorder(bt1): Left1Right1Root1Preorder (bt4): Root1Left1Right1falsetrue

Estruturas de Dados 14

Page 64: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

6. Aplicação de Árvore Binária: Heaps e o Algoritmo Heapsort

Um heap é definido como uma árvore binária completa que tem elementos armazenados em seus nodes e satisfaz a propriedade ordem-heap. Uma árvore binária completa, como definida na lição anterior, resulta quando zero ou mais nodes são excluídos de uma árvore binária completa em ordem inversa de nível. Conseqüentemente, suas folhas ficam no máximo em dois níveis adjacentes e as folhas do nível mais baixo ficam na posição mais à esquerda da árvore binária completa.

A propriedade ordem-heap define que para todo node u exceto a raiz, a chave armazenada em u é menor ou igual à chave armazenada em seu respectivo node pai. Então, a raiz sempre contém o valor máximo.

Nesta aplicação, os elementos armazenados em um heap satisfazem a ordem total. Uma ordem total é uma relação entre os elementos de um conjunto de objetos, nomeado S, que satisfazem as propriedades de quaisquer objetos x, y e z em S:

• Transitividade: se x < y e y < z então x < z.

• Tricotomia: para quaisquer dois objetos x e y em S, exatamente uma destas relações é verdadeira: x > y, x = y ou x < y.

Figura 15: Duas representações dos elementos a l g o r i t h m s

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

Page 65: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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; }}

6.2. Representação Seqüencial de uma Árvore Binária Completa

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,

Figura 16: Uma árvore binária completa

pode então ser representada seqüencialmente por meio de um vetor de nome CHAVE, conforme demonstrado abaixo:

Figura 17: Representação Seqüencial de uma Árvore Binária Completa

Estruturas de Dados 16

Page 66: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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

O método abaixo, inserido na classe SequentialHeap, implementa o processo de shift-up em uma árvore binária completa representada seqüencialmente:

// Converte uma árvore binária com n nodes e raiz em uma stackprivate void shiftUp (int i, int n) { int k = key[i]; // mantém a chave na raiz da stack int child = 2 * i; // child à esquerda

while (child <= n) { // se child à direita é maior, aponta-o para o da direita

if (child < n && key[child+1]> key[child]) child++ ; // se a propriedade de stack não for satisfeita

if (key[child] > k) { key[i] = key[child] ; // Move child para cima

i = child;child = 2 * i; //Considera child da esquerda novamente

} else break;

} key[i] = k ; // aqui começa a raiz}

Para converter uma árvore binária em uma quase-stack:

// Converte chave em quase-stackfor (int i=n/2; i>1; i--){ // o primeiro node que tem children é n/2 shiftUp(i, n);}

6.3. O Algoritmo Heapsort

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 completa2. Converta esta árvore binária em um heap aplicando o método shift-up aos seus nodes em

ordem reversa de nível3. 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

O método a seguir, inserido na classe SequentialHeap, implementa o algoritmo heapsort em Java:

Estruturas de Dados 17

Page 67: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

public void sort() { int n = key.length-1; // converte a chave para almost-heap for (int i=n/2; i>1; i--) { // primeiro node como child é n/2 shiftUp(i, n); }

// Muda o corrente tamanho da chave[1] com a chave[i] for (int i=n; i>1; i--) { shiftUp(1, i); int temp = key[i]; key[i] = key[1]; key[1] = temp; }}

O exemplo seguinte mostra a execução do heapSort com as chaves de entrada:

a l g o r i t h m s

Estruturas de Dados 18

Page 68: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Estruturas de Dados 19

Page 69: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Estruturas de Dados 20

Page 70: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Estruturas de Dados 21

Page 71: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Estruturas de Dados 22

Page 72: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 18: Exemplo de Heapsort

Poderíamos testar esta classe com a implementação do seguinte método principal:

public static void main(String args[]){ int keys[] = {'a','l','g','o','r','i','t','h','m','s'}; SequentialHeap heap = new SequentialHeap(keys); System.out.print("Before sorting: "); for (int i=0; i < keys.length;i++) System.out.print((char) keys[i] + " "); System.out.println(); heap.sort(); System.out.print("Before sorting: "); for (int i=0; i < keys.length;i++) System.out.print((char) keys[i] + " "); System.out.println();}

E iremos obter o seguinte resultado:

Before sorting: a l g o r i t h m s Before sorting: a g h i l m o r s t

Estruturas de Dados 23

Page 73: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

7. Exercícios

1. Varredura. Percorra a seguinte árvore em pré-ordem, em ordem e pós-ordem.

a) b)

c)

2. Heapsort. Organize os seguintes elementos em ordem crescente. Mostre o estado da árvore em cada passo.

a) C G A H F E D J B Ib) 1 6 3 4 9 7 5 8 2 12 10 14

7.1. Exercícios para Programar

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

Page 74: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Operação Descrição

^ Exponenciação (maior precedência)

* / Multiplicação e Divisão

+ - Adição e Subtração

Na sua implementação, considere a precedência e prioridade dos operadores.

2. Modifique o algoritmo heapsort para retornar as chaves em ordem decrescente, ao invés de crescente, deslocando para cima as chaves menores ao invés das chaves maiores.

Estruturas de Dados 25

Page 75: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

Módulo 3Estruturas de Dados

Lição 5Árvores

Versão 1.0 - Mai/2007

Page 76: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

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

Page 77: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

2. Definições e Conceitos Relacionados

2.1. Árvores Ordenadas

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.

Figura 1: Uma árvore ordenada

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.

2.2. Árvore Orientada

Uma árvore orientada é uma árvore em que a ordem de cada subárvore de cada node da árvore é secundário.

Figura 2: Uma Árvore Orientada

No exemplo acima, as duas árvores são duas árvores orientadas diferentes, mas são a mesma árvore.

Estruturas de Dados 5

Page 78: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

2.3. Árvore Livre

Uma árvore livre não tem um node designado como raiz e a orientação de um node para outro é sem importância.

Figura 3: Uma Árvore Livre

2.4. Progressão de Árvores

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.

Figura 4: Progressão de Árvores

Estruturas de Dados 6

Page 79: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

3. Representação Ligada de Árvores

Alocação ligada pode ser usada para representar árvores. A figura abaixo mostra a estrutura dos nodes usados nessa representação.

Figura 5: Estrutura de node de uma Árvore

Na estrutura de nodes, k é o grau da árvore. SON1, 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.

Figura 6: Node de Árvore Binária

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

Page 80: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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.

4.1. Correspondência Natural: Árvore Binária, representação de Floresta

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:

a) Se n = 0, então B(F) é vazia.

b) Se n > 0, então a raiz de B(F) é a raiz de T1; a subárvore esquerda de B(F) é B(T11, T12, ... T1m), onde T11, T12, ... T1m são subárvores da raiz de T1; e a subárvore direita de B(F) é B(T2, T3, ..., Tn).

A correspondência natural pode ser implementada usando uma abordagem não-recursiva:

1. Ligar os filhos de cada família da esquerda para direita. (Nota: as raízes da árvore na floresta são irmãs, filhas de um pai desconhecido.)

2. Remover ligações de um pai para todos os seus filhos exceto o filho mais velho (ou mais à esquerda).

3. Inclinar a figura resultante em 45 graus.

O exemplo a seguir ilustra a transformação de uma floresta em sua árvore binária equivalente:

Estruturas de Dados 8

Page 81: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 8: Exemplo de Correspondência Natural

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.

4.2. Atravessando a Floresta

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

Page 82: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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 NFloresta 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 : A B C D E F G H I K J L M NB(F) em-ordem : C D E F B A H K I L J G N MB(F) pós-ordem : F E D C B K L J I H N M G A

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

Page 83: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

4.3. Representação Seqüencial de Florestas

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.

4.3.1. Representação Seqüencial em Pré-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.

Figura 10: Representação seqüencial pré-ordem da floresta F

Figura 11: Representação interna real

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:

E isto é representado internamente como:

Estruturas de Dados 11

Page 84: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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.

4.3.2. Representação Seqüencial de Percurso por Família (Family-Order)

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.

Figura 12: Representação seqüencial família-ordem da floresta F

Figura 13: Representação interna real

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

Page 85: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 14: Representação interna real

4.3.3. Representação Seqüencial de Percurso por Nível (Level-Order)

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:

Figura 15: Representação seqüencial de percurso por nível

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:

4.3.4. Convertendo Representação Seqüencial para Representação por Link

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:

public class SeqForest{ int RTAG[]; int INFO[]; int LTAG[]; int n = 0;

Estruturas de Dados 13

Page 86: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

public SeqForest(int size) { n = size; RTAG = new int[n]; INFO = new int[n]; LTAG = new int[n]; } public SeqForest(int[] rtag, int[] info, int[] ltag) { n = rtag.length; RTAG = rtag; INFO = info; LTAG = rtag; } public BinaryTree convert() { BTNode alpha = new BTNode(null); BinaryTree t = new BinaryTree(alpha); LinkedStack stack = new LinkedStack(); BTNode sigma; BTNode beta; // Gera o resto de uma árvore binária for (int i=0; i<n-1; i++) { // alpha.setInfo(INFO[i]); beta = new BTNode(null); if (RTAG[i] == 1) stack.push(alpha); else alpha.setRight(null); 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

Page 87: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

5. Representações de Árvores Aritméticas

As árvores podem ser representadas seqüencialmente utilizando uma representação aritmética. A árvore pode ser armazenada seqüencialmente baseada em sua pré-ordem, pós-ordem e ordem de níveis de seus nodes. O grau ou o peso da árvore pode ser armazenado com sua informação. O grau, como definido anteriormente, se refere ao número de filhos que um node possui, enquanto que o peso se refere ao número de descendentes de um node.

Figura 5.1 árvore T ordenada

A seguir são representadas as diversas maneiras de se representar a árvore acima.

Sequência de Pré-ordem com Grau

INFO 1 2 5 11 6 12 13 7 3 8 4 9 14 15 16 10GRAU 3 3 1 0 2 0 0 0 1 0 2 1 2 0 0 0

Sequência de Pré-ordem com Peso

INFO 1 2 5 11 6 12 13 7 3 8 4 9 14 15 16 10PESO 15 6 1 0 2 0 0 0 1 0 5 3 2 0 0 0

Sequência de Pós-ordem com Grau

INFO 11 5 12 13 6 7 2 8 3 15 16 14 9 10 4 1GRAU 0 1 0 0 2 0 3 0 1 0 0 2 1 0 2 3

Sequência de Pós-ordem com Peso

INFO 11 5 12 13 6 7 2 8 3 15 16 14 9 10 4 1PESO 0 1 0 0 2 0 6 0 1 0 0 2 3 0 5 15

Sequência de ordem de níveis com Grau

INFO 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16GRAU 3 3 1 2 1 2 0 0 1 0 0 0 0 2 0 0

Sequência de ordem de níveis com Peso

INFO 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16PESO 15 6 1 5 1 2 0 0 3 0 0 0 0 2 0 0

Estruturas de Dados 15

Page 88: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

5.1. Aplicação: Árvores e o problema da Equivalência

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:

(a) Transitividade: se x ≡ y e y ≡ z então x ≡ z (b) Simetria: se x ≡ y então y ≡ x (c) Reflexitividade: x ≡ x

Exemplos de relações de equivalência são as relações “é igual a” (= ) e a “similaridade” entre as árvores binárias.

5.1.1. O problema da Equivalência

Dados quaisquer pares de relações de equivalência na forma de i ≡ j para qualquer i, j em S, determine se K é equivalente a i, para qualquer K, i pertence a S, com base nos pares dados. Para solucionar o problema utilizaremos o seguinte teorema:

Uma relação de equivalência particiona seu conjunto S em classes disjuntas, chamadas classes de equivalência, tal que dois elementos são equivalentes se e somente se eles pertencerem a mesma classe de equivalência.

Por exemplo, considere o conjunto S = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}. Suponha que as relações de equivalência definidas no conjunto são: 1 ≡ 10, 9 ≡ 12, 9 ≡ 3, 6 ≡ 10, 10 ≡ 12, 2 ≡ 5, 7 ≡ 8, 4 ≡ 11, 2 ≡ 13 e 1 ≡ 9. Agora, a pergunta, 10 ≡ 2 é verdadeiro? 6 ≡ 12 é verdadeiro? Para responder a essas perguntas precisamos criar as classes de equivalência.

Entrada Classes de equivalência Modificações

1 ≡ 10 C1 = {1, 10} Criar uma nova classe(C1) que contenha 1 e 10

9 ≡ 12 C2 = {9, 12} Criar uma nova classe(C2) que contenha 9 e 12

9 ≡ 3 C2 = {9, 12, 3} Adicionar 3 a C2

6 ≡ 10 C1 = {1, 10, 6} Adicionar 6 a C1

10 ≡ 12 C2 = {1, 10, 6, 9, 12, 3} Juntar C1 e C2 dentro de C2, descartar C1

2 ≡ 5 C3 = {2, 5} Criar uma nova classe(C3) que contenha 2 e 5

7 ≡ 8 C4 = {7, 8} Criar uma nova classe(C4) que contenha 7 e 8

4 ≡ 11 C5 = {4, 11} Criar uma nova classe(C5) que contenha 4 e 11

6 ≡ 13 C2 = {1, 10, 6, 9, 12, 3, 13} Adicionar 13 a C2

1 ≡ 9 Sem mudanças

Desde que 13 não tenha equivalências, as classes finais são:

C2 = {1, 10, 6, 9, 12, 3, 13}C3 = {2, 5}C4 = {7, 8}C5 = {4, 11}

Estruturas de Dados 16

Page 89: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

E 10 ≡ 2? Já que se encontram em classes diferentes, não são equivalentes.

E 6 ≡ 12? Já que se ambos pertencem a C2, são equivalentes.

5.1.2. Implementação no Computador

Para implementar a solução para o problema da equivalência, é preciso um modo de representar as classes de equivalência. Precisa-se também de um modo de unir as classes de equivalência(a operação union) e determinar se 2 objetos pertencem a uma mesma classe de equivalência ou não(a operação find).

Para solucionar a primeira preocupação, as árvores podem ser usadas para representar as classes de equivalência, i.e., uma árvore representa a classe. Neste caso, configurando uma árvore, chamemos t1, como uma sub-árvore de uma outra árvore, chamemos t2, pode-se implementar a união de classes equivalentes. Também é possível informar se dois objetos pertencem a uma mesma classe ou não respondendo a uma simples questão, “os objetos possuem a mesma origem na árvore? “

Considere o seguinte:

Figura 16: União de classes Equivalentes

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;

Os exemplos seguintes mostram a ilustração gráfica do problema de equivalência descrito:

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

Page 90: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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

Page 91: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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

Figura 5.4 Exemplo de equivalência

A seguir a implementação do algoritmo para resolver o problema da equivalência:

class Equivalence { int[] FATHER; int n;

public Equivalence() { }

public Equivalence(int size) { n = size+1; // +1 desde FATHER[0] que não será usado FATHER = new int[n]; } public void setSize(int size) { FATHER = new int[size+1]; } // Gera a equivalência de classes baseada na equivalência dos pares j,k public void setEquivalence(int a[], int b[]) { int j, k; for (int i=0; i<a.length; i++) { // Obtêm a equivalência do par j,k

j = a[i];k = b[i];

// Pega a raiz de j e kwhile (FATHER[j] > 0)

j = FATHER[j];while (FATHER[k] > 0)

k = FATHER[k];

// Se não é equivalente, combina as duas árvoresif (j != k)

FATHER[j] = k; }

Estruturas de Dados 19

Page 92: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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];

// Se possuírem a mesma raiz, são equivalentes return (j == k); } public static void main(String args[]){ int[] j = {1, 9, 9, 6, 10, 2, 7, 4}; int[] k = {10, 12, 3, 10, 12, 5, 8, 11}; int n = 13; Equivalence eq = new Equivalence(n); eq.setEquivalence(j, k); System.out.println(eq.test(10, 2)); System.out.println(eq.test(6, 12)); } }

5.1.3. Degeneração e o Enfraquecimento da Regra para União

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:

Figura 17: pior caso Floresta (Árvore) em um Problema Equivalente.

Agora, considere a seguinte árvore:

Estruturas de Dados 20

Page 93: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 18: Melhor-caso Floresta (Árvore) em um Problema Equivalente

Em termos de classificações equivalentes, as duas árvores simbolizam a mesma classe. Contudo a segunda árvore possui apenas um ramo a ser atravessado de qualquer node à raiz enquanto que na primeira árvore possui n-1 ramos.

Exemplificaremos com a seguinte classe:

public class Equivalence2 { private int[] FATHER; private int n; 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ãopublic 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

Page 94: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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(n2). Fora isso, se aplicado, o tempo de tempo de complexidade é O(n log2n).

5.1.4. Árvores Pior Caso

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:

Figura 19: Árvores pior caso

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

Page 95: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 20: Desmontar por Ordem de Pesquisa 1

Em um relacionamento i ≡ j para qualquer j, requer pelo menos m passos, i.e., executar o i = PAI(i), para receber a raiz.

Na desmontar por ordem de pesquisa, descobrir n1, n2, ..., nm quais nodes estão no caminho entre o node n1 ao node raiz r. para desmontar, faremos r o pai de np, 1 ≤ p < m:

Figura 21: Desmontar por Ordem de Pesquisa 2

O seguinte método adicionado na classe Sequence2, implementa este processo:

// Implementa Desmontar por Ordem de Pesquisa. Retorna a raiz de ipublic int find(int i) { int j, k, l;

k = i; // Procura raiz

while (FATHER[k] > 0) k = FATHER[k];

// Resumir caminho do node i j = i; while (j != k){

Estruturas de Dados 23

Page 96: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

l = FATHER[j]; FATHER[j] = k; j = l; } return k;}

A operação find é proporcional ao tamanho do caminho do node i para a raiz.

5.1.5. Solução final para o Problema Equivalente

Os métodos a seguir implementados na classe Sequence2, finalizam a solução para o problema equivalente:

// Gerar equivalentes classes baseada na equivalência do par j,kpublic 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++) { // Retorna a equivalência entre o par j,k j = a[i];

k = b[i];

// Retorna as raizes de j e k j = find(j); k = find(k);

// Se não são equivalentes, junte as duas árvores if (j != k)

union(j, k); }}

/* Aceitar dois elementos j e k. Retorna verdadeiro se forem equivalentes, senão retorna falso*/public boolean test(int j, int k) {

// retorna raízes para j e k j = find(j);

k = find(k);

// Se possuírem a mesma raiz, são equivalentes return (j == k); }

A seguir é mostrado o estado de classes equivalentes após esta solução final de equivalência o problema é resolvido:

Entrada Floresta PAI

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

Estruturas de Dados 24

Page 97: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Entrada Floresta PAI

9 ≡ 12

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

10 ≡ 12, count(10) = count(12)

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

Page 98: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Entrada Floresta PAI

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

Podemos utilizar o seguinte método principal para testar esta classe:

public static void main(String args[]){ int[] j = {1, 9, 9, 6, 10, 2, 7, 4, 6, 1}; int[] k = {10, 12, 3, 10, 12, 5, 8, 11, 13, 9}; int n = 13; Equivalence2 eq = new Equivalence2(n); eq.setEquivalence(j, k); System.out.println(eq.test(10, 2)); System.out.println(eq.test(1, 3)); System.out.println(eq.test(6, 12)); for (int i=0; i<n;i++) System.out.println(eq.FATHER[i]); }

Estruturas de Dados 26

Page 99: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

6. Exercícios

1. Para cada uma das florestas abaixo,

FLORESTA 1

FLORESTA 2

FLORESTA 3

a) Converta para a árvore binária correspondente.b) Dê a representação seqüencial em pré-ordem com pesos.c) Dê a representação seqüencial em ordem de família com graus.d) Usando representação seqüencial em pré-ordem, mostre a disposição interna usada para

armazenar a floresta (com seqüências LTAG e RTAG).

2. Mostre a representação da floresta abaixo usando a representação seqüencial em pré-ordem com pesos:

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

Page 100: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

a) Dado o conjunto S = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} e os pares de equivalência: (1,4), (5,8), (1,9), (5,6), (4,10), (6,9), (3,7) e (3,10), construir a classe de equivalência usando florestas. 7≡6? 9≡10?

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.

6.1. Exercícios para Programar

1. Criar a definição da classe de Java de representações seqüenciais em ordem de nível de florestas. Criar também um método que converta a representação seqüencial em ordem de nível em sua representação de ponteiros.

Estruturas de Dados 28

Page 101: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

Módulo 3Estruturas de Dados

Lição 6Grafos

Versão 1.0 - Mai/2007

Page 102: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

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

Page 103: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

2. Definições e Conceitos Relacionados

Um grafo, G = (V, E), consiste de um conjunto finito não-vazio de vértices (ou nodes), V, e um conjunto de arestas, E. São exemplos de grafos:

Figura 1: Exemplos de Grafos

Um grafo não-dirigido é um grafo no qual o par de vértices representando uma aresta é desordenado. Por exemplo, (i,j ) e (j,i) representa o mesmo vértice.

Figura 2: Grafo não-dirigidos

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.

Figura 3: Aresta (i, 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

Page 104: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 4: Grafo não-dirigido completo

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.

Figura 5: Grafo dirigido

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.

Figura 6: Grafo dirigido completo

Um subgrafo de um grafo não-dirigido (dirigido) G = (V,E) é um grafo não-dirigido (dirigido) G’ =

(V’,E’) no qual V ⊆ V e E’ ⊆ E.

Figura 7: Exemplo de subgrafo

Estruturas de Dados 6

Page 105: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Um caminho do vértice u para o vértice w em um grafo não-dirigido [dirigido] G = (V,E) é uma seqüência de vértices vo, v1, v2, ..., vm-1, vm onde vo ≡ u e vm ≡ w, no qual (v0,v1),(v1,v2),..., (vm-1, vm) [ <v0,v1>, <v1,v2>, ..., <vm-1, vm>] são arestas em E.

O comprimento de um caminho refere-se ao número de arestas contidas nele.

Um caminho simples é um caminho no qual todos os vértices são distintos, exceto, possivelmente, o primeiro e o último.

Um caminho simples é um ciclo simples se ele tiver o mesmo vértice de começo e fim.

Dois vértices i e j são conectados se existir um caminho do vértice i para o vértice j. Se para cada par de vértices distintos i e j existir um caminho direto de e para ambos os vértices, isso é conhecido como sendo fortemente conectado. Um subgrafo conectado máximo de um grafo não-dirigido é conhecido como componente conectado em um grafo não-dirigido. Em um grafo dirigido G, o componente fortemente conectado refere-se ao componente fortemente conectado em G.

Um grafo ponderado é um grafo com pesos e custos designados para suas arestas.

Figura 8: Grafo Ponderado

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.

Figura 9: Árvore Geradora

Estruturas de Dados 7

Page 106: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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

3.1. Matriz de Adjacência para grafos dirigidos

Um grafo dirigido pode ser representado usando uma matriz bidimensional, digamos A, com dimensões n x n, onde n é o número de vértices. Os elementos de A são definidos como:

A(i,j) =1 se a aresta <i,j> existir, 1 ≤ i, j ≤ n = 0 se a aresta <i,j> não existir

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

Figura 10: Representação da Matriz de Custo Adjacência para Grafos Dirigidos

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

3.2. Lista de Adjacência para Grafos Dirigidos

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

Page 107: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

LISTA INFO CUSTO PRÓX

1 4 9 2 1 Λ2 5 10 4 5 3 2 Λ3 5 3 Λ4 5 8 3 4 Λ5 1 6 Λ

Figura 11: Representação dos custos em Lista de Adjacência para Grafos Direcionados

3.3. Matriz de adjacência para Grafos Não Direcionados

Assim como o diagrama, a matriz de adjacência pode ser usada para representar Grafos Não Direcionados. Entretanto, eles diferem no sentido que são simétricos para Grafos Não Direcionados, por exemplo, A(i, j) = A(j, i). Para representar o Grafo são necessários elementos na menor ou maior diagonal. A outra parte pode ser considerada como ”Não se preocupe” (*).

Para um Grafo Não Direcionado G com n vértices, o número de elementos não-zero é A ≤ n(n-1)/2. O limite superior é alcançado quando G é completado.

Por exemplo, o seguinte Grafo Não Direcionado não é ponderado. Por essa razão, a matriz de adjacência tem como entrada A(i,j) = 1 se a aresta existir, senão a entrada é 0.

1 2 3 4 5

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

3.4. Lista 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:

LISTA INFO PRÓX

1 5 4 Λ2 5 3 Λ3 4 2 Λ4 3 1 Λ5 2 1 Λ

Figura 13: Representação dos custos em Lista de Adjacência para Grafos Não Direcionados

Estruturas de Dados 9

Page 108: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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.

4.1. Busca em Profundidade

Na busca em profundidade (depth first search - DFS), o grafo é percorrido da forma mais profunda possível. Dado um grafo com vértices marcados como não visitados, o percurso é executado conforme apresentado a seguir:

1. Selecione um vértice não visitado para iniciar. Se nenhum vértice for encontrado, a busca em profundidade termina

2. Marque o vértice inicial como visitado

3. Processar o vértice adjacente:

a) Selecione um vértice não visitado, por exemplo u, adjacente do vértice inicialb) Marque o vértice adjacente como visitadoc) Inicie a busca em profundidade usando u como vértice inicial. Se nenhum vértice for

encontrado, vá para o passo (1)

4. Se mais vértices adjacentes forem encontrados, vá para o passo (3c)

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

Page 109: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 14: Exemplo de Busca em Profundidade

Estruturas de Dados 11

Page 110: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

4.2. Busca em Primeira Largura

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.

2. Repetir (1) até que todos os vértices no grafo sejam visitados.

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

Page 111: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 15: Exemplo de Busca em Largura

Estruturas de Dados 13

Page 112: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

5. Árvore Geradora de Custo Mínimo para Grafos Não Direcionados

A Árvore Geradora de Custo Mínimo (Minimum Cost Spanning Tree - MST), como definida anteriormente, é um subgrafo de um dado grafo G, no qual todos os vértices estão conectados e tem o custo mais baixo. Isso é particularmente útil para encontrar o caminho mais barato para conectar computadores em uma rede, bem como aplicações similares.

Encontrar árvore geradora de custo mínimo para um grafo não direcionado usando abordagem de força bruta não é aconselhável se o número de árvores geradoras para n vértices distintos seja nn-2. Por essa razão, é imperativo usar outra abordagem na busca da árvore geradora de custo mínimo e nessa lição, iremos cobrir algoritmos que utilizam uma abordagem gulosa. Nessa abordagem, uma seqüência de escolhas oportunistas terão êxito na busca pelo ótimo global. Para resolver o problema da árvore geradora de custo mínimo, usaremos os algoritmos de Prim e Kruskal, os quais são, ambos, algoritmos gulosos.

5.1. Teorema MST

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.

5.2. Algoritmo Prim

Esse algoritmo encontra a aresta de menor custo ligando algum vértice U para um vértice v em (V – U) para cada passo do algoritmo:

Considere G = (V,E) um grafo não-dirigido, ponderado e conexo. Considere que U indica o conjunto de vértices escolhidos e T indica o conjunto de arestas preparadas incluído em alguma instância do algoritmo.

1. Escolha um vértice inicial de V e coloque-o em U

2. Entre os vértices em V - U escolha aquele vértice, v, que é conexo a algum vértice, u, em U por uma aresta de menor custo. Adicione vértice v para U e a aresta (u, v) para T

3. Repita (2) até U = V, em que, T é uma árvore geradora de custo mínimo para G

Por exemplo,

Figura 16: Um grafo não-dirigido ponderado

Estruturas de Dados 14

Page 113: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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:

Figura 17: Resultado da aplicação do algoritmo Prim sobre o grafo anterior

5.3. Algoritmo Kruskal

Outro importante algoritmo usado para encontrar a MST foi desenvolvido por Kruskal. Nesse algoritmo, os vértices são listados em ordem crescente de peso. A primeira aresta a ser adicionada em T, que é o MST, é a de menor custo. Uma extremidade é considerada se pelo menos um do vértices não estiver na longe da árvore encontrada.

Agora o algoritmo:

Considere G = (V,E) um grafo não-dirigido, ponderado e conexo. A árvore geradora de custo mínimo, T, é construída aresta por aresta, com as arestas consideradas em ordem crescente de seus custos.

1. Escolha a aresta com o baixo custo como a aresta inicial.

2. A aresta de baixo custo entre as arestas restantes em E é considerada para inclusão em T. Se o ciclo for criado, a aresta em T é rejeitada.

Por exemplo,

Figura 18: Um grafo não-dirigido ponderado

A tabela a seguir mostra a execução do algoritmo Kruskal para resolver o problema MST do grafo acima:

Estruturas de Dados 15

Page 114: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Arestas MST U V-U Comentário

(c, e) – 1 (c, d) – 4(a, e) – 5(a, c) – 6(d, e) – 8(a, d) – 10(a, b) – 11(d, f) – 12(b, c) – 13(e, f) – 20

- a, b, c, d, e, f

Lista de arestas em ordem crescente de peso

(c, e) – 1 aceito(c, d) – 4(a, e) – 5(a, c) – 6(d, e) – 8(a, d) – 10(a, b) – 11(d, f) – 12(b, c) – 13(e, f) – 20

(c,e) – 1 c, e a, b, d, f

c e e não em U

(c, d) – 4 aceito(a, e) – 5(a, c) – 6(d, e) – 8(a, d) – 10(a, b) – 11(d, f) – 12(b, c) – 13(e, f) – 20

(c, e) – 1(c,d) – 4

c, d, e a, b, f d não em U

(a, e) – 5 aceito(a, c) – 6(d, e) – 8(a, d) – 10(a, b) – 11(d, f) – 12(b, c) – 13(e, f) – 20

(c, e) – 1(c, d) – 4(a, e) – 5

a, c, d, e b, f a não em U

(a, c) – 6 rejeitado (d, e) – 8(a, d) – 10(a, b) – 11(d, f) – 12(b, c) – 13(e, f) – 20

(c, e) – 1(c, d) – 4(a, e) – 5

a, c, d, e b, f a e c estão em U

Estruturas de Dados 16

Page 115: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Arestas MST U V-U Comentário

(d, e) – 8 rejeitado(a, d) – 10(a, b) – 11(d, f) – 12(b, c) – 13(e, f) – 20

(c, e) – 1(c, d) – 4(a, e) – 5

a, c, d, e b, f d e e estão em U

(a, d) – 10 rejeitado(a, b) – 11(d, f) – 12(b, c) – 13(e, f) – 20

(c, e) – 1(c, d) – 4(a, e) – 5

a, c, d, e b, f a e d estão em U

(a, b)– 11 aceito(d, f) – 12(b, c) – 13(e, f) – 20

(c, e) – 1(c, d) – 4(a, e) – 5(a,b) – 11

a, b, c, d, e

f b não em U

(d, f)– 12 aceito(b, c) – 13(e, f) – 20

(c, e) – 1(c, d) – 4(a, e) – 5(a,b) – 11(d,f) – 12

a, b, c, d, e, f

f não em U

(b, c) – 13(e, f) – 20

Todos os vértices estão agora em UCOST = 33

Figura 19: Resultado da aplicação do algoritmo Kruskal sobre o grafo anterior

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

Page 116: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

6. Problemas de Menor Caminho para Grafos Direcionados

Outro tipo clássico de problemas em grafos é achar o menor caminho dado um grafo ponderado. Para achar o menor caminho, é necessário obter o comprimento que, neste caso, é a soma dos custos não negativos de cada aresta do caminho.

Existem dois tipos de problemas com grafos ponderados:

• Problema de Menor Caminho de Início Único (Single Source Shortest Paths (SSSP)) que determina o custo do menor caminho de um vértice inicial u para um vértice final v, onde u e v são elementos de V.

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

6.1. Algoritmo de Dijkstra para o Problema SSSP

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.

• Um vértice de Classe 2 é um vértice o qual a sua menor distância de k ainda precisa ser encontrada; seu valor é a sua distância do vértice k encontrada até agora.

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

3. Faça o seguinte até o vértice v seja colocado na classe 1:a. Defina o vértice pivô como o vértice colocado mais recentemente na classe 1b. Ajuste todos os nodes da classe 2 do seguinte modo:

i. Se um vértice não está conectado ao vértice pivô, seu valor permanece o mesmo

ii. Se um vértice está conectado ao vértice pivô, substitua seu valor pelo mínimo entre seu valor atual e o valor do vértice pivô mais a distância do pivô até o vértice na classe 2. Defina seu caminho como pivô

c. Escolha um vértice de classe 2 com valor mínimo e coloque-o na classe 1

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

Page 117: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 20: Um gráfico ponderado e direcionado para o exemplo SSSP

A tabela seguinte mostra a execução do algoritmo:

Vértice classe valor caminho Descrição

1234567

1222222

0∞∞∞∞∞∞

0000000

O vértice inicial é 1. Sua classe é definida como 1. Todas as outras são definidas como 2. O valor do vértice 1 é 0 enquanto os demais forem ∞. Os caminhos de todos os vértices são definidos como 0 uma vez que os caminhos do vértice inicial para o vértice ainda não foram encontrados.

Pivô 1234567

1222222

043∞∞∞∞

0000000

O vértice inicial é o primeiro pivô. Ele está conectado aos vértices 2 e 3. Portanto, os valores dos vértices estão definidos como 4 e 3 respectivamente. O vértice de classe 2 de menor valor é 3, então ele é escolhido como o próximo pivô.

Pivô

1234567

1212222

0437∞∞∞

0113000

A classe do vértice 3 é definida como 1. Ele é adjacente aos vértices 1, 2 e 4, mas o valor de 1 e 2 é menor que o valor se o caminho que inclui o vértice 3 é considerado, então não haverá mudança em seus valores. Para o vértice 4, o valor é definido como (valor de 3) + custo(3, 4) = 3 + 4 = 7. Caminho(4) é definido como 3.

Pivô1234567

1112222

0437516∞

0113220

O próximo pivô é 2. Ele é adjacente a 4, 5 e 6. Adicionar o pivô ao menor caminho atual para 4 aumentará seu custo, mas o mesmo não ocorre com 5 e 6, onde os valores são mudados para 5 e 16 respectivamente.

Estruturas de Dados 19

Page 118: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Vértice classe valor caminho Descrição

Pivô

1234567

1112122

04375711

0113255

O próximo pivô é 5. Ele está conectado aos vértices 6 e 7. Adicionar o vértice 5 ao caminho mudará o valor do vértice 6 para 7 e do vértice 7 para 11.

Pivô

1234567

1111122

04375711

0113255

O próximo pivô é 4. Embora ele seja adjacente ao vértice 6, o valor de 6 não mudará se o pivô for adicionado ao seu caminho.

Pivô

1234567

1111112

0437578

0113256

Tornar 6 o pivô impõe mudar no valor do vértice 7 de 11 para 8 e também adicionar o vértice 6 ao caminho do anterior.

Pivô

1234567

1111111

0437578

0113256

Agora, o vértice final v é colocado na classe 1. O algoritmo termina.

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) = 6caminho(6) = 5caminho(5) = 2caminho(2) = 1

Portanto, o menor caminho é 1 --> 2 --> 5 --> 6 --> 7, e o custo é valor(7) = 8.

6.2. Algoritmo Floyd para o problema APSP

Para encontrar o menor caminho para todos os pares, algoritmo Dijkstra pode ser usado com todos os pares de origens e destinos. Contudo, essa não é a melhor solução existente para o problema APSP. Uma solução mais elegante e apropriada é usar o algoritmo criado por Floyd.

O algoritmo faz uso da representação de matriz de adjacência de custo de um grafo. Ela tem uma dimensão de n x n para n vértices. Nesse algoritmo, o COST é dado pela matriz de adjacência. A é a matriz que contém p custo do caminho mais curto, inicialmente igual ao COST. Outra matriz n x n, PATH, contém os vértices eminentes ao lado do caminho mais curto:

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

Page 119: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

mesma existir

= k se incluir k no caminho de i a j pela k-ésima interação, produz um caminho de maior custo

O algoritmo é como se segue:

1. Inicialize A para ser igual ao COST:

A(i, j) = COST(i, j), 1 ≤ i, j ≤ n

2. Se o custo de passagem atravessar o vértice intermediário k do vértice i ao vértice j custar menos que o acesso direto de i to j, substitua A(i,j) com esse custo e atualize PATH(i,j), ou seja:

Para k = 1, 2, 3, ..., n

a) A(i, j) = mínimo [ Ak-1(i, j), Ak-1(i, k) + Ak-1(k, j) ] , 1 ≤ i, j ≤ n

b) Se ( A(i, j) == Ak-1(i, k) + Ak-1(k, j) ) atribua PATH(i, j) = k

Por exemplo, resolva o problema APSP do grafo a seguir usando o algoritmo de Floyd:

Figura 21: Um Grafo Ponderado e Direcionado para o exemplo APSP

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

Como A(2, 2) = 0, nunca haverá mudança na k-ésima linha e coluna.

A seguir é mostrada a execução do algoritmo de Floyd:

1 2 3 4 1 2 3 4

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

Para a primeira interação k=1:

Estruturas de Dados 21

Page 120: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

A(2, 3) = mínimo( A(2, 3) , A(2, 1) + A(1, 3) ) = mínimo(∞, ∞) = ∞A(2, 4) = mínimo( A(2, 4) , A(2, 1) + A(1, 4) ) = mínimo(12, 20) = 12A(3, 2) = mínimo( A(3, 2) , A(3, 1) + A(1, 2) ) = mínimo(10, 7) = 7A(3, 4) = mínimo( A(3, 4) , A(3, 1) + A(1, 4) ) = mínimo(7, 17) = 7A(4, 2) = mínimo( A(4, 2) , A(4, 1) + A(1, 2) ) = mínimo(∞, ∞) = ∞ A(4, 3) = mínimo( A(4, 3) , A(4, 1) + A(1, 3) ) = mínimo(1, ∞) = 1

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 PATHPara 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) = 9A(3, 1) = mínimo( A(3, 1) , A(3, 2) + A(2, 1) ) = mínimo(5, 15) = 5A(3, 4) = mínimo( A(3, 4) , A(3, 2) + A(2, 4) ) = mínimo(7, 12) = 7A(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 PATHPara 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) = 6A(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 PATHPara k=3:

A(1, 2) = mínimo( A(1, 2) , A(1, 4) + A(4, 2) ) = mínimo (2, 17) = 2

Estruturas de Dados 22

Page 121: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

A(1, 3) = mínimo ( A(1, 3) , A(1, 4) + A(4, 3) ) = mínimo (∞, 10) = 10A(2, 1) = mínimo ( A(2, 1) , A(2, 4) + A(4, 1) ) = mínimo (8, 13) = 8A(2, 3) = mínimo ( A(2, 3) , A(2, 4) + A(4, 3) ) = mínimo (∞, 8) = 8A(3, 1) = mínimo ( A(3, 1) , A(3, 4) + A(4, 1) ) = mínimo (5, 13) = 5A(3, 2) = mínimo ( A(3, 2) , A(3, 4) + A(4, 2) ) = mínimo (7, 15) = 7

1 2 3 4 1 2 3 4

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

Page 122: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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

Page 123: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

a)

DIJKSTRA 1 (Origem: 8, Destino: 4)

b)

DIJKSTRA 2 (Origem: 1, Destino: 7)

4. Resolva o APSP dos grafos a seguir dando as matrizes A e Path usando o algoritmo Floyd:

7.1. Exercícios para Programar

1. Crie uma definição de classe Java para grafos direcionados ponderados usando representação de matriz de adjacência.

2. Implemente os dois algoritmos de grafo transversal.

3. Caminho mais curto usando o algoritmo Dijkstra.

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

Page 124: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

número determinado para o destino e k o custo da chegada de j a i (Lembre-se, usamos grafos direcionados aqui). Conclua a entrada usando (0, 0, 0).

Após a entrada, crie um mapa e mostre-o para usuário. Solicite ao usuário informar a fonte e o destino e dê o caminho mais curto usando o algoritmo Dijkstra.

Mostre a saída na tela. A saída consiste no caminho e seu custo. O caminho deve seguir a seguinte forma:

origem localização 2 … localização n-1 destino

Amostragem do arquivo de entrada

(1, Math Building)(2, Science Building)(3, Engineering)..(0, 0)(1, 2, 10)(1, 3, 5)(3, 2, 2)..(0, 0, 0)

início da definição de aresta

Amostragem do fluxo do programa

[ MAP and LEGEND ]Origem de entrada: 1Destino de entrada: 2Origem: Math BuildingDestino: Science BuildingCaminho: Math Building Engineering Science BuildingCusto: 7

Estruturas de Dados 26

Page 125: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

Módulo 3Estruturas de Dados

Lição 7Listas

Versão 1.0 - Mai/2007

Page 126: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

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

Page 127: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

2. Definições e conceitos relacionados

Uma Lista é um conjunto finito com nenhum ou "n" elementos. Os elementos de uma lista podem ser átomos ou listas. Um átomo é distinguível de uma lista. Listas são classificadas em dois tipos – lineares e generalizadas. Listas lineares contém apenas elementos átomos, e são ordenadas seqüencialmente. Listas generalizadas podem conter ambos elementos átomo e lista.

2.1. Lista Linear

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

2.2. Lista Generalizada

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

Page 128: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 1. Uma lista generalizada

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

Page 129: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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.

3.1. Representação seqüencial de lista linear encadeada simples

Na representação seqüencial, os elementos são armazenados contiguamente. Há um ponteiro para o último item na lista. A seguir é mostrada uma lista usando representação seqüencial:

Figura 2. Representação Seqüencial de Lista

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.

3.2. Representação encadeada de lista linear encadeada simples

Uma corrente de nodes encadeados pode ser usada para representar uma lista.

Figura 3. Representação Encadeada de 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

Page 130: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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:

Operação Representação Seqüencial

Representação 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.

3.3. Lista circular encadeada simples

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:

Figura 4. Lista Circular Encadeada Simples

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

Page 131: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

3.3.1. Inserção a Esquerda

O seguinte método inserido na classe realiza o procedimento de inserir um elemento X na ponta da esquerda da lista circular:

public void insertLeft(Object x) { Node alpha = new Node(x,null); if (F == null) { alpha.link = alpha; F = alpha; } else { alpha.link = F.link; F.link = alpha; } L = alpha; }

No método, se a lista é inicialmente vazia, resultará a seguinte lista circular:

Figura 5. Inserção em uma lista vazia

De outra forma, será realizado, conforme o seguinte diagrama:

Figura 6. Inserção em uma lista não vazia

3.3.2. Inserção a Direita

O seguinte método inserido na classe realiza o procedimento de inserir o elemento X na ponta da direita da lista circular:

public void insertRight(Object x) { Node alpha = new Node(x,null); if (L == null) { alpha.link = alpha; F = alpha; } else { alpha.link = L.link; L.link = alpha;

Estruturas de Dados 9

Page 132: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

} L = alpha; }

Se a lista é inicialmente vazia, o resultado de insertRight é similar ao de insertLeft, entretanto para uma lista não vazia,

Figura 7. Inserção à Direita

3.3.3. Exclusão a Esquerda

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; } }

Execução de deleteLeft em uma lista não vazia é ilustrada conforme a figura 8.

Figura 8. Remoção à Esquerda

Todos esses três procedimentos têm complexidade de tempo de O(1).

3.3.4. Concatenação de duas listas

Outra operação que possui tempo O(1) em uma lista circular é a concatenação. Isto é, dadas duas listas:

Estruturas de Dados 10

Page 133: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

L1 = (a1, a2, ..., am) e L2 = (b1, b2, ..., bn)Onde m, n ≥ 0, as listas resultantes são:

L1 = (a1, a2, ..., am, b1, b2, ..., bn) L2 = null

Isso pode ser feito por simples manipulações do campo de ponteiro dos nodes realizando uma junção de duas listas:

public void concatenate(Node List2) { if ((List2 != null) && (L != null)) { Node alpha = L.link; L.link = List2.link; List2.link = alpha; } }

Se L2 é não vazia, o processo de concatenação é ilustrado abaixo:

Figura 9. Concatenação de duas listas

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); } public static void main(String[] args) throws Exception { // Cria uma lista com 4 elementos inseridos pela esquerda CircularList cL = new CircularList(); for (int i = 1; i < 5; i++) cL.insertLeft("Element " + i + "L"); System.out.println("Left"); cL.showList(cL.F, 5);

Estruturas de Dados 11

Page 134: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

// Elimina o elemento mais a esquerda System.out.println("After DeleteLeft"); cL.deleteLeft(); cL.showList(cL.F, 4);

// Cria uma lista com 4 elementos inseridos pela direita CircularList cR = new CircularList(); for (int i = 1; i < 5; i++) cR.insertLeft("Element " + i + "R"); System.out.println("Right"); cR.showList(cR.F, 5); // Concatena a lista da esquerda com a da direita cL.concatenate(cR.F); System.out.println("Concate"); cL.showList(cL.L, 8); }

E como resultado será produzido:

LeftElement 1LElement 4LElement 3LElement 2LElement 1LAfter DeleteLeftElement 1LElement 3LElement 2LElement 1LRightElement 1RElement 4RElement 3RElement 2RElement 1RConcateElement 3LElement 4RElement 3RElement 2RElement 1RElement 2LElement 1LElement 3L

observe que sempre listamos um elemento a mais para mostrar a circularidade da lista.

3.4. Lista encadeada simples com header nodes

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

Page 135: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

os finais, para fazer inserção ou armazenamento de realocação rapidamente. O cabeçalho da lista é também usado para representar a lista vazia por um objeto não nulo em uma linguagem de programação orientada a objeto como Java, então esses métodos como inserção podem ser invocados em uma lista vazia. A próxima figura ilustra uma lista circular com um cabeçalho de lista:

Figura 10. Lista Encadeada Simples com Cabeçalho de Lista

3.5. Lista encadeada dupla

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.

Figura 11. Estrutura de nodes de Lista Encadeada Dupla

A seguir um exemplo de uma lista encadeada dupla:

Figura 12. 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:

Figura 13. Remoção em uma Lista Encadeada Dupla

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

Propriedades de lista encadeada dupla

Estruturas de Dados 13

Page 136: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

• LLINK(L) = RLINK(L) = L significa que a lista L é vazia.• Podemos deletar qualquer node, como o node α, em L no tempo O(1), sabendo apenas o

endereço α.• Podemos inserir um novo node, como o node β, à esquerda (ou direita) de qualquer node,

como o node α, no tempo O(1), sabendo apenas α sendo que só precisa de ajustes de ponteiro, como ilustrado abaixo:

Figura 14. Inserção em uma Lista Encadeada

Estruturas de Dados 14

Page 137: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

4. Aplicação: Aritmética Polinomial

As listas podem ser usadas para representar polinômios nos quais podem ser aplicadas operações aritméticas. Há dois assuntos que têm que ser tratados:

• Um caminho para representar os termos de um polinômio, tal que cada termo pode ser acessado e processado facilmente pelas entidades que os incluem.

• Uma estrutura dinâmica, isto é, para crescer e diminuir conforme necessário.

Para tratar estes assuntos, lista circular simplesmente ligada com cabeça de lista pode ser usada, com uma estrutura de node como ilustrado abaixo:

Figura 15. Estrutura do node

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) = 20x5y4 – 5x2yz + 13yz3:

Figura 16. Um Polinômio Simbolizado, usando Representação Ligada

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:

Figura 17. Zero Polinomial

Em Java, o seguinte é a definição de um termo polinomial:

Estruturas de Dados 15

Page 138: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

class PolyTerm { int expo; int coef; PolyTerm link;

// Cria um termo novo que contém o expo -001 (cabeça de lista) public PolyTerm() { expo = -1; coef = 0; link = null; } // Cria um termo novo com expo e o coeficiente public PolyTerm(int e, int c) { expo = e; coef = c; link = null; }}

e o seguinte é a definição de um Polinômio:

class Polynomial { PolyTerm head = new PolyTerm(); // list head public Polynomial() { head.link = head; } // Cria um novo polinômio com head h public Polynomial(PolyTerm h) { head = h; h.link = head; }}

Para inserir termos de forma canônica, o seguinte é um método da classe Polynomial:

/* Insere um termo para [this] polinômio inserindoem seu próprio local, para manter a forma canônica */public void insertTerm(PolyTerm p) { PolyTerm alpha = head.link; // ponteiro móvel PolyTerm beta = head; if (alpha == head) { head.link = p; p.link = head; return; } else { while (true) {

/* Se o termo corrente é menor do que alpha ou é o menor no polinômio, então insire */if ((alpha.expo < p.expo) || (alpha == head)) { p.link = alpha; beta.link = p; return;}

// Avançar alpha e betaalpha = alpha.link;beta = beta.link;

Estruturas de Dados 16

Page 139: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

// Se o círculo está completo, retornaif (beta == head)

return; } }}

4.1. Algoritmos de Aritmética Polinomial

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

O estado de α e β cairá em um dos seguintes casos:

• EXPO (α) < EXPO (β)

Ação: avançar os ponteiros para o polinômio Q um node acima

• EXPO (α) > EXPO (β)

Ação: copiar o termo corrente em P e insira-o antes do termo corrente em Q, então avançar α

• EXPO (α) = EXPO (β)

• 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

Ação: terminar o procedimento

• Senão, α e β estão apontando para dois termos que podem ser adicionados

Ação: adicionar os coeficientes. Quando o resultado é zero, deletar o node de Q. Mover P e Q para o termo seguinte.

Por exemplo, adicionar os dois polinômios seguintes:

P = 67xy2z + 32x2yz – 45xz5 + 6x – 2x3y2z

Q = 15x3y2z - 9xyz + 5y4z3 - 32x2yz

Já que os dois polinômios não estão em forma canônica, seus termos devem ser reordenados antes deles serem representados, como:

Estruturas de Dados 17

Page 140: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 18. Polinômios P e Q

Adicionar os dois,

Expo(α) = Expo(β)

adicionar α e β:

Expo(α) = Expo(β)

adicionar α e β, resultados para excluir o node apontado por β:

Expo(α) > Expo(β)

inserir α entre σ ε β:

Estruturas de Dados 18

Page 141: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Expo(α) < Expo(β), avançar σ e β:

Expo(α) > Expo(β), inserir α em Q:

Expo(α) > Expo(β), inserir α em Q:

Figura 19. Adição dos Polinômios P e Q

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ômiopublic 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

Page 142: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

// Ponteiro para o node atrás de beta, usado na inserção para Q PolyTerm sigma = head; PolyTerm tau; while (true) { // Termo corrente em P > termo corrente em Q if (alpha.expo < beta.expo) {

// Avançar os ponteiros em Q sigma = beta;

beta = beta.link; } else if (alpha.expo > beta.expo) {

// Inserir o termo corrente em P para Q tau = new PolyTerm();

tau.coef = alpha.coef;tau.expo = alpha.expo;sigma.link = tau;tau.link = beta;sigma = tau;alpha = alpha.link; // Avançar o ponteiro em P

} else { // Termos em P e Q podem ser adicionadosif (alpha.expo < 0) return; // A soma já está em Qelse { beta.coef = beta.coef + alpha.coef; // Se adicionando causará cancelamento do termo if (beta.coef == 0) { // tau = beta; sigma.link = beta.link; beta = beta.link; } else { // Avançar os ponteiros em Q

sigma = beta; beta = beta.link; }

// Avançar o ponteiro em P alpha = alpha.link;

} }

} }

4.1.2. Subtração Polinomial

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.

// Executa a operação Q = Q-P, Q é [this] polinômiopublic void subtract(Polynomial P){ PolyTerm alpha = P.head.link; // Negar todos os termos em P while (alpha.expo != -1) { alpha.coef = - alpha.coef; alpha = alpha.link; } // Adicionar P para [this] polinômio this.add(P); // Restaurar P

Estruturas de Dados 20

Page 143: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

while (alpha.expo != -1) { alpha = alpha.link; alpha.coef = - alpha.coef; }}

4.1.3. Multiplicação Polinomial

Para multiplicar dois polinômios P e Q, um polinômio R inicialmente zero é necessário para conter o produto, isto é, R = R + P*Q. No processo, todos os termos em P são multiplicados com todos os termos em Q.

O seguinte é a implementação Java:

/* Executar a operação R = R + P*Q, onde T é inicialmente um zero polinomial e R é this polinômio */public void multiply(Polynomial P, Polynomial Q) { // Criar o polinômio temporário T, para conter termo do produto Polynomial T = new Polynomial(); // Ponteiro móvel em T PolyTerm tau = new PolyTerm(); // Conter o produto Polynomial R = new Polynomial(); // Ponteiro móvel em P e Q PolyTerm alpha, beta; // Inicializar T e tau T.head.link = tau; tau.link = T.head; // Multiplicar alpha = P.head.link; // Para todos os termos em P while (alpha.expo != -1) { beta = Q.head.link; // multiplicar com todos os termos em Q while (beta.expo != -1) { tau.coef = alpha.coef * beta.coef;

tau.expo = expoAdd(alpha.expo, beta.expo);R.add(T);beta = beta.link;

} alpha = alpha.link; } this.head = R.head; // Fazer [this] polinômio ser R

}/* Executar a adição de expoentes do triplo(x,y,z) Método auxiliar usado por multiply */public int expoAdd(int expo1, int expo2) {

int ex = expo1/100 + expo2/100; int ey = expo1%100/10 + expo2%100/10; int ez = expo1%10 + expo2%10;

return (ex * 100 + ey * 10 + ez);}

Estruturas de Dados 21

Page 144: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

5. Alocação Dinâmica de Memória

Alocação Dinâmica de Memória (DMA - Dynamic Memory Allocation) refere-se ao gerenciamento de uma área contínua de memória, chamada memory pool, usando técnicas para alocar e desalocar blocos. Assume-se que o memory pool consiste de unidades individuais endereçáveis chamadas words. O tamanho de um bloco é mensurado em words. A alocação dinâmica de memória também é conhecida como alocação dinâmica de armazenamento ou dynamic storage allocation. Na DMA, blocos existem em tamanho variável, daqui, getNode e retNode, como discutido no lição 1, não serão suficientes para gerenciar alocação de blocos.

Existem duas operações relacionadas à DMA: reserva e liberação. Durante a reserva, um bloco de memória é alocado para uma tarefa de requisição (requesting). Quando um bloco não é mais necessário, ele está pronto para ser liberado. Liberação é o processo para retorná-lo ao memory pool.

Existem duas técnicas gerais na DMA – método sequential fit e buddy-system.

5.1. Gerenciando o Memory Pool

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

5.2. Método Sequential-Fit: Reserva

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.

Figura 20. lista disponível

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

Page 145: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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,

Figura 21. Um Memory Pool

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

Page 146: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Task Request

E 7K

Figura 22. Resultado de aplicação de três métodos Sequential Fit

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.

5.3. Método Sequential-Fit: Liberação

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.

As figuras seguintes demonstram os quatro possíveis cenários durante liberação de um bloco:

Estruturas de Dados 24

Page 147: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

(A)bloco

reservado

(A)bloco

reservado

(A)blocolivre

(A)blocolivre

(B)blocolivre

(B)blocolivre

(B)blocolivre

(B)blocolivre

(C)bloco

reservado

(C)blocolivre

(C)bloco

reservado

(C)blocolivre

(a) (b) (c) (d)

Figura 23. Casos possíveis na Liberação

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

Na técnica sorted-list, a lista disponível é convertida em uma lista singly-linked e é considerada para ser ordenada no aumento de endereços e memória. Quando um bloco está liberado, são necessárias as seguintes interações:

• O bloco recentemente liberado vem antes de um bloco liberado;• O bloco recentemente liberado vem após um bloco livre; ou• O bloco recentemente liberado antes e após blocos livres.

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.

Figura 24. Exemplos de Liberação

Estruturas de Dados 25

Page 148: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

5.3.2. A Técnica Boundary-Tag

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

(a) bloco livre (b) bloco reservado

Figura 25. node de estrutura na técnica Boundary-Tag

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:

Figura 26. O estado inicial do Memory Pool

Estruturas de Dados 26

Page 149: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Após usar a memória por algum tempo, ficará com segmentos descontínuos, assim teremos a seguinte lista disponível:

Figura 27. lista disponível após algumas alocações e desalocações

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

5.4. Método Buddy-System

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)

Nesta sessão, iremos cobrir apenas o método binary buddy-system.

5.4.1. O método Binary Buddy-System

Figura 28. buddies no Binary Buddy-System

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

Page 150: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

2. Se o tamanho do bloco é o menor múltiplo de 2 ≥ n, então retorna o bloco reservado para a tarefa de requisiçãoCaso contrário vai para 3

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

Page 151: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Estruturas de Dados 29

Page 152: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 29. Exemplo de método Binary Buddy System

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.

Localizar o buddy é um passo crucial na operação de liberação e é feito pela computação:

Deixe β(k:α) = endereço do buddy do bloco de tamanho 2k no endereço α

β(k:α) = α + 2k se α mod 2k+1 = 0

β(k:α) = α - 2k de outro modo.

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

Page 153: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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

Page 154: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 30. Método Binary Buddy System Exemplo de Liberação

Na implementação, a seguinte estrutura, uma lista doubly-linked, será usada para as listas disponíveis:

LLINK TAG KVAL RLINKTAG = 0 se o bloco estiver livre 1 se o bloco estiver liberadoKVAL = 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.

5.5. Fragmentação interna e externa na DMA

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

Page 155: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

6. Exercícios

1. Mostre a representação de ligação circular do polinômio

P(x,y,z) = 2x2y2z2 + x5z4 + 10x4y2z3 + 12x4y5z6 + 5y2z + 3y + 21z2

2. Mostre a representação da lista dos seguintes polinômios e dê o resultado quando Polynomial.add(P, Q) é executado:

P(x, y, z) = 5xyz2 - 12X2y3 + 7xyQ(x, y, z) = 13xy - 10xyz2 + 9y33z2

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

Page 156: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

6.1. Exercícios para Programar

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.

4. Pela implementação da interface criada no exercício 1, crie uma classe para:

a) uma lista singly-linked

b) uma circular-list

c) uma lista doubly-linked

Estruturas de Dados 34

Page 157: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

Módulo 3Estruturas de Dados

Lição 8Tabelas

Versão 1.0 - Mai/2007

Page 158: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

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

Page 159: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

2. Definições e Conceitos Correlatos

Uma tabela é definida como um grupo de elementos, cada um chamado de registro. Cada registro tem uma única chave associada com o seu registro distinto a ser utilizado.

Chave DadoK0 X0

K1 X1

… …Ki Xi

… …Kn-1 Xn-1

Na tabela acima, n registros estão armazenados. Ki é a chave da posição i, enquanto Xi é associado ao dado. A notação usada para um registro é (Ki, Xi).

A classe definição utilizada para a tabela em Java é

class Table { int key[]; int data[]; int size;

// Cria uma tabela vazia public Table() { }

// Cria uma tabela de tamanho s public Table(int s) { size = s; key = new int[size]; data = new int[size]; }}

2.1. Tipos de Chaves

Se uma chave é contida dentro de um registro e este é relativo ao início do registro específico, esta é conhecida como interna ou chave embutida. Se a chave esta contida em uma tabela separada como ponteiros associando-a aos dados, a chave é classificada como uma chave externa.

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:

• Busca por registro em que Ki = K, onde K é dado pelo usuário• Inserção• Deleção• Busca do registro com chave menor (mais larga)• Dada uma chave Ki, encontrar o registro com a próxima chave mais larga (menor)• E outras…

Estruturas de Dados 5

Page 160: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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:

• Tamanho de espaço de chavo Uk, isto é, o número de chaves possíveis• Natureza da tabela: dinâmica ou estática• Tipo e misto de operações realizadas na tabela

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

Page 161: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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.

3.1. Organização de Tabela

Há dois modos genéricos para organizar uma tabela: ordenado e desordenado. Em uma tabela ordenada, os elementos são sorteados baseados em suas chaves. A referência ao primeiro elemento, ao segundo elemento, e assim sucessivamente torna-se possível. Em uma tabela desordenada, não existem relações presumidas entre os registros e suas chaves associadas.

3.2. Busca Sequencial em uma Tabela Desordenada

Buscas sequenciais lêem cada registro seguidamente do início até que o registro ou registros procurados sejam encontrados. Isto é aplicável a uma tabela que é organizada também como um array ou como uma lista linkada. Esta busca é também conhecida como busca linear.

CHAVE DADO1 K0 X0

2 K1 X1

… … …i Ki Xi

… … …n Kn Xn

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 = 02. Compare: se K = Ki, pare – busca com sucesso3. Avance: Incremente i por 1 4. Fim do arquivo?: se i < n, vá para o passo 2. então pare: Busca sem sucesso

Eis uma implementação de busca sequencial:

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

Page 162: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

3.3. Buscando em uma Tabela Ordenada

Existem três métodos de busca em uma tabela ordenada: busca sequencial indexada, busca binária e busca de Fibonacci.

Busca Sequencial Indexada

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

A figura seguinte mostra um exemplo:

ID No Ponteiro ID No Nome Data Nascimento

Curso

1 12345 45678 Andres Agor 23/01/87 BSCS

2 23456 56789 Juan Ramos 14/10/85 BSIE

3 34567 23456 Maria dela Cruz 07/12/86 BSCS

4 45678 78901 Mayumi Antonio 18/09/85 BSCE

5 56789 34567 Jose Santiago 17/06/86 BS Biology

6 67890 12345 Bituin Abad 21/04/84 BSBA

7 78901 67890 Frisco Aquino 22/08/87 BSME

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

Onde I é inicialmente o valor médio do índice.

O Algoritmo

// Retorna o índice da chave k se encontrado, senão -1public int binarySearch(int k, Table t) { int lower = 0;

Estruturas de Dados 8

Page 163: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

int upper = t.size - 1; int middle; while (lower < upper) { // assume o médio middle = (int) Math.floor((lower + upper) / 2); if (k == t.key[middle])

return middle; // busca com sucesso else if (k > t.key[middle])

lower = middle + 1; // menor metade descartada else upper = upper – 1; // maior metade descartada }

return notFound; // busca sem sucesso}

Por exemplo, busca pela chave k = 34567

0 1 2 3 4 5 6

12345 23456 34567 45678 56789 67890 78901

menor = 0, maior = 6, médio = 3: k < kmiddle (45678)

menor = 0, maior = 2, médio = 1: k > kmiddle (23456)

menor = 2, maior = 2, médio = 2: k = kmiddle (34567) ==> busca com sucesso

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.

Busca Binária Multiplicativa

É 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

2. Organize os registros na tabela de acordo com sequência level-order correspondente na árvore binária

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

Page 164: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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 = 0F1 = 1 Fj = Fi-2 + Fi-1 , i ≥ 2

Ou seja, a sequência 0, 1, 1, 2, 3, 5, 8, 13, 21, ...

Em Java,

public int fibonacci(int i) { if (i == 0) return 0; else if (i == 1) return 1; else return fibonacci(i-1) + fibonacci(i-2);}

No algoritmo de busca, duas variáveis auxiliares, p e q, são usadas: p = Fi-1

q = Fi-2

Kj é escolhida inicialmente de tal forma que j = Fi, onde Fi é o maior número da série de Fibonacci que é menor ou igual ao tamanho da tabela (n).

É uma suposição que a tabela é de seja de tamanho n=Fi+1-1.

Três estados de comparação são possíveis:

• Se K = Kj, pare: busca bem sucedida

• 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

Page 165: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

• Desloque p e q duas posições para a esquerda na sequência numérica

• K < Kj e q=0 ou K > Kj e p=1: busca mal sucedida

Esse algorítimo encontra o elemento do índice 1 ao n e, como a indexação no Java começa em 0, há uma necessidade de lidar com o caso onde k = key[0].

Por exemplo, procure pela chave k = 34567:

0 1 2 3 4 5 6

12345 23456 34567 45678 56789 67890 78901

0 1 1 2 3 5 8 13 F0 1 2 3 4 5 6 7 ii = 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

Outro exemplo, procure pela chave = 15:

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

i = 7; Fi = 13; (Suposição) table size = Fi+1 - 1 = 20

j = 13; p = 8, q=5: k > key[j] j = 18; p = 3; q=2: k < key[j] j = 15; p = 2; q=1: k = key[j] Bem sucedido

Este é o algorítimo:

public int fibonacciSearch(int k, int key[]) { int p = 0, q = 0, j = 0; int f = 0, i = 0; int temp; while (true) { f = fibonacci(i); if (key.length < f) { j = f; p = fibonacci(i-1); q = fibonacci(i-2); break; } i++; } if (k == key[0]) return 0; while (true) { if (j >= key.length) j = key.length-1; if (k == key[j]) return j;

Estruturas de Dados 11

Page 166: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

else if (k < key[j]) { if (q == 0) break; else { j = j - q; temp = p; p = q; q = temp - q; } } else { if (p == 1) break; else { j = j + q; p = p - q; q = q - p; } } } return notFound;}

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"); int inicial = Integer.parseInt(sdR.format(new Date())); System.out.println("Time: " + sdT.format(new Date())); for (int i = 0; i < size; i++) s.sequentialSearch(i, key); int fim = Integer.parseInt(sdR.format(new Date())); System.out.println("Time: " + sdT.format(new Date())); System.out.println("Result Sequential: " + (fim - inicial));

inicial = Integer.parseInt(sdR.format(new Date())); System.out.println("Time: " + sdT.format(new Date())); for (int i = 0; i < size; i++) s.binarySearch(i, key); fim = Integer.parseInt(sdR.format(new Date())); System.out.println("Time: " + sdT.format(new Date())); System.out.println("Result Binary: " + (fim - inicial));

inicial = Integer.parseInt(sdR.format(new Date())); System.out.println("Time: " + sdT.format(new Date())); for (int i = 0; i < size; i++) s.multiplicativeBinarySearch(i, key); fim = Integer.parseInt(sdR.format(new Date())); System.out.println("Time: " + sdT.format(new Date())); System.out.println("Result Multiplicative Binary: " + (fim - inicial));

inicial = Integer.parseInt(sdR.format(new Date()));

Estruturas de Dados 12

Page 167: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

System.out.println("Time: " + sdT.format(new Date())); for (int i = 0; i < size; i++) s.fibonacciSearch(i, key); fim = Integer.parseInt(sdR.format(new Date())); System.out.println("Time: " + sdT.format(new Date())); System.out.println("Result Fibonacci: " + (fim – inicial));}

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:0593Time: 05:19:43:0609Result Sequential: 16Time: 05:19:43:0609Time: 05:19:43:0734Result Binary: 125Time: 05:19:43:0734Time: 05:19:43:0750Result Multiplicative Binary: 16Time: 05:19:43:0750Time: 05:19:44:0187Result 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

Page 168: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

4. Exercícios

Utilizando os métodos de pequisa abaixo,

a) Pesquisa Sequencialb) Pesquisa Binária Multiplicativac) Pesquisa Bináriad) 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

4.1. Exercício para Programar

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

Page 169: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

Módulo 3Estruturas de Dados

Lição 9Árvores de Pesquisa Binária

Versão 1.0 - Mai/2007

Page 170: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

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.

Figura 1. Árvore Binária (Binary Tree)

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

Page 171: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

2. Operações em Árvores de Pesquisa Binária

As operações mais comuns em uma Árvores de Pesquisa Binária (Binary Search Tree ou BST) são as inserções de novas chaves, remoção de uma chave existente, pesquisa por uma chave e recuperação de dados numa ordem de classificação. Nesta seção, as três primeiras operações serão apresentadas. A quarta operação poderá ser realizada pela leitura da BST seguindo uma ordem.

Nas três operações, assume-se que a árvore de pesquisa binária não está vazia e que não armazena valores duplicados. Além disso, é utilizada a estrutura de node BSTNode (Esquerda, Info, Direita), como ilustrado abaixo:

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:

Figura 3. Representação da Árvore de Pesquisa Binária T2

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

Page 172: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

BSTNode bstHead = new BSTNode();

// Criar uma BST vazia public BST() { bstHead.left = bstHead; bstHead.right = bstHead; } // Criar uma BST com raiz r, ponteiro para bstHead.right public BST(BSTNode r) { bstHead.left = bstHead; bstHead.right = r; }}

2.1. Pesquisando

Na Pesquisa por um valor, digamos k, três condições são possíveis:

• k = valor que está no node (pesquisa com sucesso)

• k < valor que está no node (pesquisa pela subárvore da esquerda)

• k > valor que está no node (pesquisa pela subárvore da direita)

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 encontradopublic BSTNode search(int k) { BSTNode p = bstHead.right; // node raiz

// Se a árvore está vazia, retorna null

if (p == bstHead) return null;

// 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

Page 173: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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çãoelse 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 = knewNode.left = nullnewNode.right = nullif (k < p.info) p.left = newNodeelse p.right = newNode

Em Java,

// Insere k na árvore de pesquisa bináriapublic boolean insert(int k) { BSTNode p = bstHead.right; // node raiz BSTNode newNode = new BSTNode();

// Se a árvore está vazia, torna o novo node raiz if (p == bstHead) { newNode.info = k; bstHead.right = newNode; return true; }

// Procura o local certo para inserir k while (true) { if (k == p.info) // chave já existe return false; else if (k < p.info) // pela esquerda if (p.left != null)

p = p.left;else

break; else if (p.right != null) // pela direita

p = p.right; else

break; }

// Insere a nova chave no local apropriado newNode.info = k; if (k < p.info)

p.left = newNode; else

p.right = newNode; return true;}

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

Page 174: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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

Ação: Atualize o ponteiro filho do parente p:Se d é um filho da esquerda, ajuste p.left=nulldo contrário, ajuste p.right=null

Figura 4. Eliminar um node folha

2. node d é interno (Existem dois sub-casos):

Caso 1: Se d tem uma subárvore à esquerda,

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 node2. Obtenha o parente de ip, diga p_ip 3. Substitua d com ip4. 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 ip2. 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

Page 175: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 5. Excluindo um node interno com uma subárvore à esquerda

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

Figura 6. Remoção de um node interno sem subárvore à esquerda

Estrutura de Dados 9

Page 176: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

O seguinte código Java implementa este procedimento:

// Retorna true se a chave foi removida com sucessopublic boolean delete(int k) { BSTNode delNode = bstHead.right; // o node raiz boolean toLeft = false; // direção do pai BSTNode parent = bstHead; // pai do node removido

// Pesquisa do node a remover while (true) {

// delNode aponta para o node que será removido if (k == delNode.info)

break; else if (k < delNode.info) // Vá pela esquerda

if (delNode.left != null) { toLeft = true; parent = delNode; delNode = delNode.left;} else

return false; // não encontrado else if (delNode.right != null) { // Vá pela direita

toLeft = false; parent = delNode; delNode = delNode.right; } else

return false; // não encontrado }

// Caso 1: Se delNode está na extremidade, atualiza o pai e remove o node if ((delNode.left == null) && (delNode.right == null)) { if (toLeft)

parent.left = null; else

parent.right = null; }

// Case 2.1: Se delNode é interno e tem filho a esquerda else if (delNode.left != null) {

BSTNode inPre = delNode.left; // predecessor inorder BSTNode inPreParent = null; // pai do predecessor inorder

// Procura pelo sucessor inorder de delNode while (inPre.right != null) {

inPreParent = inPre;inPre = inPre.right;

}

// Substitui delNode por inPre if (toLeft)

parent.left = inPre; else

parent.right = inPre;

// Remove inSuc de seu local de origem

// Se inPre não é um node folha if (inPreParent != null) {

Estrutura de Dados 10

Page 177: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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

// Procura o sucessor inorder de delNode while (inSuc.left != null) { inSucParent = inSuc; inSuc = inSuc.left; }

// Substitui delNode por inSuc if (toLeft)

parent.left = inSuc; else

parent.right = inSuc;

// Remove inSuc de seu local de origem

// Se inSuc não é um node folha if (inSucParent != null) { inSucParent.left = inSuc.right; inSuc.right = delNode.right; }

inSuc.left = delNode.left; }

delNode = null; // limpeza da memória (garbage collection) return true; // retorna sucesso}

2.4. Complexidade no tempo de resposta em uma BST

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:

Figura 7. Exemplos de Árvores com pior caso de pesquisa

Estrutura de Dados 11

Page 178: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

3. Árvore Binária de Pesquisa Balanceada

É o balanceamento pobre de uma Árvore Binária a faz ter uma performance de O(n). Por isso, deve-se assegurar de que a Pesquisa gaste O(log2 n), de modo que o balanceamento possa ser mantido. Uma árvore balanceada só será criada se metade dos registros inserida depois de um dado registro r com chave k tenha chaves menores do que k e, similarmente, metade das chaves maiores do que k. Isso quando não há tratamento de balanceamento durante a inserção. No entanto, em situações reais, as chaves são inseridas em ordem aleatória. Portanto, há a necessidade de manter o balanceamento à medida que chaves são inseridas ou apagadas.

O Balanceamento de um node é um fator muito importante na manutenção de balanceamento. Ele é definido como a diferença de altura das subárvores de um node, i.e., a altura da subárvore à esquerda menos a altura da subárvore à direita.

3.1. Árvore AVL

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:

Figura 8. Árvores de Pesquisa Binária com 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

3.1.1. Balanceamento da Árvore

As seguintes árvores são exemplos de árvores AVL:

Figura 9. Árvores AVL

Estrutura de Dados 12

Page 179: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

As seguintes árvores são exemplos de árvores não-AVL:

Figura 10. Árvores não-AVL

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

Figura 11. Rotação Simples à Direita

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

Figura 12. Rotação simples para esquerda

• 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

Page 180: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 13. Rotação Esquerda Direita

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

Figura 14. Rotação Direita Esquerda

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:

1. Uma folha do node é introduzido na árvore com equilíbrio 0;2. Começando pelo node novo, uma mensagem de altura da subárvore que contém o node novo incrementa por 1 é passada acima da árvore seguindo o trajeto até a raiz. Se a mensagem for recebida por um node de sua subárvore à esquerda, 1 é adicionado a seu equilíbrio, caso contrário -1 é adicionado. Se o equilíbrio resultante for +2 ou -2, a rotação tem que ser executada como descrita.

Por exemplo, introduzir os seguintes elementos em uma árvore de AVL:

4 10 9 3 11 8 2 1 5 7 6

Estrutura de Dados 14

Page 181: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 15. Inserção na árvore AVL

Estrutura de Dados 15

Page 182: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

4. Exercícios

1. Insira as seguintes chaves em uma árvore AVL, baseada na ordem informada:

a) 1 6 7 4 2 5 8 9 0 3

b) A R F G E Y B X C S T I O P L V

4.1. Exercícios para programar

1. Estenda a classe BST definindo a construção de uma árvore AVL, i.e.,

class AVL extends BST{}

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

Page 183: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

Módulo 3Estruturas de Dados

Lição 10Hash Table e Técnicas de Hashing

Versão 1.0 - Mai/2007

Page 184: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

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:

chave ====> Função Hash H(k) ===> endereço

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

Page 185: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

2. Técnicas Simples de Hash

Duas ou mais chaves de entrada, sejam k1 e k2, quando aplicadas em uma função hash, podem resultar no mesmo endereço, um acidente conhecido como colisão. A colisão pode ser reduzida alocando-se mais espaço de arquivo que o mínimo necessário para armazenar a quantidade de chaves. Entretanto, essa abordagem leva a desperdício de espaço. Existem diversas formas de lidar com colisões, que serão discutidas mais adiante.

Em hashing, existe a necessidade de se escolher uma boa função hash e, conseqüentemente, selecionar um método para resolver, se não eliminar, as colisões. Uma boa função hash executa cálculos eficientes, com complexidade de tempo O(1), e produz pouca (ou nenhuma) colisão.

Existem muitas técnicas de hash disponíveis, mas discutiremos apenas duas – divisão por número primo e desdobramento.

2.1. Método de Divisão por Números Primos

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

onde k é a chave, um número inteiro, e n é um número primo.

Se n é o número total de locações relativas no arquivo, este método pode ser usado para mapear as chaves em n locações de registro. n deve ser escolhido de forma a reduzir o número de colisões. Se n é par, o resultado da função hash é um número par, se n é impar, o resultado é ímpar. A divisão por número primo não resultará em muitas colisões. Por isso é a melhor escolha para o divisor nesse método. Poderíamos escolher um número primo que seja próximo ao número de registros no arquivo. Entretanto, este método pode ser usado mesmo se n não for primo, mas prepare-se para lidar com mais colisões.

Por exemplo, considere n = 13

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

Para implementar este hashing em Java, basta usar:

Estruturas de Dados 5

Page 186: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

public int hash(int k, int n) { return (k % n); }

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

3. Dígitos ímpares, desdobrando ao meio 7453212 => 7453+212 => 7665

4. Dígitos desiguais, desdobrando em terços 74532123 => 745+32+123 => 900

5. Usando XOR, desdobrando ao meio100101110 => 10010 XOR 1110 => 11100

6. Alternando dígitos125758 => 155+278 => 433

A seguir, alguns exemplos de desdobramento pelos extremos:

1. Dígitos pares, desdobrando ao meio 125758 => 125+857 => 982

2. Desdobrando em terços 125758 => 21+57+85 => 163

3. Dígitos ímpares, desdobrando ao meio 7453212 => 7453+212 => 7665

4. Dígitos desiguais, desdobrando em terços 74532123 => 547+32+321 => 900

Estruturas de Dados 6

Page 187: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

5. Usando XOR, desdobrando ao meio 100100110 => 10010 XOR 0110 => 10100

6. Alternando dígitos125758 => 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

Page 188: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

3. Técnicas de Resolução de Colisões

Escolher um bom algoritmo de hashing baseado em quão poucas colisões espera-se que ocorram é a primeira etapa para se evitar colisões. Entretanto, isso irá apenas minimizar, e não erradicar o problema. Para evitar colisões, poderíamos:

• espalhar os registros: por exemplo, encontrar um algoritmo de hashing que distribua os registros de maneira uniforme entre os endereços disponíveis. Entretanto é difícil achar um algoritmo de hashing que distribua os registros dessa forma.

• usar mais memória: se temos muitos endereços de memória para distribuir registros, é mais fácil de se encontrar um algoritmo de hashing do que se tivermos aproximadamente o mesmo número de endereços e registros. Uma vantagem é que os registros ficarão distribuídos uniformemente, conseqüentemente diminuindo as colisões. Entretanto, esse método desperdiça espaço.

• utilizar buckets: por exemplo, coloque mais de um registro no mesmo endereço.

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

Temos a seguinte tabela hash:

CHAVE LINK CHAVE LINK

0 Λ 0 845 234 Λ1 Λ 1 Λ2 Λ 2 444 431 652 Λ3 Λ 3 458 Λ4 Λ 4 745 459 Λ5 Λ 5 902 382 421 Λ

Estruturas de Dados 8

Page 189: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

CHAVE LINK CHAVE LINK

6 Λ 6 981 Λ7 Λ 7 345 254 Λ8 Λ 8 125 Λ9 Λ 9 256 Λ

10 Λ 10 569 725 Λ11 Λ 11 947 Λ12 Λ 12 792 Λ

Tabela Inicial Depois de Inserções

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.

3.2. Utilização de Buckets

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:

CHAVE1 CHAVE2 CHAVE3

0 845 234

1

2 444 431 652

3 458

4 745 459

5 902 382 421

6 981

7 345 254

8 125

9 256

10 569 725

11 947

12 792

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

Page 190: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

3.3. Endereçamento Aberto (Por Verificação)

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.

3.3.1. Verificação Linear

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

resultando na seguinte tabela hash:

CHAVE1 CHAVE2

0 845 234

1

2 444 431

3 652 458

Estruturas de Dados 10

Page 191: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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

3.3.2. Hashing Duplo

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:

rh(i, k) = ( i + h2(k)) mod m

onde m é a quantidade de endereços

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

Page 192: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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:

h2(652) = 652 mod 11 = 3rh(2, 652) = (2 + 3) mod 13 = 5,

mas o endereço 5 já está cheio, então aplicamos rehash novamente:

rh(5, 652) = (5 + 3) mod 13 = 8, tem espaço – então armazena aqui.

CHAVE1 CHAVE2

0 845 234

1

2 444 431

3

4 745 459

Estruturas de Dados 12

Page 193: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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

rh(5, 421) = (5 + 3) mod 13 = 8,

mas o endereço 8 já está cheio, então aplicamos rehash novamente:

rh(8, 421) = (8 + 3) mod 13 = 11, tem espaço – então armazenamos aqui.

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

Page 194: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

4. Arquivos Dinâmicos & Hashing

As técnicas de hashing discutidas até agora fazem uso de espaço de endereçamento fixo (tabela hash com n endereços). Com espaço de endereçamento estático, o estouro de armazenamento é possível. Se os dados a serem armazenados tiverem natureza dinâmica, ou seja, muitas exclusões e inclusões possíveis, não é recomendado usar tabelas hash estáticas. É aqui que tabelas hash dinâmicas tornam-se úteis. Nessa seção, discutiremos dois métodos: hashing extensível e hashing dinâmico.

4.1. Hashing Extensível

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

Figura 1. Árvore e Hash Table

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

Page 195: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Por esse motivo o endereço A é expandido para dois endereços:

Figura 2. Hash Table

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.

4.2. Hashing Dinâmico

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

Page 196: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Figura 3. Exemplo de Hashing Dinâmico

O espaço de endereçamento original consiste de quatro endereços. Um estouro no endereço 4 resulta na sua expansão para usar dois dígitos 40 e 41. Existe também um estouro no endereço 2, então ele é expandido para 20 e 21. Um estouro no endereço 41 resultou na utilização de três dígitos 410 e 411.

Estruturas de Dados 16

Page 197: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

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.

Chave Valor Hash Equivalente BinárioBanana 2 010Melon 5 101Raspberry 1 001Kiwi 6 110Orange 7 111Apple 0 000

5.1. Exercícios para programar

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

Page 198: Projeto JEDI - Estruturas de Dados - Java - 198 páginas

JEDITM

Parceiros que tornaram JEDITM possível

Instituto CTSPatrocinador do DFJUG.

Sun MicrosystemsFornecimento de servidor de dados para o armazenamento dos vídeo-aulas.

Java Research and Development Center da Universidade das FilipinasCriador da Iniciativa JEDITM.

DFJUGDetentor dos direitos do JEDITM nos países de língua portuguesa.

Banco do BrasilDisponibilização de seus telecentros para abrigar e difundir a Iniciativa JEDITM.

PolitecSuporte e apoio financeiro e logístico a todo o processo.

BorlandApoio internacional para que possamos alcançar os outros países de língua portuguesa.

Instituto Gaudium/CNBBFornecimento 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