Você está na página 1de 148

Algoritmos e

Estruturas de
dados

2022
STITUTO SUPER

LICENCIATURA EM ENGENHARIA
INFORMÁTICA

MÓDULO DE ALGORITMOS E ESTRUTURAS DE DADOS

1º Ano

Disciplina: Algoritmos e estruturas de dados


Código:
Total Horas/1o Semestre: 100
Créditos (SNATCA):
Número de Temas: 08
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Direitos de autor (copyright)

Este manual é propriedade da Universidade Aberta ISCED (UnISCED), e contém


reservados todos os direitos. É proibida a duplicação ou reprodução parcial ou total
deste manual, sob quaisquer formas ou por quaisquer meios (electrónicos, mecânico,
gravação, fotocópia ou outros), sem permissão expressa de entidade editora
Universidade Aberta ISCED (UnISCED).

A não observância do acima estipulado o infractor é passível a aplicação de processos


judiciais em vigor no País.

Universidade Aberta ISCED (UnISCED)


Direcção de Pós-graduação, Pesquisa e Extensão
Rua Dr. Almeida Lacerda, No 212 Ponta - Gêa
Beira - Moçambique
Telefone: +258 23 323501
Cel: +258 82 3055839

Fax: 23323501
E-mail: unisced@unisced.edu.mz
Website: www.unisced.edu.mz

i
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Agradecimentos

O Universidade Aberta ISCED (UnISCED) agradece a colaboração dos seguintes


indivíduos e instituições na elaboração deste manual:

Autor Miro de Nélio Salvador Tucua

Direcção Académica
Coordenação
Universidade Aberta ISCED (UnISCED)
Design
Instituto Africano de Promoção da Educação a Distância
Financiamento e Logística
(IAPED)
Revisão Científica e
Linguística
2022
Ano de Publicação
UnISCED – BEIRA
Local de Publicação

ii
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Índice

VISÃO GERAL 1
Benvindo à Disciplina/Módulo de Algoritmos e Estruturas de Dados ............................. 1
Objectivos do Módulo....................................................................................................... 1
Quem deveria estudar este módulo ................................................................................. 1
Como está estruturado este módulo ................................................................................ 1
Ícones de actividade ......................................................................................................... 3
Habilidades de estudo ...................................................................................................... 3
Precisa de apoio? .............................................................................................................. 5
Tarefas (avaliação e auto-avaliação) ................................................................................ 5
Avaliação ........................................................................................................................... 6

TEMA 1. INTRODUÇÃO À ANÁLISE DE ALGORITMOS. 9


1.1. Introdução ...................................................................................................... 9
1.2. Algoritmo ....................................................................................................... 9
1.3. Notação O (Ó grande) .................................................................................. 11
1.3.1. Classes de Complexidade mais Comuns .............................................. 13
1.4. Outros Limites .............................................................................................. 13
1.5. Estruturas de Dados ..................................................................................... 14
1.6. Sumário ........................................................................................................ 15
1.7. Exercícios de Auto-Avaliação ....................................................................... 15

TEMA 2. ESPECIFICAÇÃO FORMAL DE TIPOS DE DADOS ABSTRACTOS 17


2.1. Introdução .................................................................................................... 17
2.2. Pilhas (Stacks) - LIFO ................................................................................................ 18
2.2.1. Introdução ................................................................................................... 18
2.2.2. Especificação da Pilha.................................................................................. 19
2.2.3. A Interface da Pilha ..................................................................................... 20
2.2.4. Implementação Estática da Pilha ................................................................ 21
2.2.5. Uso da Classe VStack ................................................................................... 23
2.2.6. Sumário........................................................................................................ 24
2.2.7. Exercícios de Auto-Avaliação....................................................................... 24
2.3. Filas (Queue): FIFO ................................................................................................... 25
2.3.1. Introdução ................................................................................................... 25
2.3.2. Especificação da Fila .................................................................................... 25
2.3.3. A Interface da Fila ........................................................................................ 26
2.3.3. Implementação Estática da Fila................................................................... 27
2.3.4. Uso da Classe VQueue ................................................................................. 31
2.3.5. Filas de Espera com Prioridade ................................................................... 31
2.3.6. Sumário........................................................................................................ 32

iii
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

2.3.7. Exercícios de Auto-Avaliação....................................................................... 32

TEMA 3. LISTAS LIGADAS 33


3.1. Introdução .................................................................................................... 33
3.2. Listas Ligadas ou Simples ............................................................................. 34
3.2.1. Introdução ........................................................................................... 34
3.2.2. Algoritmos Básicos das Listas Ligadas ................................................. 35
3.2.3. Implementação Genérica da Lista Simples.......................................... 39
3.2.4. Sumário ................................................................................................ 42
3.2.5. Exercícios de Auto-Avaliação ............................................................... 42
3.3. Listas Biligadas ou Duplamente Ligadas ...................................................... 43
3.3.1. Introdução ........................................................................................... 43
3.3.2. Algoritmos Básicos da Lista Biligada .................................................... 45
3.3.3. Implementação Genérica da Lista Biligada ......................................... 48
3.3.4. Sumário ................................................................................................ 50
3.3.5. Exercícios de Auto-Avaliação ............................................................... 51
3.4. Listas Skip ou com Atalhos ........................................................................... 51
3.4.1. Introdução ........................................................................................... 51
3.4.2. Algoritmos Básicos das Listas Skip ....................................................... 53
3.4.3. Implementação Genérica .................................................................... 61
3.4.4. Sumário ................................................................................................ 64
3.4.5. Exercícios de Auto-Avaliação ............................................................... 64

TEMA 4. TABELAS DE DISPERSÃO 66


4.1. Introdução .................................................................................................... 66
4.2. Especificação da Tabela ............................................................................... 66
4.3. A Interface Tabela ........................................................................................ 67
4.4. Tabelas de Dispersão ................................................................................... 69
4.4.1. Endereçamento Fechado (do inglês closed addressing ou direct
chaining) 70
4.4.2. Endereçamento Aberto (do inglês open addressing) .......................... 71
4.4.3. A Interface HushFunction .................................................................... 73
4.4.4. A Classe HashRegister.......................................................................... 74
4.4.5. A Classe HashTable .............................................................................. 74
4.4.6. Uso da Classe HashTable ..................................................................... 78
4.5. Sumário ........................................................................................................ 79
4.6. Exercícios de Auto-Avaliação ....................................................................... 79

TEMA 5. ÁRVORES GENÉRICAS E BINÁRIAS. 81


5.1. Introdução .................................................................................................... 81
5.2. Árvores Binárias ........................................................................................... 82
5.2.1. Os Percursos de uma Árvore Binária ................................................... 83
5.2.2. Especificação da Árvore Binária .......................................................... 83

iv
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

5.2.3. A Interface Árvore Binária ................................................................... 85


5.2.4. Implementação Dinâmica da Árvore Binária....................................... 88
5.2.5. Uso da Interface Visitor e a Classe DBinTree ...................................... 92
5.2.6. Representação Estática ....................................................................... 93
5.3. Árvores Genéricas ........................................................................................ 93
5.3.1. Árvores (a,b) e Árvores-B..................................................................... 94
5.4. Sumário ........................................................................................................ 97
5.5. Exercícios de Auto-Avaliação ....................................................................... 97

TEMA 6. IMPLEMANTAÇÃO DE ESTRUTURAS DINÂMICAS EM MEMÓRIA SECUNDÁRIA 99


6.1. Introdução .................................................................................................... 99
6.2. Memória Fila ................................................................................................ 99
6.2.1. Caracterização da Memória Fila ........................................................ 100
6.2.2. Implementação da Memória Fila ...................................................... 100
6.3. Memória Pilha ............................................................................................ 102
6.3.1. Caracterização da Memória Pilha ...................................................... 102
6.3.2. Implementacao da Memória Pilha .................................................... 103
6.4. Memória Fila com Prioridade .................................................................... 104
6.4.1. Caracterização da Memória Fila com Prioridade .............................. 105
6.4.2. Implementação da Memória Fila com Prioridade ............................. 106
6.5. Sumário ...................................................................................................... 108
6.6. Exercícios de Auto-Avaliação ..................................................................... 108

TEMA 7. FILAS E ÁRVORES COM PRIORIDADE 110


7.1. Introdução .................................................................................................. 110
7.2. Fila com Prioridade .................................................................................... 110
7.2.1. Implementação Estática .................................................................... 110
7.2.2. Implementação semi-estática ........................................................... 116
7.2.3. Implementação dinâmica .................................................................. 118
7.3. Árvore com Prioridade (Heap) ................................................................... 121
7.3.1. Implementação Estática .................................................................... 123
7.3.2. Uso da Classe VHeap ......................................................................... 128

v
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

7.4. Sumário ...................................................................................................... 128


7.5. Exercícios de Auto-Avaliação ..................................................................... 129

TEMA 8. ORDENAÇÃO INTERNA 130


8.1. Introdução .................................................................................................. 130
8.2. Algoritmo Insertionsort .............................................................................. 130
8.3. Algoritmo Selectionsort.............................................................................. 131
8.4. Algoritmo Bubblesort ................................................................................. 132
8.5. Algoritmo Quicksort ................................................................................... 132
8.6. Algoritmo Heapsort.................................................................................... 135
8.7. A Classe Sort ............................................................................................... 136
8.8. Sumário ...................................................................................................... 139
8.9. Exercícios de Auto-Avaliação ..................................................................... 139

BIBLIOGRAFIA 140

vi
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

VISÃO GERAL

Benvindo à Disciplina/Módulo de Algoritmos e


Estruturas de Dados

Objectivos do Módulo

Ao terminar o estudo deste módulo de Algoritmos e Estruturas de


Dados deverá ser capaz de utilizar os conceitos fundamentais dos
algoritmos e estruturas de dados abstractos utilizando uma
linguagem de programação orientada à objectos.

▪ Resolver problemas de forma eficiente


▪ Desenvolver códigos da estrutura de dados em java
▪ Realizar análises de algoritmos.
▪ Aplicar as técnicas de ordenação e pesquisa de informação.
Objectivos ▪ Saber utilizar as estruturas de abstractas lineares e não lineares.
Específicos

Quem deveria estudar este módulo

Este Módulo foi concebido para estudantes do 1º ano do curso de


Licenciatura em Engenharia Informática da UnISCED. Poderá
ocorrer, contudo, que haja leitores que queiram se actualizar e
consolidar seus conhecimentos nessa disciplina, esses serão bem-
vindos, não sendo necessário para tal se inscrever. Mas poderá
adquirir o manual.

Como está estruturado este módulo

Este módulo de Algoritmos e Estruturas de Dados, para estudantes


do 1º ano do curso de licenciatura em Engenharia Informática, à
semelhança dos restantes da UnISCED, está estruturado como se
segue:

Páginas introdutórias
▪ Um índice completo.
▪ Uma visão geral detalhada dos conteúdos do módulo,
resumindo os aspectos-chave que você precisa conhecer para
melhor estudar. Recomendamos vivamente que leia esta secção
com atenção antes de começar o seu estudo, como componente
de habilidades de estudos.

1
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Conteúdo desta Disciplina / módulo


Este módulo está estruturado em Temas. Cada tema, por sua vez
comporta certo número de unidades temáticas ou simplesmente
unidades. Cada unidade temática se caracteriza por conter uma
introdução, objectivos, conteúdos.
No final de cada unidade temática ou do próprio tema, são
incorporados antes o sumário, exercícios de auto-avaliação, só
depois é que aparecem os exercícios de avaliação.
Os exercícios de avaliação têm as seguintes características: puros
exercícios teóricos/Práticos, Problemas não resolvidos e
actividades práticas, incluído estudo de caso.

Outros recursos
A equipa dos académicos e pedagogos da UnISCED, pensando em
si, num cantinho, recôndito deste nosso vasto Moçambique e cheio
de dúvidas e limitações no seu processo de aprendizagem,
apresenta uma lista de recursos didácticos adicionais ao seu
módulo para você explorar. Para tal a UnISCED disponibiliza na
biblioteca do seu centro de recursos mais material de estudos
relacionado com o seu curso como: Livros e/ou módulos, CD, CD-
ROOM, DVD. Para além deste material físico ou electrónico
disponível na biblioteca, pode ter acesso a Plataforma digital
moodle para alargar mais ainda as possibilidades dos seus estudos.

Auto-avaliação e Tarefas de avaliação


Tarefas de auto-avaliação para este módulo encontram-se no final
de cada unidade temática e de cada tema. As tarefas dos exercícios
de auto-avaliação apresentam duas características: primeiro
apresentam exercícios resolvidos com detalhes. Segundo,
exercícios que mostram apenas respostas.
Tarefas de avaliação devem ser semelhantes às de auto-avaliação,
mas sem mostrar os passos e devem obedecer o grau crescente de
dificuldades do processo de aprendizagem, umas a seguir a outras.
Parte das tarefas de avaliação será objecto dos trabalhos de campo
a serem entregues aos tutores/docentes para efeitos de correcção
e subsequentemente nota. Também constará do exame do fim do
módulo. Pelo que, caro estudante, fazer todos os exercícios de
avaliação é uma grande vantagem.

2
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Comentários e sugestões
Use este espaço para dar sugestões valiosas, sobre determinados
aspectos, quer de natureza científica, quer de natureza didáctico-
Pedagógica, etc., sobre como deveriam ser ou estar apresentadas.
Pode ser que graças as suas observações que, em gozo de
confiança, classificamo-las de úteis, o próximo módulo venha a ser
melhorado.

Ícones de actividade

Ao longo deste manual irá encontrar uma série de ícones nas


margens das folhas. Estes ícones servem para identificar diferentes
partes do processo de aprendizagem. Podem indicar uma parcela
específica de texto, uma nova actividade ou tarefa, uma mudança
de actividade, etc.

Habilidades de estudo

O principal objectivo deste campo é o de ensinar aprender a


aprender. Aprender aprende-se.
Durante a formação e desenvolvimento de competências, para
facilitar a aprendizagem e alcançar melhores resultados, implicará
empenho, dedicação e disciplina no estudo. Isto é, os bons
resultados apenas se conseguem com estratégias eficientes e
eficazes. Por isso é importante saber como, onde e quando estudar.
Apresentamos algumas sugestões com as quais esperamos que caro
estudante possa rentabilizar o tempo dedicado aos estudos,
procedendo como se segue:
1º Praticar a leitura. Aprender a Distância exige alto domínio de
leitura.
2º Fazer leitura diagonal aos conteúdos (leitura corrida).
3º Voltar a fazer leitura, desta vez para a compreensão e
assimilação crítica dos conteúdos (ESTUDAR).
4º Fazer seminário (debate em grupos), para comprovar se a
sua aprendizagem confere ou não com a dos colegas e com
o padrão.
5º Fazer TC (Trabalho de Campo), algumas actividades práticas
ou as de estudo de caso se existirem.
IMPORTANTE: Em observância ao triângulo modo-espaço-tempo,
respectivamente como, onde e quando...estudar, como foi referido
no início deste item, antes de organizar os seus momentos de estudo
reflicta sobre o ambiente de estudo que seria ideal para si: Estudo
melhor em casa/biblioteca/café/outro lugar? Estudo melhor à
noite/de manhã/de tarde/fins-de-semana/ao longo da semana?

3
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Estudo melhor com música/num sítio sossegado/num sítio


barulhento!? Preciso de intervalo em cada 30 minutos, em cada
hora, etc.
É impossível estudar numa noite tudo o que devia ter sido estudado
durante um determinado período de tempo; deve estudar cada
ponto da matéria em profundidade e passar só ao seguinte quando
achar que já domina bem o anterior.
Privilegia-se saber bem (com profundidade) o pouco que puder ler e
estudar, que saber tudo superficialmente! Mas a melhor opção é
juntar o útil ao agradável: saber com profundidade todos conteúdos
de cada tema, no módulo.
Dica importante: não recomendamos estudar seguidamente por
tempo superior a uma hora. Estudar por tempo de uma hora
intercalado por 10 (dez) a 15 (quinze) minutos de descanso (chama-
se descanso à mudança de actividades). Ou seja, que durante o
intervalo não se continuar a tratar dos mesmos assuntos das
actividades obrigatórias.
Uma longa exposição aos estudos ou ao trabalho intelectual
obrigatório pode conduzir ao efeito contrário: baixar o rendimento
da aprendizagem. Por que o estudante acumula um elevado volume
de trabalho, em termos de estudos, em pouco tempo, criando
interferência entre os conhecimentos, perde sequência lógica, por
fim ao perceber que estuda tanto, mas não aprende, cai em
insegurança, depressão e desespero, por se achar injustamente
incapaz!
Não estude na última da hora; quando se trate de fazer alguma
avaliação. Aprenda a ser estudante de facto (aquele que estuda
sistematicamente), não estudar apenas para responder a questões
de alguma avaliação, mas sim estude para a vida, sobre tudo, estude
pensando na sua utilidade como futuro profissional, na área em que
está a se formar.
Organize na sua agenda um horário onde define a que horas e que
matérias deve estudar durante a semana; Face ao tempo livre que
resta, deve decidir como o utilizar produtivamente, decidindo
quanto tempo será dedicado ao estudo e a outras actividades.
É importante identificar as ideias principais de um texto, pois será
uma necessidade para o estudo das diversas matérias que
compõem o curso: A colocação de notas nas margens pode ajudar
a estruturar a matéria de modo que seja mais fácil identificar as
partes que está a estudar e Pode escrever conclusões, exemplos,
vantagens, definições, datas, nomes, pode também utilizar a
margem para colocar comentários seus relacionados com o que
está a ler; a melhor altura para sublinhar é imediatamente a seguir
à compreensão do texto e não depois de uma primeira leitura;

4
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Utilizar o dicionário sempre que surja um conceito cujo significado


não conhece ou não lhe é familiar;

Precisa de apoio?

Caro estudante temos a certeza que por uma ou por outra razão, o
material de estudos impresso, lhe pode suscitar algumas dúvidas
como falta de clareza, alguns erros de concordância, prováveis erros
ortográficos, falta de clareza, fraca visibilidade, página trocada ou
invertidas, etc.). Nestes casos, contacte os serviços de atendimento
e apoio ao estudante do seu Centro de Recursos (CR), via telefone,
SMS, E-mail, se tiver tempo, escreva mesmo uma carta participando
a preocupação.
Uma das atribuições dos Gestores dos CR e seus assistentes
(Pedagógico e Administrativo), é a de monitorar e garantir a sua
aprendizagem com qualidade e sucesso. Dai a relevância da
comunicação no Ensino a Distância (EAD), onde o recurso as TICs se
torna incontornável: entre estudantes, estudante – Tutor, estudante
– CR, etc.
As sessões presenciais são um momento em que você caro
estudante, tem a oportunidade de interagir fisicamente com staff do
seu CR, com tutores ou com parte da equipa central da UnISCED
indigitada para acompanhar as suas sessões presenciais. Neste
período pode apresentar dúvidas, tratar assuntos de natureza
pedagógica e/ou administrativa.
O estudo em grupo, que está estimado para ocupar cerca de 30%
do tempo de estudos a distância, é muita importância, na medida
em que lhe permite situar, em termos do grau de aprendizagem
com relação aos outros colegas. Desta maneira ficará a saber se
precisa de apoio ou precisa de apoiar aos colegas. Desenvolver
hábito de debater assuntos relacionados com os conteúdos
programáticos, constantes nos diferentes temas e unidade
temática, no módulo.

Tarefas (avaliação e auto-avaliação)

O estudante deve realizar todas as tarefas (exercícios, actividades e


auto−avaliação), contudo nem todas deverão ser entregues, mas é
importante que sejam realizadas. As tarefas devem ser entregues
duas semanas antes das sessões presenciais seguintes.
Para cada tarefa serão estabelecidos prazos de entrega, e o não
cumprimento dos prazos de entrega, implica a não classificação do
estudante. Tenha sempre presente que a nota dos trabalhos de

5
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

campo conta e é decisiva para ser admitido ao exame final da


disciplina/módulo.
Os trabalhos devem ser entregues ao Centro de Recursos (CR) e os
mesmos devem ser dirigidos ao tutor/docente.
Podem ser utilizadas diferentes fontes e materiais de pesquisa,
contudo os mesmos devem ser devidamente referenciados,
respeitando os direitos do autor.
O plágio1 é uma violação do direito intelectual do(s) autor(es). Uma
transcrição à letra de mais de 8 (oito) palavras do testo de um autor,
sem o citar é considerado plágio. A honestidade, humildade
científica e o respeito aos direitos autorais devem caracterizar a
realização dos trabalhos e seu autor (estudante da UnISCED).

Avaliação

Muitos perguntam: Como é possível avaliar estudantes à distância,


estando eles fisicamente separados e muito distantes do
docente/tutor? Nós dissemos: Sim é muito possível, talvez seja uma
avaliação mais fiável e consistente.
Você será avaliado durante os estudos à distância que contam com
um mínimo de 90% do total de tempo que precisa de estudar os
conteúdos do seu módulo. Quando o tempo de contacto presencial
conta com um máximo de 10%) do total de tempo do módulo. A
avaliação do estudante consta detalhada do regulamentado de
avaliação.
Os trabalhos de campo por si realizados, durante estudos e
aprendizagem no campo, pesam 25% e servem para a nota de
frequência para ir aos exames.
Os exames são realizados no final da cadeira disciplina ou modulo e
decorrem durante as sessões presenciais. Os exames pesam no
mínimo 75%, o que adicionado aos 25% da média de frequência,
determinam a nota final com a qual o estudante conclui a cadeira.
A nota de 10 (dez) valores é a nota mínima de conclusão da cadeira.
Nesta cadeira o estudante deverá realizar pelo menos 2 (dois)
trabalhos e 1 (um) (exame).
Algumas actividades práticas, relatórios e reflexões serão utilizados
como ferramentas de avaliação formativa.
Durante a realização das avaliações, os estudantes devem ter em
consideração a apresentação, a coerência textual, o grau de
cientificidade, a forma de conclusão dos assuntos, as
recomendações, a identificação das referências bibliográficas
utilizadas, o respeito aos direitos do autor, entre outros.

1
Plágio - copiar ou assinar parcial ou totalmente uma obra literária, propriedade
intelectual de outras pessoas, sem prévia autorização.

6
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Os objectivos e critérios de avaliação constam do Regulamento de


Avaliação.

7
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

TEMA 1. INTRODUÇÃO À ANÁLISE DE ALGORITMOS.

Unidade temática 1.1. Introdução


Unidade temática 1.2. Algoritmo
Unidade temática 1.3. Notação O (Ó grande)
Unidade temática 1.4. Outros limites
Unidade temática 1.5. Estruturas de dados

1.1. Introdução

Ao completar este tema, o estudante deverá ser capaz de:


▪ Explicar a importância da análise de algoritmos.
▪ Identificar e descrever a complexidade de um dado
algoritmo.
Objectivos ▪ Conhecer as classes de complexidade mais comuns da
específicos notação Ó grande.
▪ Conhecer os outros tipos de limites de classes de
complexidade.

1.2. Algoritmo

Algoritmo é um conjunto preciso de ordens para a resolução de um


problema. Ele representa uma medida de tempo.
Memória é a informação lida e alterada pela execução do algoritmo. Ela
representa uma medida de espaço.
Consideremos o problema/tarefa de preparar uma omelete. Para a
resolução desta tarefa seguimos os passos:
1) Obter uma frigideira
2) Obter o óleo
a. Temos o óleo?
i. Se sim, coloca-lo na frigideira
ii. Se não, desejamos comprar o óleo?
1. Se sim, ir comprá-lo
2. Se não, podemos terminar
3) Ligar o fogão, etc…
O mérito dos algoritmos é basicamente julgado pela sua precisão (se o
algoritmo resolve o problema num número finito de passos) e pela sua
eficiência (quantos recursos – memória e tempo - são gastos ou são
necessários para a execução do algoritmo).
A análise dos algoritmos serve para comparar o tempo de execução, a
memória utilizada, o esforço do desenvolvedor, entre outros factores.
A análise do tempo de execução do algoritmo consiste em determinar
o incremento do tempo de processamento quando se incrementa o
tamanho do problema (tamanho de entradas). Os tipos de entrada mais
comuns são:

9
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

• Tamanho de um array
• Grau polinomial
• Número de elementos numa matriz
• Número de bits na representação binária da entrada
• Vértices e pontas nos gráficos.
Para melhor análise de um algoritmo sugere-se que se expresse o
tempo de execução de um dado algoritmo como uma função do
tamanho de entrada n, f(n), e se compare o tempo de execução dessas
diferentes funções. Pois este tipo de comparação é independente do
tempo de máquina, do estilo de programação, entre outros factores.
A taxa de crescimento dos algoritmos é medida em aproximação da
função do tamanho de entrada mais alto.

Tabela 1.1. Relação entre as taxas de crescimento mais comuns.


Complexidade
Designação Exemplo
temporal
Adicionar um elemento a frente da lista
1 Constante
ligada
Encontrar um elemento num array
log(n) Logarítmica
ordenado
Encontrar um elemento num array não
n Linear
ordenado
Ordenar n itens por classificação por
Logarítmica
n*log(n) mesclagem divisão e conquista (divide-
Linear
and-conquer mergesort)
Caminho mais curto entre dois nós num
n2 Quadrática
gráfico
n3 Cúbica Multiplicação de matrizes
2n Exponencial O problema das tores de Hanói

Tipos de analises
Os algoritmos geralmente analisam-se consoante o tempo de execução
e memória utilizada. Para analisar um dado algoritmo, precisamos saber
para quais dados de entrada o algoritmo é executado em menos tempo
e para quais leva mais tempo a ser executado. São eles:
• O pior caso
o Define os dados de entrada que o algoritmo leva maior
tempo para ser executado.
o Os dados de entrada para os quais o algoritmo executa
mais lentamente.
𝑓(𝑛) = 𝑛2 + 500
• O melhor caso

10
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

o Define os dados de entrada que o algoritmo leva menos


tempo para ser executado.
o Os dados de entrada para os quais o algoritmo executa
mais rapidamente.
𝑓(𝑛) = 𝑛 + 100𝑛 + 500
• O caso médio
o Providencia a previsão acerca do tempo de execução do
algoritmo.
o Executa o algoritmo diversas vezes, usando diferentes
dados de entrada provenientes de distribuições que
geram tais dados e depois calcula média pela fórmula
𝐿𝑜𝑤𝑒𝑟𝐵𝑜𝑢𝑛𝑑 ≤ 𝐴𝑣𝑒𝑟𝑎𝑔𝑒𝑇𝑖𝑚𝑒 ≤ 𝑈𝑝𝑝𝑒𝑟𝐵𝑜𝑢𝑛𝑑.
o Assume que os dados de entrada são aleatórios.

1.3. Notação O (Ó grande)

Definição:
Considere f e g duas funções de domínio inteiro não negativo e
contradomínio real. Diz-se que g domina assimptoticamente f,
escrevendo-se f é O(g) ou f é da ordem de O(g), se e só se,
∃𝑘>0 ∃𝐶>0 ∀𝑛>0 ∶ 𝑛 > 𝑘 ⇒ |𝑓(𝑛)| ≤ 𝐶 ∗ |𝑔(𝑛)|
Existem constantes k natural e C real tais que, para todo o argumento
n>k, a função g multiplicada pela constante C é maior do que f. Isto
determina um limite superior ao crescimento da função f. Note que O(g)
representa um conjunto de todas as funções limitadas superiormente
por g. Diz-se que O(g) é uma classe de complexidade. Sendo O(g) um
conjunto de funções, pode-se escrever f é O(g) como f∈O(g) ou ainda
O(f)⊆O(g).
A figura 1.1 apresenta um exemplo com a função g(n)=3*n/8 e a função
f(n)=3*log10(n+1). Com n=1, a função f é maior do que g. Porém, há que
n aumenta, a função g cresce mais depressa e, inevitavelmente, chega
a um valor a partir do qual é sempre maior. Assim, é possível definir um
valor para k (um qualquer valor maior que 27) de modo a que a
expressão lógica da definição anterior seja satisfeita (o valor de C, neste
caso, pode ser igual a 1). Logo f é O(g), por exemplo, f(n)=3*log10(n+1)
é O(3*n/8).

11
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

g(n) = 3*n/8

f(n) = 3*log 10(n+1)

n
k (com C=1)
Figura 1.1. Ilustração da complexidade assimptótica através do uso da
notação O.

O produto das constantes positivas ou a adição de funções com taxas


de crescimento inferior não alteram a classe da complexidade. Para
simplificar a expressão, a identificação das classes é adoptada pelas
funções de maior crescimento. Por exemplo,
• 1000000 é de complexidade O(1)
• 4*m-1 é de complexidade O(m)
• 10/n é de complexidade O(1/n)
• 5*x2 é de complexidade O(x3)
• 5*x2 é de complexidade O(x2)
• 5*x2 não é de complexidade O(x)
• 500*n4-n2) é de complexidade O(n4)
• 500*n4+n2) é de complexidade O(n4)
• n2*log10(n) é de complexidade O(n2*log(n))
• 3*log10(n+1) é de complexidade O(n)
• 45*2n é de complexidade O(еn) е≈2.71828…
• 6 não é de complexidade O(5 )
n n

• 2n é de complexidade O(n!)
• n! é de complexidade O(nn)

A notação O representa o limite superior da taxa de crescimento de uma


função. Apesar de ser correcto dizer 3*log10(n+1) é O(n10) interessa
reduzir o limite superior de uma função o quanto possível, de forma a
maximizar o nosso conhecimento sobre os limites de crescimento da
função. Melhor que afirmar 3*log10(n+1) é O(n) é afirmar que
3*log10(n+1) é O(log(n)).
Note que:
• Para comparar algoritmos da mesma classe de complexidade
pode ser útil apresentar um maior detalhe da função de
crescimento de modo a compará-los de forma mais precisa. Por
exemplo, f(n)=n2+n*log(n) cresce mais depressa que g(n)=n2+n,
apesar de f e g serem ambas O(n2).

12
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

• A constante do factor de maior crescimento também é útil nessa


comparação. Por exemplo, a função f(n)=n2/10 cresce mais
devagar que g(n)=10*n2.
• A base do logaritmo em O(log(n)) é irrelevante porque basta
multiplicar o valor por uma constante para mudar de base:
logan=logab*logbn.
• As constantes das funções, raramente, podem ser relevantes na
comparação de algoritmos de classes destintas. Por exemplo,
f(n)=n2/100000 é menor que a função g(n)=n100000 para um extenso
conjunto de valores de n, apesar de g ser O(f) e f não ser O(g).

1.3.1. Classes de Complexidade mais Comuns


A seguir são apresentadas as classes de complexidade mais comuns,
partindo da menor para a maior.
• Constante: O(n0), ou simplesmente O(1)
• Logarítmica: O(log(n))
• Polinomial: ⋃p≥1 O(np)
o Linear: O(n)
o Pseudo-linear: O(n*log(n))
o Quadrática: O(n2)
o Cúbica: O(n3)
• Exponencial: ⋃p≥1 O(np)
• Factorial: O(n!) (n!≈√(2πn(n/e)n))

1.4. Outros Limites

Para limite inferior


Definição:
Considere f e g duas funções de domínio inteiro não negativo e
contradomínio real. Diz-se que f é Ω(g), se e só se,
∃𝑘>0 ∃𝐶>0 ∀𝑛>0 ∶ 𝑛 > 𝑘 ⇒ |𝑓(𝑛)| ≥ 𝐶 ∗ |𝑔(𝑛)|
Exemplos:
• 1000000 é de complexidade Ω(1)
• 4*m - 1 é de complexidade Ω(m)
• 5*x2 é de complexidade Ω(x)
• 5*x2 é de complexidade Ω(x2)
• 5*x2 não é de complexidade Ω(x3)
• n2*log(n) é de complexidade Ω(n2)

Caso os limites superiores e inferiores sejam iguais utiliza-se a seguinte


notação
Definição:

13
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Considere f e g duas funções de domínio inteiro não negativo e


contradomínio real. Diz-se que f é Θ(g), se e só se, f é O(g) e f é
Ω(g).
Exemplos:
• 1000000 é de complexidade Θ(1)
• 4*m - 1 é de complexidade Θ(m)
• 5*x2 não é de complexidade Θ(x)
• 5*x2 é de complexidade Θ(x2)
• 5*x2 não é de complexidade Θ(x3)
• n2*log(n) não é de complexidade Θ(n2)

Existem situações em que, pela dificuldade de calcular a classe de


complexidade O, calcula-se uma classe de complexidade alternativa, a
classe o (ó pequeno).
Definição:
Considere f e g duas funções de domínio inteiro não negativo e
contradomínio real. Diz-se que f é o(g), se e só se,
𝑓(𝑛)
lim =0
𝑛→+∞ 𝑔(𝑛)
Existe uma propriedade que afirma f é o(g) ⇒ f é O(g) estabelecendo-
se uma relação entre as duas classes de complexiadade.

1.5. Estruturas de Dados


Booleanas (lógicas)
Inteiros
Numéricas
Primitivas Reais
Caracteres alfanuméricos
Ponteiros

Estruturas de Vector
Comprimento fixo
dados Bidimensional
(variáveis indexadas)
Matriz ...
Multi-indexado
Pilhas
Não Primitivas Lineares Filas
(Complexas) Comprimento variável Listas
(Listas - adjacência física)
Árvores
Não lineares
Grafos
Ficheiros Sequenciais
(memória secundária) Sequenciais indexados
Directos
Figura 1.2. Estruturas de dados.

Lembremos que as estruturas de dados são estruturas que armazenam


e organizam dados/informações de modo que estes possam ser
acessados e manipulados de forma eficiente.
A figura 1.2 ilustra diferentes estruturas de dados. As estruturas de
dados primitivas são directamente manipuladas em linguagem máquina

14
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

(binária); e as não primitivas ou complexas representam estruturas de


informação em conjuntos (formados por estruturas de dados
primitivas) logicamente relacionados.
Para construção de um algoritmo e eficiente manipulação das
estruturas de dados para a resolução de um determinado problema,
devemos:
1) Compreender a relação entre os dados
2) Decidir operações a executar nos dados logicamente
relacionados
3) Representar os elementos dos dados de modo a:
i) Manter as relações lógicas entre os dados
ii) Executar de forma eficiente as operações nos dados
4) Construir o algoritmo e escolher a linguagem de programação
mais apropriada que permita de modo natural e expressivo
representar as operações a executar nos dados.
Pelo escopo do módulo, o presente manual abordará apenas algumas
estruturas de dados não primitivas.

1.6. Sumário

Neste tema introduzimos os conceitos básicos para a análise de


algoritmos, abordamos a notação Ó grande, descrevemos as classes de
complexidade de algoritmos mais comumente usadas e fizemos uma
breve revisão sobre as estruturas de dados.

1.7. Exercícios de Auto-Avaliação

Perguntas
1. Algoritmo é a medida de espaço que representa instruções para a
resolução de um problema.
A. Verdadeiro
B. Falso
2. A análise dos algoritmos serve para comparar o tempo de execução,
a memória utilizada, o esforço do desenvolvedor, entre outros
factores.
A. Verdadeiro
B. Falso
3. Na análise de algoritmos o pior caso define os dados de entrada para
os quais o algoritmo executa mais lentamente.
A. Verdadeiro
B. Falso
4. Na análise de algoritmos o melhor caso define os dados de entrada
que o algoritmo leva mais tempo para ser executado.
A. Verdadeiro
B. Falso

15
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

5. A notação O (ó grande) representa o limite inferior da taxa de


crescimento de uma função.
A. Verdadeiro
B. Falso

Respostas
1. B; 2. A; 3. A; 4. B; 5. B

16
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

TEMA 2. ESPECIFICAÇÃO FORMAL DE TIPOS DE DADOS ABSTRACTOS

Unidade temática 2.1. Introdução


Unidade temática 2.2. Pilhas
Unidade temática 2.3. Filas

2.1. Introdução

Lista Linear
A lista linear é uma estrutura de dados que permite representar os
elementos ou dados de um mesmo tipo de dados de forma a preservar
a relação de ordem entre si. Por exemplo, pessoas na fila de um banco;
dias da semana; itens em estoque em uma loja; etc. Cada elemento da
lista é denominado nó.
Definição: Conjunto de N nós, onde N≥0, x1, x2, …, xn, organizados de
forma a reflectir a posição relativa dos mesmos. Se N≥0,
então x1 é o primeiro nó. Para 1<k<n, o nó xk é precedido
pelo nó xk-1 e seguido pelo nó xk+1 e xn é o último nó.
Quando N=0, diz-se que a lista está vazia.

Alocação de uma Lista


1. Sequencial ou Contígua. Numa lista linear contígua os nós estão
em sequência tanto física como lógica. A utilização do vector é
um exemplo deste tipo de alocação, pois os nós são armazenados
em endereços contíguos.
2. Encadeada. Os elementos não estão necessariamente
armazenados sequencialmente na memória, porém a ordem
lógica entre os elementos que compõem a lista deve ser mantida.

Tipos de Listas Lineares


1. Pilhas (Stack). Uma pilha é uma lista linear do tipo LIFO (Last In
First Out), o último elemento que entrou, é o primeiro a sair. Na
pilha existe apenas uma entrada, o topo, a partir da qual os
dados entram e saem.
2. Filas (Queue). Uma fila é uma lista linear do tipo FIFO (First In
First Out), o primeiro elemento a entrar será o primeiro a sair.
Na fila os elementos entram por trás e saem pela frente.
3. Deques. Um deque (Double-Ended QUEue) é uma lista linear em
que os elementos entram e saem tanto pela frente como por
trás.

17
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

2.2. Pilhas (Stacks) - LIFO

2.2.1. Introdução
Ao completar esta unidade, você deverá ser capaz de:
▪ Definir e identificar as pilhas;
▪ Entender o funcionamento das pilhas, como inserir e
remover elementos, entre outros métodos.
Objectivos ▪ Saber como implementar uma pilha.
específicos

As pilhas são estruturas de dados do tipo LIFO, onde o último elemento


a ser inserido, será o primeiro a ser retirado. A pilha permite acesso
apenas ao último item de dados, isto é, para processar o penúltimo item
inserido, deve-se primeiro remover o último item.

Empilhar (push) Desempilhar (pop)

n+1
n
...
3
2
1
Figura 2.1. Pilha.

A implementação de pilhas pode ser realizada através de vector


(alocação de espaço de memória para os elementos é contigua) ou
através de listas encadeadas.
A manipulação dos elementos da pilha é realizada em apenas pela
camada de topo, dai o termo LIFO, pois o primeiro elemento fica
armazenado na base.

Operações com pilhas


• Criação da pilha – informar a capacidade no caso de
implementação sequencial
• Empilhar (push)
• Desempilhar (pop)
• Mostrar o topo
• Verificar se a pilha está vazia (isEmpty)
• Verificar se a pilha está cheia (isFull) – implementação
sequencial

18
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Empilhar (8) Empilhar (17) Empilhar (21)


4 4 4
3 3 3
2 2 2 21 topo

1 1 17 topo 1 17
topo
0 8 0 8 0 8
a) b) c)
Figura 2.2. Empilhamento.
Desempilhar () Desempilhar ()
4 4 4
3 3 3
2 21 topo 2 2
1 17 1 17 topo 1
0 8 0 8 0 8 topo

a) b) c)
Figura 2.3. Desempilhamento.

2.2.2. Especificação da Pilha


A representação de uma pilha é definida recursivamente a partir de
duas operações (construtores): empty que representa a pilha vazia e
push que representa uma pilha constituída por uma informação (de
qualquer tipo Element) no topo e uma pilha restante.
As outras operações com pilhas
• isEmpty - Verificar se a pilha está vazia
• top – devolve o elemento que está no topo da pilha
• pop – devolve a pilha sem o topo
• equals – verifica se duas pilhas são iguais

A seguir o tipo de dados abstrato da pilha:


especificação Stack<Element> =
importa Boolean
géneros Stack
operações
empty : → Stack
push : Stack Element → Stack
isEmpty : Stack → Boolean
top : Stack ↛ Element
pop : Stack →/ Stack
equals : Stack Stack → Boolean
axiomas
isEmpty(empty) = TRUE
isEmpty(push(S, I)) = FALSE

19
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

top(push(S, I)) = I
pop(push(S, I)) = S
equals(S1, S2) =
if isEmpty (S1)
then isEmpty(S2)
else not isEmpty(S2) and
(equals(top(S1), top(S2)) and
equals(pop(S1), pop(S2)))
pré-condições
top(S) requer not isEmpty(S)
pop(S) requer not isEmpty(S)
fim-especificação

Duas pilhas são iguais se e só se são ambas vazias ou contêm os mesmos


elementos pela mesma ordem.

2.2.3. A Interface da Pilha


Construção da interface que se segue (no ficheiro Stack.java):
package dataStructures;
/** A interface do TDA Pilha. O desenho da pilha com contractos. */
public interface Stack {
//@ public invariant !isEmpty() ==> top() != null;
/** Criar uma pilha vazia
* @return uma pilha vazia. */
//@ ensures \result.isEmpty();
/*@ pure @*/ Stack empty();
/** Inserir um elemento no topo da pilha
* @param item A referência do elemento a inserir no topo. */
/*@ requires item != null;
@ requires !isEmpty() ==>item.getClass() == top().getClass();
@ ensures top().equals(item);
@*/
void push(Object item);
/** A pilha está vazia?
* @return TRUE se está vazia, FALSE c.c. */
/*@ pure @*/ boolean isEmpty();
/** Devolver o topo da pilha
* @return a referência para o elemento no topo da pilha. */
//@ requires !isEmpty();
/*@ pure @*/ Object top();
/** Remover o topo de uma pilha. */
//@ requires !isEmpty();
//@ ensures (POP(PUSH(S, I)) = S *);
void pop();
/** Serão as duas pilhas iguais?
* @param s A pilha a ser comparada
* @return TRUE se forem iguais, FALSE c.c. */
//@ also
//@ requires s != null;

20
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

/*@ pure @*/ boolean equals(Object s);


/** Devolver uma cópia da estrutura.
* @return a Referência para a cópia. */
//@ also
//@ ensures (\result != this) && equals(\result);
/*@ pure @*/ Object clone();
} // endInterface Stack

Interface 2.1. Interface da Pilha.

Define-se uma invariante a ser satisfeita por qualquer objecto deste


tipo: não é possível armazenar referências nulas.
Na pós-condição do construtor push garantimos que uma vez concluída
a execução do método, o elemento está no topo da pilha (como
especificado no TDA). A segunda pré-condição verifica se o novo
elemento é do tipo dos elementos armazenados. A estrutura de dados
é polimórfica (dado guardar elementos de qualquer classe deriva de
Object) mas passa a estar restrita a elementos do mesmo tipo.
O comentário sobre a pós-condição do método não é verificável porque
a asserção a ser avaliada produziria um efeito secundário de modificar
o próprio objecto.

2.2.4. Implementação Estática da Pilha


Esta implementação usa uma representação estática de informação, o
vector, para armazenar os dados da pilha. Este vector está relacionado
com um atributo inteiro (que é incrementado ou decrementado quando
inserido ou removido um elemento) que determina o topo actual.

package dataStructures;
/** Uma implementação vectorial da pilha */
public class VStack implements Stacks, Cloneable {
private final int DELTA = 128;
private Object[] stack;
private int top;
public VStack(int cap) {
stack = new Object[DELTA];
top = -1;
}
public Stack empty() {
return new VStack();
}
public boolean isEmpty() {
return top = -1;
}
public void push(Object item) {
if (top+1 == stack.length)
grow();

21
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

top++;
stack[top] = item;
}
public void grow() {
Object[] newStack = new Object[stack.length + DELTA];
for (int i=0; i<stack.length; i++)
newStack[i] = stack[i];
stack = newStack;
}
public Object top() {
return stack[top];
}
public void pop() {
stack[top--] = null;
}
public boolean equals(Object s) {
if (!(s instanceOf Stack))
return false;
int i=top;
Stack cp=(Stack) (((Stack) s).clone());
while (!cp.isEmpty()) {
if (i<0 || !stack[i].equals(cp.top()))
return false;
cp.pop();
i--;
}
return i<0;
}
public Object clone() {
VStack cp = new VStack;
cp.stack = new Object[stack.length];
for (int i=0; i<=top; i++)
cp.stack[i] = stack[i];
cp.top = top;
return cp;
}
public String toString() {
if (this.isEmpty())
return “[ ]>”;
StringBuffer result = new StringBuffer(“[“ + stack[0]);
for (int i=0; i<=top; i++)
result.append(“, ” + stack[i]);
return result.toString() + “]>”;
}
} // endClass VStack
Classe 2.1. Implementação estática da Pilha.

Segundo a convenção, a classe denomina-se VStack por se tratar de


uma implementação estática do tipo Stack.

22
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

A configuração da classe é constituída por três atributos: (i) uma


constante DELTA inteira que define a dimensão do vector bem como o
seu eventual crescimento; (ii) um vector de referências stack para os
objectos que a pilha armazena; e (iii) o atributo inteiro top que indica o
índice da referência para o objecto do topo.
Existem os atributos que definem a configuração da classe e cujos
valores determinam os estados dos objectos (stack e top) e os úteis na
construção da classe que não fazem parte da configuração (DELTA).
Inicializa-se a configuração do objecto para descrever uma pilha vazia.
O atributo do topo é negativo quando não existem objectos
armazenados, pois por convenção este valor será utilizado na
interrogação isEmpty():
A inserção de um novo elemento passa primeiro por conferir se a
capacidade actual da pilha é sufuciente. Se houver espaço, basta
incrementar o atributo top e inserir a referência do objecto na posição
do vector (o novo objecto do topo). Se não houver espaço, o método
vai reservar mais espaço através do método privado grow() criando um
vector maior para onde se copia o conteúdo da pilha.
No método pop() é conveniente colocar as posições não utilizadas a um
valor nulo. Caso contrário, enquanto o vector da pilha não fosse
ocupado por nova informação, os objectos anteriormente armazenados
na pilha ficariam com uma referência que impediria a sua eventual
libertação pela recolha de lixo.
Como não se deve alterar a pilha dada, faz-se uma cópia da mesma para
se remover os elementos um por um. Para cada elemento removido
deve-se verificar se os elementos na mesma posição são iguais,
utilizando o método equals() dos objectos armazenados. Se uma dessas
comparações for falsa, ou as dimensões das pilhas forem diferentes, o
método devolve falso. Caso contrário, o método devolve verdadeiro.
Para clonar uma pilha, constrói-se uma nova pilha com a mesma
capacidade e armazenam-se apenas as referências dos objectos na nova
pilha, não se duplicam os objectos. Pois clonar tudo acarreta enormes
recursos, enquanto que clonar apenas referências custa sempre o
mesmo, por exemplo, em sistemas de 32 bits cada referência ocupa 32
bits.
A representacao textual é semelhante à utilizada na lista, mas adiciona-
se o caracter ‘>’ como indicação do topo da pilha.

2.2.5. Uso da Classe VStack


No exemplo que se segue, as duas pilhas criadas armazenam referências
para objectos da mesma classe Integer:
public static void main (String[] args) {
VStack v1 = new VStack(), v2 = new VStack();
v1.push(new Integer(3));
v1.push(new Integer(4));
v2.push(new Integer(3));

23
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

v2.push(new Integer(4));
v1.pop();
System.out.println(v1 + “ ” + v2.clone());
System.out.print(v1.equals(v2) ? “==” : “!=”);
}
Programa 2.1. Uso da classe VStack.

⎕ [3]> [3, 4]> ⤶


!=

2.2.6. Sumário
Nesta unidade temática definimos a pilha, detalhamos o seu
funcionamento, assim como os métodos defindos pelo tipo de dados
abstracto. Abordamos ainda a implementação estática da pilha e o uso
da classe VStack.

2.2.7. Exercícios de Auto-Avaliação

Perguntas
1. Tanto as pilhas como as listas são listas lineares.
A. Verdadeiro
B. Falso
2. Enquanto na pilha o último elemento a entrar é o primeiro a sair, na
fila o último a sair é o primeiro que entrou.
A. Verdadeiro
B. Falso
3. Numa pilha os elementos são inseridos e retirados apenas pela
camada de topo.
A. Verdadeiro
B. Falso
4. A pilha é definida recursivamente a partir dos construtores empty()
e equals().
A. Verdadeiro
B. Falso
5. Na implementação estática da pilha, o método insert() incrementa
o atributo top e insere a referência do objecto na posição do vector.
A. Verdadeiro
B. Falso

Respostas
1. A; 2. A; 3. A; 4. B; 5. A

24
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

2.3. Filas (Queue): FIFO

2.3.1. Introdução
Ao completar esta unidade, você deverá ser capaz de:
▪ Identificar os diferentes tipos de filas
▪ Entender o funcionamento das filas, como inserir e
remover elementos, entre outros métodos.
Objectivos ▪ Saber como implementar as filas.
Específicos
As filas são estruturas de dados do tipo FIFO (First-In First-Out), onde o
primeiro elemento a ser inserido, será o primeiro a ser retirado, isto é,
adicionam-se os itens duma extremidade (fim) e são removidos doutra
extremidade (início).
Remove Insere

1 2 3 ... n n+1
Figura 2.4. Fila.

Alguns exemplos de filas incluem controle de documentos para


impressão; troca de mensagens entre computadores numa rede; entre
outros.
A implementação de filas pode ser realizada através de vector ou de
listas encadeadas.

2.3.2. Especificação da Fila


A representação de uma fila é definida recursivamente a partir de duas
operações (construtores): empty que representa a fila vazia e enqueue
que representa uma fila constituída por uma informação (de qualquer
tipo Element) situada no fim e a restante fila.
As outras operações definidas sobre o tipo de dados abstracto fila são:
• isEmpty - Verifica se a fila está vazia
• front – devolve o elemento que está na frente da fila
• dequeue – devolve a fila sem o elemento inicial
• equals – verifica se duas filas são iguais
A seguir o tipo de dados abstrato:
especificação Queue<Element> =
importa Boolean
géneros Queue
operações
empty : → Queue
enqueue : Queue Element → Queue
isEmpty : Queue → Boolean
front : Queue →/ Element
dequeue : Queue →/ Queue
equals : Queue Queue → Boolean
axiomas

25
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

isEmpty(empty) = TRUE
isEmpty(enqueue(Q, I)) = FALSE
front(enqueue(Q, I)) =
if isEmpty(Q)
then I
else front(Q)
dequeue(enqueue(Q, I)) =
if isEmpty(Q)
then empty
else enqueue(dequeue(Q), I)
equals(Q1, Q2) =
if isEmpty (Q1)
then isEmpty(Q2)
else not isEmpty(Q2) and
(equals(front(Q1), front(Q2)) and
equals(dequeue(Q1), dequeue(Q2)))
pré-condições
front(S) requer not isEmpty(Q)
dequeue(S) requer not isEmpty(Q)
fim-especificação

As operações front() e dequeue() devem aceder o primeiro elemento a


inserido, pois na fila funciona o FIFO.

2.3.3. A Interface da Fila


Construção da interface que se segue (no ficheiro Queue.java):
package dataStructures;
/** A interface do TDA Fila. O desenho da fila com contractos. */
public interface Queue {
//@ public invariant !isEmpty() ==> front() != null;
/** Criar uma fila vazia.
* @return uma pilha vazia. */
//@ ensures \result.isEmpty();
/*@ pure @*/ Queue empty();
/** Inserir um elemento no fim da fila.
* @param item A referência do elemento a inserir no fim. */
/*@ requires item != null;
@ requires !isEmpty() ==> item.getClass() == front().getClass();
@ ensures !isEmpty();
@ ensures \old(isEmpty()) ==> front().equals(item);
@ ensures !(\old(isEmpty())) ==> front().equals(\old(front()));
@*/
void enqueue(Object item);
/** A fila está vazia?
* @return TRUE se está vazia, FALSE c.c. */
/*@ pure @*/ boolean isEmpty();
/** Devolver o primeiro elemento da fila.
* @return a referência do elemento na frente da fila. */
//@ requires !isEmpty();

26
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

/*@ pure @*/ Object front();


/** Remover o primeiro elemento da fila. */
//@ requires !isEmpty();
/*@ ensures (* dequeue(enqueue(Q, I)) =
@ if isEmpty(Q) then empty
@ else enqueue(dequeue(Q), I) *);
@*/
void dequeue();
/** Serão as duas filas iguais?
* @param q A fila a ser comparada
* @return TRUE se forem iguais, FALSE c.c. */
//@ also
//@ requires q != null;
/*@ pure @*/ boolean equals(Object q);
/** Devolver uma cópia da estrutura.
* @return a referência para a cópia. */
//@ also
//@ ensures (\result != this) && equals(\result);
/*@ pure @*/ Object clone();
} // endInterface Queue
Interface 2.2. A Interface da Fila.

Para além da pré-condição referente a invariante (não permite a


inserção de referências nulas), o construtor garante que nas pós-
condições que: (i) a fila não fica vazia; (ii) se a fila está vazia, o elemento
inserido encontra-se na frente da fila; e (iii) se a fila não estava vazia, o
elemento na frente da lista não se alterou com a execução do método.

2.3.3. Implementação Estática da Fila


Esta implementação usa uma representação estática de informação, o
vector, para armazenar os dados da fila. Nesta representação utiliza-se
um atributo para indicar begin (o índice do vector onde se encontra o
primeiro objecto do vector) e outro para indicar o end (o índice do
vector onde se encontra o último objecto).

27
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

begin: 0 end: -1 begin: 0 end: 0


A
a) Fila vazia b) Inserir A

begin: 0 end: 1 begin: 0 end: 2


A B A B C
c) Inserir B d) Inserir C

begin: 1 end: 2 begin: 2 end: 2


B C C
e) Remover a frente f) Remover a frente

begin: 3 end: 2 begin: 3 end: 3


D
g) Remover a frente h) Inserir D
Figura 2.5. Implementação estática de uma fila.

• Quando se insere um elemento incrementa-se o end.


• Quando se remove um elemento incrementa-se o begin.
• A fila está vazia quando o valor do end é igual ao valor do begin
menos 1.
Na figura 2.5, depois de todas as inserções e remoções, os valores do
begin e do end são iguais a 3. Como o vector tem a dimensão de seis
posições, metade da capacidade já está inutilizada (os índices de 0 a 2).
Para resolver esta situação, convencionou-se que se num incremento
dos atributos begin ou end, o seu valor for igual ao último índice, o
próximo valor desse atributo é zero. Esta convenção torna possível
aproveitar todo o espaço, pois o vector é visto como um vector circular.
0 1
7 2

6 3

5 4

Figura 2.6. Representação de um vector circular.

Numa fila cheia, o valor do end é igual ao valor do begin menos um


(depois de dada a volta inteira). Para distinguir a fila cheia da fila vazia
existem duas opções: (i) deixar uma célula do vector por usar
(diminuindo a capacidade da fila) ou (ii)

28
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

usar um terceiro atributo (o número de elementos da fila) para


distinguir as duas situações, aumentando assim a configuração dos
objectos.
Para a presente implementação, optou-se pela opção (i), deixando por
usar a célula imediatamente antes da primeira informação. Assim
sendo, a fila está cheia quando o fim estiver a uma célula de distância
do princípio.

package dataStructures;
/** Uma implementação vectorial da fila. Descricao: A implementação usa
um vector circular. */
public class VQueue implements Queue, Cloneable {
private final int DELTA = 128;
private Object[] queue;
private int begin, end;
public VQueue() {
queue = new Object[DELTA];
begin = 0;
end = queue.length - 1;
}
public Queue empty() {
return new VQueue();
}
public boolean isEmpty() {
return (end + 1) % queue.lenght == begin;
}
public void grow() {
Object[] newQueue = new Object[queue.length + DELTA];
for (int i = begin; i != (end + 1) % queue.length;
i = (I + 1)queue.length, j++)
newQueue[j] = queue[i];
begin = 0;
end = queue.length - 2;
queue = newQueue;
}
public void enqueue(Object item) {
stack[top--] = null;
if ((end + 2) % queue.length == begin)
grow();
end = (end + 1) % queue.length;
queue[end] = item;
}
public Object front() {
return queue[begin];
}
public void dequeue() {
queue[begin] = null;
begin = (begin + 1) % queue.length;
}

29
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

public boolean equals(Object q) {


if (!(q instanceOf Queue))
return false;
int actual = begin;
Queue cp = (Queue) (((Queue) q).clone());
while (!cp.isEmpty()) {
if (actual == (end+1) % queue.length ||
!queue[actual].equals(cp.front()))
return false;
cp.dequeue();
actual = (actual + 1) % queue.length;
}
return actual = (end + 1) % queue.length;
}
public Object clone() {
VQueue copy = new VQueue;
copy.queue = new Object[queue.length];
copy.begin = begin;
copy.end = end;
for (int i = 0; i <= queue.length; i++)
copy.queue[i] = queue[i];
return copy;
}
public String toString() {
if (this.isEmpty())
return “<[ ]<”;
StringBuffer result = new StringBuffer(“<[“ + queue[begin]);
for (int i = (begin + 1) % queue.length; i != (end + 1) % queue.length; i
= (i + 1)queue.length)
result.append(“, ” + queue[i]);
return result.toString() + “]<”;
}
} // endClass VQueue
Classe 2.1. VQueue

Se o begin é zero, o end seria -1. Porém, numa aritmética de módulo


queue.length, o predecessor de zero é queue.length–1. isEmpty().
No método enqueue() é incrementado o valor de end e armazenada a
referência. A actualização de end é actualizada em aritmética módulo
queue.length. Esta aritmética é implementada através do operador %
que devolve o resto de divisão inteira.
No método dequeue() a remoção de um elemento faz-se actualizando
o valor do begin (aumentar uma unidade em aritmética linear) e
decrementando o número de elementos.
A comparação entre dois objectos do tipo Queue (não VQueue) passa
pela criação de um clone do argumento, de onde são retirados todos os
elementos que se comparem com os elementos da estrutura interna do
objecto que invocou o método.

30
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

O método clone() apenas percorre todos os elementos armazenados na


fila, pois copiam-se apenas as referências e não os objectos.
A representação textual da fila é semelhante à utilizada na lista, mas
com o caracter ‘<’ à esquerda a indicar o início da fila e o à direita a
marcar o fim.

2.3.4. Uso da Classe VQueue


O exemplo que se segue refere-se ao uso desta classe:
public static void main (String[] args) {
VQueue v1 = new VQueue (), v2 = new VQueue ();
v1.enqueue(new Integer(5));
v1.enqueue (new Integer(4));
v1.enqueue (new Integer(3));
v2.enqueue(new Integer(5));
v2.enqueue(new Integer(4));
v2.enqueue(new Integer(3));
System.out.println(v1.equals(v2) ? “v1==v2” : “v1!=v2”);
v1.dequeue();
VQueue v3 = (VQueue)v1.clone();
v1.dequeue();
System.out.println(v1 + “ ” + v2 + “ ” + v3);
}
Programa 2.2
⎕ v1==v2 ⤶
<[3]< <[5, 4, 3]< <[4, 3]< ⤶

2.3.5. Filas de Espera com Prioridade


Geralmente, objectos diferentes da fila podem possuir prioridades
diferentes, não sendo inseridos no fim da fila. Tais como os carros
prioritários no código da estrada; os acentos para mulheres grávidas,
idosos e crianças no colo em transportes públicos; entre outros. Para
modelar este tipo de situações é conveniente utilizar uma fila de espera
com prioridade (priority queue).
Nas filas de espera com prioridade, cada objecto vem associado à um
valor que define a sua prioridade, permitindo que este ultrapasse todos
os objectos com prioridade inferior até encontrar algum com igual ou
maior prioridade.
Deve-se evitar que um dado objecto de menor prioridade fique retido
indefinidamente na fila (starvation) devido à constante chegada de
elementos com prioridade mais elevada. Para tal existem duas
possíveis:
• Define-se que existe um tempo máximo de espera, que quando
esgotado, o objecto é imediatamente processado.
• A cada período de espera, os objectos aumentam de prioridade.
Garantindo que ao fim de um intervalo suficiente o objecto seja
tratado devido a sua alta prioridade (é conveniente existir uma
prioridade máxima)

31
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

2.3.6. Sumário
Nesta unidade temática vimos como identificar os diferentes tipos de
filas. Detalhamos o funcionamento da fila, assim como os métodos
defindos pelo seu tipo de dados abstracto. Abordamos a
implementação estática da fila e o uso da classe VQueue. Fizemos ainda
algumas considerações sobre as filas com prioridade.

2.3.7. Exercícios de Auto-Avaliação

Perguntas
1. A figura que se segue representa uma fila.
push pop

n+1
n
...
3
2
1
A. Verdadeiro
B. Falso
2. Enquanto na pilha o último elemento a entrar é o primeiro a sair, na
fila o último a sair é o primeiro que entrou.
A. Verdadeiro
B. Falso
3. Na implementação estática de uma fila (vector), diz-se que esta está
vazia quando end = begin - 1.
A. Verdadeiro
B. Falso
4. Na implementação estática da fila, caso não haja espaço suficiente
para inserir um novo elemento, utiliza-se o método privado grow()
para reservar mais espaço criando um vector maior para onde se
copia o conteúdo da fila.
A. Verdadeiro
B. Falso
5. dequeue() é um construtor da fila que devolve a fila sem o elemento
inicial.
A. Verdadeiro
B. Falso

Respostas
1. B; 2. A; 3. A; 4. B; 5. B

32
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

TEMA 3. LISTAS LIGADAS

Unidade temática 3.1. Introdução


Unidade temática 3.2. Listas ligadas ou simples
Unidade temática 3.3. Listas duplamente ligadas
Unidade temática 3.4. Listas skip

3.1. Introdução

As estruturas de dados apresentam diversas formas alternativas de


utilização da memória do computador, como apresentados nas figuras
3.1 à 3.3.
Elemento Elemento Elemento Elemento Elemento Elemento

Figura 3.1. Implementação estática de um conjunto de elementos.

Elemento Elemento Elemento

Figura 3.2. Implementação semiestática de um conjunto de elementos.

Cabeça
da lista
Elemento Elemento Elemento

Figura 3.3. Implementação dinâmica de um conjunto de elementos.

Na implementação estática a dimensão é fixa e os elementos


apresentam-se de forma sequencial, estando os elementos úteis em
posições contíguas da sequência (os elementos sombreados na figura
3.1). Normalmente os tipos de dados são primitivos e eliminação de um
elemento pode exigir a deslocação de alguns elementos da sequência.
A implementação semiestática é baseada numa sequência de
referências para os elementos, sendo depois criados quando
necessário. A inserção e eliminação de elementos pode exigir de
algumas referências. Porém, é menos onerosa em tempo de execução
pois uma referência ocupa menos espaço que o próprio elemento.
A implementação dinâmica é baseada na combinação elementos e de
referências numa organização denominada lista ligada (linked list). A
dimensão da lista é inicialmente nula e depois vai crescendo e
diminuindo à medida em que os elementos são criados ou destruídos.
A inserção de um novo elemento na lista exige a criação do respectivo
nó e da sua ligação à lista, por ajuste das ligações aos elementos
contíguos. A eliminação de um elemento implica a desligação do
respectivo nó da lista, por ajuste das ligações aos elementos contíguos.
A lista é acedida através de uma referência para o início da lista (list

33
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

head). Pode também existir uma referência para o fim ou cauda da lista
(list tail).

3.2. Listas Ligadas ou Simples

3.2.1. Introdução
Ao completar esta unidade, você deverá ser capaz de:
▪ Definir e identificar uma lista ligada ou simples;
▪ Conhecer os algoritmos básicos das listas ligadas ou
simples;
Objectivos ▪ Ententer a implementação genérica das listas ligadas ou
Específicos simples.

As listas ligadas são estruturas de dados dinâmicas para


armazenamento de colecção de dados e tem as seguintes propriedades
• Elementos sucessivos estão ligados através de ponteiros
• O último elemento aponta para nulo
• O seu tamanho pode incrementar ou decrementar durante a
execução
• Pode ser tão grande quanto necessário (até esgotar a memória
do sistema)
• Não desperdiça memória, embora utilize memória extra para os
ponteiros. Aloca memória enquanto a lista cresce.
As operações com as listas ligadas incluem
• Inserir: insere um elemento na lista
• Apagar: remove e devolve a posição do elemento específico a
lista
• Apagar Lista: apaga todos os elementos da lista
• Contar: devolve o número de elementos da lista
• Encontrar o n-ésimo nó do final da lista

A figura 3.4 ilustra uma lista ligada ordenada por ordem crescente. O
último nó aponta para null, indicação do fim da lista.
Cabeça 20 30 40
Figura 3.4. Lista ligada.

A figura 3.5 ilustra os dois tipos que os nós da lista ligada podem ser. A
esquerda representa um nó compacto que armazena um elemento de
um tipo de dados primitivo, sendo cada nó constituído pela referência
para o nó seguinte da lista (atributo next) e pelo elemento que se
pretende armazenar na lista (atributo elem). Na direita temos um nó
decomposto que armazena um elemento de um tipo de dados
referência, sendo cada nó constituído por uma referência para o nó
seguinte e outra para o elemento que se pretende armazenar na lista.

34
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Esta versão permite construir uma lista ligada genérica.

next Node next; next


elem tipo elem;
elem
Figura 3.5. Nós da lista ligada.

Para implementar uma classe do tipo de dados List (lista), precisamos


implementar uma classe do tipo de dados Node (nó da lista), como
apresentado na classe 3.1. O construtor do nó copia o parâmetro para
o atributo elem e coloca o nó no final da lista. Note que se o tipo dos
elementos da lista for um tipo referência, então o objecto já foi criado
e a cópia da sua referência para o atributo elem liga-o ao nó da lista.
public class List {
private static class Node { // classe privada de nós da lista
public Node next; // atributo para o nó seguinte da lista
public int elem; // atributo para o elemento a armazenar na lista
public Node(int pVal) { // construtor do nó da lista
elem = pVal;
next = null;
}
}
private Node head; // atributo cabeça da lista
public List() { // construtor da lista
head = null;
}
public boolean isEmpty() { // método da lista vazia
return head == null;
}
… // métodos públicos e privados da lista
}
Classe 3.1. Esqueleto da classe lista ligada para armazenar inteiros.

A classe List tem os atributos


• head para representar a cabeça da lista
• tail representa a cauda da lista
• nelem indica o número de elementos.
Os métodos
• isEmpty() detecta se a lista está ou não vazia, verificando se a
cabeça da lista é ou não uma referência nula
• isFull() verifica se a lista está cheia.

3.2.2. Algoritmos Básicos das Listas Ligadas


O método search() percorre sequencialmente a lista a partir do seu
início até encontrar o elemento pretendido, ou até ser ultrapassado o
fim da lista. O algoritmo pode ser
35
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

optimizado para parar a pesquisa assim que detectar que o elemento


não se encontra na lista, desde que se tenha a certeza que se está
perante uma lista ordenada.
public boolean search(int pElem) {
for (Node node = head; node != null; node = node.next)
if (pElem == node.elem) // pesquisa com sucesso
return true;
return false; // pesquisa sem sucesso
}
Algoritmo 3.1. Pesquisa na lista.

O algoritmo 3.1 apresenta a pesquisa de um elemento inteiro na lista.


Se o tipo dos elementos for um tipo referência, então a comparação
entre o elemento da lista e o elemento procurado pElem é feita como
método compareTo ou outro método de comparação consoante o
critério de colocação dos elementos na lista. E a função devolve a
função da referência do elemento node.elem ou a referência nula, caso
ele não exista na lista.
A pesquisa na lista tem a complexidade linear, ordem O(n). Embora a
pesquisa na lista seja um problema de recursividade, ela é
implementada de maneira repetitiva para não esgotar a memória pilha
do sistema.
O algoritmo 3.2 apresenta o método toString() que cria uma sequência
de caracteres com o conteúdo da lista, sendo os elementos separados
pelo caracter tabulador. Em caso do tipo dos elementos for um tipo
referência, este método retornará a sua representação textual.

public String toString() {


if (isEmpty()) // teste de lista vazia
return “Lista vazia\n”;
String str = “Lista Ligada : ”;
for (Node node = head; node != null; node = node.next)
str += “\t” + node.elem;
return str + “\n”;
}
Algoritmo 3.2. Impressão do conteúdo da lista.
A figura 3.6 apresenta três exemplos de inserção de novos elementos
numa lista ordenada por ordem crescente. Para inserir um elemento é
preciso criar o nó com o elemento e determinar a referência do
elemento à frente do qual se vai inserir o novo elemento.
x
Cabeça 4 20 30 50
3

ins 10 prev 2

a)

36
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Cabeça 20 30 x
50
3
4
prev 40
ins

b)

x
Cabeça 20 30 50
3
4
prev 60
ins

c)
Figura 3.6. Inserção de elementos na lista ordenada por ordem crescente.

Na figura 3.6.a), para inserir o elemento 10, deve-se primeiro pesquisar


a posição de inserção, no caso retorna referência null por o elemento a
inserir ser menos que os elementos já existentes na lista.
Consequentemente o elemento será colocado à cabeça da lista e
actualizam-se as referências. Se a lista estiver vazia, ele é adicionado e
a referência aponta para null, indicando este ser o único elemento da
lista.
Na figura 3.6.b), para inserir o elemento 40, ao pesquisar a posição da
inserção, detecta-se que o novo elemento deve ser colocado entre os
elementos 30 e 50. Depois actualizam-se as referências.
Na figura 3.6.c), para inserir o elemento 60, detecta-se este ser maior
que todos existente, logo este é colocado no fim da lista a apontar para
null.
O algoritmo 3.3 apresenta a inserção numa lista ordenada por ordem
crescente. No final da pesquisa, actual aponta para o elemento maior
ou igual ao novo elemento e prev aponta para o elemento anterior (o
menor que o novo elemento). Caso o elemento seja repetido, o novo é
colocado por detrás do primeiro igual a ele.

public void insert(int pElem) {


Node ins = new Node(pElem);
// pesquisar a posição do elemento
Node actual = head;
Node prev = null;
while (actual != null && pElem > actual.elem) {
prev = actual;
actual = actual.next;
}
if (prev == null) { // inserir na cabeça da lista
ins.next = head;
head = ins;
} else { // inserir à frente de um elemento
ins.next = actual;
prev.next = ins;

37
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

}
}
Algoritmo 3.3. Inserção na lista ordenada por ordem crescente.

Se o tipo dos elementos for um tipo referência, então a comparação


entre o elemento da lista e o elemento a inserir pElem é feita com o
método compareTo ou outro método de comparação consoante o
critério de colocação dos elementos da lista.
A figura 3.7 apresenta três exemplos de remoção de elementos de uma
lista ordenada por ordem crescente. Para remover um elemento da lista
deve-se determinar as referências desse elemento e do que se encontra
por detrás dele, para poder liga-lo ao que está a frente do que se
pretende remover.
4
Cabeça X X
10
3
X 20 30

del prev 2

a)
3
Cabeça 10 x 20 X
4
x 30

prev del

b)

3
Cabeça 10 20 x 30 X
4
x

prev del

c)
Figura 3.7. Remoção de elementos na lista ordenada por ordem crescente.

Na figura 3.7.a) para remover o elemento 10, não precisa procurar a


posição de remoção, apenas actualiza-se a cabeça para apontar para o
próximo elemento da lista. Se a lista tiver apenas um elemento, então
coloca-se a cabeça a apontar para null.
Na figura 3.7.b) para remover o elemento 20, detecta-se a posição do
elemento a ser removido e elemento seguinte. Depois remove-se o
elemento pretendido e coloca-se o anterior ao removido a apontar para
o em frente do elemento a ser removido (detectado anteriormente).
Na figura 3.7.c) para remover o último elemento, após detectar que ele
é o último elemento, remove-se e coloca-se o anterior a apontar para
null, indicando que passa a ser o último elemento. O nó desligado da
lista deixa de estar referenciado, libertando assim a memória ocupada
pelo nó e sua respectiva referência.

38
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

O algoritmo 3.4 apresenta a remoção de um elemento de uma lista


ordenada por ordem crescente. Inicialmente verifica-se se trata-se de
uma lista vazia, caso sim, termina, caso contrário determina a posição
do elemento na lista utilizando as referências actual e prev. Quando a
pesquisa termina o actual aponta para o elemento a ser removido e o
prev aponta para o elemento anterior, excepto se o elemento estiver à
cabeça da lista. Se o elemento estiver à cabeça da lista, então actualiza-
se a cabeça da lista, senão o elemento anterior é ligado ao elemento
seguinte. Caso o elemento não exista na lista, nada é feito.

public void delete(int pElem) {


if (head == null) // teste de lista vazia
return;
// pesquisar a posição do elemento
Node del = head;
Node prev = null;
while (del != null && pElem != del.elem) {
prev = del;
del = del.next;
}
if (del == head) // se o elemento existir removê-lo
head = head.next; // remover o nó da cabeça
else if (del != null) { // remover outro nó
prev.next = del.next;
}
Algoritmo 3.4. Remoção na lista ordenada por ordem crescente.

Se o tipo de elementos for um tipo referência, então a comparação


entre o elemento da lista e o elemento a remover pElem é feita com o
método compareTo ou outro método de comparação, consoante o
critério de colocação dos elementos na lista.
Quando uma lista armazena elementos agregados, remover o elemento
da lista significa desligar o seu nó com vista à libertação da memória por
ele ocupada e devolver o elemento. Nesse caso, utiliza-se uma função
que devolve a referência do elemento actual.elem ou referência nula.
Para reinicializar a lista, método clear, coloca-se a cabeça da lista a
apontar para null. Caso a lista tenha um contador de elementos, este
deve ser reinicializado.

3.2.3. Implementação Genérica da Lista Simples


A Classe 3.2 apresenta a lista ligada genérica parametrizada GenericList.
Considerando tratar-se de uma lista ordenada, os seus elementos têm
de ser comparáveis, permitindo assim a utilização do método
compareTo. Devemos definir um tipo de dados genérico comparável <T
extends Comparable<T>>.

39
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

public class GenericList<T extends Comparable<T>> {


private static class Node { // classe privada de nós da lista
public Node<T> next; // atributo para o nó seguinte
public T elem; // atributo para o elemento a armazenar no nó
public Node(T pVal) { // construtor do nó da lista
elem = pVal;
next = null;
}
}
private Node<T> head; // atributo cabeça da lista
public GenericList() { // construtor da lista
head = null;
}
public boolean isEmpty() { // método da lista vazia
return head == null;
}
public T search(T pElem) { // método de pesquisa na lista
for (Node<T> node = head; node != null; node = node.next)
if (pElem.compareTo(node.elem) == 0)
return node.elem; // pesquisa com sucesso
return null; // pesquisa sem sucesso
}
public void insert(T pElem) { // método de inserção
Node<T> ins = new Node<T>(pElem);
Node<T> actual = head;
Node<T> prev = null; // pesquisar o elemento
while (actual != null && pElem.compareTo(actual.elem) > 0) {
prev = actual;
actual = actual.next;
}
if (prev == null) { // inserir na cabeça da lista
ins.next = head;
head = ins;
} else { // inserir à frente de um elemento
ins.next = actual;
prev.next = ins;
}
}
public T delete(T pElem) { // método de remoção
if (head == null) // teste de lista vazia
return null;
Node<T> del = head;
Node<T> prev = null; // pesquisar o elemento
while (prev != null && pElem.compareTo(del.elem) != 0) {
prev = del;
del = del.next;
}
// se o elemento existir remover o elemento actual da lista
if (del == head)
head = head.next; // remover da cabeça

40
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

else if (del != null)


prev.next = del.next;
// devolução da referência do elemento ou da referência nula
if (del != null)
return del.elem; // remoção com sucesso
else
return null; // remoção sem sucesso
}
public String toString() { // método de impressão
if (isEmpty()) // teste de lista vazia
return “Lista vazia\n”;
String str = “Lista Ligada : ”;
for (Node<T> node = head; node != null; node = node.next)
str += “\t” + node.elem.toString();
return str + “\n”;
}
}
Classe 3.2. Tipo de dados abstrato GenericList.

O programa 3.1 apresenta a simulação de lista genérica com elementos


inteiros recorrendo a classe de embrulho Integer. A declaração do tipo
de dados da variável list e a invocação do construtor é
GenericList<Integer>.

import static pt.ua.prog.WIO.*


public class TestGenericList {
private static void main(String[] args) {
Integer val;
Integer ret;
// criar a lista genérica vazia para armazenar elementos Integer
GenericList<Integer> list = new GenericList<Integer>();
printf(“%s”, list); // escrita da lista inicial vazia
// inserir elementos e escrita da lista após cada inserção
printf(“Inserir elementos na lista [0 para parar]\n”);
val = readInt(“Valor? ”);
while (val != 0) {
list.insert(val);
printf(“%s”, list);
val = readInt(“Valor? ”);
}
// pesquisar elementos e escrita do valor pesquisado no monitor
printf(“Pesquisar elementos na lista [0 para parar]\n”);
val = readInt(“Valor? ”);
while (val != 0) {
ret = list.search(val);
if (ret == null)
printf(“O %s não está na lista\n”, val);
else
printf(“O %s está na lista\n”, ret);

41
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

val = readInt(“Valor? ”);


}
// remover elementos e escrita da lista após cada remoção
printf(“Remover elementos na lista [0 para parar]\n”);
val = readInt(“Valor? ”);
while (val != 0) {
ret = list.delete(val);
if (ret == null)
printf(“O %s não está na lista\n”, val);
else
printf(“O %s foi retirado da lista\n”, ret);
printf(“%s”, list);
val = readInt(“Valor? ”);
}
}
}

Programa 3.1. Simulação da lista genérica simples para números inteiros.

3.2.4. Sumário
As listas, mesmo quando ordenadas para optimizar a pesquisa de
elementos, são estruturas lineares que só podem ser percorridas
sequencialmente. Por isso, são pouco eficientes para inserir e remover
elementos, devido ao tempo gasto na pesquisa da posição de inserção
e de remoção, a não ser quando os elementos são inseridos e
removidos na cabeça e na cauda da lista. Por esta razão, as listas são
geralmente utilizadas com acesso limitado às suas extremidades,
nomeadamente para implementar memórias de tipo fila e do tipo pilha.
Nesta unidade temática detalhamos sobre as listas ligadas ou simples e
apresentamos os seus algoritmos básicos. Abordamos ainda a
implementação genérica das listas ligadas ou simples.

3.2.5. Exercícios de Auto-Avaliação

Perguntas
1. O tamanho da lista ligada em execução depende da disponibilidade
da memória do sistema.
A. Verdadeiro
B. Falso
2. O último elemento da lista simples aponta para o primeiro.
A. Verdadeiro
B. Falso
3. A figura a seguir representa a implementação estática da lista de
uma lista ligada.
Cabeça 20 30 40
A. Verdadeiro
*B. Falso
42
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

4. As listas ligadas desperdiçam memória, pois utilizam memória extra


para os ponteiros.
A. Verdadeiro
B. Falso
5. A figura que se segue representa o nó decomposto de uma lista
next Node next; next
elem tipo elem;
elem
ligada.
A. Verdadeiro
B. Falso
6. O método toString() da lista ligada somente funciona se a lista não
estiver vazia.
A. Verdadeiro
B. Falso

Respostas
1. A; 2. B; 3. B; 4. B; 5. B

3.3. Listas Biligadas ou Duplamente Ligadas

3.3.1. Introdução
Ao completar esta unidade, você deverá ser capaz de:
▪ Identificar uma lista biligada ou duplamente ligada;
▪ Conhecer os algoritmos básicos das listas biligadas ou
duplamente ligadas;
Objectivos ▪ Ententer a implementação genérica das listas biligadas
específicos ou duplamente ligadas.

Os nós da lista biligada ou duplamente ligada têm duas referências, uma


para o elemento anterior e outra para o elemento seguinte, permitindo
assim que se percorra nos dois sentidos, apesar de ter apenas uma
referência para a cabeça da lista. A figura 3.8 ilustra uma lista biligada
ordenada por ordem crescente. O primeiro nó e o último nó apontam
para null, indicação do fim da lista consoante o sentido do percurso.

Cabeça 20 30 40

Figura 3.8. Lista biligada.

Os nós da lista biligada (binós) podem ser um dos dois tipos


apresentados na figura 3.9. À esquerda representa um nó compacto
que armazena um elemento de um tipo de dados primitivo, sendo cada
nó constituído pela referência para o nó anterior da lista prev,
referência para o nó seguinte da lista

43
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

next e pelo elemento que se pretende armazenar na lista elem. À direita


temos um nó decomposto que armazena um elemento do tipo de dados
referência, sendo cada nó constituído por três referências, uma para o
nó anterior da lista, uma para o nó seguinte da lista e uma para o
elemento que se pretende armazenar na lista.

prev next BiNode prev; prev next


elem BiNode next;
tipo elem; elem
Figura 3.9. Nós da lista biligada.

A classe 3.3 apresenta um esqueleto para a implementação do tipo de


dados lista biligada, DoubleList, de inteiros. A classe privada BiNode é
do tipo de dados binós da lista e tem os seus atributos públicos para
facilitar o acesso aos binós da lista.

public class DoubleList {


private static class BiNode { // classe privada de nós da lista biligada
public BiNode prev; // atributo para o binó anterior da lista
public BiNode next; // atributo para o nó seguinte da lista
public int elem; // atributo para o elemento a armazenar na lista
public BiNode(int pVal) { // construtor do binó da lista biligada
elem = pVal;
next = null;
}
}
private BiNode head; // atributo cabeça da lista biligada
public DoubleList() { // construtor da lista biligada
head = null;
}
public boolean isEmpty() { // método da lista biligada vazia
return head == null;
}
… // métodos públicos e privados da lista biligada
}
Classe 3.3. Esqueleto da classe lista biligada para armazenar inteiros.

O construtor do binó copia o parâmetro para o atributo elem e coloca


as referências para os binós anterior prev e seguinte next a apontar para
null. Se o tipo dos elementos da lista biligada for um tipo referência,
então o objecto já foi criado e a cópia da sua referência para o atributo
elem liga-o ao binó da lista.
A classe DoubleList tem o atributo head para representar a cabeça da
lista biligada. Pode ter também o atributo tail para representar a cauda
da lista e um atributo para indicar o seu número de elementos nelem.
A detecção de lista biligada vazia isEmpty é feita verificando se a cabeça
da lista é ou não uma referência nula.

44
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

3.3.2. Algoritmos Básicos da Lista Biligada


O método search() da lista duplamente ligada é igual ao da lista
simplesmente ligada. Ver algoritmo 3.1 para referência.
A figura 3.10 apresenta dois exemplos de inserção de elementos numa
lista biligada ordenada por ordem crescente. Para fazer a inserção de
um elemento, determina-se a posição onde o nó do elemento deve ser
inserido. Cria-se um nó com o elemento e, a não ser que o elemento
seja inserido a cabeça da lista, determina-se a referência do elemento
à frente do qual se vai inserir o novo elemento. A inserção a trás do
primeiro elemento da lista implica actualizar a cabeça da lista
x
x
Cabeça 4 20 30 50
2

3
ins 10

a)
x
Cabeça 20 30 x 50
3 4
5 6
prev ins 40

b)
Figura 3.10. Inserção na lista biligada ordenada por ordem crescente.

Consoante a aplicação a ser feita da lista como estrutura de


armazenamento de informação, podem existir ou não elementos
repetidos numa lista. Uma forma de criar uma lista geral e versátil
consiste em autonomizar a questão de existência de elementos
repetidos e da ordem de sua colocação na lista, numa função de
pesquisa da posição de inserção, que, sendo autónoma, pode ser
alterada para mudar as características da lista sem que isso afecte a
operação de inserção.
O algoritmo 3.5 apresenta a inserção de um elemento numa lista
biligada ordenada por ordem crescente, sendo qua pesquisa da posição
de inserção está autonomizada na operação interna posInsert.

public void insert(int pElem) {


BiNode ins = new BiNode(pElem), prev;
if (head == null || head.elem > pElem) { // inserir à cabeça da lista
ins.next = head;
if (ins.next == null)
ins.next.prev = ins;
head = ins;
} else { // inserir à frente do nó de inserção
prev = posInsert(head, pElem)
if (prev != null) {
ins.prev = prev;

45
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

ins.next = prev.next;
prev.next = ins;
if (ins.next != null)
ins.next.prev = ins;
}
}
}
Algoritmo 3.5. Inserção na lista biligada ordenada por ordem crescente.

Se a estiver vazia ou o novo elemento for menor do que o elemento


colocado à cabeça da lista, então o novo elemento é colocado à cabeça
da lista. Se a lista não estiver vazia, é preciso ligar o primeiro elemento
para trás, ao novo elemento. A cabeça da lista é sempre actualizada,
ficando a apontar para o novo elemento. Caso o elemento seja maior
do que o primeiro, então a operação posInsert é invocada para
determinar o elemento da lista, à frente do qual vai ser inserido o novo
elemento. Se a função devolver uma referência nula, então o novo
elemento é ligado para trás ao elemento de inserção e para ao seu
elemento seguinte. O elemento de inserção é ligado para frente ao
novo elemento e o seu elemento seguinte, caso ele exista, é ligado para
trás ao novo elemento. Se a função devolver uma referencia nula então
é sinal de que algo de errado aconteceu (a lista está corrompida ou a
função foi incorrectamente invocada), logo a inserção do novo
elemento não é efectuada.
O algoritmo 3.6 apresenta a função de pesquisa posInsert que
determina a posição de inserção do elemento. O algoritmo apresenta
uma política de existência de elementos repetidos na lista, sendo um
elemento repetido colocado depois dos elementos iguais já existentes
na lista, a pesquisa termina quando se encontra um elemento na lista
maior do que o novo elemento. Esta função só deve ser invocada se o
elemento procurado não estiver à cabeça da lista e devolve a referência
para o elemento, à frente do qual se deve inserir o novo elemento.
Senão devolve a referência nula como indicação da pesquisa mal
invocada.
public static BiNode posInsert(BiNode pHead, int pElem) {
BiNode actual = pHead;
BiNode prev = null;
while (actual != null && pElem > actual.elem) {
prev = actual;
actual = actual.next;
}
return prev;
}
Algoritmo 3.6. Determinação da posição de inserção do elemento na lista
biligada.
Se o tipo dos elementos for um tipo referência, então a comparação
entre o elemento da lista biligada e o elemento a inserir pElem é feita

46
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

com o método compareTo ou outro método de comparação, consoante


o critério de colocação dos elementos na lista.
A figura 3.11 apresenta dois exemplos de remoção de elementos numa
lista biligada ordenada por ordem crescente. Para remover um
elemento da lista precisa-se determinar a referência do elemento que
se pretende remover. A remoção do primeiro elemento da lista implica
actualizar a cabeça da lista biligada.

Cabeça
x x
20
x
x
2
30 50

del

a)

Cabeça 20
x
x x
30
x
x 50

del

b)
Figura 3.11. Remoção na lista biligada ordenada por ordem crescente.

O algoritmo 3.7 apresenta a remoção de um elemento de uma lista


biligada ordenada por ordem crescente, sendo qua a pesquisa da
posição de remoção está autonomizada na operação posDelete.

public void delete(int pElem) {


BiNode del = posDelete(head, pElem);
if (del == null) // lista vazia ou elemento inexistente
return;
if (del == head) { // remoção do elemento da cabeça da lista
if (del.next != null)
del.next.prev = null;
head = del.next;
} else { // remoção de outro elemento da lista
del.prev.next = del.next;
if (del.next != null)
del.next.prev = del.prev;
}
}
Algoritmo 3.7. Remoção na lista biligada ordenada por ordem crescente.

O algoritmo começa por verificar se o elemento existe ou não na lista


biligada. Caso exista a operação posDelete devolve a referência do
elemento. Se ele for o primeiro elemento da lista biligada, o seu
desligamento implica colocar o segundo elemento, caso ele exista,
senão ficará a apontar para o null, para indicar que a lista está vazia. Se
o elemento a remover não for o primeiro da lista biligada, então o
elemento anterior fica a apontar para o elemento da frente. Se o

47
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

elemento a remover for o último da lista, esta operação coloca o


elemento anterior a apontar para null, pois ele torna-se o último
elemento da lista. Se existir um elemento à frente do elemento a
remover, ele fica a apontar para o elemento detrás do elemento a
remover.
O algoritmo 3.8 apresenta a função de pesquisa posDelete que
determina a posição de remoção do elemento. Implementa uma
política de existência de elementos repetidos na lista, sendo um
elemento repetido removido pela ordem de colocação (FIFO).
Consequentemente, a função devolve a referência para a primeira
ocorrência do elemento, ou a referência nula caso ele não se encontre
na lista ou a lista esteja vazia.

public static BiNode posDelete(BiNode pHead, int pElem) {


BiNode actual = pHead;
while (actual != null && pElem != actual.elem)
actual = actual.next;
return actual;
}
Algoritmo 3.8. Determinação da posição de remoção do elemento na lista
biligada.

Se o tipo dos elementos for um tipo referência, então a comparação


entre o elemento da lista biligada e o elemento a remover pElem é feita
com o método compareTo ou outro método de comparação, consoante
o critério de colocação dos elementos na lista.
Esta implementação apenas desliga o binó do elemento da lista biligada,
tornando-o inacessível e consequentemente é eliminado pelo gestor de
objectos para libertar a memória por ele ocupada. Este procedimento é
transformado numa função que devolve a referência do elemento,
binode.elem, ou a referência nula, caso o elemento não se encontre na
lista.
Para reiniciar a lista biligada, método clear, é necessário remover todos
os seus elementos, bastando para tal colocar a cabeça da lista biligada
a apontar para null. Caso a lista tenha um contador de elementos, este
deve ser reinicializado.

3.3.3. Implementação Genérica da Lista Biligada


A Classe 3.4 apresenta a lista biligada genérica parametrizada
GenericDoubleList. É semelhante à lista ligada apresentada na Classe
3.1.

48
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

public class GenericDoubleList<T extends Comparable<T>> {


private static class BiNode { // classe privada de binós da bilista
public BiNode<T> prev; // atributo para o binó anterior
public BiNode<T> next; // atributo para o binó seguinte
public T elem; // atributo para o elemento a armazenar no binó
public BiNode(T pVal) { // construtor do binó da lista biligada
elem = pVal;
prev = null;
next = null;
}
}
private BiNode<T> head; // atributo cabeça da lista biligada
public GenericDoubleList() { // construtor da lista biligada
head = null;
}
public boolean isEmpty() { // método da lista biligada vazia
return head == null;
}
public T search(T pElem) { // método de pesquisa na lista biligada
for (BiNode<T> binode = head; binode != null; binode = binode.next)
if (pElem.compareTo(binode.elem) == 0)
return binode.elem; // pesquisa com sucesso
return null; // pesquisa sem sucesso
}
public void insert(T pElem) { // método de inserção
BiNode<T> ins = new BiNode<T>(pElem), prev;
if (head == null || head.elem.compareTo(pElem) > 0) {
// inserção à cabeça da lista
ins.next = head;
if (ins.next != null)
ins.next.prev = ins;
head = ins;
} else { // inserir à frente do nó de inserção
prev = posInsert(head, pElem);
if (prev != null) {
ins.prev = prev;
ins.next = prev.next;
prev.next = ins;
if (ins.next != null)
ins.next.prev = ins;
}
}
}
public T delete(T pElem) { // método de remoção
BiNode<T> del = posDelete(head, pElem);
if (del == null) // lista vazia ou elemento inexistente
return null;
if (del == head) { // remoção do elemento da cabeça da lista
if (del.next != null)
head = head.next;

49
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

head = del.next;
} else { // remoção de outro elemento da lista
del.prev.next = del.next;
if (del.next != null)
del.next.prev = del.prev;
}
return del.elem; // devolver o elemento
}
public String toString() { // método de impressão
if (isEmpty()) // teste de lista vazia
return “Lista vazia\n”;
String str = “Lista Biligada : ”;
for (BiNode<T> binode = head; binode != null; binode = binode.next)
str += “\t” + binode.elem.toString();
return str + “\n”;
}
// método interno de pesquisa para inserção
private BiNode<T> posInsert(BiNode<T> pHead, T pElem) {
BiNode<T> actual = pHead, prev = null;
while (actual != null && pElem.compareTo(actual.elem) > 0) {
prev = actual;
actual = actual.next;
}
return prev;
}
// método interno de pesquisa para remoção
private BiNode<T> posDelete(BiNode<T> pHead, T pElem) {
BiNode<T> actual = pHead;
while (actual != null && pElem.compareTo(actual.elem) != 0)
actual = actual.next;
return prev;
}
}
Classe 3.4. Tipo de Dados Abstrato GenericDoubleList.

O Programa 3.1 apresentado na secção 3.2.3 serve também para


simular a lista biligada genérica com elementos inteiros, bastando para
tal mudar a declaração e a criação da variável lista de
GenericList<Integer> para GenericDoubleList<Integer>.

3.3.4. Sumário
Geralmente as listas biligadas são utilizadas para armazenar colecções
de dados, que exigem que os dados estejam sempre ordenados e que
sejam pesquisados por chave de identificação. Por exemplo, para
implementar versões dinâmicas de memórias de tipo associativo e de
tipo fila com prioridade.
Nesta unidade temática estudamos as listas biligadas e detalhamos os
seus algoritmos. Abordamos ainda a implementação genérica da lista
biligada.

50
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

3.3.5. Exercícios de Auto-Avaliação

Perguntas
1. Tal como na lista ligada, a lista biligada têm referências para o
elemento anterior e para o elemento seguinte.
A. Verdadeiro
B. Falso
2. O primeiro e o último nó da lista duplamente ligada apontam para
null.
A. Verdadeiro
B. Falso
3. A figura que se segue representa um nó decomposto da lista

prev next BiNode prev; prev


elem BiNode next;
tipo elem; elem
duplamente ligada
A. Verdadeiro
B. Falso
4. O método search() da lista duplamente ligada percorre de forma
aleatória os nós da lista até encontrar o nó pretendido.
A. Verdadeiro
B. Falso
5. O método posDelete() da lista duplamente ligada é igual ao da lista
ligada.
A. Verdadeiro
B. Falso

Respostas:
1. B; 2. B; 3. B; 4. B; 5. B

3.4. Listas Skip ou com Atalhos

3.4.1. Introdução
Ao completar esta unidade, você deverá ser capaz de:
▪ Definir e identificar uma lista skip;
▪ Conhecer os algoritmos básicos das listas skip;
▪ Ententer a implementação genérica das listas skip.
Objectivos
específicos
Uma lista skip ou com atalhos é uma lista ligada simples em que cada
nó tem um número variável de ligações, sendo que cada nível de ligação
implementa uma lista ligada simples constituída por um numero
diferente de nós. Em outras palavras, é um conjunto de listas paralelas,
todas elas devidamente terminadas com a referência nula, que permite

51
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

implementar diferentes travessias, sendo que cada travessia avança


sobre os nós com um determinado número de ligações inferior ao nível
em que ela decorre. Quanto maior é o nível da travessia, maior é o
espaçamento dos nós percorridos, sendo que uma travessia pode
descer de nível para prosseguir de uma maneira menos espaçada ao
longo da lista. Apesar da travessia ao longo de um nível ser sequencial
As listas skip, skip lists, permitem operações de pesquisa, inserção e
remoção mais eficientes do que as listas simples e biligadas. A figura
3.12 apresenta uma lista skip ordenada por ordem crescente.
Cabeça

0 10 20 40 50 60 70 80

Figura 3.12. Lista skip ordenada por ordem crescente.

Uma lista skip é caracterizada por três parâmetros


• A cabeça da lista: nó extra que armazena um valor menor do que
todos os valores armazenados na lista;
• O número máximo de níveis da lista: número de níveis da cabeça
da lista, que deve estar entre cinco e trinta; e
• O nível activo actual: inicialmente zero e vai sendo actualizado
com o nível do nó, com excepção da cabeça, pois tem mais
níveis.
A figura 3.13 apresenta os tipos de nós que uma lista skip pode ter. À
esquerda representa-se um nó compacto que armazena um elemento
de um tipo de dados primitivo. À direita representa-se um nó
decomposto que armazena um elemento de um tipo de dados
referência. Por o número de referências ser variável entre os nós, o
atributo next não é uma simples referência para o nó seguinte, mas sim
uma sequência de referências para os nós seguintes, que é criada com
a dimensão pretendida a quando da criação do nó.
next SkipNode[] next; next
elem tipo elem;
elem
Figura 3.13. Nós da lista skip.

A classe 3.5 apresenta o esqueleto para implementar uma classe do tipo


de dados lista skip para uma lista de inteiros. Para sua implementação
é necessária a implementação de uma classe privada do tipo de dados
nó da lista skip, SkipNode. O construtor do nó copia o primeiro
parâmetro para o atributo elem e cria a sequência de referências para
os nós seguintes next, com a dimensão indicada pelo segundo
parâmetro. Se o tipo de elementos da lista skip for um tipo referência
então o objecto já foi criado e a cópia da sua referência para o atributo
elem liga-o ao nó da lista skip.

52
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

public class SkipList {


private static class SkipNode { // classe privada de nós da lista skip
public SkipNode[] next; // atributo para os nós seguintes da lista
public int elem; // atributo para o elemento a armazenar na lista
public SkipNode(int pVal, int pLev) { // construtor do nó da lista
elem = pVal;
next = new SkipNode [pLev];
}
}
private SkipNode head; // atributo cabeça da lista skip
private int maxLevel; // atributo número máximo de níveis
private int actualLevel; // atributo nível actual
public SkipList(int pVal, int pMaxLevel) { // construtor da lista skip
head = new SkipNode(pVal, pMaxLevel);
maxLevel = pMaxLevel;
actualLevel = 0;
}
public boolean isEmpty() { // método da lista skip vazia
for (int i = 0; i < head.next.length; i++)
if (head.next[i] != null)
return false;
return true;
}
… // métodos públicos e privados da lista skip
}
Classe 3.5. Esqueleto da classe lista skip para armazenar inteiros.

A classe SkipList tem o atributo head para representar a cabeça da lista,


o atributo maxLevel para representar o número máximo de níveis e o
atributo actualLevel para representar o nível actual, pode ter o atributo
nelem para indicar o seu número de elementos. O construtor da lista
skip cria o nó da cabeça, com o número máximo de níveis da lista e
armazena o valor de base da lista, e inicializa o número máximo de
níveis da lista e o nível actual zero.
Uma lista skip está vazia quando apenas existe o nó da cabeça, com as
referências todas nulas. Assim sendo, a detecção de lista vazia isEmpty
exige verificar a sequência das referências da sua cabeça.
Alternadamente, pode-se manter um contador actualizado do número
de elementos da lista.

3.4.2. Algoritmos Básicos das Listas Skip


O algoritmo 3.9 apresenta o método toString() que cria uma sequência
de caracteres com o conteúdo da lista skip, em que os elementos são
separados pelo caracter tabulador. As primeiras maxLevel-1 linhas
mostram a estrutura de ligações, em que o caracter > representa uma
ligação ao elemento seguinte e indica o valor do elemento, e o caracter
* representa a referência nula. A última linha mostra os elementos

53
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

armazenados na lista. Se o tipo dos elementos for um tipo referência,


então usa-se o método toString do elemento para obter a sua
representação textual.

public String toString() {


if (isEmpty()) // teste de lista vazia
return “Lista vazia\n”;
SkipNode node;
SkipNode test;
String str = “Lista Skip\n”;
for (Node node = head; node != null; node = node.next) {
// impressão das ligações dos níveis superiores
node = head;
if (node.next[i] == null)
str += “*\n”;
else {
str += “> ” + node.next[i].elem;
test = head.next[0];
for (node = node.next[i]; node != null; node = node.next[i]) {
for (; test != node; test = test.next[0])
str += “\t”;
if (node.next[i] == null)
str += “\t*”;
else
str += “\t> ” + node.next[i].elem;
test = test.next[0];
}
Str += “\n”;
}
}
str += head.elem; // impressão de elementos no nível zero
for (node = head.next[0]; node != null; node = node.next[0])
str += “\t” + node.elem;
return str + “\t*\n”;
}
Algoritmo 3.9. Impressão do conteúdo da lista skip.

A figura 3.14 apresenta três exemplos de pesquisa de pesquisa de


elementos numa lista skip ordenada por ordem crescente. A pesquisa
inicia na cabeça da lista, no nível superior activo, descendo para o nível
imediatamente inferior sempre que se encontra um elemento da lista
com valor maior do que o elemento procurado. A pesquisa termina
quando encontra o elemento procurado ou quando se esgotam todos
os níveis de pesquisa.

54
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Cabeça
0 10 20 40 50 60 70 80

a)
Cabeça

0 10 20 40 50 60 70 80

b)
Cabeça

0 10 20 40 50 60 70 80

c)
Figura 3.14. Pesquisa de elementos numa lista skip ordenada por ordem
crescente.
Na figura 3.14.a) pesquisamos o elemento 80. A travessia começa na
cabeça da lista no nível três e chega ao elemento 60. Por este ser menor
que o valor procurado, ela continua no nível um, pois as listas dos níveis
acima já terminaram. A lista atinge o elemento 80com sucesso após
duas comparações.
Na figura 3.14.b) pesquisamos o elemento 50. A travessia começa na
cabeça da lista no nível três e chega ao elemento 60. Por este ser maior
que o valor procurado, a travessia regressa a cabeça, desce um nível e
chega ao elemento 40. Como este é menor do que o valor procurado, a
travessia continua no mesmo nível e chega outra vez ao elemento 60.
Como este é maior que o valor procurado, mais uma vez regressa ao
elemento 40, desce um nível e atinge o elemento 50 com sucesso ao
fim de cinco comparações.
Na figura 3.14.c) pesquisamos o elemento 30. A travessia começa na
cabeça da lista no nível três e chega ao elemento 60. Por ser maior que
o procurado, a pesquisa regressa à cabeça, desce um nível e chega ao
elemento 40. Por este ainda ser maior, a pesquisa regressa outra vez à
cabeça da lista, desce um nível e chega ao elemento 20. Como este é
menor que o valor procurado, a pesquisa continua no mesmo nível e
chega outra vez ao elemento 40. De novo, a pesquisa regressa ao
elemento 20, desce um nível e chega mais uma vez ao elemento 40.
Finalmente, a pesquisa regressa outra vez ao elemento 20, desce a um
nível negativo, concluindo a pesquisa sem sucesso ao fim de cinco
comparações.
55
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

O algoritmo 3.10 apresenta a pesquisa de um elemento numa lista skip


ordenada por ordem crescente. O algoritmo é decomposto em duas
partes, sendo que a pesquisa propriamente dita é feita de forma
recursiva pela função interna recSearch. Consoante o resultado desta
função, a função pública devolve verdadeiro, em caso de sucesso, ou
falso, caso o elemento procurado não exista na lista.

public boolean search(int pElem) {


return recSeach(head, pElem, actualLevel-1) != null;
}
…………………………………………………………………………………………………………………
private static SkipNode recSearch(SkipNode node, int pElem, int pLev) {
Node ins = new Node(pElem);
if (pElem == pNode.elem) // pesquisa com sucesso
return pNode;
while (pLev > 0 && pNode.next[pLev] == null)
pLev--;
if (pLev < 0 || pNode.next[0] == null) // pesquisa sem sucesso
return null;
if (pElem < pNode.next[pLev].elem) {
if (pLev == 0) // pesquisa sem sucesso
return null;
return recSearch(pNode, pElem, pLev-1);
}
return recSearch(pNode.next[pLev], pElem, pLev);
}
Algoritmo 3.10. Pesquisa na lista skip.

A função recursiva de pesquisa começa por verificar se o elemento


inicial da travessia é o valor procurado. Em caso afirmativo obtém-se
uma pesquisa com sucesso e é devolvida a referência do elemento. Caso
contrário, a travessia desce para o nível superior activo do elemento,
caso ele exista. Senão atingiu um elemento terminador da lista skip,
pelo que a pesquisa termina sem sucesso e é devolvida a referência
nula. Caso o elemento não seja um elemento terminal e o valor
procurado seja menor do que o elemento seguinte, então a pesquisa é
invocada para o nível inferior, enquanto ainda houver um nível por
analisar, caso contrário, a pesquisa também termina sem sucesso. Pelo
contrário, se o valor do elemento procurado for maior do que o
elemento seguinte, então a pesquisa é invocada para o elemento
seguinte da lista no mesmo nível. A invocação inicial da pesquisa é feita
para a cabeça da lista skip e, tendo em consideração que os índices das
sequências na linguagem Java começam em zero, para o nível actual
menos uma unidade.
Se o tipo de elementos for um tipo referência, então a comparação
entre o elemento da lista skip e o elemento procurado pElem é feita
com o método de comparação compareTo ou outro método de

56
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

comparação, consoante o critério de colocação dos elementos na lista.


E a função devolve a referência do elemento, pNode.elem, ou a
referência nula, caso ele não exista na lista.
A inserção de um elemento na lista skip implica que todas as listas
ligadas que interceptadas pelo novo elemento têm de ser ajustadas. A
figura 3.15 apresenta dois exemplos de inserção de elementos, sendo
que as ligações efectuadas estão assinaladas a tracejado.
Para inserir o 40, com três níveis, o nível activo da lista passa para três.
Passa a existir uma lista ligada de nível três entre a cabeça da lista e o
novo elemento. A lista ligada de nível dois entre os elementos 20 e 80,
e a lista ligada de nível um entre os elementos 20 e 50, são
interrompidas pelo novo elemento, consequentemente ajustadas,
figura 3.15. b).
No caso de inserção do 60, com quatro níveis, o nível activo actual passa
para quatro. Passa a existir uma lista ligada de nível quatro entre a
cabeça da lista e o nove elemento. E a lista ligada de nível três é
prolongada entre o elemento 40 e o novo elemento. As listas ligadas de
nível um entre os elementos 40 e 80 e de nível zero entre os elementos
50 e 70 são ajustadas, figura 3.15.c).
ins
Cabeça

40

0 10 20 50 70 80

a)

ins
Cabeça

60

0 10 20 40 50 70 80

b)
Cabeça

0 10 20 40 50 60 70 80

c)
Figura 3.15. Inserção de elementos numa lista skip ordenada por ordem
crescente.

O algoritmo 3.11 apresenta a inserção de um elemento na lista skip


ordenada por ordem crescente. O algoritmo é decomposto em duas
partes: a criação do nó do elemento, que inclui a determinação do seu
nível de inserção; e a inserção do nó na lista, que é feita de forma

57
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

recursiva pela função interna recInsert e que é baseada no algoritmo de


pesquisa.

public void insert(int pElem) {


// criação do novo elemento e a sua inserção na lista skip
SkipNode ins = new SkipNode(pElem, ran());
recInsert(head, ins, actualLevel-1);
}
…………………………………………………………………………………………………………………
private static void recInsert(SkipNode pHead, SkipNode pNew, int pLev) {
if (pHead.next[pLev] == null || pNew.elem < pHead.next[pLev].elem) {
if (pLev < pNew.next.length) {
pNew.next[pLev] = pHead.next[pLev];
pHead.next[pLev] = pNew;
}
If (pLev == 0)
return;
recInsert(pHead, pNew, pLev-1);
} else
recInsert(pHead.next[pLev], pNew, pLev);
}
Algoritmo 3.11. Inserção na lista skip ordenada por ordem crescente.

O algoritmo de inserção do nó na lista analisa recursivamente todas as


listas ligadas, desde o nível activo até ao nível zero, altura em que o
processo de actualização das ligações acaba e o elemento é
efectivamente inserido na lista. Para cada lista ligada determina o local
de inserção do novo elemento (o seu elemento anterior) e depois faz as
ligações do novo elemento ao elemento seguinte do seu elemento
anterior e do elemento anterior ao novo elemento.
Se o tipo de elementos for um tipo referência, então a comparação
entre o elemento da lista skip e o elemento a inserir pElem é feita com
o método compareTo ou outro método de comparação, consoante o
critério de colocação dos elementos na lista.
O desempenho da lista skip depende da composição das diferentes
listas ligadas. Para obter uma pesquisa com eficiência logarítmica, ela
deve ter listas com espaçamentos do tipo 2nível. Mas este critério rígido
dificultaria a operação de inserção. Logo, é preferível decidir o nível do
nó aleatoriamente, mas assegurando uma distribuição probabilística
em que a lista tenha 50% dos elementos no primeiro nível, 25% dos
elementos no segundo nível, 12.5% dos elementos no terceiro nível, por
outra1/2i dos elementos no nível i.
O algoritmo 3.12 apresenta uma função que determina aleatoriamente
o nível i (com i Є [1...maxLevel]), com probabilidade 1/2i. sempre que o
nível gerado é superior ao nível actual da lista actualLevel, este é
actualizado.

58
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

private int rand() {


int i;
int j;
double t = Math.random();
for (i = 1, j = 2; i < maxLevel; i++, j += j)
if (t * j > 1.0)
break;
if (i > actualLevel)
actualLevel = i;
return;
}
Algoritmo 3.12. Determinação do nível do nó da lista skip.

A remoção de um elemento da lista skip implica que todas as listas


ligadas que estavam interceptadas pelo nó do elemento removido têm
de ser ajustadas. A figura 3.16 apresenta dois exemplos de remoção de
elementos, sendo que as ligações afectadas estão assinaladas a
tracejado.
No caso da remoção do 20, que tem apenas dois níveis, pelo que as
listas ligadas de nível quatro e três não precisam ser actualizadas. A lista
ligada de nível dois passa a ligar o seu elemento anterior, a cabeça da
lista ao elemento seguinte 40. A lista ligada de nível um passa a ligar o
elemento anterior 10 ao elemento seguinte 40.
No caso da remoção do 80, que é o último elemento da lista, todas as
listas que terminavam nesse elemento passam agora a terminar nos
elementos anteriores.
Cabeça

0 10 20 40 50 60 70 80

a)

del
20
Cabeça

0 10 40 50 60 70 80

b)

59
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

del
80

Cabeça
0 10 40 50 60 70

c)
Figura 3.16. Remoção de elementos de uma lista skip ordenada por ordem
crescente.

O algoritmo 3.13 apresenta a remoção de um elemento da lista skip


ordenada por ordem crescente. O algoritmo é decomposto em duas
partes, sendo que a remoção propriamente dita é feita de forma
recursiva pela função interna recDelete, que é baseada no algoritmo de
pesquisa. O algoritmo de remoção analisa recursivamente todas as
listas ligadas, desde o nível activo actual até ao nível zero, altura em que
o processo de actualização das ligações acaba e o elemento é
efectivamente removido da lista. Para cada lista ligada determina o
elemento anterior ao elemento a remover e depois faz a ligação do
elemento anterior ao elemento seguinte.

public void delete(int pElem) {


recDelete(head, pElem, actualLevel-1);
}
…………………………………………………………………………………………………………………
private static void recDelete(SkipNode pHead, int pElem, int pLev) {
while (pLev > 0 && pHead.next[pLev] == null)
plev--;
if (pLev < 0 || pHead.next[0] == null)
return;
SkipNode node = pHead.next[pLev];
if (node.elem >= pElem) {
if (node.elem == pElem) {
pHead.next[pLev] = node.next[pLev];
if (pLev == 0)
return;
}
recDelete(pHead, pElem, pLev-1);
} else
recDelete(pHead.next[pLev], pElem, pLev);
}
Algoritmo 3.13. Remoção da lista skip ordenada por ordem crescente.

Se o tipo de elementos for um tipo referência, então a comparação


entre o elemento da lista skip e o elemento a remover pElem é feita
com o método compareTo ou outro método de comparação, consoante
o critério de colocação dos elementos na lista.

60
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Esta implementação do algoritmo apenas desliga o nó do elemento da


lista skip e liberta a memória por ele ocupada. Geralmente, quando uma
lista skip armazena elementos agregados, remover o elemento da lista
significa desligar o seu nó com vista à libertação da memória por ele
ocupada e devolver o elemento. Nesse caso, o procedimento é
transformado numa função que devolve a referência do elemento
node.elem ou a referência nula, caso o elemento não se encontre na
lista.
Para reinicializar a lista skip, método clear, é preciso reinicializar o nível
actual da lista e remover todos os seus elementos. Para tal, basta
colocar as referências do nó da cabeça da lista a apontar para null. Caso
a lista tenha um contador de elementos, este tem de ser reinicializado.

3.4.3. Implementação Genérica


A classe 3.6 apresenta a lista skip genérica parametrizada
GeneralSkipList, sendo que todas as observações expostas sobre a
implementação e utilização da classe GenericList também se aplicam a
esta classe.
public class GenericSkipList<T extends Comparable<T>> {
private static class SkipNode { // classe privada de nós da lista skip
public SkipNode <T> next; // atributo para os skipnós seguintes
public T elem; // atributo para o elemento a armazenar no nó
// suprimir o aviso gerado pelo cast
@SupperssWarnings(“unchecked”)
public SkipNode (T pVal, int pLev) { // construtor do nó da lista skip
elem = pVal;
next = (SkipNode<T>[]) new SkipNode[pLev];
}
}
private SkipNode <T> head; // atributo cabeça da lista skip
private int maxLevel; // atributo número máximo de níveis
private int actualLevel; // atributo para o nível actual
public GenericSkipList(T pVal, int pMaxLevel) { // construtor da lista
head = new SkipNode<T>(pVal, pMaxLevel);
maxLevel = pMaxLevel;
actualLevel = 0;
}
public boolean isEmpty() { // método de lista skip vazia
for (int i = 0; i < head.next.length; i++)
if (head.next[i] != null)
return false;
return true;
}
public T search(T pElem) { // método de pesquisa na lista skip
return recSearch(head, pElem, actualLevel-1);
}
public void insert(T pElem) { // método de inserção
// criação do novo elemento e sua inserção na lista skip

61
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

SkipNode<T> ins = new SkipNode<T>(pElem, rand());


recInsert(head, ins, actualLevel-1);
}
public T delete(T pElem) { // método de remoção
return recDelete(head, pElem, actualLevel-1);
}
public String toString() { // método de impressão
if (isEmpty()) // teste de lista vazia
return “Lista vazia\n”;
SkipNode<T> node, test;
String str = “Lista Skip : ”;
// impressão das ligações dos níveis superiores
for (int i = maxLevel-1; i > 0; i--) {
node = head;
if (node.next[i] == null)
str += “*\n”;
else {
str += “> ” + node.next[i].elem.toString();
test = head.next[0];
for (node = node.next[i]; node != null; node = node.next[i]) {
for ( ; test != node; test = test.next[0])
str += “\t”;
if (node.next[i] == null)
str += “\t*”;
else
str += “\t” + node.next[i].elem.toString();
test = test.next[0];
}
str += “\n”;
}
}
str += head.elem;
// impressão dos elementos do nível zero
for (node = head.next[0]; node!= null; node = node.next[0])
str += “\t” + node.elem.toString();
return str += “\t*\n”;
}
// método interno de pesquisa
private T recSearch(SkipNode<T> pNode, T pElem, int pLev) {
if (pElem.compareTo(pNode.next[pLev]) == null)
pLev--; // pesquisa com sucesso
while (pLev > 0 && pNode.next[pLev] == null)
pLev--;
if (pLev < 0 || pNode.next[0] == null) // pesquisa sem sucesso
return null;
if (pElem.compareTo(pNode.next[pLev].elem) < 0) {
if (pLev == 0) // pesquisa sem sucesso
return null;
return recSearch(pNode, pElem, pLev-1);
}

62
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

return recSearch(pNode.next[pLev], pElem, pLev);


}
// método interno de inserção
private void recInsert(SkipNode<T> pHead, SkipNode<T> pNew, int
pLev) {
if (pHead.next[pLev] == null ||
pNew.elem.compareTo(pHead.next[pLev].elem) < 0) {
if (pLev < pNew.next.length) {
pNew.next[pLev] = pHead.next[pLev];
pHead.next[pLev] = pNew;
}
if (pLev == 0)
return;
recInsert(pHead, pNew, pLev-1);
} else
recInsert(pHead.next[pLev], pNew, pLev);
}
// método interno de geração do nível do nó
private int rand() {
int i;
int j;
double t = Math.random();
for (i = 1, j = 2; i < maxLevel; i++, j += j)
if (t * j > 1.0)
break;
if (i > actualLevel)
actualLevel = I;
return i;
}
// método interno de remoção
private T recDelete(SkipNode<T> pHead, T pElem, int pLev) {
while (pLev > 0 && pHead.next[pLev] == null)
pLev--;
if (pLev < 0 || pHead.next[pLev] == null)
return null;
SkipNode<T> node = pHead.next[pLev];
if (node.elem.compareTo(pElem) >= 0) {
if (node.elem.compareTo(pElem) == 0) {
pHead.next[pLev] = node.next[pLev];
if (pLev == 0)
return node.elem;
}
recDelete(pHead, pElem, pLev-1);
} else
recDelete(pHead.next[pLev], pElem, pLev);
return null;
}
}
Classe 3.6. Tipo de dados abstrato GenericSkipList.

63
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

O programa 3.1, apresentado na secção 3.2.3, também pode ser usado


para simular a lista skip genérica com elementos inteiros, bastando para
tal mudar a declaração e a criação da variável lista de
GenericList<Integer> para GenericSkipList<Integer>. Note que o
construtor deve ser invocado com dois parâmetros, o valor de base da
lista e o número máximo de níveis da lista.

3.4.4. Sumário
As listas com atalhos permitem a existência de elementos repetidos, o
que pode influenciar na sua operação. A pesquisa não garante que
encontre a primeira ocorrência de um elemento repetido. Como a
inserção usa a comparação “menor do que”, insere o novo elemento à
frente de eventuais elementos com o mesmo valor. A remoção elimina
um dos elementos repetidos mas pode, eventualmente, desfazer
algumas ligações dos outros elementos com o mesmo valor,
degradando o desempenho futuro da lista.
As listas com atalhos geradas aleatoriamente têm algoritmos com
eficiência logarítmica, sendo assim, mais eficientes do que as listas
simples e biligadas, e equivalentes às árvores balanceadas. Esta
eficiência é obtida à custa de mais memória para manter um maior
número e diversidade de ligações possíveis entre os nós da lista. Os seus
algoritmos são recursivos e de fácil implementação, se comparados
com os algoritmos das árvores. São uma estrutura de dados alternativas
às árvores, geralmente usadas para armazenar colecções de dados que
exigem que os dados estejam sempre ordenados e que sejam
pesquisados por chave de identificação.
Nesta unidade temática estudamos as listas skip e detalhamos os seus
algoritmos. Abordamos ainda a implementação genérica da lista skip.

3.4.5. Exercícios de Auto-Avaliação

Perguntas
1. O nó composto da lista skip é igual ao nó composto da lista simples.
A. Verdadeiro
B. Falso
2. O nó decomposto da lista skip é igual ao nó decomposto da lista
ligada.
A. Verdadeiro
B. Falso
3. A lista skip tal como a lista ligada está vazia quando não existem
elementos nela armazenados.
A. Verdadeiro
B. Falso
4. As listas skip são menos eficientes que as listas duplamente ligadas.
A. Verdadeiro
B. Falso

64
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

5. O tamanho da lista skip é definido, a partir do construtor, no


momento da criação da lista.
A. Verdadeiro
B. Falso

Respostas
1. A; 2. A; 3. B; 4. B; 5. A

65
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

TEMA 4. TABELAS DE DISPERSÃO

Unidade temática 5.1. Introdução


Unidade temática 5.2. Especificação da tabela
Unidade temática 5.3. A interface da tabela
Unidade temática 5.4. Tabelas de dispersão

4.1. Introdução

Ao completar este tema, você deverá ser capaz de:


▪ Saber definir e identificar as tabelas;
▪ Compreender as especificações do tipo de dados abstracto
tabela;
Objectivos ▪ Entender e saber aplicar as tabelas de dispersao, os seus
específicos endereçamentos, interfaces e classes.

A tabela, ou dicionário (dictionary), é uma estrutura de dados que


armazena um conjunto finito de registos (records), em que cada registo
é constituído por um par de valores:
• Uma chave, que identifica univocamente esse elemento em
relação aos restantes elementos
• Um elemento, de um dado tipo, que contém a informação do
que se pretende armazenar.
As tabelas, geralmente, são de complexidade linear, sendo possível
reduzi-las à complexidade logarítmica quando se armazenam os
elementos ordenados por chave e realiza-se uma pesquisa binária. Pode
ainda, gastar mais memória obtendo-se algoritmos de busca e inserção
que, no caso médio, reduzem a complexidade temporal para constante,
O(1).

4.2. Especificação da Tabela

A representação de uma tabela é definida recursivamente a partir de


duas operações construtoras, empty e insert.
As operações definidas pelo TDA são:
• empty – construtor que representa a tabela vazia
• insert – construtor que representa uma tabela constituída por
um registo (uma chave do tipo key e o elemento to tipo
Element) e a tabela restante.
• isEmpty – verifica se a tabela está vazia.
• contains – verifica se um elemento representado pela chave
está na tabela.
• retrieve – devolve o elemento associado à chave dada.

66
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

• remove – remove da tabela o elemento associado à chave dada.


Segue o TDA
especificação Table<Element, Key> =
importa Boolean
géneros Table
operações
empty : → Table
insert : Table Element Key → Table
isEmpty : Table → Boolean
contains : Table Key → Boolean
retrieve : Table Key →/ Element
remove : Table Key → Table
axiomas
isEmpty(empty) = TRUE
isEmpty(insert(T, I, K)) = FALSE
contains(empty, K) = FALSE
contains(insert(T, I, K1), K) = equals(K, K1) or contains(T, K)
retrieve(insert(T, I, K1), K) =
if equals(K, K1)
then I
else retrieve(T, K)
remove(empty, K) = empty
remove(insert(T, I, K1), K) =
if equals(K, K1)
then T
else insert(remove(T, K), I, K1)
pré-condições
insert(T, I, K) requer not contains(T, K)
retrieve(T, K) requer contains(T, K)
fim-especificação

4.3. A Interface Tabela

Segue a interface Java

67
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

package dataStructures;
/** Título: A interface do TDA Tabela. Descrição: O desenho com
contractos. */
public interface Table {
/** Criar uma tabela vazia
* @return Uma tabela vazia. */
//@ ensures \result.isEmpty();
/*@ pure @*/ Table empty();
/**
* Inserir na tabela o item com chave key
* @param key A chave do elemento
* @param item A referência para o elemento a inserir.
* As três pós-condições do método insert() referem-se
* respectivamente aos axiomas das operações isEmpty, contains,
* e retrieve.
*/
//@ requires item != null && key != null;
//@ requires !contains(key);
//@ ensures !isEmpty();
//@ ensures contains(key);
//@ ensures retrieve(key).equals(item);
void insert(Object item, Object key);
/**
* A tabela está vazia?
* @return TRUE se a estrutura está vazia, FALSE caso contrário
*/
/*@ pure @*/ boolean !isEmpty();
/**
* Esta chave pertence à tabela?
* @param key A chave a procurar
* @return TRUE se pertence, FALSE caso contrário
*/
//@ requires key != null;
//@ ensures \old(isEmpty()) ==> !\result;
/*@ pure @*/ boolean contains(Object key);
/**
* Devolver o elemento com chave key
* @param key A chave a procurar
* @return A referência do objecto com a chave
*/
//@ requires key != null;
//@ requires contains(key);
/*@ pure @*/ Object retrieve(Object key);
/**
* Remover o elemento com chave key
* @param key A chave a procurar
*/
//@ requires key != null;
//@ ensures \old(isEmpty()) ==> isEmpty();
//@ ensures !contains(key);

68
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

void remove(Object key);


/**
* A tabela ‘t’ é igual a esta?
* @param t A tabela a ser comparada
* @return TRUE se são iguais, FALSE caso contrário
*/
//@ also
//@ requires t != null;
/*@ pure @*/ boolean equals(Object t);
/**
* Iterador para os elementos do conjunto
* @return Um iterados para os elementos do conjunto
*/
/*@ pure @*/ Iterator iterator();
/**
* Devolver uma cópia da estrutura
* @return Devolve uma referência para a cópia
*/
//@ also
//@ ensures (\result != this) && equals(\result);
/*@ pure @*/ Object clone();
} //endInterface Table

4.4. Tabelas de Dispersão

A tabela de dispersão (do inglês, hash table) é uma implementação do


TDA tabela em que o valor da chave associado ao elemento é usado
para calcular a posição onde esse elemento deve ser inserido e
procurado.
Função de dispersão (do inglês, hash function), h(), é uma função que
relaciona o domínio do tipo da chave K e o contradomínio definido pelo
conjunto de endereços da estrutura de dados A.
ℎ: 𝐾 → 𝐴
Colisão é o termo usado para indicar quando mais do que uma chave
resultará em um mesmo endereço.
O uso da tabela de dispersão requer que se lide com dois desafios: (1)
Que função h usar? E (2) Como fazer para resolver potenciais colisões?
Para o desafio (1) é preciso conhecer os detalhes específicos do
problema (qual o tipo de chave? Qual a memória disponível, por
exemplo, qual a cardinalidade de A?). consideramos que o vector onde
e armazenada a informação possui uma dimensão N (desde o índice 0
até ao índice N-1). Admitimos também a existência de uma função ord
que transforma a chave num número inteiro
ℎ(𝑘) = 𝑜𝑟𝑑(𝑘)𝑚𝑜𝑑 𝑁
Esta função foi utilizada na implementação estática de filas de espera
para criar um vector circular.

69
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Técnicas de processamento inicial da chave que se reflecte na função


ord:
• Remover as chaves que possuem pouca diversidade antes de
aplicar a função de dispersão. Por exemplo, se a chave é o
número do BI e o universo de elementos a inserir são pessoas
com sensivelmente a mesma idade, a variação dos dígitos na
ordem de milhões é muito pequena.
• Dividir as chaves e agrupa-las em alguma forma, visando
melhorar a variabilidade da chave inicial. Por exemplo, sendo
uma chave uma palavra atribui-se a cada caracter um valor e
adicionam-se para criar um número: ord(“zebra”) = ord(“z”)*3 +
ord(“e”)*32 + ord(“b”)*33 + ord(“r”)*34 + ord(“a”)*35 , onde o
valor de uma letra é igual a posição do alfabeto.
Método da multiplicação
O método da multiplicação é descrito pela função:
ℎ(𝑘) = ⎿𝑁. (𝐴. 𝑜𝑟𝑑(𝑘) − ⎿𝐴. 𝑜𝑟𝑑(𝑘)⏌)⏌, 𝐴 ∈ [0, 1]
Geralmente o A toma o valor φ = (√5-1)/2 ≈ 0.6180339887… e o N,
por não são ser crítico no método, pode uma potência de 2 para
acelerar as multiplicações.
No segundo desafio – como se resolvem as colisões? – depende
essencialmente da estrutura de dados utilizada

4.4.1. Endereçamento Fechado (do inglês closed addressing ou direct chaining)


No endereçamento fechado resolve-se a questão armazenando todos
os elementos cuja a chave devolve o mesmo valor da função de
dispersão numa lista.
A figura 4.1 representa uma tabela com quatro posições, em que foram
inseridos elementos com chaves de ‘a’ a ‘g’ usando uma função de
dispersão que nas chaves em questão devolveu
h(a) = h(e) = 0, h(g) = h(f) = h(d) = h(b) = 2 e h(c) = 3.
e a
0
/

1 /

g f d b
2
/

c
3
/

Figura 4.1. Tabela de dispersão.

No endereçamento fechado, a colisão é resolvida inserindo o novo


elemento à cabeça do objecto da lista, mantendo na mesma lista os
elementos cuja a chave devolve o mesmo valor.

70
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Apesar do algoritmo de inserção ser O(1), se a função de dispersão não


trabalha de forma efectiva, a complexidade da busca pode degenerar
numa busca linear, dado ser necessário procurar dentro de uma lista
progressivamente maior pelo elemento pretendido. No exemplo da
figura 4.1 poder-se-ia suspeitar da existência de uma assimetria quanto
ao número de elementos por posição.
Para remover um elemento da tabela basta remover o elemento da
lista.

4.4.2. Endereçamento Aberto (do inglês open addressing)


O endereçamento aberto é geralmente utilizado por uma estrutura de
dados vectorial, em que cada posição armazena apenas um e um único
registo ao invés de uma lista. Quando uma posição está ocupada é
necessário que haja uma segunda função, função de pesquisa (do
inglês, collision handling), que permita encontrar uma ou mais posições
alternativas para a chave que resultou em colisão. A função de pesquisa
recebe dois argumentos: a chave do elemento a procurar/inserir e um
valor inteiro n que representa a n-ésima tentativa.
ℎ𝑖 : K ∗ ℕ0 → A
É conveniente que esta função garanta as condições seguintes:
• hi(k, 0) = h(k), por exemplo, a primeira tentativa procura na
posição indicada pela função de dispersão.
• Dado o vector de dimensão N, a sequência de pesquisa (do
inglês, probing sequence) de valores hi(k, 0), hi(k, 1), hi(k, 2), …,
hi(k, N-1) deve passar por todos os valores possíveis do vector.
O ideal seria que a função de pesquisa satisfizesse
hi(k, 1) = hi(k, N)
assim, se não existirem posições livres, a função irá chegar ao local de
partida: memória cheia.
Pesquisa linear (do inglês, linear probing)
ℎ𝑖 (k, i) = (ℎ(k) + i) 𝑚𝑜𝑑 N
Apesar da sua simplicidade, este método tem a desvantagem dos
elementos se agruparem em aglomerados à volta de uma ou mais
chaves relativamente próximas, diminuindo assim o desempenho da
busca, degenerando pesquisas lineares. Esta pesquisa pode ser
generalizada
ℎ𝑖 (k, i) = (ℎ(k) + 𝑎. i) 𝑚𝑜𝑑 N
Onde a deve ser primo em relação a N. Se o máximo divisor comum
(m.d.c) fosse x > 1, apenas 1/x da tabela seria visada pela sequência da
pesquisa.
Pesquisa quadrática
ℎ𝑖 (k, i) = (ℎ(k) + i2 ) 𝑚𝑜𝑑 N
Esta função minimiza a desvantagem da linear, porém, viola a segunda
condição do endereçamento aberto: nem todos os valores da tabela são
visitados. Na prática, tal como é fácil ocorrerem colisões, é fácil uma

71
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

posição vaga, dai que este tipo de problema ocorre apenas quando a
tabela está praticamente cheia.
Dado o factor de saturação (do inglês, load factor) α (igual ao número
de posições ocupadas a dividir pelo número total de posições) é possível
através da análise estatística do caso médio, determinar que o número
esperado de colisões é igual a
α
1−2
C=
1−α
Por exemplo, mesmo para a pesquisa linear, se α=0.5, C=1.5. Se α=0.95,
C=10.5: mesmo com 95% da tabela ocupada o método de pesquisa mais
fraco, procura em média 10 ou 11 posições.
Pesquisa com dispersão dupla
O método necessita uma segunda função de dispersão (do inglês,
rehashing), h1, para ser usada na sequência de pesquisa
ℎ𝑖 (k, i) = (ℎ(k) + i. ℎ1 (k)) 𝑚𝑜𝑑 N
Este método é considerado melhor para criar uma sequencia de
pesquisa, porque mesmo que as chaves de dois elementos produzam o
mesmo valor quando aplicada a função h, os valores já serão diferentes
na função h1 diminuindo assim a criação de aglomerados.
Pesquisa aleatória
Neste método é usado um gerador de números pseudoaleatórios para
evitar a criação de aglomerados
ℎ𝑖 (k, i) = (ℎ(k) + i. 𝑟𝑎𝑛𝑑()) 𝑚𝑜𝑑 N
• O gerador deve ser uniforme no intervalo [0, N-1] para permitir
que todas as posições tenham a mesma probabilidade de serem
escolhidas
• O gerador deve ser previsível, isto é, tem de possuir um
mecanismo de semente inicializado com a chave do elemento a
inserir. Doutra forma, não seria possível pesquisar
eficientemente um elemento, não haveria forma de saber onde
tinha sido inserido.
Apesar da distribuição dos elementos para evitar colisões ser muito
eficiente, o gerador pode demorar muito tempo a encontrar um espaço
vago se a tabela for relativamente grande e estiver cheia.
Remoção de um elemento
A figura 4.2 ilustra a remoção de um elemento da tabela de dispersão
com endereçamento aberto. Do aglomerado (a, b, c, d), o elemento d
está na posição em que a função de dispersão lhe atribuiu inicialmente,
enquanto a, b e c possuem chaves com o mesmo índice, logo b e c estão
em posições resultantes de um e três colisões, h(a) = h(b) = h(c) = h(d)-
2.

72
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

a b d c f g h

a)

a d c f g h
b)

a d c f g h

c)
Figura 4.2. Remoção de um elemento da tabela de dispersão com
endereçamento aberto.
Se removemos o elemento b devemos marcar o espaço com vago com
uma seta, no caso da figura 4.2.c. Isto permite que quando procurarmos
pelo elemento c após a remoção do b, a pesquisa continue, caso não se
marque a posição vaga a pesquisa consideraria vazia e daria por
terminada, informando a não existência do elemento c.

4.4.3. A Interface HushFunction


A interface da função de dispersão exige a concretização de três
métodos:
• int hashValue(Object key) – a função de dispersão h()
• boolean isHashable(Object key) – um método que verifica se o
objecto referenciado por key pertence ao domínio da função de
dispersão
• int nextValue() – a função de pesquisa que devolve o próximo
valor. É assumido que pesquisa todas as posições da tabela e
que ao fim de uma pesquisa completa volta ao valor inicial da
função de dispersão.

package dataStrucures;
/** Título: A interface do TDA Hashable. Descrição: Para uso de tabelas de
dispersão. */
public interface HashFunction {
/** Calcular o valor de dispersão de uma dada chave.
@param key A chave a ser calculada.
@return O valor dessa chave.
*/
//@ requires isHashable(key);
int hashValue(Object key)
/** Este objecto tem valor de dispersão?.
@param key A chave a verificar.
@return TRUE se tem valor de dispersão, FALSE c.c.
*/
/*@ pure @*/ boolean isHashable(Object key);

73
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

/** Calcular o n-ésimo valor depois de (n-1) colisões, em relação à última


chave inquerida.
@return O próximo valor.
*/
int nextValue();
} //endInterface HashFunction

Interface 4.1. Interface HashFunction.


Nesta interface definem-se o tipo de funções de dispersão que podem
ser usadas nas classes que implementam a interface Table e
disponibiliza o serviço da função de dispersão hashValue() e da função
de pesquisa nextValue(). Pode-se igualmente definir interfaces distintas
para cada uma destas funções.

4.4.4. A Classe HashRegister


Define-se a classe, não pública, do tipo registo que possui dois atributos,
a chave e a informação associada, e os métodos toString() e clone().

package dataStrucures; // ficheiro hashTable.java


/** Título: O tipo dos registos a guardar na tabela */
class HashRegister implements Clonable {
public Object item;
public Object key;
public HashRegister(Object o, Object k) {
item = o;
key = k;
}
public String toString() {
return “ (” + item + “: ” + key + “) ”;
}
public Object clone() {
return new HashRegister(item, key);
}
} //endClass HashRegister

Classe 4.1. Classe HashRegister.

4.4.5. A Classe HashTable


// … continuação do ficheiro hashTable.java
/** Título: Uma implementação vectorial da tabela */
public class HashTable implements Table, Cloneable {
private final int DELTA = 1001;
protected final double LOAD_FACTOR = 0.75;
// availableCell é o símbolo especial que representa a posição vaga
depois da remoção de um registo
protected static final HashRegister availableCell = new
HashRegister(null, null);
private int nElems; // quantos elementos existem

74
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

private HashRegister[] table; // o vector da tabela


private HashFunction h; // a função de dispersão
// implementação para o endereçamento aberto
/** Construtor que referencia a função de dispersão a ser usada.
*/
public HashTable(HashFunction h) {
this.h = h;
table = new HashRegister[DELTA];
nElems = 0;
}
public Table empty() {
return new HashTable(h);
}
public boolean isEmpty() {
return nElems == 0;
}
private boolean isCellFree(int n) {
return table[n] == null;
}
private boolean isCellAvailable(int n) {
return table[n] == availableCell;
}
/** Devolver o índice onde se encontra a chave. @param key A chave
a procurar. @return O índice ou -1 se a chave não existir. */
private int searchPos(Object key) {
int pos = h.hashValue(key) % table.lenght;
int back = post;
do {
if (isCellFree(pos))
return -1;
if (isCellAvailable(pos))
pos = h.nextValue() % table.length;
else if (key.equals(table[pos].key))
return pos;
else
pos = h.nextValue() % table.length;
} while (back != pos)
return -1;
}
public boolean contains(Object key) {
return searchPos(key) >= 0;
}
public Object retrieve(Object key) {
return table[searchPos(key)].item;
}
public void remove(Object key) {
int n = searchPos(key);
if (n != -1) {
table[n] = availableCell;
nElems--;

75
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

}
}
private void grow() {
HashRegister[] oldTable = table;
int newLength = getNextPrime(table.length + DELTA);
table = new HashRegister[newLength];
nElems = 0;
for (int i = 0; i < oldTable.length; i++)
if (oldTable[i] != availableCell && oldTable[i] != null)
insert(oldTable[i].item, oldTable[i].key);
}
/** Calcular os próximos números primos */
private boolean isPrime(int n) {
if (n % 2 == 0)
return n == 2;
int limit = (int)Math.round(Math.sqrt(n));
for (int i = 3; i <= limit; i += 2)
if (n % i == 0)
return FALSE;
return TRUE
}
private int getNextPrime(int n) {
while (!isPrime)
n++;
return n;
}
/** Método de inserção */
public void insert(Object item, Object key) {
if ((double)nElems / table.length > LOAD_FACTOR)
grow();
// encontrar a posição inicial
while (!isCellFree(pos) && isCellAvailable(pos))
pos = h.nextValue() % table.length;
table[pos] = new HashRegister(item, key);
nElems++;
}
/** Traduzir a tabela, e.g: [|(key1: item1), (key2: item2)|]. @return
Descrição da tabela numa string. */
public String toString() {
StringBuffer result = new StringBuffer(“[|”);
for (int i = 0; i < table.length; i++)
if (!isCellFree(i) && !isCellAvailable(i))
result.append(table[i] + “, ”);
return result.substring(0, result.length() - 1) + “|]”
}
public Object clone() {
HashTable cp = new HashTable(h);
cp.table = new HashRegister[table.length];
cp.nElems = nElems;
cp.h = h;

76
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

for (int i = 0; i < table.length; i++) {


if (isCellFree(i))
continue;
if (isCellAvailable(i))
cp.table[i] = availableCell;
else
cp.table[i] = (HashRegister) (table[i].clone());
}
return cp;
}
public boolean equals(Table t) {
if (!(t instanceof Table))
return FALSE;
Iterator it = ((Table) t).iterator();
while (it.hasNext())
if (!contains(((HashRegister) it.next()).key))
return FALSE;
it = iterator();
while (it.hasNext())
if (!((Table) t).contains(((HashRegister) it.next()).key))
return FALSE;
return TRUE;
}
public Iterator iterator() {
return new TableIterator();
}
/** Iterador */
private class TableIterator implements Iterator {
private int nextElem;
private TableIterator() {
nextElem = isEmpty() ? -1 : 0;
}
public Object next() {
if (nextElem >= 0)
for (int i = nextElem; i < table.length; i++)
if (!isCellAvailable(i) && !isCellFree(i)) {
nextElem = i + 1;
return table[i];
}
nextElem = -1;
return null;
}
public boolean hasNext() {
if (nextElem != 0)
for (int i = nextElem; i < table.length; i++)
if (!isCellAvailable(i) && !isCellFree(i))
return TRUE;
return FALSE;
}
public void remove() {

77
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

throw new UnsupportedOperationException();


}
} //endLocalClass TableIterator
} //endClass HashTable

Classe 4.2. Classe HashTable.

4.4.6. Uso da Classe HashTable


Considere um registo constituído por uma chave do tipo String que
identifica uma informação do mesmo tipo de dados. Pretendemos
implementar uma classe HashFunction para uma função de dispersão
que soma todos os caracteres da chave. A sequencia da pesquisa é
linear. Para usar esta classe é preciso definir uma função de dispersão
conveniente no contexto do problema.

class HashString implements HashFunction {


private int actualValue;
public HashString() {
actualValue = 0;
}
public int hashValue(Object key) {
actualValue = 0;
for (int i = 0; i < ((String) key).length(); i++);
actualValue += (int) ((String) key).charAt(i);
return actualValue;
}
public boolean isHashable(Object key) {
return key instanceof String;
}
public int nextValue() {
actualValue += 1;
return actualValue;
}
}
Classe 4.3. Classe HashString, implementação da classe HashFunction.

public static void main(String[] args) {


HashTable ht = new HashTable(new HashTable());
ht.insert(“elefante”, “03e”);
ht.insert(“tigre”, “12t”);
ht.insert(“zebra”, “03z”);
ht.insert(“gnu”, “54p”);
ht.remove(“12t”);
ht.insert(“foca”, “02f”);
HashTable cp = (HashTable)(ht.clone());

78
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

ht.insert(“tigre2”, “13t”);
System.out.println(ht);
System.out.println(cp);
}
Programa 4.1. Uso da classe HashTable.

[] (03z:zebra), (02f:foca), (03e:elefante), (12t:tigre2), (54p:gnu),


[] (03z:zebra), (o2f:foca), (03e:elefante), (54p:gnu),

Figura 4.3. Resultado da implementação do programa 4.1.

4.5. Sumário

As tabelas de dispersão apresentam vantagens de simplicidade de


implementação; no caso médio a busca na tabela requer O(1);
considerando K o conjunto de chaves armazenadas, a tabela requer
espaço Ө(|K|) ao invés de Ө(|U|). Mas apresentam desvantagens da
busca na tabela requerer O(|K|) no pior caso e serem propensas a
colisões.
Neste tema, estudamos e discutimos essencialmente o tipo de dados
abstracto tabela, sua especificação e interface, com maior destaque às
tabelas de dispersao, onde abordamos os seus endereçamentos, sua
interface e suas principais classes.

4.6. Exercícios de Auto-Avaliação

Perguntas
1. Nas tabelas de dispersão ocorre uma colisão quando um mesmo
endereço está associado à diversas chaves.
A. Verdadeiro
B. Falso
2. Implementar o TDA tabela numa tabela de dispersão com
endereçamento fechado.
3. Criar para exemplo da secção anterior, as implementações
seguintes do tipo HashFunction:
a. Com uma função de pesquisa quadrática,
b. Com uma função de pesquisa aleatória,
c. Com uma função de pesquisa com dispersão dupla.
4. Um vector flexível (do inglês, flexible array) possui o acesso e
actualização a qualquer elemento através do seu índice e, além
disso, permite a inserção e remoção de elementos na sua
primeira e última posição. Apresente a especificação do vector
flexível (defina o TODA flexArray) que inclui as seguintes
funções:

79
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

a. empty, para criar um novo vector,


b. insertBg, para inserir um novo elemento (do tipo
Element) no início do vector,
c. removeBg, para remover o elemento do início do vector,
d. insertEnd, para inserir um novo elemento (do tipo
Element) no fim do vector,
e. removeEnd, para remover o elemento do fim do vector,
f. acess, que dado um vector e um índice, devolve o
elemento nesse índice do vector,
g. update, que dado um vector, um índice e um elemento,
colocar esse elemento nesse índice do vector,
h. isEmpty, que verifica se o vector está vazio,
i. length, que devolve o número de elementos,
j. equals, que comapre se dois vectores são iguais.

Respostas
1. B

80
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

TEMA 5. ÁRVORES GENÉRICAS E BINÁRIAS.

Unidade temática 5.1. Introdução


Unidade temática 5.2. Árvores binárias
Unidade temática 5.3. Árvores genéricas

5.1. Introdução

Ao completar este tema, você deverá ser capaz de:


▪ Definir e identificar as árvores binárias e genéricas;
▪ Entender os percursos de uma árvore binária, assim como a sua
especificação e sua interface;
Objectivos ▪ Compreender a implementação dinâmica e representação estática
específicos da árvore binária;
▪ Explicar os algoritmos das árvores (a,b).

As árvores são estruturas de dados não lineares. Cada elemento da


árvore tem zero, um ou mais elementos seguintes. Não existe uma
única forma de percorrer uma árvore. Pode-se definir uma árvore
recursivamente como:
• Uma estrutura vazia;
• Um nó designado raiz (root) e um número finito de árvores
(subárvores).
4

7 2

3 12 9

Figura 5.1. Árvore de inteiros.

• Cada nó é pai dos nós incluídos nas respectivas subárvores.


• Os descendentes (descendants) de um nó são os seus filhos mais
os descendentes desses filhos.
• Os ascendentes (ascestors) de um nó são o conjunto de nós para
o qual este nó é descendente.
• Uma folha (leaf) é um nó sem filhos.
• Um nó interior é um nó que não é raiz nem folha.
• Um caminho (path) é uma sequencia de nós desde a raiz até as
folhas.
• A altura (height) ou a profundidade (depth) de uma árvore é a
dimensão do maior caminho.
• O nível (level) de um nó é igual a dimensão da sequência de nós
desde a raiz até ao próximo nó. A raiz tem o nível um.

81
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

• O grau (degree) ou aridade (arity) de um nó é igual ao seu


número de filhos. O grau ou aridade da árvore é igual ao maior
grau dos seus nós.

5.2. Árvores Binárias

Uma árvore binária (binary tree) é uma árvore de grau dois, pois
contem exactamente duas subárvores, subárvore de esquerda e
subárvore da direita.
/

+ *

- 10 3 -

4 5 2

Figura 5.2. Árvore binária.


A figura 5.2 representa uma árvore binária de expressões aritméticas
arbitrárias, neste caso representa a expressão
((4 - 5) + 10) / (3 * -2).
Algumas definições:
• Uma árvore binária está equilibrada se a diferença das alturas
das subárvores não é superior a 1 e todas as subárvores estão
equilibradas.
• Uma árvore binária está cheia se for vazia ou se as duas
subárvores tiverem a mesma altura e se ambas são cheias.
Mostra-se por indução que uma árvore cheia de altura h possui
2h - 1 nós.
• Uma árvore binária de altura h está completa se estiver cheia
até ao nível h-1 e todos os nós do último nível estão o mais à
esquerda possível.
• Uma árvore binária é estritamente binária se todos os nós
possuírem grau 0 ou 2.

Figura 5.3. Árvore binária cheia, completa, equilibrada e estritamente


binária.

82
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Figura 5.4. Árvore binária completa, equilibrada e estritamente binária.

Figura 5.5. Árvore binária equilibrada.

5.2.1. Os Percursos de uma Árvore Binária


Para exemplificar os percursos de uma árvore binária, recorremos a
figura 5.2, a árvore da expressão
((4 – 5) + 10) / (3 * - 2).
Percurso em profundidade desce às folhas da árvore, depois retrocede
para vistar os nós ainda não percorridos
• Percurso prefixo trata o conteúdo do nó actual para, de seguida,
percorrer a subárvore esquerda e depois a subárvore direita.
/ + - 4 5 10 * 3 - 2
• Percurso infixo percorre primeiro a subárvore esquerda, depois
trata o conteúdo e por fim percorre a subárvore direita.
4 – 5 + 10 / 3 * - 2
• Percurso sufixo percorre primeiro as subárvores esquerda e
direita, para depois tratar o conteúdo.
4 5 – 10 3 2 - * /
Percurso em largura visita a raiz, depois os nós de nível 2, depois os nós
do nível 3 e assim por diante.
/ + * 10 3 – 4 5 2
Enquanto que nos percursos em profundidade basta saber qual o
caminho e qual a próxima subárvore a percorrer, no percurso em
largura é necessário armazenar todos os nós do nível actual para poder
percorrer os nós do próximo nível.

5.2.2. Especificação da Árvore Binária


As operações definidas por este TDA são:
• empty – construtor que representa a árvore vazia
• constr – construtor que representa uma árvore constituída por
um Element e por duas árvores
• isEmpty – verifica se a árvore está vazia
• left – acede à subárvore da esquerda
• right – acede à subárvore da direita

83
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

• isLeaf – verifica se a árvore é uma folha


• length – devolve o número de nós da árvore
• height – devolve a altura da árvore
• equals – verifica se duas árvores são iguais
A seguir o TDA
especificação BinTree<Element> =
importa Integer, Boolean
géneros BinTree
operações
empty : → BinTree
constr : BinTree BinTree Element → BinTree
isEmpty : BinTree → Boolean
root : BinTree ↛ Element
left : BinTree ↛ BinTree
right : BinTree ↛ BinTree
isLeaf : BinTree → Boolean
lenght : BinTree → Integer
height : BinTree → Integer
equals : BinTree BinTree → Boolean
axiomas
isEmpty(empty) = TRUE
isEmpty(contr(TL, TR, I)) = FALSE
root(contr(TL, TR, I)) = I
left(contr(TL, TR, I)) = TL
right(contr(TL, TR, I)) = TR
isLeaf(T) = not isEmpty(T) and isEmpty(left(T)) and
isEmpty(right(T)
length(T) =
if isEmpty(T)
then 0
else 1 + length(left(T)) + length(right(T))
height(T) =
if isEmpty(T)
then 0
else 1 + max(length(left(T)), height(right(T)))
equals(T1, T2) =
if isEmpty(T1) or isEmpty(T2)
then isEmpty(T1) and isEmpty(T2)
else equals(root(T1), root(T2)) and
equals(left(T1, left(T2)) and
equals(right(T1), right(T2))
pré-condições
root(T) requer not isEmpty(T)
left(T) requer not isEmpty(T)
right(T) requer not isEmpty(T)
fim-especificação

84
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

5.2.3. A Interface Árvore Binária


Para poder aplicar operações sobre as árvores define-se um novo tipo
denominado visitor, para o percurso da árvore.
5 10

4 10 8 20

2 7 4 14

a) b)
Figura 5.6. Aplicação de um visitor sobre a árvore binária.

A figura 5.6.a) representa uma árvore binária. A figura 5.6.b) representa


a mesma árvore depois de aplicado visitor para duplicar o valor de cada
nó.

package dataStructures;
public interface Visitor {
/** Executar a operação. @param info A referência da informação a ser
tratada. */
void visit(Object info);

/** Devolver o resultado. @return O resultado da(s) visita(s). */


Object result();
} // endInterface Visitor

Interface 5.1. A interface do TDA Árvore Binária.


A classe que implementa este operador define a operação a aplicar
sobre a árvore (o somatório dos nós, a duplicação dos nós, entre
outras).
package dataStructures;
import java.utils.*;
/** A interface do TDA Árvore Binária. O desenho da Árvore Binária com
contractos. */
public interface BinTree {
//@ public invariant !isEmpty() ==> root() != null;
/** Criar uma árvore vazia.
* @return Uma árvore vazia. */
//@ ensures \result.isEmpty();
/*@ pure @*/ BinTree empty();

/** A árvore está vazia?


* @return TRUE se está vazia, FALSE caso contrário. */
/*@ pure @*/ boolean isEmpty();

/** Construir uma árvore. @param item A referência do objecto a ser


guardado. @param left A referência da árvore da esquerda.
@param right A referência da árvore da direita. */
/*@ requires right != null && !right.isEmpty() ==>

85
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

@ item.getClass() == right.root().getClass();
@ requires left != null && !left.isEmpty() ==>
@ item.getClass() == left.root().getClass();
@ ensures item != null ==> !isEmpty();
@ ensures item != null ==> root().equals(item);
@ ensures item != null ==> left().equals(left);
@ ensures item != null ==> right().equals(right);
@*/
void constr(Object item, BinTree left, BinTree right); 2
/** Devolver a informação da raiz árvore
* @return A referência do objecto situado na raiz. */
//@ requires !isEmpty();
/*@ pure @*/ Object root();
/**
* Devolver a árvore da esquerda
* @return A referência da subárvore à esquerda
*/
//@ requires !isEmpty();
/*@ pure @*/ BinTree left();
/**
* Devolver a árvore da direita
* @return A referência da subárvore à direita
*/
//@ requires !isEmpty();
/*@ pure @*/ BinTree right();
/**
* Devolver o número de elementos da árvore
* @return O número de elementos da árvore
*/
/*@ ensures isEmpty() ==> \result == 0;
@ ensures !isEmpty() ==>
@ \result == 1 + left().length() + right().length();
@*/
/*@ pure @*/ int length();
/**
* Devolver a altura da árvore
* @return A altura da árvore
*/
/*@ ensures isEmpty() ==> \result == 0;
@ ensures !isEmpty() ==> \result == 1 +
@ (left().height() > right().height() ?
@ left().height() : right().height());
@*/
/*@ pure @*/ int height();
/**

2
As précondições do método constr() garantem que a árvore armazena elementos do
mesmo tipo. As quatro póscondições são referentes aos axiomas das interrogações cons,
root, left e right no TDA. Porque o método pode ser invocado com referências nulas é
colocado uma guarda.

86
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

* É uma folha?
* @return TRUE se é uma folha, FALSE caso contrário
*/
/*@ ensures \result == (!isEmpty() && left().isEmpty()
@ && right().isEmpty());
@*/
/*@ pure @*/ Boolean isLeaf();
/**
* Verificar se as árvores são iguais
* @param t A árvore a ser comparada
* @return TRUE se forem iguais, FALSE caso contrário
*/
/*@ also
@ requires t != null;
@ ensures isEmpty() <==> ((BinTree)t).isEmpty();
@ ensures !isEmpty() && !((BinTree)t).isEmpty() ==>
@ \result == (root().equals(((BinTree)t).root()) &&
@ left().equals(((BinTree)t).left()) &&
@ right().equals(((BinTree)t).right()));
@*/
/*@ pure @*/ boolean equals(Object t);
/**
* Percorrer a árvore de forma prefixa
* @param op O operador a ser executado em cada nó
*/
//@ requires op != null;
void prefix(Visitor op);
/**
* Percorrer a árvore de forma infixa
* @param op O operador a ser executado em cada nó
*/
//@ requires op != null;
void infix(Visitor op);

/** Percorrer a árvore de forma sufixa.


* @param op O operador a ser executado em cada nó. */
//@ requires op != null;
void sufix(Visitor op);
Iterator iterator(); 3

/** Devolver uma cópia da árvore.


* @return Uma referência para a cópia. */
//@ also
//@ ensures (\result != this) && equals(\result);
/*@ pure @*/ Object clone();
} // endInterface Visitor
Interface 5.2. O desenho da Árvore Binária com contractos.

3
A forma de implementar o iterador dependerá da implementação.

87
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

5.2.4. Implementação Dinâmica da Árvore Binária


A ávore é apropriada para uma representação dinâmica por ela consistir
em um conteúdo e duas referências (uma para a sub-árvore à esquerda
e outra para a sub-árvore à direita). Porem, a implementação da lista
não reflecte a definição recursiva da lista, pois a árvore é um nó explicto
e referências para outras árvores.
i

Figura 5.7. Implementação dinâmica da árvore binária.

Considerando que uma árvore vazia seja representada por um nó onde


todas as referências são nulas, convencionou-se que uma folha seja
uma árvore que referencie duas árvores vazias. Embora esta
abordagem facilite a construção dos métodos sobre a estrutura da
árvore, ela tem a desvantagem de gastar mais memória.
/
/ /
Figura 5.8. Representação da árvore binária vazia.

A figura 5.9.a) apresenta o diagrama da árvore binária e a figura 5.9.b)


apresenta a respectiva estrutura em memória.
A

A
B C

B C
/ / / /
a)
/ / / / / / / /
b)
Figura 5.9. Representação da estrutura da árvore binária em memória e o
respectivo diagrama.

package dataStructures;
import java.util.*;
/** Título: Uma implementação dinâmica da Árvore Binária */
public class DBinTree implements BinTree, Cloneable {
private DBinTree leftTree, rightTree;
private Object infoTree;
public DBinTree() {
infoTree = null;
leftTree = rightTree = null;
}
public DBinTree(Object o, BinTree left, BinTree right) {

88
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

constr(o, left, right);


}4
public BinTree empty() {
return new DBinTree();
}
public void constr(Object item, BinTree left, BinTree right) {
infoTree = item;
leftTree = (DBinTree) left;
rightTree = (DBinTree) right;
}
public Boolean isEmpty() {
return infoTree == null;
}
public Object root() {
return infoTree;
}
public BinTree left() {
return leftTree;
}
public BinTree right() {
return rightTree;
}
public int length() {
if (isEmpty())
return 0;
return 1 + left().length() + right().length();
} // 5
public int height() {
if (isEmpty())
return 0;
return 1 + Math.max(right().height(), left().height());
}
public boolean isLeaf() {
if (isEmpty())
return false;
return left().isEmpty && right().isEmpty();
}
public Boolean equals() {
if (!(t instanceOf BinTree))
return false;
if (isEmpty() && ((BinTree) t).isEmpty())
return true;
if (isEmpty() || ((BinTree) t).isEmpty())
return false;
return root().equals(((BinTree) t).root()) &&

4
Este construtor permite construir árvores a partir de árvores já existentes.
5
O teste da existência de subárvores a esquerda e a direita torna-se desnecessário, pois
estes nós podem ser vazios. A complexidade temporal deste algoritmo é Θ(n) porque
cada nó é percorrido apenas uma vez.

89
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

left().equals(((BinTree) t).left()) &&


right().equals(((BinTree) t).right());
} // 6
public void prefix(Visitor op) {
if (!isEmpty()) {
op.visit(root());
left().prefix(op);
right().prefix(op);
}
}
public void infix(Visitor op) {
if (!isEmpty()) {
left().infix(op);
op.visit(root());
right().infix(op);
}
}
public void sufix(Visitor op) {
if (!isEmpty()) {
left().sufix(op);
right().sufix(op);
op.visit(root());
}
}
/** Transformação do método recursivo prefix num método iterativo
utilizando uma pilha para simular invocações recursivas. */
public void prefixIterative(Visitor op) {
if (isEmpty())
return;
VStack stack = new VStack();
stack.push(this);
while (!stack.isEmpty()) {
DBinTree actual = (DBinTree) stack.top();
stack.pop();
op.visit(actual.root());
if (!actual.right().isEmpty())
stack.push(actual.right();
if (!actual.left().isEmpty())
stack.push(actual.left();
}
}
public Iterator iterator() {
return new BinTreeIterator(this);
}
private class BinTreeIterator implements Iterator {
private VQueue qNode;
private Object actualObject;

6
Duas árvores são iguais se ambas são vazias ou ambas possuem o mesmo conteúdo e
duas subárvores iguais.

90
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

private BinTreeIterator(DBinTree t) {
actualObject = null;
qNodes = new VQueue();
qNodes.enqueue(t);
}
public Boolean hasNext() {
return !qNodes.isEmpty();
};
public Object next() {
if (qNodes.isEmpty()) {
actualObject = null;
return null;
}
DBinTree t = (DBinTree) (qNodes.front());
if (!t.leftTree.isEmpty())
qNodes.enqueue(t.leftTree);
if (!t.rightTree.isEmpty())
qNodes.enqueue(t.rightTree);
qNodes.dequeue();
actualObject = t.infoTree;
return actualObject;
}; // 7
public void remove() {
throw new UnsupportedOperationException();
}
} // endInnerClass BinTreeOperator
public String toString() {
// Descrição textual da estrutura da árvore
return isEmpty() ? “[]” : makeTree(0, this);
}
private String makeTree() {
if (t.isEmpty()) // base da recursão
return mark(level) + “[]\n”;
if (t.isLeaf()) // base da recursão
return mark(level) + t.root() + “\n” +
makeTree(level + 1, (DBenTree) t.left()) +
makeTree(level + 1, (DBinTree) t.right);
}
private String mark(int level) {
String s = “”;
for (int i = 0; i < level - 1; i++)
s += “ ”;
return s += ((level > 0) ? “|” : “”)
}
// Devolver cópia exacta da árvore (apenas a estrutura).
public object clone() {

7
O método next vai buscar à da lista o próximo nó, actualiza o objecto actual para o
conteúdo do nó recolhido e coloca no fim da lista os eventuais filhos desse nó. Se a fila
estiver vazia, o método devolve a referência nula.

91
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

if (isEmpty())
return new DBinTree();
return new DBinTree(root(), (DBinTree) (left().clone()),
(DBinTree) (right().clone()));
}
} // endInterface Visitor

Classe 5.1. Classe DBinTree.

A
A |B
|D
B C
|E
D E F |C
|[]
a) |F
b)
Figura 5.7. Representação da árvore binária a) e sua respectiva descrição
b).

5.2.5. Uso da Interface Visitor e a Classe DBinTree


/** Título: Operador para concatenar os conteúdos da árvore a percorrer.
*/
class Concat implements Visitor {
private String result = “”;
public void visit(Object info) { // da interface
result += (String) info;
}
public Object result() { // da interface
return result;
}
public void reset() {
result = “”;
}
/** O método main constroe um nó vazio partilhado por todas as folhas.
*/
Public static void main(String[] args) {
Concat cc = new Concat();
DBinTree tv = new DBinTree();
DBinTree t1 = new DBinTree(“F”, tv, tv);
DBinTree t2 = new DBinTree(“E”, tv, tv);
DBinTree t3 = new DBinTree(“D”, tv, tv);
DBinTree t4 = new DBinTree(“C”, tv, t1);
DBinTree t5 = new DBinTree(“B”, t3, t2);
DBinTree t6 = new DBinTree(“A”, t5, t4);

92
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

System.out.println(t6);
t6.prefix(cc);
System.out.print(cc.result() + “ ”);
cc.reset();
t6.infix(cc);
System.out.print(cc.result() + “ ”);
cc.reset();
t6.sufix(cc);
System.out.print(cc.result() + “ ”);
Iterator it = t6.iterator();
while (it.hasNext())
System.out.print(it.next() + “ / ”);
}

⎕A⏌
|B
|D
|E
|C
|[]
|F
ABDECF DBEACF DEBFCA ABCDEF
Classe 5.2. Classe Concat.

5.2.6. Representação Estática


A representação estática da árvore binária é a representação vectorial
do conteúdo e a estrutura da árvore, onde cada posição do vector é
constituída por:
i. Uma referência para o conteúdo,
ii. Um inteiro que representa o índice no vector da subárvore
esquerda e
iii. Um inteiro que representa o índice no vector da subárvore
direita (se não existir subárvore é igual a -1).

A 0 A 1 2
1 B 3 4
B C 2 C -1 5
3 D -1 -1
D E F 4 E -1 -1
5 F -1 -1
Figura 5.8. Árvore binária e sua respectiva representação por vector.

5.3. Árvores Genéricas

Árvore genérica é uma árvore que tem um número finito de filhos,


sendo a árvore binária um caso particular dela. As árvores genéricas
possuem uma referência para o

93
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

primeiro filho e uma referência para o próximo irmão (em que o


primeiro e o próximo são definidos por um certo critério).
A figura 5.9 ilustra a representação de uma árvore genérica e sua
respectiva estrutura.
4
/

4
7 4
/ /
7 2

3 5 9
3 5 9
/ / / /
Figura 5.9. Árvore genérica e sua respectiva estrutura.

5.3.1. Árvores (a,b) e Árvores-B


O acesso à leitura e escrita de informação em memória secundária é
muito mais lento, por isso torna-se necessário minimizá-lo para um
melhor desempenho dos sistemas
• Efectuar a leitura e escrita da memória secundária em conjuntos
de bytes (também designados por blocos ou páginas),
normalmente em potências de 2.
• Efectuar múltiplos acessos à memória primária visando reduzir
o número de acessos à memória secundária.
Uma árvore (a,b) é uma árvore em que
i. Cada nó, exceptuando a raiz e as folhas, contém no mínimo a-1
elementos e a filhos e no máximo b-1 elementos e b filhos, com
2≤a≤(b+1)/2
ii. Todas as folhas estão ao mesmo nível
iii. Os elementos a armazenar na árvore têm de ser comparáveis
entre si
iv. Os elementos estão ordenados em cada nó e os elementos
contidos em cada filho são maiores ou iguais ao elemento
anterior e menores que o elemento seguinte.
A altura de uma árvore (a,b) que armazena n elementos é Θ(log(n)), ou
seja, Ω(logb(n)) e O(loga(n)). As árvores genéricas armazenam de a-1 a
b-1 elementos por nó.

Algoritmo de pesquisa
A procura nas árvores (a,b) percorre o nó actual até encontrar o
elemento; se o elemento não se encontra nesse nó, percorre o nó
referenciado entre os dois elementos do nó actual no qual o elemento
a procurar eventualmente se situa. Na figura 5.10, ao procurar o
número 5. O algoritmo iria requisitar o nó referenciado entre os
números 4 e 7 para continual a pesquisa. Assim, o número de nós
percorridos (e carregados da memória secundária) pode ser
substancialmente reduzido.
94
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

4 7

/ 1 / 3 / / 5 / / 9 /
Figura 5.10. Representação da procura numa árvore (a,b).

Algoritmo de inserção
• Pesquisar por x. a pesquisa terminará numa determinada folha
(com insucesso se não for permitido repetições) numa
determinada posição.
• Adicionar x nessa mesma folha respeitando a ordem do nó.
• Se a folha, depois da inserção, tiver menos de b elementos,
parar. Senão:
o Escolher um elemento do meio (ou no caso de o número
de elementos ser par, escolher o primeiro elemento da
segunda partição) designado w. dividir essa folha em
duas novas folhas, F1 com os elementos (e filhos) até w
e F2 com os elementos (e filhos) depois de w. É devido a
este processo de divisão que se restringe os valores a
2≤a≤(b+1)/2.
• Passar w para o nó pai (se a folha a dividir era uma raiz, criar
uma nova raiz) e inseri-lo na posição correcta. O nó F1 será o
filho antes de w e F2 o filho depois de w.
• Caso o nó pai, após a inserção de w, ficar com b elementos,
repetir os dois últimos passos.

4 7 2 4 7

/ 1 / 3 / / 5 / / 9 / / 1 / / 3 / / 5 / / 9 /
a) c)

4 7 4

/ 1 / 2 / 3 / / 5 / / 9 / 2 7

b)
/ 1 / / 3 / / 5 / / 9 /
d)

Figura 5.11. Inserção do valor 2 na árvore (2,3).

A figura 5.11 ilustra a inserção do valor 2 numa árvore (2,3). O processo


inicia com a procura do valor 2, que para em 3 pois o valor procurado
deveria se encontrar entre 1 e 3. Na b) insere 2, mas a folha fica com
demasiados elementos. Então é realizada a separação em c), mas o nó
pai também fica em excesso. Finalmente a árvore é reorganizada.
A operação de divisão modifica um número constante de elementos e,
no máximo, há um número de divisões proporcional à altura da árvore.
A operação de inserção numa árvore (a,b) de n nós é O(log(n)).

95
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Algoritmo de remoção
• Encontrar o elemento x.
• Trocar x pelo próximo elemento, i.e., o elemento que está mais
à esquerda da árvore da direita, ou se esta estiver vazia pelo
próximo elemento. Repetir até x chegar a uma folha.
• Apagar x. Se a folha tiver a-1 elementos ou mais parar. Senão:
o Verificar se a soma dos elementos da folha anterior com
os da folha actual é menor que b-1. Se for, fundir essas
folhas juntamente com o elemento pai e formar uma
nova folha (pode significar uma nova iteração de todo
este processo, porque agora o nó pai ficou com menos
um elemento). Se não for, transferir elementos de uma
das folhas para a outra (incluindo o valor do nó pai) de
modo a restabelecer o número mínimo de elementos
por nó.

4 7 5 / 7

/ 1 / 3 / / 5 / / 9 / / 1 / 3 / / 9 /
a) c)

5 7 3 7

/ 1 / 3 / / 4 / / 9 / / 1 / / 5 / / 9 /
b) d)
Figura 5.12. Remoção do elemento 4 da árvore (2,3).

A figura 5.12. ilustra a remoção do 4 da árvore (2,3). 5.12.a) Troca 4 pelo


próximo. 5.12.b) chegou a uma folha: apagar! 5.12.c) Uma das folhas
tem menos que o número admissível! 5.12.d) Reagrupar com a folha
anterior.

3 / 7 7

/ 1 / / 9 / / 1 / 3 / / 9 /
a) b)
Figura 5.13. Remoção do elemento 5.

Na figura 5.13 a folha (que ficou vazia), a folha anterior mais o elemento
pai (o número 3) têm elementos suficientes para criar uma nova folha.
O nó pai ficou com menos um elemento (7). Se o nó pai ficar com menos
elementos que o admitido, provaca-se um novo processo de fusão que
pode ser propagado até a raiz. É somente nestes casos que as árvores
(a,b) podem perder a altura.

96
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

O processo de troca tal como o processo de fusão ocorrem no máximo,


um número de vezes igual a altura da árvore. Assim, a complexidade da
remoção numa árvore (a,b) com n elementos é O(log(n)).

Uma árvore-B (do inglês, B-tree), estrutura concebida por R. Bayer em


1970, é uma variante das árvore (a,b). Dado um valor inteiro e positivo
n, a árvore-B de ordem n é uma árvore (⎾n/2⏋, n).

Uma árvore-B+ é uma variante da árvore-B onde as chaves dos


elementos a procurar encontram-se nos nós intermediários (são
aquelas que orientam a procura) e a informação dos elementos é
armazenada somente nas folhas. As árvores-B+ são geralmente usadas
para armazenar informação em memória secundária. O valor de n deve
ser igual ao número de elementos (incluindo as referências para os
filhos) que podem ser armazenados numa página, i.e., num único
acesso de informação à memória secundária.

5.4. Sumário

Neste tema estudamos e discutimos fundamentalmente as árvores


binárias e genéricas. Estudamos os percursos de uma árvore binária, a
especificação e interface da mesma. Discutimos ainda a implementação
dinâmica e representação estática da árvore binária, assim como os
algoritmos das árvores (a-b).

5.5. Exercícios de Auto-Avaliação

Perguntas
1. As árvores binárias são também conhecidas como árvores n-árias.
A. Verdadeiro
B. Falso
2. As árvores binárias são um caso especial das árvores genéricas.
A. Verdadeiro
B. Falso
3. Tal como as árvores binárias as árvores genéricas contêm apenas
um elemento em cada nó.
A. Verdadeiro
B. Falso
4. A árvore genérica é um caso particular das árvores binárias.
A. Verdadeiro
B. Falso
5. A figura que se segue representa uma árvore (a-b).

97
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

7 2

3 5 9

A. Verdadeiro
B. Falso

Respostas
1. B; 2. A; 3. B; 4. B; 5. B

98
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

TEMA 6. IMPLEMANTAÇÃO DE ESTRUTURAS DINÂMICAS EM MEMÓRIA SECUNDÁRIA

Unidade temática 6.1. Introdução


Unidade temática 6.2. Memória fila
Unidade temática 6.3. Memória pilha
Unidade temática 6.2. Memória fila com prioridade

6.1. Introdução

Ao completar este tema, você deverá ser capaz de:


▪ Caracterizar as memórias fila, pilha e fila com prioridade;
▪ Implementar as memórias fila, pilha e fila com prioridade.

Objectivos
específicos
Os tipos de dados abstractos que armazenam colecções de dados são
deginados memórias. Estes definem a política de acesso aos dados da
colecção. Para o escopo deste módulo abordaremos apenas as
memórias fila, pilha e fila com prioridade.
Existem essencialmente três tipos de implementação da memória do
computador
• Implementação estática
• Implementação dinâmica
• Implementação semi-estática

6.2. Memória Fila

A memória fila (do inglês queue) é uma memória que só é possível


processar os seus elementos pela ordem de chegada, isto é, o primeiro
a chegar será o primeiro a sair (FIFO, Fisrt In First Out).

99
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

6.2.1. Caracterização da Memória Fila


public interface GenericQueueIneterface<T> {
//Especificação da GenericQueue
/** Verifica se a fila está vazia. Pré-condições: nenhuma. Pós-condições:
TRUE em caso afirmativo e FALSE caso contrário. */
public boolean isEmpty();
/** Verifica se a fila está cheia. Pré-condições: nenhuma. Pós-condições:
TRUE em caso afirmativo e FALSE caso contrário. */
public boolean isFull();
/** Insere o elemento pElem na cauda da fila. Pré-condições: fila não
cheia e elemento válido. Pós-condições: pElem fica colocado na
cauda da fila e o número de elementos da fila é incrementado uma
unidade. */
public void enqueue(T pElem)
throws NullPointerException, QueueFullException;
/** Retira o elemento pElem da cabeça da fila e devolve-o. Pré-condições:
fila não vazia. Pós-condições: valor de retorno = elemento da cabeça
da fila e o número de elementos da fila é decrementado uma unidade.
*/
public T dequeue() throws QueueEmptyException;
}
Interface 6.1. Memória fila.

A inserção de um novo elemento na fila, enqueue(), consiste na adição


de um novo elemento no fim da fila, a cauda (do inglês queue tail) e na
escrita da informação nesse elemento.
A remoção de um elemento na fila, dequeue (), consiste na leitura da
informação armazenada no elemento que se encontra na cabeça da fila
(do inglês queue head) e a sua eliminação da fila. Consequentemente o
elemento seguinte, caso haja, passa a ser a cabeça da fila.
As outras operações são: isEmpty() verifica se a fila está vazia. isFull()
verifica se a fila está cheia. size() determina o número de elementos
efectivamente armazenados na fila.

6.2.2. Implementação da Memória Fila


A implementação estática da fila é apenas realizada por tipos de dados
primitivos em forma circlar. Ao inserir um novo elemento na fila, o
indicador da cauda é deslocado para o elemento seguinte da sequência.
Ao remover um elemento da fila, o indicador da cabeça é deslocado
para o elemento seguinte da sequência, evitando assim que toda a fila
seja deslocada para a frente.

100
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Queue[0] Queue[1]
dequeue

cabeça da fila

Queue[N-1]
Queue[2]

...
...

Queue[I]
cauda da fila
...
enqueue

Figura 6.1. Implementação estática da memória fila.


Ao inserir um novo elemento, se a cauda da fila se sobrepõe a cabeça é
sinal que a fila está cheia. Ao remover um elemento, se a cabeça da fila
se sobrepõe a cauda é sinal que a fila ficou vazia.

A implementação semi-estática é apenas utilizada para elementos do


tipo de dados referências. Implementação similar a estática com as
seguintes diferenças: (1) a operção de inserção de um elemento na fila
é responsável pela ligação do elemento à memória e (2) a operação de
remoção de um elemento é responsável pela desligação do elemento
da memória.

Queue[0]
Queue[1]
dequeue

Queue[2]
Queue[N-1]
Elemento
cabeça
da fila

cauda ...
da fila
...
Elemento

Queue[I]
...
Elemento
enqueue

Figura 6.2. Implementação semi-estática da memória fila.

A implementação dinâmica basea-se na lista ligada. Cada nó da lista é


constituído pelo elemento que armazena a informação ou pela
referência para o elemento e pela referência para o nó seguinte da lista.

101
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

O último nó da lista aponta para null, indicando o fim da fila.


A memória para os nós da lista é atribuída quando um é elemento é
adicionado na fila e é libertada quando um elemento é removido da fila.
A cabeça da fila é uma referência para o elemento mais antigo da fila e
que será o primeiro a ser removido. A cauda da fila é uma referência
para o último elemento que foi inserido na fila e à frente do qual um
novo elemento pode ser inserido. A indicação de fila vazia é quando a
cabeça e a cauda ambos apontar para null. Note que a fila dinâmica
nunca está cheia, pode sim é faltar memória para inserir novos
elementos.

cabeça

enqueue

Elemento Elemento Elemento Elemento


cauda
dequeue
Figura 6.3. Implementação dinâmica da memória fila.

6.3. Memória Pilha

Na memória pilha (do inglês stack) só é possível processar os elementos


armazenados a partir do topo, isto é, o último elemento a chegar é o
primeiro a sair (LIFO, Last In First Out).

6.3.1. Caracterização da Memória Pilha


public interface GenericStackInterface<T> {
//Especificaçao da GenericPriorityQueue
/** Verifica se a pilha está vazia. Pré-condições: nenhuma. Pós-
condições: TRUE em caso afirmativo e FALSE caso contrário. */
public boolean isEmpty();
/** Verifica se a pilha está cheia. Pré-condições: nenhuma. Pós-
condições: TRUE em caso afirmativo e FALSE caso contrário. */
public boolean isFull();
/** Insere o elemento pElem no topo da pilha. Pré-condições: fila não
cheia e elemento válido. Pós-condições: pElem = elemento do topo da
pilha e o número de elementos da pilha é incrementado uma unidade.
*/
public void push(T pElem)
throws NullPointerException, StackFullException;
/** Retira o elemento pElem do topo da pilha e devolve-o. Pré-condições:
pilha não vazia. Pós-condições: valor de retorno = elemento do topo
da pilha e o número de elementos da pilha é decrementado uma
unidade. */
public T pop() throws StackEmptyException;
}
Interface 6.2. Memória pilha.
102
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

A colocação de um novo elemento na pilha, push(), consiste na adição


de um novo elemento em cima do topo da pilha e na escrita da
informação nesse elemento, passando este novo elemento a ser o topo.
A remoção de um elemento na pilha, pop(), consiste na leitura da
informação armazenada no elemento do topo da pilha e a sua
eliminação, ficando o elemento anterior, caso exista, o topo da pilha.
As outras operações são: isEmpty() verifica se a fila está vazia. isFull()
verifica se a fila está cheia. size() determina o número de elementos
efectivamente armazenados na fila com prioridade.
Quando é removido o último elemento da pilha, esta fica vazia.

6.3.2. Implementacao da Memória Pilha


A implementação estática só é possível para o tipo de dados primitivos.
A pilha tem um indicador numérico que indica a posição do elemento
que é o topo da pilha e a primeira posição livre para a próxima inserção
de um novo elemento. Sempre que se insere um novo elemento na
pilha, o indicador de topo da pilha é deslocado para o elemento
seguinte da sequência. Uma pilha está cheia quando o topo atinge a
posição N (excede o elemento final da sequência). Sempre que se retira
um elemento da pilha, (i) o indicador de topo da pilha é deslocado para
o elemento anterior da sequência e (ii) o elemento é retirado da
sequência. Uma pilha está vazia quando o indicador de topo atinge a
posição zero (chega ao elemento inicial da sequência).
Elemento Stack[N-1]
Elemento ...

Elemento Stack[I]
topo da
push Elemento ...
pilha

pop Elemento Stack[2]

Elemento Stack[1]

Elemento Stack[0]
Figura 6.4. Implementação estática da memória pilha.

A implementação semi-estática só é possível para o tipo de dados


referências. Está implementação assemelha-se a implementação
estática, porém, (i) a de inserção de um novo elemento é responsável
pela ligação do elemento à memória e (ii) a remoção de um elemento é
responsável pelo desligamento do elemento da memória.

103
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Stack[N-1]
...

Stack[I]

... topo da pilha push

Stack[2] Elemento pop

Stack[1] Elemento

Stack[0] Elemento
Figura 6.5. Implementação semi-estática da memória pilha.

A implementação dinâmica é baseada em uma lista ligada, em que cada


nó aponta para o nó anterior e o primeiro nó aponta para null, servindo
assim de indicador de início da pilha. A memória para os nós da lista é
atribuída quando um elemento é inserido na pilha e é libertada quando
um elemento é removido da pilha. O indicador do topo da pilha é uma
referência que aponta para o último elemento que foi colocado na
pilha, que será o primeiro elemento a ser removido e à frente do qual
um novo elemento será inserido. Quando o topo da pilha aponta para
null é sinal de que a pilha está vazia. Uma pilha dinâmica nunca está
cheia, pode sim faltar memória para continuar a inserir novos
elementos.
push
topo da
pilha Elemento pop

Elemento

Elemento

Figura 6.6. Implementação dinâmica da memória pilha.

6.4. Memória Fila com Prioridade

Filas com prioridade são estruturas em que os elementos são com uma
chave indicando as suas prioridades e que para o acesso aos elementos
consideram-se os de maior ou menor prioridade.
A fila com prioridade é uma memória simplificada, pois somente se
acede a um elemento com uma chave específica. Ela pode ou não
manter os elementos ordenados. Se a fila tem os elementos ordenados
é facilitada a remoção dos elementos e dificultada a sua inserção, caso

104
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

contrário a inserção é facilitada e a remoção dificultada.

6.4.1. Caracterização da Memória Fila com Prioridade


public interface GenericPQueue<T extends Comparable<T>> {
//Especificaçao da GenericPriorityQueue
/** Verifica se a fila com prioridade está vazia. Pré-condições: nenhuma.
Pós-condições: TRUE em caso afirmativo e FALSE caso contrário. */
public boolean isEmpty();
/** Verifica se a fila com prioridade está cheia. Pré-condições:
nenhuma. Pós-condições: TRUE em caso afirmativo e FALSE caso
contrário. */
public boolean isFull();
/** Insere o element pElem na PQueue. Pré-condições: fila com
prioridade não cheia e elemento válido. Pós-condições: pElem fica
colocado na posição correcta da PQueue e o número de elementos
da fila é incrementado uma unidade. */
public void insert(T pElem)
throws NulPointerException, PQueueFullException;
/** Retira o maior element da fila com prioridade e devolve-o. Pré-
condições: fila com prioridade não vazia. Pós-condições: valor de
retorno = maior elemento da PQueue e o número de elementos da
fila é decrementado uma unidade. */
public T deleteMax() throws PQueueEmptyException;
}
Interface 6.3. Memória fila com prioridade (orientada aos máximos).

A remoção de um elemento na fila com prioridade, deleteMax(),


consiste na leitura da informação armazenada no elemento com a
maior (caso organizada com prioridade aos máximos) chave e a sua
eliminação da fila.
A inserção de um novo elemento na fila com prioridade, insert(),
consiste na adição de um novo elemento na fila e na escrita da
informação nesse elemento. O tamanho da fila com prioridade é
dinâmico e depende do número de elementos inseridos ou retirados da
fila. A posição de inserção de um novo elemento na fila com prioridade
normalmente é a primeira posição livre da fila, excepto quando a fila
está ordenada, em que a posição de inserção corresponde à sua chave
de acesso, mantendo assim a fila ordenada.
As outras operações são: isEmpty() verifica se a fila está vazia. isFull()
verifica se a fila está cheia. size() determina o número de elementos
efectivamente armazenados na fila com prioridade.
Quando é removido o último elemento da fila com prioridade esta fica
vazia. Em certas aplicações torna-se necessário promover (aumentar a
prioridade, increase) ou despromover (diminuir a prioridade, decrease)
os elementos presentes na fila.

105
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

6.4.2. Implementação da Memória Fila com Prioridade


A implementação estática só é possível para os tipos de dados
primitivos. Para implementar a pesquisa da memória, é necessário que
esta mantenha um contador que indica o número de elementos úteis
armazenados, um indicador da posição do último elemento da memória
onde foi escrita a informação. As operações de inserção e remoção de
elementos na memória são responsáveis pela actualização deste
contador.
Na figura 6.7 os elementos são inseridos de forma a estarem sempre
ordenados por ordem crescente.
deleteMax

PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]


CHAVE 1 CHAVE 2 CHAVE 3 CHAVE 4 CHAVE 5
Elemento Elemento
Elemento Elemento Elemento Elemento Elemento
pesquisa binária
insert
Figura 6.7. Implementação estática ordenada da fila com prioridade
(orientada aos máximos).

Para inserir um novo elemento deve-se determinar a sua posição de


colocação na sequência, sendo a pesquisa binária a mais eficiente. Se a
inserção é feita entre elementos já existentes, deslocam-se os
elementos na posição de inserção e os seguintes uma posição para a
frente, para libertar essa posição da sequência.
A remoção de elementos fica simplificada, pois remove-se o elemento
com maior chave e este se encontra sempre na última posição ocupada
da sequência. Apos a remoção de um elemento, é necessário
decrementar o contador de elementos para reutilizar essa posição na
próxima inserção.
A figura 6.8 apresenta a implementação estática não ordenada baseada
numa sequência de elementos, que são colocados na sequência pela
ordem de inserção na fila com prioridade. A inserção de um novo
elemento é feita na primeira posição livre da sequência (a seguir ao
último elemento armazenado na sequência).
A remoção do elemento com maior chave exige determinar a sua
posição de armazenamento, usando a pesquisa sequencial. Para evitar
o deslocamento de elementos desencadeado pela remoção, o
elemento armazenado na última posição da sequência é copiado para
essa posição e o contador de elementos efectivamente armazenados é
decrementado.
PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]
CHAVE 1 CHAVE 2 CHAVE 3 CHAVE 4 CHAVE 5
Elemento Elemento
Elemento Elemento Elemento Elemento Elemento
pesquisa sequencial
deleteMax insert

106
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Figura 6.8. Implementação estática não ordenada da fila com prioridade.

A figura 6.9 apresenta uma implementação semi-estática ordenada. As


operações de inserção e de remoção de elementos são responsáveis,
respectivamente, pela ligação do elemento à memória e pela desligação
do elemento da memória.
PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]

CHAVE 1 CHAVE 2 CHAVE 3 CHAVE 4 CHAVE 5


Elemento Elemento Elemento Elemento Elemento
pesquisa binária
insert deleteMax
Figura 6.9. Implementação semi-estática ordenada da fila com prioridade.

A implementação dinâmica, seja ela ordenada ou não, é a


implementação mais eficiente da fila com prioridade, por exemplo uma
lista ligada.
A figura 6.10 apresenta a implementação dinâmica linear ordenada da
fila com prioridade (para elementos ordenados de um tipo de dados
referência) baseada numa lista ligada, sendo os elementos inseridos de
forma a estarem sempre ordenados por ordem decrescente.
cabeça

CHAVE 5 CHAVE 4 CHAVE 3 CHAVE 2 CHAVE 1


Elemento Elemento Elemento Elemento Elemento
pesquisa sequencial
deleteMax insert
Figura 6.10. Implementação dinâmica linear ordenada da fila com
prioridade.

Para a inserção de um novo elemento, é preciso determinar a sua


posição de colocação na lista, implicando pesquisar sequencialmente a
lista. O elemento com a maior chave está sempre colocado sempre na
cabeça da lista, simplificando assim a sua remoção.
Uma estrutura de dados dinâmica hierárquica, tipo árvore binária,
permite a implementação mais eficiente da fila com prioridade pois
permite a pesquisa binária. Sendo que a remoção precise apenas aceder
a maior chave da fila, um amontoado binário seria a ideal
implementação, pois pode ser armazenada em uma sequência,
poupando memória e simplificando as operações de travessia da
árvore.
A figura 6.11 apresenta a implementação da fila com prioridade
baseada em um amontoado binário, neste caso numa implementação
estática só possível para armazenar elementos dos tipos de dados

107
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

primitivos, sendo que os elementos estão sempre parcialmente


ordenados por ordem decrescente, com o maior valor sempre colocado
à raiz do amontoado (posição inicial da sequência).
PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]
CHAVE 6 CHAVE 5 CHAVE 1 CHAVE 4 CHAVE 3
Elemento Elemento
Elemento Elemento Elemento Elemento Elemento
ajustar o amontoado após cada operação de inserção/remoção
deleteMax insert

Figura 6.11. Implementação da fila com prioridade com um amontoado


binário.

A inserção de elementos e a remoção do maior elemento de um


amontoado binário são operações com eficiencial logarítmica. Apesar
disso, esta implementação é estática, pelo que para além de precisar
de um contador, para controlar o número de elementos efectivamente
armazenados na fila com prioridade, tem as limitações inerentes às
estruturas de dados estáticas. As aplicações que usam uma fila com
prioridade conseguem determinar a priori a dimensão da fila que
precisam.
Caso se pretenda armazenar elementos dos tipos de dados referências,
então usa-se a implementação semi-estática que pode ser generalizada.

6.5. Sumário

Neste tema estudamos e discutimos fundamentalmente a


caracterização das memórias fila, pilha e fila com prioridade.
Abordamos ainda os diferentes tipos de implementação das memórias
fila, pilha e fila com prioridade.

6.6. Exercícios de Auto-Avaliação

Perguntas
1. Na memória pilha a operação push() retira o elemento do topo da
pilha e devolve-o.
A. Verdadeiro
B. Falso
2. A memória fila retira o elemento que se na cauda da fila através da
operação dequeue().
A. Verdadeiro
B. Falso
3. A memória fila com prioridade utiliza a pesquisa binária na inserção
de um novo elemento.
A. Verdadeiro
B. Falso
4. A implementação dinâmica da memória fila com prioridade utiliza a
pesquisa sequencial na inserção de um novo elemento.

108
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

A. Verdadeiro
B. Falso
5. Na memória fila com prioridade é imperioso que os elementos
estejam ordenados para os poder aceder.
A. Verdadeiro
B. Falso

Respostas
1. B; 2. B; 3. A; 4. A; 5. B

109
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

TEMA 7. FILAS E ÁRVORES COM PRIORIDADE

Unidade temática 7.1. Introdução


Unidade temática 7.2. Fila com prioridade
Unidade temática 7.3. Árvore com prioridade

7.1. Introdução

Ao completar este tema, você deverá ser capaz de:


▪ Identificar as filas e as árvores com prioridades;
▪ Compreender as diferentes formas de implementação das filas e
das árvores com prioridade.
Objectivos
específicos
A fila com prioridade é uma colecção de dados organizada de tal forma
que somente é possível retirar o dado que nesse instante tem a maior
prioridade (maior ou menor chave) entre os elementos da colecção.

7.2. Fila com Prioridade

Filas com prioridade são estruturas em que os elementos são


armazenados com uma chave indicando as suas prioridades e que para
o acesso aos elementos consideram-se os de maior ou menor
prioridade.
A fila com prioridade é uma memória simplificada, pois somente se
acede a um elemento com uma chave específica. Ela pode ou não
manter os elementos ordenados. Se a fila tem os elementos ordenados
é facilitada a remoção dos elementos e dificultada a sua inserção, caso
contrário a inserção é facilitada e a remoção dificultada.

7.2.1. Implementação Estática


Esta implementação exige um contador de número de elementos úteis
armazenados na sequência, que funciona como indicador da posição do
último elemento. Quando o contador é zero, a fila com prioridade está
vazia e, quando é igual a N, a fila está cheia. A figura 7.1 apresenta o
comportamento da operação de inserção de elementos com chave 3,
chave 6 e chave 4 na fila com prioridade ordenada orientada aos
máximos.

110
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]


numElem
Elemento Elemento Elemento Elemento Elemento Elemento Elemento
0

PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]


CHAVE 3 numElem
Elemento Elemento Elemento Elemento Elemento Elemento
Elemento 1

PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]


CHAVE 3 CHAVE 6 numElem
Elemento Elemento Elemento Elemento Elemento
Elemento Elemento 2

PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]


CHAVE 3 CHAVE 4 CHAVE 6 numElem
Elemento Elemento Elemento Elemento
Elemento Elemento Elemento 3
Figura 7.1. Inserção de vários elementos na fila com prioridade estática
ordenada.

Para que os elementos fiquem colocados com a chave ordenada, a


inserção de um novo elemento implica a pesquisa da sua posição de
insercaona sequência. Como referido na secção 6.4, a pesquisa binária
é a mais eficiente para o caso. Caso essa posição não seja à frente do
último elemento armazenado, os elementos colocados na posição de
inserção e seguintes são deslocados uma posição para a frente, para
libertar essa posição da sequência. Razão pela qual quando o elemento
com a chave 4 chega, o elemento de chave 6 é copiado da posição 1
para a posição 2 e o novo elemento é colocado na posição 1 da
sequência.
A figura 7.2 apresenta o comportamento da operação de remoção dos
dois elementos com a maior chave da fila com prioridade (elementos
com chave 6 e chave 4). Como os elementos estão armazenados por
ordem crescente, então os máximos se encontram sempre na última
posição ocupada da sequência. A operação consiste em fazer a cópia do
elemento e decrementar o número de elementos efectivamente
armazenados na fila com prioridade.
PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]
CHAVE 3 CHAVE 4 CHAVE 6 numElem
Elemento Elemento Elemento Elemento
Elemento Elemento Elemento 3

PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]


CHAVE 3 CHAVE 4 numElem
Elemento Elemento Elemento Elemento Elemento
Elemento Elemento 2

PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]


CHAVE 3 numElem
Elemento Elemento Elemento Elemento Elemento Elemento
Elemento 1
Figura 7.2. Remoção de vários elementos na fila com prioridade estática
ordenada.

111
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

A classe 7.1 implementa a fila com prioridade estática ordenada


concreta que armazena carecteres, simplificando o controlo de erros. A
estrutura de dados da fila com prioridade é constituída pela sequência
pQueue de caracteres e pela variável inteira nElem para indicar o
número de elementos armazenados na fila.

public class OrdArrayCharPQueue {


private char[] pQueue; //área de armazenamento da fila
private int nElem; //número de elementos armazenados na fila
/** Construtor da fila. Cria a sequência com a dimensão indicada e
inicializa o contador de número de elementos. */
public OrdArrayCharQueue(int pSize) { //
if (pSize > 0) {
pQueue = new char[pSize];
nElem = 0;
}
}
public void insert(int pElem) { // método de inserção
if (isFull())
return; // fila cheia, não é possível inserir
int pi = findPlace(pQueue, nElem, pElem); // procurar a posição
if (pi != nElem)
shiftDown(pQueue, nElem, pi) // deslocar os elementos
pQueue[pi] = pElem; // inserir
nElem++; // actuazação do contador
}
public char deleteMax() { // método de remoção
if (isEmpty())
return; // fila vazia, não é possível remover
nElem--; // actuazação do contador
return pQueue[nElem]; // remoção e devolução do elemento
}
public boolean isEmpty() { // método de teste da fila vazia
return nElem == 0;
}
public boolean isFull() { // método de teste da fila cheia
return nElem == pQueue.length;
}
/** Devolve a posição de inserção do elemento. Caso já existam
elementos com uma mesma chave, a posição de inserção será a
seguir aos elementos já existentes. */
public static int findPlace(char[] pPQ, int pN, char pElem) {
int pos = binSearch (pPQ, pN, pElem);
if (pPQ[pos] != pElem)
return pos;
do {
pos++;
} while (pos < pN && pPQ[pos] == pElem);
return pos;

112
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

}
/** Pesquisa binária que determina se o elemento pretendido existe na
sequência. Caso exista, devolve a sua posição. Caso contrário,
devolve a posição onde o elemento devia estar. */
private static int binSearch(char[] pPQ, int pN, char pElem) {
int min = 0, med = 0, max = pN-1;
if (pN == 0)
return 0;
while (min <= max) {
med = (min + max) / 2;
if (pPQ[med] < pElem)
max = med - 1;
else if (pPQ[med] < pElem)
min = med + 1;
else
break;
}
if (pPQ[med] >= pElem)
return med;
else
return med + 1;
}
/** Desloca todos os elementos, a partir da posição pPos inclusive, uma
posição para a frente. */
private static void shiftDown (char[] pPQ, int pN, int pPos) {
for (int i = pN; i > pPos; i--)
pPQ[i] = pPQ[i-1];
}
}
Classe 7.1. Memória fila com prioridade estática ordenada concreta.

A inserção tem uma complexidade logarítmica de operações, pois a


pesquisa da posição de inserção usa a pesquisa binária. Porém, para
libertar a posição da sequência onde deve ser colocado o novo
elemento a complexidade é linear. A remoção tem complexidade nula.
A figura 7.3 apresenta o comportamento da operação de inserção dos
elementos com chave 3, chave 6 e chave 4 na fila com prioridade não
ordenada. Nesta implementação a inserção é sempre feita na primeira
posição livre da sequência.

113
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]


numElem
Elemento Elemento Elemento Elemento Elemento Elemento Elemento
0

PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]


CHAVE 3 numElem
Elemento Elemento Elemento Elemento Elemento Elemento
Elemento 1

PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]


CHAVE 3 CHAVE 6 numElem
Elemento Elemento Elemento Elemento Elemento
Elemento Elemento 2

PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]


CHAVE 3 CHAVE 6 CHAVE 4 numElem
Elemento Elemento Elemento Elemento
Elemento Elemento Elemento 3
Figura 7.3. Inserção de vários elementos na fila com prioridade estática não
ordenada.

A figura 7.4 apresenta o comportamento da operação de remoção dos


três elementos com maior chave da fila com prioridade. Deve se realizar
a pesquisa sequencial, pois os elementos não estão armazenados
ordenadamente pela chave.
PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]
CHAVE 3 CHAVE 6 CHAVE 4 CHAVE 5 numElem
Elemento Elemento Elemento
Elemento Elemento Elemento Elemento 4

PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]


CHAVE 3 CHAVE 5 CHAVE 4 numElem
Elemento Elemento Elemento Elemento
Elemento Elemento Elemento 3

PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]


CHAVE 3 CHAVE 4 numElem
Elemento Elemento Elemento Elemento Elemento
Elemento Elemento 2

PQueue[0] PQueue[1] PQueue[2] PQueue[3] PQueue[4] ... PQueue[N-1]


CHAVE 3 numElem
Elemento Elemento Elemento Elemento Elemento Elemento
Elemento 1
Figura 7.4. Remoção de vários elementos na fila com prioridade estática
não ordenada.

Depois de copiar o elemento da sequência, a sua posição é ocupada


pelo elemento que se encontra na última posição da sequência, caso
este não seja o elemento removido. E o número de elementos
armazenado na sequência é decrementado.
A classe 7.2 implementa a fila com prioridade estática não ordenada
concreta que armazena caracteres, simplificando o controlo de erros.
Note que diverge da versão ordenada apenas nas operações de inserção
de elementos e remoção do elemento com maior chave.

114
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

public class ArrayCharPQueue {


private char[] pQueue; //área de armazenamento da fila
private int nElem; //número de elementos armazenados na fila
public ArrayCharQueue(int pSize) { // construtor da fila
if (pSize > 0) {
pQueue = new char[pSize];
nElem = 0;
}
}
public void insert(char pElem) { // método de inserção
if (isFull())
return; // fila cheia, não é possível inserir
pQueue[nElem] = pElem; // inserir no fim da fila
nElem++; // actuazação do contador
}
public char deleteMax() { // método de remoção
if (isEmpty())
return; // fila vazia, não é possível remover
int pr = searchMax(pQueue, nElem); // procurar a posição
char c = pQueue[pr]; // cópia do elemento
nElem--; // actuazação do contador
if (pr != nElem)
pQueue[pr] = pQueue[nElem]; // remoção do elemento
return c; // devolução do elemento
}
public boolean isEmpty() { // método de teste da fila vazia
return nElem == 0;
}
public boolean isFull() { // método de teste da fila cheia
return nElem == pQueue.length;
}
/** Pesquisa sequencial, determina e devolve a posição da sequência
onde está armazenado o elemento com a maior chave. */
public static int searchMax(char[] pPQ, int pN) {
int pos = 0;
for (int i = 1; i < pN; i++)
if (pPQ[i] > pPQ[pos])
pos = i;
return pos;
}
}
Classe 7.2. Memória fila com prioridade estática não ordenada.

A operação de inserção tem uma complexidade nula, pois não exige


qualquer pesquisa ou deslocamento de elementos. A operação de
remoção do elemento com maior chave tem a complexidade linear por
exigir a determinação da posição usando a pesquisa linear. Esta
implementação permite a existência de elementos distintos com a
mesma chave na fila, sendo estes armazenados por ordem cronológica
de inserção. Porem, não há garantias
115
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

de que a remoção de elementos distintos com a mesma chave vá ser


feita por ordem cronológica, devido à cópia do último elemento para
colmatar o elemento removido.

7.2.2. Implementação semi-estática


Com excepção da necessidade de criar e eliminar os elementos
armazenadores de informação (objects) por parte do programa que usa
a fila com prioridade, o funcionamento da implementação semi-estática
assemelha-se ao da implementação estática. A classe 7.3 implementa a
fila com prioridade semi-estática ordenada genérica parametrizada
com lançamento de excepções.
public class GenericOrdArrayCharPQueue<T extends Comparable<T>> {
private T[] pQueue; //área de armazenamento da fila
private int nElem; //número de elementos armazenados na fila
// suprimir o aviso gerado pelo
@SuppressWarnings(“unchecked”)
// construtor da fila
public GenericOrdArrayCharQueue(int pSize) throws
NegativeArraySizeException {
if (pSize <= 0)
throw new NegativeArraySizeException();
pQueue = (T[]) new Comparable[pSize];
nElem = 0;
}
// método de inserção
public void insert(T pElem) throws NullPointerException,
PQueueFullException {
if (isFull())
throw new PQueueFullException(); // fila cheia
if (pElem == null)
throw new NullPointerException(); // fila sem elementos
int pi = findPlace(pQueue, nElem, pElem); // procurar a posição
if (pi != nElem)
shiftDown(pQueue, nElem, pi); // deslocar elementos
pQueue[pi] = pElem; // inserir
nElem++; // actuazação do contador
}
// método de remoção
public T deleteMax() throws PQueueEmptyException{
if (isEmpty())
throw new PQueueEmptyException(); // fila vazia
nElem--; // actuazação do contador
T elem = pQueue[nElem]; // elemento máximo da fila
pQueue[nElem] = null; // eliminar a referência
return elem; // devolução do elemento
}
public boolean isEmpty() { // método de teste da fila vazia
return nElem == 0;
}

116
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

public boolean isFull() { // método de teste da fila cheia


return nElem == pQueue.length;
}
/** Desloca todos os elementos a partir da posição pPos inclusive, uma
posição para a frente. */
public static int shiftDown(T[] pPQ, int pN, int pPos) {
for (int i = pN; i > pPos; i++)
pPQ[i] = pPQ[i-1];
}
/** Devolve a posição de inserção do elemento. Caso já existam
elementos com a mesma chave, a posição de inserção será a seguinte
aos elementos já existentes. */
public static int findPlace(T[] pPQ, int pN, T pElem) {
int pos = binSearch(pPQ, pN, pElem);
if (pPQ[pos] != pElem)
return pos;
do {
pos++;
} while (pos < pN && pPQ[pos].compareTo(pElem) == 0);
return pos;
}
/** Pesquisa Binária. Determina se o elemento pretendido existe na
sequência. Em caso afirmativo, devolve a sua posição. Caso contrário,
devolve a posição onde o elemento devia estar. */
public static int binSearch(T[] pPQ, int pN, T pElem) {
int min = 0;
int med = 0;
int max = pN-1;
if (pN == 0)
return 0;
while (min <= max) {
med = (min + max) / 2;
if (pPQ[med].compareTo(pElem) > 0)
max = med - 1;
else if (pPQ[med].compareTo(pElem) < 0)
min = med + 1;
else
break;
}
if (pPQ[med].compareTo(pElem) >= 0)
return med;
else
return med + 1;
}
}
Classe 7.3. Memória fila com prioridade semi-estática ordenada genérica.

Quando um programa pretende inserir um elemento na fila com


prioridade, tem de criar o elemento antes de invocar o método de
inserção. Quando se retira um

117
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

elemento da fila, é necessário libertar a memória por ele ocupada.


Assim, depois de se fazer a cópia do elemento, a sua referência deve ser
libertada (pQueue[nElem = null;) para que o elemento deixe de estar
referenciado e seja depois eliminado pelo gestor de objectos.

7.2.3. Implementação dinâmica


Como já visto na secção 6.4, uma fila com prioridade dinâmica é
baseada numa lista biligada. Ela pode ser ordenada (ver figura 6.10) ou
não. Quando ordenada a operação de remoção é simplificada. A lista é
constituída por nós decompostos onde cada nó é constituído por três
referências, uma para o elemento que armazena a informação, outra
para o elemento anterior da lista e outra para o elemento seguinte da
lista. O primeiro elemento aponta para trás para null e o último
elemento aponta para frente para null, servindo de indicadores de
terminação da lista. Quando um novo elemento é inserido na lista é
atribuída memória para este elemento e é libertada a memória quando
um elemento é removido da lista.
O indicador de cabeça da fila com prioridade é uma referência que
aponta para o elemento de maior chave, o primeiro elemento a ser
retirado da fila. Uma fila com prioridade está vazia quando o indicador
de cabeça é uma referência nula; e, nunca está cheia, mas pode faltar
memória para continuar a acrescentar elementos.
A inserção de um novo elemento na fila com prioridade inicia com a
criação do nó da lista que é ligado o elemento a ser armazenado. Caso
novo elemento seja o primeiro da fila, a cabeça passa a apontar para
este.
A figura 7.5.a) apresenta a fila com prioridade vazia. A figura 7.5.b)
apresenta a colocação do primeiro elemento na fila com prioridade. Na
figura 7.5.c) é inserido um novo elemento, como a lista é ordenada em
ordem decrescente, o elemento com a maior chave fica sempre
colocado à cabeça da fila. A figura 7.5.d) apresenta a inserção do
elemento com maior chave, que provoca a actualização da cabeça da
fila.
cabeça Fila com cabeça
prioridad
e vazia
a)
CHAVE 4 CHAVE 3
Elemento Elemento
c)

cabeça cabeça

CHAVE 4 CHAVE 6 CHAVE 4 CHAVE 3


Elemento Elemento Elemento Elemento
b) d)

118
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

Figura 7.5. Inserção de vários elementos na fila com prioridade dinâmica


ordenada por ordem decrescente.

A figuras 7.6.b) e 7.6.c) apresentam, respectivamente, a remoção do


elemento com a maior chave, um precesso simples pois este se
encontra colocado à cabeça da fila. Na figura 7.6.d) apos a remoção do
último elemento, a cabeça da fila fica a apontar para o null.

cabeça cabeça

CHAVE 6 CHAVE 4 CHAVE 3 CHAVE 4


Elemento Elemento Elemento Elemento
a) c)

cabeça Fila com


cabeça
prioridad
e vazia
d)
CHAVE 4 CHAVE 3
Elemento Elemento
b)
Figura 7.6. Remoção de vários elementos na fila com prioridade dinâmica
ordenada por ordem decrescente.

A classe 7.4 implementa a fila com prioridade dinâmica genérica


parametrizada com lançamento de excepções, em que a sua estrutura
é constituída apenas pela referência head para a cabeça. Os elementos
são armazenados numa lista biligada de nós genéricos da classe
BiNode<T>, sendo cada nó constituído pelas referências elem para um
elemento do tipo T e pelas referências prev e next para fazer a ligação
ao elemento, respectivamente, anterior e seguinte da fila com
prioridade, caso eles existam.

public class GenericListPQueue<T extends Comparable<T>> implements


GenericPQueueInterface<T> {
// Classe privada de nós da lista
private static class BiNode<T> {
public BiNode<T> prev; // nó anterior
public BiNode<T> next; // nó seguinte
public T elem; // elemento a armazenar no nó
// construtor do nó da lista biligada
public BiNode (T pVal) {
elem = pVal;
prev = next = null;
}
}
private BiNode<T> head; // cabeça da fila com prioridade
// construtor da fila

119
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

public GenericListPQueue() throws NullPointerException {


head = null;
}
// método de inserção
public void insert(T pElem) throws NullPointerException {
if (pElem == null)
throw new NullPointerException (); // sem elementos
BiNode<T> newNode = new BiNode<T> (pElem); // criação do nó
if (head == null || pElem.compareTo(head.elem) > 0) {
// inserção à cabeça da lista
newNode.next = head;
head = newNode
if (newNode.next != null)
newNode.next.prev = newNode;
} else {
// inserção à frente do nó de inserção
BiNode<T> nodeIns = findPlace(head, pElem);
newNode.next = nodeIns.next;
if (nodeIns.next != null)
newNode.next.prev = newNode;
newNode.prev = nodeIns;
nodeIns.next = newNode;
}
}
// método de remoção
public T deleteMax() throws PQueueEmptyException {
if (isEmpty())
throw new PQueueEmptyException(); // fila vazia
T elem = head.elem; // copiar o elemento da cabeça da fila
head = head.next; // remoção do nó da cabeça da lista
if (head != null)
head.prev = null;
return elem; // devolver o elemento removido
}
public boolean isEmpty() { // método de teste da fila vazia
return head == null;
}
public boolean isFull() { // método de teste da fila cheia
return false;
}
/** Método de impressão. Cria uma sequência de caracteres
representativa do conteúdo da fila com prioridade. */
public String toString() {
if (isEmpty())
return “Fila com prioridade vazia \n”;
String str = “Fila com prioridade : ”;
for (BiNode<T> node = head; node != null; node = node.next)
str += “\t” + node.elem.toString();
return str + “\n”;
}

120
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

/** Devolve um ponteiro para o nó à frente do qual deve ser feita a


inserção do novo elemento. Caso já existam elementos com a mesma
chave, a posição de inserção será a seguir aos elementos já
existentes. */
private BiNode<T> findPlace(BiNode<T> pHead, T pElem) {
BiNode<T> prev = null;
for (BiNode<T> node = pHead; node != null; node = node.next) {
if (pElem.compareTo(node.elem) > 0)
break;
prev = node;
}
return prev;
}
}
Classe 7.4. Memória fila com prioridade dinâmica ordenada genérica.

O método de inserção começa por verificar se o elemento que vai ser


colocado na fila com prioridade é uma referência válida para criar o nó
e liga-lo ao elemento.
Nesta implementação, a operação de inserção tem uma complexidade
linear de comparações, pois usa a pesquisa linear para a pesquisa da
posição de inserção, porem, não exige deslocação de elementos. A
remoção de elementos tem complexidade nula, pois não realiza
pesquisa. Esta implementação permite a existência de elementos
distintos com a mesma chave, que são colocados por ordem cronológica
de inserção, sendo na mesma ordem a sua remoção.

7.3. Árvore com Prioridade (Heap)

Um amontoado ou árvore binária com prioridade é uma árvore binária


em que (1) as informações contidas nos nós das árvores são
comparáveis entre si; (2) a informação em cada nó pai é maior/menor
ou igual que as dos seus nós filhos; (3) é uma árvore completa, isto é,
está cheia até ao penúltimo nível e todos os nós do último nível estão
dispostos da esquerda para a direita.
O amontoado binário é uma estrutura de dados muito eficiente, pois
permite a inserção e a remoção de elementos com complexidade
logarítmica. Apesar de ser uma estrutura de dados estática, o
amontoado é muito útil em situações que se pretenda manter os dados
parcialmente ordenados.
O amontoado permite uma implementação estática, para armazenar
elememntos dos tipos de dados primitivos, ou uma implementação
semi-estática, para armazenar elementos dos tipos de dados
referências, que pode ser genérica. O comportamento das operações
de inserção e de remoção se assemelham às anteriormente
apresentadas.

121
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

A classe 7.5 implementa a fila com prioridade semi-estática genérica


parametrizada, baseada num amontoado binário, com lançamento de
excepções.
Esta implementação permite a existência de elementos distintos com a
mesma chave, sendo estes colocados em ordem cronológica de
inserção. Porem, devido aos deslocamentos de elementos para ajustar
o amontoado, não existe garantia de que a remoção de elementos
distintos com a mesma chave ocorra por ordem cronológica.

public class GenericHeapPQueue<T extends Comparable<T>> implements


GenericPQueueInterface<T> {
private T[] heap; //área de armazenamento da fila
private int nElem; //número de elementos armazenados na fila
//suprimir o aviso gerado pelo cast
@SuppressWarnings (“unchecked”)
/** Construtor da fila. Cria o amontoado com a dimensão indicada e
inicializa o indicador do número de elementos. */
public GenericHeapPQueue(int pSize) throws
NegativeArraySizeException {
if (pSize <= 0)
throws new NegativeArraySizeException();
heap = (T[]) new comparable[pSize];
nElem = 0;
}
public boolean isEmpty() { // método de teste de fila vazia
return nElem == 0;
}
public boolean isFull() { // método de teste de fila cheia
return nElem == heap.length;
}
// método de inserção
public void insert(T pElem) throws NullPointerException,
PQueueFullException {
int i;
if (isFull()) // fila cheia
throw new PQueueFullException();
if (pElem == null) // sem elemento
throw new NullPointerException();
// ajustar o amontoado, descendo os pais menores do que o elemento
for (i = nElem; i > 0 && heap[(i - 1) / 2]).compareTo(pElem) < 0; i = (i -
1) / 2)
heap[i] = heap[(i - 1) / 2];
heap[i] = pElem; // colocar o novo elemento no amontoado
nElem++; // incrementar o número de elementos
}
// método de remoção
public T deleteMax() throws PQueueEmptyExcepation {
int i; // posição do pai
int son; // posição do filho

122
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

if (isEmpty()) // fila vazia, não é possível remover


throw new PQueueEmptyExcepation;
T max = heap[0]; // copiar a raiz do amontoado
nElem--; // decrementar o número de elementos
for (int i = 0; i * 2 + 1 <= nElem; i = son) {
son = 2 * i + 1; // determinar o maior dos filhos
if (son < nElem && heap[son].compareTo(heap[son + 1]) < 0)
son++; // subir o filho maior do que o último elemento
if (heap[son].compareTo(heap[nElem]) > 0)
heap[i] = heap[son];
else
break;
}
// recolocar o último elemento do amontoado
heap[i] = heap[nElem];
heap[nElem] = null; // eliminar a referência
return max; // devolução do elemento
}
/** Método de impressão. */
public String toString() {
if (nElem == 0)
return “Fila com prioridade vazia \n”;
String str = “Fila com prioridade : ”;
for (int i = 0; i < nElem; i++)
str += “\t” + heap[i].toString();
return str + “\n”;
}
}
Classe 7.5. Memória fila com prioridade semiestática genérica baseada
num amontoado binário.

7.3.1. Implementação Estática


Por definição do amontoado, a árvore deve estar completa. A
representação vectorial é ideal para esta implementação:
8

7 4 8 7 4 1 3 2
Índice: 0 1 2 3 4 5
1 3 2 b)
a)
Figura 7.7. Amontoado binário e sua respectiva representação vectorial.

A figura 7.7 apresenta um amontoado binária a) e sua respectiva


representação vectorial. Preenche-se o vector distribuindo os
elementos de forma a representar o percurso em largura do
amontoado. Depois calculam-se os índices dos filhos de um nó pai: se o
nó pai está no índice i, o filho da esquerda está em 2i+1 e o filho da

123
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

direita está em 2i+2. Dado o nó de índice i, o pai é dado pela


expressão⎿(i-1)/2⏌.

package dataStructures;
/** Uma implementação vectorial do amontoado. */
public class VHeap implements Heap, Cloneable {
private final int DELTA = 128;
private int nElem; //número de elementos armazenados na fila
private Object[] heap;
/** Construtor da fila. Cria o amontoado com a dimensão indicada e
inicializa o indicador do número de elementos. */
public VHeap() {
heap = new Object[DELTA];
nElem = 0;
}
//@ requires isHeap(t);
/** Segundo construtor da fila. Traduz uma árvore binária num
amontoado, quando satisfeita a condição. */
public VHeap(BinTree t) {
this ();
constr(t.root(), t.left(), t.right());
}
public BinTree empty() {
return new VHeap();
}
public void constr(Object item, BinTree left, BinTree right) {
heap = new Object[DELTA + left.length() + right.length()];
heap[0] = item;
nElem = 1;
VQueue qTree = new VQueue(); // fila de árvores
VQueue qIndex = new VQueue(); // fila de inteiros
qTree.enqueue(left);
qIndex.enqueue(new Integer(1));
qTree.enqueue(right);
qIndex.enqueue(new Integer(2));
while (!qTree.isEmpty()) {
BinTree t = ((BinTree) (qTree.front()));
int I = ((Integer) (qIndex.front())).intValue();
heap[i] = t.root;
nElem = i+1;
if (!t.left().isEmpty()) {
qTree.enqueue(t.left());
qIndex.enqueue(new Integer(left(i)));
}
if (!t.right().isEmpty()) {
qTree.enqueue(t.right());
qIndex.enqueue(new Integer(right(i)));
}
}
}

124
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

public boolean isEmpty() { // método de teste de fila vazia


return nElem == 0;
}
private int left(int index) {
return 2 * index + 1;
}
private int right(int index) {
return 2 * index + 2;
}
private int father(int index) {
return (index - 1)/2;
}
private boolean isEmpty(int index) {
return índex >= nElem || heap[índex] == null;
}
private boolean isLeaf(int index) {
return isLeaf(0);
}
public Object root() {
return heap[0];
}
public BinTree left() {
int i = 0;
VHeap result = new VHeap();
result.heap = new Object[nElem];
for (int start = 2, size = 1; start <= nElem; start *= 2, size *= 2)
for (int j = start - 1; j < start -1 + size; j++) {
if (j >= nElem)
break;
result.heap[i++] = heap[j];
}
result.nElem = i;
return result;
}
public BinTree right() {
int i = 0;
VHeap result = new VHeap();
result.heap = new Object[nElem];
for (int start = 2, size = 1; start <= nElem; start *= 2, size *= 2)
for (int j = start - 1; j < start -1 + 2 * size; j++) {
if (j >= nElem)
break;
result.heap[i++] = heap[j];
}
result.nElem = i;
return result;
}
/** Método de teste de ávore cheia. Opcional.
* @param t A árvore
* @return TRUE se ‘t’ é cheia, FALSE c.c. */

125
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

public boolean isFull(BinTree t) {


return Math.pow(2, t.height()) - 1 == t.length();
}
/** Método de teste de ávore completa. Opcional.
* @param t A árvore
* @return TRUE se ‘t’ é completa, FALSE c.c. */
public boolean isComplete(BinTree t) {
if (t.isEmpty())
return true;
BinTree l = t.left(), r = t.right();
return (l.height() == r.height() && isFull(1) && isComplete(r)) ||
(l.height() == r.height() + 1 && isFull(r) && isComplete(l));
}
public Boolean isHeap(BinTree t) {
return isComplete(t) && isPriority(t);
}
private int length(int index) {
if (isEmpty(index))
return 0;
return 1 + length(left(index)) + length(right(index));
}
private int height(int index) {
if (isEmpty(index))
return 0;
return 1 + Math.max(height(left(index)), height(right(index)));
}
public int length() {
return length(0);
}
public int height() {
return height(0);
}
/** Método cresimento. Aumenta a capacidade do objecto. */
private void grow() {
Object[] newHeap = new Object[heap.length + DELTA];
for (int i = 0; i < heap.length; i++)
newHeap[i] = heap[i];
heap = newHeap;
}
/** Método de inserção. */
public void insert(Comparable o) {
if (nElem == heap.length)
grow();
heap[nElem++] = 0;
moveUp();
}
/** Método de deslocamento. */
private void moveUp() {
int pos = nElem – 1;

126
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

while (pos != 0 && (Comparable)


heap[pos]).compareTo(heap[father(pos)]) > 0) {
swap(pos, father(pos));
pos = father(pos);
}
}
/** Método auxiliar de troca de posições. */
private void swap(int i, int j) {
Object tmp = heap[i];
heap[i] = heap[j];
heap[j] = tmp;
}
public void remRoot() {
if (nElem > 1) {
heap[0] = heap[nElem -1];
moveDown();
}
heap[--nElem] = null;
}
/** Método de deslocamento. */
private void moveDown() {
int maxChild, pos = 0;
while (pos <= father(nElem - 1) {
maxChild = left(pos);
if (maxChild < nElem - 1)
if (((Comparable) heap[maxChild]) .compareTo(heap[maxChild +
1]) < 0)
maxChild++;
if (((Comparable) heap[pos]).compareTo(heap[maxChild]) > 0)
break;
swap(pos, maxChild);
pos = maxChild;
}
}
public Iterator iterator() {
return null;
}
public void prefix(Visitor op) {};
public void sufix(Visitor op) {};
public void infix(Visitor op) {};
public boolean equals(Object t) {
if (!(t instanceof BinTree))
return false;
if (isEmpty())
return ((BinTree) t).isEmpty();
if (((BinTree) t).isEmpty)
return false;
return ((BinTree) t).root().equals(root()) && ((BinTree)
t).left().equals(left()) && ((BinTree)
t).right().equals(right());

127
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

}
/** Método de impressão. */
public String toString() {
if (isEmpty())
return “[ ]”;
StringBuffer result = new StringBuffer(“[ ” + heap[0]);
for (int i = 0; i < nElem; i++)
result.append(“, ” + heap[i] == null? “ [ ] ” : heap[i]);
return result.toString() + “] ”;
}
public Object clone() {
VHeap newHeap = new VHeap();
newHeap.heap = (Object[]) (heap.clone());
newHeap.nElem = nElem;
return newHeap;
}
}
Classe 7.6. Implementação do amontoado binário com prioridade.

7.3.2. Uso da Classe VHeap


public static void main(String[] args) {
VHeap h = new VHeap();
h.insert(new Integer(3));
h.insert(new Integer(2));
h.insert(new Integer(1));
h.insert(new Integer(21));
h.insert(new Integer(14));
h.insert(new Integer(25));
h.insert(new Integer(12));
h.insert(new Integer(0));
h.insert(new Integer(122));
System.out.println(h);
h.remRoot();
System.out.println(h);
System.out.println(“height: ” + h.height() “\n”);
System.out.println(“length: ” + h.length());
}

▭ [122, 25, 21, 14, 3, 1, 12, 0, 2] ↵


[25, 21, 14, 3, 1, 12, 0, 2] ↵
height: 4
length: 8

7.4. Sumário

Neste tema estudamos e discutimos fundamentalmente as filas e as


árvores com prioridades. Abordamos as diferentes formas de

128
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

implementação das filas com prioridade, assim como a implementação


estática da árvore com prioridade.

7.5. Exercícios de Auto-Avaliação

Perguntas
1. Na fila com prioridade é imperioso que os elementos estejam
ordenados para os poder aceder.
A. Verdadeiro
B. Falso
2. A tabela unidimensional ordenada é um exemplo da representação
estática das filas prioritárias.
A. Verdadeiro
B. Falso
3. A árvore binária é uma representação dinâmica das filas prioritárias.
A. Verdadeiro
B. Falso
4. A lista ligada ordenada é uma representação dinâmica das filas
prioritárias.
A. Verdadeiro
B. Falso
5. Nas tabelas de dispersão ocorre uma colisão quando um mesmo
endereço está associado à diversas chaves.
A. Verdadeiro
B. Falso

Respostas
1.B; 2.A, 3.A; 4.A; 5. B

129
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

TEMA 8. ORDENAÇÃO INTERNA

Unidade temática 8.1. Introdução


Unidade temática 8.2. Algoritmo Insertionsort
Unidade temática 8.3. Algoritmo Selectionsort
Unidade temática 8.4. Algoritmo Bubblesort
Unidade temática 8.5. Algoritmo Quicksort
Unidade temática 8.6. Algoritmo Heapsort
Unidade temática 8.7. A Classe Sort

8.1. Introdução

Ao completar esta unidade, você deverá ser capaz de:


▪ Conhecer e explicar o funcionamento dos algoritmos de ordenação
interna;
▪ Identificar os casos para o uso do algoritmo de ordenação interna
Objectivos que melhor se adequa;
específicos ▪ Entender o funcionamento da classe Sort.

A ordenação é o processo de organizar um conjunto de elementos


segundo uma determinada ordem, para facilitar a pesquisa. Ela
classifica-se em:
Ordenação interna: refere-se à ordenação da informação armazenada
em sequências.
Ordenação externa: refere-se à ordenação da informação armazenada
em ficheiros.
Os algoritmos de ordenação interna são categorizados em ordenação
por selecção, ordenação por troca e ordenação por inserção.

8.2. Algoritmo Insertionsort

O algoritmo insertionsort procura para cada índice do vector, excepto o


primeiro, quais dos elementos à sua esquerda são maiores (de menor
índice). Se atingir o índice zero ou encontrar um elemento menor ou
igual que o elemento associado ao índice actual, desloca todos os
elementos uma casa para a direita e coloca o elemento actual na
posição deixada vaga.

130
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

6 5 3 9 7 6 5 3 9 7 5 6 3 9 7

j i j i j i

5 6 3 9 7 5 6 3 9 7 5 6 3 9 7

j i j i j i

3 5 6 9 7 3 5 6 9 7 3 5 6 9 7

j i j i j i

3 5 6 9 7 3 5 6 7 9 3 5 6 7 9

j i j i
Figura 8.1. Ilustração do Algorítmo insertionsort.

O insertionsort, no melhor caso possível é O(n). No pior caso e médio


caso é O(n2) pois tanto o número de comparações como o numero de
trocas é elevado.

8.3. Algoritmo Selectionsort

Para o índice i do vector v, o algoritmo selectionsort seleciona o menor


valor que se encontra entre o índice i e o último índice. Na primeira
iteração (i = 0) encontra o menor valor do vector e o troca com v[0].
Depois encontra o menor valor entre v[1] e o último índice e troca-o
com v[1]. Repete o processo até ao penúltimo índice, concluindo assim
o processo de ordenamento.

6 5 3 9 7 3 5 6 9 7 3 5 6 9 7

i i j i

3 5 6 9 7 3 5 6 9 7 3 6 3 7 9

i i i j
Figura 8.2. Ilustração do Algorítmo Selectionsort

O selectionsort possui uma complexidade O(n2) para o melhor caso, o


pior caso e o médio caso, pois procura sempre o menor elemento para
cada posição do vector. Porém, o número de trocas é baixo para o pior
caso O(n).

131
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

8.4. Algoritmo Bubblesort

O algoritmo bubblesort compara cada par de valores adjacentes,


trocando as posições dos dois elementos para cada par na ordem
errada. No fim da primeira passagem, o maior valor encontra-se na
última posição. Na segunda passagem são vistos os valores do vector
exceptuando o último e assim sucessivamente até a última passagem
que compara apenas os dois primeiros elementos.

6 5 7 9 3 5 6 7 9 3 5 6 7 9 3

i j i j i

5 6 7 9 3 5 6 7 3 9 5 6 7 3 9

j i j i i

5 6 7 3 9 5 6 7 3 9 5 6 3 7 9

j i j i j i

5 6 3 7 9 5 6 3 7 9 5 3 6 7 9

i j i j i

5 3 6 7 9 3 5 6 7 9 3 5 6 7 9

i j i
Figura 8.3. Ilustração do Algorítmo Bubblesort

A complexidade do algoritmo bubblesort é O(n2) em todos os casos. O


número de comparações em todos os casos é limitado por O(n2).

8.5. Algoritmo Quicksort

O algoritmo quicksort é recursivo, dividindo a tarefa de ordenar o vector


em duas ordenações de vectores menores (a técnica de dividir e
conquistar), até chegar ao caso de ordenação trivial.
Os subvectores a ordenar são processados de modo a distribuir os
menores valores para um vector e os maiores para outro, para deopis
anexar os subvectores já ordenados.
Por exemplo, para ordenar o vector:

132
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

3 9 6 7 5 1 4 2 8 0

Colocar-se-ia os menores valores na metade esquerda e os maiores na


direita:
3 2 4 1 0 7 6 5 8 9

Dividir-se-ia os dois vectores para serem ordenados


3 2 4 1 0 7 6 5 8 9

Quando as soluções estiverem disponíveis (através da invocação


recursiva do algoritmo)…
0 1 2 3 4 5 6 7 8 9

… unem-se os dois vectores produzidos pelos subproblemas de modo a


compor a resposta do problema inicial.
0 1 2 3 4 5 6 7 8 9

Para separar os menos valores, o algoritmo escolhe um valor arbitrário


no vector (normalmente no meio do vector) designado por pivot. O
pivot é utilizado para comparar os valores do vector, os valores menores
são armazenados na esquerda, os maiores na direita. Em cada
invocação do algoritmo existem duas variáveis, begin e end que
avançam em direcção ao pivot. Cada par de valores nas metades
incorrectas é trocado. Usando o exemplo anterior
3 9 6 7 5 1 4 2 8 0

O pivot armazena o valor que se encontra na metade do vector


3 9 6 7 5 1 4 2 8 0

begin pivot end

O índice do begin avança para a direita até encontrar um valor maior


que o pivot, enquanto o end avança para a esquerda até encontrar um
valor menor que o pivot
3 9 6 7 5 1 4 2 8 0

begin end

Nesse momento, o par de valores é trocado e o processo recomeça

133
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

3 0 6 7 5 1 4 2 8 9

begin end

3 0 6 7 5 1 4 2 8 9

begin end

3 0 2 7 5 1 4 6 8 9

begin end
3 0 2 7 5 1 4 6 8 9

begin end

3 0 2 4 5 1 7 6 8 9

begin end

3 0 2 4 5 1 7 6 8 9

begin end

3 0 2 4 1 5 7 6 8 9

begin end

3 0 2 4 1 5 7 6 8 9

end begin
Quando o valor do end é menor que o do begin terminou o processo de
separação e iniciou-se a resolução dos subproblemas.
3 0 2 4 1 5 7 6 8 9

Embora não exista um método rápido para determinar o valor do pivot,


sem influenciar no desempenho do algoritmo, este não deve ser
demasiadamente grande nem
134
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

pequeno, pois produziria subvectores de dimensões muito dispares. O


valor no meio do vector pode ser considerado, pois em muitas situações
o vector já se encontra parcialmente ordenado. Outro opção seria
considerar o primeiro, o último e o valor do meio, e escolher a mediana
dos três.
A complexidade no melhor caso, os dois subvectores são separados
exactamente ao meio, é dada pela função
𝜃(1) ,𝑛 ≤ 1
𝑇(𝑛) = { 𝑛
2. 𝑇 ( ) + 𝜃(𝑛), 𝑛 > 1
2
Pelo método mestre,
𝑇(𝑛) ∈ 𝑂(𝑛. log(𝑛))
No caso médio, a separação tende a ser mais desequilibrada. Por
exemplo, para n=4 pode ocorrer entre o primeiro e o segundo, entre o
segundo e o terceiro, entre o terceiro e o quarto ou depois do quarto,
cada separação com probabilidade de ¼. Assim
1 1 1
𝑇(4) = (𝑇(0) + 𝑇(3)) + (𝑇(1) + 𝑇(2)) + (𝑇(2) + 𝑇(1))
4 4 4
1
+ (𝑇(3) + 𝑇(0)) + 𝜃(𝑛)
4
Em termos gerais e simplificando a expressão anterior
𝜃(1) ,𝑛 ≤ 1
𝑛−1
𝑇(𝑛) = {1
(∑ 2𝑇(𝑖)) + 𝜃(𝑛), 𝑛 > 1
𝑛
𝑖=0
Obtemos
𝑇(𝑛) ∈ 𝑂(𝑛. log(𝑛))

O pior caso, um dos subvectores tem uma dimensão igual ao vector


inicial menos um, é dado pela função
𝜃(1) ,𝑛 ≤ 1
𝑇(𝑛) = {
𝑇(𝑛 − 1) + 𝜃(𝑛), 𝑛 > 1
Cuja a complexidade é O(n2). Porém, se os valores iniciais dos vectores
forem distribuídos aleatoriamente, a probabilidade de ocorrer o pior
caso decresce exponencialmente com a dimensão do vector.
O algoritmo quicksort é mais rápido se comparado com os de
complexidade O(n.log(n)) no pior caso. Ele funciona melhor para vector
maiores (acima de 30 elementos).

8.6. Algoritmo Heapsort

O algoritmo heapksort é geralmente usado em estrutura de dados


amontoados e sua complexidade é O(n.log(n)) para todos os casos.
Inicialmente, cada valor é inserido no amontoado:

135
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

6 5 3 9 7

6 5 3 9 7 6

i
6 5 3 9 7 6 5

i
6 5 3 9 7 6 5 3

6 5 3 9 7 9 6 3 5

6 5 3 9 7 9 7 3 5 6

Quando o amontoado contém todos os elementos do vector removem-


se as sucessivas raízes do amontoado e colocam-se os valores da última
para a primeira posição:
6 5 3 9 9 7 6 3 5

i
6 5 3 7 9 6 5 3

i
6 5 6 7 9 5 3

6 5 6 7 9 3

3 5 6 7 9

O algoritmo quicksort é preferível em problemas de tempo real, onde é


necessário garantir resultados em qualquer situação.

8.7. A Classe Sort

136
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

package dataStructures;
/** A classe utilitária para ordenação. Possui vários algoritmos de
ordenação sobre vectores de objectos comparáveis. Não se pode
construir objectos nesta classe. */
public class Sort {
/** O construtor privado impede a criação de objectos. */
private Sort() {}
/** O método swap é utilizado pelos algoritmos de ordenação e
corresponde à troca dos dois objectos do vector situados
nos índices i e j. */
private static void swap(Object[] v, int i, int j) {
Object tmp = v[i];
v[i] = v[j];
v[j] = tmp;
}
/** Algoritmo insertionsort, complexidade O(|v|^2)
* @param v O vector de elementos a ordenar
*/
private static void insert(Comparable[] v) {
int i, j;
Comparable tmp;
for (i=1; i<v.length; i++) {
tmp = v[i];
for (j=i; j>0 && v[j-1].compareTo(tmp)>0; j--)
v[j] = v[j-1];
v[j] = tmp;
}
}
/** Algoritmo selectionsort, complexidade O(|v|^2)
* @param v O vector de elementos a ordenar
*/
private static void selection(Comparable[] v) {
int least;
for (int i=1; i<v.length-1; i++) {
least = i;
for (int j=i+1; j<v.length; j++)
if (v[j].comparableTo(v[least])<0)
least = j;
swap(v, i, least);
}
}
/** Algoritmo bubblesort, complexidade O(|v|^2)
* @param v O vector de elementos a ordenar
*/
private static void bubble(Comparable[] v) {
for (int i=v.length; i>0; i--)
for (int j= 1; j<i; j++)
if (v[j].comparableTo(v[j-1])<0)
swap(v, j, j-1);
}

137
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

/** Método auxiliar do quicksort


* @param v O vector de elementos a ordenar
*/
private static void quicksort(Comparable[] v, int begin, int end) {
int lower=begin, higher=end;
Comparable pivot;
if (end>begin) {
// O pivot , arbitrariamente é o do meio
pivot = v[(begin+end.)/2];
while (lower<=higer) {
while ((lower<end) &&
(v[lower].comparableTo(pivot)>0))
++lower;
while ((higher>begin) &&
(v[higher].comparableTo(pivot)>0))
--higher;
// Se os índices ainda não se cruzaram…
if (lower<=higher) // …trocar os elementos
swap(v, lower++, higher--);
}
if (begin<higher)
quicksort(v, begin, higher);
if (lower<end)
quicksort(v, lower, end);
}
}
/** Algoritmo quicksport, complexidade O(|v|*log(|v|)) no caso
médio.
* @param v O vector de elementos a ordenar
*/
public static void quick(Comparable[] v) {
quicksort(v, 0, v.length-1);
}
/** Algoritmo heapsort, complexidade O(|v|*log(|v|)).
* @param v O vector de elementos a ordenar
*/
private static void heap(Comparable[] v) {
VHeap h = new VHeap();
for (int i=0; i<v.length; i++)
h.insert(v[i]); // constrói o amontoado
for (int i=v.length-1;
!h.isEmpty(); //os maiores colocam-se
h.remRoot(), i--) // no fim do vector
v[i] = (Comparable)h.root();
}
} // endClass Sort
Classe 8.1. A classe Sort.

138
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

8.8. Sumário

Neste tema estudamos e discutimos fundamentalmente o


funcionamento dos algoritmos de ordenação interna. Abordamos como
identificar os casos para o uso do algoritmo de ordenação interna que
melhor se adequam. Explicamos o funcionamento da classe Sort.

8.9. Exercícios de Auto-Avaliação

Perguntas
1. O algoritmo insertionsort é um algoritmo de ordenação interna que
utiliza a comparação entre os adjacentes para efectuar a ordenação.
A. Verdadeiro
B. Falso
2. O algoritmo de ordenação interna quicksort é mais eficiente quando
utilizado em amostragens relactivamente maiores.
A. Verdadeiro
B. Falso
3. O algoritmo de ordenação externa bubblesort é um algoritmo
recursivo que os elementos adjacentes, colocando os maiores na
direita.
A. Verdadeiro
B. Falso
4. O algoritmo de ordenação bubblesort é mais eficiente quando
utilizado em amostragens relactivamente maiores.
A. Verdadeiro
B. Falso
5. O algoritmo de ordenação selectionsort começa por procurar o
menor valor não ordenado para o mover para a primeira posição.
A. Verdadeiro
B. Falso

Respostas
1. B; 2. A; 3. B; 4. B; 5. A

139
UnISCED CURSO: LICENCIATURA EM ENGENHARIA INFORMÁTICA; 10 Ano
Disciplina/Módulo: Algoritmos e Estruturas de Dados

BIBLIOGRAFIA

1. António Andrego da Rocha; Estruturas de Dados e Algoritmos


em Java; FCA; 2011.
2. João Neto; Programação, Algoritmos e Estruturas de Dados; 2ª
Edição; Escolar Editora; 2008.
3. Thomas Cormen, Charles Leiserson, Ronald Riverst, Clifford
Stein; Introduction to Algorithm; 2nd Ed; MIT Press; 2001.

140

Você também pode gostar