Projeto de Algoritmos em C Nivio Ziviani

Fazer download em pdf ou txt
Fazer download em pdf ou txt
Você está na página 1de 922

Projeto de Algoritmos ∗

Introdução

Última alteração: 30 de Agosto de 2010

∗ Transparências elaboradas por Charles Ornelas Almeida, Israel Guerra e Nivio Ziviani
Projeto de Algoritmos – Cap.1 Introdução 1

Conteúdo do Capítulo

1.1 Algoritmos, Estruturas de Dados e Programas

1.2 Tipos de Dados e Tipos Abstratos de Dados

1.3 Medida do tempo de Execução de um Programa


1.3.1 Comportamento Assintótico de Funções
1.3.2 Classes de Comportamento Assintótico

1.4 Técnicas de Análise de Algoritmos

1.5 Pascal
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.1 2

Algoritmos, Estruturas de Dados e Programas


• Os algoritmos fazem parte do dia-a-dia das pessoas. Exemplos de
algoritmos:
– instruções para o uso de medicamentos,
– indicações de como montar um aparelho,
– uma receita de culinária.
• Sequência de ações executáveis para a obtenção de uma solução
para um determinado tipo de problema.
• Segundo Dijkstra, um algoritmo corresponde a uma descrição de um
padrão de comportamento, expresso em termos de um conjunto finito
de ações.
– Executando a operação a + b percebemos um padrão de
comportamento, mesmo que a operação seja realizada para
valores diferentes de a e b.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.1 3

Estruturas de dados

• Estruturas de dados e algoritmos estão intimamente ligados:


– não se pode estudar estruturas de dados sem considerar os
algoritmos associados a elas,
– assim como a escolha dos algoritmos em geral depende da
representação e da estrutura dos dados.

• Para resolver um problema é necessário escolher uma abstração da


realidade, em geral mediante a definição de um conjunto de dados que
representa a situação real.

• A seguir, deve ser escolhida a forma de representar esses dados.


Projeto de Algoritmos – Cap.1 Introdução – Seção 1.1 4

Escolha da Representação dos Dados

• A escolha da representação dos dados é determinada, entre outras,


pelas operações a serem realizadas sobre os dados.

• Considere a operação de adição:


– Para pequenos números, uma boa representação é por meio de
barras verticais (caso em que a operação de adição é
– Já a representação por dígitos decimais requer regras
relativamente complicadas, as quais devem ser memorizadas.
– Entretanto, quando consideramos a adição de grandes números é
mais fácil a representação por dígitos decimais (devido ao princípio
baseado no peso relativo da posição de cada dígito).
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.1 5

Programas
• Programar é basicamente estruturar dados e construir algoritmos.
• Programas são formulações concretas de algoritmos abstratos,
baseados em representações e estruturas específicas de dados.
• Programas representam uma classe especial de algoritmos capazes
de serem seguidos por computadores.
• Um computador só é capaz de seguir programas em linguagem de
máquina (sequência de instruções obscuras e desconfortáveis).
• É necessário construir linguagens mais adequadas, que facilitem a
tarefa de programar um computador.
• Uma linguagem de programação é uma técnica de notação para
programar, com a intenção de servir de veículo tanto para a expressão
do raciocínio algorítmico quanto para a execução automática de um
algoritmo por um computador.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.2 6

Tipos de Dados

• Caracteriza o conjunto de valores a que uma constante pertence, ou


que podem ser assumidos por uma variável ou expressão, ou que
podem ser gerados por uma função.

• Tipos simples de dados são grupos de valores indivisíveis (como os


tipos básicos integer, boolean, char e real do Pascal).
– Exemplo: uma variável do tipo boolean pode assumir o valor
verdadeiro ou o valor falso, e nenhum outro valor.

• Os tipos estruturados em geral definem uma coleção de valores


simples, ou um agregado de valores de tipos diferentes.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.2 7

Tipos Abstratos de Dados (TAD)


• Modelo matemático, acompanhado das operações definidas sobre o
modelo.
– Exemplo: o conjunto dos inteiros acompanhado das operações de
adição, subtração e multiplicação.
• TADs são utilizados como base para o projeto de algoritmos.
• A implementação do algoritmo em uma linguagem de programação
exige a representação do TAD em termos dos tipos de dados e dos
operadores suportados.
• A representação do modelo matemático por trás do tipo abstrato de
dados é realizada mediante uma estrutura de dados.
• Podemos considerar TADs como generalizações de tipos primitivos e
procedimentos como generalizações de operações primitivas.
• O TAD encapsula tipos de dados. A definição do tipo e todas as
operações ficam localizadas numa seção do programa.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.2 8

Implementação de TADs (1)

• Considere uma uma lista de inteiros. Poderíamos definir TAD Lista,


com as seguintes operações:
1. faça a lista vazia;
2. obtenha o primeiro elemento da lista; se a lista estiver vazia, então retorne
nulo;
3. insira um elemento na lista.

• Há várias opções de estruturas de dados que permitem uma


implementação eficiente para listas (por ex., o tipo estruturado arranjo).
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.2 9

Implementação de TADs (2)

• Cada operação do tipo abstrato de dados é implementada como um


procedimento na linguagem de programação escolhida.

• Qualquer alteração na implementação do TAD fica restrita à parte


encapsulada, sem causar impactos em outras partes do código.

• Cada conjunto diferente de operações define um TAD diferente,


mesmo que atuem sob um mesmo modelo matemático.

• A escolha adequada de uma implementação depende fortemente das


operações a serem realizadas sobre o modelo.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 10

Medida do Tempo de Execução de um Programa

• O projeto de algoritmos é fortemente influenciado pelo estudo de seus


comportamentos.

• Depois que um problema é analisado e decisões de projeto são


finalizadas, é necessário estudar as várias opções de algoritmos a
serem utilizados, considerando os aspectos de tempo de execução e
espaço ocupado.

• Muitos desses algoritmos são encontrados em áreas como pesquisa


operacional, otimização, teoria dos grafos, estatística, probabilidades,
entre outras.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 11

Tipos de Problemas na Análise de Algoritmos


• Análise de um algoritmo particular.
– Qual é o custo de usar um dado algoritmo para resolver um
problema específico?
– Características que devem ser investigadas:
∗ análise do número de vezes que cada parte do algoritmo deve
ser executada,
∗ estudo da quantidade de memória necessária.
• Análise de uma classe de algoritmos.
– Qual é o algoritmo de menor custo possível para resolver um
problema particular?
– Toda uma família de algoritmos é investigada.
– Procura-se identificar um que seja o melhor possível.
– Coloca-se limites para a complexidade computacional dos
algoritmos pertencentes à classe.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 12

Custo de um Algoritmo

• Determinando o menor custo possível para resolver problemas de uma


classe, temos a medida da dificuldade inerente para resolver o
problema.

• Quando o custo de um algoritmo é igual ao menor custo possível, o


algoritmo é ótimo para a medida de custo considerada.

• Podem existir vários algoritmos para resolver o mesmo problema.

• Se a mesma medida de custo é aplicada a diferentes algoritmos, então


é possível compará-los e escolher o mais adequado.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 13

Medida do Custo pela Execução do Programa


• Tais medidas são inadequadas e os resultados jamais devem ser
generalizados:
– os resultados são dependentes do compilador que pode favorecer
algumas construções em detrimento de outras;
– os resultados dependem do hardware;
– quando grandes quantidades de memória são utilizadas, as
medidas de tempo podem depender desse aspecto.
• Apesar disso, há argumentos a favor de medidas reais de tempo.
– Ex.: quando há vários algoritmos para resolver um mesmo tipo de
problema, todos com um custo de execução dentro da mesma
ordem de grandeza.
– Assim, são considerados tanto os custos reais das operações como
os custos não aparentes, tais como alocação de memória,
indexação, carga, dentre outros.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 14

Medida do Custo por meio de um Modelo Matemático

• Usa um modelo matemático baseado em um computador idealizado.

• Deve ser especificado o conjunto de operações e seus custos de


execuções.

• É mais usual ignorar o custo de algumas das operações e considerar


apenas as operações mais significativas.

• Ex.: algoritmos de ordenação. Consideramos o número de


comparações entre os elementos do conjunto e ignoramos operações
aritméticas, de atribuição e manipulações de índices, entre outras.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 15

Função de Complexidade
• Para medir o custo de execução de um algoritmo é comum definir uma
função de custo ou função de complexidade f .
• f (n) é a medida do tempo necessário para executar um algoritmo para
um problema de tamanho n.
• Função de complexidade de tempo: f (n) mede o tempo necessário
para executar um algoritmo em um problema de tamanho n.
• Função de complexidade de espaço: f (n) mede a memória
necessária para executar algoritmo em um problema de tamanho n.
• Utilizaremos f para denotar uma função de complexidade de tempo
daqui para a frente.
• A complexidade de tempo na realidade não representa tempo
diretamente, mas o número de vezes que determinada operação
considerada relevante é executada.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 16

Exemplo - Maior Elemento (1)

• Considere o algoritmo para encontrar o maior elemento de um vetor de


inteiros A[1..n], n ≥ 1.

int Max(TipoVetor A)
{ int i , Temp;
Temp = A[ 0 ] ;
for ( i = 1; i < N; i ++) i f (Temp < A[ i ] ) Temp = A[ i ] ;
return Temp;
}

• Seja f uma função de complexidade tal que f (n) é o número de


comparações entre os elementos de A, se A contiver n elementos.

• Logo f (n) = n − 1, para n > 0.

• Vamos provar que o algoritmo apresentado é ótimo.


Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 17

Exemplo - Maior Elemento (2)

• Teorema: Qualquer algoritmo para encontrar o maior elemento de um


conjunto com n elementos, n ≥ 1, faz pelo menos n − 1 comparações.

• Prova: Cada um dos n − 1 elementos tem de ser mostrado, por meio


de comparações, que é menor do que algum outro elemento.

• Logo n − 1 comparações são necessárias. 2

• O teorema diz que, se o número de comparações for utilizado como


medida de custo, então a função Max é ótima.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 18

Tamanho da Entrada de Dados

• A medida do custo de execução de um algoritmo depende


principalmente do tamanho da entrada dos dados.

• É comum considerar o tempo de execução de um programa como uma


função do tamanho da entrada.

• Para alguns algoritmos, o custo de execução é uma função da entrada


particular dos dados, não apenas do tamanho da entrada.

• No caso da função Max do programa do exemplo, o custo é uniforme


sobre todos os problemas de tamanho n.

• Já para um algoritmo de ordenação isso não ocorre: se os dados de


entrada já estiverem quase ordenados, então o algoritmo pode ter que
trabalhar menos.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 19

Melhor Caso, Pior Caso e Caso Médio (1)

• Melhor caso: menor tempo de execução sobre todas as entradas de


tamanho n.

• Pior caso: maior tempo de execução sobre todas as entradas de


tamanho n.

• Se f é uma função de complexidade baseada na análise de pior caso,


o custo de aplicar o algoritmo nunca é maior do que f (n).

• Caso médio (ou caso esperado): média dos tempos de execução de


todas as entradas de tamanho n.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 20

Melhor Caso, Pior Caso e Caso Médio (2)

• Na análise do caso esperado, uma distribuição de probabilidades


sobre o conjunto de entradas de tamanho n é suposta e o custo médio
é obtido com base nessa distribuição.

• A análise do caso médio é geralmente muito mais difícil de obter do


que as análises do melhor e do pior caso.

• É comum supor uma distribuição de probabilidades em que todas as


entradas possíveis são igualmente prováveis.

• Na prática isso nem sempre é verdade.


Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 21

Exemplo - Registros de um Arquivo


• Considere o problema de acessar os registros de um arquivo.
• Cada registro contém uma chave única que é utilizada para recuperar
registros do arquivo.
• O problema: dada uma chave qualquer, localize o registro que
contenha esta chave.
• O algoritmo mais simples é o que faz a pesquisa sequencial.
• Seja f uma função de complexidade tal que f (n) é o número de
registros consultados no arquivo (número de vezes que a chave de
consulta é comparada com a chave de cada registro).
– melhor caso: f (n) = 1 (registro procurado é o primeiro consultado);
– pior caso: f (n) = n (registro procurado é o último consultado ou
não está presente no arquivo);
– caso médio: f (n) = (n + 1)/2.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 22

Exemplo - Registros de um Arquivo

• No estudo do caso médio, vamos considerar que toda pesquisa


recupera um registro.
• Se pi for a probabilidade de que o i-ésimo registro seja procurado, e
para recuperar o i-ésimo registro são necessárias i comparações,
então
f (n) = 1 × p1 + 2 × p2 + 3 × p3 + · · · + n × pn .
• Para calcular f (n) basta conhecer a distribuição de probabilidades pi .
• Se cada registro tiver a mesma probabilidade de ser acessado que
todos os outros, então pi = 1/n, 1 ≤ i ≤ n.
 
1 1 n(n+1) n+1
• Nesse caso f (n) = n
(1 + 2 + 3 + · · · + n) = n 2
= 2
·
• A análise do caso esperado revela que uma pesquisa com sucesso
examina aproximadamente metade dos registros.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 23

Exemplo - Maior e Menor Elemento (1)

• Encontrar o maior e o menor elemento de A[1..n], n ≥ 1.

• Um algoritmo simples pode ser derivado do algoritmo para achar o


maior elemento.

void MaxMin1(TipoVetor A, int ∗Max, int ∗Min)


{ int i ; ∗Max = A[ 0 ] ; ∗Min = A[ 0 ] ;
for ( i = 1; i < N; i ++)
{ i f (A[ i ] > ∗Max) ∗Max = A[ i ] ;
i f (A[ i ] < ∗Min) ∗Min = A[ i ] ;
}
}

• Seja f (n) o número de comparações entre os n elementos de As.

• f (n) = 2(n − 1), para n > 0, no melhor caso, pior caso e caso médio.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 24

Exemplo - Maior e Menor Elemento (2)


void MaxMin2(TipoVetor A, int ∗Max, int ∗Min)
{ int i ; ∗Max = A[ 0 ] ; ∗Min = A[ 0 ] ;
for ( i = 1; i < N; i ++)
{ i f (A[ i ] > ∗Max) ∗Max = A[ i ] ;
else i f (A[ i ] < ∗Min) ∗Min = A[ i ] ;
}
}

• MaxMin1 pode ser facilmente melhorado: a comparação A[i] < Min só é


necessária quando a comparação A[i] > Max dá falso.
• Para a nova implementação temos:
– melhor caso: f (n) = n − 1 (elementos estão em ordem crescente);
– pior caso: f (n) = 2(n − 1) (elementos estão em ordem decrescente);
– caso médio: f (n) = 3n/2 − 3/2.
• No caso médio, A[i] é maior do que Max a metade das vezes.
3n
• Logo f (n) = n − 1 + n−1
2 = 2 − 32 , para n > 0.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 25

Exemplo - Maior e Menor Elemento (3)

• Considerando o número de comparações realizadas, existe a


possibilidade de obter um algoritmo mais eficiente:
1) Compare os elementos de A aos pares, separando-os em dois
subconjuntos (maiores em um e menores em outro), a um custo de
⌈n/2⌉ comparações.
2) O máximo é obtido do subconjunto que contém os maiores
elementos, a um custo de ⌈n/2⌉ − 1 comparações.
3) O mínimo é obtido do subconjunto que contém os menores
elementos, a um custo de ⌈n/2⌉ − 1 comparações.

d d d ··· d
Contém o máximo



d d d ··· d
Contém o mínimo

Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 26

Exemplo - Maior e Menor Elemento (4)


void MaxMin3(TipoVetor A, int ∗Max, int ∗Min)
{ int i , FimDoAnel;
i f ( (N & 1) > 0) { A[N] = A[N − 1]; FimDoAnel = N; }
else FimDoAnel = N − 1;
i f (A[0] > A[ 1 ] )
{ ∗Max = A[ 0 ] ; ∗Min = A[ 1 ] ; }
else { ∗Max = A[ 1 ] ; ∗Min = A[ 0 ] ; }
i = 3;
while ( i <= FimDoAnel)
{ i f (A[ i − 1] > A[ i ] )
{ i f (A[ i − 1] > ∗Max) ∗Max = A[ i − 1];
i f (A[ i ] < ∗Min) ∗Min = A[ i ] ; }
else { i f (A[ i − 1] < ∗Min) ∗Min = A[ i − 1];
i f (A[ i ] > ∗Max) ∗Max = A[ i ] ; }
i += 2;
}
}
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 27

Exemplo - Maior e Menor Elemento (5)

• Os elementos de A são comparados dois a dois e os maiores são


comparados com Max e os menores com Min.

• Quando n é ímpar, o elemento que está na posição A[n] é duplicado


na posição A[n + 1] para evitar um tratamento de exceção.
3n
• Para esta implementação, f (n) = n2 + n−2
2
+ n−2
2
= 2
− 2, para n > 0,
para o melhor caso, pior caso e caso médio.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 28

Comparação entre MaxMin1, MaxMin2 e MaxMin3


• A tabela apresenta o número de comparações dos programas
MaxMin1, MaxMin2 e MaxMin3.
• Os algoritmos MaxMin2 e MaxMin3 são superiores ao algoritmo
MaxMin1 de forma geral.
• O algoritmo MaxMin3 é superior ao algoritmo MaxMin2 com relação ao
pior caso e bastante próximo quanto ao caso médio.

Os três f (n)
algoritmos Melhor caso Pior caso Caso médio

MaxMin1 2(n − 1) 2(n − 1) 2(n − 1)


MaxMin2 n−1 2(n − 1) 3n/2 − 3/2
MaxMin3 3n/2 − 2 3n/2 − 2 3n/2 − 2
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 29

Limite Inferior - Uso de um Oráculo

• Existe possibilidade de obter um algoritmo MaxMin mais eficiente?

• Para responder temos de conhecer o limite inferior para essa classe


de algoritmos.

• Técnica muito utilizada: uso de um oráculo.

• Dado um modelo de computação que expresse o comportamento do


algoritmo, o oráculo informa o resultado de cada passo possível (no
caso, o resultado de cada comparação).

• Para derivar o limite inferior, o oráculo procura sempre fazer com que o
algoritmo trabalhe o máximo, escolhendo como resultado da próxima
comparação aquele que cause o maior trabalho possível necessário
para determinar a resposta final.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 30

Exemplo de Uso de um Oráculo


• Teorema: Qualquer algoritmo para encontrar o maior e o menor
elemento de um conjunto com n elementos não ordenados, n ≥ 1, faz
pelo menos ⌈3n/2⌉ − 2 comparações.
• Prova: Define um oráculo que descreve o comportamento do
algoritmo por meio de um conjunto de n–tuplas, mais um conjunto de
regras que mostram as tuplas possíveis (estados) que um algoritmo
pode assumir a partir de uma dada tupla e uma única comparação.
• Uma 4–tupla, representada por (a, b, c, d), onde os elementos de:
– a → nunca foram comparados;
– b → foram vencedores e nunca perderam em comparações
realizadas;
– c → foram perdedores e nunca venceram em comparações
realizadas;
– d → foram vencedores e perdedores em comparações realizadas.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 31

Exemplo de Uso de um Oráculo


• O algoritmo inicia no estado (n, 0, 0, 0) e termina com (0, 1, 1, n − 2).
• Após cada comparação a tupla (a, b, c, d) consegue progredir apenas
se ela assume um dentre os seis estados possíveis abaixo:
– (a − 2, b + 1, c + 1, d) se a ≥ 2 (dois elementos de a são comparados)
– (a − 1, b + 1, c, d) ou (a − 1, b, c + 1, d) ou (a − 1, b, c, d + 1) se a ≥ 1
(um elemento de a comparado com um de b ou um de c)
– (a, b − 1, c, d + 1) se b ≥ 2 (dois elementos de b são comparados)
– (a, b, c − 1, d + 1) se c ≥ 2 (dois elementos de c são comparados)
– O primeiro passo requer necessariamente a manipulação do
componente a.
– O caminho mais rápido para levar a até zero requer ⌈n/2⌉
mudanças de estado e termina com a tupla (0, n/2, n/2, 0) (por
meio de comparação dos elementos de a dois a dois).
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3 32

Exemplo de Uso de um Oráculo

• A seguir, para reduzir o componente b até 1 são necessárias ⌈n/2⌉ − 1


mudanças de estado (mínimo de comparações necessárias para obter
o maior elemento de b).

• Idem para c, com ⌈n/2⌉ − 1 mudanças de estado.

• Logo, para obter o estado (0, 1, 1, n − 2) a partir do estado (n, 0, 0, 0)


são necessárias

⌈n/2⌉ + ⌈n/2⌉ − 1 + ⌈n/2⌉ − 1 = ⌈3n/2⌉ − 2

comparações. 2

• O teorema nos diz que se o número de comparações entre os


elementos de um vetor for utilizado como medida de custo, então o
algoritmo MaxMin3 é ótimo.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.1 33

Comportamento Assintótico de Funções

• O parâmetro n fornece uma medida da dificuldade para se resolver o


problema.

• Para valores suficientemente pequenos de n, qualquer algoritmo custa


pouco para ser executado, mesmo os ineficientes.

• A escolha do algoritmo não é um problema crítico para problemas de


tamanho pequeno.

• Logo, a análise de algoritmos é realizada para valores grandes de n.

• Estuda-se o comportamento assintótico das funções de custo


(comportamento de suas funções de custo para valores grandes de n)

• O comportamento assintótico de f (n) representa o limite do


comportamento do custo quando n cresce.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.1 34

Dominação assintótica

• A análise de um algoritmo geralmente conta com apenas algumas


operações elementares.

• A medida de custo ou medida de complexidade relata o crescimento


assintótico da operação considerada.

• Definição: Uma função f (n) domina assintoticamente outra função


g(n) se existem duas constantes positivas c e m tais que, para n ≥ m,
temos |g(n)| ≤ c × |f (n)|.
f,g
c f (n) • Sejam g(n) = (n + 1)2 e f (n) = n2 .
g(n) • As funções g(n) e f (n) dominam assintoti-
camente uma a outra, desde que
|(n + 1)2 | ≤ 4|n2 | para n ≥ 1
m
n e |n2 | ≤ |(n + 1)2 | para n ≥ 0.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.1 35

Notação O
• Escrevemos g(n) = O(f (n)) para expressar que f (n) domina
assintoticamente g(n). Lê-se g(n) é da ordem no máximo f (n).
• Exemplo: quando dizemos que o tempo de execução T (n) de um
programa é O(n2 ), significa que existem constantes c e m tais que,
para valores de n ≥ m, T (n) ≤ cn2 .
• Exemplo gráfico de dominação assintótica que ilustra a notação O.
f,g
c f (n)
g(n) O valor da constante m é o menor
possível, mas qualquer valor maior é
válido.
n
m

• Definição: Uma função g(n) é O(f (n)) se existem duas constantes


positivas c e m tais que g(n) ≤ cf (n), para todo n ≥ m.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.1 36

Exemplos de Notação O

• Exemplo: g(n) = (n + 1)2 .


– Logo g(n) é O(n2 ), quando m = 1 e c = 4.
– Isto porque (n + 1)2 ≤ 4n2 para n ≥ 1.

• Exemplo: g(n) = n e f (n) = n2 .


– Sabemos que g(n) é O(n2 ), pois para n ≥ 0, n ≤ n2 .
– Entretanto f (n) não é O(n).
– Suponha que existam constantes c e m tais que para todo n ≥ m,
n2 ≤ cn.
– Logo c ≥ n para qualquer n ≥ m, e não existe uma constante c que
possa ser maior ou igual a n para todo n.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.1 37

Exemplos de Notação O

• Exemplo: g(n) = 3n3 + 2n2 + n é O(n3 ).


– Basta mostrar que 3n3 + 2n2 + n ≤ 6n3 , para n ≥ 0.
– A função g(n) = 3n3 + 2n2 + n é também O(n4 ), entretanto esta
afirmação é mais fraca do que dizer que g(n) é O(n3 ).

• Exemplo: g(n) = log5 n é O(log n).


– O logb n difere do logc n por uma constante que no caso é logb c.
– Como n = clogc n , tomando o logaritmo base b em ambos os lados
da igualdade, temos que logb n = logb clogc n = logc n × logb c.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.1 38

Operações com a Notação O

f (n) = O(f (n))


c × O(f (n)) = O(f (n)) c = constante
O(f (n)) + O(f (n)) = O(f (n))
O(O(f (n)) = O(f (n))
O(f (n)) + O(g(n)) = O(max(f (n), g(n)))
O(f (n))O(g(n)) = O(f (n)g(n))
f (n)O(g(n)) = O(f (n)g(n))

Exemplo: regra da soma O(f (n)) + O(g(n)).


• Suponha três trechos cujos tempos são O(n), O(n2 ) e O(n log n).
• O tempo de execução dos dois primeiros é O(max(n, n2 )), que é O(n2 ).
• O tempo dos três trechos é então O(max(n2 , n log n)), que é O(n2 ).

Exemplo: O produto de [log n + k + O(1/n)] por [n + O( n)] é

n log n + kn + O( n log n).
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.1 39

Notação Ω

• Especifica um limite inferior para g(n).

• Definição: Uma função g(n) é Ω(f (n)) se existirem duas constantes c


e m tais que g(n) ≥ cf (n), para todo n ≥ m.

• Exemplo: Para mostrar que g(n) = 3n3 + 2n2 é Ω(n3 ) basta fazer c = 1,
e então 3n3 + 2n2 ≥ n3 para n ≥ 0.

• Exemplo: Seja g(n) = n para n ímpar (n ≥ 1) e g(n) = n2 /10 para n


par (n ≥ 0). Nesse caso g(n) é Ω(n2 ), para c = 1/10 e n = 0, 2, 4, 6, . . .

f,g
g(n)

c f (n) Para todos os valores à direita de m,


o valor de g(n) está sobre ou acima
do valor de cf (n).
n
m
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.1 40

Notação Θ
• Definição: Uma função g(n) é Θ(f (n)) se existirem constantes c1 , c2 e
m tais que 0 ≤ c1 f (n) ≤ g(n) ≤ c2 f (n), para todo n ≥ m.
• Exemplo gráfico para a notação Θ
f,g
c2 f ( n )

g(n)
c1 f ( n )

n
m

• Dizemos que g(n) = Θ(f (n)) se existirem constantes c1 , c2 e m tais


que, para todo n ≥ m, o valor de g(n) está sobre ou acima de c1 f (n) e
sobre ou abaixo de c2 f (n).
• Para todo n ≥ m, g(n) é igual a f (n) a menos de uma constante.
• Nesse caso, f (n) é um limite assintótico firme.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.1 41

Exemplo de Notação Θ
• Seja g(n) = n2 /3 − 2n.

• Vamos mostrar que g(n) = Θ(n2 ).

• Temos de obter c1 , c2 e m tais que c1 n2 ≤ 31 n2 − 2n ≤ c2 n2 para todo n ≥ m.


1 2
• Dividindo por n2 leva a c1 ≤ 3 − n ≤ c2 .

• O lado direito da desigualdade será sempre válido para qualquer valor de


n ≥ 1 quando escolhemos c2 ≥ 1/3.

• Escolhendo c1 ≤ 1/21, o lado esquerdo da desigualdade será válido para


qualquer valor de n ≥ 7.

• Logo, escolhendo c1 = 1/21, c2 = 1/3 e m = 7, verifica-se que


n2 /3 − 2n = Θ(n2 ).

• Outras constantes podem existir, mas o importante é que existe alguma


escolha para as três constantes.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.1 42

Notação o

• Usada para definir um limite superior que não é assintoticamente firme.

• Definição: Uma função g(n) é o(f (n)) se, para qualquer constante
c > 0, então 0 ≤ g(n) < cf (n) para todo n ≥ m.

• Exemplo: 2n = o(n2 ), mas 2n2 6= o(n2 ).

• Em g(n) = O(f (n)), a expressão 0 ≤ g(n) ≤ cf (n) é válida para


alguma constante c > 0, mas em g(n) = o(f (n)), a expressão
0 ≤ g(n) < cf (n) é válida para todas as constantes c > 0.

• Na notação o, a função g(n) tem um crescimento muito menor que


f (n) quando n tende para infinito.
g(n)
• Alguns autores usam limn→∞ f (n)
= 0 para a definição da notação o.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.1 43

Notação ω

• Por analogia, a notação ω está relacionada com a notação Ω da


mesma forma que a notação o está relacionada com a notação O.

• Definição: Uma função g(n) é ω(f (n)) se, para qualquer constante
c > 0, então 0 ≤ cf (n) < g(n) para todo n ≥ m.
n2 n2
• Exemplo: 2
= ω(n), mas 2
6= ω(n2 ).
g(n)
• A relação g(n) = ω(f (n)) implica limn→∞ f (n)
= ∞, se o limite existir.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.2 44

Classes de Comportamento Assintótico


• Se f é uma função de complexidade para um algoritmo F , então
O(f ) é considerada a complexidade assintótica do algoritmo F .
• A relação de dominação assintótica permite comparar funções de
complexidade.
• Entretanto, se as funções f e g dominam assintoticamente uma a
outra, então os algoritmos associados são equivalentes.
• Nesses casos, o comportamento assintótico não serve para comparar
os algoritmos.
• Por exemplo, considere dois algoritmos F e G aplicados à mesma
classe de problemas, sendo que F leva três vezes o tempo de G ao
serem executados, isto é, f (n) = 3g(n), sendo que O(f (n)) = O(g(n)).
• Logo, o comportamento assintótico não serve para comparar os
algoritmos F e G, porque eles diferem apenas por uma constante.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.2 45

Comparação de Programas

• Podemos avaliar programas comparando as funções de complexidade,


negligenciando as constantes de proporcionalidade.

• Um programa com tempo O(n) é melhor que outro com tempo O(n2 ).

• Porém, as constantes de proporcionalidade podem alterar esta consideração.

• Exemplo: um programa leva 100n unidades de tempo para ser executado e


outro leva 2n2 . Qual dos dois programas é melhor?
– Para n < 50, o programa com tempo 2n2 é melhor do que o que possúi
tempo 100n.
– Para problemas com entrada de dados pequena é preferível usar o
programa cujo tempo de execução é O(n2 ).
– Entretanto, quando n cresce, o programa com tempo de execução O(n2 )
leva muito mais tempo que o programa O(n).
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.2 46

Principais Classes de Problemas


• f (n) = O(1).
– Algoritmos de complexidade O(1) são ditos de complexidade constante.
– Uso do algoritmo independe de n.
– As instruções do algoritmo são executadas um número fixo de vezes.
• f (n) = O(log n).
– Um algoritmo de complexidade O(log n) é dito ter complexidade
logarítmica.
– Típico em algoritmos que transformam um problema em outros menores.
– Pode-se considerar o tempo de execução como menor que uma
constante grande.
– Quando n é mil, log2 n ≈ 10, quando n é 1 milhão, log2 n ≈ 20.
– Para dobrar o valor de log n temos de considerar o quadrado de n.
– A base do logaritmo muda pouco estes valores: quando n é 1 milhão, o
log2 n é 20 e o log10 n é 6.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.2 47

Principais Classes de Problemas


• f (n) = O(n).
– Um algoritmo de complexidade O(n) é dito ter complexidade linear.
– Em geral, um pequeno trabalho é realizado sobre cada elemento de
entrada.
– É a melhor situação possível para um algoritmo que tem de
processar/produzir n elementos de entrada/saída.
– Cada vez que n dobra de tamanho, o tempo de execução dobra.
• f (n) = O(n log n).
– Típico em algoritmos que quebram um problema em outros menores,
resolvem cada um deles independentemente e ajuntando as soluções
depois.
– Quando n é 1 milhão, nlog2 n é cerca de 20 milhões.
– Quando n é 2 milhões, nlog2 n é cerca de 42 milhões, pouco mais do que
o dobro.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.2 48

Principais Classes de Problemas


• f (n) = O(n2 ).
– Um algoritmo de complexidade O(n2 ) é dito ter complexidade
quadrática.
– Ocorrem quando os itens de dados são processados aos pares, muitas
vezes em um anel dentro de outro.
– Quando n é mil, o número de operações é da ordem de 1 milhão.
– Sempre que n dobra, o tempo de execução é multiplicado por 4.
– Úteis para resolver problemas de tamanhos relativamente pequenos.

• f (n) = O(n3 ).
– Um algoritmo de complexidade O(n3 ) é dito ter complexidade cúbica.
– Úteis apenas para resolver pequenos problemas.
– Quando n é 100, o número de operações é da ordem de 1 milhão.
– Sempre que n dobra, o tempo de execução fica multiplicado por 8.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.2 49

Principais Classes de Problemas


• f (n) = O(2n ).
– Um algoritmo de complexidade O(2n ) é dito ter complexidade
exponencial.
– Geralmente não são úteis sob o ponto de vista prático.
– Ocorrem na solução de problemas quando se usa força bruta para
resolvê-los.
– Quando n é 20, o tempo de execução é cerca de 1 milhão. Quando n
dobra, o tempo fica elevado ao quadrado.
• f (n) = O(n!).
– Um algoritmo de complexidade O(n!) é dito ter complexidade exponencial,
apesar de O(n!) ter comportamento muito pior do que O(2n ).
– Geralmente ocorrem quando se usa força bruta na solução do problema.
– n = 20 → 20! = 2432902008176640000, um número com 19 dígitos.
– n = 40 → um número com 48 dígitos.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.2 50

Comparação de Funções de Complexidade (1)

Função Tamanho n
de custo 10 20 30 40 50 60

n 0,00001 0,00002 0,00003 0,00004 0,00005 0,00006


s s s s s s

n2 0,0001 0,0004 0,0009 0,0016 0,0.35 0,0036


s s s s s s

n3 0,001 0,008 0,027 0,64 0,125 0.316


s s s s s s

n5 0,1 3,2 24,3 1,7 5,2 13


s s s min min min

2n 0,001 1 17,9 12,7 35,7 366


s s min dias anos séc.

3n 0,059 58 6,5 3855 108 1013


s min anos séc. séc. séc.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.2 51

Comparação de Funções de Complexidade (2)

Função de Computador Computador Computador


custo atual 100 vezes 1.000 vezes
de tempo mais rápido mais rápido
n t1 100 t1 1000 t1
n2 t2 10 t2 31, 6 t2
n3 t3 4, 6 t3 10 t3
2n t4 t4 + 6, 6 t4 + 10
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.2 52

Algoritmos Polinomiais × Algoritmos Exponenciais


• Algoritmo exponencial no tempo de execução tem função de complexidade
O(cn ), c > 1.
• Algoritmo polinomial no tempo de execução tem função de complexidade
O(p(n)), onde p(n) é um polinômio.
• A distinção entre estes dois tipos de algoritmos torna-se significativa quando
o tamanho do problema a ser resolvido cresce.
• Os algoritmos polinomiais são mais úteis na prática que os exponenciais.
• Algoritmos exponenciais são geralmente variações de pesquisa exaustiva.
• Algoritmos polinomiais são geralmente obtidos mediante melhor
entendimento da estrutura do problema.
• Um problema é considerado:
– intratável: se não existe um algoritmo polinomial para resolvê-lo.
– bem resolvido: quando existe um algoritmo polinomial para resolvê-lo.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.2 53

Algoritmos Polinomiais × Algoritmos Exponenciais

• A distinção entre algoritmos polinomiais eficientes e algoritmos


exponenciais ineficientes possui várias exceções.

• Exemplo: um algoritmo com função de complexidade f (n) = 2n é mais


rápido que um algoritmo g(n) = n5 para valores de n menores ou
iguais a 20.

• Também existem algoritmos exponenciais que são muito úteis na


prática.

• Exemplo: o algoritmo Simplex para programação linear possui


complexidade de tempo exponencial para o pior caso mas executa
muito rápido na prática.

• Tais exemplos não ocorrem com frequência na prática, e muitos


algoritmos exponenciais conhecidos não são muito úteis.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.2 54

Exemplo de Algoritmo Exponencial


• Um caixeiro viajante deseja visitar n cidades de tal forma que sua
viagem inicie e termine em uma mesma cidade, e cada cidade deve
ser visitada uma única vez.
• Supondo que sempre há uma estrada entre duas cidades quaisquer, o
problema é encontrar a menor rota para a viagem.
• A figura ilustra o exemplo para quatro cidades c1 , c2 , c3 , c4 , em que os
números nos arcos indicam a distância entre duas cidades.
c1

9 4 8
5 c3 3

c2 c4
8

• O percurso < c1 , c3 , c4 , c2 , c1 > é uma solução para o problema, cujo


percurso total tem distância 24.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.3.2 55

Exemplo de Algoritmo Exponencial

• Um algoritmo simples seria verificar todas as rotas e escolher a menor


delas.
• Há (n − 1)! rotas possíveis e a distância total percorrida em cada rota
envolve n adições, logo o número total de adições é n!.
• No exemplo anterior teríamos 24 adições.
• Suponha agora 50 cidades: o número de adições seria 50! ≈ 1064 .
• Em um computador que executa 109 adições por segundo, o tempo
total para resolver o problema com 50 cidades seria maior do que 1045
séculos só para executar as adições.
• O problema do caixeiro viajante aparece com frequência em
problemas relacionados com transporte, mas também aplicações
importantes relacionadas com otimização de caminho percorrido.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.4 56

Técnicas de Análise de Algoritmos

• Determinar o tempo de execução de um programa pode ser um


problema matemático complexo;
• Determinar a ordem do tempo de execução, sem preocupação com o
valor da constante envolvida, pode ser uma tarefa mais simples.
• A análise utiliza técnicas de matemática discreta, envolvendo
contagem ou enumeração dos elementos de um conjunto:
– manipulação de somas,
– produtos,
– permutações,
– fatoriais,
– coeficientes binomiais,
– solução de equações de recorrência.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.4 57

Análise do Tempo de Execução


• Comando de atribuição, de leitura ou de escrita: O(1).

• Sequência de comandos: determinado pelo maior tempo de execução de


qualquer comando da sequência.

• Comando de decisão: tempo dos comandos dentro do comando condicional,


mais tempo para avaliar a condição, que é O(1).

• Anel: soma do tempo do corpo do anel mais o tempo de avaliar a condição


para terminação (geralmente O(1)), multiplicado pelo número de iterações.

• Procedimentos não recursivos: cada um deve ser computado


separadamente um a um, iniciando com os que não chamam outros
procedimentos. Avalia-se então os que chamam os já avaliados (utilizando os
tempos desses). O processo é repetido até chegar no programa principal.

• Procedimentos recursivos: associada uma função de complexidade f (n)


desconhecida, onde n mede o tamanho dos argumentos.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.4 58

Procedimento não Recursivo


Algoritmo para ordenar os n elementos de um conjunto A em ordem ascendente.
void Ordena(TipoVetor A)
{ /∗ordena o vetor A em ordem ascendente∗/ • Seleciona o menor elemento
int i , j , min, x ; do conjunto.
for ( i = 1; i < n ; i ++) • Troca este com o primeiro ele-
{ min = i ; mento A[1].
for ( j = i + 1; j <= n ; j ++)
• Repita as duas operações
i f ( A[ j − 1] < A[min − 1] ) min = j ;
acima com os n − 1 elementos
/∗ troca A[min ] e A[ i ] ∗/
restantes, depois com os n − 2,
x = A[min − 1];
até que reste apenas um.
A[min − 1] = A[ i − 1];
A[ i − 1] = x ;
}
}
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.4 59

Análise do Procedimento não Recursivo

Anel Interno

• Contém um comando de decisão, com um comando apenas de


atribuição. Ambos levam tempo constante para serem executados.

• Quanto ao corpo do comando de decisão, devemos considerar o pior


caso, assumindo que serSS sempre executado.

• O tempo para incrementar o índice do anel e avaliar sua condição de


terminação é O(1).

• O tempo combinado para executar uma vez o anel é


O(max(1, 1, 1)) = O(1), conforme regra da soma para a notação O.

• Como o número de iterações é n − i, o tempo gasto no anel é


O((n − i) × 1) = O(n − i), conforme regra do produto para a notação O.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.4 60

Análise do Procedimento não Recursivo

Anel Externo

• Contém, além do anel interno, quatro comandos de atribuição.


O(max(1, (n − i), 1, 1, 1)) = O(n − i).

• A linha (1) é executada n − 1 vezes, e o tempo total para executar o


programa está limitado ao produto de uma constante pelo somatório
n(n−1) n2
de (n − i): n−1 (n − i) = = − n
= O(n 2
)
P
1 2 2 2

• Considerarmos o número de comparações como a medida de custo


relevante, o programa faz (n2 )/2 − n/2 comparações para ordenar n
elementos.

• Considerarmos o número de trocas, o programa realiza exatamente


n − 1 trocas.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.4 61

Procedimento Recursivo

Pesquisa(n) ;
(1) i f (n ≤ 1)
(2) ‘inspecione elemento’ e termine
else{
(3) para cada um dos n elementos ‘ inspecione elemento ’ ;
(4) Pesquisa(n/ 3 ) ;
}

• Para cada procedimento recursivo é associada uma função de


complexidade f (n) desconhecida, onde n mede o tamanho dos
argumentos para o procedimento.
• Obtemos uma equação de recorrência para f (n).
• Equação de recorrência: maneira de definir uma função por uma
expressão envolvendo a mesma função.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.4 62

Análise do Procedimento Recursivo


• Seja T (n) uma função de complexidade que represente o número de
inspeções nos n elementos do conjunto.
• O custo de execução das linhas (1) e (2) é O(1) e o da linha (3) é
exatamente n.
• Usa-se uma equação de recorrência para determinar o no de
chamadas recursivas.
• O termo T (n) é especificado em função dos termos anteriores T (1),
T (2), . . ., T (n − 1).
• T (n) = n + T (n/3), T (1) = 1 (para n = 1 fazemos uma inspeção)
• Por exemplo, T (3) = T (3/3) + 3 = 4, T (9) = T (9/3) + 9 = 13, e assim
por diante.
• Para calcular o valor da função seguindo a definição são necessários
k − 1 passos para computar o valor de T (3k ).
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.4 63

Exemplo de Resolução de Equação de Recorrência

• Sustitui-se os termos T (k), k < n, até que todos os termos T (k), k > 1,
tenham sido substituídos por fórmulas contendo apenas T (1).

T (n) = n + T (n/3)
T (n/3) = n/3 + T (n/3/3)
T (n/3/3) = n/3/3 + T (n/3/3/3)
.. ..
. .
T (n/3/3 · · · /3) = n/3/3 · · · /3 + T (n/3 · · · /3)

• Adicionando lado a lado, temos


T (n) = n + n · (1/3) + n · (1/32 ) + n · (1/33 ) + · · · + (n/3/3 · · · /3) que
representa a soma de uma série geométrica de razão 1/3, multiplicada
por n, e adicionada de T (n/3/3 · · · /3), que é menor ou igual a 1.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.4 64

Exemplo de Resolução de Equação de Recorrência

T (n) = n + n · (1/3) + n · (1/32 ) + n · (1/33 ) + · · · +


+ (n/3/3 · · · /3)

• Se desprezarmos o termo T (n/3/3 · · · 


/3), quando

n tende para
i 1 3n
infinito, então T (n) = n i=0 (1/3) =n = ·
P∞
1− 31 2

• Se considerarmos o termo T (n/3/3/3 · · · /3) e denominarmos x o


número de subdivisões por 3 do tamanho do problema, então
n/3x = 1, e n = 3x . Logo x = log3 n.

• Lembrando que T (1) = 1 temos


n(1−( 31 )x ) 3n
− 21 ·
Px−1 n Px−1
T (n) = i=0 3i + T ( 3nx ) =n i=0 (1/3)
i
+1= (1− 31 )
+1= 2

• Logo, o programa do exemplo é O(n).


Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 65

A Linguagem de Programação Pascal (1)

• Os programas apresentados no livro usam apenas as características básicas


do Pascal.

• São evitadas as facilidades mais avançadas disponíveis em algumas


implementações.

• Não apresentaremos a linguagem na sua totalidade, apenas examinamos


algumas características.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 66

A Linguagem de Programação Pascal (2)


• As várias partes componentes de um programa Pascal são:

program cabeçalho do programa


label declaração de rótulo para goto
const definição de constantes
type definição de tipos de dados
var declaração de variáveis
procedure declaração de subprogramas
ou function
begin
..
. comandos do programa

end
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 67

Tipos em Pascal

• Regra geral: tornar explícito o tipo associado quando se declara uma


constante, variável ou função.

• Isso permite testes de consistência durante o tempo de compilação.

• A definição de tipos permite ao programador alterar o nome de tipos


existentes e criar um número ilimitado de outros tipos.

• No caso do Pascal, os tipos podem ser:


– simples,
– estruturados e
– apontadores.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 68

Tipos Simples (1)

• São grupos de valores indivisíveis (integer, boolean, char e real).

• Tipos simples adicionais podem ser enumerados por meio de:


– listagem de novos grupos de valores; Exemplo:
type cor = (vermelho, azul, rosa);
..
.
var c : cor;
..
.
c := rosa;
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 69

Tipos Simples (2)

• indicação de subintervalos. Exemplo:


type ano = 1900..1999;
type letra = ’A’..’Z’;
..
.
var a : ano;
var b : letra;

as atribuições a:=1986 e b:=‘B’ são possíveis, mas a:=2001 e b:=7 não


o são.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 70

Tipos Estruturados
• Definem uma coleção de valores simples, ou um agregado de valores
de tipos diferentes.
• Existem quatro tipos estruturados primitivos:
– Arranjos: tabela n-dimensional de valores homogêneos de
qualquer tipo. Indexada por um ou mais tipos simples, exceto o tipo
real.
– Registros: união de valores de tipos quaisquer, cujos campos
podem ser acessados pelos seus nomes.
– Conjuntos: coleção de todos os subconjuntos de algum tipo
simples, com operadores especiais ∗ (interseção), + (união), −
(diferença) e in (pertence a) definidos para todos os tipos
conjuntos.
– Arquivos: sequência de valores homogêneos de qualquer tipo.
Geralmente é associado com alguma unidade externa.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 71

Tipo Estruturado Arranjo - Exemplo

type cartão = array [1..80] of char;


type matriz = array [1..5, 1..5] of real;
type coluna = array [1..3] of real;
type linha = array [ano] of char;
type alfa = packed array [1..n] of char;
type vetor = array [1..n] of integer;

A constante n deve ser previamente declarada

const n = 20;

Dada a variável

var x: coluna;

as atribuições x[1]:=0.75, x[2]:=0.85 e x[3]:=1.5 são possíveis.


Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 72

Tipo Estruturado Registro - Exemplo (1)

type data = record


dia : 1..31;
mês : 1..12;
end;
type pessoa = record
sobrenome : alfa;
primeironome : alfa;
aniversário : data;
sexo : (m, f);
end;
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 73

Tipo Estruturado Registro - Exemplo (2)


Declarada a variável

var p: pessoa;

valores particulares podem ser atribuídos:

p.sobrenome := ’Ziviani’;
p.primeironome := ’Patricia’;
p.aniversário.dia := 21;
p.aniversário.mês := 10;
p.sexo := f;
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 74

Tipo Estruturado Conjunto - Exemplo

type conjint = set of 1..9;


type conjcor = set of cor;
type conjchar = set of char;

O tipo cor deve ser previamente definido

type cor = (vermelho, azul, rosa);

Declaradas as variáveis

var ci : conjint;
var cc : array [1..5] of conjcor;
var ch: conjchar;
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 75

Tipo Estruturado Conjunto - Exemplo

Valores particulares podem ser construídos e atribuídos:

ci := [1,4,9];
cc[2] := [vermelho..rosa];
cc[4] := [ ];
cc[5] := [azul, rosa];

Prioridade: “interseção” precede “união” e “diferença”, que precedem


“pertence a”.

[1..5,7] ∗ [4,6,8] é [4]


[1..3,5] + [4,6] é [1..6]
[1..3,5] − [2,4] é [1,3,5]
2 in [1..5] é true
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 76

Tipo Estruturado Arquivo - Exemplo


#define N 30
typedef char TipoAlfa [N] ;
typedef struct { int Dia ; int Mes; } TipoData;
typedef struct { O programa ao lado copia o
TipoAlfa Sobrenome, PrimeiroNome; conteúdo do arquivo Velho no
TipoData Aniversario ; arquivo Novo. (Atribuição de
enum { Mas, Fem } Sexo; } Pessoa; nomes de arquivos externos
int main( int argc , char∗ argv [ ] ) ao programa varia de compi-
{ FILE ∗Velho, ∗Novo; Pessoa Registro ; lador para compilador.)
i f ( ( Velho = fopen(argv[1] , " r " )) == NULL )
{ p r i n t f ( "arquivo nao pode ser aberto \n" ) ; exit ( 1 ) ; }
i f ( (Novo = fopen(argv[2] , "w" )) == NULL)
{ p r i n t f ( "arquivo nao pode ser aberto \n" ) ; exit ( 1 ) ; }
while ( fread(&Registro , sizeof(Pessoa) , 1 , Velho) > 0)
fwrite(&Registro , sizeof(Pessoa) , 1 , Novo) ;
fclose (Velho ) ; fclose (Novo) ; return ( 0 ) ;
}
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 77

Tipo Apontador (1)

• São úteis para criar estruturas de dados encadeadas, do tipo listas, árvores
e grafos.

• Um apontador é uma variável que referencia outra variável alocada


dinamicamente.

• Em geral, a variável referenciada é definida como um registro com um


apontador para outro elemento do mesmo tipo.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 78

Tipo Apontador (2)

Exemplo:

type Apontador = ^No;


type No = record
Chave: integer
Apont: Apontador;
end;

Dada uma variável

var Lista: Apontador;

é possível criar uma lista como a ilustrada.

Lista 50 100 ... 200 nil


Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 79

Separador de Comandos

• O ponto e vírgula atua como um separador de comandos.

• Quando mal colocado, pode causar erros que não são detectados em
tempo de compilação.

Exemplo: o trecho de programa abaixo está sintaticamente correto.


Entretanto, o comando de adição serSS executado sempre, e não somente
quando o valor da variável a for igual a zero.

if a = 0 then;
a := a + 1;
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 80

Passagem de Parâmetros (1)

• Por valor ou por variável (ou referência).

• A passagem de parâmetro por variável deve ser utilizada se o valor


pode sofrer alteração dentro do procedimento, e o novo valor deve
retornar para quem chamou o procedimento.
Projeto de Algoritmos – Cap.1 Introdução – Seção 1.5 81

Passagem de Parâmetros (2)

Exemplo: SomaUm recebe o parâmetro x por valor e o parâmetro y por variável.

void SomaUm( int x , int∗y)


{ x = x+1;
∗y = (∗y)+1;
p r i n t f ( "Funcao SomaUm: %d %d\n" , x,∗y ) ;
}
int main( )
{ int a=0, b=0;
SomaUm(a,&b) ;
p r i n t f ( "Programa principal: %d %d\n" , a,b) ;
return(0);
}

Resultado da execução:
Procedimento SomaUm : 1 1
Programa principal :0 1
Paradigmas de Projeto de
Algoritmos ∗

Última alteração: 2 de Setembro de 2010

∗ Transparências elaboradas por Charles Ornelas Almeida, Israel Guerra e Nivio Ziviani
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos 1

Conteúdo do Capítulo

2.1 Indução

2.2 Recursividade
2.2.1 Como Implementar Recursividade
2.2.2 Quando Não Usar Recursividade

2.3 Algoritmos Tentativa e Erro

2.4 Divisão e Conquista

2.5 Balanceamento

2.6 Programação Dinâmica

2.7 Algoritmos Gulosos

2.8 Algoritmos Aproximados


Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.1 2

Indução Matemática
• É útil para provar asserções sobre a correção e a eficiência de
algoritmos.
• Consiste em inferir uma lei geral a partir de instâncias particulares.
• Seja T um teorema que tenha como parâmetro um número natural n
Provando que T é válido para todos os valores de n, provamos que:
1. T é válido para n = 1;
2. Para todo n > 1, se T é válido para n − 1, então T é válido para n.
• A condição 1 é chamada de passo base.
• Provar a condição 2 é geralmente mais fácil que provar o teorema
diretamente (podemos usar a asserção de que T é válido para n − 1).
• Esta afirmativa é a hipótese de indução ou passo indutivo.
• As condições 1 e 2 implicam T válido para n = 2, o que junto com a
condição 2 implica T também válido para n = 3, e assim por diante.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.1 3

Exemplo de Indução Matemática

S(n) = 1 + 2 + · · · + n = n(n + 1)/2

• Para n = 1 a asserção é verdadeira, pois S(1) = 1 = 1 × (1 + 1)/2


(passo base).

• Assumimos que a soma dos primeiros n números naturais S(n) é


n(n + 1)/2 (hipótese de indução).

• Pela definição de S(n) sabemos que S(n + 1) = S(n) + n + 1.

• Usando a hipótese de indução,


S(n + 1) = n(n + 1)/2 + n + 1 = (n + 1)(n + 2)/2, que é exatamente o
que queremos provar.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.1 4

Limite Superior de Equações de Recorrência

• A solução de uma equação de recorrência pode ser difícil de ser


obtida.

• Nestes casos, pode ser mais fácil tentar advinhar a solução ou obter
um limite superior para a ordem de complexidade.

• Advinhar a solução funciona bem quando estamos interessados


apenas em um limite superior, ao invés da solução exata.

• Mostrar que um certo limite existe é mais fácil do que obter o limite.

• Ex.: T (2n) ≤ 2T (n) + 2n − 1, T (2) = 1, definida para valores de n que


são potências de 2.
– O objetivo é encontrar um limite superior na notação O, onde o lado
direito da desigualdade representa o pior caso.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.1 5

Indução Matemática para Resolver Equação de


Recorrência
T (2n) ≤ 2T (n) + 2n − 1, T (2) = 1 quando n é potência de 2.
• Procuramos f (n) tal que T (n) ≤ O(f (n)), mas fazendo com que f (n)
seja o mais próximo possível da solução real para T (n).
• Vamos considerar o palpite f (n) = n2 .
• Queremos provar que T (n) = O(f (n)) por indução matemática em n.
• Passo base: T (2) = 1 ≤ f (2) = 4.
• Passo de indução: provar que T (n) ≤ f (n) implica T (2n) ≤ f (2n).

T (2n) ≤ 2T (n) + 2n − 1, (def. da recorrência)


≤ 2n2 + 2n − 1, (hipótese de indução)
< (2n)2 ,

que é exatamente o que queremos provar. Logo, T (n) = O(n2 ).


Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.1 6

Indução Matemática para Resolver Equação de


Recorrência

• Vamos tentar um palpite menor, f (n) = cn, para alguma constante c.

• Queremos provar que T (n) ≤ cn implica em T (2n) ≤ c2n. Assim:

T (2n) ≤ 2T (n) + 2n − 1, (def. da recorrência)


≤ 2cn + 2n − 1, (hipótese de indução)
> c2n.

• cn cresce mais lentamente que T (n), pois c2n = 2cn e não existe
espaço para o valor 2n − 1.

• Logo, T (n) está entre cn e n2 .


Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.1 7

Indução Matemática para Resolver Equação de


Recorrência
• Vamos então tentar f (n) = n log n.
• Passo base: T (2) < 2 log 2.
• Passo de indução: vamos assumir que T (n) ≤ n log n.
• Queremos mostrar que T (2n) ≤ 2n log 2n. Assim:

T (2n) ≤ 2T (n) + 2n − 1, (def. da recorrência)


≤ 2n log n + 2n − 1, (hipótese de indução)
< 2n log 2n,

• A diferença entre as fórmulas agora é de apenas 1.


• De fato, T (n) = n log n − n + 1 é a solução exata de
T (n) = 2T (n/2) + n − 1, T (1) = 0, que descreve o comportamento do
algoritmo de ordenação Mergesort.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.2 8

Recursividade
• Um procedimento que chama a si mesmo é dito ser recursivo.
• Recursividade permite descrever algoritmos de forma mais clara e
concisa, especialmente problemas recursivos ou que utilizam
estruturas recursivas.
• Ex.: árvore binária de pesquisa:
– Registros com chaves menores estão na subárvore esquerda;
– Registros com chaves maiores estão na subárvore direita.

5 typedef struct TipoRegistro {


int Chave;
3 7
}TipoRegistro ;
2 4 6
typedef struct TipoNo∗ TipoApontador;
1 typedef struct TipoNo {
TipoRegistro Reg;
TipoApontador Esq, Dir ;
}TipoNo;
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.2 9

Recursividade
• Algoritmo para percorrer todos os registros em ordem de
caminhamento central:
1. caminha na subárvore esquerda na ordem central;
2. visita a raiz;
3. caminha na subárvore direita na ordem central.
• Os nós são visitados em ordem lexicográfica das chaves.

void Central (TipoApontador p)


{ i f (p == NULL)
return ;
Central (p −> Esq) ;
p r i n t f ( "%ld \n" , p −> Reg.Chave) ;
Central (p −> Dir ) ;
}
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.2.1 10

Implementação de Recursividade

• Usa uma pilha para armazenar os dados usados em cada chamada


de um procedimento que ainda não terminou.

• Todos os dados não globais vão para a pilha, registrando o estado


corrente da computação.

• Quando uma ativação anterior prossegue, os dados da pilha são


recuperados.

• No caso do caminhamento central:


– para cada chamada recursiva, o valor de p e o endereço de retorno
da chamada recursiva são armazenados na pilha.
– Quando encontra p=nil o procedimento retorna para quem chamou
utilizando o endereço de retorno que está no topo da pilha.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.2.1 11

Problema de Terminação em Procedimentos Recursivos

• Procedimentos recursivos introduzem a possibilidade de iterações que


podem não terminar: existe a necessidade de considerar o problema
de terminação.

• É fundamental que a chamada recursiva a um procedimento P esteja


sujeita a uma condição B, a qual se torna não-satisfeita em algum
momento da computação.

• Esquema para procedimentos recursivos: composição C de comandos


Si e P : P ≡ if B then C[Si , P ]

• Para demonstrar que uma repetição termina, define-se uma função


f (x), sendo x o conjunto de variáveis do programa, tal que:
1. f (x) ≤ 0 implica na condição de terminação;
2. f (x) é decrementada a cada iteração.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.2.1 12

Problema de Terminação em Procedimentos Recursivos

• Uma forma simples de garantir terminação é associar um parâmetro n


para P (no caso por valor) e chamar P recursivamente com n − 1.

• A substituição da condição B por n > 0 garante terminação.


P ≡ if n > 0 then P[Si , P (n − 1)]

• É necessário mostrar que o nível mais profundo de recursão é finito, e


também possa ser mantido pequeno, pois cada ativação recursiva usa
uma parcela de memória para acomodar as variáveis.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.2.2 13

Quando Não Usar Recursividade

• Nem todo problema de natureza recursiva deve ser resolvido com um


algoritmo recursivo.

• Estes podem ser caracterizados pelo esquema P ≡ if B then (S, P )

• Tais programas são facilmente transformáveis em uma versão não


recursiva P ≡ (x := x0 ; while B do S)
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.2.2 14

Exemplo de Quando Não Usar Recursividade (1)

• Cálculo dos números de Fibonacci


f0 = 0, f1 = 1,
fn = fn−1 + fn−2 paran ≥ 2

• Solução: fn = √1 [Φn − (−Φ) −n
], onde Φ = (1 + 5)/2 ≈ 1, 618 é a
5
razão de ouro.

• O procedimento recursivo obtido diretamente da equação é o seguinte:

unsigned int FibRec(unsigned int n)


{ i f (n < 2)
return n;
else return (FibRec(n − 1) + FibRec(n − 2));
}
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.2.2 15

Exemplo de Quando Não Usar Recursividade (2)

• É extremamente ineficiente porque recalcula o mesmo valor várias


vezes.

• Considerando que a medida de complexidade de tempo f (n) é o


número de adições, então f (n) = O(Φn ).

• A complexidade de espaço para calcular fn é O(n), pois apesar do


número de chamadas ser O(Φn ), o tamanho da pilha equivale a um
caminho na árvore de recursividade, que é equivalente a altura da
árvore, isto é, O(log Φn ) = O(n).
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.2.2 16

Versão iterativa do Cálculo de Fibonacci

unsigned int FibIter (unsigned int n)


{ unsigned int i = 1 , k , F = 0; • O programa tem complexidade
for ( k = 1; k <= n ; k++) de tempo O(n) e de espaço O(1).
{
• Evitar uso de recursividade
F += i ;
quando existe solução óbvia por
i = F− i;
iteração.
}
return F; • Comparação versões recursiva e
} iterativa:

n 20 30 50 100
Recursiva 1 seg 2 min 21 dias 109 anos
Iterativa 1/3 mseg 1/2 mseg 3/4 mseg 1,5 mseg
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.3 17

Algoritmos Tentativa e Erro (Backtracking) (1)

• Tentativa e erro: decompor o processo em um número finito de


subtarefas parciais que devem ser exploradas exaustivamente.

• O processo de tentativa gradualmente constrói e percorre uma árvore


de subtarefas.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.3 18

Algoritmos Tentativa e Erro (Backtracking) (2)

• Algoritmos tentativa e erro não seguem regra fixa de computação:


– Passos em direção à solução final são tentados e registrados;
– Caso esses passos tomados não levem à solução final, eles podem
ser retirados e apagados do registro.

• Quando a pesquisa na árvore de soluções cresce rapidamente é


necessário usar algoritmos aproximados ou heurísticas que não
garantem a solução ótima mas são rápidas.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.3 19

Backtracking: Passeio do Cavalo

void Tenta( )
{ i n i c i a l i z a selecao de movimentos;
• Tabuleiro com n × n posi-
do ções: cavalo se movimenta
{ seleciona proximo candidato ao movimento; segundo regras do xadrez.
i f ( aceitavel ) • Problema: a partir de
{ registra movimento;
(x0 , y0 ), encontrar, se exis-
i f ( tabuleiro nao esta cheio)
tir, um passeio do cavalo
{ tenta novo movimento;
que visita todos os pontos
i f (nao sucedido)
do tabuleiro uma única vez.
apaga registro anterior ;
}
}
} while ( ! ( ( movimento bem sucedido)
ou (acabaram-
se os candidatos a movimento) ) ) ;
}
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.3 20

Exemplo de Backtracking - Passeio do Cavalo


• O tabuleiro pode ser representado por uma matriz n × n.
• A situação de cada posição pode ser representada por um inteiro para
recordar o histórico das ocupações:
– t[x,y] = 0, campo < x, y > não visitado,
– t[x,y] = i, campo < x, y > visitado no i-ésimo movimento, 1 ≤ i ≤ n2 .
• Regras do xadrez para os movimentos do cavalo:

3 2

4 1

5 8

6 7
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.3 21

Implementação do Passeio do Cavalo


#define N 8 /∗ Tamanho do lado do tabuleiro ∗/
#define FALSE 0
#define TRUE 1
int i , j , t [N] [N] , a[N] , b[N] ;
short q;
int main( int argc , char ∗argv [ ] ) { /∗ programa principal ∗/
a[ 0 ] = 2 ; a[ 1 ] = 1 ; a[2] = −1; a[3] = −2;
b[ 0 ] = 1 ; b[ 1 ] = 2 ; b[ 2 ] = 2 ; b[3] = 1;
a[4] = −2; a[5] = −1; a[ 6 ] = 1 ; a[7] = 2;
b[4] = −1; b[5] = −2; b[6] = −2; b[7] = −1;
for ( i = 0; i < N; i ++) for ( j = 0; j < N; j ++) t [ i ] [ j ] = 0;
t [0][0] = 1; /∗ escolhemos uma casa do tabuleiro ∗/
Tenta(2 , 0 , 0 , &q) ;
i f ( ! q ) { p r i n t f ( "Sem solucao \n" ) ; return 0 ; }
for ( i = 0; i < N; i ++)
{ for ( j = 0; j < N; j ++) p r i n t f ( " %4d" , t [ i ] [ j ] ) ; putchar( ’ \n ’ ) ; }
return 0;
}
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.3 22

Implementação do Passeio do Cavalo


void Tenta( int i , int x , int y , short ∗q)
{ int u, v ; int k = −1; short q1;
/∗ i n i c i a l i z a selecao de movimentos ∗/
do { ++k ; q1 = FALSE ;
u = x + a[ k ] ; v = y + b[ k ] ;
i f (u >= 0 && u < N && v >= 0 && v < N)
i f ( t [u ] [ v] == 0)
{ t [u ] [ v ] = i ;
i f ( i < N ∗ N) { /∗ tabuleiro nao esta cheio ∗/
Tenta( i + 1 , u, v, &q1 ) ; /∗ tenta novo movimento ∗/
i f ( ! q1)
t [u ] [ v ] = 0 ; /∗ nao sucedido apaga registro anterior ∗/
}
else q1 = TRUE ;
}
} while ( ! ( q1 | | k == 7));
∗q = q1;
}
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.4 23

Divisão e Conquista (1)

• Consiste em dividir o problema em partes menores, encontrar


soluções para as partes, e combiná-las em uma solução global.

• Exemplo: encontrar o maior e o menor elemento de um vetor de


inteiros, A[1..n], n ≥ 1.

• Cada chamada de MaxMin4 atribui à Max e Min o maior e o menor


elemento em A[Linf], A[Linf + 1], · · · , A[Lsup], respectivamente.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.4 24

Divisão e Conquista (2)

void MaxMin4( int Linf , int Lsup, int ∗Max, int ∗Min)
{ int Max1, Max2, Min1, Min2, Meio;
i f (Lsup − Linf <= 1)
{ i f (A[ Linf − 1] < A[Lsup − 1])
{ ∗Max = A[Lsup − 1]; ∗Min = A[ Linf − 1]; }
else { ∗Max = A[ Linf − 1]; ∗Min = A[Lsup − 1]; }
return ;
}
Meio = ( Linf + Lsup) / 2 ;
MaxMin4( Linf , Meio, &Max1, &Min1) ;
MaxMin4(Meio + 1 , Lsup, &Max2, &Min2) ;
i f (Max1 > Max2) ∗Max = Max1; else ∗Max = Max2;
i f (Min1 < Min2) ∗Min = Min1; else ∗Min = Min2;
}
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.4 25

Divisão e Conquista - Análise do Exemplo


• Seja f (n) o número de comparações entre os elementos de A.
f (n) = 1, para n ≤ 2,
f (n) = f (⌊n/2⌋) + f (⌈n/2⌉) + 2, para n > 2.
• Quando n = 2i para algum inteiro positivo i:
f (n) = 2f (n/2) + 2
2f (n/2) = 4f (n/4) + 2 × 2
.. ..
. .
2i−2 f (n/2i−2 ) = 2i−1 f (n/2i−1 ) + 2i−1

• Adicionando lado a lado, obtemos:


f (n) = 2i−1 f (n/2i−1 ) + i−1
k=1 2
k
P

= 2i−1 f (2) + 2i − 2
= 2i−1 + 2i − 2
3n
= 2 − 2.

• Logo, f (n) = 3n/2 − 2 para o melhor caso, pior caso e caso médio.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.4 26

Divisão e Conquista - Análise do Exemplo

• Conforme mostrado no Capítulo 1, o algoritmo dado neste exemplo é


ótimo.

• Entretanto, ele pode ser pior do que os apresentados no Capítulo 1,


pois, a cada chamada recursiva, salva Linf, Lsup, Max e Min, além do
endereço de retorno da chamada para o procedimento.

• Além disso, uma comparação adicional é necessária a cada chamada


recursiva para verificar se Lsup − Linf ≤ 1.

• n + 1 deve ser menor do que a metade do maior inteiro que pode ser
representado pelo compilador, para não provocar overflow na
operação Linf + Lsup.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.4 27

Divisão e Conquista - Teorema Mestre


• Teorema Mestre: Sejam a ≥ 1 e b > 1 constantes, f (n) uma função
assintoticamente positiva e T (n) uma medida de complexidade
definida sobre os inteiros. A solução da equação de recorrência:

T (n) = aT (n/b) + f (n),

para b uma potência de n é:


1. T (n) = Θ(nlogb a ), se f (n) = O(nlogb a−ǫ ) para alguma constante ǫ > 0,
2. T (n) = Θ(nlogb a log n), se f (n) = Θ(nlogb a ),
3. T (n) = Θ(f (n)), se f (n) = Ω(nlogb a+ǫ ) para alguma constante ǫ > 0,
e se af (n/b) ≤ cf (n) para alguma constante c < 1 e todo n a partir
de um valor suficientemente grande.
• O problema é dividido em a subproblemas de tamanho n/b cada um
sendo resolvidos recursivamente em tempo T (n/b) cada.
• A função f (n) descreve o custo de dividir o problema em
subproblemas e de combinar os resultados de cada subproblema.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.4 28

Teorema Mestre: O Que Diz o Teorema

• Em cada um dos três casos a função f (n) é comparada com a função


nlogb a e a solução de T (n) é determinada pela maior das duas funções:
– No caso 1, f (n) tem de ser polinomialmente menor do que nlogb a .
– No caso 2, se as duas funções são iguais, então
T (n) = Θ(nlogb a log n) = Θ(f (n) log n).
– No caso 3, f (n) tem de ser polinomialmente maior do que nlogb a e,
além disso, satisfazer a condição de que af (n/b) ≤ cf (n).
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.4 29

Teorema Mestre: Outros Aspectos

• No caso 1, f (n) tem de ser polinomialmente menor do que nlogb a , isto


é, f (n) tem de ser assintoticamente menor do que nlogb a por um fator
de nǫ , para alguma constante ǫ > 0.

• No caso 3, f (n) tem de ser polinomialmente maior do que nlogb a e,


além disso, satisfazer a condição de que af (n/b) ≤ cf (n).

• Logo, os três casos não cobrem todas as funções f (n) que poderemos
encontrar. Existem algumas poucas aplicações práticas que ficam
entre os casos 1 e 2 (quando f (n) é menor do que nlogb a , mas não
polinomialmente menor) e entre os casos 2 e 3 (quando f (n) é maior
do que nlogb a , mas não polinomialmente maior). Assim, se a função
f (n) cai em um desses intervalos ou se a condição af (n/b) ≤ cf (n)
não é satisfeita, então o Teorema Mestre não pode ser aplicado.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.4 30

Teorema Mestre

• A prova para o caso em que f (n) = cnk , onde c > 0 e k ≥ 0 são duas
constantes inteiras, é tratada no Exercício 2.13.

• A prova desse teorema não precisa ser entendida para ser aplicado,
conforme veremos em exemptrados a seguir.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.4 31

Teorema Mestre: Exemplos do Uso


• Considere a equação de recorrência: T (n) = 4T (n/2) + n,
onde a = 4, b = 2, f (n) = n e nlogb a = nlog2 4 = Θ(n2 ).
O caso 1 se aplica porque f (n) = O(nlogb a−ǫ ) = O(n), onde ǫ = 1, e a
solução é T (n) = Θ(n2 ).
• Considere a equação de recorrência: T (n) = 2T (n/2) + n − 1,
onde a = 2, b = 2, f (n) = n − 1 e nlogb a = nlog2 2 = Θ(n).
O caso 2 se aplica porque f (n) = Θ(nlogb a ) = Θ(n), e a solução é
T (n) = Θ(n log n).
• Considere a equação de recorrência: T (n) = T (2n/3) + n,
onde a = 1, b = 3/2, f (n) = n e nlogb a = nlog3/2 1 = n0 = 1.
O caso 3 se aplica porque f (n) = Ω(nlog3/2 1+ǫ ), onde ǫ = 1 e
af (n/b) = 2n/3 ≤ cf (n) = 2n/3, para c = 2/3 e n ≥ 0. Logo, a solução
é T (n) = Θ(f (n)) = Θ(n).
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.4 32

Teorema Mestre: Exemplos do Uso

• Considere a equação de recorrência: T (n) = 3T (n/4) + n log n,


onde a = 3, b = 4, f (n) = n log n e nlogb a = nlog3 4 = n0.793 .
O caso 3 se aplica porque f (n) = Ω(nlog3 4+ǫ ), onde ǫ ≈ 0.207 e
af (n/b) = 3(n/4) log(n/4) ≤ cf (n) = (3/4)n log n, para c = 3/4 e n
suficientemente grande.
• O Teorema Mestre não se aplica à equação de recorrência:
T (n) = 3T (n/3) + n log n,
onde a = 3, b = 3, f (n) = n log n e nlogb a = nlog3 3 = n.
O caso 3 não se aplica porque, embora f (n) = n log n seja
assintoticamente maior do que nlogb a = n, a função f (n) não é
polinomialmente maior: a razão f (n)/nlogb a = (n log n)/n = log n é
assintoticamente menor do que nǫ para qualquer constante ǫ positiva.
Logo, a solução é T (n) = Θ(f (n)) = Θ(n log n).
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.5 33

Balanceamento

• No projeto de algoritmos, é importante procurar sempre manter o


balanceamento na subdivisão de um problema em partes menores.

• Divisão e conquista não é a única técnica em que balanceamento é


útil.

Vamos considerar um exemplo de ordenação

• Seleciona o menor elemento de A[1..n] e troca-o com o primeiro


elemento A[1].

• Repete o processo com os n − 1 elementos, resultando no segundo


maior elemento, o qual é trocado com o segundo elemento A[2].

• Repetindo para n − 2, n − 3, . . ., 2 ordena a seqüência.


Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.5 34

Balanceamento - Análise do Exemplo


• Equação de recorrência para comparações entre elementos:
T (n) = T (n − 1) + n − 1, T (1) = 0
• Substituindo:
T (n) = T (n − 1) + n − 1
T (n − 1) = T (n − 2) + n − 2
.. ..
. .
T (2) = T (1) + 1

• Adicionando lado a lado, obtemos:


n(n−1)
T (n) = T (1) + 1 + 2 + · · · + n − 1 = 2
= O(n2 )·
• Embora o algoritmo possa ser visto como uma aplicação recursiva de
divisão e conquista, ele não é eficiente para valores grandes de n.
• Para obter eficiência assintotica é necessário balanceamento: dividir
em dois subproblemas de tamanhos aproximadamente iguais, ao invés
de um de tamanho 1 e o outro de tamanho n − 1.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.5 35

Exemplo de Balanceamento - Mergesort


• Intercalação: unir dois arquivos ordenados gerando um terceiro
arquivo ordenado (merge).
• Colocar no terceiro arquivo o menor elemento entre os menores dos
dois arquivos iniciais, desconsiderando este mesmo elemento nos
passos posteriores.
• Este processo deve ser repetido até que todos os elementos dos
arquivos de entrada sejam escolhidos.
• Algoritmo de ordenação (Mergesort):
– dividir recursivamente o vetor a ser ordenado em dois, até obter n
vetores de 1 único elemento.
– Aplicar a intercalação tendo como entrada 2 vetores de um
elemento, formando um vetor ordenado de dois elementos.
– Repetir este processo formando vetores ordenados cada vez
maiores até que todo o vetor esteja ordenado.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.5 36

Exemplo de Balanceamento - Mergesort

void Mergesort( int ∗A, int i , int j )


{ int m; • Considere n uma potência de 2.
if ( i < j )
• Merge(A, i, m, j) recebe 2 seqüências or-
{ m = ( i + j ) / 2;
denadas A[i..m] e A[(m + 1)..j] e produz
Mergesort(A, i , m) ;
outra seqüência ordenada dos elementos
Mergesort(A, m + 1 , j ) ;
de A[i..m] e A[m + 1..j].
Merge(A, i , m, j ) ; i
} • Como A[i..m] e A[m + 1..j] estão ordena-
} dos, Merge requer no máximo n − 1 com-
parações.
• Merge seleciona repetidamente o menor
dentre os menores elementos restantes
em A[i..m] e A[m + 1..j]. Se empatdar
retira de qualquer uma delas.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.5 37

Análise do Mergesort
• Na contagem de comparações, o comportamento do Mergesort pode
ser representado por: T (n) = 2T (n/2) + n − 1, T (1) = 0
• No caso da equação acima temos:

T (n) = 2T (n/2) + n − 1
n
2T (n/2) = 22 T (n/22 ) + 2 − 2 × 1
2
.. ..
. .
n
2i−1 T (n/2i−1 ) = 2i T (n/2i ) + 2i−1 i−1 − 2i−1
2
• Adicionando lado a lado:
i−1 i−1
i i
X 2i−1+1 − 1
X
k
T (n) = 2 T (n/2 ) + n− 2 = in − = n log n − n + 1.
k=0 k=0 2−1

• Para valores grandes de n, o balanceamento levou a um resultado


muito superior, saimos de O(n2 ) para O(n log n).
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.6 38

Programação Dinâmica

• Quando a soma dos tamanhos dos subproblemas é O(n) então é


provável que o algoritmo recursivo tenha complexidade polinomial.

• Quando a divisão de um problema de tamanho n resulta em n


subproblemas de tamanho n − 1 então é provável que o algoritmo
recursivo tenha complexidade exponencial.

• Nesse caso, a técnica de programação dinâmica pode levar a um


algoritmo mais eficiente.

• A programação dinâmica calcula a solução para todos os


subproblemas, partindo dos subproblemas menores para os maiores,
armazenando os resultados em uma tabela.

• A vantagem é que uma vez que um subproblema é resolvido, a


resposta é armazenada em uma tabela e nunca mais é recalculado.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.6 39

Programação Dinâmica - Exemplo


Produto de n matrizes
• M = M1 × M2 × · · · × Mn , onde cada Mi é uma matriz com di−1 linhas
e di colunas.
• A ordem da multiplicação pode ter um efeito enorme no número total
de operações de adição e multiplicação necessárias para obter M .
• Considere o produto de uma matriz p × q por outra matriz q × r cujo
algoritmo requer O(pqr) operações.
• Considere o produto
M = M1 [10, 20] × M2 [20, 50] × M3 [50, 1] × M4 [1, 100], onde as
dimensões de cada matriz está mostrada entre colchetes.
• A avaliação de M na ordem M = M1 × (M2 × (M3 × M4 )) requer
125.000 operações, enquanto na ordem M = (M1 × (M2 × M3 )) × M4
requer apenas 2.200.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.6 40

Programação Dinâmica - Exemplo


• Tentar todas as ordens possíveis para minimizar o número de operações f (n)
é exponencial em n, onde f (n) ≥ 2n−2 .
• Usando programação dinâmica é possível obter um algoritmo O(n3 ).
• Seja mij menor custo para computar Mi × Mi+1 × · · · × Mj , para
1 ≤ i ≤ j ≤ n.
• Nesse caso, 
 0, se i = j,
mij =
 Min
i≤k<j (mik + mk+1,j + di−1 dk dj ), se j > i.

• mik representa o custo mínimo para calcular M ′ = Mi × Mi+1 × · · · × Mk


• mk+1,j representa o custo mínimo para calcular
M ′′ = Mk+1 × Mk+2 × · · · × Mj .
• di−1 dk dj representa o custo de multiplicar M ′ [di−1 , dk ] por M ′′ [dk , dj ].
• mij , j > i representa o custo mínimo de todos os valores possíveis de k
entre i e j − 1, da soma dos três termos.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.6 41

Programação Dinâmica - Exemplo

• O enfoque programação dinâmica calcula os valores de mij na ordem


crescente das diferenças nos subscritos.

• O calculo inicia com mii para todo i, depois mi,i+1 para todo i, depois
mi,i+2 , e assim sucessivamente.

• Desta forma, os valores mik e mk+1,j estarão disponíveis no momento


de calcular mij .

• Isto acontece porque j − i tem que ser estritamente maior do que


ambos os valores de k − i e j − (k + 1) se k estiver no intervalo
i ≤ k < j.

• Programa para computar a ordem de multiplicação de n matrizes,


M1 × M2 × · · · × Mn , de forma a obter o menor número possível de
operações.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.6 42

Programação Dinâmica - Implementação


#define MAXN 10
int main( int argc , char ∗argv [ ] )
{ int i , j , k , h, n, temp, d[ MAXN + 1 ] , m[ MAXN ] [ MAXN ] ;
p r i n t f ( "Numero de matrizes n: " ) ; scanf( "%d%∗[^\n] " , &n) ;
p r i n t f ( "Dimensoes das matrizes : " ) ;
for ( i = 0; i <= n ; i ++) scanf( "%d" , &d[ i ] ) ;
for ( i = 0; i < n ; i ++) m[ i ] [ i ] = 0;
for (h = 1; h <= n − 1; h++)
{ for ( i = 1; i <= n − h ; i ++)
{ j = i + h ; m[ i −1][ j −1] = INT_MAX ;
for ( k = i ; k <= j − 1; k++)
{ temp = m[ i −1][k−1] + m[ k ] [ j − 1] + d[ i − 1] ∗ d[ k ] ∗ d[ j ] ;
i f (temp < m[ i −1][ j −1]) m[ i − 1][ j − 1] = temp; }
p r i n t f ( " m[ %d, %d]= %d" , i − 1 , j − 1 , m[ i − 1][ j − 1]);
}
putchar( ’ \n ’ ) ;
}
return 0 ; }
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.6 43

Programação Dinâmica - Implementação

• A execução do programa obtém o custo mínimo para multiplicar as n


matrizes, assumindo que são necessárias pqr operações para
multiplicar uma matriz p × q por outra matriz q × r.

• A execução do programa para as quatro matrizes onde d0 , d1 , d2 , d3 , d4


são 10, 20, 50, 1, 100, resulta:

m11 = 0 m22 = 0 m33 = 0 m44 = 0


m12 = 10.000 m23 = 1.000 m34 = 5.000
m13 = 1.200 m24 = 3.000
m14 = 2.200
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.6 44

Programação Dinâmica - Princípio da Otimalidade

• A ordem de multiplicação pode ser obtida registrando o valor de k para


cada entrada da tabela que resultou no mínimo.

• Essa solução eficiente está baseada no princípio da otimalidade:


– em uma seqüência ótima de escolhas ou de decisões cada
subseqüência deve também ser ótima.

• Cada subseqüência representa o custo mínimo, assim como mij , j > i.

• Assim, todos os valores da tabela representam escolhas ótimas.

• O princípio da otimalidade não pode ser aplicado indiscriminadamente.

• Quando o princípio não se aplica é provável que não se possa resolver


o problema com sucesso por meio de programação dinâmica.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.6 45

Aplicação do Princípio da Otimalidade

• Por exemplo, quando o problema utiliza recursos limitados, quando o


total de recursos usados nas subinstâncias é maior do que os recursos
disponíveis.

• Se o caminho mais curto entre Belo Horizonte e Curitiba passa por


Campinas:
– o caminho entre Belo Horizonte e Campinas também é o mais curto
possível
– assim como o caminho entre Campinas e Curitiba.
– Logo, o princípio da otimalidade se aplica.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.6 46

Não Aplicação do Princípio da Otimalidade

• No problema de encontrar o caminho mais longo entre duas cidades:


– Um caminho simples nunca visita uma mesma cidade duas vezes.
– Se o caminho mais longo entre Belo Horizonte e Curitiba passa por
Campinas, isso não significa que o caminho possa ser obtido
tomando o caminho simples mais longo entre Belo Horizonte e
Campinas e depois o caminho simples mais longo entre Campinas
e Curitiba.
– Quando os dois caminhos simples são ajuntados é pouco provável
que o caminho resultante também seja simples.
– Logo, o princípio da otimalidade não se aplica.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.7 47

Algoritmos Gulosos

• Resolve problemas de otimização.


• Ex: encontrar o menor caminho entre dois vértices de um grafo.
– Escolhe a aresta que parece mais promissora em qualquer
instante;
– Independente do que possa acontecer, nunca reconsidera a
decisão.
• Não necessita avaliar alternativas, ou usar procedimentos sofisticados
para desfazer decisões tomadas previamente.
• Problema geral: dado um conjunto C, determine um subconjunto
S ⊆ C tal que:
– S satisfaz uma dada propriedade P , e
– S é mínimo (ou máximo) em relação a algum critério α.
• O algoritmo guloso consiste em um processo iterativo em que S é
construído adicionando-se ao mesmo elementos de C um a um.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.7 48

Características dos Algoritmos Gulosos


• Para construir a solução ótima existe um conjunto ou lista de
candidatos.
• São acumulados um conjunto de candidatos considerados e
escolhidos, e o outro de candidatos considerados e rejeitados.
• Existe função que verifica se um conjunto particular de candidatos
produz uma solução (sem considerar otimalidade no momento).
• Outra função verifica se um conjunto de candidatos é viável (também
sem preocupar com a otimalidade).
• Uma função de seleção indica a qualquer momento quais dos
candidatos restantes é o mais promissor.
• Uma função objetivo fornece o valor da solução encontrada, como o
comprimento do caminho construído (não aparece de forma explicita
no algoritmo guloso).
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.7 49

Pseudo Código de Algoritmo Guloso

Conjunto Guloso(Conjunto C) • Inicialmente, o conjunto S de can-


/∗ C: conjunto de candidatos ∗/ didatos escolhidos está vazio.
{ S = ∅ ; /∗ S contem conjunto solucao ∗/ • A cada passo, o melhor candidato
while( (C ! = ∅) && !( solucao(S) ) ) restante ainda não tentado é con-
{ x = seleciona (C) ; siderado. O critério de escolha é
C = C − x; ditado pela função de seleção.
i f viavel (S + x ) S = S + x ;
}
i f solucao(S) return(S) else return( ’Nao existe solucao ’ ) ;
}

• Se o conjunto aumentado de candidatos se torna inviável, o candidato é


rejeitado. Senão, o candidato é adicionado ao conjunto S de escolhidos.

• A cada aumento de S verificamos se S constitui uma solução.


Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.7 50

Características da Implementação de Algoritmos Gulosos


• Quando funciona corretamente, a primeira solução encontrada é
sempre ótima.
• A função de seleção é geralmente relacionada com a função objetivo.
• Se o objetivo é:
– maximizar ⇒ provavelmente escolherá o candidato restante que
proporcione o maior ganho individual.
– minimizar ⇒ então será escolhido o candidato restante de menor
custo.
• O algoritmo nunca muda de idéia:
– Uma vez que um candidato é escolhido e adicionado à solução ele
lá permanece para sempre.
– Uma vez que um candidato é excluído do conjunto solução, ele
nunca mais é reconsiderado.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.8 51

Algoritmos Aproximados

• Problemas que somente possuem algoritmos exponenciais para


resolvê-los são considerados “difíceis”.

• Problemas considerados intratáveis ou difíceis são muito comuns.

• Exemplo: problema do caixeiro viajante cuja complexidade de tempo


é O(n!).

• Diante de um problema difícil é comum remover a exigência de que o


algoritmo tenha sempre que obter a solução ótima.

• Neste caso procuramos por algoritmos eficientes que não garantem


obter a solução ótima, mas uma que seja a mais próxima possível da
solução ótima.
Projeto de Algoritmos – Cap.2 Paradigmas de Projeto de Algoritmos – Seção 2.8 52

Tipos de Algoritmos Aproximados

• Heurística: é um algoritmo que pode produzir um bom resultado, ou


até mesmo obter a solução ótima, mas pode também não produzir
solução alguma ou uma solução que está distante da solução ótima.

• Algoritmo aproximado: é um algoritmo que gera soluções


aproximadas dentro de um limite para a razão entre a solução ótima e
a produzida pelo algoritmo aproximado (comportamento monitorado
sob o ponto de vista da qualidade dos resultados).
Estruturas de Dados Básicas ∗

Última alteração: 16 de Setembro de 2010

∗ Slides elaborados por Charles Ornelas Almeida, Israel Guerra e Nivio Ziviani
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas 1

Conteúdo do Capítulo

3.1 Listas Lineares


3.1.1 Implementação de Listas por meio de Arranjos
3.1.2 Implementação de Listas por meio de Apontadores

3.2 Pilhas
3.2.1 Implementação de Pilhas por meio de Arranjos
3.2.2 Implementação de Pilhas por meio de Apontadores

3.3 Filas
3.3.1 Implementação de Filas por meio de Arranjos
3.3.2 Implementação de Filas por meio de Apontadores
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1 2

Listas Lineares
• Uma das formas mais simples de interligar os elementos de um
conjunto.
• Estrutura em que as operações inserir, retirar e localizar são definidas.
• Podem crescer ou diminuir de tamanho durante a execução de um
programa, de acordo com a demanda.
• Itens podem ser acessados, inseridos ou retirados de uma lista.
• Duas listas podem ser concatenadas para formar uma lista única, ou
uma pode ser partida em duas ou mais listas.
• Adequadas quando não é possível prever a demanda por memória,
permitindo a manipulação de quantidades imprevisíveis de dados, de
formato também imprevisível.
• São úteis em aplicações tais como manipulação simbólica, gerência
de memória, simulação e compiladores.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1 3

Definição de Listas Lineares

• Seqüência de zero ou mais itens x1 , x2 , · · · , xn , na qual xi é de um


determinado tipo e n representa o tamanho da lista linear.

• Sua principal propriedade estrutural envolve as posições relativas dos


itens em uma dimensão.
– Assumindo n ≥ 1, x1 é o primeiro item da lista e xn é o último item
da lista.
– xi precede xi+1 para i = 1, 2, · · · , n − 1
– xi sucede xi−1 para i = 2, 3, · · · , n
– o elemento xi é dito estar na i-ésima posição da lista.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1 4

TAD Listas Lineares


• O conjunto de operações a ser definido depende de cada aplicação.
• Um conjunto de operações necessário a uma maioria de aplicações é:
1. Criar uma lista linear vazia.
2. Inserir um novo item imediatamente após o i-ésimo item.
3. Retirar o i-ésimo item.
4. Localizar o i-ésimo item para examinar e/ou alterar o conteúdo de
seus componentes.
5. Combinar duas ou mais listas lineares em uma lista única.
6. Partir uma lista linear em duas ou mais listas.
7. Fazer uma cópia da lista linear.
8. Ordenar os itens da lista em ordem ascendente ou descendente, de
acordo com alguns de seus componentes.
9. Pesquisar a ocorrência de um item com um valor particular em
algum componente.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1 5

Implementações de Listas Lineares


• Várias estruturas de dados podem ser usadas para representar listas
lineares, cada uma com vantagens e desvantagens particulares.
• As duas representações mais utilizadas são as implementações por
meio de arranjos e de apontadores.
• Exemplo de Conjunto de Operações:
1. FLVazia(Lista). Faz a lista ficar vazia.
2. Insere(x, Lista). Insere x após o último item da lista.
3. Retira(p, Lista, x). Retorna o item x que está na posição p da lista,
retirando-o da lista e deslocando os itens a partir da posição p+1
para as posições anteriores.
4. Vazia(Lista). Esta função retorna true se lista vazia; senão retorna
false.
5. Imprime(Lista). Imprime os itens da lista na ordem de ocorrência.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.1 6

Implementação de Listas por meio de Arranjos


• Os itens da lista são armazenados Itens
em posições contíguas de memória. Primeiro = 1 x1
2 x2
• A lista pode ser percorrida em qual- ..
.
quer direção.
Último−1 xn
• A inserção de um novo item pode ..
.
ser realizada após o último item com MaxTam
custo constante.
• A inserção de um novo item no meio da lista requer um deslocamento
de todos os itens localizados após o ponto de inserção.

• Retirar um item do início da lista requer um deslocamento de itens


para preencher o espaço deixado vazio.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.1 7

Estrutura da Lista Usando Arranjo

#define INICIOARRANJO 1 • Os itens são armazenados em


#define MAXTAM 1000 um array de tamanho suficiente
para armazenar a lista.
typedef int TipoApontador;
• O campo Último aponta para a
typedef int TipoChave;
posição seguinte a do último ele-
typedef struct {
TipoChave Chave;
mento da lista.
/∗ −−− outros componentes−−− ∗/ • O i-ésimo item da lista está arma-
} TipoItem ; zenado na i-ésima posição do ar-
typedef struct { ray, 1 ≤ i <Último.
TipoItem Item [ MAXTAM ] ;
• A constante MaxTam define o ta-
TipoApontador Primeiro , Ultimo ;
} TipoLista ;
manho máximo permitido para a
lista.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.1 8

Operações sobre Lista Usando Arranjo

void FLVazia( TipoLista ∗ Lista )


{ Lista−>Primeiro = INICIOARRANJO ; Lista−>Ultimo = Lista−>Primeiro ; }

int Vazia( TipoLista Lista )


{ return ( Lista . Primeiro == Lista . Ultimo ) ; } /∗ Vazia ∗/

void Insere (TipoItem x , TipoLista ∗ Lista )


{ i f ( Lista −> Ultimo > MAXTAM)
p r i n t f ( " Lista esta cheia \n" ) ;
else { Lista −> Item [ Lista −> Ultimo − 1] = x ;
Lista −> Ultimo++;
}
} /∗ Insere ∗/
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.1 9

Operações sobre Lista Usando Arranjo

void Retira (TipoApontador p, TipoLista ∗ Lista , TipoItem ∗Item)


{ int Aux;
i f ( Vazia(∗ Lista ) | | p >= Lista −> Ultimo)
{ p r i n t f ( "Erro : Posicao nao existe \n" ) ;
return ;
}
∗Item = Lista −> Item [p − 1];
Lista −> Ultimo−−;
for (Aux = p ; Aux < Lista −> Ultimo ; Aux++)
Lista −> Item [Aux − 1] = Lista −> Item [Aux] ;
} /∗ Retira ∗/
void Imprime( TipoLista Lista )
{ int Aux;
for (Aux = Lista . Primeiro − 1; Aux <= ( Lista . Ultimo − 2); Aux++)
p r i n t f ( "%d\n" , Lista . Item [Aux] .Chave) ;
} /∗ Imprime ∗/
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.1 10

Lista Usando Arranjo - Vantagens e Desvantagens

• Vantagem: economia de memória (os apontadores são implícitos


nesta estrutura).

• Desvantagens:
– custo para inserir ou retirar itens da lista, que pode causar um
deslocamento de todos os itens, no pior caso;
– em aplicações em que não existe previsão sobre o crescimento da
lista, a utilização de arranjos em linguagens como o Pascal pode
ser problemática porque nesse caso o tamanho máximo da lista
tem de ser definido em tempo de compilação.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.2 11

Implementação de Listas por meio de Apontadores

• Cada item é encadeado com o seguinte mediante uma variável do tipo


Apontador.

• Permite utilizar posições não contíguas de memória.

• É possível inserir e retirar elementos sem necessidade de deslocar os


itens seguintes da lista.

• Há uma célula cabeça para simplificar as operações sobre a lista.

Lista x1 ... xn nil


Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.2 12

Estrutura da Lista Usando Apontadores

typedef int TipoChave; • A lista é constituída de células.


typedef struct {
• Cada célula contém um item da
TipoChave Chave;
lista e um apontador para a cé-
/∗ outros componentes ∗/
lula seguinte.
} TipoItem ;
typedef struct TipoCelula ∗TipoApontador; • O registro TipoLista contém um
typedef struct TipoCelula { apontador para a célula cabeça
TipoItem Item ; e um apontador para a última cé-
TipoApontador Prox; lula da lista.
} TipoCelula ;

typedef struct {
TipoApontador Primeiro , Ultimo ;
} TipoLista ;
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.2 13

Operações sobre Lista Usando Apontadores


void FLVazia( TipoLista ∗ Lista )
{ Lista−>Primeiro = (TipoApontador ) malloc(sizeof(TipoCelula ) ) ;
Lista−>Ultimo = Lista−>Primeiro ; Lista−>Primeiro−>Prox = NULL ;
}

int Vazia( TipoLista Lista )


{ return ( Lista . Primeiro == Lista . Ultimo ) ; }

void Insere (TipoItem x , TipoLista ∗ Lista )


{ Lista−>Ultimo−>Prox = (TipoApontador ) malloc(sizeof(TipoCelula ) ) ;
Lista−>Ultimo = Lista−>Ultimo−>Prox ; Lista−>Ultimo−>Item = x ;
Lista−>Ultimo−>Prox = NULL ;
}
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.2 14

Operações sobre Lista Usando Apontadores


void Retira (TipoApontador p, TipoLista ∗ Lista , TipoItem ∗Item)
{ /∗−−O item a ser retirado e o seguinte ao apontado por p−−∗/
TipoApontador q;
i f ( Vazia(∗ Lista ) | | p == NULL | | p−>Prox == NULL)
{ p r i n t f ( " Erro : Lista vazia ou posicao nao existe \n" ) ;
return ;
}
q = p−>Prox; ∗ Item = q−>Item ; p−>Prox = q−>Prox;
i f ( p−>Prox == NULL ) Lista−>Ultimo = p;
free (q) ;
}
void Imprime( TipoLista Lista )
{ TipoApontador Aux;
Aux = Lista . Primeiro−>Prox;
while (Aux ! = NULL)
{ p r i n t f ( "%d\n" , Aux−>Item .Chave) ; Aux = Aux−>Prox ; }
}
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.2 15

Listas Usando Apontadores - Vantagens e Desvantagens

• Vantagens:
– Permite inserir ou retirar itens do meio da lista a um custo constante
(importante quando a lista tem de ser mantida em ordem).
– Bom para aplicações em que não existe previsão sobre o
crescimento da lista (o tamanho máximo da lista não precisa ser
definido a priori).

• Desvantagem: utilização de memória extra para armazenar os


apontadores.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.2 16

Exemplo de Uso Listas - Vestibular


• Num vestibular, cada candidato tem direito a três opções para tentar
uma vaga em um dos sete cursos oferecidos.
• Para cada candidato é lido um registro:
– Chave: número de inscrição do candidato.
– NotaFinal: média das notas do candidato.
– Opção: vetor contendo as três opções de curso do candidato.

Chave : 1..999;
NotaFinal : 0..10;
Opcao : array [ 1 . . 3 ] of 1 . . 7 ;

• Problema: distribuir os candidatos entre os cursos, segundo a nota


final e as opções apresentadas por candidato.
• Em caso de empate, os candidatos serão atendidos na ordem de
inscrição para os exames.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.2 17

Vestibular - Possível Solução


• Ordenar registros por NotaFinal, respeitando a ordem de inscrição.
• Percorrer registros com mesma NotaFinal, começando pelo conjunto
de NotaFinal 10, depois NotaFinal 9, e assim por diante.
– Para um conjunto de mesma NotaFinal encaixar cada registro em
um dos cursos, na primeira opção em que houver vaga (se houver).
• Primeiro refinamento:

int Nota ; ordena os registros pelo campo NotaFinal ;


for (Nota = 10; Nota >= 0; Nota −−)
{ while houver registro com mesma nota
{ if existe vaga em um dos cursos de opção do candidato
{ insere registro no conjunto de aprovados}
else insere registro no conjunto de reprovados;
}
}
imprime aprovados por curso ; imprime reprovados;
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.2 18

Vestibular - Classificação dos Alunos

• Uma boa maneira de representar um conjunto de registros é com o


uso de listas.

• Os registros são armazenados em listas para cada nota.

• Após a leitura do último registro os candidatos estão automaticamente


ordenados por NotaFinal.

NotaFinal • Dentro de cada lista, os registros estão orde-


0
nados por ordem de inscrição, desde que os
...

registros sejam lidos e inseridos na ordem


7 de inscrição de cada candidato.
8 ...
9 Registro nil
10 Registro Registro nil
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.2 19

Vestibular - Classificação dos Alunos


• As listas de registros são percorridas, iniciando-se pela de NotaFinal
10, seguida pela de NotaFinal 9, e assim sucessivamente.
• Cada registro é retirado e colocado em uma das listas da abaixo, na
primeira das três opções em que houver vaga.
Cursos
1 Registro Registro ...
2 Registro ...
3 ...
4
5
6
7

• Se não houver vaga, o registro é colocado em uma lista de reprovados.


• Ao final a estrutura acima conterá a relação de candidatos aprovados
em cada curso.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.2 20

Vestibular - Segundo Refinamento


int Nota; TipoChave Chave; lê número de vagas para cada curso;
inicializa listas de classificação, de aprovados e de reprovados;
lê registro;
while ( Chave ! = 0 )
{ insere registro nas listas de classificação, conforme nota final; lê registro; }
for ( Nota= 10; Nota>= 0; Nota−−)
{ while ( houver próximo registro com mesma NotaFinal )
{ retira registro da lista;
if existe vaga em um dos cursos de opção do candidato
{ insere registro na lista de aprovados;
decrementa o número de vagas para aquele curso;
}
else { insere registro na lista de reprovados; }
obtém próximo registro;
}
}
imprime aprovados por curso; imprime reprovados;
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.2 21

Vestibular - Estrutura Final da Lista


#define NOPCOES 3
#define NCURSOS 7
typedef short TipoChave;
typedef struct TipoItem {
TipoChave Chave;
int NotaFinal ;
int Opcao[ NOPCOES ] ;
} TipoItem ;
typedef struct TipoCelula∗ TipoApontador;
typedef struct TipoCelula {
TipoItem Item ;
TipoApontador Prox;
} TipoCelula ;
typedef struct TipoLista {
TipoApontador Primeiro , Ultimo ;
} TipoLista ;
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.2 22

Vestibular - Refinamento Final (1)


#define NOPCOES 3
#define NCURSOS 7
#define FALSE 0
#define TRUE 1
/∗−−−Entram aqui os tipos do Slide 21−−−∗/
TipoItem Registro ;
TipoLista Classificacao [11];
TipoLista Aprovados[ NCURSOS ] ;
TipoLista Reprovados;
long Vagas[ NCURSOS ] ; short Passou; long i , Nota;
/∗−−−Entram aqui os operadores sobre listas dos Slides 13 e 14−−−∗/
void LeRegistro(TipoItem ∗Registro)
{ /∗−−−os valores lidos devem estar separados por brancos−−−∗/
long i ;
scanf( "%hd%d" , &Registro −> Chave, &Registro −> NotaFinal ) ;
for ( i = 0; i < NOPCOES ; i ++) scanf( "%d" , &Registro −> Opcao[ i ] ) ;
}
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.2 23

Vestibular - Refinamento Final (2)


{ /∗−−−Programa principal−−−∗/
for ( i = 1; i <= NCURSOS ; i ++) scanf( "%ld " , &Vagas[ i −1]);
scanf( "%∗[^\n] " ) ;
getchar ( ) ;
for ( i = 0; i <= 10; i ++) FLVazia(&Classificacao [ i ] ) ;
for ( i = 1; i <= NCURSOS ; i ++) FLVazia(&Aprovados[ i −1]);
FLVazia(&Reprovados) ;
LeRegistro(&Registro ) ;
while ( Registro .Chave != 0)
{
Insere (Registro, &Classificacao [ Registro . NotaFinal ] ) ;
LeRegistro(&Registro ) ;
}
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.2 24

Vestibular - Refinamento Final (3)


for (Nota = 10; Nota >= 0; Nota−−)
{ while ( ! Vazia( Classificacao [Nota ] ) )
{ Retira ( Classificacao [Nota ] . Primeiro , &Classificacao [Nota] , &Registro ) ;
i = 1; Passou = FALSE ;
while ( i <= NOPCOES && !Passou)
{ i f (Vagas[ Registro .Opcao[ i −1] − 1] > 0)
{ Insere (Registro, &Aprovados[ Registro .Opcao[ i −1] − 1]);
Vagas[ Registro .Opcao[ i −1] − 1]−−; Passou = TRUE ;
}
i ++;
}
i f ( !Passou) Insere (Registro, &Reprovados) ;
}
}
for ( i = 1; i <= NCURSOS ; i ++)
{ p r i n t f ( "Relacao dos aprovados no Curso%ld \n" , i ) ; Imprime(Aprovados[ i −1]); }
p r i n t f ( "Relacao dos reprovados\n" ) ; Imprime(Reprovados) ;
return 0;
}
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.1.2 25

Vestibular - Refinamento Final

• Observe que o programa é completamente independente da


implementação do tipo abstrato de dados Lista.

• O exemplo mostra a importância de utilizar tipos abstratos de dados


para escrever programas, em vez de utilizar detalhes particulares de
implementação.

• Altera-se a implementação rapidamente. Não é necessário procurar as


referências diretas às estruturas de dados por todo o código.

• Esse aspecto é importante em programas de grande porte.


Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.2 26

Pilha

• É uma lista linear em que todas as inserções, retiradas e, geralmente,


todos os acessos são feitos em apenas um extremo da lista.

• Os itens são colocados um sobre o outro. O item inserido mais


recentemente está no topo e o inserido menos recentemente no fundo.

• O modelo intuitivo é o de um monte de pratos em uma prateleira,


sendo conveniente retirar ou adicionar pratos na parte superior.

• Esta imagem está freqüentemente associada com a teoria de


autômato, na qual o topo de uma pilha é considerado como o
receptáculo de uma cabeça de leitura/gravação que pode empilhar e
desempilhar itens da pilha.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.2 27

Propriedade e Aplicações das Pilhas


• Propriedade: o último item inserido é o primeiro item que pode ser retirado da
lista. São chamadas listas lifo (“last-in, first-out”).
• Existe uma ordem linear para pilhas, do “mais recente para o menos recente”.
• É ideal para estruturas aninhadas de profundidade imprevisível.
• Uma pilha contém uma seqüência de obrigações adiadas. A ordem de
remoção garante que as estruturas mais internas serão processadas antes
das mais externas.
• Aplicações em estruturas aninhadas:
– Quando é necessário caminhar em um conjunto de dados e guardar uma
lista de coisas a fazer posteriormente.
– O controle de seqüências de chamadas de subprogramas.
– A sintaxe de expressões aritméticas.
• As pilhas ocorrem em estruturas de natureza recursiva (como árvores). Elas
são utilizadas para implementar a recursividade.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.2 28

TAD Pilhas

• Conjunto de operações:
1. FPVazia(Pilha). Faz a pilha ficar vazia.
2. Vazia(Pilha). Retorna true se a pilha está vazia; caso contrário,
retorna false.
3. Empilha(x, Pilha). Insere o item x no topo da pilha.
4. Desempilha(Pilha, x). Retorna o item x no topo da pilha, retirando-o
da pilha.
5. Tamanho(Pilha). Esta função retorna o número de itens da pilha.

• Existem várias opções de estruturas de dados que podem ser usadas


para representar pilhas.

• As duas representações mais utilizadas são as implementações por


meio de arranjos e de apontadores.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.2.1 29

Implementação de Pilhas por meio de Arranjos

• Os itens da pilha são armazenados em posições contíguas de


memória.

• Como as inserções e as retiradas ocorrem no topo da pilha, um cursor


chamado Topo é utilizado para controlar a posição do item no topo da
pilha.

Itens
Primeiro = 1 x1
2 x2
..
.
Topo xn
..
.
MaxTam
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.2.1 30

Estrutura da Pilha Usando Arranjo


• Os itens são armazenados em um array do tamanho da pilha.
• O outro campo do mesmo registro contém um apontador para o item
no topo da pilha.
• A constante MaxTam define o tamanho máximo permitido para a pilha.

#define MAXTAM 1000


typedef int TipoApontador;
typedef int TipoChave;
typedef struct {
TipoChave Chave;
/∗ −−− outros componentes−−− ∗/
} TipoItem ;
typedef struct {
TipoItem Item [ MAXTAM ] ;
TipoApontador Topo;
} TipoPilha ;
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.2.1 31

Operações sobre Pilhas Usando Arranjos

void FPVazia( TipoPilha ∗ Pilha )


{ Pilha−>Topo = 0 ; }

int Vazia( TipoPilha Pilha )


{ return ( Pilha .Topo == 0); }

void Empilha(TipoItem x , TipoPilha ∗ Pilha )


{ i f ( Pilha−>Topo == MaxTam) p r i n t f ( "Erro : pilha esta cheia \n" ) ;
else { Pilha−>Topo++; Pilha−>Item [ Pilha−>Topo − 1] = x ; }
}
void Desempilha( TipoPilha ∗Pilha , TipoItem ∗Item)
{ i f ( Vazia(∗Pilha ) ) p r i n t f ( "Erro : pilha esta vazia \n" ) ;
else { ∗ Item = Pilha−>Item [ Pilha−>Topo − 1]; Pilha−>Topo −−; }
}
int Tamanho( TipoPilha Pilha )
{ return ( Pilha .Topo) ; }
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.2.2 32

Implementação de Pilhas por meio de Apontadores

• Há uma célula cabeça no topo


nil
para facilitar a implementação das
6
operações empilha e desempilha Fundo - x1
quando a pilha está vazia.
6
• Para desempilhar o item xn basta ..
.
desligar a célula cabeça da lista e 6
a célula que contém xn passa a ser xn
a célula cabeça.
6
• Para empilhar um novo item, basta Topo -
fazer a operação contrária, criando
Cabeça
uma nova célula cabeça e colo-
cando o item na antiga.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.2.2 33

Estrutura da Pilha Usando Apontadores

• O campo Tamanho evita a typedef int TipoChave;


contagem do número de typedef struct {
itens na função Tamanho. int Chave;
/∗ outros componentes ∗/
• Cada célula de uma pilha
} TipoItem ;
contém um item da pilha e
typedef struct TipoCelula ∗TipoApontador;
um apontador para outra cé- typedef struct TipoCelula {
lula. TipoItem Item ;
• O registro TipoPilha contém TipoApontador Prox;

um apontador para o topo } TipoCelula ;


typedef struct {
da pilha (célula cabeça) e
TipoApontador Fundo, Topo;
um apontador para o fundo
int Tamanho;
da pilha.
} TipoPilha ;
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.2.2 34

Operações sobre Pilhas Usando Apontadores

void FPVazia( TipoPilha ∗ Pilha )


{ Pilha−>Topo = (TipoApontador ) malloc(sizeof(TipoCelula ) ) ;
Pilha−>Fundo = Pilha−>Topo;
Pilha−>Topo−>Prox = NULL;
Pilha−>Tamanho = 0;
}
int Vazia( TipoPilha Pilha )
{ return ( Pilha .Topo == Pilha .Fundo) ; }

void Empilha(TipoItem x , TipoPilha ∗ Pilha )


{ TipoApontador Aux;
Aux = (TipoApontador ) malloc(sizeof(TipoCelula ) ) ;
Pilha−>Topo−>Item = x ;
Aux−>Prox = Pilha−>Topo;
Pilha−>Topo = Aux;
Pilha−>Tamanho++;
}
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.2.2 35

Operações sobre Pilhas Usando Apontadores

void Desempilha( TipoPilha ∗Pilha , TipoItem ∗Item)


{ TipoApontador q;
i f ( Vazia(∗Pilha ) ) { p r i n t f ( "Erro : l i s t a vazia \n" ) ; return ; }
q = Pilha−>Topo;
Pilha−>Topo = q−>Prox;
∗Item = q−>Prox−>Item ;
free (q ) ; Pilha−>Tamanho−−;
}

int Tamanho( TipoPilha Pilha )


{ return ( Pilha .Tamanho) ; }
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.2.2 36

Exemplo de Uso Pilhas - Editor de Textos (ET)


• “#”: cancelar caractere anterior na linha sendo editada. Ex.: UEM##FMB#G
→ UFMG.
• “\”: cancela todos os caracteres anteriores na linha sendo editada.
• “*”: salta a linha. Imprime os caracteres que pertencem à linha sendo
editada, iniciando uma nova linha de impressão a partir do caractere
imediatamente seguinte ao caractere salta-linha. Ex: DCC*UFMG.* → DCC
UFMG.
• Vamos escrever um Editor de Texto (ET) que aceite os três comandos
descritos acima.
• O ET deverá ler um caractere de cada vez do texto de entrada e produzir a
impressão linha a linha, cada linha contendo no máximo 70 caracteres de
impressão.
• O ET deverá utilizar o tipo abstrato de dados Pilha definido anteriormente,
implementado por meio de arranjo.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.2.2 37

Sugestão de Texto para Testar o ET


Este et# um teste para o ET, o extraterrestre em

PASCAL.*Acabamos de testar a capacidade de o ET saltar de linha,

utilizando seus poderes extras (cuidado, pois agora vamos estourar

a capacidade máxima da linha de impressão, que é de 70

caracteres.)*O k#cut#rso dh#e Estruturas de Dados et# h#um

cuu#rsh#o #x# x?*!#?!#+.* Como et# bom

n#nt#ao### r#ess#tt#ar mb#aa#triz#cull#ado nn#x#ele!\ Sera

que este funciona\\\? O sinal? não### deve ficar! ~


Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.2.2 38

ET - Implementação
• Este programa utiliza um tipo abstrato de dados sem conhecer
detalhes de sua implementação.
• A implementação do TAD Pilha que utiliza arranjo pode ser substituída
pela que utiliza apontadores sem causar impacto no programa.

#define MAXTAM 70
#define CANCELACARATER ’# ’
#define CANCELALINHA ’ \ \ ’
#define SALTALINHA ’∗ ’
#define MARCAEOF ’~ ’
typedef char TipoChave;
/ ∗ Entram aqui os tipos da transparência 30 ∗ /
var Pilha : TipoPilha ;
x : TipoItem ;
/ ∗ Entram aqui os operadores da transparência 31 ∗ /
/ ∗ Entra aqui o procedimento Imprime ( transp . 40 ) ∗ /
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.2.2 39

ET - Implementação
int main( int argc , char ∗argv [ ] )
{ TipoPilha Pilha ; TipoItem x ;
FPVazia(&Pilha ) ; x .Chave = getchar ( ) ;
i f ( x .Chave == ’ \n ’ ) x .Chave = ’ ’ ;
while ( x .Chave ! = MARCAEOF)
{ i f ( x .Chave == CANCELACARATER)
{ i f ( ! Vazia( Pilha ) ) Desempilha(&Pilha , &x ) ; }
else i f ( x .Chave == CANCELALINHA ) FPVazia(&Pilha ) ;
else i f ( x .Chave == SALTALINHA ) Imprime(&Pilha ) ;
else { i f (Tamanho( Pilha ) == MAXTAM ) Imprime(&Pilha ) ;
Empilha(x, &Pilha ) ;
}
x .Chave = getchar ( ) ; i f ( x .Chave == ’ \n ’ ) x .Chave = ’ ’ ;
}
i f ( ! Vazia( Pilha ) ) Imprime(&Pilha ) ; return 0;
}
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.2.2 40

ET - Implementação (Procedimento Imprime)

void Imprime( TipoPilha ∗ Pilha )


{ TipoPilha Pilhaux ;
TipoItem x ;
FPVazia(&Pilhaux ) ;
while ( ! Vazia(∗Pilha ) )
{ Desempilha( Pilha , &x ) ; Empilha(x, &Pilhaux ) ;
}
while ( ! Vazia(Pilhaux ) )
{ Desempilha(&Pilhaux, &x ) ; putchar(x .Chave) ;
}
putchar( ’ \n ’ ) ;
}
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.3 41

Fila
• É uma lista linear em que todas as inserções são realizadas em um
extremo da lista, e todas as retiradas e, geralmente, os acessos são
realizados no outro extremo da lista.
• O modelo intuitivo de uma fila é o de uma fila de espera em que as
pessoas no início da fila são servidas primeiro e as pessoas que
chegam entram no fim da fila.
• São chamadas listas fifo (“first-in”, “first-out”).
• Existe uma ordem linear para filas que é a “ordem de chegada”.
• São utilizadas quando desejamos processar itens de acordo com a
ordem “primeiro-que-chega, primeiro-atendido”.
• Sistemas operacionais utilizam filas para regular a ordem na qual
tarefas devem receber processamento e recursos devem ser alocados
a processos.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.3 42

TAD Filas

• Conjunto de operações:
1. FFVazia(Fila). Faz a fila ficar vazia.
2. Enfileira(x, Fila). Insere o item x no final da fila.
3. Desenfileira(Fila, x). Retorna o item x no início da fila, retirando-o
da fila.
4. Vazia(Fila). Esta função retorna true se a fila está vazia; senão
retorna false.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.3.1 43

Implementação de Filas por meio de Arranjos


• Os itens são armazenados em posições contíguas de memória.
• A operação Enfileira faz a parte de trás da fila expandir-se.
• A operação Desenfileira faz a parte da frente da fila contrair-se.
• A fila tende a caminhar pela memória do computador, ocupando
espaço na parte de trás e descartando espaço na parte da frente.
• Com poucas inserções e retiradas, a fila vai ao encontro do limite do
espaço da memória alocado para ela.
• Solução: imaginar o array como um círculo. A primeira posição segue
a última.
n 1
2 Frente
3
...

4
Tras
´ 8 5
7 6
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.3.1 44

Implementação de Filas por meio de Arranjos


n 1
2 Frente
3

...
4
Tras
´ 8 5
7 6

• A fila se encontra em posições contíguas de memória, em alguma


posição do círculo, delimitada pelos apontadores Frente e Trás.

• Para enfileirar, basta mover o apontador Trás uma posição no sentido


horário.

• Para desenfileirar, basta mover o apontador Frente uma posição no


sentido horário.
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.3.1 45

Estrutura da Fila Usando Arranjo


• O tamanho do array circular é definido pela constante MaxTam.
• Os outros campos do registro TipoPilha contêm apontadores para a
parte da frente e de trás da fila.

#define MAXTAM 1000


typedef int TipoApontador;
typedef int TipoChave;
typedef struct {
TipoChave Chave;
/∗ outros componentes ∗/
} TipoItem ;
typedef struct {
TipoItem Item [ MAXTAM ] ;
TipoApontador Frente , Tras;
} TipoFila ;
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.3.1 46

Operações sobre Filas Usando Arranjos

• Nos casos de fila cheia e fila vazia, os apontadores Frente e Trás


apontam para a mesma posição do círculo.

• Uma saída para distinguir as duas situações é deixar uma posição


vazia no array.

• Nesse caso, a fila está cheia quando Trás+1 for igual a Frente.

void FFVazia( TipoFila ∗ Fila )


{ Fila−>Frente = 1;
Fila−>Tras = Fila−>Frente ;
}

int Vazia( TipoFila Fila )


{ return ( Fila . Frente == Fila .Tras ) ; }
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.3.1 47

Operações sobre Filas Usando Arranjos


• A implementação utiliza aritmética modular nos procedimentos
Enfileira e Desenfileira (função mod do Pascal).

void Enfileira (TipoItem x , TipoFila ∗ Fila )


{ i f ( Fila−>Tras % MAXTAM + 1 == Fila−>Frente)
p r i n t f ( " Erro f i l a est a cheia \n" ) ;
else { Fila−>Item [ Fila−>Tras − 1] = x ;
Fila−>Tras = Fila−>Tras % MAXTAM + 1;
}
}
void Desenfileira ( TipoFila ∗ Fila , TipoItem ∗Item)
{ i f ( Vazia(∗ Fila ) )
p r i n t f ( "Erro f i l a esta vazia \n" ) ;
else { ∗ Item = Fila−>Item [ Fila−>Frente − 1];
Fila−>Frente = Fila−>Frente % MAXTAM + 1;
}
}
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.3.2 48

Implementação de Filas por meio de Apontadores

• Há uma célula cabeça é para facilitar a implementação das operações


Enfileira e Desenfileira quando a fila está vazia.

• Quando a fila está vazia, os apontadores Frente e Trás apontam para a


célula cabeça.

• Para enfileirar um novo item, basta criar uma célula nova, ligá-la após
a célula que contém xn e colocar nela o novo item.

• Para desenfileirar o item x1 , basta desligar a célula cabeça da lista e a


célula que contém x1 passa a ser a célula cabeça.

- x1 - ··· - xn - nil

6 6
Frente Trás
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.3.2 49

Estrutura da Fila Usando Apontadores

• A fila é implementada por typedef struct TipoCelula ∗TipoApontador;


meio de células. typedef int TipoChave;
typedef struct TipoItem {
• Cada célula contém um
TipoChave Chave;
item da fila e um aponta-
/∗ outros componentes ∗/
dor para outra célula. } TipoItem ;
• O registro TipoFila con- typedef struct TipoCelula {
tém um apontador para TipoItem Item ;
a frente da fila (célula TipoApontador Prox;
} TipoCelula ;
cabeça) e um apontador
typedef struct TipoFila {
para a parte de trás da
TipoApontador Frente , Tras;
fila.
} TipoFila ;
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.3.2 50

Operações sobre Filas Usando Apontadores

void FFVazia( TipoFila ∗ Fila )


{ Fila−>Frente = (TipoApontador ) malloc(sizeof(TipoCelula ) ) ;
Fila−>Tras = Fila−>Frente ;
Fila−>Frente−>Prox = NULL ;
}

int Vazia( TipoFila Fila )


{ return ( Fila . Frente == Fila .Tras ) ; }

void Enfileira (TipoItem x , TipoFila ∗ Fila )


{ Fila−>Tras−>Prox = (TipoApontador ) malloc(sizeof(TipoCelula ) ) ;
Fila−>Tras = Fila−>Tras−>Prox;
Fila−>Tras−>Item = x ;
Fila−>Tras−>Prox = NULL ;
}
Projeto de Algoritmos – Cap.3 Estruturas de Dados Básicas – Seção 3.3.2 51

Operações sobre Filas Usando Apontadores

void Desenfileira ( TipoFila ∗ Fila , TipoItem ∗Item)


{ TipoApontador q;
i f ( Vazia(∗ Fila ) ) { p r i n t f ( "Erro f i l a esta vazia \n" ) ; return ; }
q = Fila−>Frente ;
Fila−>Frente = Fila−>Frente−>Prox;
∗Item = Fila−>Frente−>Item ;
free (q) ;
}
Ordenação∗

Última alteração: 31 de Agosto de 2010

∗ Transparências elaboradas por Charles Ornelas Almeida, Israel Guerra e Nivio Ziviani
Projeto de Algoritmos – Cap.4 Ordenação 1

Conteúdo do Capítulo
4.1 Ordenação Interna 4.1.7 Ordenação em Tempo Linear
4.1.1 Seleção ∗ Ordenação por Contagem
4.1.2 Inserção ∗ Radixsort para Inteiros
4.1.3 Shellsort ∗ Radixsort para Cadeias de
4.1.4 Quicksort Caracteres

4.1.5 Heapsort 4.2 Ordenação Externa


∗ Filas de Prioridades 4.2.1 Intercalação Balanceada de
∗ Heaps Vários Caminhos
4.1.6 Ordenação Parcial 4.2.2 Implementação por meio de
∗ Seleção Parcial Seleção por Substituição
∗ Inserção Parcial 4.2.3 Considerações Práticas
∗ Heapsort Parcial 4.2.4 Intercalação Polifásica
∗ Quicksort Parcial
4.2.5 Quicksort Externo
Projeto de Algoritmos – Cap.4 Ordenação 2

Introdução - Conceitos Básicos

• Ordenar: processo de rearranjar um conjunto de objetos em uma


ordem ascendente ou descendente.

• A ordenação visa facilitar a recuperação posterior de itens do conjunto


ordenado.
– Dificuldade de se utilizar um catálogo telefônico se os nomes das
pessoas não estivessem listados em ordem alfabética.

• Notação utilizada nos algoritmos:


– Os algoritmos trabalham sobre os registros de um arquivo.
– Cada registro possui uma chave utilizada para controlar a
ordenação.
– Podem existir outros componentes em um registro.
Projeto de Algoritmos – Cap.4 Ordenação 3

Introdução - Conceitos Básicos


• Estrutura de um registro:

typedef long TipoChave;


typedef struct TipoItem {
TipoChave Chave;
/∗ outros componentes ∗/
} TipoItem ;

• Qualquer tipo de chave sobre o qual exista uma regra de ordenação


bem-definida pode ser utilizado.
• Um método de ordenação é estável se a ordem relativa dos itens com
chaves iguais não se altera durante a ordenação.
• Alguns dos métodos de ordenação mais eficientes não são estáveis.
• A estabilidade pode ser forçada quando o método é não-estável.
• Sedgewick (1988) sugere agregar um pequeno índice a cada chave
antes de ordenar, ou então aumentar a chave de alguma outra forma.
Projeto de Algoritmos – Cap.4 Ordenação 4

Introdução - Conceitos Básicos

• Classificação dos métodos de ordenação:


– Interna: arquivo a ser ordenado cabe todo na memória principal.
– Externa: arquivo a ser ordenado não cabe na memória principal.
• Diferenças entre os métodos:
– Em um método de ordenação interna, qualquer registro pode ser
imediatamente acessado.
– Em um método de ordenação externa, os registros são acessados
seqüencialmente ou em grandes blocos.
• A maioria dos métodos de ordenação é baseada em comparações
das chaves.
• Existem métodos de ordenação que utilizam o princípio da
distribuição.
Projeto de Algoritmos – Cap.4 Ordenação 5

Introdução - Conceitos Básicos

• Exemplo de ordenação por distribuição: considere o problema de


ordenar um baralho com 52 cartas na ordem:
A < 2 < 3 < · · · < 10 < J < Q < K

e
♣ < ♦ < ♥ < ♠.

• Algoritmo:
1. Distribuir as cartas em treze montes: ases, dois, três, . . ., reis.
2. Colete os montes na ordem especificada.
3. Distribua novamente as cartas em quatro montes: paus, ouros,
copas e espadas.
4. Colete os montes na ordem especificada.
Projeto de Algoritmos – Cap.4 Ordenação 6

Introdução - Conceitos Básicos

• Métodos como o ilustrado são também conhecidos como ordenação


digital, radixsort ou bucketsort.

• O método não utiliza comparação entre chaves.

• Uma das dificuldades de implementar este método está relacionada


com o problema de lidar com cada monte.

• Se para cada monte nós reservarmos uma área, então a demanda por
memória extra pode tornar-se proibitiva.

• O custo para ordenar um arquivo com n elementos é da ordem de


O(n).
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1 7

Ordenação Interna
• Na escolha de um algoritmo de ordenação interna deve ser
considerado o tempo gasto pela ordenação.
• Sendo n o número registros no arquivo, as medidas de complexidade
relevantes são:
– Número de comparações C(n) entre chaves.
– Número de movimentações M (n) de itens do arquivo.
• O uso econômico da memória disponível é um requisito primordial na
ordenação interna.
• Métodos de ordenação in situ são os preferidos.
• Métodos que utilizam listas encadeadas não são muito utilizados.
• Métodos que fazem cópias dos itens a serem ordenados possuem
menor importância.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1 8

Ordenação Interna

• Classificação dos métodos de ordenação interna:


– Métodos simples:
∗ Adequados para pequenos arquivos.
∗ Requerem O(n2 ) comparações.
∗ Produzem programas pequenos.
– Métodos eficientes:
∗ Adequados para arquivos maiores.
∗ Requerem O(n log n) comparações.
∗ Usam menos comparações.
∗ As comparações são mais complexas nos detalhes.
∗ Métodos simples são mais eficientes para pequenos arquivos.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1 9

Ordenação Interna

• Tipos de dados e variáveis utilizados nos algoritmos de ordenação


interna:

typedef int TipoIndice ;


typedef TipoItem TipoVetor [ MAXTAM + 1];
/∗ MAXTAM + 1 por causa da sentinela em Insercao ∗/
TipoVetor A;

• O índice do vetor vai de 0 até M axT am, devido às chaves sentinelas.

• O vetor a ser ordenado contém chaves nas posições de 1 até n.


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.1 10

Ordenação por Seleção (1)

• Um dos algoritmos mais simples de ordenação.

• Algoritmo:
– Selecione o menor item do vetor.
– Troque-o com o item da primeira posição do vetor.
– Repita essas duas operações com os n − 1 itens restantes, depois
com os n − 2 itens, até que reste apenas um elemento.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.1 11

Ordenação por Seleção (2)

• O método é ilustrado abaixo:


1 2 3 4 5 6
Chaves iniciais: O R D E N A
i=1 A R D E N O
i=2 A D R E N O
i=3 A D E R N O
i=4 A D E N R O
i=5 A D E N O R

• As chaves em negrito sofreram uma troca entre si.


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.1 12

Ordenação por Seleção

void Selecao(TipoItem ∗A, TipoIndice n) • Comparações entre chaves e


{ TipoIndice i , j , Min;
movimentações de registros:
TipoItem x ;
for ( i = 1; i <= n − 1; i ++) C(n) = n2
− n
2 2
{ Min = i ;
M (n) = 3(n − 1)
for ( j = i + 1; j <= n ; j ++)
i f (A[ j ] .Chave < A[Min ] .Chave) Min = j ;
x = A[Min ] ; A[Min] = A[ i ] ; A[ i ] = x ;
}
}

• A atribuição M in := j é executada em média n log n vezes, Knuth


(1973).
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.1 13

Ordenação por Seleção

Vantagens:

• Custo linar para o número de movimentos de registros.

• É o algoritmo a ser utilizado para arquivos com registros muito


grandes.

• É muito interessante para arquivos pequenos.

Desvantagens:

• O fato de o arquivo já estar ordenado não ajuda em nada, pois o custo


continua quadrático.

• O algoritmo não é estável.


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.2 14

Ordenação por Inserção

• Método preferido dos jogadores de cartas.

• Algoritmo:
– Em cada passo a partir de i=2 faça:
∗ Selecione o i-ésimo item da seqüência fonte.
∗ Coloque-o no lugar apropriado na seqüência destino de acordo
com o critério de ordenação.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.2 15

Ordenação por Inserção

• O método é ilustrado abaixo:


1 2 3 4 5 6
Chaves iniciais: O R D E N A
i=2 O R D E N A
i=3 D O R E N A
i=4 D E O R N A
i=5 D E N O R A
i=6 A D E N O R

• As chaves em negrito representam a seqüência destino.


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.2 16

Ordenação por Inserção

void Insercao(TipoItem ∗A, TipoIndice n)


{ TipoIndice i , j ;
TipoItem x ;
for ( i = 2; i <= n ; i ++)
{ x = A[ i ] ; j = i − 1;
A[0] = x ; /∗ sentinela ∗/
while ( x .Chave < A[ j ] .Chave)
{ A[ j +1] = A[ j ] ; j −−;
}
A[ j +1] = x ;
}
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.2 17

Ordenação por Inserção


Considerações sobre o algoritmo:

• O processo de ordenação pode ser terminado pelas condições:


– Um item com chave menor que o item em consideração é
encontrado.
– O final da seqüência destino é atingido à esquerda.

• Solução:
– Utilizar um registro sentinela na posição zero do vetor.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.2 18

Ordenação por Inserção

• Seja C(n) a função que conta o número de comparações.

• No anel mais interno, na i-ésima iteração, o valor de Ci é:

Melhor caso : Ci (n) = 1


Pior caso : Ci (n) = i
Caso médio : Ci (n) = 1i (1 + 2 + · · · + i) = i+1
2

• Assumindo que todas as permutações de n são igualmente prováveis


no caso médio, temos:

Melhor caso : C(n) = (1 + 1 + · · · + 1) = n − 1


n2 n
Pior caso : C(n) = (2 + 3 + · · · + n) = 2
+ 2
−1
n2
Caso médio : C(n) = 21 (3 + 4 + · · · + n + 1) = 4
+ 3n
4
−1
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.2 19

Ordenação por Inserção

• Seja M (n) a função que conta o número de movimentações de


registros.

• O número de movimentações na i-ésima iteração é:

Mi (n) = Ci (n) − 1 + 3 = Ci (n) + 2

• Logo, o número de movimentos é:

Melhor caso : M (n) = (3 + 3 + · · · + 3) = 3(n − 1)


n2 5n
Pior caso : M (n) = (4 + 5 + · · · + n + 2) = 2
+ 2
−3
2
Caso médio : M (n) = 21 (5 + 6 + · · · + n + 3) = n4 + 11n
4
− 3
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.2 20

Ordenação por Inserção

• O número mínimo de comparações e movimentos ocorre quando os


itens estão originalmente em ordem.

• O número máximo ocorre quando os itens estão originalmente na


ordem reversa.

• É o método a ser utilizado quando o arquivo está “quase” ordenado.

• É um bom método quando se deseja adicionar uns poucos itens a um


arquivo ordenado, pois o custo é linear.

• O algoritmo de ordenação por inserção é estável.


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.3 21

Shellsort

• Proposto por Shell em 1959.

• É uma extensão do algoritmo de ordenação por inserção.

• Problema com o algoritmo de ordenação por inserção:


– Troca itens adjacentes para determinar o ponto de inserção.
– São efetuadas n − 1 comparações e movimentações quando o
menor item está na posição mais à direita no vetor.

• O método de Shell contorna este problema permitindo trocas de


registros distantes um do outro.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.3 22

Shellsort

• Os itens separados de h posições são rearranjados.

• Todo h-ésimo item leva a uma seqüência ordenada.

• Tal seqüência é dita estar h-ordenada.

• Exemplo de utilização:

1 2 3 4 5 6
Chaves iniciais: O R D E N A
h=4 N A D E O R
h=2 D A N E O R
h=1 A D E N O R

• Quando h = 1 Shellsort corresponde ao algoritmo de inserção.


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.3 23

Shellsort

• Como escolher o valor de h:


– Seqüência para h:

h(s) = 3h(s − 1) + 1, para s > 1

h(s) = 1, para s = 1.

– Knuth (1973, p. 95) mostrou experimentalmente que esta


seqüência é difícil de ser batida por mais de 20% em eficiência.
– A seqüência para h corresponde a 1, 4, 13, 40, 121, 364, 1.093,
3.280, . . .
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.3 24

Shellsort

void Shellsort (TipoItem ∗A, TipoIndice n)


{ int i , j ; int h = 1;
TipoItem x ;
do h = h ∗ 3 + 1; while (h < n) ;
do
{ h /= 3;
for ( i = h + 1; i <= n ; i ++)
{ x = A[ i ] ; j = i;
while (A[ j − h ] .Chave > x .Chave)
{ A[ j ] = A[ j − h ] ; j −= h;
i f ( j <= h) goto L999;
}
L999: A[ j ] = x ;
}
} while (h ! = 1 ) ;
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.3 25

Shellsort

• A implementação do Shellsort não utiliza registros sentinelas.

• Seriam necessários h registros sentinelas, uma para cada


h-ordenação.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.3 26

Shellsort: Análise

• A razão da eficiência do algoritmo ainda não é conhecida.

• Ninguém ainda foi capaz de analisar o algoritmo.

• A sua análise contém alguns problemas matemáticos muito difíceis.

• A começar pela própria seqüência de incrementos.

• O que se sabe é que cada incremento não deve ser múltiplo do


anterior.

• Conjecturas referente ao número de comparações para a seqüência


de Knuth:
Conjetura 1 : C(n) = O(n1,25 )

Conjetura 2 : C(n) = O(n(ln n)2 )


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.3 27

Shellsort

• Vantagens:
– Shellsort é uma ótima opção para arquivos de tamanho moderado.
– Sua implementação é simples e requer uma quantidade de código
pequena.

• Desvantagens:
– O tempo de execução do algoritmo é sensível à ordem inicial do
arquivo.
– O método não é estável,
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.4 28

Quicksort

• Proposto por Hoare em 1960 e publiccado em 1962.

• É o algoritmo de ordenação interna mais rápido que se conhece para


uma ampla variedade de situações.

• Provavelmente é o mais utilizado.

• A idéia básica é dividir o problema de ordenar um conjunto com n itens


em dois problemas menores.

• Os problemas menores são ordenados independentemente.

• Os resultados são combinados para produzir a solução final.


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.4 29

Quicksort

• A parte mais delicada do método é o processo de partição.

• O vetor A[Esq..Dir] é rearranjado por meio da escolha arbitrária de um


pivô x.

• O vetor A é particionado em duas partes:


– A parte esquerda com chaves menores ou iguais a x.
– A parte direita com chaves maiores ou iguais a x.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.4 30

Quicksort

• Algoritmo para o particionamento:


1. Escolha arbitrariamente um pivô x.
2. Percorra o vetor a partir da esquerda até que A[i] ≥ x.
3. Percorra o vetor a partir da direita até que A[j] ≤ x.
4. Troque A[i] com A[j].
5. Continue este processo até os apontadores i e j se cruzarem.

• Ao final, o vetor A[Esq..Dir] está particionado de tal forma que:


– Os itens em A[Esq], A[Esq + 1], . . . , A[j] são menores ou iguais a x.
– Os itens em A[i], A[i + 1], . . . , A[Dir] são maiores ou iguais a x.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.4 31

Quicksort

• Ilustração do processo de partição:

1 2 3 4 5 6
O R D E N A
A R D E N O
A D R E N O

• O pivô x é escolhido como sendo A[(i + j) div 2].

• Como inicialmente i = 1 e j = 6, então x = A[3] = D.

• Ao final do processo de partição i e j se cruzam em i = 3 e j = 2.


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.4 32

Quicksort
Procedimento Particao:

void Particao ( TipoIndice Esq, TipoIndice Dir , TipoIndice ∗ i , TipoIndice ∗ j , TipoItem ∗A)
{ TipoItem x , w;
∗ i = Esq; ∗ j = Dir ;
x = A[(∗ i + ∗ j ) / 2 ] ; /∗ obtem o pivo x ∗/
do
{ while ( x .Chave > A[∗ i ] .Chave) ( ∗ i )++;
while ( x .Chave < A[∗ j ] .Chave) ( ∗ j )−−;
i f (∗ i <= ∗ j )
{ w = A[∗ i ] ; A[∗ i ] = A[∗ j ] ; A[∗ j ] = w;
(∗ i )++; (∗ j )−−;
}
} while (∗ i <= ∗ j ) ;
}

• O anel interno do procedimento Particao é extremamente simples.


• Razão pela qual o algoritmo Quicksort é tão rápido.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.4 33

Quicksort
Procedimento Quicksort:

/∗−− Entra aqui o procedimento Particao da transparencia 32−−∗/


void Ordena( TipoIndice Esq, TipoIndice Dir , TipoItem ∗A)
{ TipoIndice i , j ;
Particao (Esq, Dir , & i , & j , A) ;
i f (Esq < j ) Ordena(Esq, j , A) ;
i f ( i < Dir ) Ordena( i , Dir , A) ;
}

void QuickSort(TipoItem ∗A, TipoIndice n)


{ Ordena(1 , n, A) ; }
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.4 34

Quicksort

• Exemplo do estado do vetor em cada chamada recursiva do


procedimento Ordena:

1 2 3 4 5 6
Chaves iniciais: O R D E N A
1 A D R E N O
2 A D
3 E R N O
4 N R O
5 O R
A D E N O R
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.4 35

Quicksort: Análise
• Seja C(n) a função que conta o número de comparações.
• Pior caso:
C(n) = O(n2 )

• O pior caso ocorre quando, sistematicamente, o pivô é escolhido como


sendo um dos extremos de um arquivo já ordenado.
• Isto faz com que o procedimento Ordena seja chamado
recursivamente n vezes, eliminando apenas um item em cada
chamada.
• O pior caso pode ser evitado empregando pequenas modificações no
algoritmo.
• Para isso basta escolher três itens quaisquer do vetor e usar a
mediana dos três como pivô.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.4 36

Quicksort: Análise

• Melhor caso:

C(n) = 2C(n/2) + n = n log n − n + 1

• Esta situação ocorre quando cada partição divide o arquivo em duas


partes iguais.

• Caso médio de acordo com Sedgewick e Flajolet (1996, p. 17):

C(n) ≈ 1, 386n log n − 0, 846n,

• Isso significa que em média o tempo de execução do Quicksort é


O(n log n).
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.4 37

Quicksort

• Vantagens:
– É extremamente eficiente para ordenar arquivos de dados.
– Necessita de apenas uma pequena pilha como memória auxiliar.
– Requer cerca de n log n comparações em média para ordenar n
itens.

• Desvantagens:
– Tem um pior caso O(n2 ) comparações.
– Sua implementação é muito delicada e difícil:
∗ Um pequeno engano pode levar a efeitos inesperados para
algumas entradas de dados.
– O método não é estável.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 38

Heapsort

• Possui o mesmo princípio de funcionamento da ordenação por


seleção.

• Algoritmo:
1. Selecione o menor item do vetor.
2. Troque-o com o item da primeira posição do vetor.
3. Repita estas operações com os n − 1 itens restantes, depois com
os n − 2 itens, e assim sucessivamente.

• O custo para encontrar o menor (ou o maior) item entre n itens é n − 1


comparações.

• Isso pode ser reduzido utilizando uma fila de prioridades.


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 39

Heapsort
Filas de Prioridades

• É uma estrutura de dados onde a chave de cada item reflete sua


habilidade relativa de abandonar o conjunto de itens rapidamente.

• Aplicações:
– SOs usam filas de prioridades, nas quais as chaves representam o
tempo em que eventos devem ocorrer.
– Métodos numéricos iterativos são baseados na seleção repetida de
um item com maior (menor) valor.
– Sistemas de gerência de memória usam a técnica de substituir a
página menos utilizada na memória principal por uma nova página.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 40

Heapsort
Filas de Prioridades - Tipo Abstrato de Dados
• Operações:
1. Constrói uma fila de prioridades a partir de um conjunto com n
itens.
2. Informa qual é o maior item do conjunto.
3. Retira o item com maior chave.
4. Insere um novo item.
5. Aumenta o valor da chave do item i para um novo valor que é maior
que o valor atual da chave.
6. Substitui o maior item por um novo item, a não ser que o novo item
seja maior.
7. Altera a prioridade de um item.
8. Remove um item qualquer.
9. Ajunta duas filas de prioridades em uma única.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 41

Heapsort
Filas de Prioridades - Representação

• Representação através de uma lista linear ordenada:


– Neste caso, Constrói leva tempo O(n log n).
– Insere é O(n).
– Retira é O(1).
– Ajunta é O(n).

• Representação é através de uma lista linear não ordenada:


– Neste caso, Constrói tem custo linear.
– Insere é O(1).
– Retira é O(n).
– Ajunta é O(1) para apontadores e O(n) para arranjos.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 42

Heapsort
Filas de Prioridades - Representação

• A melhor representação é através de uma estruturas de dados


chamada heap:
– Neste caso, Constrói é O(n).
– Insere, Retira, Substitui e Altera são O(log n).

• Observação:
Para implementar a operação Ajunta de forma eficiente e ainda
preservar um custo logarítmico para as operações Insere, Retira,
Substitui e Altera é necessário utilizar estruturas de dados mais
sofisticadas, tais como árvores binomiais (Vuillemin, 1978).
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 43

Heapsort
Filas de Prioridades - Algoritmos de Ordenação
• As operações das filas de prioridades podem ser utilizadas para
implementar algoritmos de ordenação.
• Basta utilizar repetidamente a operação Insere para construir a fila de
prioridades.
• Em seguida, utilizar repetidamente a operação Retira para receber os
itens na ordem reversa.
• O uso de listas lineares não ordenadas corresponde ao método da
seleção.
• O uso de listas lineares ordenadas corresponde ao método da
inserção.
• O uso de heaps corresponde ao método Heapsort.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 44

Heap

• É uma seqüência de itens com chaves c[1], c[2], . . . , c[n], tal que:

c[i] ≥ c[2i],
c[i] ≥ c[2i + 1],

para todo i = 1, 2, . . . , n/2.

• A definição pode ser facilmente visualizada em uma árvore binária


completa:

1 S

2 R O 3

4 E 5 N A 6 D 7
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 45

Heap

• Árvore binária completa:


– Os nós são numerados de 1 a n.
– O primeiro nó é chamado raiz.
– O nó ⌊k/2⌋ é o pai do nó k, para 1 < k ≤ n.
– Os nós 2k e 2k + 1 são os filhos à esquerda e à direita do nó k, para
1 ≤ k ≤ ⌊k/2⌋.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 46

Heap
• As chaves na árvore satisfazem a condição do heap.
• A chave em cada nó é maior do que as chaves em seus filhos.
• A chave no nó raiz é a maior chave do conjunto.
• Uma árvore binária completa pode ser representada por um array:

1 2 3 4 5 6 7
S R O E N A D

• A representação é extremamente compacta.


• Permite caminhar pelos nós da árvore facilmente.
• Os filhos de um nó i estão nas posições 2i e 2i + 1.
• O pai de um nó i está na posição i div 2.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 47

Heap

• Na representação do heap em um arranjo, a maior chave está sempre


na posição 1 do vetor.

• Os algoritmos para implementar as operações sobre o heap operam


ao longo de um dos caminhos da árvore.

• Um algoritmo elegante para construir o heap foi proposto por Floyd em


1964.

• O algoritmo não necessita de nenhuma memória auxiliar.

• Dado um vetor A[1], A[2], . . . , A[n].

• Os itens A[n/2 + 1], A[n/2 + 2], . . . , A[n] formam um heap:


– Neste intervalo não existem dois índices i e j tais que j = 2i ou
j = 2i + 1.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 48

Heap

1 2 3 4 5 6 7
Chaves iniciais: O R D E N A S
Esq = 3 O R S E N A D
Esq = 2 O R S E N A D
Esq = 1 S R O E N A D

• Os itens de A[4] a A[7] formam um heap.

• O heap é estendido para a esquerda (Esq = 3), englobando o item


A[3], pai dos itens A[6] e A[7].
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 49

Heap

• A condição de heap é violada:


– O heap é refeito trocando os itens D e S.
• O item R é incluindo no heap (Esq = 2), o que não viola a condição de
heap.
• O item O é incluindo no heap (Esq = 1).
• A Condição de heap violada:
– O heap é refeito trocando os itens O e S, encerrando o processo.
O Programa que implementa a operação que informa o item com maior
chave:

TipoItem Max(TipoItem ∗A)


{ return (A[ 1 ] ) ; }
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 50

Heap
Programa para refazer a condição de heap:

void Refaz( TipoIndice Esq, TipoIndice Dir , TipoItem ∗A)


{ TipoIndice i = Esq;
int j ; TipoItem x ;
j = i ∗ 2;
x = A[ i ] ;
while ( j <= Dir )
{ i f ( j < Dir )
{ i f (A[ j ] .Chave < A[ j +1].Chave)
j ++;
}
i f ( x .Chave >= A[ j ] .Chave) goto L999;
A[ i ] = A[ j ] ; i = j ; j = i ∗ 2;
}
L999: A[ i ] = x ;
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 51

Heap

Programa para construir o heap:

void Constroi(TipoItem ∗A, TipoIndice n)


{ TipoIndice Esq;
Esq = n / 2 + 1 ;
while (Esq > 1)
{ Esq−−;
Refaz(Esq, n, A) ;
}
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 52

Heap

Programa que implementa a operação de retirar o item com maior chave:

TipoItem RetiraMax(TipoItem ∗A, TipoIndice ∗n)


{ TipoItem Maximo;
i f (∗n < 1)
p r i n t f ( "Erro : heap vazio \n" ) ;
else { Maximo = A[ 1 ] ; A[1] = A[∗n ] ; (∗n)−−;
Refaz(1 , ∗n, A) ;
}
return Maximo;
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 53

Heap
Programa que implementa a operação de aumentar o valor da chave do
item i:

void AumentaChave( TipoIndice i , TipoChave ChaveNova, TipoItem ∗A)


{ TipoItem x ;
i f (ChaveNova < A[ i ] .Chave)
{ p r i n t f ( "Erro : ChaveNova menor que a chave atual \n" ) ;
return ;
}
A[ i ] .Chave = ChaveNova;
while ( i > 1 && A[ i / 2 ] .Chave < A[ i ] .Chave)
{ x = A[ i / 2 ] ; A[ i / 2 ] = A[ i ] ; A[ i ] = x ;
i /= 2;
}
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 54

Heap
• Exemplo da operação de aumentar o valor da chave do item na
posição i:
(a) S (b) S

R O R O

i i
E N A D E N U D

(c) S (d) U i

i
R U R S

E N O D E N O D

• O tempo de execução do procedimento AumentaChave em um item do


heap é O(log n).
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 55

Heap

Programa que implementa a operação de inserir um novo item no heap:

void Insere (TipoItem ∗x , TipoItem ∗A, TipoIndice ∗n)


{(∗n)++; A[∗n] = ∗x ; A[∗n ] .Chave = INT_MIN ;
AumentaChave(∗n, x−>Chave, A) ;
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 56

Heapsort

• Algoritmo:
1. Construir o heap.
2. Troque o item na posição 1 do vetor (raiz do heap) com o item da
posição n.
3. Use o procedimento Refaz para reconstituir o heap para os itens
A[1], A[2], . . . , A[n − 1].
4. Repita os passos 2 e 3 com os n − 1 itens restantes, depois com os
n − 2, até que reste apenas um item.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 57

Heapsort
• Exemplo de aplicação do Heapsort:

1 2 3 4 5 6 7
S R O E N A D
R N O E D A S
O N A E D R
N E A D O
E D A N
D A E
A D

• O caminho seguido pelo procedimento Refaz para reconstituir a


condição do heap está em negrito.
• Por exemplo, após a troca dos itens S e D na segunda linha da Figura,
o item D volta para a posição 5, após passar pelas posições 1 e 2.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 58

Heapsort
Programa que mostra a implementação do Heapsort:

void Heapsort(TipoItem ∗A, TipoIndice n) Análise


{ TipoIndice Esq, Dir ; • O procedimento Refaz
TipoItem x ;
gasta cerca de log n opera-
Constroi(A, n ) ; /∗ constroi o heap ∗/
ções, no pior caso.
Esq = 1; Dir = n;
while ( Dir > 1)
{ /∗ ordena o vetor ∗/
x = A[ 1 ] ; A[1] = A[ Dir ] ; A[ Dir ] = x ; Dir−−;
Refaz(Esq, Dir , A) ;
}
}

• Logo, Heapsort gasta um tempo de execução proporcional a n log n, no


pior caso.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 59

Heapsort

• Vantagens:
– O comportamento do Heapsort é sempre O(n log n), qualquer que
seja a entrada.

• Desvantagens:
– O anel interno do algoritmo é bastante complexo se comparado
com o do Quicksort.
– O Heapsort não é estável.

• Recomendado:
– Para aplicações que não podem tolerar eventualmente um caso
desfavorável.
– Não é recomendado para arquivos com poucos registros, por causa
do tempo necessário para construir o heap.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 60

Comparação entre os Métodos


Complexidade:

Complexidade
Inserção O(n2 )
Seleção O(n2 )
Shellsort O(n log n)
Quicksort O(n log n)
Heapsort O(n log n)

• Apesar de não se conhecer analiticamente o comportamento do


Shellsort, ele é considerado um método eficiente).
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 61

Comparação entre os Métodos


Tempo de execução:

• Oservação: O método que levou menos tempo real para executar


recebeu o valor 1 e os outros receberam valores relativos a ele.

• Registros na ordem aleatória:

5.00 5.000 10.000 30.000


Inserção 11,3 87 161 –
Seleção 16,2 124 228 –
Shellsort 1,2 1,6 1,7 2
Quicksort 1 1 1 1
Heapsort 1,5 1,6 1,6 1,6
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 62

Comparação entre os Métodos

• Registros na ordem ascendente:

500 5.000 10.000 30.000


Inserção 1 1 1 1
Seleção 128 1.524 3.066 –
Shellsort 3,9 6,8 7,3 8,1
Quicksort 4,1 6,3 6,8 7,1
Heapsort 12,2 20,8 22,4 24,6
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 63

Comparação entre os Métodos


Tempo de execução:

• Registros na ordem descendente:

500 5.000 10.000 30.000


Inserção 40,3 305 575 –
Seleção 29,3 221 417 –
Shellsort 1,5 1,5 1,6 1,6
Quicksort 1 1 1 1
Heapsort 2,5 2,7 2,7 2,9
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 64

Comparação entre os Métodos


Observações sobre os métodos:
1. Shellsort, Quicksort e Heapsort têm a mesma ordem de grandeza.
2. O Quicksort é o mais rápido para todos os tamanhos aleatórios
experimentados.
3. A relação Heapsort/Quicksort é constante para todos os tamanhos.
4. A relação Shellsort/Quicksort aumenta se o número de elementos aumenta.
5. Para arquivos pequenos (500 elementos), o Shellsort é mais rápido que o
Heapsort.
6. Se a entrada aumenta, o Heapsort é mais rápido que o Shellsort.
7. O Inserção é o mais rápido se os elementos estão ordenados.
8. O Inserção é o mais lento para qualquer tamanho se os elementos estão em
ordem descendente.
9. Entre os algoritmos de custo O(n2 ), o Inserção é melhor para todos os
tamanhos aleatórios experimentados.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 65

Comparação entre os Métodos


Influência da ordem inicial dos registros:
Shellsort Quicksort Heapsort
5.000 10.000 30.000 5.000 10.000 30.000 5.000 10.000 30.000
Asc 1 1 1 1 1 1 1,1 1,1 1,1
Des 1,5 1,6 1,5 1,1 1,1 1,1 1 1 1
Ale 2,9 3,1 3,7 1,9 2,0 2,0 1,1 1 1

1. Shellsort é bastante sensível à ordenação ascendente ou descendente da


entrada.
2. Em arquivos do mesmo tamanho, o Shellsort executa mais rápido para
arquivos ordenados.
3. Quicksort é sensível à ordenação ascendente ou descendente da entrada.
4. Em arquivos do mesmo tamanho, o Quicksort executa mais rápido para
arquivos ordenados.
5. O Quicksort é o mais rápido para qualquer tamanho para arquivos na ordem
ascendente.
6. O Heapsort praticamente não é sensível à ordenação da entrada.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 66

Comparação entre os Métodos


Método da Inserção:

• É o mais interessante para arquivos com menos do que 20 elementos.

• O método é estável.

• Possui comportamento melhor do que o método da bolha


(Bubblesort) que também é estável.

• Sua implementação é tão simples quanto as implementações do


Bubblesort e Seleção.

• Para arquivos já ordenados, o método é O(n).

• O custo é linear para adicionar alguns elementos a um arquivo já


ordenado.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 67

Comparação entre os Métodos


Método da Seleção:

• É vantajoso quanto ao número de movimentos de registros, que é


O(n).

• Deve ser usado para arquivos com registros muito grandes, desde que
o tamanho do arquivo não exceda 1.000 elementos.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 68

Comparação entre os Métodos


Shellsort:

• É o método a ser escolhido para a maioria das aplicações por ser


muito eficiente para arquivos de tamanho moderado.

• Mesmo para arquivos grandes, o método é cerca de apenas duas


vezes mais lento do que o Quicksort.

• Sua implementação é simples e geralmente resulta em um programa


pequeno.

• Não possui um pior caso ruim e quando encontra um arquivo


parcialmente ordenado trabalha menos.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 69

Comparação entre os Métodos


Quicksort:
• É o algoritmo mais eficiente que existe para uma grande variedade de
situações.
• É um método bastante frágil no sentido de que qualquer erro de
implementação pode ser difícil de ser detectado.
• O algoritmo é recursivo, o que demanda uma pequena quantidade de
memória adicional.
• Seu desempenho é da ordem de O(n2 ) operações no pior caso.
• O principal cuidado a ser tomado é com relação à escolha do pivô.
• A escolha do elemento do meio do arranjo melhora muito o
desempenho quando o arquivo está total ou parcialmente ordenado.
• O pior caso tem uma probabilidade muito remota de ocorrer quando os
elementos forem aleatórios.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 70

Comparação entre os Métodos


Quicksort:

• Geralmente se usa a mediana de uma amostra de três elementos para


evitar o pior caso.

• Esta solução melhora o caso médio ligeiramente.

• Outra importante melhoria para o desempenho do Quicksort é evitar


chamadas recursivas para pequenos subarquivos.

• Para isto, basta chamar um método de ordenação simples nos


arquivos pequenos.

• A melhoria no desempenho é significativa, podendo chegar a 20%


para a maioria das aplicações (Sedgewick, 1988).
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 71

Comparação entre os Métodos


Heapsort:

• É um método de ordenação elegante e eficiente.

• Apesar de ser cerca de duas vezes mais lento do que o Quicksort, não
necessita de nenhuma memória adicional.

• Executa sempre em tempo proporcional a n log n,

• Aplicações que não podem tolerar eventuais variações no tempo


esperado de execução devem usar o Heapsort.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.5 72

Comparação entre os Métodos


Considerações finais:
• Para registros muito grandes é desejável que o método de ordenação
realize apenas n movimentos dos registros.
• Com o uso de uma ordenação indireta é possível se conseguir isso.
• Suponha que o arquivo A contenha os seguintes registros:
A[1], A[2], . . . , A[n].
• Seja P um arranjo P [1], P [2], . . . , P [n] de apontadores.
• Os registros somente são acessados para fins de comparações e toda
movimentação é realizada sobre os apontadores.
• Ao final, P [1] contém o índice do menor elemento de A, P [2] o índice
do segundo menor e assim sucessivamente.
• Essa estratégia pode ser utilizada para qualquer dos métodos de
ordenação interna.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 73

Ordenação Parcial

• Consiste em obter os k primeiros elementos de um arranjo ordenado


com n elementos.

• Quando k = 1, o problema se reduz a encontrar o mínimo (ou o


máximo) de um conjunto de elementos.

• Quando k = n caímos no problema clássico de ordenação.


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 74

Ordenação Parcial
Aplicações:

• Facilitar a busca de informação na Web com as máquinas de busca:


– É comum uma consulta na Web retornar centenas de milhares de
documentos relacionados com a consulta.
– O usuário está interessado apenas nos k documentos mais
relevantes.
– Em geral k é menor do que 200 documentos.
– Normalmente são consultados apenas os dez primeiros.
– Assim, são necessários algoritmos eficientes de ordenação parcial.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 75

Ordenação Parcial
Algoritmos considerados:

• Seleção parcial.

• Inserção parcial.

• Heapsort parcial.

• Quicksort parcial.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 76

Seleção Parcial

• Um dos algoritmos mais simples.

• Princípio de funcionamento:
– Selecione o menor item do vetor.
– Troque-o com o item que está na primeira posição do vetor.
– Repita estas duas operações com os itens n − 1, n − 2 . . . n − k.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 77

Seleção Parcial

void SelecaoParcial(TipoVetor A,
Análise:
TipoIndice n, TipoIndice k)
{ TipoIndice i , j , Min ; TipoItem x ; • Comparações entre cha-
for ( i = 1; i <= k ; i ++) ves e movimentações de
{ Min = i ; registros:
for ( j = i + 1; j <= n ; j ++)
k2 k
i f (A[ j ] .Chave < A[Min ] .Chave) Min = j ; C(n) = kn − 2
− 2
x = A[Min ] ; A[Min] = A[ i ] ; A[ i ] = x ;
M (n) = 3k
}
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 78

Seleção Parcial

• É muito simples de ser obtido a partir da implementação do algoritmo


de ordenação por seleção.

• Possui um comportamento espetacular quanto ao número de


movimentos de registros:
– Tempo de execução é linear no tamanho de k.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 79

Inserção Parcial

• Pode ser obtido a partir do algoritmo de ordenação por Inserção por


meio de uma modificação simples:
– Tendo sido ordenados os primeiros k itens, o item da k-ésima
posição funciona como um pivô.
– Quando um item entre os restantes é menor do que o pivô, ele é
inserido na posição correta entre os k itens de acordo com o
algoritmo original.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 80

Inserção Parcial

void InsercaoParcial (TipoVetor A, TipoIndice n,


TipoIndice k) • A modificação realizada
{ /∗−−Nao preserva o restante do vetor −−∗/ verifica o momento em
TipoIndice i , j ; TipoItem x ; que i se torna maior do
for ( i = 2; i <= n ; i ++) que k e então passa a
{ x = A[ i ] ; considerar o valor de j
i f ( i > k ) j = k ; else j = i − 1; igual a k a partir deste
A[0] = x ; /∗ sentinela ∗/ ponto.
while ( x .Chave < A[ j ] .Chave)
{ A[ j +1] = A[ j ] ;
j −−;
}
A[ j +1] = x ;
}
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 81

Inserção Parcial: Preserva Restante do Vetor

void InsercaoParcial2 (TipoVetor A, TipoIndice n, TipoIndice k)


{ /∗−− Preserva o restante do vetor −−∗/
TipoIndice i , j ; TipoItem x ;
for ( i = 2; i <= n ; i ++)
{ x = A[ i ] ;
i f ( i > k)
{ j = k ; i f ( x .Chave < A[ k ] .Chave) A[ i ] = A[ k ] ; }
else j = i − 1;
A[0] = x ; /∗ sentinela ∗/
while ( x .Chave < A[ j ] .Chave)
{ i f ( j < k ) {A[ j +1] = A[ j ] ; }
j −−;
}
i f ( j < k ) A[ j +1] = x ;
}
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 82

Inserção Parcial: Análise


• No anel mais interno, na i-ésima iteração o valor de Ci é:

Melhor caso : Ci (n) = 1


Pior caso : Ci (n) = i
Caso médio : Ci (n) = 1i (1 + 2 + · · · + i) = i+1
2

• Assumindo que todas as permutações de n são igualmente prováveis,


o número de comparações é:

Melhor caso : C(n) = (1 + 1 + · · · + 1) = n − 1


Pior caso : C(n) = (2 + 3 + · · · + k + (k + 1)(n − k))
k2 k
= kn + n − 2 − 2 −1
Caso médio : C(n) = 12 (3 + 4 + · · · + k + 1 + (k + 1)(n − k))
kn n k2 k
= 2 + 2 − 4 + 4 −1
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 83

Inserção Parcial: Análise


• O número de movimentações na i-ésima iteração é:
Mi (n) = Ci (n) − 1 + 3 = Ci (n) + 2

• Logo, o número de movimentos é:

Melhor caso : M (n) = (3 + 3 + · · · + 3) = 3(n − 1)


Pior caso : M (n) = (4 + 5 + · · · + k + 2 + (k + 1)(n − k))
k2 3k
= kn + n − 2 + 2 −3
Caso médio : M (n) = 21 (5 + 6 + · · · + k + 3 + (k + 1)(n − k))
kn n k2 5k
= 2 + 2 − 4 + 4 −2

• O número mínimo de comparações e movimentos ocorre quando os


itens estão originalmente em ordem.
• O número máximo ocorre quando os itens estão originalmente na
ordem reversa.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 84

Heapsort Parcial

• Utiliza um tipo abstrato de dados heap para informar o menor item do


conjunto.

• Na primeira iteração, o menor item que está em a[1] (raiz do heap) é


trocado com o item que está em A[n].

• Em seguida o heap é refeito.

• Novamente, o menor está em A[1], troque-o com A[n-1].

• Repita as duas últimas operações até que o k-ésimo menor seja


trocado com A[n − k].

• Ao final, os k menores estão nas k últimas posições do vetor A.


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 85

Heapsort Parcial

/∗−− Entram aqui os procedimentos Refaz e Constroi das transparencias 50 e 51−−∗/


/∗−− Coloca menor em A[n] , segundo menor em A[n− 1 ] , . . . , −−∗/
/∗−− k−ésimo em A[n−k ] −−∗/
void HeapsortParcial(TipoItem ∗A, TipoIndice n, TipoIndice k)
{ TipoIndice Esq = 1; TipoIndice Dir ;
TipoItem x ; long Aux = 0;
Constroi(A, n ) ; /∗ constroi o heap ∗/
Dir = n;
while (Aux < k)
{ /∗ ordena o vetor ∗/
x = A[ 1 ] ;
A[1] = A[n − Aux] ;
A[n − Aux] = x ;
Dir−−; Aux++;
Refaz(Esq, Dir , A) ;
}
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 86

Heapsort Parcial: Análise:

• O HeapsortParcial deve construir um heap a um custo O(n).

• O procedimento Refaz tem custo O(log n).

• O procedimento HeapsortParcial chama o procedimento Refaz k


vezes.

• Logo, o algoritmo apresenta a complexidade:



 O(n) n

se k ≤ log n
O(n + k log n) =
 O(k log n) n

se k > log n
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 87

Quicksort Parcial

• Assim como o Quicksort, o Quicksort Parcial é o algoritmo de


ordenação parcial mais rápido em várias situações.

• A alteração no algoritmo para que ele ordene apenas os k primeiros


itens dentre n itens é muito simples.

• Basta abandonar a partição à direita toda vez que a partição à


esquerda contiver k ou mais itens.

• Assim, a única alteração necessária no Quicksort é evitar a chamada


recursiva Ordena(i,Dir).
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 88

Quicksort Parcial
1 2 3 4 5 6
Chaves iniciais: O R D E N A
1 A D R E N O
2 A D
3 E R N O
4 N R O
5 O R
A D E N O R

• Considere k = 3 e D o pivô para gerar as linhas 2 e 3.


• A partição à esquerda contém dois itens e a partição à direita, quatro itens.
• A partição à esquerda contém menos do que k itens.
• Logo, a partição direita não pode ser abandonada.
• Considere E o pivô na linha 3.
• A partição à esquerda contém três itens e a partição à direita também.
• Assim, a partição à direita pode ser abandonada.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 89

Quicksort Parcial

void Ordena(TipoVetor A, TipoIndice Esq, TipoIndice Dir , TipoIndice k)


{ TipoIndice i , j ;
Particao (A, Esq, Dir , & i , & j ) ;
i f ( j − Esq >= k − 1) { i f (Esq < j ) Ordena(A, Esq, j , k ) ; return ; }
i f (Esq < j ) Ordena(A, Esq, j , k ) ;
i f ( i < Dir ) Ordena(A, i , Dir , k ) ;
}

void QuickSortParcial (TipoVetor A, TipoIndice n, TipoIndice k)


{ Ordena(A, 1 , n, k ) ; }
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 90

Quicksort Parcial: Análise:

• A análise do Quicksort é difícil.

• O comportamento é muito sensível à escolha do pivô.

• Podendo cair no melhor caso O(k log k).

• Ou em algum valor entre o melhor caso e O(n log n).


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 91

Comparação entre os Métodos de Ordenação Parcial (1)


n, k Seleção Quicksort Inserção Inserção2 Heapsort
n : 101 k : 100 1 2,5 1 1,2 1,7
n : 101 k : 101 1,2 2,8 1 1,1 2,8
n : 102 k : 100 1 3 1,1 1,4 4,5
n : 102 k : 101 1,9 2,4 1 1,2 3
n : 102 k : 102 3 1,7 1 1,1 2,3
n : 103 k : 100 1 3,7 1,4 1,6 9,1
n : 103 k : 101 4,6 2,9 1 1,2 6,4
n : 103 k : 102 11,2 1,3 1 1,4 1,9
n : 103 k : 103 15,1 1 3,9 4,2 1,6
n : 105 k : 100 1 2,4 1,1 1,1 5,3
n : 105 k : 101 5,9 2,2 1 1 4,9
n : 105 k : 102 67 2,1 1 1,1 4,8
n : 105 k : 103 304 1 1,1 1,3 2,3
n : 105 k : 104 1445 1 33,1 43,3 1,7
n : 105 k : 105 ∞ 1 ∞ ∞ 1,9
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 92

Comparação entre os Métodos de Ordenação Parcial (2)


n, k Seleção Quicksort Inserção Inserção2 Heapsort
n : 106 k : 100 1 3,9 1,2 1,3 8,1
n : 106 k : 101 6,6 2,7 1 1 7,3
n : 106 k : 102 83,1 3,2 1 1,1 6,6
n : 106 k : 103 690 2,2 1 1,1 5,7
n : 106 k : 104 ∞ 1 5 6,4 1,9
n : 106 k : 105 ∞ 1 ∞ ∞ 1,7
n : 106 k : 106 ∞ 1 ∞ ∞ 1,8
n : 107 k : 100 1 3,4 1,1 1,1 7,4
n : 107 k : 101 8,6 2,6 1 1,1 6,7
n : 107 k : 102 82,1 2,6 1 1,1 6,8
n : 107 k : 103 ∞ 3,1 1 1,1 6,6
n : 107 k : 104 ∞ 1,1 1 1,2 2,6
n : 107 k : 105 ∞ 1 ∞ ∞ 2,2
n : 107 k : 106 ∞ 1 ∞ ∞ 1,2
n : 107 k : 107 ∞ 1 ∞ ∞ 1,7
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.6 93

Comparação entre os Métodos de Ordenação Parcial

1. Para valores de k até 1.000, o método da InserçãoParcial é imbatível.

2. O QuicksortParcial nunca ficar muito longe da InserçãoParcial.

3. Na medida em que o k cresce,o QuicksortParcial é a melhor opção.

4. Para valores grandes de k, o método da InserçãoParcial se torna ruim.

5. Um método indicado para qualquer situação é o QuicksortParcial.

6. O HeapsortParcial tem comportamento parecido com o do


QuicksortParcial.

7. No entano, o HeapsortParcial é mais lento.


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 94

Ordenação em Tempo Linear

• Nos algoritmos apresentados a seguir não existe comparação entre


chaves.

• Eles têm complexidade de tempo linear na prática.

• Necessitam manter uma cópia em memória dos itens a serem


ordenados e uma área temporária de trabalho.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 95

Ordenação por Contagem

• Este método assume que cada item do vetor A é um número inteiro


entre 0 e k.

• O algoritmo conta, para cada item x, o número de itens antes de x.

• A seguir, cada item é colocado no vetor de saída na sua posição


definitiva.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 96

Ordenação por Contagem


1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
A 1 4 0 3 0 1 4 1 B 1
0 1 2 3 4 0 1 2 3 4 0 1 2 3 4
C 2 3 0 1 2 2 5 5 6 8 C 2 4 5 6 8
(a) (b) (c)
1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
B 1 4 B 1 1 4 B 0 0 1 1 1 3 4 4
0 1 2 3 4 0 1 2 3 4
C 2 4 5 6 7 C 2 3 5 6 7
(d) (e) (f)

• A contém oito chaves de inteiros entre 0 e 4. Cada etapa mostra:


– (a) o vetor de entrada A e o vetor auxiliar C contendo o número de
itens iguais a i, 0 ≤ i ≤ 4;
– (b) o vetor C contendo o número de itens ≤ i, 0 ≤ i ≤ 4;
– (c), (d), (e) os vetores auxiliares B e C após uma, duas e três
iterações, considerando os itens em A da direita para a esquerda;
– (f) o vetor auxiliar B ordenado.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 97

Ordenação por Contagem

void Contagem(TipoItem ∗A, TipoIndice n, int k)


{ int i ;
for ( i = 0; i <= k ; i ++) C[ i ] = 0;
for ( i = 1; i <= n ; i ++) C[A[ i ] .Chave] = C[A[ i ] .Chave] + 1;
for ( i = 1; i <= k ; i ++) C[ i ] = C[ i ] + C[ i −1];
for ( i = n ; i > 0; i−−)
{ B[C[A[ i ] .Chave] ] = A[ i ] ;
C[A[ i ] .Chave] = C[A[ i ] .Chave] − 1;
}
for ( i = 1; i <= n ; i ++)
A[ i ] = B[ i ] ;
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 98

Ordenação por Contagem

• Os arranjos auxiliares B e C devem ser declarados fora do


procedimento Contagem para evitar que sejam criados a cada
chamada do procedimento.

• No quarto for, como podem haver itens iguais no vetor A, então o valor
de C[A[j]] é decrementado de 1 toda vez que um item A[j] é colocado
no vetor B. Isso garante que o próximo item com valor igual a A[j], se
existir, vai ser colocado na posição imediatamente antes de A[j] no
vetor B.

• O último for copia para A o vetor B ordenado. Essa cópia pode ser
evitada colocando o vetor B como parâmetro de retorno no
procedimento Contagem, como mostrado no Exercício 4.24.

• A ordenação por contagem é um método estável.


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 99

Ordenação por Contagem: Análise

• O primeiro for tem custo O(k).

• O segundo for tem custo O(n).

• O terceiro for tem custo O(k).

• O quarto for tem custo O(n + k).

• Na prática o algoritmo deve ser usado quando k = O(n), o que leva o


algoritmo a ter custo O(n).

• De outra maneira, as complexidades de espaço e de tempo ficam


proibitivas. Na seção seguinte vamos apresentar um algoritmo prático
e eficiente para qualquer valor de k.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 100

Radixsort para Inteiros

• Utiliza o princípio da distribuição das antigas classificadoras de


cartões perfurados.

• Os cartões eram organizados em 80 colunas e cada coluna permitia


uma perfuração em 1 de 12 lugares.

• Para números inteiros positivos, apenas 10 posições da coluna eram


usadas para os valores entre 0 e 9.

• A classificadora examinava uma coluna de cada cartão e distribuia


mecanicamente o cartão em um dos 12 escaninhos, dependendo do
lugar onde fora perfurado.

• Um operador então recolhia os 12 conjuntos de cartões na ordem


desejada, ascendente ou descendente.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 101

Radixsort para Inteiros

• Radixsort considera o dígito menos significativo primeiro e ordena os


itens para aquele dígito.

• Depois repete o processo para o segundo dígito menos significativo, e


assim sucessivamente.

07 01 01
33 22 07
18 ⇒ 33 ⇒ 07
22 07 18
01 07 22
07 18 33
↑ ↑
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 102

Radixsort para Inteiros

Primeiro refinamento:

#define BASE 256


#define M 8
#define NBITS 32
RadixsortInt (TipoItem ∗A, TipoIndice n)
{ for ( i = 0; i < NBITS / M; i ++)
Ordena A sobre o dígito i menos significativo usando um algoritmo estável ;
}

• O programa recebe o vetor A e o tamanho n do vetor.

• O número de bits da chave (NBits) e o número de bits a considerar em


cada passada (m) determinam o número de passadas, que é igual a
NBits div m.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 103

Radixsort para Inteiros

• O algoritmo de ordenação por contagem é uma excelente opção para


ordenar o vetor A sobre o dígito i por ser estável e de custo O(n).

• O vetor auxiliar C ocupa um espaço constante que depende apenas


da base utilizada.
– Por exemplo, para a base 10, o vetor C armazena valores de k
entre 0 e 9, isto é, 10 posições.

• A implementação a seguir utiliza Base = 256 e o vetor C armazena


valores de k entre 0 e 255 para representar os caracteres ASCII.

• Nesse caso podemos ordenar inteiros de 32 bits (4 bytes com valores


entre 0 e 232 ) em apenas d = 4 chamadas do algoritmo de ordenação
por contagem.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 104

Radixsort para Inteiros

• O algoritmo de ordenação por contagem precisa ser alterado para


ordenar sobre m bits de cada chave do vetor A.

• A função GetBits extrai um conjunto contíguo de m bits do número


inteiro.

• Em linguagem de máquina, os bits são extraídos de números binários


usando operações and, shl (shift left), shr (shift right), e not
(complementa todos os bits).

• Por exemplo, os 2 bits menos significativos de um número x de 10 bits


são extraídos movendo os bits para a direita com x shr 2 e uma
operação and com a máscara 0000000011.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 105

Ordenação por Contagem Alterado


#define GetBits(x, k, j ) ( x >> k) & ~((~0) < < j )
void ContagemInt(TipoItem ∗A, TipoIndice n, int Pass)
{ int i , j ;
for ( i = 0; i <= BASE − 1; i ++) C[ i ] = 0;
for ( i = 1; i <= n ; i ++)
{ j = GetBits(A[ i ] .Chave, Pass ∗ M, M) ;
C[ j ] = C[ j ] + 1;
}
i f (C[0] == n) return ;
for ( i = 1; i <= BASE − 1; i ++) C[ i ] = C[ i ] + C[ i −1];
for ( i = n ; i > 0; i−−)
{ j = GetBits(A[ i ] .Chave, Pass ∗ M, M) ;
B[C[ j ] ] = A[ i ] ;
C[ j ] = C[ j ] − 1;
}
for ( i = 1; i <= n ; i ++) A[ i ] = B[ i ] ;
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 106

Radixsort para Inteiros

• No Programa, quando qualquer posição i do vetor C contém um valor


igual a n significa que todos os n números do vetor de entrada A são
iguais a i.

• Isso é verificado no comando if logo após o segundo for para C[0].


Nesse caso todos os valores de A são iguais a zero no byte
considerado como chave de ordenação e o restante do anel não
precisa ser executado.

• Essa situação ocorre com frequência nos bytes mais significativos de


um número inteiro.

• Por exemplo, para ordenar números de 32 bits que tenham valores


entre 0 e 255, os três bytes mais significativos são iguais a zero.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 107

Radixsort para Inteiros

void RadixsortInt (TipoItem ∗A, TipoIndice n)


{ int i ;
for ( i = 0; i < NBITS / M; i ++) ContagemInt(A, n, i ) ;
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 108

Radixsort para Inteiros: Análise


• Cada passada sobre n inteiros em ContagemInt custa O(n + Base).
• Como são necessárias d passadas, o custo total é O(dn + dBase).
• Radixsort tem custo O(n) quando d é constante e Base = O(n).
• Se cada número cabe em uma palavra de computador, então ele pode
ser tratado como um número de d dígitos na notação base n.
• Para A contendo 1 bilhão de números de 32 bits (4 dígitos na base
28 = 256), apenas 4 chamadas de Contagem são necessárias.
• Se considerarmos um algoritmo que utiliza o princípio da comparação
de chaves, como o Quicksort, então são necessárias ≈ log n = 30
operações por número (considerando que uma palavra de computador
ocupa O(log n) bits).
• Isso significa que o Radixsort é mais rápido para ordenar inteiros.
• O aspecto negativo é o espaço adicional para B e C.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 109

Radixsort para Cadeias de Caracteres


• O algoritmo de ordenação por contagem precisa ser alterado para
ordenar sobre o caractere k da chave de cada item x do vetor A.

void ContagemCar(TipoItem ∗A, TipoIndice n, int k)


{ int i , j ;
for ( i = 0; i <= BASE − 1; i ++) C[ i ] = 0;
for ( i = 1; i <= n ; i ++)
{ j = ( int ) A[ i ] .Chave[ k ] ; C[ j ] = C[ j ] + 1;
}
i f (C[0] == n) return ;
for ( i = 1; i <= BASE − 1; i ++) C[ i ] = C[ i ] + C[ i −1];
for ( i = n ; i > 0; i−−)
{ j = ( int ) A[ i ] .Chave[ k ] ;
B[C[ j ] ] = A[ i ] ; C[ j ] = C[ j ] − 1;
}
for ( i = 1; i <= n ; i ++) A[ i ] = B[ i ] ;
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.1.7 110

Radixsort para Cadeias de Caracteres

void RadixsortCar(TipoItem ∗A, TipoIndice n)


{ int i ;
for ( i = TAMCHAVE − 1; i >= 0; i −−) ContagemCar (A, n, i ) ;
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2 111

Ordenação Externa
• A ordenação externa consiste em ordenar arquivos de tamanho maior
que a memória interna disponível.
• Os métodos de ordenação externa são muito diferentes dos de
ordenação interna.
• Na ordenação externa os algoritmos devem diminuir o número de
acesso as unidades de memória externa.
• Nas memórias externas, os dados ficam em um arquivo seqüencial.
• Apenas um registro pode ser acessado em um dado momento. Essa é
uma restrição forte se comparada com as possibilidades de acesso
em um vetor.
• Logo, os métodos de ordenação interna são inadequados para
ordenação externa.
• Técnicas de ordenação diferentes devem ser utilizadas.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2 112

Ordenação Externa
Fatores que determinam as diferenças das técnicas de ordenação externa:

1. Custo para acessar um item é algumas ordens de grandeza maior.

2. O custo principal na ordenação externa é relacionado a transferência


de dados entre a memória interna e externa.

3. Existem restrições severas de acesso aos dados.

4. O desenvolvimento de métodos de ordenação externa é muito


dependente do estado atual da tecnologia.

5. A variedade de tipos de unidades de memória externa torna os


métodos dependentes de vários parâmetros.

6. Assim, apenas métodos gerais serão apresentados.


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2 113

Ordenação Externa

• O método mais importante é o de ordenação por intercalação.


• Intercalar significa combinar dois ou mais blocos ordenados em um
único bloco ordenado.
• A intercalação é utilizada como uma operação auxiliar na ordenação.
• Estratégia geral dos métodos de ordenação externa:
1. Quebre o arquivo em blocos do tamanho da memória interna
disponível.
2. Ordene cada bloco na memória interna.
3. Intercale os blocos ordenados, fazendo várias passadas sobre o
arquivo.
4. A cada passada são criados blocos ordenados cada vez maiores,
até que todo o arquivo esteja ordenado.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2 114

Ordenação Externa

• Os algoritmos para ordenação externa devem reduzir o número de


passadas sobre o arquivo.

• Uma boa medida de complexidade de um algoritmo de ordenação por


intercalação é o número de vezes que um item é lido ou escrito na
memória auxiliar.

• Os bons métodos de ordenação geralmente envolvem no total menos


do que dez passadas sobre o arquivo.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.1 115

Intercalação Balanceada de Vários Caminhos

• Considere um arquivo armazenado em uma fita de entrada:


INTERCALACAOBALANCEADA

• Objetivo:
– Ordenar os 22 registros e colocá-los em uma fita de saída.

• Os registros são lidos um após o outro.

• Considere uma memória interna com capacidade para para três


registros.

• Considere que esteja disponível seis unidades de fita magnética.


Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.1 116

Intercalação Balanceada de Vários Caminhos

• Fase de criação dos blocos ordenados:

fita 1: INT ACO ADE


fita 2: CER ABL A
fita 3: AAL ACN
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.1 117

Intercalação Balanceada de Vários Caminhos

• Fase de intercalação - Primeira passada:


1. O primeiro registro de cada fita é lido.
2. Retire o registro contendo a menor chave.
3. Armazene-o em uma fita de saída.
4. Leia um novo registro da fita de onde o registro retirado é
proveniente.
5. Ao ler o terceiro registro de um dos blocos, sua fita fica inativa.
6. A fita é reativada quando o terceiro registro das outras fitas forem
lidos.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.1 118

Intercalação Balanceada de Vários Caminhos

• Fase de intercalação - Primeira passada:


7. Neste instante um bloco de nove registros ordenados foi formado
na fita de saída.
8. Repita o processo para os blocos restantes.

• Resultado da primeira passada da segunda etapa:

fita 4: AACEILNRT
fita 5: AAABCCLNO
fita 6: AADE
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.1 119

Intercalação Balanceada de Vários Caminhos

• Quantas passadas são necessárias para ordenar um arquivo de


tamanho arbitrário?
– Seja n, o número de registros do arquivo.
– Suponha que cada registro ocupa m palavras na memória interna.
– A primeira etapa produz n/m blocos ordenados.
– Seja P (n) o número de passadas para a fase de intercalação.
– Seja f o número de fitas utilizadas em cada passada.
– Assim:
n
P (n) = logf
.
m
No exemplo acima, n=22, m=3 e f=3 temos:
22
P (n) = log3 = 2.
3
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.1 120

Intercalação Balanceada de Vários Caminhos

• No exemplo foram utilizadas 2f fitas para uma


intercalação-de-f -caminhos.

• É possível usar apenas f + 1 fitas:


– Encaminhe todos os blocos para uma única fita.
– Redistribuia estes blocos entre as fitas de onde eles foram lidos.
– O custo envolvido é uma passada a mais em cada intercalação.

• No caso do exemplo de 22 registros, apenas quatro fitas seriam


suficientes:
– A intercalação dos blocos a partir das fitas 1, 2 e 3 seria toda
dirigida para a fita 4.
– Ao final, o segundo e o terceiro blocos ordenados de nove registros
seriam transferidos de volta para as fitas 1 e 2.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.2 121

Implementação por meio de Seleção por Substituição


• A implementação do método de intercalação balanceada pode ser
realizada utilizando filas de prioridades.
• As duas fases do método podem ser implementadas de forma
eficiente e elegante.
• Operações básicas para formar blocos ordenados:
– Obter o menor dentre os registros presentes na memória interna.
– Substituí-lo pelo próximo registro da fita de entrada.
• Estrutura ideal para implementar as operações: heap.
• Operação de substituição:
– Retirar o menor item da fila de prioridades.
– Colocar um novo item no seu lugar.
– Reconstituir a propriedade do heap.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.2 122

Implementação por meio de Seleção por Substituição


Algoritmo:

1. Inserir m elementos do arquivo na fila de prioridades.

2. Substituir o menor item da fila de prioridades pelo próximo item do


arquivo.

3. Se o próximo item é menor do que o que saiu, então:


• Considere-o membro do próximo bloco.
• Trate-o como sendo maior do que todos os itens do bloco corrente.

4. Se um item marcado vai para o topo da fila de prioridades então:


• O bloco corrente é encerrado.
• Um novo bloco ordenado é iniciado.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.2 123

Implementação por meio de Seleção por Substituição

Entra 1 2 3
Entra 1 2 3
E I N T
L C O A*
R N E* T
A L O A*
C R E* T
N O A* A*
A T E* C*
C A* N* A*
L A* E* C*
E A* N* C*
A C* E* L*
A C* N* E*
C E* A L*
D E* N* A
A L* A C
A N* D A
O A A C
A D A
B A O C
A D
A B O C
D

• Primeira passada sobre o arquivo exemplo.


• Os asteriscos indicam quais chaves pertencem a blocos diferentes.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.2 124

Implementação por meio de Seleção por Substituição

• Fase de intercalação dos blocos ordenados obtidos na primeira fase:


– Operação básica: obter o menor item dentre os ainda não retirados
dos f blocos a serem intercalados.

Algoritmo:

• Monte uma fila de prioridades de tamanho f .

• A partir de cada uma das f entradas:


– Substitua o item no topo da fila de prioridades pelo próximo item do
mesmo bloco do item que está sendo substituído.
– Imprima em outra fita o elemento substituído.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.2 125

Implementação por meio de Seleção por Substituição


• Exemplo:

Entra 1 2 3 • Para f pequeno não é vantajoso


A A C I utilizar seleção por substituição
L A C I para intercalar blocos:
E C L I
– Obtém-se o menor item fa-
R E L I
zendo f − 1 comparações.
N I L R
L N R
• Quando f é 8 ou mais, o método
T N R
é adequado:
R T – Obtém-se o menor item fa-
T zendo log2 f comparações.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.3 126

Considerações Práticas

• As operações de entrada e saída de dados devem ser implementadas


eficientemente.

• Deve-se procurar realizar a leitura, a escrita e o processamento


interno dos dados de forma simultânea.

• Os computadores de maior porte possuem uma ou mais unidades


independentes para processamento de entrada e saída.

• Assim, pode-se realizar processamento e operações de E/S


simultaneamente.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.3 127

Considerações Práticas

• Técnica para obter superposição de E/S e processamento interno:


– Utilize 2f áreas de entrada e 2f de saída.
– Para cada unidade de entrada ou saída, utiliza-se duas áreas de
armazenamento:
1. Uma para uso do processador central
2. Outra para uso do processador de entrada ou saída.
– Para entrada, o processador central usa uma das duas áreas
enquanto a unidade de entrada está preenchendo a outra área.
– Depois a utilização das áreas é invertida entre o processador de
entrada e o processador central.
– Para saída, a mesma técnica é utilizada.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.3 128

Considerações Práticas

• Problemas com a técnica:


– Apenas metade da memória disponível é utilizada.
– Isso pode levar a uma ineficiência se o número de áreas for grande.
Ex: Intercalação-de-f -caminhos para f grande.
– Todas as f áreas de entrada em uma intercalação-de-f -caminhos
se esvaziando aproximadamente ao mesmo tempo.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.3 129

Considerações Práticas

• Solução para os problemas:


– Técnica de previsão:
∗ Requer a utilização de uma única área extra de armazenamento
durante a intercalação.
∗ Superpõe a entrada da próxima área que precisa ser preenchida
com a parte de processamento interno do algoritmo.
∗ É fácil saber qual área ficará vazia primeiro.
∗ Basta olhar para o último registro de cada área.
∗ A área cujo último registro é o menor, será a primeira a se
esvaziar.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.3 130

Considerações Práticas

• Escolha da ordem de intercalação f :


– Para fitas magnéticas:
∗ f deve ser igual ao número de unidades de fita disponíveis
menos um.
∗ A fase de intercalação usa f fitas de entrada e uma fita de saída.
∗ O número de fitas de entrada deve ser no mínimo dois.
– Para discos magnéticos:
∗ O mesmo raciocínio acima é válido.
∗ O acesso seqüencial é mais eficiente.
– Sedegwick (1988) sugere considerar f grande o suficiente para
completar a ordenação em poucos passos.
– Porém, a melhor escolha para f depende de vários parâmetros
relacionados com o sistema de computação disponível.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.4 131

Intercalação Polifásica

• Problema com a intercalação balanceada de vários caminhos:


– Necessita de um grande número de fitas.
– Faz várias leituras e escritas entre as fitas envolvidas.
– Para uma intercalação balanceada de f caminhos são necessárias
2f fitas.
– Alternativamente, pode-se copiar o arquivo quase todo de uma
única fita de saída para f fitas de entrada.
– Isso reduz o número de fitas para f + 1.
– Porém, há um custo de uma cópia adicional do arquivo.

• Solução:
– Intercalação polifásica.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.4 132

Intercalação Polifásica

• Os blocos ordenados são distribuídos de forma desigual entre as fitas


disponíveis.

• Uma fita é deixada livre.

• Em seguida, a intercalação de blocos ordenados é executada até que


uma das fitas esvazie.

• Neste ponto, uma das fitas de saída troca de papel com a fita de
entrada.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.4 133

Intercalação Polifásica

• Exemplo:
– Blocos ordenados obtidos por meio de seleção por substituição:

fita 1: INRT ACEL AABCLO


fita 2: AACEN AAD
fita 3:

– Configuração após uma intercalação-de-2-caminhos das fitas 1 e 2


para a fita 3:

fita 1: AABCLO
fita 2:
fita 3: AACEINNRT AAACDEL
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.4 134

Intercalação Polifásica
• Exemplo:
– Depois da intercalação-de-2-caminhos das fitas 1 e 3 para a fita 2:
fita 1:
fita 2: AAAABCCEILNNORT
fita 3: AAACDEL

– Finalmente:
fita 1: AAAAAAABCCCDEEILLNNORT
fita 2:
fita 3:

– A intercalação é realizada em muitas fases.


– As fases não envolvem todos os blocos.
– Nenhuma cópia direta entre fitas é realizada.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.4 135

Intercalação Polifásica

• A implementação da intercalação polifásica é simples.

• A parte mais delicada está na distribuição inicial dos blocos ordenados


entre as fitas.

• Distribuição dos blocos nas diversas etapas do exemplo:

fita 1 fita 2 fita 3 Total


3 2 0 5
1 0 2 3
0 1 1 2
1 0 0 1
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.4 136

Intercalação Polifásica
Análise:

• A análise da intercalação polifásica é complicada.

• O que se sabe é que ela é ligeiramente melhor do que a intercalação


balanceada para valores pequenos de f .

• Para valores de f > 8, a intercalação balanceada pode ser mais


rápida.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.5 137

Quicksort Externo

• Foi proposto por Monard em 1980.

• Utiliza o paradigma de divisão e conquista.

• O algoritmo ordena in situ um arquivo A = {R1 , . . . , Rn } de n registros.

• Os registros estão armazenados consecutivamente em memória


secundária de acesso randômico.

• O algoritmo utiliza somente O(log n) unidades de memória interna e


não é necessária nenhuma memória externa adicional.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.5 138

Quicksort Externo

• Seja Ri , 1 ≤ i ≤ n, o registro que se encontra na i-ésima posição de A.

• Algoritmo:
1. Particionar A da seguinte forma:
{R1 , . . . , Ri } ≤ Ri+1 ≤ Ri+2 ≤ . . . ≤ Rj−2 ≤ Rj−1 ≤ {Rj , . . . , Rn },
2. chamar recursivamente o algoritmo em cada um dos subarquivos
A1 = {R1 , . . . , Ri } e A2 = {Rj , . . . , Rn }.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.5 139

Quicksort Externo

• Para o partionamento é utilizanda uma área de armazenamento na


memória interna.

• Tamanho da área: TamArea = j − i − 1, com TamArea ≥ 3.

• Nas chamadas recusivas deve-se considerar que:


– Primeiro deve ser ordenado o subarquivo de menor tamanho.
– Condição para que, na média, O(log n) subarquivos tenham o
processamento adiado.
– Subarquivos vazios ou com um único registro são ignorados.
– Caso o arquivo de entrada A possua no máximo TamArea registros,
ele é ordenado em um único passo.
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.5 140

Quicksort Externo
i Li Ls j i Li Ls j
área Linf Lsup área Linf Lsup
a) 5 3 10 6 1 7 4 b) 5 3 10 6 1 7 4 4

Ei Es Ei Es
i Li Ls j i Li Ls j

c) 5 3 10 6 1 7 4 4 5 d) 5 3 10 6 1 7 4 4 5 7

Ei Es Ei Es
i Li Ls j i Li Ls j

e) 5 3 10 6 1 7 7 4 5 7 f) 5 3 10 6 1 7 7 3 4 5 7

Ei Es Ei Es
i Li Ls j i Li Ls j

g) 3 3 10 6 1 7 7 4 5 3 7 h) 3 3 10 6 1 7 7 4 5 3 7

Ei Es Ei Es
Li
i Li Ls j i Ls j

i) 3 1 10 6 1 7 7 4 5 3 7 j) 3 1 10 6 1 7 7 4 5 3 7

Ei Es Ei Es
Li
i Ls j i Ls Li j

k) 3 1 10 6 1 10 7 4 5 3 7 l) 3 1 10 6 1 10 7 4 5 6 3 7

Ei Es Ei Es
i Ls Li j i Ls Li j

m) 3 1 10 6 6 10 7 4 5 3 7 n) 3 1 4 5 6 10 7 3 7

Ei Es Es Ei
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.5 141

Quicksort Externo
void QuicksortExterno(FILE ∗∗ArqLi , FILE ∗∗ArqEi , FILE ∗∗ArqLEs,
int Esq, int Dir )
{ int i , j ;
TipoArea Area ; /∗ Area de armazenamento interna ∗/
i f ( Dir − Esq < 1) return ;
FAVazia(&Area) ;
Particao (ArqLi , ArqEi , ArqLEs, Area, Esq, Dir , & i , & j ) ;
i f ( i − Esq < Dir − j )
{ /∗ ordene primeiro o subarquivo menor ∗/
QuicksortExterno(ArqLi , ArqEi , ArqLEs, Esq, i ) ;
QuicksortExterno(ArqLi , ArqEi , ArqLEs, j , Dir ) ;
}
else
{ QuicksortExterno(ArqLi , ArqEi , ArqLEs, j , Dir ) ;
QuicksortExterno(ArqLi , ArqEi , ArqLEs, Esq, i ) ;
}
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.5 142

Quicksort Externo: Procedimentos Auxiliares


void LeSup(FILE ∗∗ArqLEs, TipoRegistro ∗UltLido , int ∗Ls,short ∗OndeLer)
{ fseek(∗ArqLEs, ( ∗Ls − 1) ∗ sizeof(TipoRegistro ) ,SEEK_SET ) ;
fread ( UltLido , sizeof(TipoRegistro ) , 1 , ∗ArqLEs) ;
(∗Ls)−−; ∗OndeLer = FALSE ;
}

void LeInf (FILE ∗∗ArqLi , TipoRegistro ∗UltLido , int ∗ Li ,short ∗OndeLer)


{ fread ( UltLido , sizeof(TipoRegistro ) , 1 , ∗ ArqLi ) ;
(∗ Li )++; ∗OndeLer = TRUE ;
}

void InserirArea (TipoArea ∗Area, TipoRegistro ∗UltLido , int ∗NRArea)


{ /∗Insere UltLido de forma ordenada na Area∗/
InsereItem(∗UltLido , Area ) ; ∗NRArea = ObterNumCelOcupadas(Area) ;
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.5 143

Quicksort Externo: Procedimentos Auxiliares


void EscreveMax(FILE ∗∗ArqLEs, TipoRegistro R, int ∗Es)
{ fseek(∗ArqLEs, ( ∗Es − 1) ∗ sizeof(TipoRegistro ) ,SEEK_SET ) ;
fwrite(&R, sizeof(TipoRegistro ) , 1 , ∗ArqLEs) ; (∗Es)−−;
}

void EscreveMin(FILE ∗∗ArqEi , TipoRegistro R, int ∗Ei )


{ fwrite(&R, sizeof(TipoRegistro ) , 1 , ∗ArqEi ) ; (∗Ei )++; }

void RetiraMax(TipoArea ∗Area, TipoRegistro ∗R, int ∗NRArea)


{ RetiraUltimo (Area, R) ; ∗NRArea = ObterNumCelOcupadas(Area ) ; }

void RetiraMin(TipoArea ∗Area, TipoRegistro ∗R, int ∗NRArea)


{ RetiraPrimeiro (Area, R) ; ∗NRArea = ObterNumCelOcupadas(Area ) ; }
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.5 144

Quicksort Externo: Procedimento Particao


void Particao (FILE ∗∗ArqLi , FILE ∗∗ArqEi , FILE ∗∗ArqLEs,
TipoArea Area, int Esq, int Dir , int ∗ i , int ∗ j )
{ int Ls = Dir , Es = Dir , Li = Esq, Ei = Esq,
NRArea = 0 , Linf = INT_MIN , Lsup = INT_MAX ;
short OndeLer = TRUE ; TipoRegistro UltLido , R;
fseek (∗ArqLi , ( Li − 1)∗ sizeof(TipoRegistro ) , SEEK_SET ) ;
fseek (∗ArqEi , ( Ei − 1)∗ sizeof(TipoRegistro ) , SEEK_SET ) ;
∗ i = Esq − 1; ∗ j = Dir + 1;
while (Ls >= Li )
{ i f ( NRArea < TAMAREA − 1)
{ i f (OndeLer)
LeSup(ArqLEs, &UltLido , &Ls, &OndeLer) ;
else LeInf (ArqLi, &UltLido , & Li , &OndeLer) ;
InserirArea(&Area, &UltLido , &NRArea ) ;
continue;
}
i f (Ls == Es)
LeSup(ArqLEs, &UltLido , &Ls, &OndeLer) ;
else i f ( Li == Ei ) LeInf (ArqLi, &UltLido , & Li , &OndeLer) ;
else i f (OndeLer) LeSup(ArqLEs, &UltLido , &Ls, &OndeLer) ;
else LeInf (ArqLi, &UltLido , & Li , &OndeLer) ;
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.5 145

Quicksort Externo: Procedimento Particao


i f ( UltLido .Chave > Lsup)
{ ∗ j = Es; EscreveMax(ArqLEs, UltLido , &Es) ;
continue;
}
i f ( UltLido .Chave < Linf )
{ ∗ i = Ei ; EscreveMin(ArqEi , UltLido , &Ei ) ;
continue;
}
InserirArea(&Area, &UltLido , &NRArea ) ;
i f ( Ei − Esq < Dir − Es)
{ RetiraMin(&Area, &R, &NRArea ) ;
EscreveMin(ArqEi , R, &Ei ) ; Linf = R.Chave;
}
else { RetiraMax(&Area, &R, &NRArea ) ;
EscreveMax(ArqLEs, R, &Es) ; Lsup = R.Chave;
}
}
while ( Ei <= Es)
{ RetiraMin(&Area, &R, &NRArea ) ;
EscreveMin(ArqEi , R, &Ei ) ;
}
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.5 146

Quicksort Externo: Programa Teste


typedef int TipoApontador;
/∗−−Entra aqui o Programa C.23−−∗/
typedef TipoItem TipoRegistro ;
/∗Declaracao dos tipos utilizados pelo quicksort externo∗/
FILE ∗ArqLEs; /∗ Gerencia o Ls e o Es ∗/
FILE ∗ArqLi ; /∗ Gerencia o Li ∗/
FILE ∗ArqEi ; /∗ Gerencia o Ei ∗/
TipoItem R;
/∗−−Entram aqui os Programas J.4, D.26, D.27 e D.28−−∗/
int main( int argc , char ∗argv [ ] )
{ ArqLi = fopen ( " teste . dat" , "wb" ) ;
i f (ArqLi == NULL ) { p r i n t f ( "Arquivo nao pode ser aberto \n" ) ; exit ( 1 ) ; }
R.Chave = 5; fwrite(&R, sizeof(TipoRegistro ) , 1 , ArqLi ) ;
R.Chave = 3; fwrite(&R, sizeof(TipoRegistro ) , 1 , ArqLi ) ;
R.Chave = 10; fwrite(&R, sizeof(TipoRegistro ) , 1 , ArqLi ) ;
R.Chave = 6; fwrite(&R, sizeof(TipoRegistro ) , 1 , ArqLi ) ;
R.Chave = 1; fwrite(&R, sizeof(TipoRegistro ) , 1 , ArqLi ) ;
R.Chave = 7; fwrite(&R, sizeof(TipoRegistro ) , 1 , ArqLi ) ;
R.Chave = 4; fwrite(&R, sizeof(TipoRegistro ) , 1 , ArqLi ) ;
fclose (ArqLi ) ;
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.5 147

Quicksort Externo: Programa Teste


ArqLi = fopen ( " teste . dat" , " r+b" ) ;
i f ( ArqLi == NULL ) { p r i n t f ( "Arquivo nao pode ser aberto \n" ) ; exit ( 1 ) ; }
ArqEi = fopen ( " teste . dat" , " r+b" ) ;
i f ( ArqEi == NULL ) { p r i n t f ( "Arquivo nao pode ser aberto \n" ) ; exit ( 1 ) ; }
ArqLEs = fopen ( " teste . dat" , " r+b" ) ;
i f (ArqLEs == NULL ) { p r i n t f ( "Arquivo nao pode ser aberto \n" ) ; exit ( 1 ) ; }
QuicksortExterno(&ArqLi, &ArqEi, &ArqLEs, 1 , 7 ) ;
fflush (ArqLi ) ; fclose (ArqEi ) ; fclose (ArqLEs) ; fseek(ArqLi ,0 , SEEK_SET ) ;
while( fread(&R, sizeof(TipoRegistro ) , 1 , ArqLi ) ) { p r i n t f ( "Registro=%d\n" , R.Chave) ; }
fclose (ArqLi ) ; return 0;
}
Projeto de Algoritmos – Cap.4 Ordenação – Seção 4.2.5 148

Quicksort Externo: Análise


• Seja n o número de registros a serem ordenados.
• Seja e b o tamanho do bloco de leitura ou gravação do Sistema
operacional.
• Melhor caso: O( nb )
– Por exemplo, ocorre quando o arquivo de entrada já está ordenado.
n2
• Pior caso: O( TamArea )
– ocorre quando um dos arquivos retornados pelo procedimento
Particao tem o maior tamanho possível e o outro é vazio.
– A medida que n cresce, a probabilidade de ocorrência do pior caso
tende a zero.
n
• Caso Médio: O( nb log( TamArea ))
– É o que tem amaior probabilidade de ocorrer.
Pesquisa em Memória Primária∗

Última alteração: 7 de Setembro de 2010

∗ Transparências elaboradas por Fabiano C. Botelho, Israel Guerra e Nivio Ziviani


Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 1

Pesquisa em Memória Primária


• Introdução - Conceitos Básicos • Pesquisa Digital
• Pesquisa Sequencial – Trie
• Pesquisa Binária – Patricia

• Árvores de Pesquisa • Transformação de Chave


(Hashing)
– Árvores Binárias de Pesquisa
sem Balanceamento – Funções de Transformação
– Árvores Binárias de Pesquisa – Listas Encadeadas
com Balanceamento – Endereçamento Aberto
∗ Árvores SBB – Hashing Perfeito com or-
∗ Transformações para Manu- dem Preservada
tenção da Propriedade SBB – Hashing Perfeito Usando
Espaço Quase Ótimo
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 2

Introdução - Conceitos Básicos

• Estudo de como recuperar informação a partir de uma grande massa


de informação previamente armazenada.

• A informação é dividida em registros.

• Cada registro possui uma chave para ser usada na pesquisa.

• Objetivo da pesquisa:
Encontrar uma ou mais ocorrências de registros com chaves iguais à
chave de pesquisa.

• Pesquisa com sucesso X Pesquisa sem sucesso.


Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 3

Introdução - Conceitos Básicos

• Conjunto de registros ou arquivos ⇒ tabelas

• Tabela:
associada a entidades de vida curta, criadas na memória interna
durante a execução de um programa.

• Arquivo:
geralmente associado a entidades de vida mais longa, armazenadas
em memória externa.

• Distinção não é rígida:


tabela: arquivo de índices
arquivo: tabela de valores de funções.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 4

Escolha do Método de Pesquisa mais Adequado a uma


Determinada Aplicação

• Depende principalmente:
1. Quantidade dos dados envolvidos.
2. Arquivo estar sujeito a inserções e retiradas frequentes.

Se conteúdo do arquivo é estável é importante minimizar o tempo


de pesquisa, sem preocupação com o tempo necessário para
estruturar o arquivo
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5

Algoritmos de Pesquisa ⇒ Tipos Abstratos de Dados

• É importante considerar os algoritmos de pesquisa como tipos


abstratos de dados, com um conjunto de operações associado a uma
estrutura de dados, de tal forma que haja uma independência de
implementação para as operações.

• Operações mais comuns:


1. Inicializar a estrutura de dados.
2. Pesquisar um ou mais registros com determinada chave.
3. Inserir um novo registro.
4. Retirar um registro específico.
5. Ordenar um arquivo para obter todos os registros em ordem de
acordo com a chave.
6. Ajuntar dois arquivos para formar um arquivo maior.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 6

Dicionário
• Nome comumente utilizado para descrever uma estrutura de dados
para pesquisa.
• Dicionário é um tipo abstrato de dados com as operações:
1. Inicializa
2. Pesquisa
3. Insere
4. Retira
• Analogia com um dicionário da língua portuguesa:
– Chaves ⇐⇒ palavras
– Registros ⇐⇒ entradas associadas com cada palavra:
∗ pronúncia
∗ definição
∗ sinônimos
∗ outras informações
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.1 7

Pesquisa Sequencial
• Método de pesquisa mais simples: a partir do primeiro registro,
pesquise sequencialmente até encontrar a chave procurada; então
pare.
• Armazenamento de um conjunto de registros por meio do tipo
estruturado arranjo:
#define MAXN 10
typedef long TipoChave;
typedef struct TipoRegistro {
TipoChave Chave;
/∗ outros componentes ∗/
} TipoRegistro ;
typedef int TipoIndice ;
typedef struct TipoTabela {
TipoRegistro Item [ MAXN + 1];
TipoIndice n;
} TipoTabela;
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.1 8

Pesquisa Sequencial

void Inicializa (TipoTabela ∗T)


{ T−>n = 0 ; }

TipoIndice Pesquisa(TipoChave x , TipoTabela ∗T)


{ int i ;
T−>Item [ 0 ] .Chave = x ; i = T−>n + 1;
do { i −−;} while (T−>Item [ i ] .Chave ! = x ) ;
return i ;
}

void Insere (TipoRegistro Reg, TipoTabela ∗T)


{ i f (T−>n == MAXN)
p r i n t f ( "Erro : tabela cheia \n" ) ;
else { T−>n++; T−>Item [T−>n] = Reg; }
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.1 9

Pesquisa Sequencial

• Pesquisa retorna o índice do registro que contém a chave x;

• Caso não esteja presente, o valor retornado é zero.

• A implementação não suporta mais de um registro com uma mesma


chave.

• Para aplicações com esta característica é necessário incluir um


argumento a mais na função Pesquisa para conter o índice a partir do
qual se quer pesquisar.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.1 10

Pesquisa Sequencial

• Utilização de um registro sentinela na posição zero do array:


1. Garante que a pesquisa sempre termina:
se o índice retornado por Pesquisa for zero, a pesquisa foi sem
sucesso.
2. Não é necessário testar se i > 0, devido a isto:
– o anel interno da função Pesquisa é extremamente simples: o
índice i é decrementado e a chave de pesquisa é comparada
com a chave que está no registro.
– isto faz com que esta técnica seja conhecida como pesquisa
sequencial rápida.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.1 11

Pesquisa Sequencial: Análise

• Pesquisa com sucesso:

melhor caso : C(n) = 1


pior caso : C(n) = n
caso médio : C(n) = (n + 1)/2

• Pesquisa sem sucesso:


C ′ (n) = n + 1.

• O algoritmo de pesquisa sequencial é a melhor escolha para o


problema de pesquisa em tabelas com até 25 registros.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.2 12

Pesquisa Binária

• Pesquisa em tabela pode ser mais eficiente ⇒ Se registros forem


mantidos em ordem

• Para saber se uma chave está presente na tabela


1. Compare a chave com o registro que está na posição do meio da
tabela.
2. Se a chave é menor então o registro procurado está na primeira
metade da tabela
3. Se a chave é maior então o registro procurado está na segunda
metade da tabela.
4. Repita o processo até que a chave seja encontrada, ou fique
apenas um registro cuja chave é diferente da procurada,
significando uma pesquisa sem sucesso.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.2 13

Algoritmo de Pesquisa Binária

TipoIndice Binaria (TipoChave x , TipoTabela ∗T)


{ TipoIndice i , Esq, Dir ;
i f (T−>n == 0)
return 0;
else
Pesquisa para a chave G:
{ Esq = 1;
Dir = T−>n; 1 2 3 4 5 6 7 8
do A B C D E F G H
{ i = (Esq + Dir ) / 2 ; E F G H
i f ( x > T−>Item [ i ] .Chave) G H
Esq = i + 1;
else Dir = i − 1;
} while ( x ! = T−>Item [ i ] .Chave && Esq <= Dir ) ;
i f ( x == T−>Item [ i ] .Chave) return i ; else return 0;
}
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.2 14

Pesquisa Binária: Análise

• A cada iteração do algoritmo, o tamanho da tabela é dividido ao meio.

• Logo: o número de vezes que o tamanho da tabela é dividido ao meio


é cerca de log n.

• Ressalva: o custo para manter a tabela ordenada é alto:


a cada inserção na posição p da tabela implica no deslocamento dos
registros a partir da posição p para as posições seguintes.

• Consequentemente, a pesquisa binária não deve ser usada em


aplicações muito dinâmicas.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3 15

Árvores de Pesquisa

• A árvore de pesquisa é uma estrutura de dados muito eficiente para


armazenar informação.

• Particularmente adequada quando existe necessidade de considerar


todos ou alguma combinação de:
1. Acesso direto e sequencial eficientes.
2. Facilidade de inserção e retirada de registros.
3. Boa taxa de utilização de memória.
4. Utilização de memória primária e secundária.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.1 16

Árvores Binárias de Pesquisa sem Balanceamento


• Para qualquer nó que contenha um registro

E D

Temos a relação invariante

E R D

1. Todos os registros com chaves menores estão na subárvore à


esquerda.
2. Todos os registros com chaves maiores estão na subárvore à
direita.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.1 17

Árvores Binárias de Pesquisa sem Balanceamento

3 7

2 4 6

• O nível do nó raiz é 0.
• Se um nó está no nível i então a raiz de suas subárvores estão no
nível i + 1.
• A altura de um nó é o comprimento do caminho mais longo deste nó
até um nó folha.
• A altura de uma árvore é a altura do nó raiz.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.1 18

Implementação do Tipo Abstrato de Dados Dicionário


usando a Estrutura de Dados Árvore Binária de Pesquisa
Estrutura de dados:

typedef long TipoChave;


typedef struct TipoRegistro {
TipoChave Chave;
/∗ outros componentes ∗/
} TipoRegistro ;
typedef struct TipoNo ∗ TipoApontador;
typedef struct TipoNo {
TipoRegistro Reg;
TipoApontador Esq, Dir ;
} TipoNo;
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.1 19

Procedimento para Pesquisar na Árvore Uma Chave x


• Compare-a com a chave que está na raiz.
• Se x é menor, vá para a subárvore esquerda.
• Se x é maior, vá para a subárvore direita.
• Repita o processo recursivamente, até que a chave procurada seja
encontrada ou um nó folha é atingido.
• Se a pesquisa tiver sucesso o conteúdo retorna no próprio registro x.

void Pesquisa(TipoRegistro ∗x , TipoApontador ∗p)


{ i f (∗p == NULL)
{ p r i n t f ( "Erro : Registro nao esta presente na arvore \n" ) ; return ; }
i f ( x−>Chave < (∗p)−>Reg.Chave)
{ Pesquisa(x, &(∗p)−>Esq) ; return ; }
i f ( x−>Chave > (∗p)−>Reg.Chave) Pesquisa(x, &(∗p)−>Dir ) ;
else ∗x = (∗p)−>Reg;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.1 20

Procedimento para Inserir na Árvore


• Atingir um apontador nulo em um processo de pesquisa significa uma
pesquisa sem sucesso.
• O apontador nulo atingido é o ponto de inserção.

void Insere (TipoRegistro x , TipoApontador ∗p)


{ i f (∗p == NULL)
{ ∗p = (TipoApontador)malloc(sizeof(TipoNo) ) ;
(∗p)−>Reg = x ; ( ∗p)−>Esq = NULL ; ( ∗p)−>Dir = NULL ;
return ;
}
i f ( x .Chave < (∗p)−>Reg.Chave)
{ Insere (x, &(∗p)−>Esq) ; return ; }
i f ( x .Chave > (∗p)−>Reg.Chave)
Insere (x, &(∗p)−>Dir ) ;
else p r i n t f ( "Erro : Registro ja existe na arvore \n" ) ;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.1 21

Procedimentos para Inicializar e Criar a Árvore

void Inicializa (TipoApontador ∗ Dicionario )


{ ∗ Dicionario = NULL ; }
end ; { Inicializa }

{−− Entra aqui a definição dos tipos mostrados no slide 18 −−}


{−− Entram aqui os procedimentos Insere e Inicializa −−}
int main( int argc , char ∗argv [ ] )
{ TipoDicionario Dicionario ; TipoRegistro x ;
Inicializa(&Dicionario ) ;
scanf( "%d%∗[^\n] " , &x .Chave) ;
while(x .Chave > 0)
{ Insere (x,&Dicionario ) ;
scanf( "%d%∗[^\n] " , &x .Chave) ;
}
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.1 22

Procedimento para Retirar x da Árvore

• Alguns comentários:
1. A retirada de um registro não é tão simples quanto a inserção.
2. Se o nó que contém o registro a ser retirado possui no máximo um
descendente ⇒ a operação é simples.
3. No caso do nó conter dois descendentes o registro a ser retirado
deve ser primeiro:
– substituído pelo registro mais à direita na subárvore esquerda;
– ou pelo registro mais à esquerda na subárvore direita.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.1 23

Exemplo da Retirada de um Registro da Árvore

3 7

2 4 6

Assim: para retirar o registro com chave 5 na árvore basta trocá-lo pelo
registro com chave 4 ou pelo registro com chave 6, e então retirar o nó que
recebeu o registro com chave 5.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.1 24

Procedimento para Retirar x da Árvore

void Antecessor(TipoApontador q, TipoApontador ∗ r )


{ i f ( ( ∗ r)−>Dir ! = NULL)
{ Antecessor(q, &(∗ r)−>Dir ) ;
return ;
}
q−>Reg = (∗ r)−>Reg;
q = ∗r;
∗r = (∗ r)−>Esq;
free (q) ;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.1 25

Procedimento para Retirar x da Árvore

void Retira (TipoRegistro x , TipoApontador ∗p)


{ TipoApontador Aux;
i f (∗p == NULL ) { p r i n t f ( "Erro : Registro nao esta na arvore \n" ) ; return ; }
i f ( x .Chave < (∗p)−>Reg.Chave) { Retira (x, &(∗p)−>Esq) ; return ; }
i f ( x .Chave > (∗p)−>Reg.Chave) { Retira (x, &(∗p)−>Dir ) ; return ; }
i f ( ( ∗p)−>Dir == NULL)
{ Aux = ∗p ; ∗p = (∗p)−>Esq;
free (Aux) ; return ;
}
i f ( ( ∗p)−>Esq ! = NULL ) { Antecessor(∗p, &(∗p)−>Esq) ; return ; }
Aux = ∗p ; ∗p = (∗p)−>Dir ;
free (Aux) ;
}

• Obs.: proc. recursivo Antecessor só é ativado quando o nó que


contém registro a ser retirado possui 2 descendentes. Solução usada
por Wirth, 1976, p.211.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.1 26

Outro Exemplo de Retirada de Nó

bye

and easy

be to

bye

and to

be

be be

and to
and to
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.1 27

Caminhamento Central

• Após construída a árvore, pode ser necessário percorrer todos os


registros que compõem a tabela ou arquivo.

• Existe mais de uma ordem de caminhamento em árvores, mas a mais


útil é a chamada ordem de caminhamento central.

• O caminhamento central é mais bem expresso em termos recursivos:


1. caminha na subárvore esquerda na ordem central;
2. visita a raiz;
3. caminha na subárvore direita na ordem central.

• Uma característica importante do caminhamento central é que os nós


são visitados de forma ordenada.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.1 28

Caminhamento Central

void Central (TipoApontador p) • Percorrer a árvore usando ca-


{ i f (p == NULL ) return ; minhamento central recupera,
Central (p−>Esq) ; na ordem: 1, 2, 3, 4, 5, 6, 7.
p r i n t f ( "%ld \n" , p−>Reg.Chave) ;
Central (p−>Dir ) ; 5
}
3 7

2 4 6

1
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.1 29

Análise

• O número de comparações em uma pesquisa com sucesso:


melhor caso : C(n) = O(1)
pior caso : C(n) = O(n)
caso médio : C(n) = O(log n)

• O tempo de execução dos algoritmos para árvores binárias de


pesquisa dependem muito do formato das árvores.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.1 30

Análise

1. Para obter o pior caso basta que as chaves sejam inseridas em ordem
crescente ou decrescente. Neste caso a árvore resultante é uma lista
linear, cujo número médio de comparações é (n + 1)/2.
2. Para uma árvore de pesquisa randômica o número esperado de
comparações para recuperar um registro qualquer é cerca de
1, 39 log n, apenas 39% pior que a árvore completamente balanceada.
• Uma árvore A com n chaves possui n + 1 nós externos e estas n
chaves dividem todos os valores possíveis em n + 1 intervalos. Uma
inserção em A é considerada randômica se ela tem probabilidade igual
de acontecer em qualquer um dos n + 1 intervalos.
• Uma árvore de pesquisa randômica com n chaves é uma árvore
construida através de n inserções randômicas sucessivas em uma
árvore inicialmente vazia.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2 31

Árvores Binárias de Pesquisa com Balanceamento


• Árvore completamente balanceada ⇒ nós externos aparecem em no
máximo dois níveis adjacentes.
• Minimiza tempo médio de pesquisa para uma distribuição uniforme
das chaves, onde cada chave é igualmente provável de ser usada em
uma pesquisa.
• Contudo, custo para manter a árvore completamente balanceada após
cada inserção é muito alto.
• Para inserir a chave 1 na árvore à esquerda e obter a árvore à direita é
necessário movimentar todos os nós da árvore original.

5 4

3 7 2 6

2 4 6 1 3 5 7
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2 32

Uma Forma de Contornar este Problema


• Procurar solução intermediária que possa manter árvore
“quase-balanceada”, em vez de tentar manter a árvore completamente
balanceada.
• Objetivo: Procurar obter bons tempos de pesquisa, próximos do
tempo ótimo da árvore completamente balanceada, mas sem pagar
muito para inserir ou retirar da árvore.
• Heurísticas: existem várias heurísticas baseadas no princípio acima.
• Gonnet e Baeza-Yates (1991) apresentam algoritmos que utilizam
vários critérios de balanceamento para árvores de pesquisa, tais como
restrições impostas:
– na diferença das alturas de subárvores de cada nó da árvore,
– na redução do comprimento do caminho interno
– ou que todos os nós externos apareçam no mesmo nível.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2 33

Uma Forma de Contornar este Problema

• Comprimento do caminho interno: corresponde à soma dos


comprimentos dos caminhos entre a raiz e cada um dos nós internos
da árvore.

• Por exemplo, o comprimento do caminho interno da árvore à esquerda


na figura do slide 31 é 8 = (0 + 1 + 1 + 2 + 2 + 2).
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.1 34

Árvores SBB

• Árvores B ⇒ estrutura para memória secundária. (Bayer R. e


McCreight E.M., 1972)

• Árvore 2-3 ⇒ caso especial da árvore B.

• Cada nó tem duas ou três subárvores.

• Mais apropriada para memória primária.

• Exemplo: Uma árvore 2-3 e a árvore B binária correspondente

7 7

2,5 10 2 5 10

1 3,4 6 8,9 11 1 3 4 6 8 9 11
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.1 35

Árvores SBB
• Árvore 2-3 ⇒ árvore B binária (assimetria inerente)
1. Apontadores à esquerda apontam para um nó no nível abaixo.
2. Apontadores à direita podem ser verticais ou horizontais.
Eliminação da assimetria nas árvores B binárias ⇒ árvores B binárias
simétricas (Symmetric Binary B-trees – SBB)
• Árvore SBB tem apontadores verticais e horizontais, tal que:
1. todos os caminhos da raiz até cada nó externo possuem o mesmo
número de apontadores verticais, e
2. não podem existir dois apontadores horizontais sucessivos.

3 5 9

1 2 4 6 7 8 10
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 36

Transformações para Manutenção da Propriedade SBB


• O algoritmo para árvores SBB usa transformações locais no caminho
de inserção ou retirada para preservar o balanceamento.
• A chave a ser inserida ou retirada é sempre inserida ou retirada após o
apontador vertical mais baixo na árvore.
• Nesse caso podem aparecer dois apontadores horizontais sucessivos,
sendo necessário realizar uma transformação:

2 3 1 2 3 2

1 1 3

(a) Esquerda−esquerda (EE)

1 3 1 2 3 2

2 1 3

(b) Esquerda−direita (ED)


Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 37

Estrutura de Dados Árvore SBB para Implementar o Tipo


Abstrato de Dados Dicionário

typedef int TipoChave;


typedef struct TipoRegistro {
/∗ outros componentes ∗/
TipoChave Chave;
} TipoRegistro ;
typedef enum {
Vertical , Horizontal
} TipoInclinacao ;
typedef struct TipoNo∗ TipoApontador;
typedef struct TipoNo {
TipoRegistro Reg;
TipoApontador Esq, Dir ;
TipoInclinacao BitE , BitD ;
} TipoNo;
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 38

Procedimentos Auxiliares para Árvores SBB

void EE(TipoApontador ∗Ap)


{ TipoApontador Ap1;
Ap1 = (∗Ap)−>Esq; ( ∗Ap)−>Esq = Ap1−>Dir ; Ap1−>Dir = ∗Ap;
Ap1−>BitE = Vertical ; ( ∗Ap)−>BitE = Vertical ; ∗Ap = Ap1;
}

void ED(TipoApontador ∗Ap)


{ TipoApontador Ap1, Ap2;
Ap1 = (∗Ap)−>Esq; Ap2 = Ap1−>Dir ; Ap1−>BitD = Vertical ;
(∗Ap)−>BitE = Vertical ; Ap1−>Dir = Ap2−>Esq; Ap2−>Esq = Ap1;
(∗Ap)−>Esq = Ap2−>Dir ; Ap2−>Dir = ∗Ap; ∗Ap = Ap2;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 39

Procedimentos Auxiliares para Árvores SBB

void DD(TipoApontador ∗Ap)


{ TipoApontador Ap1;
Ap1 = (∗Ap)−>Dir ; ( ∗Ap)−>Dir = Ap1−>Esq; Ap1−>Esq = ∗Ap;
Ap1−>BitD = Vertical ; ( ∗Ap)−>BitD = Vertical ; ∗Ap = Ap1;
}

void DE(TipoApontador ∗Ap)


{ TipoApontador Ap1, Ap2;
Ap1 = (∗Ap)−>Dir ; Ap2 = Ap1−>Esq; Ap1−>BitE = Vertical ;
(∗Ap)−>BitD = Vertical ; Ap1−>Esq = Ap2−>Dir ; Ap2−>Dir = Ap1;
(∗Ap)−>Dir = Ap2−>Esq; Ap2−>Esq = ∗Ap; ∗Ap = Ap2;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 40

Procedimento para Inserir na Árvore SBB


void IInsere (TipoRegistro x , TipoApontador ∗Ap,
TipoInclinacao ∗IAp , short ∗Fim)
{ i f (∗Ap == NULL)
{ ∗Ap = (TipoApontador)malloc(sizeof(TipoNo) ) ;
∗IAp = Horizontal ; (∗Ap)−>Reg = x ;
(∗Ap)−>BitE = Vertical ; (∗Ap)−>BitD = Vertical ;
(∗Ap)−>Esq = NULL ; ( ∗Ap)−>Dir = NULL ; ∗Fim = FALSE ;
return ;
}
i f ( x .Chave < (∗Ap)−>Reg.Chave)
{ IInsere (x, &(∗Ap)−>Esq, &(∗Ap)−>BitE , Fim) ;
i f (∗Fim) return ;
i f ( ( ∗Ap)−>BitE ! = Horizontal ) { ∗Fim = TRUE ; return ; }
i f ( ( ∗Ap)−>Esq−>BitE == Horizontal )
{ EE(Ap) ; ∗ IAp = Horizontal ; return ; }
i f ( ( ∗Ap)−>Esq−>BitD == Horizontal ) { ED(Ap) ; ∗ IAp = Horizontal ; }
return ;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 41

Procedimento para Inserir na Árvore SBB

i f ( x .Chave <= (∗Ap)−>Reg.Chave)


{ p r i n t f ( "Erro : Chave ja esta na arvore \n" ) ;
∗Fim = TRUE ;
return ;
}
IInsere (x, &(∗Ap)−>Dir , &(∗Ap)−>BitD , Fim) ;
i f (∗Fim) return ;
i f ( ( ∗Ap)−>BitD ! = Horizontal ) { ∗Fim = TRUE ; return ; }
i f ( ( ∗Ap)−>Dir−>BitD == Horizontal )
{ DD(Ap) ; ∗ IAp = Horizontal ; return ; }
i f ( ( ∗Ap)−>Dir−>BitE == Horizontal ) { DE(Ap) ; ∗ IAp = Horizontal ; }
}

void Insere (TipoRegistro x , TipoApontador ∗Ap)


{ short Fim; TipoInclinacao IAp;
IInsere (x , Ap, &IAp, &Fim) ;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 42

Exemplo
Inserção de uma sequência de chaves em uma árvore SBB:
1. Árvore à esquerda é obtida após a inserção das chaves 7, 10, 5.
2. Árvore do meio é obtida após a inserção das chaves 2, 4 na árvore
anterior.
3. Árvore à direita é obtida após a inserção das chaves 9, 3, 6 na árvore
anterior.

5 3 5 9

5 7 10 2 4 7 10 2 4 6 7 10

void Inicializa (TipoApontador ∗ Dicionario )


{ ∗ Dicionario = NULL ; }
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 43

Procedimento Retira

• Retira contém um outro procedimento interno de nome IRetira.

• IRetira usa 3 procedimentos internos:EsqCurto, DirCurto, Antecessor.


– EsqCurto (DirCurto) é chamado quando um nó folha que é
referenciado por um apontador vertical é retirado da subárvore à
esquerda (direita) tornando-a menor na altura após a retirada;
– Quando o nó a ser retirado possui dois descendentes, o
procedimento Antecessor localiza o nó antecessor para ser trocado
com o nó a ser retirado.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 44

Procedimento para Retirar da Árvore SBB


void EsqCurto(TipoApontador ∗Ap, short ∗Fim)
{ /∗ Folha esquerda retirada => arvore curta na altura esquerda ∗/
TipoApontador Ap1;
i f ((∗Ap)−>BitE == Horizontal )
{ (∗Ap)−>BitE = Vertical ; ∗Fim = TRUE ; return ; }
i f ((∗Ap)−>BitD == Horizontal )
{ Ap1 = (∗Ap)−>Dir ; ( ∗Ap)−>Dir = Ap1−>Esq; Ap1−>Esq = ∗Ap; ∗Ap = Ap1;
i f ((∗Ap)−>Esq−>Dir−>BitE == Horizontal )
{ DE(&(∗Ap)−>Esq) ; ( ∗Ap)−>BitE = Horizontal ; }
else i f ((∗Ap)−>Esq−>Dir−>BitD == Horizontal )
{ DD(&(∗Ap)−>Esq) ; ( ∗Ap)−>BitE = Horizontal ; }
∗Fim = TRUE ; return ;
}
(∗Ap)−>BitD = Horizontal ;
i f ((∗Ap)−>Dir−>BitE == Horizontal ) { DE(Ap) ; ∗Fim = TRUE ; return ; }
i f ((∗Ap)−>Dir−>BitD == Horizontal ) { DD(Ap) ; ∗Fim = TRUE ; }
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 45

Procedimento para Retirar da Árvore SBB – DirCurto


void DirCurto(TipoApontador ∗Ap, short ∗Fim)
{ /∗ Folha direita retirada => arvore curta na altura direita ∗/
TipoApontador Ap1;
i f ((∗Ap)−>BitD == Horizontal )
{ (∗Ap)−>BitD = Vertical ; ∗Fim = TRUE ; return ; }
i f ((∗Ap)−>BitE == Horizontal )
{ Ap1 = (∗Ap)−>Esq; ( ∗Ap)−>Esq = Ap1−>Dir ; Ap1−>Dir = ∗Ap; ∗Ap = Ap1;
i f ((∗Ap)−>Dir−>Esq−>BitD == Horizontal )
{ ED(&(∗Ap)−>Dir ) ; ( ∗Ap)−>BitD = Horizontal ; }
else i f ((∗Ap)−>Dir−>Esq−>BitE == Horizontal )
{ EE(&(∗Ap)−>Dir ) ; ( ∗Ap)−>BitD = Horizontal ; }
∗Fim = TRUE ; return ;
}
(∗Ap)−>BitE = Horizontal ;
i f ((∗Ap)−>Esq−>BitD == Horizontal ) { ED(Ap) ; ∗Fim = TRUE ; return ; }
i f ((∗Ap)−>Esq−>BitE == Horizontal ) { EE(Ap) ; ∗Fim = TRUE ; }
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 46

Procedimento para Retirar da Árvore SBB – Antecessor

void Antecessor(TipoApontador q, TipoApontador ∗ r , short ∗Fim)


{ i f ( ( ∗ r)−>Dir ! = NULL)
{ Antecessor(q, &(∗ r)−>Dir , Fim) ;
i f ( ! ∗Fim) DirCurto ( r , Fim) ; return ;
}
q−>Reg = (∗ r)−>Reg; q = ∗ r ; ∗ r = (∗ r)−>Esq; free (q) ;
i f (∗ r ! = NULL) ∗Fim = TRUE ;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 47

Procedimento para Retirar da Árvore SBB


void IRetira (TipoRegistro x , TipoApontador ∗Ap, short ∗Fim)
{ TipoNo ∗Aux;
i f (∗Ap == NULL ) { p r i n t f ( "Chave nao esta na arvore \n" ) ; ∗Fim = TRUE ; return ; }
i f ( x .Chave < (∗Ap)−>Reg.Chave)
{ IRetira (x, &(∗Ap)−>Esq, Fim) ; i f ( ! ∗Fim) EsqCurto(Ap, Fim) ; return ; }
i f ( x .Chave > (∗Ap)−>Reg.Chave)
{ IRetira (x, &(∗Ap)−>Dir , Fim) ;
i f ( ! ∗Fim) DirCurto(Ap, Fim) ; return ;
}
∗Fim = FALSE ; Aux = ∗Ap;
i f (Aux−>Dir == NULL)
{ ∗Ap = Aux−>Esq; free (Aux) ;
i f (∗Ap ! = NULL) ∗Fim = TRUE ; return ;
}
i f (Aux−>Esq == NULL)
{ ∗Ap = Aux−>Dir ; free (Aux) ;
i f (∗Ap ! = NULL) ∗Fim = TRUE ; return ;
}
Antecessor(Aux, &Aux−>Esq, Fim) ;
i f ( ! ∗Fim) EsqCurto(Ap, Fim) ; /∗ Encontrou chave ∗/
}
void Retira (TipoRegistro x , TipoApontador ∗Ap)
{ short Fim; IRetira (x , Ap, &Fim) ; }
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 48

Exemplo

5 3 5 9

5 7 10 2 4 7 10 2 4 6 7 10

• A árvore à esquerda abaixo é obtida após a retirada da chave 7 da


árvore à direita acima.
• A árvore do meio é obtida após a retirada da chave 5 da árvore
anterior.
• A árvore à direita é obtida após a retirada da chave 9 da árvore
anterior.

3 5 9 4 9 4

2 4 6 10 2 3 6 10 2 3 6 10
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 49

Exemplo: Retirada de Nós da Árvore SBB


Caso 1: 4
4 2 4
2 10
2 6 10 1 3 6 10
1 3 6 12 t
6
1 3 2a chamada
DirCurto
1a chamada DirCurto

2 chamadas DirCurto

Caso 2:

4 4 4

2 10
2 6 8 10 2 8

1 3 6 8 12
1 3 1 3 6 10

1a chamada DirCurto
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 50

Exemplo: Retirada de Nós da Árvore SBB


Caso 3:
4
4

2 6 10 2 6 10

1 3 5 8 12 1 3 5 8

2 6

1 3 5 8 10

Se nodo 8 tem filho:

4 4

2 6 10 2 6 10

1 3 5 8 9 12 1 3 5 8 9

4
4
2 6 9
2 6
1 3 5 8 10
1 3 5 8 10
9
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 51

Análise

• Nas árvores SBB é necessário distinguir dois tipos de alturas:


1. Altura vertical h → necessária para manter a altura uniforme e
obtida através da contagem do número de apontadores verticais
em qualquer caminho entre a raiz e um nó externo.
2. Altura k → representa o número máximo de comparações de
chaves obtida através da contagem do número total de
apontadores no maior caminho entre a raiz e um nó externo.

• A altura k é maior que a altura h sempre que existirem apontadores


horizontais na árvore.

• Para uma árvore SBB com n nós internos, temos que


h ≤ k ≤ 2h.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.3.2.2 52

Análise
• De fato Bayer (1972) mostrou que
log(n + 1) ≤ k ≤ 2 log(n + 2) − 2.
• Custo para manter a propriedade SBB ⇒ Custo para percorrer o
caminho de pesquisa para encontrar a chave, seja para inserí-la ou
para retirá-la.
• Logo: O custo é O(log n).
• Número de comparações em uma pesquisa com sucesso é:
melhor caso : C(n) = O(1)
pior caso : C(n) = O(log n)
caso médio : C(n) = O(log n)
• Observe: Na prática o caso médio para Cn é apenas cerca de 2%
pior que o Cn para uma árvore completamente balanceada, conforme
mostrado em Ziviani e Tompa (1982).
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4 53

Pesquisa Digital

• Pesquisa digital é baseada na representação das chaves como uma


sequência de caracteres ou de dígitos.

• Os métodos de pesquisa digital são particularmente vantajosos


quando as chaves são grandes e de tamanho variável.

• Um aspecto interessante quanto aos métodos de pesquisa digital é a


possibilidade de localizar todas as ocorrências de uma determinada
cadeia em um texto, com tempo de resposta logarítmico em relação ao
tamanho do texto.
– Trie
– Patrícia
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4.1 54

Trie

• Uma trie é uma árvore M -ária cujos nós são vetores de M


componentes com campos correspondentes aos dígitos ou caracteres
que formam as chaves.

• Cada nó no nível i representa o conjunto de todas as chaves que


começam com a mesma sequência de i dígitos ou caracteres.

• Este nó especifica uma ramificação com M caminhos dependendo do


(i + 1)-ésimo dígito ou caractere de uma chave.

• Considerando as chaves como sequência de bits (isto é, M = 2), o


algoritmo de pesquisa digital é semelhante ao de pesquisa em árvore,
exceto que, em vez de se caminhar na árvore de acordo com o
resultado de comparação entre chaves, caminha-se de acordo com os
bits de chave.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4.1 55

Exemplo

• Dada as chaves de 6 bits:

B = 010010
C = 010011
H = 011000
J = 100001
M = 101000

0 1
1 0
0 1 0 1
0 H J Q
1
0 1
B C
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4.1 56

Inserção das Chaves W e K na Trie Binária


0 1
1 0
0 1 0 1
0 H J Q
1
0 1
B C

Faz-se uma pesquisa na árvore com a chave a ser inserida. Se o nó externo em


que a pesquisa terminar for vazio, cria-se um novo nó externo nesse ponto
contendo a nova chave. Exemplo: a inserção da chave W = 110110.
Se o nó externo contiver uma chave cria-se um ou mais nós internos cujos
descendentes conterão a chave já existente e a nova chave. Exemplo: inserção
da chave K = 100010. 0 1
1 0 1
0 1 0 1 W
0 H 0 Q
1 0 1
0 1 J K
B C
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4.1 57

Considerações Importantes sobre as Tries

• O formato das tries, diferentemente das árvores binárias comuns, não


depende da ordem em que as chaves são inseridas e sim da estrutura
das chaves através da distribuição de seus bits.

• Desvantagem:
– Uma grande desvantagem das tries é a formação de caminhos de
uma só direção para chaves com um grande número de bits em
comum.
– Exemplo: Se duas chaves diferirem somente no último bit, elas
formarão um caminho cujo comprimento é igual ao tamanho delas,
não importando quantas chaves existem na árvore.
– Caminho gerado pelas chaves B e C.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4.2 58

Patricia - Practical Algorithm To Retrieve Information


Coded In Alphanumeric

• Criado por Morrison D. R. 1968 para aplicação em recuperação de


informação em arquivos de grande porte.

• Knuth D. E. 1973 → novo tratamento algoritmo.

• Reapresentou-o de forma mais clara como um caso particular de


pesquisa digital, essencialmente, um caso de árvore trie binária.

• Sedgewick R. 1988 apresentou novos algoritmos de pesquisa e de


inserção baseados nos algoritmos propostos por Knuth.

• Gonnet, G.H e Baeza-Yates R. 1991 propuzeram também outros


algoritmos.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4.2 59

Mais sobre Patricia

• O algoritmo para construção da árvore Patricia é baseado no método


de pesquisa digital, mas sem o inconveniente citado para o caso das
tries.

• O problema de caminhos de uma só direção é eliminado por meio de


uma solução simples e elegante: cada nó interno da árvore contém o
índice do bit a ser testado para decidir qual ramo tomar.

• Exemplo: dada as chaves de 6 bits:

B = 010010 1

C = 010011 3 3

H = 011000 6 H J Q

J = 100001 B C

Q = 101000
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4.2 60

Inserção da Chave K

1 1

3 3 3 3

6 H J Q 6 H 5 Q

B C B C J K

• Para inserir a chave K = 100010 na árvore à esquerda, a pesquisa inicia pela raiz e
termina quando se chega ao nó externo contendo J.
• Os índices dos bits nas chaves estão ordenados da esquerda para a direita. Bit de
índice 1 de K é 1 → a subárvore direita Bit de índice 3 → subárvore esquerda que
neste caso é um nó externo.
• Chaves J e K mantêm o padrão de bits 1x0xxx, assim como qualquer outra chave
que seguir este caminho de pesquisa.
• Novo nó interno repõe o nó J, e este com nó K serão os nós externos descendentes.
• O índice do novo nó interno é dado pelo 1o bit diferente das 2 chaves em questão,
que é o bit de índice 5. Para determinar qual será o descendente esquerdo e o
direito, verifique o valor do bit 5 de ambas as chaves.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4.2 61

Inserção da Chave W
• A inserção da chave W = 110110 ilustra um outro aspecto.
• Os bits das chaves K e W são comparados a partir do primeiro para
determinar em qual índice eles diferem (nesse casod os de índice 2).
• Portanto: o ponto de inserção agora será no caminho de pesquisa
entre os nós internos de índice 1 e 3.
• Cria-se aí um novo nó interno de índice 2, cujo descendente direito é
um nó externo contendo W e cujo descendente esquerdo é a
subárvore de raiz de índice 3.

3 2

6 H 3 W

B C 5 Q

J K
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4.2 62

Estrutura de Dados
#define D 8 /∗ depende de TipoChave ∗/
typedef unsigned char TipoChave; /∗ a definir , depende da aplicacao ∗/
typedef unsigned char TipoIndexAmp;
typedef unsigned char TipoDib ;
typedef enum {
Interno , Externo
} TipoNo;
typedef struct TipoPatNo∗ TipoArvore ;
typedef struct TipoPatNo {
TipoNo nt ;
union {
struct {
TipoIndexAmp Index ;
TipoArvore Esq, Dir ;
} NInterno ;
TipoChave Chave;
} NO ;
} TipoPatNo;
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4.2 63

Funções Auxiliares

TipoDib Bit (TipoIndexAmp i , TipoChave k)


{ /∗ Retorna o i−esimo b i t da chave k a partir da esquerda ∗/
int c, j ;
i f ( i == 0)
return 0;
else { c = k ;
for ( j = 1; j <= D − i ; j ++) c /= 2;
return ( c & 1);
}
}

short EExterno(TipoArvore p)
{ /∗ Verifica se p^ e nodo externo ∗/
return ( p−>nt == Externo ) ;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4.2 64

Procedimentos para Criar Nós Interno e Externo

TipoArvore CriaNoInt( int i , TipoArvore ∗Esq, TipoArvore ∗ Dir )


{ TipoArvore p;
p = (TipoArvore)malloc(sizeof(TipoPatNo) ) ;
p−>nt = Interno ; p−>NO . NInterno .Esq = ∗Esq;
p−>NO . NInterno . Dir = ∗ Dir ; p−>NO . NInterno . Index = i ;
return p;
}

TipoArvore CriaNoExt(TipoChave k)
{ TipoArvore p;
p = (TipoArvore)malloc(sizeof(TipoPatNo) ) ;
p−>nt = Externo ; p−>NO .Chave = k ; return p;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4.2 65

Algoritmo de Pesquisa

void Pesquisa(TipoChave k , TipoArvore t )


{ i f ( EExterno( t ) )
{ i f ( k == t−>NO .Chave)
p r i n t f ( "Elemento encontrado\n" ) ;
else p r i n t f ( "Elemento nao encontrado\n" ) ;
return ;
}
i f ( Bit ( t−>NO . NInterno . Index , k) == 0)
Pesquisa(k , t−>NO . NInterno .Esq) ;
else Pesquisa(k , t−>NO . NInterno . Dir ) ;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4.2 66

Descrição Informal do Algoritmo de Inserção


• Cada chave k é inserida de acordo com os passos abaixo, partindo da
raiz:
1. Se a subárvore corrente for vazia, então é criado um nó externo
contendo a chave k (isto ocorre somente na inserção da primeira
chave) e o algoritmo termina.
2. Se a subárvore corrente for simplesmente um nó externo, os bits da
chave k são comparados, a partir do bit de índice imediatamente
após o último índice da sequência de índices consecutivos do
caminho de pesquisa, com os bits correspondentes da chave k’
deste nó externo até encontrar um índice i cujos bits difiram. A
comparação dos bits a partir do último índice consecutivo melhora
consideravelmente o desempenho do algoritmo. Se todos forem
iguais, a chave já se encontra na árvore e o algoritmo termina;
senão, vai-se para o Passo 4.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4.2 67

Descrição Informal do Algoritmo de Inserção

• Continuação:
3. Se a raiz da subárvore corrente for um nó interno, vai-se para a
subárvore indicada pelo bit da chave k de índice dado pelo nó
corrente, de forma recursiva.
4. Depois são criados um nó interno e um nó externo: o primeiro
contendo o índice i e o segundo, a chave k. A seguir, o nó interno é
ligado ao externo pelo apontador de subárvore esquerda ou direita,
dependendo se o bit de índice i da chave k seja 0 ou 1,
respectivamente.
5. O caminho de inserção é percorrido novamente de baixo para cima,
subindo com o par de nós criados no Passo 4 até chegar a um nó
interno cujo índice seja menor que o índice i determinado no Passo
2. Este é o ponto de inserção e o par de nós é inserido.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4.2 68

Algoritmo de inserção
TipoArvore InsereEntre(TipoChave k , TipoArvore ∗ t , int i )
{ TipoArvore p;
i f ( EExterno(∗ t ) | | i < (∗ t)−>NO . NInterno . Index)
{ /∗ cria um novo no externo ∗/
p = CriaNoExt(k ) ;
i f ( Bit ( i , k) == 1)
return ( CriaNoInt( i , t , &p) ) ;
else return ( CriaNoInt( i , &p, t ) ) ;
}
else
{ i f ( Bit ((∗ t)−>NO . NInterno . Index , k) == 1)
(∗ t)−>NO . NInterno . Dir = InsereEntre(k,&(∗ t)−>NO . NInterno . Dir , i ) ;
else
(∗ t)−>NO . NInterno .Esq = InsereEntre(k,&(∗ t)−>NO . NInterno .Esq, i ) ;
return (∗ t ) ;
}
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.4.2 69

Algoritmo de inserção

TipoArvore Insere (TipoChave k , TipoArvore ∗ t )


{ TipoArvore p ; int i ;
i f (∗ t == NULL ) return ( CriaNoExt(k ) ) ;
else
{ p = ∗t;
while ( ! EExterno(p) )
{ i f ( Bit (p−>NO . NInterno . Index , k) == 1) p = p−>NO . NInterno . Dir ;
else p = p−>NO . NInterno .Esq;
}
/∗ acha o primeiro b i t diferente ∗/
i = 1;
while ( ( i <= D) & ( Bit ( ( int ) i , k) == Bit ( ( int ) i , p−>NO .Chave) ) )
i ++;
i f ( i > D) { p r i n t f ( "Erro : chave ja esta na arvore \n" ) ; return (∗ t ) ; }
else return ( InsereEntre(k , t , i ) ) ;
}
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5 70

Transformação de Chave (Hashing)

• Os registros armazenados em uma tabela são diretamente


endereçados a partir de uma transformação aritmética sobre a chave
de pesquisa.

• Hash significa:
1. Fazer picadinho de carne e vegetais para cozinhar.
2. Fazer uma bagunça. (Webster’s New World Dictionary)
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5 71

Transformação de Chave (Hashing)


• Um método de pesquisa com o uso da transformação de chave é
constituído de duas etapas principais:
1. Computar o valor da função de transformação, a qual transforma
a chave de pesquisa em um endereço da tabela.
2. Considerando que duas ou mais chaves podem ser transformadas
em um mesmo endereço de tabela, é necessário existir um método
para lidar com colisões.
• Qualquer que seja a função de transformação, algumas colisões irão
ocorrer fatalmente, e tais colisões têm de ser resolvidas de alguma
forma.
• Mesmo que se obtenha uma função de transformação que distribua os
registros de forma uniforme entre as entradas da tabela, existe uma
alta probabilidade de haver colisões.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5 72

Transformação de Chave (Hashing)

• O paradoxo do aniversário (Feller,1968, p. 33), diz que em um grupo


de 23 ou mais pessoas, juntas ao acaso, existe uma chance maior do
que 50% de que 2 pessoas comemorem aniversário no mesmo dia.

• Assim, se for utilizada uma função de transformação uniforme que


enderece 23 chaves randômicas em uma tabela de tamanho 365, a
probabilidade de que haja colisões é maior do que 50%.

• A probabilidade p de se inserir N itens consecutivos sem colisão em


uma tabela de tamanho M é:
M −1 M −2 M −N +1
p= × × ... × =
M M M
N
Y M −i+1 M!
= N
i=1
M (M − N )!M
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5 73

Transformação de Chave (Hashing)

• Alguns valores de p para diferentes valores de N ,onde M = 365.

N p
10 0,883
22 0,524
23 0,493
30 0,303

• Para N pequeno a probabilidade p pode ser aproximada por


p ≈ N (N −1))
730
. Por exemplo, para N = 10 então p ≈ 87, 7%.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.1 74

Funções de Transformação

• Uma função de transformação deve mapear chaves em inteiros dentro


do intervalo [0..M − 1], onde M é o tamanho da tabela.

• A função de transformação ideal é aquela que:


1. Seja simples de ser computada.
2. Para cada chave de entrada, qualquer uma das saídas possíveis é
igualmente provável de ocorrer.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.1 75

Método mais Usado

• Usa o resto da divisão por M .


h(K) = K mod M
onde K é um inteiro correspondente à chave.

• Cuidado na escolha do valor de M . M deve ser um número primo,


mas não qualquer primo: devem ser evitados os números primos
obtidos a partir de
bi ± j
onde b é a base do conjunto de caracteres (geralmente b = 64 para
BCD, 128 para ASCII, 256 para EBCDIC, ou 100 para alguns códigos
decimais), e i e j são pequenos inteiros.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.1 76

Transformação de Chaves Não Numéricas

• As chaves não numéricas devem ser transformadas em números:


n
X
K= Chave[i] × p[i]
i=1

• n é o número de caracteres da chave.

• Chave[i] corresponde à representação ASCII do i-ésimo caractere da


chave.

• p[i] é um inteiro de um conjunto de pesos gerados randomicamente


para 1 ≤ i ≤ n.

• Vantagem de usar pesos: Dois conjuntos diferentes de p1 [i] e p2 [i],


1 ≤ i ≤ n, leva a duas funções h1 (K) e h2 (K) diferentes.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.1 77

Transformação de Chaves Não Numéricas

void GeraPesos(TipoPesos p)
{ int i ;
struct timeval semente;
/∗ Utilizar o tempo como semente para a funcao srand ( ) ∗/
gettimeofday(&semente, NULL ) ;
srand( ( int ) (semente. tv_sec + 1000000∗semente. tv_usec ) ) ;
for ( i = 0; i < n ; i ++)
p[ i ] = 1+(int) (10000.0∗rand ( ) / ( RAND_MAX+1.0));
}

typedef char TipoChave[N] ;


TipoIndice h(TipoChave Chave, TipoPesos p)
{ int i ; unsigned int Soma = 0;
int comp = strlen (Chave) ;
for ( i = 0; i < comp; i ++) Soma += (unsigned int )Chave[ i ] ∗ p[ i ] ;
return (Soma % M) ;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.1 78

Transformação de Chaves Não Numéricas: Nova Versão


• Modificação no cálculo da função h para evitar a multiplicação da
representação ASCII de cada caractere pelos pesos (Zobrist 1990).
– Este é um caso típico de troca de espaço por tempo.
• Um peso diferente é gerado randomicamente para cada um dos 256
caracteres ASCII possíveis na i−ésima posição da chave, para
1 ≤ i ≤ n.
#define TAMALFABETO 256
typedef unsigned TipoPesos[N] [ TAMALFABETO ] ;
void GeraPesos(TipoPesos p) /∗ Gera valores randomicos entre 1 e 10.000 ∗/
{ int i , j ; struct timeval semente; /∗ Utilizar o tempo como semente ∗/
gettimeofday(&semente, NULL ) ;
srand( ( int ) (semente. tv_sec + 1000000 ∗ semente. tv_usec ) ) ;
for ( i = 0; i < N; i ++)
for ( j = 0; j < TAMALFABETO ; j ++)
p[ i ] [ j ] = 1 + ( int )(10000.0 ∗ rand ( ) / ( RAND_MAX + 1.0));
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.1 79

Transformação de Chaves Não Numéricas: Nova Versão


Implementação da função hash de Zobrist:
• Para obter h é necessário o mesmo número de adições da função do
programa anterior, mas nenhuma multiplicação é efetuada.
• Isso faz com que h seja computada de forma mais eficiente.
• Nesse caso, a quantidade de espaço para armazenar h é O(n × |Σ|),
onde |Σ| representa o tamanho do alfabeto, enquanto que para a
função do programa anterior é O(n).

typedef char TipoChave[N] ;

TipoIndice h(TipoChave Chave, TipoPesos p)


{ int i ; unsigned int Soma = 0;
int comp = strlen (Chave) ;
for ( i = 0; i < comp; i ++) Soma += p[ i ] [ (unsigned int )Chave[ i ] ] ;
return (Soma % M) ;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.2 80

Listas Encadeadas
• Uma das formas de resolver as colisões é construir uma lista linear
encadeada para cada endereço da tabela. Assim, todas as chaves
com mesmo endereço são encadeadas em uma lista linear.
• Exemplo: Se a i-ésima letra do alfabeto é representada pelo número i
e a função de transformação h(Chave) = Chave mod M é utilizada
para M = 7, o resultado da inserção das chaves P E S Q U I S A na
tabela é o seguinte:
– h(A) = h(1) = 1, h(E) = h(5) = 5, h(S) = h(19) = 5, e assim por
diante.
T

0 - U - nil
1 - A - nil
2 - P - I - nil
3 - Q - nil
4 - nil
5 - E - S - S - nil
6 - nil
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.2 81

Estrutura do Dicionário para Listas Encadeadas


typedef char TipoChave[N] ;
typedef unsigned TipoPesos[N] [ TAMALFABETO ] ;
typedef struct TipoItem {
/∗ outros componentes ∗/
TipoChave Chave;
} TipoItem ;
typedef unsigned int TipoIndice ;
typedef struct TipoCelula∗ TipoApontador;
typedef struct TipoCelula {
TipoItem Item ;
TipoApontador Prox;
} TipoCelula ;
typedef struct TipoLista {
TipoCelula ∗Primeiro , ∗ Ultimo ;
} TipoLista ;
typedef TipoLista TipoDicionario [M] ;
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.2 82

Operações do Dicionário Usando Listas Encadeadas


void Inicializa ( TipoDicionario T)
{ int i ;
for ( i = 0; i < M; i ++) FLVazia(&T[ i ] ) ;
}

TipoApontador Pesquisa(TipoChave Ch, TipoPesos p, TipoDicionario T)


{ /∗ TipoApontador de retorno aponta para o item anterior da l i s t a ∗/
TipoIndice i ; TipoApontador Ap;
i = h(Ch, p) ;
i f ( Vazia(T[ i ] ) ) return NULL ; /∗ Pesquisa sem sucesso ∗/
else
{ Ap = T[ i ] . Primeiro ;
while (Ap−>Prox−>Prox ! = NULL &&
strncmp(Ch, Ap−>Prox−>Item .Chave, sizeof(TipoChave) ) )
Ap = Ap−>Prox;
i f ( ! strncmp(Ch, Ap−>Prox−>Item .Chave, sizeof(TipoChave) ) ) return Ap;
else return NULL ; /∗ Pesquisa sem sucesso ∗/
}
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.2 83

Operações do Dicionário Usando Listas Encadeadas


void Insere (TipoItem x , TipoPesos p, TipoDicionario T)
{
i f ( Pesquisa(x .Chave, p, T) == NULL)
Ins (x, &T[h(x .Chave, p ) ] ) ;
else p r i n t f ( " Registro ja esta presente \n" ) ;
}

void Retira (TipoItem x , TipoPesos p, TipoDicionario T)


{
TipoApontador Ap;
Ap = Pesquisa(x .Chave, p, T) ;
i f (Ap == NULL)
p r i n t f ( " Registro nao esta presente \n" ) ;
else Ret(Ap, &T[h(x .Chave, p)] , &x ) ;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.2 84

Análise

• Assumindo que qualquer item do conjunto tem igual probabilidade de


ser endereçado para qualquer entrada de T, então o comprimento
esperado de cada lista encadeada é N/M , onde N representa o
número de registros na tabela e M o tamanho da tabela.

• Logo: as operações Pesquisa, Insere e Retira custam O(1 + N/M )


operações em média, onde a constante 1 representa o tempo para
encontrar a entrada na tabela e N/M o tempo para percorrer a lista.
Para valores de M próximos de N , o tempo se torna constante, isto é,
independente de N .
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.3 85

Endereçamento Aberto
• Quando o número de registros a serem armazenados na tabela puder
ser previamente estimado, então não haverá necessidade de usar
apontadores para armazenar os registros.
• Existem vários métodos para armazenar N registros em uma tabela
de tamanho M > N , os quais utilizam os lugares vazios na própria
tabela para resolver as colisões. (Knuth, 1973, p.518)
• No Endereçamento aberto todas as chaves são armazenadas na
própria tabela, sem o uso de apontadores explícitos.
• Existem várias propostas para a escolha de localizações alternativas.
A mais simples é chamada de hashing linear, onde a posição hj na
tabela é dada por:

hj = (h(x) + j) mod M, para 1 ≤ j ≤ M − 1.


Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.3 86

Exemplo
• Se a i-ésima letra do alfabeto é representada pelo número i e a função
de transformação h(Chave) = Chave mod M é utilizada para M = 7.
• então o resultado da inserção das chaves L U N E S na tabela,
usando hashing linear para resolver colisões é mostrado abaixo.
• Por exemplo, h(L) = h(12) = 5, h(U ) = h(21) = 0, h(N ) = h(14) = 0,
h(E) = h(5) = 5, e h(S) = h(19) = 5.
T
0 U
1 N
2 S
3
4
5 L
6 E
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.3 87

Estrutura do Dicionário Usando Endereçamento Aberto


#define VAZIO " !!!!!!!!!! "
#define RETIRADO "∗∗∗∗∗∗∗∗∗∗"
#define M 7
#define N 11 /∗ Tamanho da chave ∗/

typedef unsigned int TipoApontador;


typedef char TipoChave[N] ;
typedef unsigned TipoPesos[N] ;
typedef struct TipoItem {
/∗ outros componentes ∗/
TipoChave Chave;
} TipoItem ;
typedef unsigned int TipoIndice ;
typedef TipoItem TipoDicionario [M] ;
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.3 88

Operações do Dicionário Usando Endereçamento Aberto


void Inicializa ( TipoDicionario T)
{ int i ;
for ( i = 0; i < M; i ++) memcpy(T[ i ] .Chave, VAZIO , N) ;
}

TipoApontador Pesquisa(TipoChave Ch, TipoPesos p, TipoDicionario T)


{ unsigned int i = 0; unsigned int Inicial ;
I n i c i a l = h(Ch, p) ;
while ( strcmp(T[ ( I n i c i a l + i ) % M] .Chave, VAZIO) != 0 &&
strcmp (T[ ( I n i c i a l + i ) % M] .Chave, Ch) != 0 && i < M)
i ++;
i f ( strcmp ( T[ ( I n i c i a l + i ) % M] .Chave, Ch) == 0)
return ( ( I n i c i a l + i ) % M) ;
else return M; /∗ Pesquisa sem sucesso ∗/
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.3 89

Operações do Dicionário Usando Endereçamento Aberto


void Insere (TipoItem x , TipoPesos p, TipoDicionario T)
{ unsigned int i = 0; unsigned int I n i c i a l ;
i f ( Pesquisa(x .Chave,p,T) < M) { p r i n t f ( "Elemento ja esta presente \n" ) ; return ; }
I n i c i a l = h(x .Chave, p) ;
while ( strcmp(T[ ( I n i c i a l + i ) % M] .Chave, VAZIO) != 0 &&
strcmp(T[ ( I n i c i a l + i ) % M] .Chave, RETIRADO) != 0 && i < M) i ++;
i f ( i < M)
{ strcpy (T[ ( I n i c i a l + i ) % M] .Chave, x .Chave) ;
/∗ Copiar os demais campos de x , se existirem ∗/ }
else p r i n t f ( " Tabela cheia \n" ) ;
}
void Retira (TipoChave Ch, TipoPesos p, TipoDicionario T)
{ TipoIndice i ;
i = Pesquisa(Ch, p, T) ;
i f ( i < M)
memcpy(T[ i ] .Chave, RETIRADO , N) ;
else p r i n t f ( "Registro nao esta presente \n" ) ;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.3 90

Análise
• Seja α = N/M o fator de carga da tabela. Conforme demonstrado por
Knuth (1973), o custo de uma pesquisa com sucesso é
 
1 1
C(n) = 1+ ·
2 1−α

• O hashing linear sofre de um mal chamado agrupamento(clustering)


(Knuth, 1973, pp.520–521).
• Esse fenômeno ocorre na medida em que a tabela começa a ficar
cheia, pois a inserção de uma nova chave tende a ocupar uma posição
na tabela que esteja contígua a outras posições já ocupadas, o que
deteriora o tempo necessário para novas pesquisas.
• Entretanto, apesar do hashing linear ser um método relativamente
pobre para resolver colisões os resultados apresentados são bons.
• O melhor caso, assim como o caso médio, é O(1).
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.3 91

Vantagens e Desvantagens de Transformação da Chave

Vantagens:

• Alta eficiência no custo de pesquisa, que é O(1) para o caso médio.

• Simplicidade de implementação.

Desvantagens:

• Custo para recuperar os registros na ordem lexicográfica das chaves é


alto, sendo necessário ordenar o arquivo.

• Pior caso é O(N ).


Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 92

Hashing Perfeito com Ordem Preservada

• Se h(xi ) = h(xj ) se e somente se i = j, então não há colisões, e a


função de transformação é chamada de função de transformação
perfeita ou função hashing perfeita(hp).

• Se o número de chaves N e o tamanho da tabela M são iguais


(α = N/M = 1), então temos uma função de transformação perfeita
mínima.

• Se xi ≤ xj e hp(xi ) ≤ hp(xj ), então a ordem lexicográfica é


preservada. Nesse caso, temos uma função de transformação
perfeita mínima com ordem preservada.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 93

Vantagens e Desvantagens de Uma Função de


Transformação Perfeita Mínima
• Nas aplicações em que necessitamos apenas recuperar o registro com
informação relacionada com a chave e a pesquisa é sempre com
sucesso, não há necessidade de armazenar a chave, pois o registro é
localizado sempre a partir do resultado da função de transformação.
• Não existem colisões e não existe desperdício de espaço pois todas as
entradas da tabela são ocupadas. Uma vez que colisões não ocorrem,
cada chave pode ser recuperada da tabela com um único acesso.
• Uma função de transformação perfeita é específica para um conjunto
de chaves conhecido. Em outras palavras, ela não pode ser uma
função genérica e tem de ser pré-calculada.
• A desvantagem no caso é o espaço ocupado para descrever a função
de transformação hp.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 94

Algoritmo de Czech, Havas e Majewski

• Czech, Havas e Majewski (1992, 1997) propõem um método elegante


baseado em grafos randômicos para obter uma função de
transformação perfeita com ordem preservada.

• A função de transformação é do tipo:

hp(x) = (g[h0 (x)] + g[h1 (x)] + . . . + g[hr−1 (x)]) mod N,

na qual h0 (x), h1 (x), . . . , hr−1 (x) são r funções não perfeitas descritas
pelos programas dos slides 77 ou 79, x é a chave de busca, e g um
arranjo especial que mapeia números no intervalo 0 . . . M − 1 para o
intervalo 0 . . . N − 1.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 95

Problema Resolvido Pelo Algoritmo

• Um hipergrafo ou r−grafo é um grafo não direcionado no qual cada


aresta conecta r vértices.

• Dado um hipergrafo não direcionado acíclico Gr = (V, A), onde


|V | = M e |A| = N , encontre uma atribuição de valores aos vértices de
Gr tal que a soma dos valores associados aos vértices de cada aresta
tomado módulo N é um número único no intervalo [0, N − 1].

• A questão principal é como obter uma função g adequada. A


abordagem mostrada a seguir é baseada em hipergrafos acíclicos
randômicos.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 96

Exemplo (Obs.: Existe Erro na Tab.5.3(a), pag.205 do livro)

Chave x h0 (x) h1 (x) hp(x) • Chaves: 12 meses do ano abre-


jan 11 14 0 viados para os três primeiros ca-
fev 14 2 1 racteres.
mar 0 10 2
• Vamos utilizar um hipergrafo ací-
abr 8 7 3
clico com r = 2 (ou 2-grafo), onde
mai 4 12 4
cada aresta conecta 2 vértices.
jun 14 6 5
• Usa duas funções de transforma-
jul 1 7 6
ção universais h0 (x) e h1 (x).
ago 12 10 7
set 11 4 8 • Objetivo: obter uma função de
out 8 13 9 transformação perfeita hp de tal
nov 3 4 10 forma que o i-ésimo mês é man-
dez 1 5 11 tido na (i − 1)-ésima posição da
tabela hash.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 97

Grafo Acíclico Randômico Gerado


• O problema de obter a função g é equiva-
0 lente a encontrar um hipergrafo acíclico
14 1
13 1
2 contendo M vértices e N arestas.
0
5 6
12 2 11
3 • Os vértices são rotulados com valores no
10
9 4 intervalo 0 . . . M − 1
11 7 4
8 • Arestas são definidas por (h1 (x), h2 (x))
10 5
para cada uma das N chaves x.
9 3 6
8 7 • Cada chave corresponde a uma aresta
que é rotulada com o valor desejado para
a função hp perfeita.
• Os valores das duas funções h1 (x) e
h2 (x) definem os vértices sobre os quais
a aresta é incidente.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 98

Obtenção da Função g a Partir do Grafo Acíclico

Passo importante: conseguir um arranjo g de vértices para inteiros no


intervalo 0 . . . N − 1 tal que, para cada aresta (h0 (x), h1 (x)), o valor de
hp(x) = g(h0 (x)) + g(h1 (x))) mod N seja igual ao rótulo da aresta.

• O primeiro passo é obter um hipergrafo randômico e verificar se ele é


acíclico.

• O Programa 7.10 do Capítulo 7 do livro para verificar se um hipergrafo


é acíclico é baseado no fato de que um r-grafo é acíclico se e somente
se a remoção repetida de arestas contendo vértices de grau 1 elimina
todas as arestas do grafo.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 99

Obtenção da Função g a Partir do Grafo Acíclico


Algoritmo:
1. O Programa 7.10 retorna os índices das arestas retiradas no arranjo
L = (2, 1, 10, 11, 5, 9, 7, 6, 0, 3, 4, 8). O arranjo L indica a ordem de
retirada das arestas.
2. As arestas do arranjo L devem ser consideradas da direita para a
esquerda, condição suficiente para ter sucesso na criação do arranjo g.
3. O arranjo g é iniciado com −1 em todas as entradas.
4. A aresta a = (4, 11) de índice ia = 8 é a primeira a ser processada.
Como inicialmente g[4] = g[11] = −1, fazemos g[11] = N e
g[4] = ia − g[11] mod N = 8 − 12 mod 12 = 8.
5. Para a próxima aresta a = (4, 12) de índice ia = 4, como g[4] = 8,
temos que g[12] = ia − g[4] mod N = 4 − 8 mod 12 = 8, e assim
sucessivamente até a última aresta de L.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 100

Algoritmo para Obter g no Exemplo dos 12 Meses

0 Chave x h0 (x) h1 (x) hp(x)


14 1
jan 11 14 0
13 2 fev 14 2 1
1
0 mar 0 10 2
5 6 3
12 2 11 abr 8 7 3
10 mai 4 12 4
7 9 4 4 jun 14 6 5
11
jul 1 7 6
8
10 5 ago 12 10 7
set 11 4 8
9 3 6 out 8 13 9
8 7 nov 3 4 10
dez 1 5 11

v 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
g[v] 3 3 1 2 8 8 5 3 12 -1 11 12 8 9 0
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 101

Rotula Grafo e Atribui Valores para o Arranjo g


void Atribuig ( TipoGrafo ∗Grafo,
TipoArranjoArestas L,
Tipog g)
{ int i , u, Soma; TipoValorVertice v ; TipoAresta a;
for ( i = Grafo−>NumVertices − 1; i >= 0; i −−) g[ i ] = INDEFINIDO ;
for ( i = Grafo−>NumArestas − 1; i >= 0; i−−)
{ a = L[ i ] ; Soma = 0;
for ( v = Grafo−>r − 1; v >= 0; v−−)
{ i f (g[a. Vertices [ v]] == INDEFINIDO ) { u = a. Vertices [ v ] ; g[u] = Grafo−>NumArestas; }
else Soma += g[a. Vertices [ v ] ] ;
}
g[u] = a.Peso − Soma;
i f (g[u] < 0) g[u] = g[u]+(Grafo−>r−1)∗Grafo−>NumArestas;
}
}

• Todas as entradas do arranjo g são feitas igual a Indefinido = −1.


• Atribua o valor N para g[vj+1 ], . . . , g[vr−1 ] que ainda estão indefinidos e
P
faça g[vj ] = (ia − vi ∈a∧g[vi ]6=−1 g[vi ]) mod N .
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 102

Programa para Obter Função de Transformação Perfeita

int main( ) • Gera hipergrafos randômicos


{ Ler um conjunto de N chaves; iterativamente e testa se o
Escolha um valor para M ; grafo gerado é acíclico.
do
{ Gera os pesos p1 [i] e p2 [i]
• Cada iteração gera novas
para 1 ≤ i ≤ MAXTAMCHAVE funções h0 , h1 , . . . , hr−1 até
Gera o grafo G = (V, A) ; que um grafo acíclico seja
Atribuig (G,g, GrafoRotulavel ) ; obtido.
} while ( ! GrafoRotulavel ) ; • A função de transformação
Retorna p1 [i] e p2 [i] e g ; perfeita é determinada pelos
}
pesos p0 , p1 , . . . , pr−1 , e pelo
arranjo g.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 103

Estruturas de dados (1)


#define MAXNUMVERTICES 100000 /∗−−No. maximo de vertices−−∗/
#define MAXNUMARESTAS 100000 /∗−−No. maximo de arestas−−∗/
#define MAXR 5
#define MAXTAMPROX MAXR∗MAXNUMARESTAS
#define MAXTAM 1000 /∗−−Usado Fila−−∗/
#define MAXTAMCHAVE 6 /∗−−No. maximo de caracteres da chave−−∗/
#define MAXNUMCHAVES 100000 /∗−−No. maximo de chaves lidas−−∗/
#define INDEFINIDO −1

typedef int TipoValorVertice ;


typedef int TipoValorAresta ;
typedef int Tipor ;
typedef int TipoMaxTamProx;
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 104

Estruturas de dados (2)


typedef int TipoPesoAresta;
typedef TipoValorVertice TipoArranjoVertices [ MAXR ] ;
typedef struct TipoAresta {
TipoArranjoVertices Vertices ;
TipoPesoAresta Peso;
} TipoAresta ;
typedef TipoAresta TipoArranjoArestas [ MAXNUMARESTAS ] ;
typedef struct TipoGrafo {
TipoArranjoArestas Arestas ;
TipoValorVertice Prim[ MAXNUMVERTICES ] ;
TipoMaxTamProx Prox[ MAXTAMPROX ] ;
TipoMaxTamProx ProxDisponivel ;
TipoValorVertice NumVertices;
TipoValorAresta NumArestas;
Tipor r ;
} TipoGrafo ;
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 105

Estruturas de dados (3)


typedef int TipoApontador;
typedef struct {
TipoValorVertice Chave;
/∗ outros componentes ∗/
} TipoItem ;
typedef struct {
TipoItem Item [MAXTAM + 1];
TipoApontador Frente , Tras;
} TipoFila ;
typedef int TipoPesos[ MAXTAMCHAVE ] ;
typedef TipoPesos TipoTodosPesos[ MAXR ] ;
typedef int Tipog[ MAXNUMVERTICES ] ;
typedef char TipoChave[ MAXTAMCHAVE ] ;
typedef TipoChave TipoConjChaves[ MAXNUMCHAVES ] ;
typedef TipoValorVertice TipoIndice ;
static TipoValorVertice M;
static TipoValorAresta N;
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 106

Gera um Grafo sem Arestas Repetidas e sem Self-Loops


void GeraGrafo (TipoConjChaves ConjChaves,
TipoValorAresta N,
TipoValorVertice M,
Tipor r,
TipoTodosPesos Pesos,
int ∗NGrafosGerados,
TipoGrafo ∗Grafo)
{ /∗ Gera um grafo sem arestas repetidas e sem self−loops ∗/
int i , j ; TipoAresta Aresta ; int GrafoValido ;

inline int VerticesIguais ( TipoAresta ∗Aresta)


{ int i , j ;
for ( i = 0; i < Grafo−>r − 1; i ++)
{ for ( j = i + 1; j < Grafo−>r ; j ++)
{ i f ( Aresta−>Vertices [ i ] == Aresta−>Vertices [ j ] )
return TRUE ;
}
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 107

Gera um Grafo sem Arestas Repetidas e sem Self-Loops


do
{ GrafoValido = TRUE ; Grafo−>NumVertices = M;
Grafo−>NumArestas = N; Grafo−>r = r ;
FGVazio ( Grafo ) ; ∗NGrafosGerados = 0;
for ( j = 0; j < Grafo−>r ; j ++) GeraPesos (Pesos[ j ] ) ;
for ( i = 0; i < Grafo−>NumArestas; i ++)
{ Aresta .Peso = i ;
for ( j = 0; j < Grafo−>r ; j ++)
Aresta . Vertices [ j ] = h (ConjChaves[ i ] , Pesos[ j ] ) ;
i f ( VerticesIguais (&Aresta ) | | ExisteAresta (&Aresta , Grafo) )
{ GrafoValido = FALSE ; break ; }
else InsereAresta (&Aresta , Grafo ) ;
}
++(∗NGrafosGerados) ;
} while ( ! GrafoValido ) ;
} /∗ Fim GeraGrafo ∗/
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 108

Programa Principal para Gerar Arranjo g (1)


{−−−−Entram aqui as estruturas de dados dos slides 103, 104, 105 −−}
{−−−−Entram aqui os operadores do Programa 3.18 −−}
{−−−−Entram aqui os operadores do slide 77 −−}
{−−−−Entram aqui os operadores do Programa 7.26 −−}
{−−−−Entram aqui VerticeGrauUm e GrafoAciclico do Programa 7.10 −−}
int main( ) {
Tipor r ; TipoGrafo Grafo ; TipoArranjoArestas L ; short GAciclico ;
Tipog g ; TipoTodosPesos Pesos; int i , j ; int NGrafosGerados;
TipoConjChaves ConjChaves; FILE ∗ArqEntrada;
FILE ∗ArqSaida ; char NomeArq[100];
p r i n t f ( "Nome do arquivo com chaves a serem lidas : " ) ;
scanf( "%s∗[^\n] " , NomeArq) ; p r i n t f ( "NomeArq = %s \n" , NomeArq) ;
ArqEntrada = fopen(NomeArq, " r " ) ;
p r i n t f ( "Nome do arquivo para gravar experimento : " ) ;
scanf( "%s∗[^\n] " , NomeArq) ; p r i n t f ( "NomeArq = %s \n" , NomeArq) ;
ArqSaida = fopen(NomeArq, "w" ) ; NGrafosGerados = 0; i = 0;
fscanf (ArqEntrada , "%d %d %d∗[^\n] " , &N, &M, & r ) ;
Ignore(ArqEntrada , ’ \n ’ ) ; p r i n t f ( "N=%d, M=%d, r=%d\n" , N, M, r ) ;
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 109

Programa Principal para Gerar Arranjo g (2)


while ( ( i < N) && (! feof (ArqEntrada ) ) )
{ fscanf (ArqEntrada, "%s∗[^\n] " , ConjChaves[ i ] ) ;
Ignore(ArqEntrada , ’ \n ’ ) ; p r i n t f ( "Chave[%d]=%s \n" , i , ConjChaves[ i ] ) ;
i ++;
}
i f ( i ! = N)
{ p r i n t f ( "Erro : entrada com menos do que ’ , N, ’ elementos . \ n" ) ;
exit (−1);
}
do{ GeraGrafo (ConjChaves, N, M, r , Pesos, &NGrafosGerados, &Grafo ) ;
ImprimeGrafo (&Grafo ) ; /∗−−Imprime estrutura de dados−−∗/
p r i n t f ( "prim : " ) ;
for ( i = 0; i < Grafo.NumVertices ; i ++) p r i n t f ( "%3d " , Grafo.Prim[ i ] ) ;
p r i n t f ( " \n" ) ; p r i n t f ( "prox : " ) ;
for ( i = 0; i < Grafo.NumArestas ∗ Grafo. r ; i ++)
p r i n t f ( "%3d " , Grafo.Prox[ i ] ) ;
p r i n t f ( " \n" ) ; GrafoAciclico (&Grafo , L, &GAciclico ) ;
} while ( ! GAciclico ) ;
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 110

Programa Principal para Gerar Arranjo g (3)


p r i n t f ( "Grafo aciclico com arestas retiradas : " ) ;
for ( i = 0; i < Grafo.NumArestas; i ++) p r i n t f ( "%3d " , L[ i ] .Peso) ;
p r i n t f ( " \n" ) ;
Atribuig (&Grafo , L, g) ;
f p r i n t f (ArqSaida , "%d (N) \n" , N) ;
f p r i n t f (ArqSaida , "%d (M) \n" , M) ;
f p r i n t f (ArqSaida , "%d ( r ) \n" , r ) ;
for ( j = 0; j < Grafo. r ; j ++)
{ for ( i = 0; i < MAXTAMCHAVE ; i ++)
f p r i n t f (ArqSaida , "%d " , Pesos[ j ] [ i ] ) ;
f p r i n t f (ArqSaida , " (p%d) \n" , j ) ;
}
for ( i = 0; i < M; i ++) f p r i n t f (ArqSaida , "%d " , g[ i ] ) ;
f p r i n t f (ArqSaida , " (g) \n" ) ;
f p r i n t f (ArqSaida , "No. grafos gerados por GeraGrafo:%d\n" ,
NGrafosGerados) ;
fclose ( ArqSaida ) ; fclose ( ArqEntrada ) ; return 0;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 111

Função de Transformação Perfeita


TipoIndice hp (TipoChave Chave,
Tipor r ,
TipoTodosPesos Pesos,
Tipog g)
{ int i , v ;
v = 0;
for ( i = 0; i < r ; i ++) v += g[h(Chave, Pesos[ i ] ) ] ;
return ( v % N) ;
} /∗ hp ∗/
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 112

Teste para a Função de Transformação Perfeita (1)


#define MAXNUMVERTICES 100000 /∗−−No. maximo de vertices−−∗/
#define MAXNUMARESTAS 100000 /∗−−No. maximo de arestas−−∗/
#define MAXR 5
#define MAXTAMCHAVE 6 /∗−−No. maximo de caracteres da chave−−∗/
#define MAXNUMCHAVES 100000 /∗−−No. maximo de chaves lidas−−∗/
typedef int TipoValorVertice ;
typedef int TipoValorAresta ;
typedef int Tipor ;
typedef int TipoPesos[ MAXTAMCHAVE ] ;
typedef TipoPesos TipoTodosPesos[ MAXR ] ;
typedef int Tipog[ MAXNUMVERTICES ] ;
typedef char TipoChave[ MAXTAMCHAVE ] ;
typedef TipoChave TipoConjChaves[ MAXNUMCHAVES ] ;
typedef TipoValorVertice TipoIndice ;
static TipoValorVertice M;
static TipoValorAresta N;
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 113

Teste para a Função de Transformação Perfeita (2)


/∗ ∗ Entra aqui a funcao hash universal do slide 77 ∗ ∗/
/∗ ∗ Entra aqui a funcao hash perfeita do slide 111 ∗ ∗/
int main( )
{ Tipor r ; Tipog g ; TipoTodosPesos Pesos; int i , j ;
TipoConjChaves ConjChaves;
FILE ∗ArqChaves; FILE ∗ArqFHPM ;
char NomeArq[100]; TipoChave Chave;
inline short VerificaFHPM ( )
{ short TabelaHash[ MAXNUMVERTICES ] ;
int i , indiceFHPM ;
for ( i = 0; i < N; i ++) TabelaHash[ i ] = FALSE ;
for ( i = 0; i < N; i ++)
{ indiceFHPM = hp (ConjChaves[ i ] , r , Pesos, g) ;
i f ( (TabelaHash[ indiceFHPM ] ) | | ( indiceFHPM >= N) ) return FALSE ;
TabelaHash[ indiceFHPM ] = TRUE ;
}
return TRUE ;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 114

Teste para a Função de Transformação Perfeita (3)


p r i n t f ( "Nome do arquivo com chaves a serem lidas : " ) ;
scanf( "%s∗[^\n] " , NomeArq) ;
p r i n t f ( "NomeArq = %s \n" , NomeArq) ;
ArqChaves = fopen(NomeArq, " r " ) ;
fscanf (ArqChaves, "%d %d %d∗[^\n] " , &N, &M, & r ) ;
Ignore(ArqChaves, ’ \n ’ ) ;
p r i n t f ( "N=%d, M=%d, r=%d\n" , N, M, r ) ;
i = 0;
while ( ( i < N) && (! feof (ArqChaves) ) )
{ fscanf (ArqChaves, "%s∗[^\n] " , ConjChaves[ i ] ) ;
Ignore(ArqChaves, ’ \n ’ ) ;
p r i n t f ( "Chave[%d]=%s \n" , i , ConjChaves[ i ] ) ;
i ++;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 115

Teste para a Função de Transformação Perfeita (4)


i f ( i ! = N)
{ p r i n t f ( "Erro : entrada com menos do que ’ , N, ’ elementos . \ n" ) ;
exit (−1);
}
p r i n t f ( "Nome do arquivo com a funcao hash perfeita : " ) ;
scanf( "%s∗[^\n] " , NomeArq) ;
p r i n t f ( "NomeArq = %s \n" , NomeArq) ;
ArqFHPM = fopen(NomeArq, "rb" ) ;
fscanf (ArqFHPM , "%d∗[^\n] " , &N) ; Ignore(ArqFHPM , ’ \n ’ ) ;
fscanf (ArqFHPM , "%d∗[^\n] " , &M) ; Ignore(ArqFHPM , ’ \n ’ ) ;
fscanf (ArqFHPM , "%d∗[^\n] " , & r ) ; Ignore(ArqFHPM , ’ \n ’ ) ;
p r i n t f ( "N=%d, M=%d, r=%d\n" , N, M, r ) ;
for ( j = 0; j < r ; j ++)
{ for ( i = 0; i < MAXTAMCHAVE ; i ++)
fscanf (ArqFHPM , "%d∗[^%d\n] " , &Pesos[ j ] [ i ] ) ;
Ignore(ArqFHPM , ’ \n ’ ) ;
p r i n t f ( " \n" ) ;
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 116

Teste para a Função de Transformação Perfeita (5)


for ( i = 0; i < MAXTAMCHAVE ; i ++)
p r i n t f ( "%d " , Pesos[ j ] [ i ] ) ;
printf ( " (p%d) \n" , j ) ;
}
for ( i = 0; i < M; i ++)
fscanf (ArqFHPM , "%d∗[%d\n] " , &g[ i ] ) ;
Ignore(ArqFHPM , ’ \n ’ ) ;
for ( i = 0; i < M; i ++) p r i n t f ( "%d " , g[ i ] ) ;
printf ( " (g) \n" ) ;
i f ( VerificaFHPM ( ) )
p r i n t f ( "FHPM f o i gerada com sucesso\n" ) ;
else p r i n t f ( "FHPM nao f o i gerada corretamente \n" ) ;
p r i n t f ( "Chave: " ) ;
scanf( "%s∗[^\n] " , Chave) ;
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 117

Teste para a Função de Transformação Perfeita (6)

while ( strcmp(Chave, "aaaaaa" ) != 0)


{ p r i n t f ( "FHPM: %d\n" , hp(Chave, r , Pesos, g) ) ;
p r i n t f ( "Chave: " ) ;
scanf( "%s∗[^\n] " , Chave) ;
}
fclose (ArqChaves) ;
fclose ( ArqFHPM ) ;
return 0;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 118

Análise

• A questão crucial é: quantas interações são necessárias para se


obter um hipergrafo Gr = (V, A) que seja acíclico?

• A resposta a esta questão depende dos valores de r e M escolhidos


no primeiro passo do algoritmo.

• Quanto maior o valor de M , mais esparso é o grafo e,


consequentemente, mais provável que ele seja acíclico.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 119

Análise: Influência do Valor de r


• Segundo Czech, Havas e Majewski (1992, 1997), quando M = cN ,
c > 2 e r = 2, a probabilidade Pra de gerar aleatoriamente um 2-grafo
acíclico G2 = (V, A), para N → ∞, é:
r
1 c−2
Pr a = e c ·
c
• Quando c = 2, 09 temos que Pra = 0, 33. Logo, o número esperado de
iterações para gerar um 2-grafo acíclico é 1/Pra = 1/0, 33 ≈ 3.
• Logo, aproximadamente 3 grafos serão testados em média.
• O custo para gerar cada grafo é linear no número de arestas do grafo.
• O procedimento GrafoAciclico para verificar se um hipergrafo é acíclico
tem complexidade O(|V | + |A|).
• Logo, a complexidade de tempo para gerar a função de transformação
é proporcional ao número de chaves N , desde que M > 2N .
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 120

Análise: Influência do Valor de r


• O grande inconveniente de usar M = 2, 09N é o espaço necessário
para armazenar o arranjo g.
• Uma maneira de aproximar o valor de M em direção ao valor de N é
usar 3-grafos, onde o valor de M pode ser tão baixo quanto 1, 23N .
• Logo, o uso de 3-grafos reduz o custo de espaço da função, mas
requer o cômputo de mais uma função de transformação auxiliar h2 .
• O problema tem naturezas diferentes para r = 2 e r > 2:
– Para r = 2, a probabilidade Pra varia continuamente com c.
– Para r > 2, se c ≤ c(r), então Pra tende para 0 quando N tende
para ∞; se c > c(r), então Pra tende para 1.
– Logo, um 3-grafo é obtido em média na primeira tentativa quando
c ≥ 1, 23.
• Obtido o hipergrafo, o procedimento Atribuig é determinístico e requer
um número linear de passos.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.4 121

Análise: Espaço Ocupado para Descrever a Função

• O número de bits por chave para descrever a função é uma medida de


complexidade de espaço importante.

• Como cada entrada do arranjo g usa log N bits, a complexidade de


espaço do algoritmo é O(log N ) bits por chave, que é o espaço para
descrever a função.

• De acordo com Majewski, Wormald, Havas e Czech (1996), o limite


inferior para descrever uma função perfeita com ordem preservada é
Ω(log N ) bits por chave, o que significa que o algoritmo que acabamos
de ver é ótimo para essa classe de problemas.

• Na próxima seção vamos apresentar um algoritmo de hashing perfeito


sem ordem preservada que reduz o espaço ocupado pela função de
transformação de O(log N ) para O(1).
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 122

Hashing Perfeito Usando Espaço Quase Ótimo


• Algoritmo proposto por Botelho (2008): obtem função hash perfeita
com número constante de bits por chave para descrever a função.
• O algoritmo gera a função em tempo linear e a avaliação da função é
realizada em tempo constante.
• Primeiro algoritmo prático descrito na literatura que utiliza O(1) bits por
chave para uma função hash perfeita mínima sem ordem preservada.
• Os métodos conhecidos anteriormente ou são empíricos e sem
garantia de que funcionam bem para qualquer conjunto de chaves, ou
são teóricos e sem possibilidade de implementação prática.
• O algoritmo utiliza hipergrafos ou r−grafos randômicos r-partidos.
Isso permite que r partes do vetor g sejam acessadas em paralelo.
• As funções mais rápidas e mais compactas são obtidas para
hipergrafos com r = 3.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 123

Hashing Perfeito Usando Espaço Quase Ótimo

111
000
(a) (b) g fhp
S 00
11 0 1 h 0 (x)

0110 000
111
00
11 0 0 0 mar
1
0 jan
00
11 0 jan

000
111
1 1
00
11
00
11 00
11 2 3 2 3
fev Geração
0
1
00
11
2 3 h 1 (x) Atribuição
0
1
00
11
3 3 3 3
0
1
mar 2 fev
4 4
0
1
00
11
3 3
0
1 5 5

00
11 0
1
4 5 h 2 (x)

0
1
00 1
11
{1,2,4} {1,3,5} {0,2,5}
0 fev jan mar L
0 1 2

(a) Para S = {jan, fev, mar}, gera um 3-grafo 3-partido acíclico com M = 6
vértices e N = 3 arestas e um arranjo de arestas L obtido no momento
de verificar se o hipergrafo é acíclico.
(b) Constrói função hash perfeita que transforma o conjunto S de chaves
para o intervalo [0, 5], representada pelo arranjo g : [0, 5] → [0, 3] de
forma a atribuir univocamente uma aresta a um vértice.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 124

Hashing Perfeito Usando Espaço Quase Ótimo

No passo (a) de geração do hipergrafo:

• Utiliza três funções h0 , h1 and h2 , com intervalos {0, 1}, {2, 3} e {4, 5},
respectivamente, cujos intervalos não se sobrepõem e por isso o grafo
é 3-partido.

• Funções constroem um mapeamento do conjunto S de chaves para o


conjunto A de arestas de um r-grafo acíclico Gr = (V, A), onde r = 3,
|V | = M = 6 e |E| = N = 3.

• No exemplo, “jan” é rótulo da aresta


{h0 (“jan”), h1 (“jan”), h2 (“jan”)} = {1, 3, 5}, “fev” é rótulo da aresta
{h0 (“fev”), h1 (“fev”), h2 (“fev”)} = {1, 2, 4}, e “mar” é rótulo da aresta
{h0 (“mar”), h1 (“mar”), h2 (“mar”)} = {0, 2, 5}.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 125

Hashing Perfeito Usando Espaço Quase Ótimo

Ainda no passo (a) de geração do hipergrafo:

• Testa se o hipergrafo randômico resultante contém ciclos por meio da


retirada iterativa de arestas de grau 1, conforme mostrado no
Programa 7.10.

• As arestas retiradas são armazenadas em L na ordem em que foram


retiradas.

• A primeira aresta retirada foi {1, 2, 4}, a segunda foi {1, 3, 5} e a


terceira foi {0, 2, 5}. Se terminar com um grafo vazio, então o grafo é
acíclico, senão um novo conjunto de funções h0 , h1 and h2 é escolhido
e uma nova tentativa é realizada.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 126

Hashing Perfeito Usando Espaço Quase Ótimo

No passo (b) de atribuição:

• Produz uma função hash perfeita que transforma o conjunto S de


chaves para o intervalo [0, M − 1], sendo representada pelo arranjo g
que armazena valores no intervalo [0, 3].

• O arranjo g permite selecionar um de três vértices de uma dada


aresta, o qual é associado a uma chave k.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 127

Hashing Perfeito Usando Espaço Quase Ótimo

• O programa no slide seguinte mostra o procedimento para obter o


arranjo g considerando um hipergrafo Gr = (V, A).

• As estruturas de dados são as mesmas dos slides 103, 104 e 105.

• Para valores 0 ≤ i ≤ M − 1, o passo começa com g[i] = r para marcar


cada vértice como não atribuído e com Visitado[i] = false para marcar
cada vértice como não visitado.

• Seja j, 0 ≤ j < r, o índice de cada vértice u de uma aresta a.

• A seguir, para cada aresta a ∈ L da direita para a esquerda, percorre


os vértices de a procurando por vértices u em a não visitados, faz
Visitado[u] = true e para o último vértice u não visitado faz
P
g[u] = (j − v∈a∧Visitado[v]=true g[v]) mod r.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 128

Rotula Grafo e Atribui Valores para o Arranjo g


void Atribuig ( TipoGrafo ∗Grafo , TipoArranjoArestas L, Tipog g)
{ int i , j , u, Soma; TipoValorVertice v ; TipoAresta a;
unsigned char Visitado [ MAXNUMVERTICES ] ;
for ( i = Grafo−>NumVertices − 1; i >= 0; i−−)
{ g[ i ] = Grafo−>r ; Visitado [ i ] = FALSE ; }
for ( i = Grafo−>NumArestas − 1; i >= 0; i−−)
{ a = L[ i ] ; Soma = 0;
for ( v = Grafo−>r − 1; v >= 0; v−−)
{ i f ( ! Visitado [a. Vertices [ v ] ] )
{ Visitado [a. Vertices [ v ] ] = TRUE ;
u = a. Vertices [ v ] ; j = v ;
}
else Soma += g[a. Vertices [ v ] ] ;
}
g[u] = ( j − Soma) % Grafo−>r ;
while (g[u] < 0) g[u] += Grafo−>r ;
}
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 129

Valor das Variáveis na Execução do Programa

i a v Visitado u j Soma
2 {0, 2, 5} 2 False → True 5 2 0
1 False → True 2 1 0
0 False → True 0 0 0
1 {1, 3, 5} 2 True - - 3
1 False → True 3 1 3
0 False → True 1 0 3
0 {1, 2, 4} 2 False → True 4 2 0
1 True 4 2 0
0 True 4 2 3

• No exemplo, a primeira aresta considerada em L é


a = {h0 (“mar”), h1 (“mar”), h2 (“mar”)} = {0, 2, 5}. A Tabela mostra os
valores das varáveis envolvidas no comando:
for v := Grafo.r - 1 downto 0 do
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 130

Valor das Variáveis na Execução do Programa

• O comando após o anel:


g[u] := (j - Soma) mod Grafo.r;
faz g[0] = (0 − 0) mod 3 = 0.

• Igualmente, para a aresta seguinte de L que é


a = {h0 (“jan”), h1 (“jan”), h2 (“jan”)} = {1, 3, 5}, o comando após o anel
faz g[1] = (0 − 3) mod 3 = −3.

• O comando a seguir:
while g[u] < 0 do g[u] := g[u] + Grafo.r;
irá fazer g[1] = g[1] + 3 = −3 + 3 = 0.

• Finalmente, para a última aresta em L que é


a = {h0 (“fev”), h1 (“fev”), h2 (“fev”)} = {1, 2, 4}, o comando após o anel
faz g[4] = (2 − 3) mod 3 = −1. faz g[4] = g[4] + 3 = −1 + 3 = 2.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 131

Atribui Valores para g Usa Apenas 1 Byte por Entrada

• Como somente um dos quatro valores 0, 1, 2, ou 3 é armazenado em


cada entrada de g, 2 bits são necessários.

• Na estrutura de dados do slide 105 o tipo do arranjo g é integer.

• Agora o comando
Tipog = array[0..MAXNUMVERTICES] of integer;
muda para
Tipog = array[0..MAXNUMVERTICES] of byte;
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 132

Obtem a Função Hash Perfeita


• A partir do arranjo g podemos obter uma função hash perfeita para
uma tabela com intervalo [0, M − 1].
• Para uma chave k ∈ S a função hp tem a seguinte forma:

hp(k) = hi(k) (k), onde i(k) = (g[h0 (k)] + g[h1 (k)] + . . . + g[hr−1 (k)]) mod r

• Considerando r = 3, o vértice escolhido para uma chave k é obtido por


uma das três funções, isto é, h0 (k), h1 (k) ou h2 (k).
• Logo, a decisão sobre qual função hi (k) deve ser usada para uma
chave k é obtida pelo cálculo
i(k) = (g[h0 (k)] + g[h1 (k)] + g[h2 (k)]) mod 3.
• No exemplo da Figura, a chave “jan” está na posição 1 da tabela
porque (g[1] + g[3] + g[5]) mod 3 = 0 e h0 (“jan”) = 1. De forma similar, a
chave “fev” está na posição 4 da tabela porque
(g[1] + g[2] + g[4]) mod 3 = 2 e h2 (“fev”) = 4, e assim por diante.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 133

Obtem a Função Hash Perfeita


TipoIndice hp (TipoChave Chave,
Tipor r ,
TipoTodosPesos Pesos,
Tipog g)
{ int i , v = 0; TipoArranjoVertices a;
for ( i = 0; i < r ; i ++)
{ a[ i ] = h(Chave, Pesos[ i ] ) ;
v += g[a[ i ] ] ;
}
v = v% r; return a[ v ] ;
}

• O procedimento recebe a chave, o valor de r, os pesos para a função


h do Programa 3.18 e o arranjo g, e segue a equação do slide 132
para descobrir qual foi o vértice da aresta escolhido para a chave.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 134

Como Empacotar Quatro Valores de g em um Byte

• Para isso foram criados dois procedimentos:


– AtribuiValor2Bits: atribui o i−ésimo valor de g em uma das quatro
posições do byte apropriado.
– ObtemValor2Bits: retorna o i−ésimo valor de g.

• Agora o tipo do arranjo g permanece como byte, mas o comando


Tipog = array[0..MAXNUMVERTICES] of byte;
muda para
const MAXGSIZE = Trunc((MAXNUMVERTICES + 3)/4)
Tipog = array[0..MAXGSIZE] of byte;
onde MAXGSIZE indica que o arranjo Tipog ocupa um quarto do
espaço e o byte passa a armazenar 4 valores.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 135

Como Empacotar Quatro Valores de g em um Byte


/∗ Assume que todas as entradas de 2 bits do vetor ∗/
/∗ g foram inicializadas com o valor 3 ∗/
void AtribuiValor2Bits ( Tipog ∗g, int Indice , unsigned char Valor)
{ int i , Pos; i = Indice / 4 ;
Pos = ( Indice % 4);
Pos = Pos ∗ 2 ; /∗ Cada valor ocupa 2 bits ∗/
g[ i ] &= ~(3U << Pos) ; /∗ zera os dois bits a atribuir ∗/
g[ i ] | = ( Valor << Pos) ; /∗ realiza a atribuicao ∗/
} /∗ AtribuiValor2Bits ∗/

char ObtemValor2Bits ( Tipog ∗g, int Indice )


{ int i , Pos;
i = Indice / 4 ;
Pos = ( Indice % 4);
Pos = Pos ∗ 2 ; /∗ Cada valor ocupa 2 bits ∗/
return (g[ i ] > > Pos) & 3U;
} /∗ ObtemValor2Bits ∗/
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 136

Como Empacotar Um Valor de g em Apenas 2 Bits


• Exemplo de “shl”: b0 , b1 , b2 , b3 , b4 , b5 , b6 , b7 shl 6 = b6 , b7 , 0, 0, 0, 0, 0, 0).
• Exemplo de “shr”: b0 , b1 , b2 , b3 , b4 , b5 , b6 , b7 shr 6 = 0, 0, 0, 0, 0, 0, b0 , b1 ).
• Na chamada do procedimento AtribuiValor2Bits, consideremos a
atribuição de Valor = 2 na posição Indice = 4 de g (no caso, g[4] = 2):
– No primeiro comando o byte que vai receber Valor = 2 = (10)2 é
determinado por i = Indice div 4 = 4 div 4 = 1 (segundo byte).
– Posição dentro do byte a seguir: Pos = Indice mod 4 = 4 mod 4 = 0
(os dois bits menos significatios do byte).
– A seguir, Pos = Pos ∗ 2 porque cada valor ocupa 2 bits do byte. A
seguir, not (3 shl Pos) = not ((00000011)2 shl 0) = (11111100)2 .
Logo, g[i] and (11111100)2 zera os 2 bits a atribuir.
– Finalmente, o comando g[i] or (Valor shl Pos) realiza a atribuição e
o byte fica como (XXXXXX10)2 , onde X representa 0 ou 1.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 137

Atribui Valores para g Usando 2 Bits por Entrada


void Atribuig ( TipoGrafo ∗Grafo , TipoArranjoArestas L, Tipog ∗g)
{ int i , j , u, Soma; TipoValorVertice v ; TipoAresta a;
unsigned int valorg2bits ; unsigned char Visitado [ MAXNUMVERTICES ] ;
i f ( Grafo−>r <= 3) /∗ valores de 2 bits requerem r <= 3 ∗/
{ for ( i = Grafo−>NumVertices − 1; i >= 0; i−−)
{ AtribuiValor2Bits (g, i , Grafo−>r ) ; Visitado [ i ] = FALSE ; }
for ( i = Grafo−>NumArestas − 1; i >= 0; i−−)
{ a = L[ i ] ; Soma = 0;
for ( v = Grafo−>r − 1; v >= 0; v−−)
{ i f ( ! Visitado [a. Vertices [ v ] ] )
{ Visitado [a. Vertices [ v ] ] = TRUE ; u = a. Vertices [ v ] ; j = v ; }
else Soma += ObtemValor2Bits(g, a. Vertices [ v ] ) ;
}
valorg2bits = ( j − Soma) % Grafo−>r ;
while ( valorg2bits > Grafo−>r ) valorg2bits += Grafo−>r ;
AtribuiValor2Bits ( g, u, valorg2bits ) ;
}
}
} /∗−−Fim Atribuig−−∗/
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 138

Função de Transformação Perfeita Usando 2 Bits

TipoIndice hp (TipoChave Chave,


Tipor r ,
TipoTodosPesos Pesos,
Tipog g) • Basta substituir no programa
{ int i , v = 0; TipoArranjoVertices a; do slide 133 o comando
for ( i = 0; i < r ; i ++) v := v + g[a[i]];
{ a[ i ] = h(Chave, Pesos[ i ] ) ; pelo comando
v += g[a[ i ] ] ; v := v + ObtemValor2Bits(g,
} a[i]);
v = v% r; return a[ v ] ;
}
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 139

Implementação da Função Rank


• Para obter uma função hash perfeita mínima precisamos reduzir o
intervalo da tabela de [0, M − 1] para [0, N − 1].
• Vamos utilizar uma estrutura de dados sucinta, acompanhada de um
algoritmo eficiente para a operação de pesquisa.
• rank: [0, M − 1] → [0, N − 1]: conta o número de posições atribuidas
antes de uma dada posição v em g em tempo constante.
• O passo de ranking constrói a estrutura de dados usada para
computar a função rank : [0, 5] → [0, 2] em tempo O(1). Por exemplo,
rank (4) = 2 porque os valores de g[0] e g[1] são diferentes de 3.

g fhp
0 0 0 mar fhpm
1 0 1 jan
0 mar
2 3 2 3
Ranking 1 jan
3 3 3 3 2 fev
4 2 4 fev
5 3 5 3
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 140

Implementação da Função Rank

Tr
• A função rank usa um
0 = (0000) 2 2 algoritmo proposto por
g 1 = (0001) 2
2
0 2 = (0010) 2 2 Pagh (2001).
0
0 TabRank 3 = (0011) 2 1
0 k=4 4 = (0100) 2 2
• Usa ǫ M bits adicionais,
1
0 0 0 5 = (0101) 2 2 0 < ǫ < 1, para armazenar
a) b)
1 2 1 6 = (0110) 2 2
2 o rank de cada k−ésimo
1 7 = (0111) 2 1
3
1 8 = (1000) 2 2 índice de g em TabRank,
1 9 = (1001) 2 2
1 10 = (1010) 2 2
onde k = ⌊log(M )/ǫ⌋.
4
0 11 = (1011) 2 1
1 12 = (1100) 2 1
• Para uma avaliação de
5
1 13 = (1101) 2 1 rank (u) em O(1), é neces-
14 = (1110) 2 1
15 = (1111) 2 0
sário usar uma tabela Tr
auxiliar.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 141

Implementação da Tabela T abRank

• TabRank armazena em cada entrada o número total de valores de 2


bits diferentes de r = 3 até cada k-ésima posição do arranjo g.

• No exemplo consideramos k = 4. Assim, existem 0 valores até a


posição 0 e 2 valores até a posição 4 de g.
void GeraTabRank ( Tipog ∗g, TipoValorVertice Tamg,
TipoK k , TipoTabRank ∗TabRank)
{ int i , Soma = 0;
for ( i = 0; i < Tamg; i ++)
{ i f ( i % k == 0) TabRank[ i / k ] = Soma;
i f ( ObtemValor2Bits(g, i ) ! = NAOATRIBUIDO ) Soma = Soma + 1;
}
} /∗ GeraTabRank ∗/
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 142

Implementação da Tabela Tr
• Para calcular o rank (u) usando as tabelas TabRank e Tr são
necessários dois passos:
– Obter o rank do maior índice precomputado v ≤ u em TabRank.
– Usar Tr para contar número de vértices atribuídos de v até u − 1.
• Na figura do slide 140 Tr possui 16 entradas necessárias para
armazenas todas as combinações possíveis de 4 bits.
• Por exemplo, a posição 0, cujo valor binário é (0000)2 , contém dois
valores diferentes de r = 3; na posição 3, cujo valor binário é (0011)2 ,
contém apenas um valor diferente de r = 3, e assim por diante.
• Cabe notar que cada valor de r ≥ 2 requer uma tabela Tr diferente.
• O procedimento a seguir considera que Tr é indexada por um número
de 8 bits e, portanto, MaxTrValue = 255. Além disso, no máximo 4
vértices podem ser empacotados em um byte, razão pela qual o anel
interno vai de 1 a 4.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 143

Implementação da Tabela Tr
void GeraTr ( TipoTr Tr)
{ int i , j , v , Soma = 0;
for ( i = 0; i <= MAXTRVALUE ; i ++)
{ Soma = 0; v = i ;
for ( j = 1; j <= 4; j ++)
{ i f ( ( v & 3) != NAOATRIBUIDO ) Soma = Soma + 1;
v = v >> 2;
}
Tr [ i ] = Soma;
}
} /∗ GeraTr ∗/
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 144

Função de Transformação Perfeita Usando 2 Bits

• A função hash perfeita mínima:

hpm(x) = rank (hp(x))

• Quanto maior for o valor de k mais compacta é a função hash perfeita


mínima resultante. Assim, os usuários podem permutar espaço por
tempo de avaliação variando o valor de k na implementação.
• Entretanto, o melhor é utilizar valores para k que sejam potências de
dois (por exemplo, k = 2bk para alguma constante bk ), o que permite
trocar as operações de multiplicação, divisão e módulo pelas
operações de deslocamento de bits à esquerda, à direita, e “and”
binário, respectivamente.
• O valor k = 256 produz funções compactas e o número de bits para
codificar k é bk = 8.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 145

Função de Transformação Perfeita Usando 2 Bits


TipoIndice hpm (TipoChave Chave, Tipor r , TipoTodosPesos Pesos, Tipog ∗ g,
TipoTr Tr , TipoK k , TipoTabRank ∗TabRank)
{ TipoIndice i , j , u, Rank, Byteg;
u = hp (Chave, r , Pesos, g) ;
j = u / k; Rank = TabRank[ j ] ;
i = j ∗ k; j = i;
Byteg = j / 4 ; j = j + 4;
while ( j < u)
{ Rank = Rank + Tr [g[Byteg ] ] ;
j = j + 4; Byteg = Byteg + 1;
}
j = j − 4;
while ( j < u)
{ i f ( ObtemValor2Bits ( g, j ) ! = NAOATRIBUIDO ) Rank = Rank+1;
j = j + 1;
}
return Rank;
} /∗ hpm ∗/
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 146

Análise de Tempo (Botelho 2008)


• Quando M = cN , c > 2 e r = 2, a probabilidade Pra de gerar
aleatoriamente um 2-grafo bipartido acíclico, para N → ∞, é:
s  2
2
Pr a = 1 − ·
c

• Quando c = 2, 09, temos que Pra = 0, 29 e o número esperado de


iterações para gerar um 2-grafo bipartido acíclico é
1/Pra = 1/0, 29 ≈ 3, 45.
• Isso significa que, em média, aproximadamente 3, 45 grafos serão
testados antes que apareça um 2-grafo bipartido acíclico.
• Quando M = cN , c ≥ 1,23 e r = 2, um 3-grafo 3-partido acíclico é
obtido em 1 tentativa com probabilidade tendendo para 1 quando
N → ∞.
• Logo, o custo para gerar cada grafo é linear no número de arestas do
grafo.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 147

Análise de Tempo
• O Programa 7.10 para verificar se um hipergrafo é acíclico do tem
complexidade O(|V | + |A|). Como |A| = O(|V |) para grafos esparsos
como os considerados aqui, a complexidade de tempo para gerar a
função de transformação é proporcional ao número de chaves N .
• O tempo necessário para avaliar a função hp do slide 132 envolve a
avaliação de três funções hash universais, com um custo final O(1).
• O tempo necessário para avaliar a função hpm do slide 144 tem um
custo final O(1), utilizando uma estrutura de dados sucinta que permite
computar em O(1) o número de posições atribuidas antes de uma
dada posição em um arranjo.
• A tabela Tr permite contar o número de vértices atribuídos em ǫ log M
bits com custo O(1/ǫ), onde 0 < ǫ < 1.
• Mais ainda, a avaliação da função rank é muito eficiente já que tanto
TabRank quanto Tr cabem inteiramente na memória cache da CPU.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 148

Análise de Espaço da Função Hash Perfeita hp

• Como somente quatro valores distintos são armazenados em cada


entrada de g, são necessários 2 bits por valor.

• Como o tamanho de g para um 3-grafo é M = cN , onde c = 1,23, o


espaço necessário para armazenar o arranjo g é de 2cn = 2,46 bits por
entrada.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 149

Análise de Espaço da Função Hash Perfeita Mínima hpm

• Espaço para g é 2.46 bits por chave.

• Espaço para a tabela TabRank:

|g| + |TabRank| = 2cn + 32 ∗ (cn/k),

assumindo que cada uma das cn/k entradas da tabela TabRank


armazena um inteiro de 32 bits e que cada uma das cn entradas de g
armazena um inteiro de 2 bits. Se tomarmos k = 256, teremos:

2cn + (32/256)cn = (2 + 1/8)cn = (2 + ǫ)cn, para ǫ = 1/8 = 0.125.

• Logo, o espaço total é (2 + ǫ)cn bits. Usando c = 1,23 e ǫ = 0,125, a


função hash perfeita mínima necessita aproximadamente 2,62 bits por
chave para ser armazenada.
Projeto de Algoritmos - Cap.5 Pesquisa em Memória Primária 5.5.5 150

Análise de Espaço da Função Hash Perfeita Mínima hpm


• Mehlhorn (1984) mostrou que o limite inferior para armazenar uma
função hash perfeita mínima é N log e + O(log N ) ≈ 1,44N . Assim, o
valor de aproximadamente 2,62 bits por chave é um valor muito
próximo do limite inferior de aproximadamente 1,44 bits por chave para
essa classe de problemas.
• Esta seção mostra um algoritmo prático que reduziu a complexidade
de espaço para armazenar uma função hash perfeita mínima de
O(N log N ) bits para O(N ) bits. Isso permite o uso de hashing perfeito
em aplicações em que antes não eram consideradas uma boa opção.
• Por exemplo, Botelho, Lacerda, Menezes e Ziviani (2009) mostraram
que uma função hash perfeita mínima apresenta o melhor
compromisso entre espaço ocupado e tempo de busca quando
comparada com todos os outros métodos de hashing para indexar a
memória interna para conjuntos estáticos de chaves.
Pesquisa em Memória Secundária∗

Última alteração: 31 de Agosto de 2010

∗ Transparências elaboradas por Wagner Meira Jr, Flávia Peligrinelli Ribeiro, Israel Guerra, Nívio Ziviani e Charles Ornelas
Almeida
Projeto de Algoritmos – Cap.1 Introdução 1

Conteúdo do Capítulo

6.1 Modelo de Computação para Memória Secundária


6.1.1 Memória Virtual
6.1.2 Implementação de um Sistema de Paginação

6.2 Acesso Sequencial Indexado


6.2.1 Discos Ópticos de Apenas-Leitura

6.3 Árvores de Pesquisa


6.3.1 Árvores B
6.3.2 Árvores B∗
6.3.3 Acesso Concorrente em Árvores B∗
6.3.4 Considerações Práticas
Projeto de Algoritmos – Cap.1 Introdução 2

Introdução
• Pesquisa em memória secundária: arquivos contém mais registros do que
a memória interna pode armazenar.
• Custo para acessar um registro é algumas ordens de grandeza maior do que
o custo de processamento na memória primária.
• Medida de complexidade: custo de trasferir dados entre a memória principal
e secundária (minimizar o número de transferências).
• Memórias secundárias: apenas um registro pode ser acessado em um dado
momento (acesso seqüencial).
• Memórias primárias: acesso a qualquer registro de um arquivo a um custo
uniforme (acesso direto).
• O aspecto sistema de computação é importante.
• As características da arquitetura e do sistema operacional da máquina
tornam os métodos de pesquisa dependentes de parâmetros que afetam
seus desempenhos.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.1 3

Modelo de Computação para Memória Secundária -


Memória Virtual
• Normalmente implementado como uma função do sistema
operacional.
• Modelo de armazenamento em dois níveis, devido à necessidade de
grandes quantidades de memória e o alto custo da memória principal.
• Uso de uma pequena quantidade de memória principal e uma grande
quantidade de memória secundária.
• Programador pode endereçar grandes quantidades de dados,
deixando para o sistema a responsabilidade de trasferir o dado da
memória secundária para a principal.
• Boa estratégia para algoritmos com pequena localidade de referência.
• Organização do fluxo entre a memória principal e secundária é
extremamente importante.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.1.1 4

Memória Virtual

• Organização de fluxo → transformar o endereço usado pelo


programador na localização física de memória correspondente.

• Espaço de Endereçamento → endereços usados pelo programador.

• Espaço de Memória → localizações de memória no computador.

• O espaço de endereçamento N e o espaço de memória M podem ser


vistos como um mapeamento de endereços do tipo: f : N → M .

• O mapeamento permite ao programador usar um espaço de


endereçamento que pode ser maior que o espaço de memória
primária disponível.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.1.1 5

Memória Virtual: Sistema de Paginação


• O espaço de endereçamento é dividido em páginas de tamanho igual,
em geral, múltiplos de 512 Kbytes.
• A memória principal é dividida em molduras de páginas de tamanho
igual.
• As molduras de páginas contêm algumas páginas ativas enquanto o
restante das páginas estão residentes em memória secundária
(páginas inativas).
• O mecanismo possui duas funções:
1. Mapeamento de endereços → determinar qual página um
programa está endereçando, encontrar a moldura, se existir, que
contenha a página.
2. Transferência de páginas → transferir páginas da memória
secundária para a memória primária e transferí-las de volta para a
memória secundária quando não estão mais sendo utilizadas.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.1.1 6

Memória Virtual: Sistema de Paginação

• Endereçamento da página → uma parte dos bits é interpretada como


um número de página e a outra parte como o número do byte dentro
da página (offset).

• Mapeamento de endereços → realizado através de uma Tabela de


Páginas.
– a p-ésima entrada contém a localização p′ da Moldura de Página
contendo a página número p desde que esteja na memória
principal.

• O mapeamento de endereços é: f (e) = f (p, b) = p′ + b, onde e é o


endereço do programa, p é o número da página e b o número do byte.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.1.1 7

Memória Virtual: Mapeamento de Endereços

N◦ da N◦ do
página byte
Endereço
de p b
programa

Tabela_de_Páginas Página p
-
- 
? ?
p′ p′ + b
p′ = nil → página não
presente na
memória
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.1.1 8

Memória Virtual: Reposição de Páginas

• Se não houver uma moldura de página vazia → uma página deverá


ser removida da memória principal.

• Ideal → remover a página que não será referenciada pelo período de


tempo mais longo no futuro.
– tentamos inferir o futuro a partir do comportamento passado.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.1.1 9

Memória Virtual: Políticas de Reposição de Páginas


• Menos Recentemente Utilizada (LRU):
– um dos algoritmos mais utilizados,
– remove a página menos recentemente utilizada,
– parte do princípio que o comportamento futuro deve seguir o
passado recente.
• Menos Freqüentemente Utilizada (LFU):
– remove a página menos feqüentemente utilizada,
– inconveniente: uma página recentemente trazida da memória
secundária tem um baixo número de acessos e pode ser removida.
• Ordem de Chegada (FIFO):
– remove a página que está residente há mais tempo,
– algoritmo mais simples e barato de manter,
– desvantagem: ignora o fato de que a página mais antiga pode ser a
mais referenciada.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.1.1 10

Memória Virtual: Política LRU

- • Toda vez que uma pá-


Fim
gina é utilizada ela é
6
removida para o fim da
fila.
?
Página p • A página que está no
. início da fila é a página
.
LRU.
6
? • Quando uma nova pá-
.
. gina é trazida da me-
 Início mória secundária ela
deve ser colocada na
moldura que contém a
página LRU.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.1.2 11

Memória Virtual: Estrutura de Dados

#define TAMANHODAPAGINA 512


#define ITENSPORPAGINA 64 /∗ TamanhodaPagina / TamanhodoItem ∗/

typedef struct TipoRegisto {


TipoChave Chave;
/∗ outros componentes ∗/
} TipoRegistro ;
typedef struct TipoEndereco {
long p;
char b;
} TipoEndereco;
typedef struct TipoItem {
TipoRegistro Reg;
TipoEndereco Esq, Dir ;
} TipoItem ;
typedef TipoItem TipoPagina[ ItensPorPagina ] ;
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.1.2 12

Memória Virtual
• Em casos em que precisamos manipular mais de um arquivo ao
mesmo tempo:
– A tabela de páginas para cada arquivo pode ser declarada
separadamente.
– A fila de molduras é única → cada moldura deve ter indicado o
arquivo a que se refere aquela página.

typedef struct TipoPagina {


char tipo ; /∗ armazena o codigo do tipo :0 ,1 ,2 ∗/
union {
TipoPaginaA Pa;
TipoPaginaB Pb;
TipoPaginaC Pc;
}P;
} TipoPagina;
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.1.2 13

Memória Virtual

• Procedimentos para comunicação com o sistema de paginação:


– ObtemRegistro → torna disponível um registro.
– EscreveRegistro → permite criar ou alterar o conteúdo de um
registro.
– DescarregaPaginas → varre a fila de molduras para atualizar na
memória secundária todas as páginas que tenham sido
modificadas.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.1.2 14

Memória Virtual - Transformação do Endereço Virtual para


Real

P1 P3
Consulta p Determina Página
- tabela de - moldura
páginas - para página p′ • Quadrados → resulta-
6 p′ dos de processos ou ar-
p ?
A2 P5 quivos.
Fila Grava página
de na memória
molduras p secundária • Retângulos → proces-
Programa p′ p′
Usuário p′ sos transformadores de
A1 A3 p′
6
Tabela
Memória  informação.
de
páginas secundária Página

6 p6
p′ p
? p′ ?
P2 P4
Determina Recupera página
endereço  da memória 
p′ Página
real secundária
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.2 15

Acesso Seqüencial Indexado


• Utiliza o princípio da pesquisa seqüencial → cada registro é lido
seqüencialmente até encontrar uma chave maior ou igual a chave de
pesquisa.
• Providências necessárias para aumentar a eficiência:
– o arquivo deve ser mantido ordenado pelo campo chave do registro,
– um arquivo de índices contendo pares de valores < x, p > deve ser
criado, onde x representa uma chave e p representa o endereço da
página na qual o primeiro registro contém a chave x.
– Estrutura de um arquivo seqüencial indexado para um conjunto de
15 registros:
3 14 25 41
1 2 3 4

1 3 5 7 11 2 14 17 20 21 3 25 29 32 36 4 41 44 48
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.2 16

Acesso Seqüencial Indexado: Disco Magnético


• Dividido em círculos concêntricos (trilhas).
• Cilindro → todas as trilhas verticalmente alinhadas e que possuem o
mesmo diâmetro.
• Latência rotacional → tempo necessário para que o início do bloco
contendo o registro a ser lido passe pela cabeça de leitura/gravação.
• Tempo de busca (seek time) → tempo necessário para que o
mecanismo de acesso desloque de uma trilha para outra (maior parte
do custo para acessar dados).
• Acesso seqüencial indexado = acesso indexado + organização
seqüencial,
• Aproveitando características do disco magnético e procurando
minimizar o número de deslocamentos do mecanismo de acesso →
esquema de índices de cilindros e de páginas.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.2 17

Acesso Seqüencial Indexado: Discos Óticos de


Apenas-Leitura (CD-ROM)
• Grande capacidade de armazenamento (600 MB) e baixo custo.
• Informação armazenada é estática.
• A eficiência na recuperação dos dados é afetada pela localização dos dados
no disco e pela seqüência com que são acessados.
• Velocidade linear constante → trilhas possuem capacidade variável e tempo
de latência rotacional varia de trilha para trilha.
• A trilha tem forma de uma espiral contínua.
• Tempo de busca: acesso a trilhas mais distantes demanda mais tempo que
no disco magnético. Há necessidade de deslocamento do mecanismo de
acesso e mudanças na rotação do disco.
• Varredura estática: acessa conjunto de trilhas vizinhas sem deslocar
mecanismo de leitura.
• Estrutura seqüencial implementada mantendo-se um índice de cilindros na
memória principal.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 18

Árvores B
• Árvores n-árias: mais de um registro por nodo.
• Em uma árvore B de ordem m:
– página raiz: 1 e 2m registros.
– demais páginas: no mínimo m registros e m + 1 descendentes e no
máximo 2m registros e 2m + 1 descendentes.
– páginas folhas: aparecem todas no mesmo nível.
• Registros em ordem crescente da esquerda para a direita.
• Extensão natural da árvore binária de pesquisa.
• Árvore B de ordem m = 2 com três níveis:


30hhh
 hhh 

10 20 40 50
 `
` ` 
``
```
   `
`    


3489 11 13 17


25 28

33 36

43 45 48

52 55
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 19

Árvores B - TAD Dicionário

• Estrutura de Dados:

typedef long TipoChave;


typedef struct TipoRegistro {
TipoChave Chave;
/∗outros componentes∗/
} TipoRegistro ;
typedef struct TipoPagina∗ TipoApontador;
typedef struct TipoPagina {
short n;
TipoRegistro r [ MM ] ;
TipoApontador p[ MM + 1];
} TipoPagina;
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 20

Árvores B - TAD Dicionário

• Operações:
– Inicializa

void Inicializa (TipoApontador ∗ Dicionario )


{ ∗Dicionario = NULL ; }

– Pesquisa
– Insere
– Remove
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 21

Árvores B - Pesquisa

void Pesquisa(TipoRegistro ∗x , TipoApontador Ap)


{ long i = 1;
i f (Ap == NULL)
{ p r i n t f ( "TipoRegistro nao esta presente na arvore \n" ) ;
return ;
}
while ( i < Ap−>n && x−>Chave > Ap−>r [ i −1].Chave) i ++;
i f ( x−>Chave == Ap−>r [ i −1].Chave)
{ ∗x = Ap−>r [ i −1];
return ;
}
i f ( x−>Chave < Ap−>r [ i −1].Chave)
Pesquisa(x , Ap−>p[ i −1]);
else Pesquisa(x , Ap−>p[ i ] ) ;
}
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 22

Árvores B - Inserção
1. Localizar a página apropriada aonde o regisro deve ser inserido.
2. Se o registro a ser inserido encontra uma página com menos de 2m
registros, o processo de inserção fica limitado à página.
3. Se o registro a ser inserido encontra uma página cheia, é criada uma
nova página, no caso da página pai estar cheia o processo de divisão
se propaga.
Exemplo: Inserindo o registro com chave 14.

1 10

 ``
```
 
 `
23489 16 20 25 29
3 (a)


110 20

 ``
` `
 
  ` `
3489 25 29 (b)
2 3 14 16 4 
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 23

Árvores B - Inserção
Exemplo de inserção das chaves: 20, 10, 40, 50, 30, 55, 3, 11, 4, 28, 36,
33, 52, 17, 25, 13, 45, 9, 43, 8 e 48

(a)
20

(b)
30 P
  PP 

10 20
40 50

(c) 10 20 30 40
( ( (
(
 Ph h hh h
( (
(   P
 P h
h


3 4
11 13 17
25 28
33 36
50 52 55

(d)
30hhh
 hhh 

10 20 40 50
 ` `
`  
``
```
   `
`     

3489 11 13 17


25 28
33 36
43 45 48
52 55
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 24

Árvores B - Primeiro refinamento do algoritmo Insere


void Ins (TipoRegistro Reg, TipoApontador Ap, short ∗Cresceu,
TipoRegistro ∗RegRetorno, TipoApontador ∗ApRetorno)
{ long i = 1; long j ; TipoApontador ApTemp;
i f (Ap == NULL)
{ ∗Cresceu = TRUE ; Atribui Reg a RegRetorno;
Atribui NULL a ApRetorno ; return ;
}
while ( i < Ap −> n && Reg.Chave > Ap −> r [ i −1].Chave) i ++;
i f (Reg.Chave == Ap −> r [ i −1].Chave) { p r i n t f ( " Erro : Registro ja esta presente \n" ) ; return ; }
i f (Reg.Chave < Ap −> r [ i −1].Chave) Ins (Reg, Ap −> p[ i −−], Cresceu, RegRetorno, ApRetorno) ;
i f ( ! ∗Cresceu) return ;
i f (Numero de registros em Ap < mm)
{ Insere na pagina Ap e ∗Cresceu = FALSE ; return ; }
/∗ Overflow : Pagina tem que ser dividida ∗/
Cria nova pagina ApTemp;
Transfere metade dos registros de Ap para ApTemp;
Atribui registro do meio a RegRetorno;
Atribui ApTemp a ApRetorno;
}

void Insere (TipoRegistro Reg, TipoApontador ∗Ap)


{ Ins (Reg, ∗Ap, &Cresceu, &RegRetorno, &ApRetorno) ;
i f (Cresceu ) { Cria nova pagina raiz para RegRetorno e ApRetorno ; }
}
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 25

Árvores B - Procedimento InsereNaPágina


void InsereNaPagina(TipoApontador Ap,
TipoRegistro Reg, TipoApontador ApDir)
{ short NaoAchouPosicao;
int k ;
k = Ap−>n ; NaoAchouPosicao = (k > 0);
while (NaoAchouPosicao)
{ i f (Reg.Chave >= Ap−>r [ k−1].Chave)
{ NaoAchouPosicao = FALSE ;
break;
}
Ap−>r [ k ] = Ap−>r [ k−1];
Ap−>p[ k+1] = Ap−>p[ k ] ;
k−−;
i f ( k < 1) NaoAchouPosicao = FALSE ;
}
Ap−>r [ k ] = Reg;
Ap−>p[ k+1] = ApDir;
Ap−>n++;
}
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 26

Árvores B - Refinamento final do algoritmo Insere


void Ins (TipoRegistro Reg, TipoApontador Ap, short ∗Cresceu,
TipoRegistro ∗RegRetorno, TipoApontador ∗ApRetorno)
{ long i = 1; long j ;
TipoApontador ApTemp;
i f (Ap == NULL)
{ ∗Cresceu = TRUE ; ( ∗RegRetorno) = Reg; ( ∗ApRetorno) = NULL ;
return ;
}
while ( i < Ap−>n && Reg.Chave > Ap−>r [ i −1].Chave) i ++;
i f (Reg.Chave == Ap−>r [ i −1].Chave)
{ p r i n t f ( " Erro : Registro ja esta presente \n" ) ; ∗Cresceu = FALSE ;
return ;
}
i f (Reg.Chave < Ap−>r [ i −1].Chave) i−−;
Ins (Reg, Ap−>p[ i ] , Cresceu, RegRetorno, ApRetorno) ;
i f ( ! ∗Cresceu) return ;
i f (Ap−>n < MM ) /∗ Pagina tem espaco ∗/
{ InsereNaPagina(Ap, ∗RegRetorno, ∗ApRetorno) ;
∗Cresceu = FALSE ;
return ;
}
/∗ Overflow : Pagina tem que ser dividida ∗/
ApTemp = (TipoApontador)malloc(sizeof(TipoPagina ) ) ;
ApTemp−>n = 0; ApTemp−>p[0] = NULL ;
{— Continua na próxima transparência —}
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 27

Árvores B - Refinamento final do algoritmo Insere


i f ( i < M + 1)
{ InsereNaPagina(ApTemp, Ap−>r [ MM−1], Ap−>p[ MM ] ) ;
Ap−>n−−;
InsereNaPagina(Ap, ∗RegRetorno, ∗ApRetorno) ;
}
else InsereNaPagina(ApTemp, ∗RegRetorno, ∗ApRetorno) ;
for ( j = M + 2; j <= MM ; j ++)
InsereNaPagina(ApTemp, Ap−>r [ j −1], Ap−>p[ j ] ) ;
Ap−>n = M; ApTemp−>p[0] = Ap−>p[M+1];
∗RegRetorno = Ap−>r [M] ; ∗ApRetorno = ApTemp;
}

void Insere (TipoRegistro Reg, TipoApontador ∗Ap)


{ short Cresceu;
TipoRegistro RegRetorno;
TipoPagina ∗ApRetorno, ∗ApTemp;
Ins (Reg, ∗Ap, &Cresceu, &RegRetorno, &ApRetorno) ;
i f (Cresceu) /∗ Arvore cresce na altura pela raiz ∗/
{ ApTemp = (TipoPagina ∗)malloc(sizeof(TipoPagina ) ) ;
ApTemp−>n = 1;
ApTemp−>r [0] = RegRetorno;
ApTemp−>p[1] = ApRetorno;
ApTemp−>p[0] = ∗Ap; ∗Ap = ApTemp;
}
}
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 28

Árvores B - Remoção

• Página com o registro a ser retirado é folha:


1. retira-se o registro,
2. se a página não possui pelo menos de m registros, a propriedade
da árvore B é violada. Pega-se um registro emprestado da página
vizinha. Se não existir registros suficientes na página vizinha, as
duas páginas devem ser fundidas em uma só.

• Pagina com o registro não é folha:


1. o registro a ser retirado deve ser primeiramente substituído por um
registro contendo uma chave adjacente.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 29

Árvores B - Remoção

Exemplo: Retirando a chave 3.

4j 4j 6j
 H H , ll
 H 
 * ,   ,, ll
j j 4j 8j
2 6 8  6 8 
@@  T    T   AA  AA
j j 5 7j9j
j jjj j 7j 9j
1 3 1 2  5 7 9 1 2 5
(a) Página vizinha possui mais do que m registros

4j 4j j
,, ll * ,
, ll
 
2j 6j j 6j 4 6 
 AA  AA    AA   T
j j 5j 7j j j jj
1 3 1 2  5 7 1 2 5 7
(b) Página vizinha possui exatamente m registros
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 30

Árvores B - Remoção

Remoção das chaves 45 30 28; 50 8 10 4 20 40 55 17 33 11 36; 3 9 52.



(a)
30hhh
 hhh 

10 20 40 50
  ` `
`  
``
```
   ``     

3489
11 13 17
25 28
33 36

43 45 48
52 55

(b) 10 25 40 50
( ( ((
 Ph h hh
( ( 
( 
P
 P hh h



3 4 8 9 11 13 17 20


33 36
43 48
52 55


(c)
13 P
  PP 

3 9 25 43 48 52


(d) 13 25 43 48

Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 31

Árvores B - Procedimento Retira


void Reconstitui (TipoApontador ApPag, TipoApontador ApPai,
int PosPai, short ∗Diminuiu)
{ TipoPagina ∗Aux; long DispAux, j ;
i f (PosPai < ApPai−>n) /∗ Aux = TipoPagina a direita de ApPag ∗/
{ Aux = ApPai−>p[PosPai+1]; DispAux = (Aux−>n − M + 1 ) / 2 ;
ApPag−>r [ApPag−>n] = ApPai−>r [PosPai ] ;
ApPag−>p[ApPag−>n + 1] = Aux−>p[ 0 ] ; ApPag−>n++;
i f (DispAux > 0) /∗ Existe folga : transfere de Aux para ApPag ∗/
{ for ( j = 1; j < DispAux ; j ++)
InsereNaPagina(ApPag, Aux−>r [ j −1], Aux−>p[ j ] ) ;
ApPai−>r [PosPai] = Aux−>r [DispAux−1]; Aux−>n −= DispAux;
for ( j = 0; j < Aux−>n ; j ++) Aux−>r [ j ] = Aux−>r [ j + DispAux ] ;
for ( j = 0; j <= Aux−>n ; j ++) Aux−>p[ j ] = Aux−>p[ j + DispAux ] ;
∗Diminuiu = FALSE ;
}
else /∗ Fusao: intercala Aux em ApPag e libera Aux ∗/
{ for ( j = 1; j <= M; j ++) InsereNaPagina(ApPag, Aux−>r [ j −1], Aux−>p[ j ] ) ;
free (Aux) ;
for ( j = PosPai + 1; j < ApPai−>n ; j ++)
{ ApPai−>r [ j −1] = ApPai−>r [ j ] ; ApPai−>p[ j ] = ApPai−>p[ j +1]; }
ApPai−>n−−;
i f (ApPai−>n >= M) ∗Diminuiu = FALSE ;
}
}
{— Continua na próxima transparência —}
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 32

Árvores B - Procedimento Retira


else /∗ Aux = TipoPagina a esquerda de ApPag ∗/
{ Aux = ApPai−>p[PosPai−1]; DispAux = (Aux−>n − M + 1 ) / 2 ;
for ( j = ApPag−>n ; j >= 1; j −−)ApPag−>r [ j ] = ApPag−>r [ j −1];
ApPag−>r [0] = ApPai−>r [PosPai−1];
for ( j = ApPag−>n ; j >= 0; j −−)ApPag−>p[ j +1] = ApPag−>p[ j ] ;
ApPag−>n++;
i f (DispAux > 0) /∗ Existe folga : transf . de Aux para ApPag ∗/
{ for ( j = 1; j < DispAux ; j ++)
InsereNaPagina(ApPag, Aux−>r [Aux−>n − j ] ,
Aux−>p[Aux−>n − j + 1 ] ) ;
ApPag−>p[0] = Aux−>p[Aux−>n − DispAux + 1];
ApPai−>r [PosPai−1] = Aux−>r [Aux−>n − DispAux ] ;
Aux−>n −= DispAux ; ∗Diminuiu = FALSE ;
}
else /∗ Fusao: intercala ApPag em Aux e libera ApPag ∗/
{ for ( j = 1; j <= M; j ++)
InsereNaPagina(Aux, ApPag−>r [ j −1], ApPag−>p[ j ] ) ;
free (ApPag) ; ApPai−>n−−;
i f (ApPai−>n >= M) ∗Diminuiu = FALSE ;
}
}
{— Continua na próxima transparência —}
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 33

Árvores B - Procedimento Retira


void Ret(TipoChave Ch, TipoApontador ∗Ap, short ∗Diminuiu)
{ long j , Ind = 1;
TipoApontador Pag;
i f (∗Ap == NULL)
{ p r i n t f ( "Erro : registro nao esta na arvore \n" ) ; ∗ Diminuiu = FALSE ;
return ;
}
Pag = ∗Ap;
while ( Ind < Pag−>n && Ch > Pag−>r [ Ind−1].Chave) Ind++;
i f (Ch == Pag−>r [ Ind−1].Chave)
{ i f (Pag−>p[ Ind−1] == NULL ) /∗ TipoPagina folha ∗/
{ Pag−>n−−; ∗Diminuiu = (Pag−>n < M) ;
for ( j = Ind ; j <= Pag−>n ; j ++) { Pag−>r [ j −1] = Pag−>r [ j ] ; Pag−>p[ j ] = Pag−>p[ j +1]; }
return ;
}
/∗ TipoPagina nao e folha : trocar com antecessor ∗/
Antecessor(∗Ap, Ind , Pag−>p[ Ind−1], Diminuiu ) ;
i f (∗Diminuiu ) Reconstitui (Pag−>p[ Ind−1], ∗Ap, Ind − 1, Diminuiu ) ;
return ;
}
i f (Ch > Pag−>r [ Ind−1].Chave) Ind++;
Ret(Ch, &Pag−>p[ Ind−1], Diminuiu ) ;
i f (∗Diminuiu ) Reconstitui (Pag−>p[ Ind−1], ∗Ap, Ind − 1, Diminuiu ) ;
}
{— Continua na próxima transparência —}
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 34

Árvores B - Procedimento Retira


void Antecessor(TipoApontador Ap, int Ind ,
TipoApontador ApPai, short ∗Diminuiu)
{ i f (ApPai−>p[ApPai−>n] ! = NULL)
{ Antecessor(Ap, Ind , ApPai−>p[ApPai−>n] , Diminuiu ) ;
i f (∗Diminuiu)
Reconstitui (ApPai−>p[ApPai−>n] , ApPai, ( long)ApPai−>n, Diminuiu ) ;
return ;
}
Ap−>r [ Ind−1] = ApPai−>r [ApPai−>n − 1];
ApPai−>n−−; ∗Diminuiu = (ApPai−>n < M) ;
}
{— Continua na próxima transparência —}
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 35

Árvores B - Procedimento Retira


void Ret(TipoChave Ch, TipoApontador ∗Ap, short ∗Diminuiu)
{ long j , Ind = 1;
TipoApontador Pag;
i f (∗Ap == NULL)
{ p r i n t f ( "Erro : registro nao esta na arvore \n" ) ; ∗ Diminuiu = FALSE ;
return ;
}
Pag = ∗Ap;
while ( Ind < Pag−>n && Ch > Pag−>r [ Ind−1].Chave) Ind++;
i f (Ch == Pag−>r [ Ind−1].Chave)
{ i f (Pag−>p[ Ind−1] == NULL ) /∗ TipoPagina folha ∗/
{ Pag−>n−−;
∗Diminuiu = (Pag−>n < M) ;
for ( j = Ind ; j <= Pag−>n ; j ++)
{ Pag−>r [ j −1] = Pag−>r [ j ] ; Pag−>p[ j ] = Pag−>p[ j +1]; }
return ;
}
/∗ TipoPagina nao e folha : trocar com antecessor ∗/
Antecessor(∗Ap, Ind , Pag−>p[ Ind−1], Diminuiu ) ;
i f (∗Diminuiu)
Reconstitui (Pag−>p[ Ind−1], ∗Ap, Ind − 1, Diminuiu ) ;
return ;
}
{— Continua na próxima transparência —}
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.1 36

Árvores B - Procedimento Retira


i f (Ch > Pag−>r [ Ind−1].Chave) Ind++;
Ret(Ch, &Pag−>p[ Ind−1], Diminuiu ) ;
i f (∗Diminuiu ) Reconstitui (Pag−>p[ Ind−1], ∗Ap, Ind − 1, Diminuiu ) ;
}

void Retira (TipoChave Ch, TipoApontador ∗Ap)


{ short Diminuiu ;
TipoApontador Aux;
Ret(Ch, Ap, &Diminuiu ) ;
i f ( Diminuiu && (∗Ap)−>n == 0) /∗ Arvore diminui na altura ∗/
{ Aux = ∗Ap; ∗Ap = Aux−>p[ 0 ] ;
free (Aux) ;
}
}
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.2 37

Árvores B* - TAD Dicionário


• Estrutura de Dados:
typedef int TipoChave;
typedef struct TipoRegistro {
TipoChave Chave;
/∗ outros componentes ∗/
} TipoRegistro ;
typedef enum {
Interna , Externa
} TipoIntExt ;
typedef struct TipoPagina ∗TipoApontador;
typedef struct TipoPagina {
TipoIntExt Pt ;
union {
struct {
int ni ;
TipoChave r i [ MM ] ;
TipoApontador pi [ MM + 1];
} U0;
struct {
int ne;
TipoRegistro re [ MM2];
} U1;
} UU ;
} TipoPagina;
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.2 38

Árvores B* - Pesquisa

• Semelhante à pesquisa em árvore B,

• A pesquisa sempre leva a uma página folha,

• A pesquisa não pára se a chave procurada for encontrada em uma


página índice. O apontador da direita é seguido até que se encontre
uma página folha.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.2 39

Árvores B* - Procedimento para pesquisar na árvore B⋆


void Pesquisa(TipoRegistro ∗x , TipoApontador ∗Ap)
{ int i ;
TipoApontador Pag;
Pag = ∗Ap;
i f ((∗Ap)−>Pt == Interna )
{ i = 1;
while ( i < Pag−>UU .U0. ni && x−>Chave > Pag−>UU .U0. r i [ i − 1]) i ++;
i f ( x−>Chave < Pag−>UU .U0. r i [ i − 1])
Pesquisa(x, &Pag−>UU .U0. pi [ i − 1]);
else Pesquisa(x, &Pag−>UU .U0. pi [ i ] ) ;
return ;
}
i = 1;
while ( i < Pag−>UU .U1.ne && x−>Chave > Pag−>UU .U1. re [ i − 1].Chave)
i ++;
i f ( x−>Chave == Pag−>UU .U1. re [ i − 1].Chave)
∗x = Pag−>UU .U1. re [ i − 1];
else p r i n t f ( "TipoRegistro nao esta presente na arvore \n" ) ;
}
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.2 40

Árvores B* - Inserção e Remoção


• Inserção na árvore B*
– Semelhante à inserção na árvore B,
– Diferença: quando uma folha é dividida em duas, o algoritmo
promove uma cópia da chave que pertence ao registro do meio
para a página pai no nível anterior, retendo o registro do meio na
página folha da direita.
• Remoção na árvore B*
– Relativamente mais simples que em uma árvore B,
– Todos os registros são folhas,
– Desde que a folha fique com pelo menos metade dos registros, as
páginas dos índices não precisam ser modificadas, mesmo se uma
cópia da chave que pertence ao registro a ser retirado esteja no
índice.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.3 41

Acesso Concorrente em Árvore B*


• Acesso simultâneo a banco de dados por mais de um usuário.
• Concorrência aumenta a utilização e melhora o tempo de resposta do
sistema.
• O uso de árvores B* nesses sistemas deve permitir o processamento
simultâneo de várias solicitações diferentes.
• Necessidade de criar mecanismos chamados protocolos para garantir
a integridade tanto dos dados quanto da estrutura.
• Página segura: não há possibilidade de modificações na estrutura da
árvore como conseqüência de inserção ou remoção.
– inserção → página segura se o número de chaves é igual a 2m,
– remoção → página segura se o número de chaves é maior que m.
• Os algoritmos para acesso concorrente fazem uso dessa propriedade
para aumentar o nível de concorrência.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.3 42

Acesso Concorrente em Árvore B* - Protocolos de


Travamentos
• Quando uma página é lida, a operação de recuperação a trava, assim,
outros processos, não podem interferir com a página.
• A pesquisa continua em direção ao nível seguinte e a trava é liberada
para que outros processos possam ler a página .
• Processo leitor → executa uma operação de recuperação
• Processo modificador → executa uma operação de inserção ou
retirada.
• Dois tipos de travamento:
– Travamento para leitura → permite um ou mais leitores acessarem
os dados, mas não permite inserção ou retirada.
– Travamento exclusivo → nenhum outro processo pode operar na
página e permite qualquer tipo de operação na página.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.4 43

Árvore B - Considerações Práticas

• Simples, fácil manutenção, eficiente e versátil.

• Permite acesso seqüencial eficiente.

• Custo para recuperar, inserir e retirar registros do arquivo é logaritmico.

• Espaço utilizado é, no mínimo 50% do espaço reservado para o


arquivo,

• Emprego onde o acesso concorrente ao banco de dados é necessário,


é viável e relativamente simples de ser implementado.

• Inserção e retirada de registros sempre deixam a árvore balanceada.

• Uma árvore B de ordem m com N registros contém no máximo cerca


de logm+1 N páginas.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.4 44

Árvore B - Considerações Práticas

• Limites para a altura máxima e mínima de uma árvore B de ordem



m
com N registros: log2m+1 (N + 1) ≤ altura ≤ 1 + logm+1 N 2+1

• Custo para processar uma operação de recuperação de um registro


cresce com o logaritmo base m do tamanho do arquivo.

• Altura esperada: não é conhecida analiticamente.

• Há uma conjectura proposta a partir do cálculo analítico do número


esperado de páginas para os quatro primeiros níveis (das folha em
direção à raiz) de uma árvore 2-3 (árvore B de ordem m = 1).

• Conjetura: a altura esperada de uma árvore 2-3 randômica com N


chaves é h(N ) ≈ log7/3 (N + 1).
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.4 45

Árvores B Randômicas - Medidas de Complexidade


• A utilização de memória é cerca de ln 2.
– Páginas ocupam ≈ 69% da área reservada após N inserções
randômicas em uma árvore B inicialmente vazia.
• No momento da inserção, a operação mais cara é a partição da página
quando ela passa a ter mais do que 2m chaves. Envolve:
– Criação de nova página, rearranjo das chaves e inserção da chave
do meio na página pai localizada no nível acima.
– P r{j partições}: probabilidade de que j partições ocorram durante
a N -ésima inserção randômica.
– Árvore 2-3: P r{0 partições} = 47 , P r{1 ou mais partições} = 37 ·
1
– Árvore B de ordem m: P r{0 partições} = 1 − (2 ln 2)m
+ O(m−2 ),
P r{1 ou + partições} = (2 ln12)m + O(m−2 ).
– Árvore B de ordem m = 70: 99% das vezes nada acontece em
termos de partições durante uma inserção.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.4 46

Árvores B Randômicas - Acesso Concorrente


• Foi proposta uma técnica de aplicar um travamento na página segura
mais profunda (Psmp) no caminho de inserção.
• Uma página é segura se ela contém menos do que 2m chaves.
• Uma página segura é a mais profunda se não existir outra página
segura abaixo dela.
• Já que o travamento da página impede o acesso de outros processos,
é interessante saber qual é a probabilidade de que a página segura
mais profunda esteja no primeiro nível.
• Árvore 2-3: P r{Psmp esteja no 1◦ nível} = 47 ,
P r{Psmp esteja acima do 1◦ nível} = 73 ·
• Árvore B de ordem m:
P r{Psmp esteja no 1◦ nível} = 1 − (2 ln12)m + O(m−2 ),
P r{Psmp esteja acima do 1◦ nível} = 73 = (2 ln12)m + O(m−2 ).
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.4 47

Árvores B Randômicas - Acesso Concorrente

• Novamente, em árvores B de ordem m = 70: 99% das vezes a Psmp


está em uma folha. (Permite alto grau de concorrência para processos
modificadores.)

• Soluções muito complicadas para permitir concorrência de operações


em árvores B não trazem grandes benefícios.

• Na maioria das vezes, o travamento ocorrerá em páginas folha.


(Permite alto grau de concorrência mesmo para os protocolos mais
simples.)
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.4 48

Árvore B - Técnica de Transbordamento (ou Overflow)


• Assuma que um registro tenha de ser inserido em uma página cheia,
com 2m registros.
• Em vez de particioná-la, olhamos primeiro para a página irmã à direita.
• Se a página irmã possui menos do que 2m registros, um simples
rearranjo de chaves torna a partição desnecessária.
• Se a página à direita também estiver cheia ou não existir, olhamos
para a página irmã à esquerda.
• Se ambas estiverem cheias, então a partição terá de ser realizada.
• Efeito da modificação: produzir uma árvore com melhor utilização de
memória e uma altura esperada menor.
• Produz uma utilização de memória de cerca de 83% para uma árvore
B randômica.
Projeto de Algoritmos – Cap.6 Pesquisa em Memória Secundária – Seção 6.3.4 49

Árvore B - Influência do Sistema de Paginação


• O número de níveis de uma árvore B é muito pequeno (três ou quatro)
se comparado com o número de molduras de páginas.
• Assim, o sistema de paginação garante que a página raiz esteja
sempre na memória principal (se for adotada a política LRU).
• O esquema LRU faz com que as páginas a serem particionadas em
uma inserção estejam disponíveis na memória principal.
• A escolha do tamanho adequado da ordem m da árvore B é
geralmente feita levando em conta as características de cada
computador.
• O tamanho ideal da página da árvore corresponde ao tamanho da
página do sistema, e a transferência de dados entre as memórias
secundária e principal é realizada pelo sistema operacional.
• Estes tamanhos variam entre 512 bytes e 4.096 bytes, em múltiplos de
512 bytes.
Algoritmos em Grafos ∗

Última alteração: 24 de Setembro de 2010

∗ Transparências elaboradas por Charles Ornelas Almeida, Israel Guerra e Nivio Ziviani
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos 1

Conteúdo do Capítulo
7.1 Definições Básicas 7.5 Busca em Largura
7.2 O Tipo Abstrato de Dados Grafo 7.6 Ordenação Topológica
7.2.1 Implementação por meio de Ma- 7.7 Componentes Fortemente Conectados
trizes de Adjacência
7.8 Árvore Geradora Mínima
7.2.2 Implementação por meio de Lis-
7.8.1 Algoritmo Genérico para Obter a
tas de Adjacência Usando Apon-
Árvore Geradora Mínima
tadores
7.8.2 Algoritmo de Prim
7.2.3 Implementação por meio de Lis-
tas de Adjacência Usando Ar- 7.8.2 Algoritmo de Kruskal
ranjos 7.9 Caminhos mais Curtos
7.2.4 Programa Teste para as Três Im- 7.10 O Tipo Abstrato de Dados Hipergrafo
plementações 7.10.1 Implementação por meio de Matri-
7.3 Busca em Profundidade zes de Incidência
7.4 Verificar se Grafo é Acíclico 7.10.1 Implementação por meio de Listas
7.4.1 Usando Busca em Profundidade de Incidência Usando Arranjos
7.4.1 Usando o Tipo Abstrato de Da-
dos Hipergrafo
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos 2

Motivação

• Muitas aplicações em computação necessitam considerar conjunto de


conexões entre pares de objetos:
– Existe um caminho para ir de um objeto a outro seguindo as
conexões?
– Qual é a menor distância entre um objeto e outro objeto?
– Quantos outros objetos podem ser alcançados a partir de um
determinado objeto?

• Existe um tipo abstrato chamado grafo que é usado para modelar tais
situações.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos 3

Aplicações

• Alguns exemplos de problemas práticos que podem ser resolvidos


através de uma modelagem em grafos:
– Ajudar máquinas de busca a localizar informação relevante na Web.
– Descobrir os melhores casamentos entre posições disponíveis em
empresas e pessoas que aplicaram para as posições de interesse.
– Descobrir qual é o roteiro mais curto para visitar as principais
cidades de uma região turística.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.1 4

Conceitos Básicos
• Grafo: conjunto de vértices e arestas.
• Vértice: objeto simples que pode ter nome e outros atributos.
• Aresta: conexão entre dois vértices.

3 aresta

0 1 4

2 vértice

• Notação: G = (V, A)
– G: grafo
– V: conjunto de vértices
– A: conjunto de arestas
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.1 5

Grafos Direcionados

• Um grafo direcionado G é um par (V, A), onde V é um conjunto finito


de vértices e A é uma relação binária em V .
– Uma aresta (u, v) sai do vértice u e entra no vértice v. O vértice v é
adjacente ao vértice u.
– Podem existir arestas de um vértice para ele mesmo, chamadas de
self-loops.

0 1 4

3 2 5
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.1 6

Grafos Não Direcionados

• Um grafo não direcionado G é um par (V, A), onde o conjunto de


arestas A é constituído de pares de vértices não ordenados.
– As arestas (u, v) e (v, u) são consideradas como uma única aresta.
A relação de adjacência é simétrica.
– Self-loops não são permitidos.

0 1 4

3 2 5
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.1 7

Grau de um Vértice

• Em grafos não direcionados:


– O grau de um vértice é o número de
arestas que incidem nele. 0 1 4

– Um vérice de grau zero é dito isolado


ou não conectado. 3 2 5

– Ex.: O vértice 1 tem grau 2 e o vértice


3 é isolado.
• Em grafos direcionados
– O grau de um vértice é o número de
arestas que saem dele (out-degree) 0 1 4
mais o número de arestas que che-
gam nele (in-degree). 3 2 5
– Ex.: O vértice 2 tem in-degree 2, out-
degree 2 e grau 4.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.1 8

Caminho entre Vértices


• Um caminho de comprimento k de um vértice x a um vértice y em um
grafo G = (V, A) é uma sequência de vértices (v0 , v1 , v2 , . . . , vk ) tal que
x = v0 e y = vk , e (vi−1 , vi ) ∈ A para i = 1, 2, . . . , k.
• O comprimento de um caminho é o número de arestas nele, isto é, o
caminho contém os vértices v0 , v1 , v2 , . . . , vk e as arestas
(v0 , v1 ), (v1 , v2 ), . . . , (vk−1 , vk ).
• Se existir um caminho c de x a y então y é alcançável a partir de x via
c.
• Um caminho é simples se todos os vértices do caminho são distintos.

Ex.: O caminho (0, 1, 2, 3) é simples e tem 0 1 4


comprimento 3. O caminho (1, 3, 0, 3) não
é simples.
3 2 5
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.1 9

Ciclos

• Em um grafo direcionado:
– Um caminho (v0 , v1 , . . . , vk ) forma um ciclo se v0 = vk e o caminho
contém pelo menos uma aresta.
– O ciclo é simples se os vértices v1 , v2 , . . . , vk são distintos.
– O self-loop é um ciclo de tamanho 1.
– Dois caminhos (v0 , v1 , . . . , vk ) e (v0′ , v1′ , . . . , vk′ ) formam o mesmo
ciclo se existir um inteiro j tal que vi′ = v(i+j) mod k para
i = 0, 1, . . . , k − 1.

Ex.: O caminho (0, 1, 2, 3, 0) forma um ciclo. 0 1 4


O caminho(0, 1, 3, 0) forma o mesmo ciclo
que os caminhos (1, 3, 0, 1) e (3, 0, 1, 3).
3 2 5
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.1 10

Ciclos

• Em um grafo não direcionado:


– Um caminho (v0 , v1 , . . . , vk ) forma um ciclo se v0 = vk e o caminho
contém pelo menos três arestas.
– O ciclo é simples se os vértices v1 , v2 , . . . , vk são distintos.

Ex.: O caminho (0, 1, 2, 0) é um ciclo. 0 1 4

3 2 5
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.1 11

Componentes Conectados

• Um grafo não direcionado é conectado se cada par de vértices está


conectado por um caminho.

• Os componentes conectados são as porções conectadas de um grafo.

• Um grafo não direcionado é conectado se ele tem exatamente um


componente conectado.

Ex.: Os componentes são: {0, 1, 2}, {4, 5} 0 1 4

e {3}.
3 2 5
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.1 12

Componentes Fortemente Conectados

• Um grafo direcionado G = (V, A) é fortemente conectado se cada


dois vértices quaisquer são alcançáveis a partir um do outro.

• Os componentes fortemente conectados de um grafo direcionado


são conjuntos de vértices sob a relação “são mutuamente
alcançáveis”.

• Um grafo direcionado fortemente conectado tem apenas um


componente fortemente conectado.

Ex.: {0, 1, 2, 3}, {4} e {5} são os compo- 0 1 4


nentes fortemente conectados, {4, 5} não o
é pois o vértice 5 não é alcançável a partir
3 2 5
do vértice 4.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.1 13

Grafos Isomorfos

• G = (V, A) e G′ = (V ′ , A′ ) são isomorfos se existir uma bijeção


f : V → V ′ tal que (u, v) ∈ A se e somente se (f (u), f (v)) ∈ A′ .

• Em outras palavras, é possível re-rotular os vértices de G para serem


rótulos de G′ mantendo as arestas correspondentes em G e G′ .

0 1

4 5 s w x t

7 6 v z y u

3 2
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.1 14

Subgrafos

• Um grafo G′ = (V ′ , A′ ) é um subgrafo de G = (V, A) se V ′ ⊆ V e


A′ ⊆ A.

• Dado um conjunto V ′ ⊆ V , o subgrafo induzido por V ′ é o grafo


G′ = (V ′ , A′ ), onde A′ = {(u, v) ∈ A|u, v ∈ V ′ }.

Ex.: Subgrafo induzido pelo conjunto de vértices {1, 2, 4, 5}.

0 1 4 1 4

3 2 5 2 5
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.1 15

Versão Direcionada de um Grafo Não Direcionado

• A versão direcionada de um grafo não direcionado G = (V, A) é um


grafo direcionado G′ = (V ′ , A′ ) onde (u, v) ∈ A′ se e somente se
(u, v) ∈ A.

• Cada aresta não direcionada (u, v) em G é substituída por duas


arestas direcionadas (u, v) e (v, u)

• Em um grafo direcionado, um vizinho de um vértice u é qualquer


vértice adjacente a u na versão não direcionada de G.

0 1 0 1

2 2
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.1 16

Versão Não Direcionada de um Grafo Direcionado

• A versão não direcionada de um grafo direcionado G = (V, A) é um


grafo não direcionado G′ = (V ′ , A′ ) onde (u, v) ∈ A′ se e somente se
u 6= v e (u, v) ∈ A.

• A versão não direcionada contém as arestas de G sem a direção e


sem os self-loops.

• Em um grafo não direcionado, u e v são vizinhos se eles são


adjacentes.

0 1 4 0 1 4

3 2 5 3 2 5
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.1 17

Outras Classificações de Grafos

• Grafo ponderado: possui pesos associados às arestas.

• Grafo bipartido: grafo não direcionado G = (V, A) no qual V pode ser


particionado em dois conjuntos V1 e V2 tal que (u, v) ∈ A implica que
u ∈ V1 e v ∈ V2 ou u ∈ V2 e v ∈ V1 (todas as arestas ligam os dois
conjuntos V1 e V2 ).

• Hipergrafo: grafo não direcionado em que cada aresta conecta um


número arbitrário de vértices.
– Hipergrafos são utilizados na Seção 5.5.4 sobre hashing perfeito.
– Na Seção 7.10 é apresentada uma estrutura de dados mais
adequada para representar um hipergrafo.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.1 18

Grafos Completos

• Um grafo completo é um grafo não direcionado no qual todos os pares


de vértices são adjacentes.

• Possui (|V |2 − |V |)/2 = |V |(|V | − 1)/2 arestas, pois do total de |V |2


pares possíveis de vértices devemos subtrair |V | self-loops e dividir
por 2 (cada aresta ligando dois vértices é contada duas vezes).

• O número total de grafos diferentes com |V | vértices é 2|V |(|V |−1)/2


(número de maneiras diferentes de escolher um subconjunto a partir
de |V |(|V | − 1)/2 possíveis arestas).
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.1 19

Árvores
• Árvore livre: grafo não direcionado acíclico e conectado. É comum
dizer apenas que o grafo é uma árvore omitindo o “livre”.
• Floresta: grafo não direcionado acíclico, podendo ou não ser
conectado.
• Árvore geradora de um grafo conectado G = (V, A): subgrafo que
contém todos os vértices de G e forma uma árvore.
• Floresta geradora de um grafo G = (V, A): subgrafo que contém
todos os vértices de G e forma uma floresta.

(a) (b)
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2 20

O Tipo Abstratos de Dados Grafo

• Importante considerar os algoritmos em grafos como tipos abstratos


de dados.

• Conjunto de operações associado a uma estrutura de dados.

• Independência de implementação para as operações.


Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2 21

Operadores do TAD Grafo


1. FGVazio(Grafo): Cria um grafo vazio.
2. InsereAresta(V1,V2,Peso, Grafo): Insere uma aresta no grafo.
3. ExisteAresta(V1,V2,Grafo): Verifica se existe uma determinada aresta.
4. Obtem a lista de vértices adjacentes a um determinado vértice (tratada
a seguir).
5. RetiraAresta(V1,V2,Peso, Grafo): Retira uma aresta do grafo.
6. LiberaGrafo(Grafo): Liberar o espaço ocupado por um grafo.
7. ImprimeGrafo(Grafo): Imprime um grafo.
8. GrafoTransposto(Grafo,GrafoT): Obtém o transposto de um grafo
direcionado.
9. RetiraMin(A): Obtém a aresta de menor peso de um grafo com peso
nas arestas.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2 22

Operação “Obter Lista de Adjacentes”

1. ListaAdjVazia(v, Grafo): retorna true se a lista de adjacentes de v está


vazia.

2. PrimeiroListaAdj(v, Grafo): retorna o endereço do primeiro vértice na


lista de adjacentes de v.

3. ProxAdj(v, Grafo, u, Peso, Aux, FimListaAdj): retorna o vértice u


(apontado por Aux) da lista de adjacentes de v, bem como o peso da
aresta (v, u). Ao retornar, Aux aponta para o próximo vértice da lista
de adjacentes de v, e FimListaAdj retorna true se o final da lista de
adjacentes foi encontrado.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2 23

Implementação da Operação “Obter Lista de Adjacentes”

• É comum encontrar um pseudo comando do tipo:


for u ∈ ListaAdjacentes (v) do { faz algo com u }

• O trecho de programa abaixo apresenta um possível refinamento do


pseudo comando acima.

i f ( ! ListaAdjVazia(v,Grafo) )
{ Aux = PrimeiroListaAdj (v,Grafo ) ;
FimListaAdj = FALSE ;
while ( ! FimListaAdj)
ProxAdj(&v , Grafo, &u, &Peso, &Aux, &FimListaAdj ) ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.1 24

Matriz de Adjacência

• A matriz de adjacência de um grafo G = (V, A) contendo n vértices é


uma matriz n × n de bits, onde A[i, j] é 1 (ou verdadeiro) se e somente
se existe um arco do vértice i para o vértice j.

• Para grafos ponderados A[i, j] contém o rótulo ou peso associado com


a aresta e, neste caso, a matriz não é de bits.

• Se não existir uma aresta de i para j então é necessário utilizar um


valor que não possa ser usado como rótulo ou peso.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.1 25

Matriz de Adjacência: Exemplo

0 1 4 0 1 4

3 2 5 3 2 5

0 1 2 3 4 5 0 1 2 3 4 5
0 1 1 0 1 1
1 1 1 1 1 1
2 1 1 2 1 1
3 1 3
4 4
5 5
(a) (b)
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.1 26

Matriz de Adjacência: Análise

• Deve ser utilizada para grafos densos, onde |A| é próximo de |V |2 .

• O tempo necessário para acessar um elemento é independente de |V |


ou |A|.

• É muito útil para algoritmos em que necessitamos saber com rapidez


se existe uma aresta ligando dois vértices.

• A maior desvantagem é que a matriz necessita Ω(|V |2 ) de espaço. Ler


ou examinar a matriz tem complexidade de tempo O(|V |2 ).
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.1 27

Matriz de Adjacência: Estrutura de Dados

• A inserção de um novo vértice ou retirada de um vértice já existente


pode ser realizada com custo constante.

#define MAXNUMVERTICES 100


#define MAXNUMARESTAS 4500

typedef int TipoValorVertice ;


typedef int TipoPeso;
typedef struct TipoGrafo {
TipoPeso Mat[ MAXNUMVERTICES + 1][MAXNUMVERTICES + 1];
int NumVertices;
int NumArestas;
} TipoGrafo ;
typedef int TipoApontador;
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.1 28

Matriz de Adjacência: Operadores


void FGVazio(TipoGrafo ∗Grafo)
{ short i , j ;
for ( i = 0; i <= Grafo−>NumVertices ; i ++)
{ for ( j = 0; j <=Grafo−>NumVertices ; j ++) Grafo−>Mat[ i ] [ j ] = 0 ; }
}

void InsereAresta( TipoValorVertice ∗V1, TipoValorVertice ∗V2,


TipoPeso ∗Peso, TipoGrafo ∗Grafo)
{ Grafo−>Mat[∗V1] [ ∗V2] = ∗Peso; }

short ExisteAresta ( TipoValorVertice Vertice1 ,


TipoValorVertice Vertice2 , TipoGrafo ∗Grafo)
{ return ( Grafo−>Mat[ Vertice1 ] [ Vertice2 ] > 0 ) ; }
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.1 29

Matriz de Adjacência: Operadores


/∗ Operadores para obter a l i s t a de adjacentes ∗/
short ListaAdjVazia( TipoValorVertice ∗ Vertice , TipoGrafo ∗Grafo)
{ TipoApontador Aux = 0;
short ListaVazia = TRUE ;
while (Aux < Grafo−>NumVertices && ListaVazia )
i f ( Grafo−>Mat[∗ Vertice ] [Aux] > 0) ListaVazia = FALSE ;
else Aux++;
return ( ListaVazia == TRUE ) ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.1 30

Matriz de Adjacência: Operadores


/∗ Operadores para obter a l i s t a de adjacentes ∗/
TipoApontador PrimeiroListaAdj ( TipoValorVertice ∗ Vertice , TipoGrafo ∗Grafo)
{ TipoValorVertice Result ;
TipoApontador Aux = 0;
short ListaVazia = TRUE ;
while (Aux < Grafo−>NumVertices && ListaVazia )
{ i f ( Grafo−>Mat[∗ Vertice ] [Aux] > 0 ) { Result = Aux; ListaVazia = FALSE ; }
else Aux++;
}
i f (Aux == Grafo−>NumVertices)
p r i n t f ( "Erro : Lista adjacencia vazia ( PrimeiroListaAdj ) \n" ) ;
return Result ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.1 31

Matriz de Adjacência: Operadores


/∗ Operadores para obter a l i s t a de adjacentes ∗/
void ProxAdj( TipoValorVertice ∗ Vertice , TipoGrafo ∗Grafo,
TipoValorVertice ∗Adj , TipoPeso ∗Peso,
TipoApontador ∗Prox , short ∗FimListaAdj)
{ /∗ Retorna Adj apontado por Prox ∗/
∗Adj = ∗Prox;
∗Peso = Grafo−>Mat[∗ Vertice ] [ ∗Prox ] ;
(∗Prox)++;
while (∗Prox < Grafo−>NumVertices &&
Grafo−>Mat[∗ Vertice ] [ ∗Prox] == 0) (∗Prox)++;
i f (∗Prox == Grafo−>NumVertices) ∗ FimListaAdj = TRUE ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.1 32

Matriz de Adjacência: Operadores


void RetiraAresta ( TipoValorVertice ∗V1, TipoValorVertice ∗V2,
TipoPeso ∗Peso, TipoGrafo ∗Grafo)
{ i f ( Grafo−>Mat[∗V1] [ ∗V2] == 0) p r i n t f ( "Aresta nao existe \n" ) ;
else { ∗Peso = Grafo−>Mat[∗V1] [ ∗V2] ; Grafo−>Mat[∗V1] [ ∗V2] = 0 ; }
}
void LiberaGrafo(TipoGrafo ∗Grafo)
{ /∗ Nao faz nada no caso de matrizes de adjacencia ∗/ }
void ImprimeGrafo(TipoGrafo ∗Grafo)
{ short i , j ; p r i n t f ( " " );
for ( i = 0; i <= Grafo−>NumVertices − 1; i ++) p r i n t f ( "%3d" , i ) ;
p r i n t f ( " \n" ) ;
for ( i = 0; i <= Grafo−>NumVertices − 1; i ++)
{ p r i n t f ( "%3d" , i ) ;
for ( j = 0; j <=Grafo−>NumVertices − 1; j ++)
p r i n t f ( "%3d" , Grafo−>Mat[ i ] [ j ] ) ;
p r i n t f ( " \n" ) ;
}
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.2 33

Listas de Adjacência Usando Apontadores

3 0 1 5
5
0 1 1 3 2 7
1
7 2
3 2 3

0 1 5
5
0 1 0 5 2 7
1
7 1 7
2
3 2
3

• Um arranjo Adj de |V | listas, uma para cada vértice em V .

• Para cada u ∈ V , Adj[u] contém os vértices adjacentes a u em G.


Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.2 34

Listas de Adjacência Usando Apontadores: Análise

• Os vértices de uma lista de adjacência são em geral armazenados em


uma ordem arbitrária.

• Possui uma complexidade de espaço O(|V | + |A|)

• Indicada para grafos esparsos, onde |A| é muito menor do que |V |2 .

• É compacta e usualmente utilizada na maioria das aplicações.

• A principal desvantagem é que ela pode ter tempo O(|V |) para


determinar se existe uma aresta entre o vértice i e o vértice j, pois
podem existir O(|V |) vértices na lista de adjacentes do vértice i.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.2 35

Listas de Adjacência Usando Apontadores (1)


#define MAXNUMVERTICES 100
#define MAXNUMARESTAS 4500
typedef int TipoValorVertice ;
typedef int TipoPeso;
typedef struct TipoItem {
TipoValorVertice Vertice ;
TipoPeso Peso;
} TipoItem ;
typedef struct TipoCelula ∗TipoApontador;
struct TipoCelula {
TipoItem Item ;
TipoApontador Prox;
} TipoCelula ;
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.2 36

Listas de Adjacência Usando Apontadores (2)


typedef struct TipoLista {
TipoApontador Primeiro , Ultimo ;
} TipoLista ;
typedef struct TipoGrafo {
TipoLista Adj [ MAXNUMVERTICES + 1];
TipoValorVertice NumVertices;
short NumArestas;
} TipoGrafo ;

• No uso de apontadores a lista é constituída de células, onde cada


célula contém um item da lista e um apontador para a célula seguinte.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.2 37

Listas de Adjacência Usando Apontadores: Operadores


/∗−− Entram aqui os operadores FLVazia, Vazia, Insere, Retira e Imprime−−∗/
/∗−− do TAD Lista de Apontadores do Programa 3.4 −−∗/
void FGVazio(TipoGrafo ∗Grafo)
{ long i ;
for ( i = 0; i < Grafo−>NumVertices ; i ++) FLVazia(&Grafo−>Adj [ i ] ) ;
}

void InsereAresta( TipoValorVertice ∗V1, TipoValorVertice ∗V2,


TipoPeso ∗Peso, TipoGrafo ∗Grafo)
{ TipoItem x ;
x . Vertice = ∗V2;
x .Peso = ∗Peso;
Insere(&x, &Grafo−>Adj[∗V1] ) ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.2 38

Listas de Adjacência usando Apontadores


short ExisteAresta ( TipoValorVertice Vertice1 ,
TipoValorVertice Vertice2 ,
TipoGrafo ∗Grafo)
{ TipoApontador Aux;
short EncontrouAresta = FALSE ;
Aux = Grafo−>Adj [ Vertice1 ] . Primeiro−>Prox;
while (Aux ! = NULL && EncontrouAresta == FALSE)
{ i f ( Vertice2 == Aux−>Item . Vertice ) EncontrouAresta = TRUE ;
Aux = Aux−>Prox;
}
return EncontrouAresta ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.2 39

Listas de Adjacência Usando Apontadores: Operadores


/∗ Operadores para obter a l i s t a de adjacentes ∗/
short ListaAdjVazia( TipoValorVertice ∗ Vertice , TipoGrafo ∗Grafo)
{ return ( Grafo−>Adj[∗ Vertice ] . Primeiro == Grafo−>Adj[∗ Vertice ] . Ultimo ) ;
}

TipoApontador PrimeiroListaAdj ( TipoValorVertice ∗ Vertice , TipoGrafo ∗Grafo)


{ return ( Grafo−>Adj[∗ Vertice ] . Primeiro−>Prox ) ; }

void ProxAdj( TipoValorVertice ∗ Vertice , TipoGrafo ∗Grafo,


TipoValorVertice ∗Adj , TipoPeso ∗Peso,
TipoApontador ∗Prox , short ∗FimListaAdj)
{ /∗ Retorna Adj e Peso do Item apontado por Prox ∗/
∗Adj = (∗Prox)−>Item . Vertice ;
∗Peso = (∗Prox)−>Item .Peso;
∗Prox = (∗Prox)−>Prox;
i f (∗Prox == NULL) ∗ FimListaAdj = TRUE ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.2 40

Listas de Adjacência Usando Apontadores: Operadores


void RetiraAresta ( TipoValorVertice ∗V1, TipoValorVertice ∗V2,
TipoPeso ∗Peso, TipoGrafo ∗Grafo)
{ TipoApontador AuxAnterior , Aux;
short EncontrouAresta = FALSE ;
TipoItem x ;
AuxAnterior = Grafo−>Adj[∗V1] . Primeiro ;
Aux = Grafo−>Adj[∗V1] . Primeiro−>Prox;
while (Aux ! = NULL && EncontrouAresta == FALSE)
{ i f (∗V2 == Aux−>Item . Vertice )
{ Retira (AuxAnterior, &Grafo−>Adj[∗V1] , &x ) ;
Grafo−>NumArestas−−;
EncontrouAresta = TRUE ;
}
AuxAnterior = Aux;
Aux = Aux−>Prox;
}
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.2 41

Listas de Adjacência Usando Apontadores: Operadores


void LiberaGrafo(TipoGrafo ∗Grafo)
{ TipoApontador AuxAnterior , Aux;
for ( i = 0; i < GRAfo−>NumVertices ; i ++)
{ Aux = Grafo−>Adj [ i ] . Primeiro−>Prox;
free (Grafo−>Adj [ i ] . Primeiro ) ; /∗Libera celula cabeca∗/
Grafo−>Adj [ i ] . Primeiro=NULL ;
while (Aux ! = NULL)
{ AuxAnterior = Aux;
Aux = Aux−>Prox;
free (AuxAnterior ) ;
}
}
Grafo−>NumVertices = 0;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.2 42

Listas de Adjacência Usando Apontadores: Operadores


void ImprimeGrafo(TipoGrafo ∗Grafo)
{ int i ;
TipoApontador Aux;
for ( i = 0; i < Grafo−>NumVertices ; i ++)
{ p r i n t f ( " Vertice%2d: " , i ) ;
i f ( ! Vazia(Grafo−>Adj [ i ] ) )
{ Aux = Grafo−>Adj [ i ] . Primeiro−>Prox;
while (Aux ! = NULL)
{ p r i n t f ( "%3d (%d) " , Aux−>Item . Vertice , Aux−>Item .Peso) ;
Aux = Aux−>Prox;
}
}
putchar( ’ \n ’ ) ;
}
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.3 43

Listas de Adjacência Usando Arranjos


3 Cab Prox Peso
0 4 4
5
0 1 1 6 5
V
2 2 0
7 3 3 0
4 1 0 5
3 2 A 5 1 6 3
6 2 0 7

Cab Prox Peso


0 4 4
5 1 6 5
0 1 V
2 7 7
3 3 0
7
4 1 0 5
3 2 5 0 6 5
A
6 2 0 7
7 1 0 7

• Cab: endereços do último item da lista de adjacentes de cada vértice


(nas |V | primeiras posições) e os vértices propriamente ditos (nas |A|
últimas posições)
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.3 44

Listas de Adjacência Usando Arranjos


#define MAXNUMVERTICES 100
#define MAXNUMARESTAS 4500
#define TRUE 1
#define FALSE 0
#define MAXTAM ( MAXNUMVERTICES + MAXNUMARESTAS ∗ 2)

typedef int TipoValorVertice ;


typedef int TipoPeso;
typedef int TipoTam;
typedef struct TipoGrafo {
TipoTam Cab[ MAXTAM + 1];
TipoTam Prox[ MAXTAM + 1];
TipoTam Peso[ MAXTAM + 1];
TipoTam ProxDisponivel ;
char NumVertices;
short NumArestas;
} TipoGrafo ;
typedef short TipoApontador;
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.3 45

Listas de Adjacência Usando Arranjos: Operadores


void FGVazio(TipoGrafo ∗Grafo)
{ short i ;
for ( i = 0; i <= Grafo−>NumVertices ; i ++)
{ Grafo−>Prox[ i ] = 0 ; Grafo−>Cab[ i ] = i ;
Grafo−>ProxDisponivel = Grafo−>NumVertices;
}
}
void InsereAresta( TipoValorVertice ∗V1, TipoValorVertice ∗V2,
TipoPeso ∗Peso, TipoGrafo ∗Grafo)
{ short Pos;
Pos = Grafo−>ProxDisponivel ;
i f ( Grafo−>ProxDisponivel == MAXTAM)
{ p r i n t f ( "nao ha espaco disponivel para a aresta \n" ) ; return ; }
Grafo−>ProxDisponivel++;
Grafo−>Prox[Grafo−>Cab[∗V1] ] = Pos;
Grafo−>Cab[Pos] = ∗V2; Grafo−>Cab[∗V1] = Pos;
Grafo−>Prox[Pos] = 0 ; Grafo−>Peso[Pos] = ∗Peso;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.3 46

Listas de Adjacência Usando Arranjos: Operadores


short ExisteAresta ( TipoValorVertice Vertice1 ,
TipoValorVertice Vertice2 , TipoGrafo ∗Grafo)
{ TipoApontador Aux;
short EncontrouAresta = FALSE ;
Aux = Grafo−>Prox[ Vertice1 ] ;
while (Aux != 0 && EncontrouAresta == FALSE)
{ i f ( Vertice2 == Grafo−>Cab[Aux] )
EncontrouAresta = TRUE ;
Aux = Grafo−>Prox[Aux] ;
}
return EncontrouAresta ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.3 47

Listas de Adjacência Usando Arranjos: Operadores


/∗ Operadores para obter a l i s t a de adjacentes ∗/
short ListaAdjVazia( TipoValorVertice ∗ Vertice , TipoGrafo ∗Grafo)
{ return ( Grafo−>Prox[∗ Vertice ] = = 0 ) ; }

TipoApontador PrimeiroListaAdj ( TipoValorVertice ∗ Vertice ,


TipoGrafo ∗Grafo)
{ return ( Grafo−>Prox[∗ Vertice ] ) ; }

void ProxAdj( TipoValorVertice ∗ Vertice , TipoGrafo ∗Grafo,


TipoValorVertice ∗Adj , TipoPeso ∗Peso,
TipoApontador ∗Prox , short ∗FimListaAdj)
{ /∗ Retorna Adj apontado por Prox ∗/
∗Adj = Grafo−>Cab[∗Prox ] ; ∗Peso = Grafo−>Peso[∗Prox ] ;
∗Prox = Grafo−>Prox[∗Prox ] ;
i f (∗Prox == 0) ∗FimListaAdj = TRUE ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.3 48

Listas de Adjacência Usando Arranjos: Operadores


void RetiraAresta ( TipoValorVertice ∗V1, TipoValorVertice ∗V2,
TipoPeso ∗Peso, TipoGrafo ∗Grafo)
{ TipoApontador Aux, AuxAnterior ; short EncontrouAresta = FALSE ;
AuxAnterior = ∗V1; Aux = Grafo−>Prox[∗V1] ;
while (Aux != 0 && EncontrouAresta == FALSE)
{ i f (∗V2 == Grafo−>Cab[Aux] ) EncontrouAresta = TRUE ;
else { AuxAnterior = Aux; Aux = Grafo−>Prox[Aux] ; }
}
i f ( EncontrouAresta ) /∗ Apenas marca como retirado ∗/
{ Grafo−>Cab[Aux] = MAXNUMVERTICES + MAXNUMARESTAS ∗ 2;
}
else p r i n t f ( "Aresta nao existe \n" ) ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.2.3 49

Listas de Adjacência Usando Arranjos: Operadores


void LiberaGrafo(TipoGrafo ∗Grafo)
{ /∗ Nao faz nada no caso de posicoes contiguas ∗/ }

void ImprimeGrafo(TipoGrafo ∗Grafo)


{ short i , forlim ;
printf ( " Cab Prox Peso\n" ) ;
forlim = Grafo−>NumVertices + Grafo−>NumArestas ∗ 2;
for ( i = 0; i <= forlim − 1; i ++)
p r i n t f ( "%2d%4d%4d%4d\n" , i , Grafo−>Cab[ i ] ,
Grafo−>Prox[ i ] , Grafo−>Peso[ i ] ) ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.3 50

Busca em Profundidade

• A busca em profundidade, do inglês depth-first search), é um algoritmo


para caminhar no grafo.

• A estratégia é buscar o mais profundo no grafo sempre que possível.

• As arestas são exploradas a partir do vértice v mais recentemente


descoberto que ainda possui arestas não exploradas saindo dele.

• Quando todas as arestas adjacentes a v tiverem sido exploradas a


busca anda para trás para explorar vértices que saem do vértice do
qual v foi descoberto.

• O algoritmo é a base para muitos outros algoritmos importantes, tais


como verificação de grafos acíclicos, ordenação topológica e
componentes fortemente conectados.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.3 51

Busca em Profundidade

• Para acompanhar o progresso do algoritmo cada vértice é colorido de


branco, cinza ou preto.

• Todos os vértices são inicializados branco.

• Quando um vértice é descoberto pela primeira vez ele torna-se cinza,


e é tornado preto quando sua lista de adjacentes tenha sido
completamente examinada.

• d[v]: tempo de descoberta

• t[v]: tempo de término do exame da lista de adjacentes de v.

• Estes registros são inteiros entre 1 e 2|V | pois existe um evento de


descoberta e um evento de término para cada um dos |V | vértices.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.3 52

Busca em Profundidade: Implementação


void VisitaDfs ( TipoValorVertice u, TipoGrafo ∗Grafo,
TipoValorTempo∗ Tempo, TipoValorTempo∗ d,
TipoValorTempo∗ t , TipoCor∗ Cor, short∗ Antecessor)
{ char FimListaAdj ; TipoValorAresta Peso; TipoApontador Aux;
TipoValorVertice v ; Cor[u] = cinza ; ( ∗Tempo)++; d[u] = (∗Tempo) ;
p r i n t f ( " Visita%2d Tempo descoberta:%2d cinza \n" , u, d[u ] ) ; getchar ( ) ;
i f ( ! ListaAdjVazia(&u, Grafo) )
{ Aux = PrimeiroListaAdj(&u, Grafo ) ; FimListaAdj = FALSE ;
while ( ! FimListaAdj)
{ ProxAdj(&u, &v, &Peso, &Aux, &FimListaAdj ) ;
i f (Cor[ v] == branco)
{ Antecessor[ v ] = u ; VisitaDfs (v , Grafo , Tempo, d, t , Cor, Antecessor ) ;
}
}
}
Cor[u] = preto ; ( ∗Tempo)++; t [u] = (∗Tempo) ;
p r i n t f ( " Visita%2d Tempo termino:%2d preto \n" , u, t [u ] ) ; getchar ( ) ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.3 53

Busca em Profundidade: Implementação


void BuscaEmProfundidade(TipoGrafo ∗Grafo)
{ TipoValorVertice x ;
TipoValorTempo Tempo;
TipoValorTempo d[ MAXNUMVERTICES + 1] , t [ MAXNUMVERTICES + 1];
TipoCor Cor[ MAXNUMVERTICES+1];
short Antecessor[ MAXNUMVERTICES+1];
Tempo = 0;
for ( x = 0; x <= Grafo−>NumVertices − 1; x++)
{ Cor[ x ] = branco;
Antecessor[ x] = −1;
}
for ( x = 0; x <= Grafo−>NumVertices − 1; x++)
{ i f (Cor[ x] == branco)
VisitaDfs (x , Grafo, &Tempo, d, t , Cor, Antecessor ) ;
}
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.3 54

Busca em Profundidade: Exemplo


b( / ) b( / ) c(1/ ) b( / ) c(1/ ) c(2/ )
0 1 0 1 0 1

b( / ) b( / ) b( / )

b( / ) 2 3 b( / ) 2 3 b( / ) 2 3

(a) (b) (c)

c(1/ ) c(2/ ) c(1/ ) c(2/ ) c(1/ ) p(2/5)


0 1 0 1 0 1

b( / ) b( / ) b( / )

c(3/ ) 2 3 p(3/4) 2 3 p(3/4) 2 3

(d) (e) (f)

p(1/6) p(2/5) p(1/6) p(2/5) p(1/6) p(2/5)


0 1 0 1 0 1

b( / ) c(7/ ) p(7/8)

p(3/4) 2 3 p(3/4) 2 3 p(3/4) 2 3

(g) (h) (i)


Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.3 55

Busca em Profundidade: Análise

• Os dois anéis da BuscaEmProfundidade têm custo O(|V |) cada um, a


menos da chamada do procedimento VisitaDfs(u) no segundo anel.

• O procedimento VisitaDfs é chamado exatamente uma vez para cada


vértice u ∈ V , desde que VisitaDfs é chamado apenas para vértices
brancos e a primeira ação é pintar o vértice de cinza.

• Durante a execução de VisitaDfs(u) o anel principal é executado


|Adj[u]| vezes.

• Desde que u∈V |Adj[u]| = O(|A|), o tempo total de execução de


P

VisitaDfs é O(|A|).

• Logo, a complexidade total da BuscaEmProfundidade é O(|V | + |A|).


Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.3.1 56

Classificação de Arestas

1. Arestas de árvore: são arestas de uma árvore de busca em


profundidade. A aresta (u, v) é uma aresta de árvore se v foi
descoberto pela primeira vez ao percorrer a aresta (u, v).

2. Arestas de retorno: conectam um vértice u com um antecessor v em


uma árvore de busca em profundidade (inclui self-loops).

3. Arestas de avanço: não pertencem à árvore de busca em


profundidade mas conectam um vértice a um descendente que
pertence à árvore de busca em profundidade.

4. Arestas de cruzamento: podem conectar vértices na mesma árvore


de busca em profundidade, ou em duas árvores diferentes.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.3.1 57

Classificação de Arestas
• Classificação de arestas pode ser útil para derivar outros algoritmos.
• Na busca em profundidade cada aresta pode ser classificada pela cor
do vértice que é alcançado pela primeira vez:
– Branco indica uma aresta de árvore.
– Cinza indica uma aresta de retorno.
– Preto indica uma aresta de avanço quando u é descoberto antes de
v ou uma aresta de cruzamento caso contrário.
3/6 2/9 1/10
arv arv
2 1 0

arv ret arv


avan cruz

3 cruz 4 cruz 5
4/5 7/8 11/12
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.4.1 58

Teste para Verificar se Grafo é Acíclico


Usando Busca em Profundidade
• A busca em profundidade pode ser usada para verificar se um grafo é
acíclico ou contém um ou mais ciclos.
• Se uma aresta de retorno é encontrada durante a busca em
profundidade em G, então o grafo tem ciclo.
• Um grafo direcionado G é acíclico se e somente se a busca em
profundidade em G não apresentar arestas de retorno.
• O algoritmo BuscaEmProfundidade pode ser alterado para descobrir
arestas de retorno. Para isso, basta verificar se um vértice v adjacente
a um vértice u apresenta a cor cinza na primeira vez que a aresta
(u, v) é percorrida.
• O algoritmo tem custo linear no número de vértices e de arestas de um
grafo G = (V, A) que pode ser utilizado para verificar se G é acíclico.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.4.2 59

Teste para Verificar se Grafo é Acíclico


Usando o Tipo Abstrato de Dados Hipergrafo
• Hipergrafos ou r−grafos Gr (V, A) são apresentados na Seção 7.10
(Slide 119).
• Representação: por meio de estruturas de dados orientadas a arestas
em que para cada vértice v do grafo é mantida uma lista das arestas
que incidem sobre o vértice v.
• Existem duas representações usuais para hipergrafos: matrizes de
incidência e listas de incidência. Aqui utilizaremos a implementação
de listas de incidência usando arranjos apresentada na Seção 7.10.2.
• O programa a seguir utiliza a seguinte propriedade de r-grafos:
Um r-grafo é acíclico se e somente se a remoção repetida de
arestas contendo apenas vértices de grau 1 (vértices sobre os
quais incide apenas uma aresta) elimina todas as arestas do grafo.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.4.2 60

Teste para Verificar se Grafo é Acíclico


Usando o Tipo Abstrato de Dados Hipergrafo

• O procedimento a seguir recebe o grafo e retorna no vetor L as arestas


retiradas do grafo na ordem em foram retiradas.

• O procedimento primeiro procura os vértices de grau 1 e os coloca em


uma fila. A seguir, enquanto a fila não estiver vazia, desenfileira um
vértice e retira a aresta incidente ao vértice.

• Se a aresta retirada tinha algum outro vértice de grau 2, então esse


vértice muda para grau 1 e é enfileirado.

• Se ao final não restar nenhuma aresta, então o grafo é acíclico. O


custo do procedimento GrafoAciclico é O(|V | + |A|).
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.4.2 61

Teste para Verificar se Grafo é Acíclico


Usando o Tipo Abstrato de Dados Hipergrafo
program GrafoAciclico ;
/∗ ∗ Entram aqui os tipos do Programa 3.17 (ou do Programa 3.19 ∗ ∗/
/∗ ∗ Entram aqui tipos do Programa 7.25 ( Slide 137) ∗ ∗/
int i , j ;
TipoAresta Aresta ;
TipoGrafo Grafo;
TipoArranjoArestas L;
short GAciclico ;
/∗ ∗ Entram aqui os operadores FFVazia, Vazia , Enfileira e ∗∗/
/∗ ∗ Desenfileira do Programa 3.18 (ou do Programa 3.20 ∗∗/
/∗ ∗ Entram aqui os operadores ArestasIguais , FGVazio, InsereAresta , ∗∗/
/∗ ∗ RetiraAresta e ImprimeGrafo do Programa 7.26 ( Slide 138 ) ∗∗/
short VerticeGrauUm( TipoValorVertice ∗V,
TipoGrafo ∗Grafo)
{ return ( Grafo−>Prim[∗V] >= 0) && (Grafo−>Prox[Grafo−>Prim[∗V]] == INDEFINIDO ) ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.4.2 62

Teste para Verificar se Grafo é Acíclico


Usando o Tipo Abstrato de Dados Hipergrafo (1)
void GrafoAciclico ( TipoGrafo ∗Grafo,
TipoArranjoArestas L, short ∗GAciclico )
{ TipoValorVertice j = 0; TipoValorAresta A1;
TipoItem x ; TipoFila Fila ; TipoValorAresta NArestas;
TipoAresta Aresta ; NArestas = Grafo−>NumArestas;
FFVazia (& Fila ) ;
while ( j < Grafo−>NumVertices)
{ i f (VerticeGrauUm (& j , Grafo) )
{ x .Chave = j ; Enfileira ( x, & Fila ) ; }
j ++;
}
while ( ! Vazia(&Fila ) && (NArestas > 0))
{ Desenfileira (& Fila , &x ) ;
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.4.2 63

Teste para Verificar se Grafo é Acíclico


Usando o Tipo Abstrato de Dados Hipergrafo (2)
i f ( Grafo−>Prim[ x .Chave] >= 0)
{ A1 = Grafo−>Prim[ x .Chave] % Grafo−>NumArestas;
Aresta = RetiraAresta(&Grafo−>Arestas [A1] , Grafo ) ;
L[Grafo−>NumArestas − NArestas] = Aresta ;
NArestas = NArestas − 1;
i f ( NArestas > 0)
{ for ( j = 0; j < Grafo−>r ; j ++)
{ i f (VerticeGrauUm(&Aresta . Vertices [ j ] , Grafo) )
{ x .Chave = Aresta . Vertices [ j ] ; Enfileira ( x, & Fila ) ;
}
}
}
}
}
i f ( NArestas == 0) ∗GAciclico = TRUE ;
else ∗GAciclico = FALSE ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.5 64

Busca em Largura

• Expande a fronteira entre vértices descobertos e não descobertos


uniformemente através da largura da fronteira.

• O algoritmo descobre todos os vértices a uma distância k do vértice


origem antes de descobrir qualquer vértice a uma distância k + 1.

• O grafo G(V, A) pode ser direcionado ou não direcionado.


Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.5 65

Busca em Largura

• Cada vértice é colorido de branco, cinza ou preto.

• Todos os vértices são inicializados branco.

• Quando um vértice é descoberto pela primeira vez ele torna-se cinza.

• Vértices cinza e preto já foram descobertos, mas são distinguidos para


assegurar que a busca ocorra em largura.

• Se (u, v) ∈ A e o vértice u é preto, então o vértice v tem que ser cinza


ou preto.

• Vértices cinza podem ter alguns vértices adjacentes brancos, e eles


representam a fronteira entre vértices descobertos e não descobertos.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.5 66

Busca em Largura: Implementação


void BuscaEmLargura(TipoGrafo ∗Grafo)
{ TipoValorVertice x ;
int Dist [MaxNumvertices + 1];
TipoCor Cor[MaxNumvertices + 1];
int Antecessor[MaxNumvertices + 1];
for ( x = 0; x <= Grafo −> NumVertices − 1; x++)
{ Cor[ x ] = branco ; Dist [ x ] = I n f i n i t o ; Antecessor[ x] = −1; }
for ( x = 0; x <= Grafo −> NumVertices − 1; x++)
{ i f (Cor[ x] == branco)
VisitaBfs ( x , Grafo , Dist , Cor, Antecessor ) ;
}
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.5 67

Busca em Largura: Implementação


/∗ ∗ Entram aqui os operadores FFVazia, Vazia, Enfileira e Desenfileira do ∗ ∗/
/∗ ∗ do Programa 3.18 ou do Programa 3.20, dependendo da implementação ∗ ∗/
/∗ ∗ da busca em largura usar arranjos ou apontadores, respectivamente ∗ ∗/
void VisitaBfs ( TipoValorVertice u, TipoGrafo ∗Grafo,
int ∗ Dist , TipoCor ∗Cor, int ∗Antecessor)
{ TipoValorVertice v ; Apontador Aux; short FimListaAdj ;
TipoPeso Peso; TipoItem Item ; TipoFila Fila ;
Cor[u] = cinza ; Dist [u] = 0;
FFVazia(&Fila ) ;
Item . Vertice = u ; Item .Peso = 0;
Enfileira (Item, & Fila ) ;
p r i n t f ( " Visita origem %2d cor : cinza F: " , u) ;
ImprimeFila( Fila ) ; getchar ( ) ;
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.5 68

Busca em Largura: Implementação


while ( ! FilaVazia ( Fila ) )
{ Desenfileira(&Fila , &Item ) ;
u = Item . Vertice ;
i f ( ! ListaAdjVazia(&u, Grafo) )
{ Aux = PrimeiroListaAdj(&u, Grafo ) ;
FimListaAdj = FALSE ;
while ( FimListaAdj == FALSE)
{ ProxAdj(&u, &v, &Peso, &Aux, &FimListaAdj ) ;
i f (Cor[ v ] ! = branco ) continue;
Cor[ v ] = cinza ; Dist [ v ] = Dist [u] + 1;
Antecessor[ v ] = u ; Item . Vertice = v ;
Item .Peso = Peso; Enfileira (Item, & Fila ) ;
}
}
Cor[u] = preto ;
p r i n t f ( " Visita %2d Dist %2d cor : preto F: " , u, Dist [u ] ) ;
ImprimeFila( Fila ) ; getchar ( ) ;
}
} /∗ VisitaBfs ∗/
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.5 69

Busca em Largura: Exemplo


c(0) b( ) b( ) p(0) c(1) b( )
0 1 4 0 1 4
F 0 F 1 3
(a) (b)
0 1 1
3 2 5 3 2 5
b( ) b( ) b( ) c(1) b( ) b( )

p(0) p(1) b( ) p(0) p(1) b( )


0 1 4 0 1 4
F 3 2 F 2
(c) (d)
1 2 2
3 2 5 3 2 5
c(1) c(2) b( ) p(1) c(2) b( )

p(0) p(1) b( ) p(0) p(1) c(0)


0 1 4 0 1 4
F F 4
(e) (f)
0
3 2 5 3 2 5
p(1) p(2) b( ) p(1) p(2) b( )

p(0) p(1) p(0) p(0) p(1) p(0)


0 1 4 0 1 4
F 5 F
(g) (h)
1
3 2 5 3 2 5
p(1) p(2) c(1) p(1) p(2) p(1)
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.5 70

Busca em Largura: Análise (Para Listas de Adjacência)

• O custo de inicialização do primeiro anel em BuscaEmLargura é


O(|V |) cada um.

• O custo do segundo anel é também O(|V |).

• VisitaBfs: enfileirar e desenfileirar têm custo O(1), logo, o custo total


com a fila é O(|V |).

• Cada lista de adjacentes é percorrida no máximo uma vez, quando o


vértice é desenfileirado.

• Desde que a soma de todas as listas de adjacentes é O(|A|), o tempo


total gasto com as listas de adjacentes é O(|A|).

• Complexidade total: é O(|V | + |A|).


Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.5 71

Caminhos Mais Curtos


• A busca em largura obtém o caminho mais curto de u até v.
• O procedimento VisitaBfs contrói uma árvore de busca em largura que
é armazenada na variável Antecessor.
• O programa a seguir imprime os vértices do caminho mais curto entre
o vértice origem e outro vértice qualquer do grafo.
void ImprimeCaminho( TipoValorVertice Origem, TipoValorVertice v,
TipoGrafo ∗Grafo , int ∗ Dist , TipoCor ∗Cor,
int ∗Antecessor)
{ i f (Origem == v ) { p r i n t f ( "%d " , Origem) ; return ; }
i f ( Antecessor[ v] == −1)
p r i n t f ( "Nao existe caminho de %d ate %d" , Origem, v ) ;
else { ImprimeCaminho(Origem,Antecessor[ v ] , Grafo , Dist , Cor, Antecessor ) ;
p r i n t f ( "%d " , v ) ;
}
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.6 72

Ordenação Topológica

• Ordenação linear de todos os vértices, tal que se G contém uma


aresta (u, v) então u aparece antes de v.

• Pode ser vista como uma ordenação de seus vértices ao longo de uma
linha horizontal de tal forma que todas as arestas estão direcionadas
da esquerda para a direita.

• Pode ser feita usando a busca em profundidade.


Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.6 73

Ordenação Topológica
• Os grafos direcionados acíclicos são usados para indicar precedências
entre eventos.
• Uma aresta direcionada (u, v) indica que a atividade u tem que ser
realizada antes da atividade v.
1/18 16/17 19/20
0 5 9
2/15 7/12
4/5 3 1 6 8 9/10

2 4 7
3/14 6/13 8/11

9 0 5 1 2 4 6 7 8 3
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.6 74

Ordenação Topológica

• Algoritmo para ordenar topologicamente um grafo direcionado acíclico


G = (V, A):
1. Chama BuscaEmProfundidade(G) para obter os tempos de término
t[u] para cada vértice u.
2. Ao término de cada vértice insira-o na frente de uma lista linear
encadeada.
3. Retorna a lista encadeada de vértices.

• A Custo O(|V | + |A|), uma vez que a busca em profundidade tem


complexidade de tempo O(|V | + |A|) e o custo para inserir cada um
dos |V | vértices na frente da lista linear encadeada custa O(1).
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.6 75

Ordenação Topológica: Implementação

• Basta inserir uma chamada para o procedimento InsLista no


procedimento BuscaDfs, logo após o momento em que o tempo de
término t[u] é obtido e o vértice é pintado de preto.

• Ao final, basta retornar a lista obtida (ou imprimí-la usando o


procedimento Imprime do Programa 3.4).

void InsLista ( TipoItem ∗Item , TipoLista ∗ Lista )


{ /∗−− Insere antes do primeiro item da l i s t a −−∗/
TipoApontador Aux;
Aux = Lista−>Primeiro−>Prox;
Lista−>Primeiro−>Prox = (TipoApontador)malloc(sizeof( tipoCelula ) ) ;
Lista−>Primeiro−>Prox−>Item = Item ;
Lista−>Primeiro−>Prox−>Prox = Aux;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.7 76

Componentes Fortemente Conectados

• Um componente fortemente conectado de G = (V, A) é um conjunto


maximal de vértices C ⊆ V tal que para todo par de vértices u e v em
C, u e v são mutuamente alcançáveis

• Podemos particionar V em conjuntos Vi , 1 ≤ i ≤ r, tal que vértices u e


v são equivalentes se e somente se existe um caminho de u a v e um
caminho de v a u.

0 1 0 1 0,1,2

3 2 3 2 3

(a) (b) (c)


Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.7 77

Componentes Fortemente Conectados: Algoritmo

• Usa o transposto de G, definido GT = (V, AT ), onde


AT = {(u, v) : (v, u) ∈ A}, isto é, AT consiste das arestas de G com
suas direções invertidas.

• G e GT possuem os mesmos componentes fortemente conectados,


isto é, u e v são mutuamente alcançáveis a partir de cada um em G se
e somente se u e v são mutuamente alcançáveis a partir de cada um
em GT .
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.7 78

Componentes Fortemente Conectados: Algoritmo

1. Chama BuscaEmProfundidade(G) para obter os tempos de término


t[u] para cada vértice u.

2. Obtem GT .

3. Chama BuscaEmProfundidade(GT ), realizando a busca a partir do


vértice de maior t[u] obtido na linha 1. Inicie uma nova busca em
profundidade a partir do vértice de maior t[u] dentre os vértices
restantes se houver.

4. Retorne os vértices de cada árvore da floresta obtida como um


componente fortemente conectado separado.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.7 79

Componentes Fortemente Conectados: Exemplo

• A parte (b) apresenta o resultado da busca em profundidade sobre o


grafo transposto obtido, mostrando os tempos de término e a
classificação das arestas.

• A busca em profundidade em GT resulta na floresta de árvores


mostrada na parte (c).
cruz
0 3
1/8 2/7 1/6 3/4
ret arv cruz
0 1 0 1
cruz arv arv ret 2

3 2 3 2 arv
cruz
4/5 3/6 7/8 2/5
1

(a) (b) (c)


Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.7 80

Componentes Fortemente Conectados: Implementação


void GrafoTransposto(TipoGrafo ∗Grafo , TipoGrafo ∗GrafoT)
{ TipoValorVertice v , Adj ;
TipoPeso Peso;
TipoApontador Aux;
FGVazio(GrafoT) ;
GrafoT−>NumVertices = Grafo−>NumVertices;
GrafoT−>NumArestas = Grafo−>NumArestas;
for ( v = 0; v <= Grafo−>NumVertices − 1; v++)
{ i f ( ! ListaAdjVazia(&v , Grafo) )
{ Aux = PrimeiroListaAdj(&v , Grafo ) ;
FimListaAdj = FALSE ;
while ( ! FimListaAdj)
{ ProxAdj(&v , Grafo, &Adj, &Peso, &Aux, &FimListaAdj ) ;
InsereAresta(&Adj, &v, &Peso, GrafoT) ;
}
}
}
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.7 81

Componentes Fortemente Conectados: Implementação


typedef struct TipoTempoTermino {
TipoValorTempo t [ MAXNUMVERTICES + 1];
short Restantes[ MAXNUMVERTICES + 1];
TipoValorVertice NumRestantes;
} TipoTempoTermino;

TipoValorVertice MaxTT(TipoTempoTermino ∗TT , TipoGrafo ∗Grafo)


{ TipoValorVertice Result ; short i = 0 , Temp;
while ( ! TT−>Restantes[ i ] ) i ++;
Temp = TT−>t [ i ] ; Result = i ;
for ( i = 0; i <= Grafo−>NumVertices − 1; i ++)
{ i f ( TT−>Restantes[ i ] )
{ i f (Temp < TT−>t [ i ] ) { Temp = TT−>t [ i ] ; Result = i ; }
}
}
return Result ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.7 82

Componentes Fortemente Conectados: Implementação


void BuscaEmProfundidadeCfc(TipoGrafo ∗Grafo , TipoTempoTermino ∗TT)
{ TipoValorTempo Tempo;
TipoValorTempo d[ MAXNUMVERTICES + 1] , t [ MAXNUMVERTICES + 1];
TipoCor Cor[ MAXNUMVERTICES + 1];
short Antecessor[ MAXNUMVERTICES + 1];
TipoValorVertice x , VRaiz ; Tempo = 0;
for ( x = 0; x <= Grafo−>NumVertices − 1; x++)
{ Cor[ x ] = branco ; Antecessor[ x] = −1; }
TT−>NumRestantes = Grafo−>NumVertices;
for ( x = 0; x <= Grafo−>NumVertices − 1; x++)
TT−>Restantes[ x ] = TRUE ;
while ( TT−>NumRestantes > 0)
{ VRaiz = MaxTT(TT , Grafo ) ;
p r i n t f ( "Raiz da proxima arvore:%2d\n" , VRaiz) ;
VisitaDfs2 (VRaiz, Grafo , TT, &Tempo, d, t ,Cor, Antecessor ) ;
}
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.7 83

Componentes Fortemente Conectados: Implementação


void VisitaDfs2 ( TipoValorVertice u, TipoGrafo ∗Grafo,
TipoTempoTermino ∗TT , TipoValorTempo ∗Tempo,
TipoValorTempo ∗d, TipoValorTempo ∗ t ,
TipoCor ∗Cor, short ∗Antecessor)
{ short FimListaAdj ; TipoPeso Peso; TipoApontador Aux; TipoValorVertice v ;
Cor[u] = cinza ;
(∗Tempo)++; d[u] = (∗Tempo) ;
TT−>Restantes[u] = FALSE ;
TT−>NumRestantes −−;
p r i n t f ( " Visita%2d Tempo descoberta:%2d cinza \n" ,u,d[u ] ) ;
getchar ( ) ;
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.7 84

Componentes Fortemente Conectados: Implementação


i f ( ! ListaAdjVazia(&u, Grafo) )
{ Aux = PrimeiroListaAdj(&u, Grafo ) ;
FimListaAdj = FALSE ;
while ( ! FimListaAdj)
{ ProxAdj(&u, Grafo, &v, &Peso, &Aux, &FimListaAdj ) ;
i f (Cor[ v] == branco)
{ Antecessor[ v ] = u;
VisitaDfs2 ( v , Grafo , TT , Tempo, d, t , Cor, Antecessor ) ;
}
}
}
Cor[u] = preto ; ( ∗Tempo)++;
t [u] = (∗Tempo) ;
p r i n t f ( " Visita%2d Tempo termino:%2d preto \n" , u, t [u ] ) ;
getchar ( ) ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.7 85

Componentes Fortemente Conectados: Análise

• Utiliza o algoritmo para busca em profundidade duas vezes, uma em G


e outra em GT .

• Logo, a complexidade total é O(|V | + |A|).


Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8 86

Árvore Geradora Mínima


• Projeto de redes de comunicações conectando n localidades.
• Arranjo de n − 1 conexões, conectando duas localidades cada.
• Objetivo: dentre as possibilidades de conexões, achar a que usa
menor quantidade de cabos.
• Modelagem:
– G = (V, A): grafo conectado, não direcionado.
– V : conjunto de cidades.
– A: conjunto de possíveis conexões
– p(u, v): peso da aresta (u, v) ∈ A, custo total de cabo para conectar
u a v.
• Solução: encontrar um subconjunto T ⊆ A, acíclico, que conecta todos
os vértices de G e cujo peso total p(T ) = (u,v)∈T p(u, v) é minimizado.
P
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8 87

Árvore Geradora Mínima


• Como G′ = (V, T ) é acíclico e conecta todos os vértices, T forma uma
árvore chamada árvore geradora de G.
• O problema de obter a árvore T é conhecido como árvore geradora
mínima (AGM).
Ex.: Árvore geradora mínima T cujo peso total é 12. T não é única,
pode-se substituir a aresta (3, 5) pela aresta (2, 5) obtendo outra árvore
geradora de custo 12.

0 0
6 5
1 1
1 2 2 3 1 2 2 3
2 2
5 6 4 4 4
4 5 4 5
3 3
(a) (b)
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.1 88

AGM - Algoritmo Genérico


void GenericoAGM( )
1{ S = ∅ ;
2 while(S não constitui uma árvore geradora mínima)
3 { ( u, v ) = seleciona(A) ;
4 i f ( aresta ( u, v ) é segura para S ) S = S+ { ( u, v ) } }
5 return S ;
}
• Uma estratégia gulosa permite obter a AGM adicionando uma aresta
de cada vez.
• Invariante: Antes de cada iteração, S é um subconjunto de uma árvore
geradora mínima.
• A cada passo adicionamos a S uma aresta (u, v) que não viola o
invariante. (u, v) é chamada de uma aresta segura.
• Dentro do while, S tem que ser um subconjunto próprio da AGM T , e
assim tem que existir uma aresta (u, v) ∈ T tal que (u, v) 6∈ S e (u, v) é
seguro para S.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.1 89

AGM - Definição de Corte


• Um corte (V ′ , V − V ′ ) de um grafo não direcionado G = (V, A) é uma
partição de V .
• Uma aresta (u, v) ∈ A cruza o corte (V ′ , V − V ′ ) se um de seus
vértices pertence a V ′ e o outro vértice pertence a V − V ′ .
• Um corte respeita um conjunto S de arestas se não existirem arestas
em S que o cruzem.
• Uma aresta cruzando o corte que tenha custo mínimo sobre todas as
arestas cruzando o corte é uma aresta leve.
p
0
p 6 5 p
1
V’ 1 2 2 3 V’
2
V − V’ 5 4 V − V’
6 4
4 5
3
b b
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.1 90

AGM - Teorema para Reconhecer Arestas Seguras

• Considere G = (V, A) um grafo conectado, não direcionado, com


pesos p sobre as arestas V .

• Considere S um subconjunto de V que está incluído em alguma AGM


para G.

• Considere (V ′ , V − V ′ ) um corte qualquer que respeita S.

• Considere (u, v) uma aresta leve cruzando (V ′ , V − V ′ ).

• Satisfeitas essas condições, (u, v) é uma aresta segura para S.


Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.2 91

Algoritmo de Prim para Obter Uma AGM

• O algoritmo de Prim para obter uma AGM pode ser derivado do


algoritmo genérico.

• O subconjunto S forma uma única árvore, e a aresta segura


adicionada a S é sempre uma aresta de peso mínimo conectando a
árvore a um vértice que não esteja na árvore.

• A árvore começa por um vértice qualquer (no caso 0) e cresce até que
“gere” todos os vértices em V .

• A cada passo, uma aresta leve é adicionada à árvore S, conectando S


a um vértice de GS = (V, S).

• De acordo com o teorema anterior, quando o algoritmo termina, as


arestas em S formam uma árvore geradora mínima.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.2 92

Algoritmo de Prim: Exemplo

(a) (b) 0 (c) 0


0 0 0
6 5 6 5 2 2
1
1 2 2 3 1 3 1 3
2 2 2
5 6 4 4 1 1
4 5 4 5 4 5
3 6 4

(d) 0 (e) 0 (f) 0


0 0 0
2 2 2 2 2 2
1 3 1 3 1 3
2 2 2
1 1 1
4 4 4 5 4 5
6 4 5 4 3 4
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.2 93

Prim: Operadores para Manter o Heap Indireto (1)


/∗ ∗ Entra aqui o operador Constroi da Seção 4.1.5 (Programa 4.10) ∗ ∗/
/∗ ∗ Trocando a chamada Refaz (Esq, n , A) por RefazInd (Esq, n, A) ∗ ∗/
void RefazInd( TipoIndice Esq, TipoIndice Dir , TipoItem ∗A,
TipoPeso ∗P, TipoValorVertice ∗Pos)
{ TipoIndice i = Esq; int j = i ∗ 2 ; TipoItem x ; x = A[ i ] ;
while ( j <= Dir )
{ i f ( j < Dir )
{ i f (P[A[ j ] .Chave] > P[A[ j +1].Chave] ) j ++; }
i f (P[ x .Chave] <= P[A[ j ] .Chave] ) goto L999;
A[ i ] = A[ j ] ; Pos[A[ j ] .Chave] = i ; i = j ;
j = i ∗ 2;
}
L999: A[ i ] = x ;
Pos[ x .Chave] = i ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.2 94

Prim: Operadores para Manter o Heap Indireto (2)


TipoItem RetiraMinInd(TipoItem ∗A, TipoPeso ∗P, TipoValorVertice ∗Pos)
{ TipoItem Result ;
i f (n < 1 ) { p r i n t f ( "Erro : heap vazio \n" ) ; return Result ; }
Result = A[ 1 ] ; A[1] = A[n ] ;
Pos[A[n ] .Chave] = 1 ; n−−;
RefazInd(1 , n, A, P, Pos ) ;
return Result ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.2 95

Prim: Operadores para Manter o Heap Indireto (3)


void DiminuiChaveInd( TipoIndice i , TipoPeso ChaveNova, TipoItem ∗A,
TipoPeso ∗P, TipoValorVertice ∗Pos)
{ TipoItem x ;
i f (ChaveNova > P[A[ i ] .Chave] )
{ p r i n t f ( "Erro : ChaveNova maior que a chave atual \n" ) ;
return ;
}
P[A[ i ] .Chave] = ChaveNova;
while ( i > 1 && P[A[ i / 2 ] .Chave] > P[A[ i ] .Chave] )
{ x = A[ i / 2 ] ; A[ i / 2 ] = A[ i ] ;
Pos[A[ i ] .Chave] = i / 2 ; A[ i ] = x ;
Pos[ x .Chave] = i ; i /= 2;
}
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.2 96

Algoritmo de Prim: Implementação


{−− Entram aqui operadores do tipo grafo do Slide 28 ou Slide 37 ou Slide 45, −−}
{−− e os operadores RefazInd , RetiraMinInd e DiminuiChaveInd do Slide 93 −−}
void AgmPrim(TipoGrafo ∗Grafo , TipoValorVertice ∗Raiz)
{ int Antecessor[ MAXNUMVERTICES + 1];
short Itensheap [ MAXNUMVERTICES + 1];
Vetor A;
TipoPeso P[ MAXNUMVERTICES + 1];
TipoValorVertice Pos[ MAXNUMVERTICES + 1 ] , u, v ;
TipoItem TEMP;
for (u = 0; u <= Grafo−>NumVertices ; u++)
{ /∗Constroi o heap com todos os valores igual a INFINITO∗/
Antecessor[u] = −1; P[u] = INFINITO ;
A[u+1].Chave = u ; /∗Heap a ser construido∗/
Itensheap [u] = TRUE ; Pos[u] = u + 1;
}
n = Grafo−>NumVertices ; P[∗Raiz] = 0;
Constroi(A, P, Pos) ;
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.2 97

Algoritmo de Prim: Implementação


while (n >= 1) /∗enquanto heap nao vazio∗/
{ TEMP = RetiraMinInd(A, P, Pos) ;
u = TEMP.Chave; Itensheap [u] = FALSE ;
i f (u != ∗Raiz)
p r i n t f ( "Aresta de arvore : v[%d ] v[%d] " ,u,Antecessor[u ] ) ; getchar ( ) ;
i f ( ! ListaAdjVazia(&u, Grafo) )
{ Aux = PrimeiroListaAdj(&u, Grafo ) ;
FimListaAdj = FALSE ;
while ( ! FimListaAdj)
{ ProxAdj(&u, Grafo, &v, &Peso, &Aux, &FimListaAdj ) ;
i f ( Itensheap [ v] && Peso < P[ v ] )
{ Antecessor[ v ] = u;
DiminuiChaveInd(Pos[ v ] , Peso, A, P, Pos) ;
}
}
}
}
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.2 98

Algoritmo de Prim: Implementação

• Para realizar de forma eficiente a seleção de uma nova aresta, todos


os vértices que não estão na AGM residem no heap A.

• O heap contém os vértices, mas a condição do heap é mantida pelo


peso da aresta através do arranjo p[v] (heap indireto).

• Pos[v] fornece a posição do vértice v dentro do heap A, para que o


vértice v possa ser acessado a um custo O(1), necessário para a
operação DiminuiChave.

• Antecessor[v] armazena o antecessor de v na árvore.

• Quando o algoritmo termina, A está vazia e a AGM está de forma


implícita como S = {(v, Antecessor [v]) : v ∈ V − {Raiz }}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.2 99

Algoritmo de Prim: Análise


• O corpo do anel while é executado |V | vezes.
• O procedimento Refaz tem custo O(log |V |).
• Logo, o tempo total para executar a operação retira o item com menor
peso é O(|V | log |V |).
• O while mais interno para percorrer a lista de adjacentes é O(|A|)
(soma dos comprimentos de todas as listas de adjacência é 2|A|).
• O teste para verificar se o vértice v pertence ao heap A custa O(1).
• Após testar se v pertence ao heap A e o peso da aresta (u, v) é menor
do que p[v], o antecessor de v é armazenado em Antecessor e uma
operação DiminuiChave é realizada sobre o heap A na posição Pos[v],
a qual tem custo O(log |V |).
• Logo, o tempo total para executar o algoritmo de Prim é
O(|V log |V | + |A| log |V |) = O(|A| log |V |).
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.3 100

AGM - Algoritmo de Kruskal


• Pode ser derivado do algoritmo genérico.
• S é uma floresta e a aresta segura adicionada a S é sempre uma
aresta de menor peso que conecta dois componentes distintos.
• Considera as arestas ordenadas pelo peso.
(a) (b) (c)
0 0 0
6 5
1
1 2 2 3 1 3 1 3
2 2 2
5 6 4 4
4 5 4 5 4 5
3

(d) (e) (f)


0 0 0

1 3 1 3 1 3
2 2 2

4 5 4 5 4 5
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.3 101

AGM - Algoritmo de Kruskal

• Sejam C1 e C2 duas árvores conectadas por (u, v):


– Como (u, v) tem de ser uma aresta leve conectando C1 com alguma
outra árvore, (u, v) é uma aresta segura para C1 .

• É guloso porque, a cada passo, ele adiciona à floresta uma aresta de


menor peso.

• Obtém uma AGM adicionando uma aresta de cada vez à floresta e, a


cada passo, usa a aresta de menor peso que não forma ciclo.

• Inicia com uma floresta de |V | árvores de um vértice: em |V | passos,


une duas árvores até que exista apenas uma árvore na floresta.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.3 102

Algoritmo de Kruskal: Implementação


• Usa fila de prioridades para obter arestas em ordem crescente de
pesos.
• Testa se uma aresta adicionada ao conjunto solução S forma um ciclo.
• Tratar conjuntos disjuntos: maneira eficiente de verificar se uma
dada aresta forma um ciclo. Utiliza estruturas dinâmicas.
• Os elementos de um conjunto são representados por um objeto.
Operações:
– CriaConjunto(x): cria novo conjunto cujo único membro, x, é seu
representante. Para que os conjuntos sejam disjuntos, x não pode
pertencer a outro conjunto.
– União(x, y): une conjuntos dinâmicos contendo x (Cx ) e y (Cy ) em
novo conjunto, cujo representante pode ser x ou y. Como os
conjuntos na coleção devem ser disjuntos, Cx e Cy são destruídos.
– EncontreConjunto(x): retorna apontador para o representante do
conjunto (único) contendo x.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.3 103

Algoritmo de Kruskal: Implementação


• Primeiro refinamento:

void Kruskal ( ) ;
{
1. S = ∅ ;
2. for (v=0;v < Grafo.NumVertices) CriaConjunto ( v ) ;
3. Ordena as arestas de A pelo peso;
4. for (cada ( u, v ) de A tomadas em ordem ascendente de peso)
5. i f ( EncontreConjunto (u) ! = EncontreConjunto ( v ) )
6. { S = S+ { (u, v ) } ;
7. Uniao ( u, v ) ;
}
}

• A implementação das operações União e EncontraConjunto deve ser


realizada de forma eficiente.
• Esse problema é conhecido na literatura como União-EncontraConjunto.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.8.3 104

AGM - Análise do Algoritmo de Kruskal


• A inicialização do conjunto S tem custo O(1).
• Ordenar arestas (linha 3) custa O(|A| log |A|).
• A linha 2 realiza |V | operações CriaConjunto.
• O anel (linhas 4-7) realiza O(|A|) operações EncontreConjunto e Uniao, a um
custo O((|V | + |A|)α(|V |)) onde α(|V |) é uma função que cresce lentamente
(α(|V |) < 4).
• O limite inferior para construir uma estrutura dinâmica envolvendo m
operações EncontreConjunto e Uniao e n operações CriaConjunto é mα(n).
• Como G é conectado temos que |A| ≥ |V | − 1, e assim as operações sobre
conjuntos disjuntos custam O(|A|α(|V |).
• Como α(|V |) = O(log |A|) = O(log |V |), o tempo total do algoritmo de Kruskal
é O(|A| log |A|).
• Como |A| < |V |2 , então log |A| = O(log |V |), e o custo do algoritmo de
Kruskal é também O(|A| log |V |).
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.9 105

Caminhos Mais Curtos: Aplicação


• Um motorista procura o caminho mais curto entre Diamantina e Ouro Preto.
Possui mapa com as distâncias entre cada par de interseções adjacentes.
• Modelagem:
– G = (V, A): grafo direcionado ponderado, mapa rodoviário.
– V : interseções.
– A: segmentos de estrada entre interseções
– p(u, v): peso de cada aresta, distância entre interseções.
Pk
• Peso de um caminho: p(c) = i=1 p(vi−1 , vi )

• Caminho mais curto:



c
n o
 min p(c) : u ; v se existir caminho de u a v
δ(u, v) =
 ∞ caso contrário

• Caminho mais curto do vértice u ao vértice v: qualquer caminho c com peso


p(c) = δ(u, v).
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.9 106

Caminhos Mais Curtos

• Caminhos mais curtos a partir de uma origem: dado um grafo


ponderado G = (V, A), desejamos obter o caminho mais curto a partir
de um dado vértice origem s ∈ V até cada v ∈ V .

• Muitos problemas podem ser resolvidos pelo algoritmo para o


problema origem única:
– Caminhos mais curtos com destino único: reduzido ao problema
origem única invertendo a direção de cada aresta do grafo.
– Caminhos mais curtos entre um par de vértices: o algoritmo
para origem única é a melhor opção conhecida.
– Caminhos mais curtos entre todos os pares de vértices:
resolvido aplicando o algoritmo origem única |V | vezes, uma vez
para cada vértice origem.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.9 107

Caminhos Mais Curtos


• A representação de caminhos mais curtos pode ser realizada pela variável
Antecessor.
• Para cada vértice v ∈ V o Antecessor [v] é um outro vértice u ∈ V ou nil (-1).
• O algoritmo atribui a Antecessor os rótulos de vértices de uma cadeia de
antecessores com origem em v e que anda para trás ao longo de um
caminho mais curto até o vértice origem s.
• Dado um vértice v no qual Antecessor [v] 6= nil , o procedimento
ImprimeCaminho pode imprimir o caminho mais curto de s até v.
• Os valores em Antecessor [v], em um passo intermediário, não indicam
necessariamente caminhos mais curtos.
• Entretanto, ao final do processamento, Antecessor contém uma árvore de
caminhos mais curtos definidos em termos dos pesos de cada aresta de G,
ao invés do número de arestas.
• Caminhos mais curtos não são necessariamente únicos.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.9 108

Árvore de caminhos mais curtos

• Uma árvore de caminhos mais curtos com raiz em u ∈ V é um


subgrafo direcionado G′ = (V ′ , A′ ), onde V ′ ⊆ V e A′ ⊆ A, tal que:
1. V ′ é o conjunto de vértices alcançáveis a partir de s ∈ G,
2. G′ forma uma árvore de raiz s,
3. para todos os vértices v ∈ V ′ , o caminho simples de s até v é um
caminho mais curto de s até v em G.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.9 109

Algoritmo de Dijkstra
• Mantém um conjunto S de vértices cujos caminhos mais curtos até um
vértice origem já são conhecidos.
• Produz uma árvore de caminhos mais curtos de um vértice origem s
para todos os vértices que são alcançáveis a partir de s.
• Utiliza a técnica de relaxamento:
– Para cada vértice v ∈ V o atributo p[v] é um limite superior do peso
de um caminho mais curto do vértice origem s até v.
– O vetor p[v] contém uma estimativa de um caminho mais curto.
• O primeiro passo do algoritmo é inicializar os antecessores e as
estimativas de caminhos mais curtos:
– Antecessor[v] = nil para todo vértice v ∈ V ,
– p[u] = 0, para o vértice origem s, e
– p[v] = ∞ para v ∈ V − {s}.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.9 110

Relaxamento

• O relaxamento de uma aresta (u, v) consiste em verificar se é


possível melhorar o melhor caminho até v obtido até o momento se
passarmos por u.

• Se isto acontecer, p[v] e Antecessor[v] devem ser atualizados.

i f (p[ v] > p[u] + peso da aresta ( u, v) )


{ p[ v ] = p[u] + peso da aresta ( u, v ) ; Antecessor[ v ] = u ; }
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.9 111

Algoritmo de Dijkstra: 1o Refinamento

void Dijkstra (Grafo , Raiz)


{
1. for (v=0;v < Grafo.NumVertices; v++)
2. p[ v ] = I n f i n i t o ;
3. Antecessor[ v] = −1;
4. p[Raiz] = 0;
5. Constroi heap no vetor A;
6. S = ∅ ;
7. while (heap > 1)
8. u = RetiraMin(A) ;
9. S = S + u;
10. for ( v ∈ ListaAdjacentes [u] )
11. i f (p[ v] > p[u] + peso da aresta (u, v) )
12. p[ v ] = p[u ] + peso da aresta (u, v ) ;
13. Antecessor[ v ] = u;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.9 112

Algoritmo de Dijkstra: 1o Refinamento

• Invariante: o número de elementos do heap é igual a V − S no início


do anel while.

• A cada iteração do while, um vértice u é extraído do heap e


adicionado ao conjunto S, mantendo assim o invariante.

• RetiraMin obtém o vértice u com o caminho mais curto estimado até o


momento e adiciona ao conjunto S.

• No anel da linha 10, a operação de relaxamento é realizada sobre


cada aresta (u, v) adjacente ao vértice u.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.9 113

Algoritmo de Dijkstra: Exemplo


(a) (b) 0 (c) 0
0 0 0
1 10 1 10 1 10
1 10 1 10
1 3 4 1 3 4 1 3 4

5 1 6 5 1 6 5 1 6
2 3 2 3 2 3
2 2 3 6 2 3

(d) 0 (e) 0 (f) 0


0 0 0
1 10 1 10 1 10
1 9 1 6 1 6
1 3 4 1 3 4 1 3 4

5 1 6 5 1 6 5 1 6
2 3 2 3 2 3
5 2 3 5 2 3 5 2 3

Iteração S d[0] d[1] d[2] d[3] d[4]

(a) ∅ ∞ ∞ ∞ ∞ ∞
(b) {0} 0 1 ∞ 3 10
(c) {0, 1} 0 1 6 3 10
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.9 114

Algoritmo de Dijkstra: Exemplo


(a) (b) 0 (c) 0
0 0 0
1 10 1 10 1 10
1 10 1 10
1 3 4 1 3 4 1 3 4

5 1 6 5 1 6 5 1 6
2 3 2 3 2 3
2 2 3 6 2 3

(d) 0 (e) 0 (f) 0


0 0 0
1 10 1 10 1 10
1 9 1 6 1 6
1 3 4 1 3 4 1 3 4

5 1 6 5 1 6 5 1 6
2 3 2 3 2 3
5 2 3 5 2 3 5 2 3

Iteração S d[0] d[1] d[2] d[3] d[4]

(d) {0, 1, 3} 0 1 5 3 9
(e) {0, 1, 3, 2} 0 1 5 3 6
(f) {0, 1, 3, 2, 4} 0 1 5 3 6
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.9 115

Algoritmo de Dijkstra

• Para realizar de forma eficiente a seleção de uma nova aresta, todos


os vértices que não estão na árvore de caminhos mais curtos residem
no heap A baseada no campo p.

• Para cada vértice v, p[v] é o caminho mais curto obtido até o momento,
de v até o vértice raiz.

• O heap mantém os vértices, mas a condição do heap é mantida pelo


caminho mais curto estimado até o momento através do arranjo p[v], o
heap é indireto.

• O arranjo Pos[v] fornece a posição do vértice v dentro do heap A,


permitindo assim que o vértice v possa ser acessado a um custo O(1)
para a operação DiminuiChaveInd.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.9 116

Algoritmo de Dijkstra: Implementação


/∗ ∗ Entram aqui os operadores de uma das implementações de grafos, bem como o ope-
rador Constroi da implementação de filas de prioridades, assim como os operadores Re-
fazInd, RetiraMinInd e DiminuiChaveInd do Programa Constroi ∗ ∗/
void Dijkstra (TipoGrafo ∗Grafo , TipoValorVertice ∗Raiz)
{ TipoPeso P[ MAXNUMVERTICES + 1];
TipoValorVertice Pos[ MAXNUMVERTICES + 1];
long Antecessor[ MAXNUMVERTICES + 1];
short Itensheap [ MAXNUMVERTICES + 1];
TipoVetor A; TipoValorVertice u, v ; TipoItem temp;
for (u = 0; u <= Grafo−>NumVertices ; u++)
{ /∗Constroi o heap com todos os valores igual a INFINITO∗/
Antecessor[u] = −1; P[u] = INFINITO ;
A[u+1].Chave = u ; /∗Heap a ser construido∗/
Itensheap [u] = TRUE ; Pos[u] = u + 1;
}
n = Grafo−>NumVertices;
P[∗(Raiz) ] = 0 ;
Constroi(A, P, Pos) ;
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.9 117

Algoritmo de Dijkstra: Implementação


while (n >= 1) /∗enquanto heap nao vazio∗/
{ temp = RetiraMinInd(A, P, Pos) ;
u = temp.Chave; Itensheap [u] = FALSE ;
i f ( ! ListaAdjVazia(&u, Grafo) )
{ Aux = PrimeiroListaAdj(&u, Grafo ) ; FimListaAdj = FALSE ;
while ( ! FimListaAdj)
{ ProxAdj(&u, Grafo, &v, &Peso, &Aux, &FimListaAdj ) ;
i f (P[ v] > (P[u] + Peso) )
{ P[ v ] = P[u] + Peso; Antecessor[ v ] = u;
DiminuiChaveInd(Pos[ v ] , P[ v ] , A, P, Pos) ;
p r i n t f ( "Caminho: v[%d ] v[%ld ] d[%d] " , v , Antecessor[ v ] , P[ v ] ) ;
scanf( "%∗[^\n] " ) ; getchar ( ) ;
}
}
}
}
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.9 118

Porque o Algoritmo de Dijkstra Funciona

• O algoritmo usa uma estratégia gulosa: sempre escolher o vértice


mais leve (ou o mais perto) em V − S para adicionar ao conjunto
solução S,

• O algorimo de Dijkstra sempre obtém os caminhos mais curtos, pois


cada vez que um vértice é adicionado ao conjunto S temos que
p[u] = δ(Raiz, u).
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10 119

O Tipo Abstrato de Dados Hipergrafo


• Um hipergrafo ou r−grafo é um grafo não direcionado Gr = (V, A) no
qual cada aresta a ∈ A conecta r vértices, sendo r a ordem do
hipergrafo.
• Os grafos estudados até agora são 2-grafos (hipergrafos de ordem 2).
• Hipergrafos são utilizados na Seção 5.5.4 sobre hashing perfeito.
• A figura apresenta um 3-grafo contendo os vértices {0, 1, 2, 3, 4, 5}, as
arestas {(1, 2, 4), (1, 3, 5), (0, 2, 5)} e os pesos 7, 8 e 9,
respectivamente.

0 11
00
1
00
11
h0 (x)
Arestas
00
11
00
11
(1, 2, 4, 7)
0
1
00
11
0
1
2
00
11
3
0
1
h1 (x) 11 (1, 3, 5, 8)
00
0
1
0
1
0
1
(0, 2, 5, 9)
4
0
1
5 h2 (x)
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10 120

O Tipo Abstrato de Dados Hipergrafo: Operações


1. Criar um hipergrafo vazio. A operação retorna um hipergrafo contendo
|V | vértices e nenhuma aresta.
2. Inserir uma aresta no hipergrafo. Recebe a aresta (V1 , V2 , . . . , Vr ) e seu
peso para serem inseridos no hipergrafo.
3. Verificar se existe determinada aresta no hipergrafo: retorna true se a
aresta (V1 , V2 , . . . , Vr ) estiver presente, senão retorna false.
4. Obter a lista de arestas incidentes em determinado vértice. Essa
operação será tratada separadamente logo a seguir.
5. Retirar uma aresta do hipergrafo. Retira a aresta (V1 , V2 , . . . , Vr ) do
hipergrafo e a retorna.
6. Imprimir um hipergrafo.
7. Obter a aresta de menor peso de um hipergrafo. A operação retira a
aresta de menor peso dentre as arestas do hipergrafo e a retorna.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10 121

O Tipo Abstrato de Dados Hipergrafo: Operações

• Uma operação que aparece com frequência é a de obter a lista de


arestas incidentes em determinado vértice.

• Para implementar esse operador de forma independente da


representação escolhida para a aplicação em pauta, precisamos de
três operações sobre hipergrafos, a saber:
1. Verificar se a lista de arestas incidentes em um vértice v está vazia.
A operação retorna true se a lista estiver vazia, senão retorna false.
2. Obter a primeira aresta incidente a um vértice v, caso exista.
3. Obter a próxima aresta incidente a um vértice v, caso exista.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10 122

O Tipo Abstrato de Dados Hipergrafo: Operações

• A forma mais adequada para representar um hipergrafo é por meio de


estruturas de dados em que para cada vértice v do grafo é mantida
uma lista das arestas que incidem sobre o vértice v, o que implica a
representação explícita de cada aresta do hipergrafo.

• Essa é uma estrutura orientada a arestas e não a vértices como as


representações apresentadas até agora.

• Existem duas representações usuais para hipergrafos: as matrizes de


incidência e as listas de incidência.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.1 123

Implementação por Matrizes de Incidência


• A matriz de incidência de Gr = (V, A) contendo
n vértices e m arestas é uma matriz n × m de
bits, em que A[i, j] = 1 se o vértice i participar
da aresta j. 0 1 2
0 9
• Para hipergrafos ponderados, A[i, j] contém o 1 7 8
rótulo ou peso associado à aresta e a matriz 2 7 9
não é de bits. 3 8
4 7
• Se o vértice i não participar da aresta j, então 5 8 9
é necessário utilizar um valor que não possa
ser usado como rótulo ou peso, tal como 0 ou
branco.
• A figura ilustra a representação por matrizes de
incidência para o hipergrafo do slide 119.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.1 124

Implementação por Matrizes de Incidência

• A representação por matrizes de incidência demanda muita memória


para hipergrafos densos, em que |A| é próximo de |V |2 .

• Nessa representação, o tempo necessário para acessar um elemento


é independente de |V | ou |A|.

• Logo, essa representação é muito útil para algoritmos em que


necessitamos saber com rapidez se um vértice participa de
determinada aresta.

• A maior desvantagem é que a matriz necessita Ω(|V |3 ) de espaço.


Isso significa que simplesmente ler ou examinar a matriz tem
complexidade de tempo O(|V |3 ).
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.1 125

Implementação por Matrizes de Incidência


#define MAXNUMVERTICES 100
#define MAXNUMARESTAS 4500
#define MAXR 5
typedef int TipoValorVertice ;
typedef int TipoValorAresta ;
typedef int Tipor ;
typedef int TipoPesoAresta;
typedef TipoValorVertice TipoArranjoVertices [ MAXR ] ;
typedef struct TipoAresta {
TipoArranjoVertices Vertices ;
TipoPesoAresta Peso;
} TipoAresta ;
typedef struct TipoGrafo {
TipoPesoAresta Mat[ MAXNUMVERTICES ] [ MAXNUMARESTAS ] ;
TipoValorVertice NumVertices;
TipoValorAresta NumArestas;
TipoValorAresta ProxDisponivel ;
Tipor r ;
} TipoGrafo ;
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.1 126

Implementação por Matrizes de Incidência

• No campo Mat os itens são armazenados em um array de duas


dimensões de tamanho suficiente para armazenar o grafo.

• As constantes MaxNumVertices e MaxNumArestas definem o maior


número de vértices e de arestas que o grafo pode ter e r define o
número de vértices de cada aresta.

• Uma possível implementação para as primeiras seis operações


definidas anteriormente é mostrada no slide a seguir.

• O procedimento ArestasIguais permite a comparação de duas arestas,


a um custo O(r).

• O procedimento InsereAresta tem custo O(r) e os procedimentos


ExisteAresta e RetiraAresta têm custo r × |A|, o que pode ser
considerado O(|A|) porque r é geralmente uma constante pequena.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.1 127

Implementação por Matrizes de Incidência


short ArestasIguais ( TipoArranjoVertices ∗ Vertices ,
TipoValorAresta NumAresta,
TipoGrafo ∗ Grafo)
{ short Aux = TRUE ; Tipor v = 0;
while ( v < Grafo−>r && Aux == TRUE)
{ i f ( Grafo−>Mat[(∗ Vertices ) [ v ] ] [NumAresta]<=0) Aux = FALSE ;
v = v + 1;
}
return Aux;
}

void FGVazio ( TipoGrafo ∗ Grafo)


{ int i , j ;
Grafo−>ProxDisponivel = 0;
for ( i = 0; i < Grafo−>NumVertices ; i ++)
for ( j = 0; j < Grafo−>NumArestas; j ++) Grafo−>Mat[ i ] [ j ] = 0;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.1 128

Implementação por Matrizes de Incidência


void InsereAresta ( TipoAresta ∗ Aresta , TipoGrafo ∗ Grafo)
{ int i ;
i f ( Grafo−>ProxDisponivel == MAXNUMARESTAS)
p r i n t f ( "Nao ha espaco disponivel para a aresta \n" ) ;
else
{ for ( i = 0; i < Grafo−>r ; i ++)
Grafo−>Mat[ Aresta−>Vertices [ i ] ] [ Grafo−>ProxDisponivel]=Aresta−>Peso;
Grafo−>ProxDisponivel = Grafo−>ProxDisponivel + 1;
}
}
short ExisteAresta ( TipoAresta ∗ Aresta , TipoGrafo ∗ Grafo)
{ TipoValorAresta ArestaAtual = 0;
short EncontrouAresta = FALSE ;
while ( ArestaAtual < Grafo−>NumArestas &&
EncontrouAresta == FALSE)
{ EncontrouAresta =
ArestasIguais(&(Aresta−>Vertices ) , ArestaAtual , Grafo ) ;
ArestaAtual = ArestaAtual + 1;
}
return EncontrouAresta;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.1 129

Implementação por Matrizes de Incidência


TipoAresta RetiraAresta ( TipoAresta ∗ Aresta , TipoGrafo ∗ Grafo)
{ TipoValorAresta ArestaAtual = 0;
int i ; short EncontrouAresta = FALSE ;
while ( ArestaAtual<Grafo−>NumArestas& EncontrouAresta == FALSE)
{ i f ( ArestasIguais(&(Aresta−>Vertices ) , ArestaAtual , Grafo) )
{ EncontrouAresta = TRUE ;
Aresta−>Peso = Grafo−>Mat[ Aresta−>Vertices [ 0 ] ] [ ArestaAtual ] ;
for ( i = 0; i < Grafo−>r ; i ++)
Grafo−>Mat[ Aresta−>Vertices [ i ] ] [ ArestaAtual] = −1;
}
ArestaAtual = ArestaAtual + 1;
}
return ∗Aresta ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.1 130

Implementação por Matrizes de Incidência


void ImprimeGrafo ( TipoGrafo ∗ Grafo)
{ int i , j ; p r i n t f ( " " );
for ( i = 0; i < Grafo−>NumArestas; i ++) p r i n t f ( "%3d" , i ) ;
p r i n t f ( " \n" ) ;
for ( i = 0; i < Grafo−>NumVertices ; i ++)
{ p r i n t f ( "%3d" , i ) ;
for ( j = 0; j < Grafo−>NumArestas; j ++)
p r i n t f ( "%3d" , Grafo−>Mat[ i ] [ j ] ) ;
p r i n t f ( " \n" ) ;
}
}
short ListaIncVazia ( TipoValorVertice ∗ Vertice , TipoGrafo ∗ Grafo)
{ short ListaVazia = TRUE ; TipoApontador ArestaAtual = 0;
while ( ArestaAtual < Grafo−>NumArestas && ListaVazia == TRUE)
{ i f ( Grafo−>Mat[∗Vertice ] [ ArestaAtual ] > 0) ListaVazia = FALSE ;
else ArestaAtual = ArestaAtual + 1;
}
return ListaVazia ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.1 131

Implementação por Matrizes de Incidência


TipoApontador PrimeiroListaInc ( TipoValorVertice ∗ Vertice , TipoGrafo ∗ Grafo)
{ TipoApontador ArestaAtual = 0;
short Continua = TRUE ; TipoApontador Resultado = 0;
while ( ArestaAtual < Grafo−>NumArestas && Continua == TRUE)
{ i f ( Grafo−>Mat[∗ Vertice ] [ ArestaAtual ] > 0 ) { Resultado = ArestaAtual ; Continua = FALSE ; }
else ArestaAtual = ArestaAtual + 1;
}
i f ( ArestaAtual == Grafo−>NumArestas) p r i n t f ( "Erro : Lista incidencia vazia \n" ) ;
return Resultado;
}
void ProxArestaInc ( TipoValorVertice ∗ Vertice , TipoGrafo ∗ Grafo,
TipoValorAresta ∗ Inc , TipoPesoAresta ∗ Peso,
TipoApontador ∗ Prox , short ∗ FimListaAdj)
{ ∗ Inc = ∗Prox;
∗Peso = Grafo−>Mat[∗ Vertice ] [ ∗Prox ] ;
∗Prox = ∗Prox + 1;
while (∗Prox < Grafo−>NumArestas && Grafo−>Mat[∗ Vertice ] [ ∗Prox] == 0) ∗Prox = ∗Prox + 1;
∗FimListaAdj = (∗Prox == Grafo−>NumArestas) ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 132

Implementação por Listas de Incidência Usando Arranjos


• A estrutura de dados usada para representar Gr = (V, A) por meio de
listas de incidência foi proposta por Ebert (1987).
• A estrutura usa arranjos para armazenar as arestas e as listas de
arestas incidentes a cada vértice. A parte (a) da figura mostra o
mesmo 3-grafo de 6 vértices e 3 arestas visto anteriormente e a parte
(b) a sua representação por listas de incidência.

0 11
00
1 h0 (x) Arestas 0 1 2
00
11 Arestas
|A|=3 (1,2,4) (1,3,5) (0,2,5)
00
11
00
11
(1, 2, 4, 7)
0
1
00
11 1
0
2 0
1
00
11
3
0
1
h1 (x) 0 (1, 3, 5, 8)
1 Prim 0 1 2 3 4 5
|V|=3 2 1 5 4 6 8
0
1
0
1
0
1
(0, 2, 5, 9)
4
0
1
5 h2 (x) Prox 0 1 2 3 4 5 6 7 8
r|A|=3x3=9 −1 0 −1 −1 −1 3 −1 −1 7
(a) (b)
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 133

Implementação por Listas de Incidência Usando Arranjos


• As arestas são armazenadas no arranjo Arestas. Em cada posição a
do arranjo são armazenados os r vértices da aresta a e o seu Peso.
• As listas de arestas incidentes nos vértices do hipergrafo são
armazenadas nos arranjos Prim e Prox .
• O elemento Prim[v] define o ponto de entrada para a lista de arestas
incidentes no vértice v, enquanto Prox [Prim[v]], Prox [Prox [Prim[v]]] e
assim por diante definem as arestas subsequentes que contêm v.
• Prim deve possuir |V | entradas, uma para cada vértice.
• Prox deve possuir r|A| entradas, pois cada aresta a é armazenada na
lista de arestas incidentes a cada um de seus r vértices.
• A complexidade de espaço é O(|V | + |A|), pois Arestas tem tamanho
O(|A|), P rim tem tamanho O(|V |) e P rox tem tamanho
r × |A| = O(|A|), porque r é geralmente uma constante pequena.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 134

Implementação por Listas de Incidência Usando Arranjos

• Para descobrir quais são as arestas que contêm determinado vértice


v, é preciso percorrer a lista de arestas que inicia em Prim[v] e termina
quando Prox [. . . Prim[v] . . .] = −1.

• Assim, para se ter acesso a uma aresta a armazenada em Arestas[a],


é preciso tomar os valores armazenados nos arranjos Prim e Prox
módulo |A|. O valor −1 é utilizado para finalizar a lista.

• Por exemplo, ao se percorrer a lista das arestas do vértice 2, os


valores {5, 3} são obtidos, os quais representam as arestas que
contêm o vértice 2 (arestas 2 e 0), ou seja, {5 mod 3 = 2, 3 mod 3 = 0}.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 135

Implementação por Listas de Incidência Usando Arranjos


• Os valores armazenados em Prim e Prox são obtidos pela equação
i = a + j|A|, 0 ≤ j ≤ r − 1, e a um índice de uma aresta no arranjo
Arestas. As entradas de Prim são iniciadas com −1.
• Ao inserir a aresta a = 0 contendo os vértices (1, 2, 4), temos que:
i = 0 + 0 × 3 = 0, Prox [i = 0] = Prim[1] = −1 e Prim[1] = i = 0,
i = 0 + 1 × 3 = 3, Prox [i = 3] = Prim[2] = −1 e Prim[2] = i = 3,
i = 0 + 2 × 3 = 6, Prox [i = 6] = Prim[4] = −1 e Prim[4] = i = 6.
• Ao inserir a aresta a = 1 contendo os vértices (1, 3, 5) temos que:
i = 1 + 0 × 3 = 1, Prox [i = 1] = Prim[1] = 0 e Prim[1] = i = 1,
i = 1 + 1 × 3 = 4, Prox [i = 4] = Prim[3] = −1 e Prim[3] = i = 4,
i = 1 + 2 × 3 = 7, Prox [i = 7] = Prim[5] = −1 e Prim[5] = i = 7.
• Ao inserir a aresta a = 2 contendo os vértices (0, 2, 5) temos que:
i = 2 + 0 × 3 = 2, Prox [i = 2] = Prim[0] = −1 e Prim[0] = i = 2,
i = 2 + 1 × 3 = 5, Prox [i = 5] = Prim[2] = 3 e Prim[2] = i = 5,
i = 2 + 2 × 3 = 8, Prox [i = 8] = Prim[5] = 7 e Prim[5] = i = 8.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 136

Implementação por Listas de Incidência Usando Arranjos

• O programa a seguir apresenta a estrutura de dados utilizando listas


de incidência implementadas por meio de arranjos.

• A estrutura de dados contém os três arranjos necessários para


representar um hipergrafo, como ilustrado na figura do slide 132:
– A variável r é utilizada para armazenar a ordem do hipergrafo.
– A variável NumVertices contém o número de vértices do hipergrafo.
– A variável NumArestas contém o número de arestas do hipergrafo.
– A variável ProxDisponivel contém a próxima posição disponível
para inserção de uma nova aresta.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 137

Implementação por Listas de Incidência Usando Arranjos


#define MAXNUMVERTICES 100
#define MAXNUMARESTAS 4500
#define MAXR 5
#define MAXTAMPROX MAXR ∗ MAXNUMARESTAS
#define INDEFINIDO −1
typedef int TipoValorVertice ;
typedef int TipoValorAresta ;
typedef int Tipor ;
typedef int TipoMaxTamProx;
typedef int TipoPesoAresta;
typedef TipoValorVertice TipoArranjoVertices [ MAXR + 1];
typedef struct TipoAresta {
TipoArranjoVertices Vertices ;
TipoPesoAresta Peso;
} TipoAresta ;
typedef TipoAresta TipoArranjoArestas [ MAXNUMARESTAS + 1];
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 138

Implementação por Listas de Incidência Usando Arranjos


typedef struct TipoGrafo {
TipoArranjoArestas Arestas ;
TipoValorVertice Prim[ MAXNUMARESTAS + 1];
TipoMaxTamProx Prox[ MAXTAMPROX + 2];
TipoMaxTamProx ProxDisponivel ;
TipoValorVertice NumVertices;
TipoValorAresta NumArestas;
Tipor r ;
} TipoGrafo ;
typedef int TipoApontador;
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 139

Implementação por Listas de Incidência Usando Arranjos

• Uma possível implementação para as primeiras seis operações


definidas anteriormente é mostrada no programa a seguir.

• O procedimento ArestasIguais permite a comparação de duas arestas


cujos vértices podem estar em qualquer ordem (custo O(r2 )).

• O procedimento InsereAresta insere uma aresta no grafo (custo O(r)).

• O procedimento ExisteAresta verifica se uma aresta está presente no


grafo (custo equivalente ao grau de cada vértice da aresta no grafo).

• O procedimento RetiraAresta primeiro localiza a aresta no grafo, retira


a mesma da lista de arestas incidentes a cada vértice em Prim e Prox
e marca a aresta como removida no arranjo Arestas. A aresta
marcada no arranjo Arestas não é reutilizada, pois não mantemos uma
lista de posições vazias.
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 140

Implementação por Listas de Incidência Usando Arranjos


short ArestasIguais ( TipoArranjoVertices V1,
TipoValorAresta ∗NumAresta, TipoGrafo ∗Grafo)
{ Tipor i = 0 , j ;
short Aux = TRUE ;
while ( i < Grafo−>r && Aux)
{ j = 0;
while ( (V1[ i ] ! = Grafo−>Arestas[∗NumAresta] . Vertices [ j ]) &&
( j < Grafo−>r ) ) j ++;
i f ( j == Grafo−>r ) Aux = FALSE ;
i ++;
}
return Aux;
}
void FGVazio(TipoGrafo ∗Grafo)
{ int i ;
Grafo−>ProxDisponivel = 0;
for ( i = 0; i < Grafo−>NumVertices ; i ++) Grafo−>Prim[ i ] = −1;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 141

Implementação por Listas de Incidência Usando Arranjos


void InsereAresta(TipoAresta ∗Aresta , TipoGrafo ∗Grafo)
{ int i , Ind ;
i f ( Grafo−>ProxDisponivel == MAXNUMARESTAS + 1)
p r i n t f ( "Nao ha espaco disponivel para a aresta \n" ) ;
else
{ Grafo−>Arestas [Grafo−>ProxDisponivel ] = ∗Aresta ;
for ( i = 0; i < Grafo−>r ; i ++)
{ Ind = Grafo−>ProxDisponivel + i ∗ Grafo−>NumArestas;
Grafo−>Prox[ Ind ] =
Grafo−>Prim[Grafo−>Arestas [Grafo−>ProxDisponivel ] . Vertices [ i ] ] ;
Grafo−>Prim[Grafo−>Arestas [Grafo−>ProxDisponivel ] . Vertices [ i ]]= Ind ;
}
}
Grafo−>ProxDisponivel++;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 142

Implementação por Listas de Incidência Usando Arranjos


short ExisteAresta (TipoAresta ∗Aresta ,
TipoGrafo ∗Grafo)
{ Tipor v ;
TipoValorAresta A1;
int Aux;
short EncontrouAresta ;
EncontrouAresta = FALSE ;
for (v = 0; v < Grafo−>r ; v++)
{ Aux = Grafo−>Prim[ Aresta−>Vertices [ v ] ] ;
while (Aux != −1 && !EncontrouAresta)
{ A1 = Aux % Grafo−>NumArestas;
i f ( ArestasIguais (Aresta−>Vertices , &A1, Grafo) )
EncontrouAresta = TRUE ;
Aux = Grafo−>Prox[Aux] ;
}
}
return EncontrouAresta ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 143

Implementação por Listas de Incidência Usando Arranjos


TipoAresta RetiraAresta (TipoAresta ∗Aresta , TipoGrafo ∗Grafo)
{ int Aux, Prev , i ; TipoValorAresta A1; Tipor v ;
for ( v = 0; v < Grafo−>r ; v++)
{ Prev = INDEFINIDO ;
Aux = Grafo−>Prim[ Aresta−>Vertices [ v ] ] ;
A1 = Aux % Grafo−>NumArestas;
while(Aux >= 0 && !ArestasIguais (Aresta−>Vertices , &A1, Grafo) )
{ Prev = Aux;
Aux = Grafo−>Prox[Aux] ;
A1 = Aux % Grafo−>NumArestas;
}
i f (Aux >= 0)
{ i f ( Prev == INDEFINIDO ) Grafo−>Prim[ Aresta−>Vertices [ v ] ] = Grafo−>Prox[Aux] ;
else Grafo−>Prox[Prev] = Grafo−>Prox[Aux] ;
}
}
TipoAresta Resultado = Grafo−>Arestas [A1] ;
for ( i = 0; i < Grafo−>r ; i ++) Grafo−>Arestas [A1] . Vertices [ i ] = INDEFINIDO ;
Grafo−>Arestas [A1] .Peso = INDEFINIDO ;
return Resultado;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 144

Implementação por Listas de Incidência Usando Arranjos


void ImprimeGrafo(TipoGrafo ∗Grafo)
{ int i , j ;
p r i n t f ( " Arestas : Num Aresta , Vertices , Peso \ n" ) ;
for ( i = 0; i < Grafo−>NumArestas; i ++)
{ p r i n t f ( "%2d" , i ) ;
for ( j = 0; j < Grafo−>r ; j ++) p r i n t f ( "%3d" , Grafo−>Arestas [ i ] . Vertices [ j ] ) ;
p r i n t f ( "%3d\n" , Grafo−>Arestas [ i ] .Peso) ;
}
p r i n t f ( " Lista arestas incidentes a cada vertice : \ n" ) ;
for ( i = 0 ; i < Grafo−>NumVertices ; i ++)
{ p r i n t f ( "%2d" , i ) ;
j = Grafo−>Prim[ i ] ;
while ( j ! = INDEFINIDO)
{ p r i n t f ( "%3d" , j % Grafo−>NumArestas) ;
j = Grafo−>Prox[ j ] ;
}
p r i n t f ( " \n" ) ;
}
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 145

Implementação por Listas de Incidência Usando Arranjos


/∗ operadores para obter a l i s t a de arestas incidentes a um vertice ∗/
short ListaIncVazia ( TipoValorVertice ∗Vertice ,
TipoGrafo ∗Grafo)
{ return Grafo−>Prim[∗ Vertice] == −1; }

TipoApontador PrimeiroListaInc ( TipoValorVertice ∗Vertice ,


TipoGrafo ∗Grafo)
{ return Grafo−>Prim[∗ Vertice ] ; }

void ProxArestaInc( TipoValorVertice ∗Vertice , TipoGrafo ∗Grafo,


TipoValorAresta ∗Inc , TipoPesoAresta ∗Peso,
TipoApontador ∗Prox , short ∗FimListaInc )
/∗ Retorna Inc apontado por Prox ∗/
{ ∗ Inc = ∗Prox % Grafo−>NumArestas;
∗Peso = Grafo−>Arestas[∗Inc ] .Peso;
i f ( Grafo−>Prox[∗Prox] == INDEFINIDO)
∗FimListaInc = TRUE ;
else ∗Prox = Grafo−>Prox[∗Prox ] ;
}
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 146

Programa Teste para Operadores do Tipo Abstrato de


Dados Hipergrafo
/∗ ∗ Entram aqui tipos do Slide 125 ou do Slide 137 ∗ ∗/
/∗ ∗ Entram aqui operadores do Slide 127 ou do Slide 138 ∗ ∗/
int main( ) {
TipoApontador Ap;
int i , j ;
TipoValorAresta Inc ;
TipoValorVertice V1;
TipoAresta Aresta ;
TipoPesoAresta Peso;
TipoGrafo Grafo;
short FimListaInc ;
p r i n t f ( "Hipergrafo r : " ) ; scanf( "%d∗[^\n] " , &Grafo. r ) ;
p r i n t f ( "No. vertices : " ) ; scanf( "%d∗[^\n] " , &Grafo.NumVertices) ;
p r i n t f ( "No. arestas : " ) ; scanf( "%d∗[^\n] " , &Grafo.NumArestas) ;
getchar ( ) ;
FGVazio (&Grafo ) ;
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 147

Programa Teste para Operadores do Tipo Abstrato de


Dados Hipergrafo
for ( i = 0; i < Grafo.NumArestas; i ++)
{ p r i n t f ( "Insere Aresta e Peso: " ) ;
for ( j =0; j < Grafo. r ; j ++) scanf( "%d∗[^\n] ",&Aresta . Vertices [ j ] ) ;
scanf( "%d∗[^\n] " , &Aresta .Peso) ;
getchar ( ) ;
InsereAresta (&Aresta, &Grafo ) ;
}
/ / Imprime estrutura de dados
p r i n t f ( "prim : " ) ; for ( i = 0; i < Grafo.NumVertices ; i ++)
p r i n t f ( "%3d" , Grafo.Prim[ i ] ) ; p r i n t f ( " \n" ) ;
p r i n t f ( "prox : " ) ; for ( i = 0; i < Grafo.NumArestas ∗ Grafo. r ; i ++)
p r i n t f ( "%3d" , Grafo.Prox[ i ] ) ; p r i n t f ( " \n" ) ;
ImprimeGrafo(&Grafo ) ;
getchar ( ) ;
p r i n t f ( " Lista arestas incidentes ao vertice : " ) ;
scanf( "%d∗[^\n] " , &V1) ;
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 148

Programa Teste para Operadores do Tipo Abstrato de


Dados Hipergrafo
i f ( ! ListaIncVazia(&V1, &Grafo) )
{ Ap = PrimeiroListaInc(&V1, &Grafo ) ;
FimListaInc = FALSE ;
while ( ! FimListaInc )
{ ProxArestaInc (&V1, &Grafo, &Inc , &Peso, &Ap, &FimListaInc ) ;
p r i n t f ( "%2d (%d) " , Inc % Grafo.NumArestas, Peso) ;
}
p r i n t f ( " \n" ) ; getchar ( ) ;
}
else p r i n t f ( " Lista vazia \n" ) ;

p r i n t f ( "Existe aresta : " ) ;


for ( j = 0; j < Grafo. r ; j ++) scanf( "%d∗[^\n] " , &Aresta . Vertices [ j ] ) ;
getchar ( ) ;
Projeto de Algoritmos – Cap.7 Algoritmos em Grafos – Seção 7.10.2 149

Programa Teste para Operadores do Tipo Abstrato de


Dados Hipergrafo

i f ( ExisteAresta(&Aresta, &Grafo) )
p r i n t f ( "Sim\n" ) ;
else p r i n t f ( "Nao\n" ) ;
p r i n t f ( " Retira aresta : " ) ;
for ( j = 0; j < Grafo. r ; j ++) scanf( "%d∗[^\n] " , &Aresta . Vertices [ j ] ) ;
getchar ( ) ;
i f ( ExisteAresta(&Aresta, &Grafo) )
{ Aresta = RetiraAresta(&Aresta, &Grafo ) ;
p r i n t f ( "Aresta retirada : " ) ;
for ( i = 0; i < Grafo. r ; i ++) p r i n t f ( "%3d" , Aresta . Vertices [ i ] ) ;
p r i n t f ( "%4d\n" , Aresta .Peso) ;
}
else p r i n t f ( "Aresta nao existe \n" ) ;
ImprimeGrafo(&Grafo ) ;
return 0;
}
Processamento de Cadeias de
Caracteres ∗

Última alteração: 8 de Novembro de 2010

∗ Transparências elaboradas por Fabiano Cupertino Botelho, Charles Ornelas Almeida, Israel Guerra e Nivio Ziviani
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres 1

Conteúdo do Capítulo

8.1 Casamento de Cadeias


8.1.1 Casamento Exato
8.1.2 Casamento Aproximado

8.2 Compressão
8.2.1 Por Que Usar Compressão
8.2.2 Compressão de Textos em Linguagem Natural
8.2.3 Codificação de Huffman Usando Palavras
8.2.4 Codificação de Huffman Usando Bytes
8.2.5 Pesquisa em Texto Comprimido
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1 2

Definição e Motivação
• Cadeia de caracteres: sequência de elementos denominados
caracteres.

• Os caracteres são escolhidos de um conjunto denominado alfabeto.


Ex.: em uma cadeia de bits o alfabeto é {0, 1}.

• Casamento de cadeias de caracteres ou casamento de padrão:


encontrar todas as ocorrências de um padrão em um texto.

• Exemplos de aplicação:
– edição de texto;
– recuperação de informação;
– estudo de sequências de DNA em biologia computacional.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1 3

Notação

• Texto: arranjo T [1..n] de tamanho n;

• Padrão: arranjo P [1..m] de tamanho m ≤ n.

• Os elementos de P e T são escolhidos de um alfabeto finito Σ de


tamanho c.
Ex.: Σ = {0, 1} ou Σ = {a, b, . . . , z}.

• Casamento de cadeias ou casamento de padrão: dados duas


cadeias P (padrão) de comprimento |P | = m e T (texto) de
comprimento |T | = n, onde n ≫ m, deseja-se saber as ocorrências de
P em T .
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1 4

Estruturas de Dados para Texto e Padrão


#define MAXTAMTEXTO 1000
#define MAXTAMPADRAO 10
#define MAXCHAR 256
#define NUMMAXERROS 10
typedef char TipoTexto [ MAXTAMTEXTO ] ;
typedef char TipoPadrao[ MAXTAMPADRAO ] ;
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1 5

Categorias de Algoritmos

• P e T não são pré-processados:


– algoritmo sequencial, on-line e de tempo-real;
– padrão e texto não são conhecidos a priori.
– complexidade de tempo O(mn) e de espaço O(1), para pior caso.

• P pré-processado:
– algoritmo sequencial;
– padrão conhecido anteriormente permitindo seu
pré-processamento.
– complexidade de tempo O(n) e de espaço O(m + c), no pior caso.
– ex.: programas para edição de textos.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1 6

Categorias de Algoritmos

• P e T são pré-processados:
– algoritmo constrói índice.
– complexidade de tempo O(log n) e de espaço é O(n).
– tempo para obter o índice é grande, O(n) ou O(n log n).
– compensado por muitas operações de pesquisa no texto.
– Tipos de índices mais conhecidos:
∗ Arquivos invertidos
∗ Árvores trie e árvores Patricia
∗ Arranjos de sufixos
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1 7

Exemplos: P e T são pré-processados

• Diversos tipos de índices: arquivos invertidos, árvores trie e Patricia, e


arranjos de sufixos.

• Um arquivo invertido possui duas partes: vocabulário e


ocorrências.

• O vocabulário é o conjunto de todas as palavras distintas no texto.

• Para cada palavra distinta, uma lista de posições onde ela ocorre no
texto é armazenada.

• O conjunto das listas é chamado de ocorrências.

• As posições podem referir-se a palavras ou caracteres.


Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1 8

Exemplo de Arquivo Invertido


1 7 16 22 26 36 45 53
Texto exemplo. Texto tem palavras. Palavras exercem fascínio.

exemplo 7
exercem 45
fascínio 53
palavras 26 36
tem 22
texto 1 16
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1 9

Arquivo Invertido - Tamanho


• O vocabulário ocupa pouco espaço.
• A previsão sobre o crescimento do tamanho do vocabulário: lei de Heaps.
• Lei de Heaps: o vocabulário de um texto em linguagem natural contendo n
palavras tem tamanho V = Knβ = O(nβ ), onde K e β dependem das
características de cada texto.
• K geralmente assume valores entre 10 e 100, e β é uma constante entre 0 e
1, na prática ficando entre 0,4 e 0,6.
• O vocabulário cresce sublinearmente com o tamanho do texto, em uma
proporção perto de sua raiz quadrada.
• As ocorrências ocupam muito mais espaço.
• Como cada palavra é referenciada uma vez na lista de ocorrências, o espaço
necessário é O(n).
• Na prática, o espaço para a lista de ocorrências fica entre 30% e 40% do
tamanho do texto.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1 10

Arquivo Invertido - Pesquisa

• A pesquisa tem geralmente três passos:


– Pesquisa no vocabulário: palavras e padrões da consulta são
isoladas e pesquisadas no vocabulário.
– Recuperação das ocorrências: as listas de ocorrências das
palavras encontradas no vocabulário são recuperadas.
– Manipulação das ocorrências: as listas de ocorrências são
processadas para tratar frases, proximidade, ou operações
booleanas.

• Como a pesquisa em um arquivo invertido sempre começa pelo


vocabulário, é interessante mantê-lo em um arquivo separado.

• Na maioria das vezes, esse arquivo cabe na memória principal.


Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1 11

Arquivo Invertido - Pesquisa


• A pesquisa de palavras simples pode ser realizada usando qualquer
estrutura de dados que torne a busca eficiente, como hashing, árvore
trie ou árvore B.
• As duas primeiras têm custo O(m), onde m é o tamanho da consulta
(independentemente do tamanho do texto).
• Guardar as palavras na ordem lexicográfica é barato em termos de
espaço e competitivo em desempenho, já que a pesquisa binária pode
ser empregada com custo O(log n).
• A pesquisa por frases usando índices é mais difícil de resolver.
• Cada elemento da frase tem de ser pesquisado separadamente e suas
listas de ocorrências recuperadas.
• A seguir, as listas têm de ser percorridas de forma sicronizada para
encontrar as posições nas quais todas as palavras aparecem em
sequência.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1 12

Arquivo Invertido Usando Trie

Arquivo invertido usando uma árvore trie para o texto:


Texto exemplo. Texto tem palavras. Palavras exercem fascínio.

m exemplo: 7
x e
e r exercem: 45
f fascínio: 53
p
palavras: 26,36
t m tem: 22
e
x texto: 1, 16

• O vocabulário lido até o momento é colocado em uma árvore trie,


armazenando uma lista de ocorrências para cada palavra.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1 13

Arquivo Invertido Usando Trie

• Cada nova palavra lida é pesquisada na trie:


– Se a pesquisa é sem sucesso, então a palavra é inserida na árvore
e uma lista de ocorrências é inicializada com a posição da nova
palavra no texto.
– Senão, uma vez que a palavra já se encontra na árvore, a nova
posição é inserida ao final da lista de ocorrências.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 14

Casamento Exato
• Consiste em obter todas as ocorrências exatas do padrão no texto.
Ex.: ocorrência exata do padrão teste.
teste
os testes testam estes alunos . . .

• Dois enfoques:

1. leitura dos caracteres do texto um a um: algoritmos força bruta,


Knuth-Morris-Pratt e Shift-And.

2. pesquisa de P em uma janela que desliza ao longo de T , pesquisando


por um sufixo da janela que casa com um sufixo de P , por
comparações da direita para a esquerda: algoritmos
Boyer-Moore-Horspool e Boyer-Moore.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 15

Força Bruta - Implementação

• É o algoritmo mais simples para casamento de cadeias.

• A idéia é tentar casar qualquer subcadeia no texto de comprimento m


com o padrão.

void ForcaBruta(TipoTexto T, long n, TipoPadrao P, long m)


{ long i , j , k ;
for ( i = 1; i <= (n − m + 1 ) ; i ++)
{ k = i; j = 1;
while (T[ k−1] == P[ j −1] && j <= m) { j ++; k++; }
i f ( j > m) p r i n t f ( " Casamento na posicao%3ld \n" , i ) ;
}
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 16

Força Bruta - Análise

• Pior caso: Cn = m × n.

• O pior caso ocorre, por exemplo, quando P = aab e T =aaaaaaaaaa.


c 1

• Caso esperado: Cn = c−1 1 − cm (n − m + 1) + O(1)

• O caso esperado é muito melhor do que o pior caso.

• Em experimento com texto randômico e alfabeto de tamanho c = 4, o


número esperado de comparações é aproximadamente igual a 1,3.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 17

Autômatos

• Um autômato é um modelo de computação muito simples.

• Um autômato finito é definido por uma tupla (Q, I, F, Σ, T ), onde Q é


um conjunto finito de estados, entre os quais existe um estado inicial
I ∈ Q, e alguns são estados finais ou estados de término F ⊆ Q.

• Transições entre estados são rotuladas por elementos de Σ ∪ {ǫ},


onde Σ é o alfabeto finito de entrada e ǫ é a transição vazia.

• As transições são formalmente definidas por uma função de transição


T.

• T associa a cada estado q ∈ Q um conjunto {q1 , q2 , . . . , qk } de estados


de Q para cada α ∈ Σ ∪ {ǫ}.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 18

Tipos de Autômatos

• Autômato finito não-determinista:


– Quando T é tal que existe um estado q associado a um dado
caractere α para mais de um estado, digamos
T (q, α) = {q1 , q2 , . . . , qk }, k > 1, ou existe alguma transição rotulada
por ǫ.
– Neste caso, a função de transição T é definida pelo conjunto de
triplas ∆ = {(q, α, q ′ ), onde q ∈ Q, α ∈ Σ ∪ {ǫ}, e q ′ ∈ T (q, α).

• Autômato finito determinista:


– Quando a função de transição T é definida pela função
δ = Q × Σ ∪ ǫ → Q.
– Neste caso, se T (q, α) = {q ′ }, então δ(q, α) = q ′ .
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 19

Exemplo de Autômatos
• Autômato finito não-determinista.
A partir do estado 0, através do caractere de transição a é possível
atingir os estados 2 e 3.
a
0 3

a c
1 2
b

• Autômato finito determinista.


Para cada caractere de transição todos os estados levam a um único
estado.
d
0 3

a c
1 2
b
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 20

Reconhecimento por Autômato

• Uma cadeia é reconhecida por (Q, I, F, Σ, ∆) ou (Q, I, F, Σ, δ) se


qualquer um dos autômatos rotula um caminho que vai de um estado
inicial até um estado final.

• A linguagem reconhecida por um autômato é o conjunto de cadeias


que o autômato é capaz de reconher.

Ex.: a linguagem reconhecida pelo autômato abaixo é o conjunto de


cadeias {a} e {abc} no estado 3.
a
0 3

a c
1 2
b
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 21

Transições Vazias

• São transições rotulada com uma cadeia vazia ǫ, também chamadas


de transições-ǫ, em autômatos não-deterministas

• Não há necessidade de se ler um caractere para caminhar através de


uma transição vazia.

• Simplificam a construção do autômato.

• Sempre existe um autômato equivalente que reconhece a mesma


linguagem sem transições-ǫ.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 22

Estados Ativos

• Se uma cadeia x rotula um caminho de I até um estado q então o


estado q é considerado ativo depois de ler x.

• Um autômato finito determinista tem no máximo um estado ativo em


um determinado instante.

• Um autômato finito não-determinista pode ter vários estados ativos.

• Casamento aproximado de cadeias pode ser resolvido por meio de


autômatos finitos não-deterministas.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 23

Ciclos em Autômatos
• Os autômatos abaixo são acíclicos pois as transições não formam ciclos.
a d
0 3 0 3

a c a c
1 2 1 2
b b

• Autômatos finitos cíclicos, deterministas ou não-deterministas, são úteis


para casamento de expressões regulares

• A linguagem reconhecida por um autômato cíclico pode ser infinita.

Ex: o autômato abaixo reconhece ba, bba, bbba, bbbba, e assim por diante.
a

b 0 1
b

a
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 24

Exemplo de Uso de Autômato


b
c
c
0 a 1 a 2 3 4
b c
a
b,c
b a

• O autômato reconhece P ={aabc}.


• A pesquisa de P sobre um texto T com alfabeto Σ ={a, b, c} pode ser
vista como a simulação do autômato na pesquisa de P sobre T .
• No início, o estado inicial ativa o estado 1.
• Para cada caractere lido do texto, a aresta correspondente é seguida,
ativando o estado destino.
• Se o estado 4 estiver ativo e um caractere c é lido o estado final se torna
ativo, resultando em um casamento de aabc com o texto.
• Como cada caractere do texto é lido uma vez, a complexidade de tempo é
O(n), e de espaço é m + 2 para vértices e |Σ| × m para arestas.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 25

Knuth-Morris-Pratt (KMP)

• O KMP é o primeiro algoritmo (1977) cujo pior caso tem complexidade


de tempo linear no tamanho do texto, O(n).

• É um dos algoritmos mais famosos para resolver o problema de


casamento de cadeias.

• Tem implementação complicada e na prática perde em eficiência para


o Shift-And e o Boyer-Moore-Horspool.

• Até 1971, o limite inferior conhecido para busca exata de padrões era
O(mn).
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 26

KMP - 2DPDA
• Em 1971, Cook provou que qualquer problema que puder ser resolvido por
um autômato determinista de dois caminhos com memória de pilha (Two-way
Deterministic Pushdown Store Automaton, 2DPDA) pode ser resolvido em
tempo linear por uma máquina RAM.
• O 2DPDA é constituído de:
– uma fita apenas para leitura;
– uma pilha de dados (memória temporária);
– um controle de estado que permite mover a fita para esquerda ou direita,
empilhar ou desempilhar símbolos, e mudar de estado.
# c 1 c 2 ... c n $ p 1 p 2 ... p m φ
Cabeça de leitura

Controle c n
Pilha
c n−1
...
c 1

#
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 27

KMP - Casamento de Cadeias no 2DPDA


# c 1 c 2 ... c n $ p 1 p 2 ... p m φ
Cabeça de leitura

Controle c n
Pilha
c n−1
...
c 1

• A entrada do autômato é a cadeia: #c1 c2 · · · cn $p1 p2 · · · pm φ.


• A partir de # todos os caracteres são empilhados até encontrar o caractere $.
• A leitura cotinua até encontrar o caractere φ.
• A seguir a leitura é realizada no sentido contrário, iniciando por pn ,
comparado-o com o último caractere empilhado, no caso cn .
• Esta operação é repetida para os caracteres seguintes, e se o caractere $ for
atingido então as duas cadeias são iguais.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 28

KMP - Algoritmo

• Primeira versão do KMP é uma simulação linear do 2DPDA

• O algoritmo computa o sufixo mais longo no texto que é também o


prefixo de P .

• Quando o comprimento do sufixo no texto é igual a |P | ocorre um


casamento.

• O pré-processamento de P permite que nenhum caractere seja


reexaminado.

• O apontador para o texto nunca é decrementado.

• O pré-processamento de P pode ser visto como a construção


econômica de um autômato determinista que depois é usado para
pesquisar pelo padrão no texto.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 29

Shift-And

• O Shift-And é vezes mais rápido e muito mais simples do que o KMP.

• Pode ser estendido para permitir casamento aproximado de cadeias


de caracteres.

• Usa o conceito de paralelismo de bit:


– técnica que tira proveito do paralelismo intrínseco das operações
sobre bits dentro de uma palavra de computador.
– É possível empacotar muitos valores em uma única palavra e
atualizar todos eles em uma única operação.

• Tirando proveito do paralelismo de bit, o número de operações que um


algoritmo realiza pode ser reduzido por um fator de até w, onde w é o
número de bits da palavra do computador.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 30

Shift-And: Operações com Paralelismo de Bit

• Para denotar repetição de bit é usado exponenciação: 013 = 0111.

• Uma sequência de bits b1 . . . bc é chamada de máscara de bits de


comprimento c, e é armazenada em alguma posição de uma palavra w
do computador.

• Operações sobre os bits da palavra do computador:


– “|”: operação or;
– “&”: operação and;
– “∼”: complementa todos os bits;
– “>>”: move os bits para a direita e entra com zeros à esquerda (por
exemplo, b1 , b2 , . . . , bc−1 , bc >> 2 = 00b3 , . . . , bc−2 ).
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 31

Shift-And - Princípio de Funcionamento


• Mantém um conjunto de todos os prefixos de P que casam com o
texto já lido.
• Utiliza o paralelismo de bit para atualizar o conjunto a cada caractere
lido do texto.
• Este conjunto é representado por uma máscara de bits
R = (b1 , b2 , . . . , bm ).
• O algoritmo Shift-And pode ser visto como a simulação de um
autômato que pesquisa pelo padrão no texto (não-determinista para
simular o paralelismo de bit).
Ex.: Autômato não-determinista que reconhece todos os prefixos de
P ={teste}
Σ
t e s t e
0 1 2 3 4 5
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 32

Shift-And - Algoritmo

• O valor 1 é colocado na j-ésima posição de R = (b1 , b2 , . . . , bm ) se e


somente se p1 . . . pj é um sufixo de t1 . . . ti , onde i corresponde à
posição corrente no texto.

• A j-ésima posição de R é dita estar ativa.

• bm ativo significa um casamento.

• R′ , o novo valor do conjunto R, é calculado na leitura do próximo


caractere ti+1 .

• A posição j + 1 em R′ ficará ativa se e somente se a posição j estava


ativa em R (p1 . . . pj era sufixo de t1 . . . ti e ti+1 casa com pj+1 ).

• Com o uso de paralelismo de bit é possível computar o novo conjunto


com custo O(1).
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 33

sHift-And - Pré-processamento

• O primeiro passo é a construção de uma tabela M para armazenar


uma máscara de bits b1 . . . , bm para cada caractere.
Ex.: máscaras de bits para os caracteres presentes em P ={teste}.

1 2 3 4 5
M[t] 1 0 0 1 0
M[e] 0 1 0 0 1
M[s] 0 0 1 0 0

• A máscara em M [t] é 10010, pois o caractere t aparece nas posições


1 e 4 de P .
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 34

Shift-And - Pesquisa
• O valor do conjunto é inicializado como R = 0m (0m significa 0 repetido
m vezes).
• Para cada novo caractere ti+1 lido do texto o valor do conjunto R′ é
atualizado: R′ = ((R >> 1) | 10m−1 ) & M [T [i]].
• A operação “>>” desloca todas as posições para a direita no passo
i + 1 para marcar quais posições de P eram sufixos no passo i.
• A cadeia vazia ǫ também é marcada como um sufixo, permitindo um
casamento na posição corrente do texto (self-loop no início do
autômato).
Σ
t e s t e
0 1 2 3 4 5

• Do conjunto obtido até o momento, são mantidas apenas as posições


que ti+1 casa com pj+1 , obtido com a operação and desse conjunto de
posições com o conjunto M [ti+1 ] de posições de ti+1 em P .
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 35

Exemplo de Funcionamento do Shif-And


Pesquisa do padrão P ={teste} no texto T ={os testes ...}.

Texto (R >> 1)|10m−1 R′


o 1 0 0 0 0 0 0 0 0 0
s 1 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0
t 1 0 0 0 0 1 0 0 0 0
e 1 1 0 0 0 0 1 0 0 0
s 1 0 1 0 0 0 0 1 0 0
t 1 0 0 1 0 1 0 0 1 0
e 1 1 0 0 1 0 1 0 0 1
s 1 0 1 0 0 0 0 1 0 0
1 0 0 1 0 0 0 0 0 0
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 36

Shift-And - Implementação

Shift−And ( P = p1 p2 · · · pm , T = t1 t2 · · · tn )
{ /∗−−Préprocessamento−−∗/
for ( c∈ Σ ) M [ c ] = 0m ;
for ( j = 1; j <= m; j ++) M [ pj ] = M [ pj ] | 0j−1 10m−j ;
/∗−− Pesquisa−−∗/
R = 0m ;
for ( i = 1; i <= n ; i ++)
{ R = ( ( R >> 1 | 10m−1 ) & M [ T [ i ] ] ) ;
i f ( R & 0m−1 1 6= 0m ) ’Casamento na posicao i − m + 1’ ;
}
}
• As operações and, or, deslocamento à direita e complemento não
podem ser realizadas com eficiência na linguagem Pascal padrão, o
que compromete o conceito de paralelismo de bit.
• Análise: O custo do algoritmo Shift-And é O(n), desde que as
operações possam ser realizadas em O(1) e o padrão caiba em umas
poucas palavras do computador.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 37

Boyer-Moore-Horspool (BMH)

• Em 1977, foi publicado o algoritmo Boyer-Moore (BM).

• A idéia é pesquisar no padrão no sentido da direita para a esquerda, o


que torna o algoritmo muito rápido.

• Em 1980, Horspool apresentou uma simplificação no algoritmo


original, tão eficiente quanto o algoritmo original, ficando conhecida
como Boyer-Moore-Horspool (BMH).

• Pela extrema simplicidade de implementação e comprovada eficiência,


o BMH deve ser escolhido em aplicações de uso geral que necessitam
realizar casamento exato de cadeias.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 38

Funcionamento do BM e BMH

• O BM e BMH pesquisa o padrão P em uma janela que desliza ao


longo do texto T .

• Para cada posição desta janela, o algoritmo pesquisa por um sufixo da


janela que casa com um sufixo de P , com comparações realizadas no
sentido da direita para a esquerda.

• Se não ocorrer uma desigualdade, então uma ocorrência de P em T


ocorreu.

• Senão, o algoritmo calcula um deslocamento que o padrão deve ser


deslizado para a direita antes que uma nova tentativa de casamento
se inicie.

• O BM original propõe duas heurísticas para calcular o deslocamento:


ocorrência e casamento.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 39

BM - Heurística Ocorrência
• Alinha a posição no texto que causou a colisão com o primeiro caractere no
padrão que casa com ele;
Ex.: P ={cacbac}, T ={aabcaccacbac}.
1 2 3 4 5 6 7 8 9 0 1 2
c a c b a c
a a b c a c c a c b a c
c a c b a c
c a c b a c
c a c b a c
c a c b a c
• A partir da posição 6, da direita para a esquerda, existe uma colisão na
posição 4 de T , entre b do padrão e c do texto.
• Logo, o padrão deve ser deslocado para a direita até o primeiro caractere no
padrão que casa com c.
• O processo é repetido até encontrar casamento a partir da posição 7 de T .
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 40

BM - Heurística Casamento
• Ao mover o padrão para a direita, faça-o casar com o pedaço do texto
anteriormente casado.
Ex.: P ={cacbac} no texto T ={aabcaccacbac}.
1 2 3 4 5 6 7 8 9 0 1 2
c a c b a c
a a b c a c c a c b a c
c a c b a c
c a c b a c
• Novamente, a partir da posição 6, da direita para a esquerda, existe uma
colisão na posição 4 de T , entre o b do padrão e o c do texto.
• Neste caso, o padrão deve ser deslocado para a direita até casar com o
pedaço do texto anteriormente casado, no caso ac, deslocando o padrão 3
posições à direita.
• O processo é repetido mais uma vez e o casamento entre P e T ocorre.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 41

Escolha da Heurística

• O algoritmo BM escolhe a heurística que provoca o maior


deslocamento do padrão.

• Esta escolha implica em realizar uma comparação entre dois inteiros


para cada caractere lido do texto, penalizando o desempenho do
algoritmo com relação a tempo de processamento.

• Várias propostas de simplificação ocorreram ao longo dos anos.

• As que produzem os melhores resultados são as que consideram


apenas a heurística ocorrência.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 42

Algoritmo Boyer-Moore-Horspool (BMH)

• A simplificação mais importante é devida a Horspool em 1980.

• Executa mais rápido do que o algoritmo BM original.

• Parte da observação de que qualquer caractere já lido do texto a partir


do último deslocamento pode ser usado para endereçar a tabela de
deslocamentos.

• Endereça a tabela com o caractere no texto correspondente ao último


caractere do padrão.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 43

BMH - Tabela de Deslocamentos

• Para pré-computar o padrão o valor inicial de todas as entradas na


tabela de deslocamentos é feito igual a m.

• A seguir, apenas para os m − 1 primeiros caracteres do padrão são


usados para obter os outros valores da tabela.

• Formalmente, d[x] = min{jtalquej = m|(1 ≤ j < m&P [m − j] = x)}.

Ex.: Para o padrão P ={teste}, os valores de d são d[t] = 1, d[e] = 3,


d[s] = 2, e todos os outros valores são iguais ao valor de |P |, nesse caso
m = 5.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 44

BMH - Implementação

void BMH(TipoTexto T, long n, TipoPadrao P, long m)


• d[ord(T[i])] equivale ao en-
{ long i , j , k , d[ MAXCHAR + 1];
for ( j = 0; j <= MAXCHAR ; j ++) d[ j ] = m; dereço na tabela d do
for ( j = 1; j < m; j ++) d[P[ j −1]] = m − j ; caractere que está na i-
i = m; ésima posição no texto, a
while ( i <= n) /∗−− Pesquisa−−∗/ qual corresponde à posi-
{ k = i; ção do último caractere de
j = m;
P.
while (T[ k−1] == P[ j −1] && j > 0) { k−−; j −−; }
i f ( j == 0)
p r i n t f ( " Casamento na posicao: %3ld \n" , k + 1);
i += d[T[ i −1]];
}
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 45

Algoritmo BMHS - Boyer-Moore-Horspool-Sunday


• Sunday (1990) apresentou outra simplificação importante para o
algoritmo BM, ficando conhecida como BMHS.
• Variante do BMH: endereçar a tabela com o caractere no texto
correspondente ao próximo caractere após o último caractere do
padrão, em vez de deslocar o padrão usando o último caractere como
no algoritmo BMH.
• Para pré-computar o padrão, o valor inicial de todas as entradas na
tabela de deslocamentos é feito igual a m + 1.
• A seguir, os m primeiros caracteres do padrão são usados para obter
os outros valores da tabela.
• Formalmente
d[x] = min{j tal que j = m | (1 ≤ j ≤ m & P [m + 1 − j] = x)}.
• Para o padrão P = teste, os valores de d são d[t] = 2, d[e] = 1,
d[s] = 3, e todos os outros valores são iguais ao valor de |P | + 1.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 46

BMHS - Implementação

void BMHS(TipoTexto T, long n, TipoPadrao P, long m) • A fase de pesquisa é


{ long i , j , k , d[ MAXCHAR + 1]; constituída por um anel
for ( j = 0; j <= MAXCHAR ; j ++) d[ j ] = m + 1; em que i varia de m
for ( j = 1; j <= m; j ++) d[P[ j −1]] = m − j + 1;
até n, com incrementos
i = m;
d[ord(T[i+1])], o que equi-
while ( i <= n) /∗−− Pesquisa−−∗/
{ k = i; vale ao endereço na ta-
j = m; bela d do caractere que
while (T[ k−1] == P[ j −1] && j > 0) { k−−; j −−; } está na i + 1-ésima posi-
i f ( j == 0) ção no texto, a qual cor-
p r i n t f ( " Casamento na posicao: %3ld \n" , k + 1); responde à posição do úl-
i += d[T[ i ] ] ;
timo caractere de P .
}
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 47

BH - Análise

• Os dois tipos de deslocamento (ocorrência e casamento) podem ser


pré-computados com base apenas no padrão e no alfabeto.

• Assim, a complexidade de tempo e de espaço para esta fase é


O(m + c).

• O pior caso do algoritmo é O(n + rm), onde r é igual ao número total


de casamentos, o que torna o algoritmo ineficente quando o número
de casamentos é grande.

• O melhor caso e o caso médio para o algoritmo é O(n/m), um


resultado excelente pois executa em tempo sublinear.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 48

BMH - Análise

• O deslocamento ocorrência também pode ser pré-computado com


base apenas no padrão e no alfabeto.

• A complexidade de tempo e de espaço para essa fase é O(c).

• Para a fase de pesquisa, o pior caso do algoritmo é O(nm), o melhor


caso é O(n/m) e o caso esperado é O(n/m), se c não é pequeno e m
não é muito grande.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.1 49

BMHS - Análise

• Na variante BMHS, seu comportamento assintótico é igual ao do


algoritmo BMH.

• Entretanto, os deslocamentos são mais longos (podendo ser iguais a


m + 1), levando a saltos relativamente maiores para padrões curtos.

• Por exemplo, para um padrão de tamanho m = 1, o deslocamento é


igual a 2m quando não há casamento.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.2 50

Casamento Aproximado
• O casamento aproximado de cadeias permite operações de inserção,
substituição e retirada de caracteres do padrão. Ex.: Três ocorrências
do padrão teste em que os casos de inserção, substituição, retirada
de caracteres no padrão acontecem:
1. um espaço é inserido entre o terceiro e quarto caracteres do
padrão;
2. o último caractere do padrão é substituído pelo caractere a;
3. o primeiro caractere do padrão é retirado.

tes te
testa
este
os testes testam estes alunos . . .
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.2 51

Distância de Edição

• Número k de operações de inserção, substituição e retirada de


caracteres necessário para transformar uma cadeia x em outra cadeia
y.

• ed(P, P ′ ): distância de edição entre duas cadeias P e P ′ ; é o menor


número de operações necessárias para converter P em P ′ , ou vice
versa.
Ex.: ed(teste, estende) = 4, valor obtido por meio de uma retirada do
primeiro t de P e a inserção dos 3 caracteres nde ao final de P .

• O problema do casamento aproximado de cadeias é o de encontrar


todas as ocorrências em T de cada P ′ que satisfaz ed(P, P ′ ) ≤ k.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.2 52

Casamento Aproximado
• A busca aproximada só faz sentido para 0 < k < m, pois para k = m toda
subcadeia de comprimento m pode ser convertida em P por meio da
substituição de m caracteres.

• O caso em que k = 0 corresponde ao casamento exato de cadeias.

• O nível de erro α = k/m, fornece uma medida da fração do padrão que pode
ser alterado.

• Em geral α < 1/2 para a maioria dos casos de interesse.

• Casamento aproximado de cadeias, ou casamento de cadeias


permitindo erros: um número limitado k de operações (erros) de inserção,
de substituição e de retirada é permitido entre P e suas ocorrências em T .

• A pesquisa com casamento aproximado é modelado por autômato


não-determinista.

• O algoritmo de casamento aproximado de cadeias usa o paralelismo de bit.


Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.2 53

Exemplo de Autômato para Casamento Aproximado


Σ
t e s t e
0 1 2 3 4 5

(a) Σ Σ Σ Σ Σ Σ • P ={teste} e k = 1.
t e s t e
0 1 2 3 4 5 • (a) inserção; (b) substitui-
Σ ção e (c) retirada.
t e s t e
0 1 2 3 4 5 • Casamento de caractere
(b) Σ Σ Σ Σ Σ
é representado por uma
e s t e
1 2 3 4 5 aresta horizontal. Avança-
Σ mos em P e T .
t e s t e
0 1 2 3 4 5
• O self-loop permite que
(c) e e e e e
uma ocorrência se inicie em
e s t e
1 2 3 4 5 qualquer posição em T .
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.2 54

Exemplo de Autômato para Casamento Aproximado

• Uma aresta vertical insere um caractere no padrão. Avançamos em T


mas não em P .
Σ
t e s t e
0 1 2 3 4 5

Σ Σ Σ Σ Σ
e s t e
1 2 3 4 5

• Uma aresta diagonal sólida substitui um caractere. Avançamos em T e


P.
Σ
t e s t e
0 1 2 3 4 5
Σ Σ Σ Σ Σ
e s t e
1 2 3 4 5
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.2 55

Exemplo de Autômato para Casamento Aproximado

• Uma aresta diagonal tracejada retira um caractere. Avançamos em P


mas não em T (transição-ǫ)
Σ
t e s t e
0 1 2 3 4 5
ε ε ε ε ε
e s t e
1 2 3 4 5
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.2 56

Exemplo de Autômato para Casamento Aproximado


Σ
t e s t e
0 1 2 3 4 5
Σ Σ Σ Σ Σ
Σ Σ Σ Σ Σ Σ
ε ε ε ε ε
t e s t e
0 1 2 3 4 5
Σ Σ Σ Σ Σ
Σ Σ Σ Σ Σ
Σ ε ε ε ε ε
t e s t e
0 1 2 3 4 5

• P ={teste} e K = 2.
• As três operações de distância de edição estão juntas em um único autômato:
– Linha 1: casamento exato (k = 0);
– Linha 2: casamento aproximado permitindo um erro (k = 1);
– Linha 3: casamento aproximado permitindo dois erros (k = 2).
• Uma vez que um estado no autômato está ativo, todos os estados nas linhas
seguintes na mesma coluna também estão ativos.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.2 57

Shift-And para Casamento Aproximado

• Utiliza paralelismo de bit.

• Simula um autômato não-determinista.

• Empacota cada linha j (0 < j ≤ k) do autômato não-determinista em


uma palavra Rj diferente do computador.

• Para cada novo caractere lido do texto todas as transições do


autômato são simuladas usando operações entre as k + 1 máscaras
de bits.

• Todas as k + 1 máscaras de bits têm a mesma estrutura e assim o


mesmo bit é alinhado com a mesma posição no texto.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.2 58

Shift-And para Casamento Aproximado


• Na posição i do texto, os novos valores Rj′ , 0 < j ≤ k, são obtidos a partir dos
valores correntes Rj :
– R0′ = ((R0 >> 1) | 10m−1 ) & M [T [i]]
– Rj′ = ((Rj >> 1) & M [T [i]]) | Rj−1 | (Rj−1 >> 1) | (Rj−1
′ >> 1) | 10m−1 ,
onde M é a tabela do algoritmo Shift-And para casamento exato.
• A pesquisa inicia com Rj = 1j 0m−j .
• R0 equivale ao algoritmo Shift-And para casamento exato.
• As outras linhas Rj recebem 1s (estados ativos) também de linhas anteriores.
• Considerando um automato para casamento aproximado, a fórmula para R′
expressa:
– arestas horizontais indicando casamento;
– verticais indicando inserção;
– diagonais cheias indicando substituição;
– diagonais tracejadas indicando retirada.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.2 59

Shif-And para Casamento Aproximado: Primeiro


Refinamento
void Shift-And-Aproximado ( P = p1 p2 . . . pm , T = t1 t2 . . . tn , k )
{ /∗−− Préprocessamento−−∗/
for ( c ∈ Σ ) M [c] = 0m ;
for ( j = 1; j <= m; j ++) M [pj ] = M [pj ] | 0j−1 10m−j ;
/∗−− Pesquisa−−∗/
for ( j = 0; j <= k ; j ++) Rj = 1j 0m−j ;
for ( i = 1; i <= n ; i ++)
{ Rant = R0 ;
Rnovo = ((Rant >> 1) | 10m−1 ) & M [T [i]] ;
R0 = Rnovo ;
for ( j = 1; j <= k ; j ++)
{ Rnovo = ((Rj >> 1 & M [T [i]]) | Rant | ((Rant | Rnovo) >> 1)) ;
Rant = Rj ;
Rj = Rnovo | 10m−1 ;
}
i f ( Rnovo & 0m−1 1 6= 0m ) ’Casamento na posicao i’ ;
}
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.2 60

Shif-And para Casamento Aproximado - Implementação


void ShiftAndAproximado(TipoTexto T, long n, TipoPadrao P, long m, long k)
{ long Masc[ MAXCHAR ] , i , j , Ri , Rant, Rnovo;
long R[ NUMMAXERROS + 1];
for ( i = 0; i < MAXCHAR ; i ++) Masc[ i ] = 0;
for ( i = 1; i <= m; i ++) { Masc[P[ i −1] + 127] |= 1 << (m − i ) ; }
R[ 0 ] = 0 ; Ri = 1 < < (m − 1);
for ( j = 1; j <= k ; j ++) R[ j ] = (1 < < (m − j ) ) | R[ j −1];
for ( i = 0; i < n ; i ++)
{ Rant = R[ 0 ] ;
Rnovo = ( ( ( (unsigned long)Rant) > > 1) | Ri) & Masc[T[ i ] + 127];
R[0] = Rnovo;
for ( j = 1; j <= k ; j ++)
{ Rnovo = ( ( ( (unsigned long)R[ j ]) > > 1) & Masc[T[ i ] + 127])
| Rant | ( ( ( unsigned long) (Rant | Rnovo)) > > 1);
Rant = R[ j ] ; R[ j ] = Rnovo | Ri ;
}
i f ( (Rnovo & 1) != 0) p r i n t f ( " Casamento na posicao %12ld \n" , i + 1);
}
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.2 61

Shif-And para Casamento Aproximado - 1 Erro de


Inserção
• Padrão: teste.

• Texto: os testes testam.

• Permitindo um erro (k = 1) de inserção.

• R0′ = (R0 >> 1)|10m−1 &M [T [i]]


R1′ = (R1 >> 1)&M [T [i]]|R0 |(10m−1 )

• Uma ocorrência exata na posição 8 (“e”) e duas, permitindo uma inserção,


nas posições 9 e 12 (“s” e “e”, respectivamente).
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.2 62

Shif-And para Casamento Aproximado - 1 Erro de


Inserção
Texto (R0 >> 1)|10m−1 R0′ R1 >> 1 R1′
o 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0
s 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0
1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0
t 1 0 0 0 0 1 0 0 0 0 0 1 0 0 0 1 0 0 0 0
e 1 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 1 0 0 0
s 1 0 1 0 0 0 0 1 0 0 0 1 1 0 0 1 1 1 0 0
t 1 0 0 1 0 1 0 0 1 0 0 1 1 1 0 1 0 1 1 0
e 1 1 0 0 1 0 1 0 0 1 0 1 0 1 1 1 1 0 1 1
s 1 0 1 0 0 0 0 1 0 0 0 1 1 0 1 1 1 1 0 1
1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 1 0 1 0 0
t 1 0 0 0 0 1 0 0 0 0 0 1 0 1 0 1 0 0 1 0
e 1 1 0 0 0 0 1 0 0 0 0 1 0 0 1 1 1 0 0 1
s 1 0 1 0 0 0 0 1 0 0 0 1 1 0 0 1 1 1 0 0
t 1 0 0 1 0 1 0 0 1 0 0 1 1 1 0 1 0 1 1 0
a 1 1 0 0 1 0 0 0 0 0 0 1 0 1 1 1 0 0 1 0
m 1 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 0 0 0 0
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.2 63

Shif-And para Casamento Aproximado - 1 Erro de


Inserção, 1 Erro de Retirada e 1 Erro de Substituição

• Padrão: teste.

• Texto: os testes testam.

• Permitindo um erro de inserção, um de retirada e um de substituição.

• R0′ = (R0 >> 1)|10m−1 &M [T [i]].


R1′ = (R1 >> 1)&M [T [i]]|R0 |(R0′ >> 1)|(R0 >> 1)|(10m−1 )

• Uma ocorrência exata na posição 8 (“e”) e cinco, permitindo um erro,


nas posições 7, 9, 12, 14 e 15 (“t”, “s”, “e”, “t” e “a”, respec.).
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.1.2 64

Shif-And para Casamento Aproximado - 1 Erro de


Inserção, 1 Erro de Retirada e 1 Erro de Substituição
Texto (R0 >> 1)|10m−1 R0′ R1 >> 1 R1′
o 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0
s 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0
1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0
t 1 0 0 0 0 1 0 0 0 0 0 1 0 0 0 1 1 0 0 0
e 1 1 0 0 0 0 1 0 0 0 0 1 1 0 0 1 1 1 0 0
s 1 0 1 0 0 0 0 1 0 0 0 1 1 1 0 1 1 1 1 0
t 1 0 0 1 0 1 0 0 1 0 0 1 1 1 1 1 1 1 1 1
e 1 1 0 0 1 0 1 0 0 1 0 1 1 1 1 1 1 1 1 1
s 1 0 1 0 0 0 0 1 0 0 0 1 1 1 1 1 1 1 1 1
1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 1 1 0
t 1 0 0 0 0 1 0 0 0 0 0 1 0 1 1 1 1 0 1 0
e 1 1 0 0 0 0 1 0 0 0 0 1 1 0 1 1 1 1 0 1
s 1 0 1 0 0 0 0 1 0 0 0 1 1 1 0 1 1 1 1 0
t 1 0 0 1 0 1 0 0 1 0 0 1 1 1 1 1 1 1 1 1
a 1 1 0 0 1 0 0 0 0 0 0 1 1 1 1 1 1 0 1 1
m 1 0 0 0 0 0 0 0 0 0 0 1 1 0 1 1 0 0 0 0
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2 65

Compressão - Motivação

• Explosão de informação textual disponível on-line:


– Bibliotecas digitais.
– Sistemas de automação de escritórios.
– Bancos de dados de documentos.
– World-Wide Web.

• Somente a Web tem hoje bilhões de páginas estáticas disponíveis.

• Cada bilhão de páginas ocupando aproximadamente 10 terabytes de


texto corrido.

• Em setembro de 2003, a máquina de busca Google


(www.google.com.br) dizia ter mais de 3,5 bilhões de páginas estáticas
em seu banco de dados.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2 66

Características necessárias para sistemas de


recuperação de informação

• Métodos recentes de compressão têm permitido:


1. Pesquisar diretamente o texto comprimido mais rapidamente do
que o texto original.
2. Obter maior compressão em relação a métodos tradicionais,
gerando maior economia de espaço.
3. Acessar diretamente qualquer parte do texto comprimido sem
necessidade de descomprimir todo o texto desde o início (Moura,
Navarro, Ziviani e Baeza-Yates, 2000; Ziviani, Moura, Navarro e
Baeza-Yates, 2000).

• Compromisso espaço X tempo:


– vencer-vencer.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.1 67

Porque Usar Compressão


• Compressão de texto - maneiras de representar o texto original em
menos espaço:
– Substituir os símbolos do texto por outros que possam ser
representados usando um número menor de bits ou bytes.
• Ganho obtido: o texto comprimido ocupa menos espaço de
armazenamento ⇒ menos tempo para ser lido do disco ou ser
transmitido por um canal de comunicação e para ser pesquisado.
• Preço a pagar: custo computacional para codificar e decodificar o
texto.
• Avanço da tecnologia: De acordo com Patterson e Hennessy (1995),
em 20 anos, o tempo de acesso a discos magnéticos tem se mantido
praticamente constante, enquanto a velocidade de processamento
aumentou aproximadamente 2 mil vezes ⇒ melhor investir mais poder
de computação em compressão em troca de menos espaço em disco
ou menor tempo de transmissão.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.1 68

Razão de Compressão

• Definida pela porcentagem que o arquivo comprimido representa em


relação ao tamanho do arquivo não comprimido.

• Exemplo: se o arquivo não comprimido possui 100 bytes e o arquivo


comprimido resultante possui 30 bytes, então a razão de compressão
é de 30%.

• Utilizada para medir O ganho em espaço obtido por um método de


compressão.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.1 69

Outros Importantes Aspectos a Considerar


Além da economia de espaço, deve-se considerar:

• Velocidade de compressão e de descompressão.

• Possibilidade de realizar casamento de cadeias diretamente no texto


comprimido.

• Permitir acesso direto a qualquer parte do texto comprimido e iniciar a


descompressão a partir da parte acessada:

Um sistema de recuperação de informação para grandes coleções


de documentos que estejam comprimidos necessitam acesso
direto a qualquer ponto do texto comprimido.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.2 70

Compressão de Textos em Linguagem Natural


• Um dos métodos de codificação mais conhecidos é o de Huffman (1952):
– Atribui códigos mais curtos a símbolos com frequências altas.
– Um código único, de tamanho variável, é atribuído a cada símbolo
diferente do texto.
– As implementações tradicionais do método de Huffman consideram
caracteres como símbolos.

• Para aliar as necessidades dos algoritmos de compressão às necessidades


dos sistemas de recuperação de informação apontadas acima, deve-se
considerar palavras como símbolos a serem codificados.

• Métodos de Huffman baseados em caracteres comprimem o texto para


aproximadamente 60%.

• Métodos de Huffman baseados em palavras comprimem o texto para valores


pouco acima de 25%.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.2 71

Métodos de Huffman Baseados em Palavras: Vantagens

• Permitem acesso randômico a palavras dentro do texto comprimido.

• Considerar palavras como símbolos significa que a tabela de símbolos


do codificador é exatamente o vocabulário do texto.

• Isso permite uma integração natural entre o método de compressão e


o arquivo invertido.

• Permitem acessar diretamente qualquer parte do texto comprimido


sem necessidade de descomprimir todo o texto desde o início.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.2 72

Família de Métodos de Compressão Ziv-Lempel

• Substitui uma sequência de símbolos por um apontador para uma


ocorrência anterior daquela sequência.

• A compressão é obtida porque os apontadores ocupam menos espaço


do que a sequência de símbolos que eles substituem.

• Os métodos Ziv-Lempel são populares pela sua velocidade, economia


de memória e generalidade.

• Já o método de Huffman baseado em palavras é muito bom quando a


cadeia de caracteres constitui texto em linguagem natural.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.2 73

Desvantagens dos Métodos de Ziv-Lempel para Ambiente


de Recuperação de Informação

• É necessário iniciar a decodificação desde o início do arquivo


comprimido ⇒ Acesso randômico muito caro.

• É muito difícil pesquisar no arquivo comprimido sem descomprimir.

• Uma possível vantagem do método Ziv-Lempel é o fato de não ser


necesário armazenar a tabela de símbolos da maneira com que o
método de Huffman precisa.

• No entanto, isso tem pouca importância em um ambiente de


recuperação de informação, já que se necessita o vocabulário do texto
para criar o índice e permitir a pesquisa eficiente.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 74

Compressão de Huffman Usando Palavras

• Técnica de compressão mais eficaz para textos em linguagem natural.

• O método considera cada palavra diferente do texto como um símbolo.

• Conta suas frequências e gera um código de Huffman para as


palavras.

• A seguir, comprime o texto substituindo cada palavra pelo seu código.

• Assim, a compressão é realizada em duas passadas sobre o texto:


1. Obtenção da frequência de cada palavra diferente.
2. Realização da compressão.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 75

Forma Eficiente de Lidar com Palavras e Separadores

• Um texto em linguagem natural é constituído de palavras e de


separadores.
• Separadores são caracteres que aparecem entre palavras: espaço,
vírgula, ponto, ponto e vírgula, interrogação, e assim por diante.
• Uma forma eficiente de lidar com palavras e separadores é
representar o espaço simples de forma implícita no texto comprimido.
• Nesse modelo, se uma palavra é seguida de um espaço, então,
somente a palavra é codificada.
• Senão, a palavra e o separador são codificados separadamente.
• No momento da decodificação, supõe-se que um espaço simples
segue cada palavra, a não ser que o próximo símbolo corresponda a
um separador.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 76

Compressão usando codificação de Huffman


Exemplo: “para cada rosa rosa, uma rosa é uma rosa”
a) 1 1 4 1 2 1 b) 4 1 2 1
para cada rosa , uma é 2 rosa , uma é
0 1
para cada

c) 4 2 d) 4 2
2 rosa 2 uma 4 rosa uma
0 1 0 1 0 1
para cada , é 2 2
0 1 0 1
para cada , é

e) 4 f) 10
6 rosa 0 1
0 1
rosa 6
uma 4 0 1
0 1
uma 4
2 2 0 1
0 1 0 1
2 2
para cada , é
0 1 0 1
para cada , é

OBS: O algoritmo de Huffman é uma abordagem gulosa.


Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 77

Árvore de Huffman

• O método de Huffman produz a árvore de codificação que minimiza o


comprimento do arquivo comprimido.

• Existem diversas árvores que produzem a mesma compressão.

• Por exemplo, trocar o filho à esquerda de um nó por um filho à direita


leva a uma árvore de codificação alternativa com a mesma razão de
compressão.

• Entretanto, a escolha preferencial para a maioria das aplicações é a


árvore canônica.

• Uma árvore de Huffman é canônica quando a altura da subárvore à


direita de qualquer nó nunca é menor que a altura da subárvore à
esquerda.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 78

Árvore de Huffman

• A representação do código na forma de árvore facilita a visualização.

• Sugere métodos de codificação e decodificação triviais:


– Codificação: a árvore é percorrida emitindo bits ao longo de suas
arestas.
– Decodificação: os bits de entrada são usados para selecionar as
arestas.

• Essa abordagem é ineficiente tanto em termos de espaço quanto em


termos de tempo.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 79

Algoritmo Baseado na Codificação Canônica com


Comportamento Linear em Tempo e Espaço

• O algoritmo é atribuído a Moffat e Katajainen (1995).

• Calcula os comprimentos dos códigos em lugar dos códigos


propriamente ditos.

• A compressão atingida é a mesma, independentemente dos códigos


utilizados.

• Após o cálculo dos comprimentos, há uma forma elegante e eficiente


para a codificação e a decodificação.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 80

O Algoritmo
• A entrada do algoritmo é um vetor A contendo as frequências das
palavras em ordem não-crescente.
• Frequências relativas à frase exemplo: “para cada rosa rosa,
uma rosa é uma rosa”
4 2 1 1 1 1

• Durante execução são utilizados vetores logicamente distintos, mas


capazes de coexistirem no mesmo vetor das frequências.
• O algoritmo divide-se em três fases:
1. Combinação dos nós.
2. Conversão do vetor no conjunto das profundidades dos nós
internos.
3. Calculo das profundidades dos nós folhas.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 81

Primeira Fase - Combinação dos nós

1 2 n
a) ...

Frequências
Folha Prox Raiz
1 n
b) ... ... ... ...

Frequências Posições Pesos dos Índices pais


dos nós folhas disponíveis nós internos nós internos
1 2 3 n
c) ...

Peso da Índices pais


árvore nós internos
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 82

Primeira Fase - Combinação dos nós

• A primeira fase é baseada em duas observações:


1. A frequência de um nó só precisa ser mantida até que ele seja
processado.
2. Não é preciso manter apontadores para os pais dos nós folhas,
pois eles podem ser inferidos.

Exemplo: nós internos nas profundidades [0, 1, 2, 3, 3] teriam nós


folhas nas profundidades [1, 2, 4, 4, 4, 4].
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 83

Pseudocódigo para a Primeira Fase


PrimeiraFase (A, n)
{ Raiz = n ; Folha = n;
for ( Prox = n ; n >= 2; Prox−−)
{ /∗ Procura Posicao ∗/
i f ( (nao existe Folha ) | | ( ( Raiz > Prox) && (A[Raiz] <= A[Folha ] ) ) )
{ A[Prox] = A[Raiz ] ; A[Raiz ] = Prox ; Raiz = Raiz − 1; /∗ No interno ∗/ }
else { A[Prox] = A[Folha ] ; Folha = Folha − 1; /∗ No folha ∗/ }
/∗ Atualiza Frequencias ∗/
i f ( (nao existe Folha ) | | ( ( Raiz > Prox) && (A[Raiz] <= A[Folha ] ) ) )
{ /∗ No interno ∗/
A[Prox] = A[Prox] + A[Raiz ] ; A[Raiz ] = Prox ; Raiz = Raiz − 1;
}
else { A[Prox] = A[Prox] + A[Folha ] ; Folha = Folha − 1; /∗ No folha ∗/ }
}
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 84

Exemplo de Processamento da Primeira Fase

1 2 3 4 5 6 Prox Raiz Folha


a) 4 2 1 1 1 1 6 6 6
b) 4 2 1 1 1 1 6 6 5
c) 4 2 1 1 1 2 5 6 4
d) 4 2 1 1 1 2 5 6 3
e) 4 2 1 1 2 2 4 6 2
f) 4 2 1 2 2 4 4 5 2
g) 4 2 1 4 4 4 3 4 2
h) 4 2 2 4 4 4 3 4 1
i) 4 2 6 3 4 4 2 3 1
j) 4 4 6 3 4 4 2 3 0
k) 10 2 3 4 4 1 2 0
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 85

Segunda Fase - Conversão do Vetor no Conjunto das


Profundidades dos nós internos

1 2 3 n
a) ...

Peso da Índices pais


árvore nós internos
Prox
1 2 n
b) ... ...

Profundidade Índices pais


dos nós internos nós internos
1 2 n
c) ...

Profundidade dos nós internos


Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 86

Pseudocódigo para a Segunda Fase


SegundaFase (A, n)
{ A[2] = 0;
for ( Prox = 3; Prox <= n ; Prox++) A[Prox] = A[A[Prox] ] + 1 ;
}

Profundidades dos nós internos obtida com a segunda fase tendo como
entrada o vetor exibido na letra k do slide 84.

0 1 2 3 3
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 87

Terceira Fase - Calculo das profundidades dos nós folhas

1 2 n
a) ...

Profundidade dos nós internos


Prox Raiz
1 n
b) ... ... ...

Comprimento Posições Profundidade


dos códigos disponíveis dos nós internos
1 n
c) ...

Comprimento dos códigos


Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 88

Pseudocódigo para a Terceira Fase


TerceiraFase (A, n)
{ Disp = 1; u = 0; h = 0; Raiz = 2; Prox = 1;
while ( Disp > 0)
{ while ( Raiz <= n && A[Raiz] == h ) { u = u + 1; Raiz = Raiz + 1 ; }
while ( Disp > u ) { A[Prox] = h ; Prox = Prox + 1; Disp = Disp − 1; }
Disp = 2 ∗ u ; h = h + 1; u = 0;
}
}

• Aplicando a Terceira Fase sobre:


0 1 2 3 3

Os comprimentos dos códigos em número de bits são obtidos:

1 2 4 4 4 4
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 89

Cálculo do comprimento dos códigos a partir de um


vertor de frequências
CalculaCompCodigo (A, n)
{ A = PrimeiraFase (A, n) ;
A = SegundaFase (A, n) ;
A = TerceiraFase (A, n) ;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 90

Código Canônico
• Os comprimentos dos códigos obedecem ao algoritmo de Huffman.
• Códigos de mesmo comprimento são inteiros consecutivos.
• A partir dos comprimentos obtidos, o cálculo dos códigos é trivial: o primeiro
código é composto apenas por zeros e, para os demais, adiciona-se 1 ao
código anterior e faz-se um deslocamento à esquerda para obter-se o
comprimento adequado quando necessário.
• Codificação Canônica Obtida:

i Símbolo Código Canônico


1 rosa 0
2 uma 10
3 para 1100
4 cada 1101
5 ,⊔ 1110
6 é 1111
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 91

Elaboração de Algoritmos Eficientes para a Codificação e


para a Decodificação

• Os algoritmos são baseados na seguinte observação:


– Códigos de mesmo comprimento são inteiros consecutivos.

• Os algoritmos são baseados no uso de dois vetores com


MaxCompCod elementos,sendo MaxCompCod o comprimento do
maior código.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 92

Vetores Base e Offset


• Vetor Base: indica, para um dado comprimento c, o valor inteiro do primeiro
código com esse comprimento.
• O vetor Base é calculado pela relação:

0 se c = 1,
Base[c] =
2 × (Base[c − 1] + wc−1 ) caso contrário,

sendo wc o número de códigos com comprimento c.


• Offset: indica o índice no vocabulário da primeira palavra de cada
comprimento de código c.
• Vetores Base e Offset para a c Base[c] Offset[c]
tabela do slide 90:
1 0 1
2 2 2
3 6 2
4 12 3
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 93

Pseudocódigo para codificação


Codifica (Base, Offset , i , MaxCompCod)
{ c = 1;
while ( i >= Offset [ c + 1] ) && (c + 1 <= MaxCompCod )
c = c + 1;
Codigo = i − Offset [ c ] + Base[ c ] ;
}

Obtenção do código:
• Parâmetros: vetores Base e Offset, índice i do símbolo (Tabela da
transparência 71) a ser codificado e o comprimento MaxCompCod dos
vetores Base e Offset.
• No anel while é feito o cálculo do comprimento c de código a ser
utilizado.
• A seguir, basta saber qual a ordem do código para o comprimento c
(i − Offset[c]) e somar esse valor à Base[c].
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 94

Exemplo de Codificação

• Para a palavra i = 4 (“cada”):


1. Verifica-se que é um código de comprimento 4.
2. Verifica-se também que é o segundo código com esse
comprimento.
3. Assim, seu código é 13 (4 − Offset[4] + Base[4]), o que corresponde
a “1101” em binário.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 95

Pseudocódigo para decodificação


Decodifica (Base, Offset , ArqComprimido, MaxCompCod)
{ c = 1;
Codigo = LeBit (ArqComprimido) ;
while ( ( ( Codigo < < 1 ) >= Base[ c + 1]) && ( c + 1 <= MaxCompCod ) )
{ Codigo = (Codigo < < 1) || LeBit (ArqComprimido) ;
c = c + 1;
}
i = Codigo − Base[ c ] + Offset [ c ] ;
}

• Parâmetros: vetores Base e Offset, o arquivo comprimido e o


comprimento MaxCompCod dos vetores Base e Offset.
• Na decodificação, o arquivo de entrada é lido bit-a-bit, adicionando-se
os bits lidos ao código e comparando-o com o vetor Base.
• O anel while mostra como identificar o código a partir de uma posição
do arquivo comprimido.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 96

Exemplo de Decodificação
• Decodificação da sequência de bits “1101”:

c LeBit Codigo Codigo << 1 Base[c + 1] • A primeira linha da tabela


1 1 1 - - é o estado inicial do while
2 1 10 or 1 = 11 10 10 quando já foi lido o primeiro
3 0 110 or 0 = 110 110 110 bit da sequência, atribuído
4 1 1100 or 1 = 1101 1100 1100
à variável Codigo.

• A linha dois e seguintes representam a situação do anel while após cada


respectiva iteração.
• Na linha dois, o segundo bit foi lido (bit “1”) e a variável Codigo recebe o
código anterior deslocado à esquerda de um bit seguido da operação or com
o bit lido.
• De posse do código, Base e Offset são usados para identificar qual o índice i
da palavra no vocabulário, sendo i = Codigo − Base[c] + Offset[c].
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 97

Pseudocódigo para Realizar a Compressão


Compressao ( ArqTexto , ArqComprimido)
{ /∗ Primeira etapa ∗/
while ( ! feof ( ArqTexto) )
{ Palavra = ExtraiProximaPalavra ( ArqTexto ) ;
Pos = Pesquisa ( Palavra , Vocabulario ) ;
i f Pos é uma posicao valida
Vocabulario [Pos] . Freq = Vocabulario [Pos] . Freq + 1
else Insere ( Palavra , Vocabulario ) ;
}
/∗ Segunda etapa ∗/
Vocabulario = OrdenaPorFrequencia ( Vocabulario ) ;
Vocabulario = CalculaCompCodigo ( Vocabulario , n) ;
ConstroiVetores (Base, Offset , ArqComprimido) ;
Grava ( Vocabulario , ArqComprimido) ;
LeVocabulario ( Vocabulario , ArqComprimido) ;
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 98

Pseudocódigo para Realizar a Compressão


/∗ Terceira etapa ∗/
PosicionaPrimeiraPosicao ( ArqTexto ) ;
while ( ! feof (ArqTexto) )
{ Palavra = ExtraiProximaPalavra ( ArqTexto ) ;
Pos = Pesquisa ( Palavra , Vocabulario ) ;
Codigo = Codifica (Base, Offset ,
Vocabulario [Pos] .Ordem, MaxCompCod) ;
Escreve (ArqComprimido, Codigo) ;
}
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.3 99

Pseudocódigo para Realizar a Descompressão


Descompressao ( ArqTexto , ArqComprimido)
{ LerVetores (Base, Offset , ArqComprimido) ;
LeVocabulario ( Vocabulario , ArqComprimido) ;
while ( ! feof (ArqComprimido) )
{ i = Decodifica (Base, Offset , ArqComprimido, MaxCompCod) ;
Grava ( Vocabulario [ i ] , ArqTexto ) ;
}
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 100

Codificação de Huffman Usando Bytes

• O método original proposto por Huffman (1952) tem sido usado como
um código binário.

• Moura, Navarro, Ziviani e Baeza-Yates (2000) modificaram a atribuição


de códigos de tal forma que uma sequência de bytes é associada a
cada palavra do texto.

• Consequentemente, o grau de cada nó passa de 2 para 256. Essa


versão é chamada de código de Huffman pleno.

• Outra possibilidade é utilizar apenas 7 dos 8 bits de cada byte para a


codificação, e a árvore passa então a ter grau 128.

• Nesse caso, o oitavo bit é usado para marcar o primeiro byte do código
da palavra, sendo chamado de código de Huffman com marcação.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 101

Exemplo de Códigos Plenos e com Marcação

• O código de Huffman com marcação ajuda na pesquisa sobre o texto


comprimido.

• Exemplo:
– Código pleno para “uma” com 3 bytes: “47 81 8”.
– Código com marcação para “uma” com 3 bytes: “175 81 8”
– Note que o primeiro byte é 175 = 47 + 128.

• Assim, no código com marcação o oitavo bit é 1 quando o byte é o


primeiro do código, senão ele é 0.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 102

Árvore de Huffman Orientada a Bytes


• A construção da árvore de Huffman orientada a bytes pode ocasionar o
aparecimento de nós internos não totalmente preenchidos:
a) Árvore ineficiente
...
... ...
254 nós vazios
256 elementos 256 elementos

b) Árvore ótima
...
... ...
254 elementos
256 elementos 2 elementos 254 nós vazios

• Na Figura, o alfabeto possui 512 símbolos (nós folhas), todos com a mesma
frequência de ocorrência.
• O segundo nível tem 254 espaços vazios que poderiam ser ocupados com
símbolos, mudando o comprimento de seus códigos de 2 para 1 byte.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 103

Movendo Nós Vazios para Níveis mais Profundos

• Um meio de assegurar que nós vazios sempre ocupem o nível mais


baixo da árvore é combiná-los com os nós de menores frequências.

• O objetivo é movê-los para o nível mais profundo da árvore.

• Para isso, devemos selecionar o número de símbolos que serão


combinados com os nós vazios, dada pela equação:
1 + ((n − BaseNum) mod (BaseNum − 1))

• No caso da Figura da transparência anterior é igual a 1 + ((512 − 256)


mod 255) = 2.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 104

Cálculo dos Comprimentos dos Códigos


void CalculaCompCodigo( TipoDicionario A, int n)
{ int u = 0; /∗ Nodos internos usados ∗/
int h = 0; /∗ Altura da arvore ∗/
int NoInt ; /∗ Numero de nodos internos ∗/
int Prox , Raiz , Folha;
int Disp = 1; int x , Resto;
i f (n > BASENUM − 1)
{ Resto = 1 + ((n − BASENUM) % (BASENUM − 1));
i f (Resto < 2) Resto = BASENUM ;
}
else Resto = n − 1;
NoInt = 1 + ((n − Resto ) / ( BASENUM − 1));
for ( x = n − 1; x >= (n − Resto) + 1 ; x−−) A[n ] . Freq += A[ x ] . Freq;
/∗ Primeira Fase ∗/
Raiz = n ; Folha = n − Resto;
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 105

Cálculo dos Comprimentos dos Códigos


for ( Prox = n − 1; Prox >= (n − NoInt ) + 1 ; Prox−−)
{ /∗ Procura Posicao ∗/
i f ( ( Folha < 1 ) | | ( ( Raiz > Prox) && (A[Raiz ] . Freq<=A[Folha ] . Freq) ) )
{ /∗ No interno ∗/
A[Prox ] . Freq = A[Raiz ] . Freq ; A[Raiz ] . Freq = Prox;
Raiz−−;
}
else { /∗ No−folha ∗/
A[Prox ] . Freq = A[Folha ] . Freq ; Folha−−;
}
/∗ Atualiza Frequencias ∗/
for ( x = 1; x <= BASENUM − 1; x++)
{ i f ( ( Folha < 1 ) | | ( ( Raiz>Prox) && (A[Raiz ] . Freq<=A[Folha ] . Freq) ) )
{ /∗ No interno ∗/
A[Prox ] . Freq += A[Raiz ] . Freq ; A[Raiz ] . Freq = Prox;
Raiz−−;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 106

Cálculo dos Comprimentos dos Códigos


else { /∗ No−folha ∗/
A[Prox ] . Freq += A[Folha ] . Freq ; Folha−−;
}
}
}
/∗ Segunda Fase ∗/
A[Raiz ] . Freq = 0;
for ( Prox = Raiz + 1; Prox <= n ; Prox++) A[Prox ] . Freq = A[A[Prox ] . Freq ] . Freq + 1;
/∗ Terceira Fase ∗/
Prox = 1;
while ( Disp > 0)
{ while ( Raiz <= n && A[Raiz ] . Freq == h ) { u++; Raiz++; }
while ( Disp > u)
{ A[Prox ] . Freq = h ; Prox++; Disp−−;
i f ( Prox > n ) { u = 0; break ; }
}
Disp = BASENUM ∗ u ; h++; u = 0;
}
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 107

Cálculo dos Comprimentos dos Códigos: Generalização

OBS: A constante BaseNum pode ser usada para trabalharmos com


quaisquer bases numéricas menores ou iguais a um byte. Por exemplo,
para a codificação plena o valor é 256 e para a codificação com marcação
o valor é 128.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 108

Mudanças em Relação ao Pseudocódigo Apresentado

• A mais sensível está no código inserido antes da primeira fase, o qual


tem como função eliminar o problema causado por nós internos da
árvore não totalmente preenchidos.

• Na primeira fase, as BaseNum árvores de menor custo são


combinadas a cada passo, em vez de duas como no caso da
codificação binária:
– Isso é feito pelo anel for introduzido na parte que atualiza
frequências na primeira fase.

• A segunda fase não sofre alterações.

• A terceira fase é alterada para indicar quantos nós estão disponíveis


em cada nível, o que é representado pela variável Disp.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 109

Codificação Orientada a Bytes


int Codifica (TipoVetoresBO VetoresBaseOffset ,
int Ordem, int ∗c,
int MaxCompCod)
{ ∗c = 1;
while (Ordem >= VetoresBaseOffset[∗c + 1]. Offset &&
∗c + 1 <= MaxCompCod) ( ∗c)++;
return (Ordem − VetoresBaseOffset[∗c ] . Offset +
VetoresBaseOffset[∗c ] .Base) ;
}

OBS: a codificação orientada a bytes não requer nenhuma alteração em


relação à codificação usando bits
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 110

Decodificação Orientada a Bytes

int Decodifica (TipoVetoresBO VetoresBaseOffset , FILE ∗ArqComprimido, int MaxCompCod)


{ int c = 1;
int Codigo = 0 , CodigoTmp = 0;
int LogBase2 = ( int )round( log (BASENUM ) / log (2.0));
fread(&Codigo, sizeof(unsigned char) , 1 , ArqComprimido) ;
i f (LogBase2 == 7) Codigo −= 128; /∗ remove o b i t de marcacao ∗/
while ( ( c + 1 <= MaxCompCod) && ((Codigo << LogBase2) >= VetoresBaseOffset [ c+1].Base) )
{ fread(&CodigoTmp, sizeof(unsigned char) , 1 , ArqComprimido) ;
Codigo = (Codigo << LogBase2) | CodigoTmp;
c++;
}
return (Codigo − VetoresBaseOffset [ c ] .Base + VetoresBaseOffset [ c ] . Offset ) ;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 111

Decodificação Orientada a Bytes

Alterações:

1. Permitir leitura byte a byte do arquivo comprimido, em vez de bit a bit.

2. O número de bits deslocados à esquerda para encontrar o


comprimento c do código (o qual indexa Base e Offset) é dado por:
log2 BaseNum
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 112

Cálculo dos Vetores Base e Offset

• O cálculo do vetor Offset não requer alteração alguma.

• Para generalizar o cálculo do vetor Base, basta substituir o fator 2 por


BaseNum, como na relação abaixo:

0 se c = 1,
Base[c] =
BaseNum × (Base[c − 1] + wc−1 ) caso contrário.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 113

Construção dos Vetores Base e Offset (1)


int ConstroiVetores(TipoVetoresBO VetoresBaseOffset ,
TipoDicionario Vocabulario ,
int n, FILE ∗ArqComprimido)
{ int Wcs[ MAXTAMVETORESDO + 1];
int i , MaxCompCod;
MaxCompCod = Vocabulario [n ] . Freq;
for ( i = 1; i <= MaxCompCod; i ++)
{ Wcs[ i ] = 0 ; VetoresBaseOffset [ i ] . Offset = 0 ; }
for ( i = 1; i <= n ; i ++)
{ Wcs[ Vocabulario [ i ] . Freq]++;
VetoresBaseOffset [ Vocabulario [ i ] . Freq ] . Offset =
i − Wcs[ Vocabulario [ i ] . Freq] + 1;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 114

Construção dos Vetores Base e Offset (2)


VetoresBaseOffset [ 1 ] .Base = 0;
for ( i = 2; i <= MaxCompCod; i ++)
{ VetoresBaseOffset [ i ] .Base =
BASENUM ∗ ( VetoresBaseOffset [ i −1].Base + Wcs[ i −1]);
i f ( VetoresBaseOffset [ i ] . Offset == 0)
VetoresBaseOffset [ i ] . Offset = VetoresBaseOffset [ i −1].Offset ;
}
/∗ Salvando as tabelas em disco ∗/
GravaNumInt(ArqComprimido, MaxCompCod) ;
for ( i = 1; i <= MaxCompCod; i ++)
{ GravaNumInt(ArqComprimido, VetoresBaseOffset [ i ] .Base) ;
GravaNumInt(ArqComprimido, VetoresBaseOffset [ i ] . Offset ) ;
}
return MaxCompCod;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 115

Procedimentos para Ler e Escrever Números Inteiros em


um Arquivo de Bytes
int LeNumInt(FILE ∗ArqComprimido)
{ int Num;
fread(&Num, sizeof( int ) , 1 , ArqComprimido) ;
return Num;
}

• O procedimento LeNumInt lê do disco cada byte de um número inteiro


e o recompõe.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 116

Procedimentos para Ler e Escrever Números Inteiros em


um Arquivo de Bytes
void GravaNumInt(FILE ∗ArqComprimido, int Num)
{ fwrite(&Num, sizeof( int ) , 1 , ArqComprimido ) ; }

• O procedimento GravaNumInt grava no disco cada byte (da esquerda


para a direita) do número inteiro passado como parâmetro.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 117

O Por Quê da Existência de LeNumInt e GravaNumInt


• São necessários em razão de a variável ArqComprimido, passada
como parâmetro, ter sido declarada no programa principal como um
arquivo de bytes.
• Isso faz com que o procedimento write (read) do Pascal escreva (leia)
do disco o byte mais à direita do número.
• Por exemplo, considere o número 300 representado em 4 bytes, como
mostrado na Figura abaixo.
• Caso fosse utilizado o procedimento write, seria gravado o número 44
em disco, que é o número representado no byte mais à direita.
• Um problema análogo ocorre ao se utlizar o procedimento read para
ler do disco um número inteiro representado em mais de um byte.

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 1 1 0 0

Byte 0 Byte 1 Byte 2 Byte 3


Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 118

Define Alfabeto Utilizado na Composição de Palavras


void DefineAlfabeto(TipoAlfabeto Alfabeto , FILE ∗ArqAlf )
{ /∗ Os Simbolos devem estar juntos em uma linha no arquivo ∗/
char Simbolos[ MAXALFABETO + 1];
int i ;
char ∗Temp;
for ( i = 0; i <= MAXALFABETO ; i ++) Alfabeto [ i ] = FALSE ;
fgets (Simbolos , MAXALFABETO + 1 , ArqAlf ) ;
Temp = strchr (Simbolos , ’ \n ’ ) ;
i f (Temp ! = NULL) ∗Temp = 0;
for ( i = 0; i <= strlen (Simbolos) − 1; i ++)
Alfabeto [Simbolos[ i ] + 127] = TRUE ;
Alfabeto [0] = FALSE ; /∗ caractere de codigo zero : separador ∗/

OBS: O procedimento DefineAlfabeto lê de um arquivo “alfabeto.txt” todos o


caracteres que serão utilizados para compor palavras.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 119

Extração do Próximo Símbolo a ser Codificado (1)


void ExtraiProximaPalavra ( TipoPalavra Result , int ∗TipoIndice ,
char ∗Linha , FILE ∗ArqTxt , TipoAlfabeto Alfabeto )
{ short FimPalavra = FALSE , Aux = FALSE ;
Result [0] = ’ \0 ’ ;
i f (∗ TipoIndice > strlen (Linha ) )
{ i f ( fgets (Linha , MAXALFABETO + 1 ,ArqTxt ) )
{ /∗ Coloca um delimitador em Linha ∗/
sprintf (Linha + strlen (Linha ) , "%c" , (char) 0 ) ; ∗TipoIndice = 1;
}
else { sprintf (Linha , "%c" , (char) 0 ) ; FimPalavra = TRUE ; }
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 120

Extração do Próximo Símbolo a ser Codificado (2)


while (∗ TipoIndice <= strlen (Linha) && !FimPalavra)
{ i f ( Alfabeto [ Linha[∗TipoIndice − 1] + 127])
{ sprintf (Result + strlen (Result ) , "%c" , Linha[∗TipoIndice − 1]);
Aux = TRUE ;
}
else { i f (Aux)
{ i f ( Linha[∗TipoIndice − 1] != (char)0) (∗ TipoIndice)−−; }
else { sprintf (Result + strlen (Result ) , "%c" ,
Linha[∗TipoIndice − 1]);
}
FimPalavra = TRUE ;
}
(∗TipoIndice)++;
}
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 121

Código para Fazer a Compressão

• O Código para fazer a compressão é dividido em três etapas:


1. Na primeira, as palavras são extraídas do texto a ser comprimido e
suas respectivas frequências são contabilizadas.
2. Na segunda, são gerados os vetores Base e Offset, os quais são
gravados no arquivo comprimido seguidamente do vocabulário.
Para delimitar os símbolos do vocabulário no disco, cada um deles
é separado pelo caractere zero.
3. Na terceira, o arquivo texto é percorrido pela segunda vez, sendo
seus símbolos novamente extraídos, codificados e gravados no
arquivo comprimido.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 122

Código para Fazer a Compressão


void Compressao(FILE ∗ArqTxt , FILE ∗ArqAlf , FILE ∗ArqComprimido)
{ TipoAlfabeto Alfabeto ;
TipoPalavra Palavra , Linha ; int Ind = 1 , MaxCompCod; TipoPesos p;
TipoDicionario Vocabulario = ( TipoDicionario )
calloc (M+1, sizeof(TipoItem ) ) ;
TipoVetoresBO VetoresBaseOffset = (TipoVetoresBO)
calloc (MAXTAMVETORESDO+1, sizeof(TipoBaseOffset ) ) ;
f p r i n t f ( stderr , "Definindo alfabeto \n" ) ;
DefineAlfabeto( Alfabeto , ArqAlf ) ; /∗Le alfabeto def . em arquivo∗/
∗Linha = ’ \0 ’ ;
f p r i n t f ( stderr , " Incializando Voc. \ n" ) ; Inicializa (Vocabulario ) ;
f p r i n t f ( stderr , "Gerando Pesos\n" ) ; GeraPesos(p) ;
f p r i n t f ( stderr , "Primeira etapa\n" ) ;
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 123

Código para Fazer a Compressão


PrimeiraEtapa(ArqTxt , Alfabeto , &Ind ,
Palavra , Linha , Vocabulario , p) ;
f p r i n t f ( stderr , "Segunda etapa\n" ) ;
MaxCompCod = SegundaEtapa(Vocabulario , VetoresBaseOffset , p,
ArqComprimido) ;
fseek(ArqTxt , 0 , SEEK_SET) ; /∗ Move cursor para inicio do arquivo∗/
Ind = 1; ∗Linha = ’ \0 ’ ;
f p r i n t f ( stderr , "Terceira etapa\n" ) ;
TerceiraEtapa(ArqTxt , Alfabeto , &Ind , Palavra , Linha , Vocabulario , p,
VetoresBaseOffset , ArqComprimido, MaxCompCod) ;
free ( Vocabulario ) ; free ( VetoresBaseOffset ) ;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 124

Primeira Etapa da Compressão (1)


void PrimeiraEtapa(FILE ∗ArqTxt , TipoAlfabeto Alfabeto , int ∗TipoIndice ,
TipoPalavra Palavra , char ∗Linha ,
TipoDicionario Vocabulario , TipoPesos p)
{ TipoItem Elemento ; int i ; char ∗ PalavraTrim = NULL ;
do
{ ExtraiProximaPalavra(Palavra , TipoIndice , Linha , ArqTxt , Alfabeto ) ;
memcpy(Elemento.Chave, Palavra , sizeof(TipoChave) ) ;
Elemento.Freq = 1;
i f (∗Palavra ! = ’ \0 ’ )
{ i = Pesquisa(Elemento.Chave, p, Vocabulario ) ;
i f ( i < M)
Vocabulario [ i ] . Freq++;
else Insere(&Elemento, p, Vocabulario ) ;
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 125

Primeira Etapa da Compressão (2)


do
{ ExtraiProximaPalavra(Palavra , TipoIndice , Linha ,
ArqTxt , Alfabeto ) ;
memcpy(Elemento.Chave, Palavra , sizeof(TipoChave) ) ;
/∗ O primeiro espaco depois da palavra nao e codificado ∗/
i f ( PalavraTrim ! = NULL ) free (PalavraTrim ) ;
PalavraTrim = Trim(Palavra ) ;
i f ( strcmp(PalavraTrim , " " ) && (∗PalavraTrim ) ! = (char)0)
{ i = Pesquisa(Elemento.Chave, p, Vocabulario ) ;
i f ( i < M) Vocabulario [ i ] . Freq++;
else Insere(&Elemento, p, Vocabulario ) ;
}
} while ( strcmp(Palavra , " " ) ) ;
i f ( PalavraTrim ! = NULL ) free (PalavraTrim ) ;
}
} while ( Palavra [ 0 ] ! = ’ \0 ’ ) ;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 126

Segunda Etapa da Compressão (1)


int SegundaEtapa( TipoDicionario Vocabulario , TipoVetoresBO VetoresBaseOffset ,
TipoPesos p, FILE ∗ArqComprimido)
{
int Result , i , j , NumNodosFolhas, PosArq;
TipoItem Elemento;
char Ch;
TipoPalavra Palavra ;
NumNodosFolhas = OrdenaPorFrequencia(Vocabulario ) ;
CalculaCompCodigo(Vocabulario , NumNodosFolhas) ;
Result = ConstroiVetores(VetoresBaseOffset , Vocabulario ,
NumNodosFolhas, ArqComprimido) ;
/∗ Grava Vocabulario ∗/
GravaNumInt(ArqComprimido, NumNodosFolhas) ;
PosArq = f t e l l (ArqComprimido) ;
for ( i = 1; i <= NumNodosFolhas; i ++)
{
j = strlen (Vocabulario [ i ] .Chave) ;
fwrite (Vocabulario [ i ] .Chave, sizeof(char) , j + 1 , ArqComprimido) ;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 127

Segunda Etapa da Compressão (2)


/∗ Le e reconstroi a condicao de hash no vetor que contem o vocabulario ∗/
fseek(ArqComprimido, PosArq, SEEK_SET) ;
Inicializa (Vocabulario ) ;
for ( i = 1; i <= NumNodosFolhas; i ++)
{
∗Palavra = ’ \0 ’ ;
do
{ fread(&Ch, sizeof(char) , 1 , ArqComprimido) ;
i f (Ch ! = (char)0)
sprintf (Palavra + strlen (Palavra ) , "%c" , Ch) ;
} while (Ch ! = (char)0);
memcpy(Elemento.Chave, Palavra , sizeof(TipoChave) ) ;
Elemento.Ordem = i ;
j = Pesquisa(Elemento.Chave, p, Vocabulario ) ;
i f ( j >= M)
Insere(&Elemento, p, Vocabulario ) ;
}
return Result ;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 128

Função para Ordenar o Vocabulário por Frequência


• O objetivo dessa função é ordenar in situ o vetor Vocabulario,
utilizando a própria tabela hash.
• Para isso, os símbolos do vetor Vocabulario são copiados para as
posições de 1 a n no próprio vetor e ordenados de forma não
crescente por suas respectivas frequências de ocorrência.
• O algoritmo de ordenação usado foi o Quicksort alterado para:
1. Receber como parâmetro uma variável definida como
TipoDicionario.
2. Mudar a condição de ordenação para não crescente.
3. Fazer com que a chave de ordenação seja o campo que representa
as frequências dos símbolos no arquivo texto.
• A função OrdenaPorFrequencia retorna o número de símbolos
presentes no vocabulário.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 129

Função para Ordenar o Vocabulário por Frequência


TipoIndice OrdenaPorFrequencia( TipoDicionario Vocabulario)
{ TipoIndice i ; TipoIndice n = 1;
TipoItem Item ;
Item = Vocabulario [ 1 ] ;
for ( i = 0; i <= M − 1; i ++)
{ i f ( strcmp(Vocabulario [ i ] .Chave, VAZIO) )
{ i f ( i ! = 1 ) { Vocabulario [n] = Vocabulario [ i ] ; n++; } }
}
i f ( strcmp(Item .Chave, VAZIO) ) Vocabulario [n] = Item ; else n−−;
QuickSort(Vocabulario, &n) ;
return n;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 130

Terceira Etapa da Compressão (1)


void TerceiraEtapa(FILE ∗ArqTxt , TipoAlfabeto Alfabeto , int ∗TipoIndice ,
TipoPalavra Palavra , char ∗Linha ,
TipoDicionario Vocabulario , TipoPesos p,
TipoVetoresBO VetoresBaseOffset ,
FILE ∗ArqComprimido, int MaxCompCod)
{ TipoApontador Pos; TipoChave Chave;
char ∗ PalavraTrim = NULL ;
int Codigo, c ;
do
{ ExtraiProximaPalavra(Palavra , TipoIndice , Linha , ArqTxt , Alfabeto ) ;
memcpy(Chave, Palavra , sizeof(TipoChave) ) ;
i f (∗Palavra ! = ’ \0 ’ )
{ Pos = Pesquisa(Chave, p, Vocabulario ) ;
Codigo = Codifica (VetoresBaseOffset , Vocabulario [Pos] .Ordem, &c,
MaxCompCod) ;
Escreve(ArqComprimido, &Codigo, &c ) ;
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 131

Terceira Etapa da Compressão (2)


do
{ ExtraiProximaPalavra(Palavra , TipoIndice , Linha , ArqTxt , Alfabeto ) ;
/∗ O primeiro espaco depois da palavra nao e codificado ∗/
PalavraTrim = Trim(Palavra ) ;
i f ( strcmp(PalavraTrim , " " ) && (∗PalavraTrim ) ! = (char)0)
{ memcpy(Chave, Palavra , sizeof(TipoChave) ) ;
Pos = Pesquisa(Chave, p, Vocabulario ) ;
Codigo = Codifica (VetoresBaseOffset ,
Vocabulario [Pos] .Ordem, &c , MaxCompCod) ;
Escreve(ArqComprimido, &Codigo, &c ) ;
}
i f ( strcmp(PalavraTrim , " " ) ) free (PalavraTrim ) ;
} while ( strcmp(Palavra , " " ) ) ;
}
} while (∗Palavra ! = ’ \0 ’ ) ;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 132

Procedimento Escreve

• O procedimento Escreve recebe o código e seu comprimento c.


• O código é representado por um inteiro, o que limita seu comprimento
a, no máximo, 4 bytes em um compilador que usa 4 bytes para
representar inteiros.
• Primeiramente, o procedimento Escreve extrai o primeiro byte e coloca
a marcação no oitavo bit fazendo uma operação or do byte com a
constante 128 (que em hexadecimal é 80.)
• Esse byte é então colocado na primeira posição do vetor Saida.
• No anel while, caso o comprimento c do código seja maior do que um,
os demais bytes são extraídos e armazenados em Saida[i], em que
2 ≤ i ≤ c.
• Por fim, o vetor de bytes Saida é gravado em disco no anel for.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 133

Implementação do Procedimento Escreve


void Escreve(FILE ∗ArqComprimido, int ∗Codigo, int ∗c)
{ unsigned char Saida[ MAXTAMVETORESDO + 1 ] ; int i = 1 , cTmp;
int LogBase2 = ( int )round( log (BASENUM ) / log (2.0));
int Mask = ( int ) pow(2 , LogBase2) −1; cTmp = ∗c ;
Saida[ i ] = ( (unsigned)(∗Codigo)) > > ((∗c − 1) ∗ LogBase2) ;
i f (LogBase2 == 7) Saida[ i ] = Saida[ i ] | 0x80;
i ++; (∗c)−−;
while (∗c > 0)
{ Saida[ i ] = ( ( (unsigned)(∗Codigo))>>((∗c−1)∗LogBase2)) & Mask;
i ++; (∗c)−−; }
for ( i = 1; i <= cTmp; i ++)
fwrite(&Saida[ i ] , sizeof(unsigned char) , 1 , ArqComprimido) ;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 134

Descrição do Código para Fazer a Descompressão

• O primeiro passo é recuperar o modelo usado na compressão. Para


isso, lê o alfabeto, o vetor Base, o vetor Offset e o vetor Vocabulario.

• Em seguida, inicia a decodificação, tomando o cuidado de adicionar


um espaço em branco entre dois símbolos que sejam palavras.

• O processo de decodificação termina quando o arquivo comprimido é


totalmente percorrido.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 135

Código para Fazer a Descompressão

void Descompressao(FILE ∗ArqComprimido, FILE ∗ArqTxt , FILE ∗ArqAlf )


{ TipoAlfabeto Alfabeto ; int Ind , MaxCompCod;
TipoVetorPalavra Vocabulario ; TipoVetoresBO VetoresBaseOffset ;
TipoPalavra PalavraAnt ;
DefineAlfabeto( Alfabeto , ArqAlf ) ; /∗ Le alfabeto definido em arq . ∗/
MaxCompCod = LeVetores(ArqComprimido, VetoresBaseOffset ) ;
LeVocabulario(ArqComprimido, Vocabulario ) ;
Ind = Decodifica (VetoresBaseOffset , ArqComprimido, MaxCompCod) ;
fputs (Vocabulario [ Ind ] , ArqTxt ) ;
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 136

Código para Fazer a Descompressão

while ( ! feof (ArqComprimido) )


{Ind = Decodifica (VetoresBaseOffset , ArqComprimido, MaxCompCod) ;
i f ( Ind > 0)
{ i f ( Alfabeto [ Vocabulario [ Ind][0]+127] && PalavraAnt [ 0 ] ! = ’ \n ’ )
putc( ’ ’ , ArqTxt ) ;
strcpy (PalavraAnt , Vocabulario [ Ind ] ) ;
fputs (Vocabulario [ Ind ] , ArqTxt ) ;
}
}
}

Obs: Na descompressão, o vocabuário é representado por um vetor de


símbolos do tipo TipoVetorPalavra.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 137

Procedimentos Auxiliares da Descompressão


int LeVetores(FILE ∗ArqComprimido, TipoBaseOffset ∗VetoresBaseOffset)
{ int MaxCompCod, i ;
MaxCompCod = LeNumInt(ArqComprimido) ;
for ( i = 1; i <= MaxCompCod; i ++)
{ VetoresBaseOffset [ i ] .Base = LeNumInt(ArqComprimido) ;
VetoresBaseOffset [ i ] . Offset = LeNumInt(ArqComprimido) ;
}
return MaxCompCod;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 138

Procedimentos Auxiliares da Descompressão


int LeVocabulario(FILE ∗ArqComprimido, TipoVetorPalavra Vocabulario)
{ int NumNodosFolhas, i ; TipoPalavra Palavra ;
char Ch;
NumNodosFolhas = LeNumInt(ArqComprimido) ;
for ( i = 1; i <= NumNodosFolhas; i ++)
{ ∗Palavra = ’ \0 ’ ;
do
{ fread(&Ch, sizeof(unsigned char) , 1 , ArqComprimido) ;
i f (Ch ! = (char)0) /∗Palavras estao separadas pelo caratere 0 ∗/
sprintf (Palavra + strlen (Palavra ) , "%c" , Ch) ;
} while (Ch ! = (char)0);
strcpy (Vocabulario [ i ] , Palavra ) ;
}
return NumNodosFolhas;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 139

Resultados Experimentais

• Mostram que não existe grande degradação na razão de compressão


na utilização de bytes em vez de bits na codificação das palavras de
um vocabulário.

• Por outro lado, tanto a descompressão quanto a pesquisa são muito


mais rápidas com uma codificação de Huffman usando bytes do que
uma codificação de Huffman usando bits, isso porque deslocamentos
de bits e operações usando máscaras não são necessárias.

• Os experimentos foram realizados em uma máquina PC Pentium de


200 MHz com 128 megabytes de RAM.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.4 140

Comparação das Técnicas de Compressão: Arquivo WSJ


Dados sobre a coleção usada nos experimentos:

Texto Vocabulário Vocab./Texto


Tam (bytes) #Palavras Tam (bytes) #Palavras Tamanho #Palavras
262.757.554 42.710.250 1.549.131 208.005 0,59% 0,48%

Razão de Tempo (min) de Tempo (min) de


Método
Compressão Compressão Descompressão
Huffman binário 27,13 8,77 3,08
Huffman pleno 30,60 8,67 1,95
Huffman com marcação 33,70 8,90 2,02
Gzip 37,53 25,43 2,68
Compress 42,94 7,60 6,78
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.5 141

Pesquisa em Texto Comprimido

• Uma das propriedades mais atraentes do método de Huffman usando


bytes em vez de bits é que o texto comprimido pode ser pesquisado
exatamente como qualquer texto não comprimido.

• Basta comprimir o padrão e realizar uma pesquisa diretamente no


arquivo comprimido.

• Isso é possível porque o código de Huffman usa bytes em vez de bits;


de outra maneira, o método seria complicado ou mesmo impossível de
ser implementado.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.5 142

Casamento Exato: Algoritmo


• Buscar a palavra no vocabulário, podendo usar busca binária nesta
fase:
– Se a palavra for localizada no vocabulário, então o código de
Huffman com marcação é obtido.
– Senão a palavra não existe no texto comprimido.
• A seguir, o código é pesquisado no texto comprimido usando qualquer
algoritmo para casamento exato de padrão.
• Para pesquisar um padrão contendo mais de uma palavra, o primeiro
passo é verificar a existência de cada palavra do padrão no
vocabulário e obter o seu código:
– Se qualquer das palavras do padrão não existir no vocabulário,
então o padrão não existirá no texto comprimido.
– Senão basta coletar todos os códigos obtidos e realizar a pesquisa
no texto comprimido.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.5 143

Pesquisa no Arquivo Comprimido


void Busca(FILE ∗ArqComprimido, FILE ∗ArqAlf )
{ TipoAlfabeto Alfabeto ;
int Ind , Codigo, MaxCompCod;
TipoVetorPalavra Vocabulario =
(TipoVetorPalavra) calloc (M+1,sizeof(TipoPalavra ) ) ;
TipoVetoresBO VetoresBaseOffset = (TipoVetoresBO)
calloc (MAXTAMVETORESDO+1,sizeof(TipoBaseOffset ) ) ;
TipoPalavra p ; int c , Ord, NumNodosFolhas;
TipoTexto T; TipoPadrao Padrao;
memset(T, 0 , sizeof T) ; memset(Padrao, 0 , sizeof Padrao) ;
int n = 1;
DefineAlfabeto( Alfabeto , ArqAlf ) ; /∗Le alfabeto def . em arquivo∗/
MaxCompCod = LeVetores(ArqComprimido, VetoresBaseOffset ) ;
NumNodosFolhas = LeVocabulario(ArqComprimido, Vocabulario ) ;
while ( fread(&T[n] , sizeof(char) , 1 , ArqComprimido) ) n++;
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.5 144

Pesquisa no Arquivo Comprimido


while (1)
{ p r i n t f ( "Padrao ( digite s para terminar ) : " ) ;
fgets (p, MAXALFABETO + 1 , stdin ) ;
p[ strlen (p) − 1] = ’ \0 ’ ;
i f ( strcmp(p, "s" ) == 0) break;
for ( Ind = 1; Ind <= NumNodosFolhas; Ind++)
i f ( ! strcmp(Vocabulario [ Ind ] , p ) ) { Ord = Ind ; break ; }
i f ( Ind == NumNodosFolhas+1)
{ p r i n t f ( "Padrao:%s nao encontrado\n" , p ) ; continue ; }
Codigo = Codifica (VetoresBaseOffset , Ord, &c , MaxCompCod) ;
Atribui (Padrao, Codigo, c ) ;
BMH(T, n, Padrao, c ) ;
}
free ( Vocabulario ) ; free ( VetoresBaseOffset ) ;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.5 145

Procedimento para Atribuir o Código ao Padrão


void Atribui (TipoPadrao P, int Codigo, int c)
{ int i = 1;
P[ i ] = (char) ( (Codigo > > ((c − 1) ∗ 7)) | 0x80) ;
i ++; c−−;
while ( c > 0)
{ P[ i ] = (char) ( (Codigo > > ((c − 1) ∗ 7)) & 127);
i ++; c−−;
}
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.5 146

Teste dos Algoritmos de Compressão, Descompressão e


Busca Exata em Texto Comprimido (1)
/∗ ∗ Entram aqui os tipos do Programa 5.28 ∗ ∗/
/∗ ∗ Entram aqui os tipos do Programa do Slide 4 ∗∗/

typedef short TipoAlfabeto [ MAXALFABETO + 1];


typedef struct TipoBaseOffset {
int Base, Offset ;
} TipoBaseOffset ;
typedef TipoBaseOffset∗ TipoVetoresBO;
typedef char TipoPalavra[256];
typedef TipoPalavra∗ TipoVetorPalavra ;
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.5 147

Teste dos Algoritmos de Compressão, Descompressão e


Busca Exata em Texto Comprimido (2)
/∗ ∗ Entra aqui o procedimento GeraPeso do Programa 5.22 ∗ ∗/
/∗ ∗ Entra aqui a função de transformação do Programa 5.23 ∗ ∗/
/∗ ∗ Entram aqui os operadores apresentados no Programa 5.29 ∗ ∗/
/∗ ∗ Entram aqui os procedimentos Particao e Quicksort dos Programas 4.6 e 4.7 ∗ ∗/

int main( int argc , char ∗argv [ ] )


{ FILE ∗ArqTxt = NULL , ∗ ArqAlf = NULL ; FILE ∗ArqComprimido = NULL ;
TipoPalavra NomeArqTxt, NomeArqAlf, NomeArqComp, Opcao;
memset(Opcao, 0 , sizeof(Opcao) ) ;
while(Opcao[ 0 ] ! = ’ t ’ )
{ p r i n t f ( "∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗\n" ) ;
printf ( "∗ Opcoes ∗\n" ) ;
p r i n t f ( " ∗ (c ) Compressao ∗\n" ) ;
p r i n t f ( " ∗ (d) Descompressao ∗\n" ) ;
p r i n t f ( " ∗ (p) Pesquisa no texto comprimido ∗\n" ) ;
p r i n t f ( " ∗ ( t ) Termina ∗\n" ) ;
p r i n t f ( "∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗\n" ) ;
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.5 148

Teste dos Algoritmos de Compressão, Descompressão e


Busca Exata em Texto Comprimido (3)
p r i n t f ( " ∗ Opcao: " ) ;
fgets (Opcao, MAXALFABETO + 1 , stdin ) ;
strcpy (NomeArqAlf, " alfabeto . t x t " ) ;
ArqAlf = fopen(NomeArqAlf, " r " ) ;
i f (Opcao[0] == ’c ’ )
{ p r i n t f ( "Arquivo texto a ser comprimido: " ) ;
fgets (NomeArqTxt, MAXALFABETO + 1 , stdin ) ;
NomeArqTxt[ strlen (NomeArqTxt)−1] = ’ \0 ’ ;
p r i n t f ( "Arquivo comprimido a ser gerado: " ) ;
fgets (NomeArqComp, MAXALFABETO + 1 , stdin ) ;
NomeArqComp[ strlen (NomeArqComp)−1] = ’ \0 ’ ;
ArqTxt = fopen(NomeArqTxt, " r " ) ;
ArqComprimido = fopen(NomeArqComp, "w+b" ) ;
Compressao(ArqTxt , ArqAlf , ArqComprimido) ;
fclose (ArqTxt ) ;
ArqTxt = NULL ;
fclose (ArqComprimido) ;
ArqComprimido = NULL ;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.5 149

Teste dos Algoritmos de Compressão, Descompressão e


Busca Exata em Texto Comprimido (4)
else i f (Opcao[0] == ’d ’ )
{ p r i n t f ( "Arquivo comprimido a ser descomprimido: " ) ;
fgets (NomeArqComp, MAXALFABETO + 1 , stdin ) ;
NomeArqComp[ strlen (NomeArqComp)−1] = ’ \0 ’ ;
p r i n t f ( "Arquivo texto a ser gerado: " ) ;
fgets (NomeArqTxt, MAXALFABETO + 1 , stdin ) ;
NomeArqTxt[ strlen (NomeArqTxt)−1] = ’ \0 ’ ;
ArqTxt = fopen(NomeArqTxt, "w" ) ;
ArqComprimido = fopen(NomeArqComp, " r+b" ) ;
Descompressao(ArqComprimido, ArqTxt , ArqAlf ) ;
fclose (ArqTxt ) ;
ArqTxt = NULL ;
fclose (ArqComprimido) ;
ArqComprimido = NULL ;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.5 150

Teste dos Algoritmos de Compressão, Descompressão e


Busca Exata em Texto Comprimido (5)
else i f (Opcao[0] == ’p ’ )
{ p r i n t f ( "Arquivo comprimido para ser pesquisado: " ) ;
fgets (NomeArqComp, MAXALFABETO + 1 , stdin ) ;
NomeArqComp[ strlen (NomeArqComp)−1] = ’ \0 ’ ;
strcpy (NomeArqComp, NomeArqComp) ;
ArqComprimido = fopen(NomeArqComp, " r+b" ) ;
Busca(ArqComprimido, ArqAlf ) ;
fclose (ArqComprimido) ;
ArqComprimido = NULL ;
}
}
return 0;
}
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.5 151

Casamento Aproximado: Algoritmo


• Pesquisar o padrão no vocabulário. Nesse caso, podemos ter:
– Casamento exato, o qual pode ser uma pesquisa binária no
vocabulário, e uma vez que a palavra tenha sido encontrada a folha
correspondente na árvore de Huffman é marcada.
– Casamento aproximado, o qual pode ser realizado por meio de
pesquisa sequencial no vocabulário, usando o algoritmo Shift-And.
Nesse caso, várias palavras do vocabulário podem ser encontradas
e a folha correspondente a cada uma na árvore de Huffman é
marcada.
• A seguir, o arquivo comprimido é lido byte a byte, ao mesmo tempo
que a árvore de decodificação de Huffman é percorrida.
• Ao atingir uma folha da árvore, se ela estiver marcada, então existe
casamento com a palavra do padrão.
• Seja uma folha marcada ou não, o caminhamento na árvore volta à
raiz ao mesmo tempo que a leitura do texto comprimido continua.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.5 152

Esquema Geral de Pesquisa para a Palavra “uma”


Permitindo 1 Erro

ama

puma

uma
umas
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.5 153

Casamento Aproximado Usando uma Frase como Padrão

• Frase: sequência de padrões (palavras), em que cada padrão pode


ser desde uma palavra simples até uma expressão regular complexa
permitindo erros.

Pré-Processamento:

• Se uma frase tem j palavras, então uma máscara de j bits é colocada


junto a cada palavra do vocabulário (folha da árvore de Huffman).

• Para uma palavra x da frase, o i-ésimo bit da máscara é feito igual a 1


se x é a i-ésima palavra da frase.

• Assim, cada palavra i da frase é pesquisada no vocabulário e a


i-ésima posição da máscara é marcada quando a palavra é
encontrada no vocabulário.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.5 154

Casamento Aproximado Usando uma Frase como Padrão


• O estado da pesquisa é controlado por um autômato finito
não-determinista de j + 1 estados.
• O autômato permite mover do estado i para o estado i + 1 sempre que
a i-ésima palavra da frase é reconhecida.
• O estado zero está sempre ativo e uma ocorrência é relatada quando
o estado j é ativado.
• Os bytes do texto comprimido são lidos e a árvore de Huffman é
percorrida como antes.
• Cada vez que uma folha da árvore é atingida, sua máscara de bits é
enviada para o autômato.
• Um estado ativo i − 1 irá ativar o estado i apenas se o i-ésimo bit da
máscara estiver ativo.
• O autômato realiza uma transição para cada palavra do texto.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.5 155

Esquema Geral de Pesquisa para Frase “uma ro* rosa”

rosa 011
roupa 010 XXX

azul 000

1XX X1X XX1


uma 100
rosas 011

• O autômato pode ser implementado eficientemente por meio do


algoritmo Shift-And
• Separadores podem ser ignorados na pesquisa de frases.
• Artigos, preposições etc., também podem ser ignorados se
conveniente, bastando ignorar as folhas correspondentes na árvore de
Huffman quando a pesquisa chega a elas.
• Essas possibilidades são raras de encontrar em sistemas de pesquisa
on-line.
Projeto de Algoritmos – Cap.8 Processamento de Cadeias de Caracteres – Seção 8.2.5 156

Tempos de Pesquisa (em segundos) para o Arquivo WSJ,


com Intervalo de Confiança de 99%

Algoritmo k=0 k=1 k=2 k=3


Agrep 23,8 ± 0,38 117,9 ± 0,14 146,1 ± 0,13 174,6 ± 0,16
Pesquisa direta 14,1 ± 0,18 15,0 ± 0,33 17,0 ± 0,71 22,7 ± 2,23
Pesquisa com autômato 22,1 ± 0,09 23,1 ± 0,14 24,7 ± 0,21 25,0 ± 0,49
Problemas N P-Completo e
Algoritmos Aproximados ∗

Última alteração: 28 de Setembro de 2010


∗ Transparências elaboradas por Charles Ornelas Almeida, Israel Guerra e Nivio Ziviani
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados 1

Conteúdo do Capítulo

9.1 Problemas N P-Completo


9.1.1 Algoritmos Não Deterministas
9.1.2 As Classes N P-Completo e N P-Difícil

9.2 Heurísticas e Algoritmos Aproximados


9.2.1 Algoritmos Exponenciais Usando Tentativa e Erro
9.2.2 Heurísticas para Problemas N P-Completo
9.2.3 Algoritmos Aproximados para Problemas N P-Completo
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados 2

Introdução
• Problemas intratáveis ou difíceis são comuns na natureza e nas áreas do
conhecimento.
• Problemas “fáceis”: resolvidos por algoritmos polinomiais.
• Problemas “difíceis”: somente possuem algoritmos exponenciais para
resolvê-los.
• A complexidade de tempo da maioria dos problemas é polinomial ou
exponencial.
• Polinomial: função de complexidade é O(p(n)), onde p(n) é um polinômio.
– Ex.: algoritmos com pesquisa binária (O(log n)), pesquisa sequencial
(O(n)), ordenação por inserção (O(n2 )), e multiplicação de matrizes
(O(n3 )).
• Exponencial: função de complexidade é O(cn ), c > 1.
– Ex.: problema do caixeiro viajante (PCV) (O(n!)).
– Mesmo problemas de tamanho pequeno a moderado não podem ser
resolvidos por algoritmos não-polinomiais.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1 3

Problemas N P-Completo

• A teoria de complexidade a ser apresentada não mostra como obter


algoritmos polinomiais para problemas que demandam algoritmos
exponenciais, nem afirma que não existem.

• É possível mostrar que os problemas para os quais não há algoritmo


polinomial conhecido são computacionalmente relacionados.

• Formam a classe conhecida como N P.

• Propriedade: um problema da classe N P poderá ser resolvido em


tempo polinomial se e somente se todos os outros problemas em N P
também puderem.

• Este fato é um indício forte de que dificilmente alguém será capaz de


encontrar um algoritmo eficiente para um problema da classe N P.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1 4

Classe N P - Problemas “Sim/Não”


• Para o estudo teórico da complexidade de algoritmos considera-se
problemas cujo resultado da computação seja “sim” ou “não”.
• Versão do Problema do Caixeiro Viajante (PCV) cujo resultado é do
tipo “sim/não”:
– Dados: uma constante k, um conjunto de cidades
C = {c1 , c2 , · · · , cn } e uma distância d(ci , cj ) para cada par de
cidades ci , cj ∈ C.
– Questão: Existe um “roteiro” para todas as cidades em C cujo
comprimento total seja menor ou igual a k?
• Característica da classe N P: problemas “sim/não” para os quais uma
dada solução pode ser verificada facilmente.
• A solução pode ser muito difícil ou impossível de ser obtida, mas uma
vez conhecida ela pode ser verificada em tempo polinomial.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1 5

Caminho em um Grafo
• Considere um grafo com peso nas arestas, dois vértices i, j e um
inteiro k > 0.
5 12 j

2 7 9 10 13

11 5 1
3 2 8 3

i 7 3

• Fácil: Existe um caminho de i até j com peso ≤ k?.


– Há um algoritmo eficiente com complexidade de tempo O(A log V ),
sendo A o número de arestas e V o número de vértices (algoritmo
de Dijkstra).
• Difícil: Existe um caminho de i até j com peso ≥ k?
– Não existe algoritmo eficiente. É equivalente ao PCV em termos de
complexidade.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1 6

Coloração de um Grafo
• Em um grafo G = (V, A), mapear C : V ← S, sendo S um conjunto
finito de cores tal que se vw ∈ A então c(v) 6= c(w) (vértices adjacentes
possuem cores distintas).
• O número cromático X(G) de G é o menor número de cores
necessário para colorir G, isto é, o menor k para o qual existe uma
coloração C para G e |C(V )| = k.
• O problema é produzir uma coloração ótima, que é a que usa apenas
X(G) cores.
• Formulação do tipo “sim/não”: dados G e um inteiro positivo k, existe
uma coloração de G usando k cores?
– Fácil: k = 2.
– Difícil: k > 2.
• Aplicação: modelar problemas de agrupamento (clustering) e de
horário (scheduling).
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1 7

Coloração de um Grafo - Otimização de Compiladores

• Escalonar o uso de um número finito de registradores (idealmente com


o número mínimo).

• No trecho de programa a ser otimizado, cada variável tem intervalos


de tempo em que seu valor tem de permanecer inalterado, como
depois de inicializada e antes do uso final.

• Variáveis com interseção nos tempos de vida útil não podem ocupar o
mesmo registrador.

• Modelagem por grafo: vértices representam variáveis e cada aresta


liga duas variáveis que possuem interseção nos tempos de vida.

• Coloração dos vértices: atibui cada variável a um agrupamento (ou


classe). Duas variáveis com a mesma cor não colidem, podendo
assim ser atribuídas ao mesmo registrador.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1 8

Coloração de um Grafo - Otimização de Compiladores

• Evidentemente, não existe conflito se cada vértice for colorido com


uma cor distinta.

• O objetivo porém é encontrar uma coloração usando o mínimo de


cores (computadores têm um número limitado de registradores).

• Número cromático: menor número de cores suficientes para colorir


um grafo.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1 9

Coloração de um Grafo - Problema de Horário

• Suponha que os exames finais de um curso tenham de ser realizados


em uma única semana.

• Disciplinas com alunos de cursos diferentes devem ter seus exames


marcados em horários diferentes.

• Dadas uma lista de todos os cursos e outra lista de todas as


disciplinas cujos exames não podem ser marcados no mesmo horário,
o problema em questão pode ser modelado como um problema de
coloração de grafos.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1 10

Ciclo de Hamilton
• Ciclo de Hamilton: ciclo simples (passa por todos os vértices uma
única vez).
• Caminho de Hamilton: caminho simples (passa por todos os
vértices uma única vez).
• Exemplo de ciclo de Hamilton: 0 1 4 2 3 0. Exemplo de caminho de
Hamilton: 0 1 4 2 3.
0 1 • Existe um ciclo de Hamilton no grafo G?

4
– Fácil: Grafos com grau máximo = 2 (vér-
tices com no máximo duas arestas inci-
3 2 dentes).
– Difícil: Grafos com grau > 2.
• É um caso especial do PCV. Pares de vértices com uma aresta entre
eles tem distância 1 e pares de vértices sem aresta entre eles têm
distância infinita.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1 11

Cobertura de Arestas
• Uma cobertura de arestas de um grafo G = (V, A) é um subconjunto
A′ ⊂ A de k arestas tal que todo v ∈ V é parte de pelo menos uma
aresta de A′ .
• O conjunto resposta para k = 4 é A′ = {(0, 3), (2, 3), (4, 6), (1, 5)}.
0 1 • Uma cobertura de vértices é um sub-
conjunto V ′ ⊂ V tal que se (u, v) ∈ A
3 4 5 então u ∈ V ′ ou v ∈ V ′ , isto é, cada
aresta do grafo é incidente em um dos
vértices de V ′ .
2 6

• Na figura, o conjunto resposta é V ′ = {3, 4, 5}, para k = 3.


• Dados um grafo e um inteiro k > 0
– Fácil: há uma cobertura de arestas ≤ k?.
– Difícil: há uma cobertura de vértices ≤ k?
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.1 12

Algoritmos Não-Deterministas

• Algoritmos deterministas: o resultado de cada operação é definido


de forma única.
• Em um arcabouço teórico, é possível remover essa restrição.
• Apesar de parecer irreal, este é um conceito importante e geralmente
utilizado para definir a classe N P.
• Neste caso, os algoritmos podem conter operações cujo resultado não
é definido de forma única.
• Algorimo não-determinista: capaz de escolher uma dentre as várias
alternativas possíveis a cada passo.
• Algoritmos não-deterministas contêm operações cujo resultado não é
unicamente definido, ainda que limitado a um conjunto especificado de
possibilidades.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.1 13

Função escolhe(C)

• Algoritmos não-deterministas utilizam uma função escolhe(C), que


escolhe um dos elementos do conjunto C de forma arbitrária.
• O comando de atribuição X ← escolhe (1:n) pode resultar na
atribuição a X de qualquer dos inteiros no intervalo [1, n].
• A complexidade de tempo para cada chamada da função escolhe é
O(1).
• Neste caso, não existe nenhuma regra especificando como a escolha
é realizada.
• Se um conjunto de possibilidades levam a uma resposta, este conjunto
é escolhido sempre e o algoritmo terminará com sucesso.
• Por outro lado, um algoritmo não-determinista termina sem sucesso se
e somente se não há um conjunto de escolhas que indica sucesso.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.1 14

Comandos sucesso e insucesso

• Algoritmos não-deterministas utilizam também dois comandos, a


saber:
– insucesso: indica término sem sucesso.
– sucesso: indica término com sucesso.

• Os comandos insucesso e sucesso são usados para definir uma


execução do algoritmo.

• Esses comandos são equivalentes a um comando de parada de um


algoritmo determinista.

• Os comandos insucesso e sucesso também têm complexidade de


tempo O(1).
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.1 15

Máquina Não-Determinista

• Uma máquina capaz de executar a função escolhe admite a


capacidade de computação não-determinista.

• Uma máquina não-determinista é capaz de produzir cópias de si


mesma quando diante de duas ou mais alternativas, e continuar a
computação independentemente para cada alternativa.

• A máquina não-determinista que acabamos de definir não existe na


prática, mas ainda assim fornece fortes evidências de que certos
problemas não podem ser resolvidos por algoritmos deterministas em
tempo polinomial, conforme mostrado na definição da classe
N P-completo à frente.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.1 16

Pesquisa Não-Determinista

• Pesquisar o elemento x em um conjunto de elementos A[1 : n], n ≥ 1.

void PesquisaND(A, 1 , n)
{ j ← escolhe(A, 1 , n)
i f (A[ j ] == x ) sucesso ; else insucesso ;
}

• Determina um índice j tal que A[j] = x para um término com sucesso


ou então insucesso quando x não está presente em A.

• O algoritmo tem complexidade não-determinista O(1).

• Para um algoritmo determinista a complexidade é Ω(n).


Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.1 17

Ordenação Não-Determinista
• Ordenar um conjunto A[1 : n] contendo n inteiros positivos, n ≥ 1.

void OrdenaND(A, 1 , n) ; – Um vetor auxiliar B[1:n] é


{ for ( i = 1; i <= n ; i ++) B[ i ] : = 0 ; utilizado. Ao final, B contém
for ( i = 1; i <= n ; i ++) o conjunto em ordem cres-
{ j ← escolhe(A, 1 , n) ; cente.
i f (B[ j ] == 0) B[ j ] : = A[ i ] ;
– A posição correta em B de
else insucesso ;
cada inteiro de A é obtida de
}
forma não-determinista pela
}
função escolhe.

• Em seguida, o comando de decisão verifica se a posição B[j] ainda


não foi utilizada.
• A complexidade é O(n). (Para um algoritmo determinista a
complexidade é Ω(n log n))
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.1 18

Problema da Satisfabilidade
• Considere um conjunto de variáveis booleanas x1 , x2 , · · · , xn , que
podem assumir valores lógicos verdadeiro ou falso.
• A negação de xi é representada por xi .
• Expressão booleana: variáveis booleanas e operações ou (∨) e e (∧).
(também chamadas respectivamente de adição e multiplicação).
• Uma expressão booleana E contendo um produto de adições de
variáveis booleanas é dita estar na forma normal conjuntiva.
• Dada E na forma normal conjuntiva, com variáveis xi , 1 ≤ i ≤ n, existe
uma atribuição de valores verdadeiro ou falso às variáveis que torne E
verdadeira (“satisfaça”)?
• E1 = (x1 ∨ x2 ) ∧ (x1 ∨ x3 ∨ x2 ) ∧ (x3 ) é satisfatível (x1 = F , x2 = V ,
x3 = V ).
• A expressão E2 = x1 ∧ x1 não é satisfatível.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.1 19

Problema da Satisfabilidade

• O algoritmo AvalND(E,n) verifica se uma expressão E na forma normal


conjuntiva, com variáveis xi , 1 ≤ i ≤ n, é satisfatível.

void AvalND(E, n) ;
{ for ( i = 1; i <= n ; i ++)
{ xi ← escolhe ( true , false ) ;
i f ( E(x1 , x2 , · · · , xn ) == true ) sucesso ; else insucesso ;
}
}
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.1 20

Problema da Satisfabilidade

• O algoritmo obtém uma das 2n atribuições possíveis de forma


não-determinista em O(n).

• Melhor algoritmo determinista: O(2n ).

• Aplicação: definição de circuitos elétricos combinatórios que


produzam valores lógicos como saída e sejam constituídos de portas
lógicas e, ou e não.

• Neste caso, o mapeamento é direto, pois o circuito pode ser descrito


por uma expressão lógica na forma normal conjuntiva.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 21

Caracterização das Classes P e N P

• P: conjunto de todos os problemas que podem ser resolvidos por


algoritmos deterministas em tempo polinomial.

• N P: conjunto de todos os problemas que podem ser resolvidos por


algoritmos não-deterministas em tempo polinomial.

• Para mostrar que um determinado problema está em N P, basta


apresentar um algoritmo não-determinista que execute em tempo
polinomial para resolver o problema.

• Outra maneira é encontrar um algoritmo determinista polinomial para


verificar que uma dada solução é válida.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 22

Existe Diferença entre P e N P?


• P ⊆ N P, pois algoritmos deterministas são um caso especial dos
não-deterministas.
• A questão é se P = N P ou P =
6 N P.
• Esse é o problema não resolvido mais famoso que existe na área de ciência
da computação.
• Se existem algoritmos polinomiais deterministas para todos os problemas em
N P, então P = N P.
• Por outro lado, a prova de que P =
6 N P parece exigir técnicas ainda
desconhecidas.
• Descrição tentativa do mundo N P, em que a classe P está contida na classe
N P.
– Acredita-se que N P ≫ P, pois para muitos pro-
NP
blemas em N P, não existem algoritmos polino-
P miais conhecidos, nem um limite inferior não-
polinomial provado.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 23

N P ⊃ P ou N P = P? - Conseqüências

• Muitos problemas práticos em N P podem ou não pertencer a P (não


conhecemos nenhum algoritmo determinista eficiente para eles).

• Se conseguirmos provar que um problema não pertence a P, então


não precisamos procurar por uma solução eficiente para ele.

• Como não existe tal prova, sempre há esperança de que alguém


descubra um algoritmo eficiente.

• Quase ninguém acredita que N P = P.

• Existe um esforço considerável para provar o contrário, mas a questão


continua em aberto!
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 24

Transformação Polinomial
• Sejam Π1 e Π2 dois problemas “sim/não”.
• Suponha que um algoritmo A2 resolva Π2 .
• Se for possível transformar Π1 em Π2 e a solução de Π2 em solução de
Π1 , então A2 pode ser utilizado para resolver Π1 .
• Se pudermos realizar as transformações nos dois sentidos em tempo
polinomial, então Π1 é polinomialmente transformável em Π2 .
Dados Dados Solução Solução
de 1 de 2 para 2 para 1

Transformação Algoritmo A 2
Transformação
Polinomial Polinomial

• Esse conceito é importante para definir a classe N P-completo.


• Para mostrar um exemplo de transformação polinomial, definiremos
clique de um grafo e conjunto independente de vértices de um grafo.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 25

Conjunto Independente de Vértices de um Grafo

• O conjunto independente de vértices de um grafo G = (V, A) é


constituído do subconjunto V ′ ⊆ V , tal que v, w ∈ V ′ ⇒ (v, w) ∈
/ A.

• Todo par de vértices de V ′ é não adjacente (V ′ é um subgrafo


totalmente desconectado).

• Exemplo de cardinalidade 4: V ′ = {0, 2, 1, 6}.

0 1

3 4 5

2 6
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 26

Conjunto Independente de Vértices - Aplicação

• Em problemas de dispersão é necessário encontrar grandes conjuntos


independentes de vértices. Procura-se um conjunto de pontos
mutuamente separados.
• Exemplo: identificar localizações para instalação de franquias.
• Duas localizações não podem estar perto o suficiente para
competirem entre si.
• Solução: construir um grafo em que possíveis localizações são
representadas por vértices, e arestas são criadas entre duas
localizações que estão próximas o suficiente para interferir.
• O maior conjunto independente fornece o maior número de franquias
que podem ser concedidas sem prejudicar as vendas.
• Em geral, cunjuntos independentes evitam conflitos entre elementos.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 27

Clique de um grafo

• Clique de um grafo G = (V, A) é constituído do subconjunto V ′ ⊆ V ,


tal que v, w ∈ V ′ ⇒ (v, w) ∈ A.

• Todo par de vértices de V ′ é adjacente (V ′ é um subgrafo completo).

• Exemplo de cardinalidade 3: V ′ = {3, 1, 4}.

0 1

3 4 5

2 6
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 28

Clique de um grafo - Aplicação


• O problema de identificar agrupamentos de objetos relacionados
freqüentemente se reduz a encontrar grandes cliques em grafos.
• Exemplo: empresa de fabricação de peças por meio de injeção
plástica que fornece para diversas outras empresas montadoras.
• Para reduzir o custo relativo ao tempo de preparação das máquinas
injetoras, pode-se aumentar o tamanho dos lotes produzidos para
cada peça encomendada.
• É preciso identificar os clientes que adquirem os mesmos produtos,
para negociar prazos de entrega comuns e assim aumentar o tamanho
dos lotes produzidos.
• Solução: construir um grafo com cada vértice representando um
cliente e ligar com uma aresta os que adquirem os mesmos produtos.
• Um clique no grafo representa o conjunto de clientes que adquirem os
mesmos produtos.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 29

Transformação Polinomial

• Considere Π1 o problema clique e Π2 o problema conjunto


independente de vértices.

• A instância I de clique consiste de um grafo G = (V, A) e um inteiro


k > 0.

• A instância f (I) de conjunto independente pode ser obtida


considerando-se o grafo complementar G de G e o mesmo inteiro k.

• f (I) é uma transformação polinomial:


1. G pode ser obtido a partir de G em tempo polinomial.
2. G possui clique de tamanho ≥ k se e somente se G possui
conjunto independente de vértices de tamanho ≥ k.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 30

Transformação Polinomial

• Se existe um algoritmo que resolve o conjunto independente em


tempo polinomial, ele pode ser utilizado para resolver clique também
em tempo polinomial.

• Diz-se que clique ∝ conjunto independente.

• Denota-se Π1 ∝ Π2 para indicar que Π1 é polinomialmente


transformável em Π2 .

• A relação ∝ é transitiva (Π1 ∝ Π2 e Π2 ∝ Π3 ⇒ Π1 ∝ Π3 ).


Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 31

Problemas N P-Completo e N P-Difícil

• Dois problemas Π1 e Π2 são polinomialmente equivalentes se e


somente se Π1 ∝ Π2 e Π2 ∝ Π1 .

• Exemplo: problema da satisfabilidade. Se SAT ∝ Π1 e Π1 ∝ Π2 ,


então SAT ∝ Π2 .

• Um problema Π é N P-difícil se e somente se SAT ∝ Π


(satisfabilidade é redutível a Π).

• Um problema de decisão Π é denominado N P-completo quando:


1. Π ∈ N P ;.
2. Todo problema de decisão Π′ ∈ N P-completo satisfaz Π′ ∝ Π.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 32

Problemas N P-Completo e N P-Difícil

• Um problema de decisão Π que seja N P-difícil pode ser mostrado ser


N P-completo exibindo um algoritmo não-determinista polinomial para
Π.

• Apenas problemas de decisão (“sim/não”) podem ser N P-completo.

• Problemas de otimização podem ser N P-difícil, mas geralmente, se


Π1 é um problema de decisão e Π2 um problema de otimização, é bem
possível que Π1 ∝ Π2 .

• A dificuldade de um problema N P-difícil não é menor do que a


dificuldade de um problema N P-completo.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 33

Exemplo - Problema da Parada

• É um exemplo de problema N P-difícil que não é N P-completo.


• Consiste em determinar, para um algoritmo determinista qualquer A
com entrada de dados E, se o algoritmo A termina (ou entra em um
loop infinito).
• É um problema indecidível. Não há algoritmo de qualquer
complexidade para resolvê-lo.
• Mostrando que SAT ∝ problema da parada:
– Considere o algoritmo A cuja entrada é uma expressão booleana
na forma normal conjuntiva com n variáveis.
– Basta tentar 2n possibilidades e verificar se E é satisfatível.
– Se for, A pára; senão, entra em loop.
– Logo, o problema da parada é N P-difícil, mas não é N P-completo.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 34

Teorema de Cook
• Existe algum problema em N P tal que se ele for mostrado estar em P,
implicaria P = N P?
• Teorema de Cook: Satisfabilidade (SAT) está em P se e somente se
P = N P.
• Ou seja, se existisse um algoritmo polinomial determinista para
satisfabilidade, então todos os problemas em N P poderiam ser resolvidos
em tempo polinomial.
• A prova considera os dois sentidos:
1. SAT está em N P (basta apresentar um algoritmo não-determinista que
execute em tempo polinomial). Logo, se P = N P, então SAT está em P.
2. Se SAT está em P, então P = N P. A prova descreve como obter de
qualquer algoritmo polinomial não determinista de decisão A, com entrada
E, uma fórmula Q(A, E) de modo que Q é satisfatível se e somente se A
termina com sucesso para E. O comprimento e tempo para construir Q é
O(p3 (n) log(n)), onde n é o tamanho de E e p(n) é a complexidade de A.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 35

Prova do Teorema de Cook


• A prova, bastante longa, mostra como construir Q a partir de A e E.
• A expressão booleana Q é longa, mas pode ser construída em tempo
polinomial no tamanho de E.
• Prova usa definição matemática da Máquina de Turing não-determinista
(MTND), capaz de resolver qualquer problema em N P.
– incluindo uma descrição da máquina e de como instruções são
executadas em termos de fórmulas booleanas.
• Estabelece uma correspondência entre todo problema em N P (expresso por
um programa na MTnd) e alguma instância de SAT.
• Uma instância de SAT corresponde à tradução do programa em uma fórmula
booleana.
• A solução de SAT corresponde à simulação da máquina executando o
programa em cima da fórmula obtida, o que produz uma solução para uma
instância do problema inicial dado.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 36

Prova de que um Problema é N P-Completo

• São necessários os seguintes passos:


1. Mostre que o problema está em N P.
2. Mostre que um problema N P-completo conhecido pode ser
polinomialmente transformado para ele.

• É possível porque Cook apresentou uma prova direta de que SAT é


N P-completo, além do fato de a redução polinomial ser transitiva
(SAT ∝ Π1 & Π1 ∝ Π2 ⇒
SAT ∝ Π2 ).

• Para ilustrar como um problema Π pode ser provado ser N P-completo,


basta considerar um problema já provado ser N P-completo e
apresentar uma redução polinomial desse problema para Π.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 37

PCV é N P-completo - Parte 1 da Prova

• Mostrar que o Problema do Caixeiro Viajante (PCV) está em N P.

• Prova a partir do problema ciclo de Hamilton, um dos primeiros que


se provou ser N P-completo.

void PCVND ; • Isso pode ser feito:


{ i = 1; – apresentando (como abaixo) um al-
for ( i = 1; i <= v ; i ++) goritmo não-determinista polinomial
{ j : = escolhe( i , lista−adj ( i ) ) ; para o PCV ou
antecessor [ j ] : = i ;
– mostrando que, a partir de uma
}
dada solução para o PCV, esta
}
pode ser verificada em tempo poli-
nomial.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 38

PCV é N P-completo - Parte 2 da Prova


• Apresentar uma redução polinomial do ciclo de Hamilton para o PCV.
Pode ser feita conforme o exemplo abaixo.

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

• Dado um grafo representando uma instância do ciclo de Hamilton,


construa uma instância do PCV como se segue:
1. Para cidades use os vértices.
2. Para distâncias use 1 se existir um arco no grafo e 2 se não existir.
• A seguir, use o PCV para achar um roteiro menor ou igual a V .
• O roteiro é o ciclo de Hamilton.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 39

Classe N P-Intermediária

• Segunda descrição tentativa do mundo N P, assumindo P =


6 N P.

NP
NPC
NPI
P

• Existe uma classe intermediária entre P e N P chamada N PI.

• N PI seria constituída por problemas que ninguém conseguiu uma


redução polinomial de um problema N P-completo para eles, onde
N PI = N P - (P ∪ N P-completo).
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 40

Membros Potenciais de N PI

• Isomorfismo de grafos: Dados G = (V, E) e G′ = (V, E ′ ), existe uma


função f : V → V , tal que (u, v) ∈ E ⇔ (f (u), f (v)) ∈ E ′ ?
– Isomorfismo é o problema de testar se dois grafos são o mesmo.
– Suponha que seja dado um conjunto de grafos e que alguma
operação tenha de ser realizada sobre cada grafo.
– Se pudermos identificar quais grafos são duplicatas, eles poderiam
ser descartados para evitar trabalho redundante.

• Números compostos: Dado um inteiro positivo k, existem inteiros


m, n > 1 tais que k = mn?
– Princípio da criptografia RSA: é fácil encontrar números primos
grandes, mas difícil fatorar o produto de dois deles.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.1.2 41

Classe N P-Completo - Resumo


• Problemas que pertencem a N P, mas que podem ou não pertencer a
P.
• Propriedade: se qualquer problema N P-completo puder ser resolvido
em tempo polinomial por uma máquina determinista, então todos os
problemas da classe podem, isto é, P = N P.
• A falha coletiva de todos os pesquisadores para encontrar algoritmos
eficientes para estes problemas pode ser vista como uma dificuldade
para provar que P = N P.
• Contribuição prática da teoria: fornece um mecanismo que permite
descobrir se um novo problema é “fácil” ou “difícil”.
• Se encontrarmos um algoritmo eficiente para o problema, então não
há dificuldade. Senão, uma prova de que o problema é N P-completo
nos diz que o problema é tão “difícil” quanto todos os outros problemas
“difíceis” da classe N P-completo.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2 42

Problemas Exponenciais
• É desejável resolver instâncias grandes de problemas de otimização
em tempo razoável.
• Os melhores algoritmos para problemas N P-completo têm
comportamento de pior caso exponencial no tamanho da entrada.
• Para um algoritmo que execute em tempo proporcional a 2N , não é
garantido obter resposta para todos os problemas de tamanho
N ≥ 100.
• Independente da velocidade do computador, ninguém poderia esperar
por um algoritmo que leva 2100 passos para terminar sua tarefa.
• Um supercomputador poderia resolver um problema de tamanho
N = 50 em 1 hora, ou N = 51 em 2 horas, ou N = 59 em um ano.
• Nem um computador paralelo com um milhão de processadores, (cada
um sendo um milhão de vezes mais rápido que o mais rápido
existente) é suficiente para N = 100.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2 43

O Que Fazer para Resolver Problemas Exponenciais?


• Usar algoritmos exponenciais “eficientes” aplicando técnicas de
tentativa e erro.
• Usar algoritmos aproximados. Acham uma resposta que pode não ser
a solução ótima, mas é garantido ser próxima dela.
• Concentrar no caso médio. Buscar algoritmos melhores que outros
neste quesito e que funcionem bem para as entradas de dados que
ocorrem usualmente na prática.
– Existem poucos algoritmos exponenciais que são muito úteis na
prática.
– Exemplo: Simplex (programação linear). Complexidade de tempo
exponencial no pior caso, mas muito rápido na prática.
– Tais exemplos são raros. A grande maioria dos algoritmos
exponenciais conhecidos não é muito útil.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.1 44

Encontrar um Ciclo de Hamilton em um Grafo


Tentativa e Erro

void Visita (long k , TipoGrafo ∗ Grafo , long ∗ Tempo, long ∗ d)


{ long j ;
(∗Tempo)++;
d[ k] = ∗Tempo; • Obter algoritmo ten-
for ( j = 0; j < Grafo−>NumVertices ; j ++) tativa e erro a par-
i f ( Grafo−>Mat[ k ] [ j ] > 0) tir de algoritmo para
i f (d[ j ] == 0) Visita ( j , Grafo , Tempo, d) ;
caminhamento em
}
um grafo.
void Dfs(TipoGrafo ∗ Grafo)
{ long Tempo, i , d[ MAXNUMVERTICES + 1]; • O Dfs faz uma
Tempo = 0; busca em profundi-
for ( i = 0; i < Grafo−>NumVertices ; i ++) d[ i ] = 0; dade no grafo em
i = 0; tempo O(|V | + |A|).
Visita ( i , Grafo, &Tempo, d) ;
}
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.1 45

Ciclo de Hamilton - Tentativa e Erro

• Aplicando o Dfs ao grafo da figura abaixo a partir do vértice 0, o


procedimento Visita obtém o caminho 0 1 2 4 3 5 6, o que não é um
ciclo simples.
0 1 2 3 4 5 6
2 6 0 1 2 6
1
1 1 2 4
1 3 2 1
5 1 2 6 2 4
2 4
4 3 2 1
2 1
4 4 2 1
5

• Para encontrar um ciclo de Hamilton, caso exista, devemos visitar os


vértices do grafo de outras maneiras.
• A rigor, o melhor algoritmo conhecido resolve o problema tentando
todos os caminhos possíveis.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.1 46

Ciclo de Hamilton - Tentando Todas as Possibilidades

• Para tentar todas as possibilidades, alteramos o procedimento Visita.

• Desmarca o vértice já visitado no caminho anterior e permite que seja


visitado novamente em outra tentativa.

void Visita (long k , TipoGrafo ∗ Grafo, • O custo é proporcional


long ∗ Tempo, long ∗ d) ao número de chamadas
{ long j ; para o procedimento Vi-
∗Tempo++; sita.
d[ k] = ∗Tempo;
for ( j = 0; j < Grafo −> NumVertices ; j ++) • Para um grafo completo,
i f ( Grafo −> Mat[ k ] [ j ] > 0) (arestas ligando todos os
i f (d[ j ] == 0) Visita ( j , Grafo , Tempo, d) ; pares de nós) existem N !
∗Tempo−−; ciclos simples. Custo é
d[ k] = 0; proibitivo.
}
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.1 47

Ciclo de Hamilton - Tentando Todas as Possibilidades

• Para o grafo

0
2 6
1
1 3 2 1
5 1 2 6
2 4
4
2 1
4
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.1 48

Ciclo de Hamilton - Tentando Todas as Possibilidades


A árvore de caminhamento é:
0

1 5 6

2 3 4 3 4 4

4 4 5 2 3 5 6 1 4 1 2 3 6 1 2 3 5

3 5 6 2 5 6 4 5 3 2 4 1 2 6 2 3 1 1 2 3 1 1 5 3

5 3 2 6 4 2 6 2 1 3 2 5 3 2 1

6 5 2

0 0

• Existem duas respostas: 0 5 3 1 2 4 6 0 e


0 6 4 2 1 3 5 0.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.1 49

Ciclo de Hamilton - Tentativa e Erro com Poda

• Diminuir número de chamadas a Visita fazendo “poda” na árvore de


caminhamento.

• No exemplo anterior, cada ciclo é obtido duas vezes, caminhando em


ambas as direções.

• Insistindo que o nó 2 apareça antes do 0 e do 1, não precisamos


chamar Visita para o nó 1 a não ser que o nó 2 já esteja no caminho.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.1 50

Ciclo de Hamilton - Tentativa e Erro com Poda

• Árvore de caminhamento obtida:


0

5 6

3 4 4

4 2 3 6 2 3 5

2 6 1 1 5 3

1 3 3

0
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.1 51

Ciclo de Hamilton - Tentativa e Erro com Poda

• Entretanto, esta técnica não é sempre possível de ser aplicada.

• Suponha que se queira um caminho de custo mínimo que não seja um


ciclo e passe por todos os vértices: 0 6 4 5 3 1 2 é solução.

• Neste caso, a técnica de eliminar simetrias não funciona porque não


sabemos a priori se um caminho leva a um ciclo ou não.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.1 52

Ciclo de Hamilton - Branch-and-Bound

• Outra saída para tentar diminuir o número de chamadas a Visita é por


meio da técnica de branch-and-bound.

• A ideia é cortar a pesquisa tão logo se saiba que não levará a uma
solução.

• Corta chamadas a Visita tão logo se chegue a um custo para qualquer


caminho que seja maior que um caminho solução já obtido.

• Exemplo: encontrando 0 5 3 1 2 4 6, de custo 11, não faz sentido


continuar no caminho 0 6 4 1, de custo 11 também.

• Neste caso, podemos evitar chamadas a Visita se o custo do caminho


corrente for maior ou igual ao melhor caminho obtido até o momento.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.2 53

Heurísticas para Problemas N P-Completo

• Heurística: algoritmo que pode produzir um bom resultado (ou até a


solução ótima), mas pode também não obter solução ou obter uma
distante da ótima.

• Uma heurística pode ser determinista ou probabilística.

• Pode haver instâncias em que uma heurística (probabilística ou não)


nunca vai encontrar uma solução.

• A principal diferença entre uma heurística probabilística e um


algoritmo Monte Carlo é que o algoritmo Monte Carlo tem que
encontrar uma solução correta com uma certa probabilidade (de
preferência alta) para qualquer instância do problema.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.2 54

Heurística para o PCV

• Algoritmo do vizinho mais próximo, heurística gulosa simples:


1. Inicie com um vértice arbitrário.
2. Procure o vértice mais próximo do último vértice adicionado que
não esteja no caminho e adicione ao caminho a aresta que liga
esses dois vértices.
3. Quando todos os vértices estiverem no caminho, adicione uma
aresta conectando o vértice inicial e o último vértice adicionado.

• Complexidade: O(n2 ), sendo n o número de cidades, ou O(d), sendo d


o conjunto de distâncias entre cidades.

• Aspecto negativo: embora todas as arestas escolhidas sejam


localmente mínimas, a aresta final pode ser bastante longa.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.2 55

Heurística para o PCV

0 1 2 3 4 5
0 3 10 11 7 25
5 1
1 8 12 9 26
2 9 4 20
4 2 3 5 15
3 4 18

• Caminho ótimo para esta instância: 0 1 2 5 3 4 0 (comprimento 58).


• Para a heurística do vizinho mais próximo, se iniciarmos pelo vértice 0,
o vértice mais próximo é o 1 com distância 3.
• A partir do 1, o mais próximo é o 2, a partir do 2 o mais próximo é o 4,
a partir do 4 o mais próximo é o 3, a partir do 3 restam o 5 e o 0.
• O comprimento do caminho 0 1 2 4 3 5 0 é 60.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.2 56

Heurística para o PCV

• Embora o algoritmo do vizinho mais próximo não encontre a solução


ótima, a obtida está bem próxima do ótimo.

• Entretanto, é possível encontrar instâncias em que a solução obtida


pode ser muito ruim.

• Pode mesmo ser arbitrariamente ruim, uma vez que a aresta final pode
ser muito longa.

• É possível achar um algoritmo que garanta encontrar uma solução que


seja razoavelmente boa no pior caso, desde que a classe de instâncias
consideradas seja restrita.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 57

Algoritmos Aproximados para Problemas N P-Completo


• Para projetar algoritmos polinomiais para “resolver” um problema de
otimização N P-completo é necessário relaxar o significado de
resolver.
• Removemos a exigência de que o algoritmo tenha sempre de obter a
solução ótima.
• Procuramos algoritmos eficientes que não garantem obter a solução
ótima, mas sempre obtêm uma próxima da ótima.
• Tal solução, com valor próximo da ótima, é chamada de solução
aproximada.
• Um algoritmo aproximado para um problema Π é um algoritmo que
gera soluções aproximadas para Π.
• Para ser útil, é importante obter um limite para a razão entre a solução
ótima e a produzida pelo algoritmo aproximado.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 58

Medindo a Qualidade da Aproximação

• O comportamento de algoritmos aproximados quanto Ă qualidade dos


resultados (não o tempo para obtê-los) tem de ser monitorado.
• Seja I uma instância de um problema Π e seja S ∗ (I) o valor da
solução ótima para I.
• Um algoritmo aproximado gera uma solução possível para I cujo valor
S(I) é maior (pior) do que o valor ótimo S ∗ (I).
• Dependendo do problema, a solução a ser obtida pode minimizar ou
maximizar S(I).
• Para o PCV, podemos estar interessados em um algoritmo aproximado
que minimize S(I): obtém o valor mais próximo possível de S ∗ (I).
• No caso de o algoritmo aproximado obter a solução ótima, então
S(I) = S ∗ (I).
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 59

Algoritmos Aproximados - Definição

• Um algoritmo aproximado para um problema Π é um algoritmo


polinomial que produz uma solução S(I) para uma instância I de Π.

• O comportamento do algoritmo A é descrito pela razão de


aproximação
S(I)
RA (I) = ∗ ,
S (I)
que representa um problema de minimização

• No caso de um problema de maximização, a razão é invertida.

• Em ambos os casos, RA (I) ≥ 1.


Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 60

Algoritmos Aproximados para o PCV


• Seja G = (V, A) um grafo não direcionado, completo, especificado por um par
(N, d).
• N é o conjunto de vértices do grafo (cidades), e d é uma função distância que
mapeia as arestas em números reais, onde d satisfaz:
1. d(i, j) = d(j, i) ∀i, j ∈ N ,
2. d(i, j) > 0 ∀i, j ∈ N ,
3. d(i, j) + d(j, k) ≥ d(i, k) ∀i, j, k ∈ N
• 1a propriedade: a distância da cidade i até outra adjacente j é igual à de j
até i.
• Quando isso não acontece, temos o problema conhecido como PCV
Assimétrico
• 2a propriedade: apenas distâncias positivas.
• 3a propriedade: desigualdade triangular. A distância de i até j somada com
a de j até k deve ser maior do que a distância de i até k.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 61

Algoritmos Aproximados para o PCV

• Quando o problema exige distâncias não restritas à desigualdade


triangular, basta adicionar uma constante k a cada distância.

• Exemplo: as três distâncias envolvidas são 2, 3 e 10, que não


obedecem à desigualdade triangular pois 2 + 3 < 10. Adicionando
k = 10 às três distâncias obtendo 12, 13 e 20, que agora satisfazem a
desigualdade triangular.

• O problema alterado terá a mesma solução ótima que o problema


anterior, apenas com o comprimento da rota ótima diferindo de n × k.

• Cabe observar que o PCV equivale a encontrar no grafo G = (V, A) um


ciclo de Hamilton de custo mínimo.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 62

Árvore Geradora Mínima (AGM)


• Considere um grafo G = (V, A), sendo V as n cidades e A as
distâncias entre cidades.
• Uma árvore geradora é uma coleção de n − 1 arestas que ligam todas
as cidades por meio de um subgrafo conectado único.
• A árvore geradora mínima é a árvore geradora de custo mínimo.
• Existem algoritmos polinomiais de custo O(|A| log |V |) para obter a
árvore geradora mínima quando o grafo de entrada é dado na forma
de uma matriz de adjacência.
• Grafo e árvore geradora mínima correspondente:
0 0
2 6 2
1 1
1 3 2 1 1 3 1
5 1 2 6 5 1 2 6
2 4 2
4
2 1 1
4 4
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 63

Limite Inferior para a Solução do PCV a Partir da AGM

• A partir da AGM, podemos derivar o limite inferior para o PCV.

• Considere uma aresta (x1 , x2 ) do caminho ótimo do PCV. Remova a


aresta e ache um caminho iniciando em x1 e terminando em x2 .

• Ao retirar uma aresta do caminho ótimo, temos uma árvore geradora


que consiste de um caminho que visita todas as cidades.

• Logo, o caminho ótimo para o PCV é necessariamente maior do que o


comprimento da AGM.

• O limite inferior para o custo deste caminho é a AGM.

• Logo, Otimo P CV > AGM .


Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 64

Limite Superior de Aproximação para o PCV


• A desigualdade triangular permite utilizar a AGM para obter um limite
superior para a razão de aproximação com relação ao comprimento
do caminho ótimo.
• Vamos considerar um algoritmo que visita todas as cidades, mas pode
usar somente as arestas da AGM.
• Uma possibilidade é iniciar em um vértice folha e usar a seguinte
estratégia:
– Se houver aresta ainda não visitada saindo do vértice corrente,
siga aquela aresta para um novo vértice.
– Se todas as arestas a partir do vértice corrente tiverem sido
visitadas, volte para o vértice adjacente pela aresta pela qual o
vértice corrente foi inicialmente alcançado.
– Termine quando retornar ao vértice inicial.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 65

Limite Superior de Aproximação para o PCV - Busca em


Profundidade
• O algoritmo descrito anteriormente é a Busca em Profundidade
aplicada à AGM.
• Verifica-se que:
– o algoritmo visita todos os vértices.
– nenhuma aresta é visitada mais do que duas vezes.
• Obtém um caminho que visita todas as cidades cujo custo é menor ou
igual a duas vezes o custo da árvore geradora mínima.
• Como o caminho ótimo é maior do que o custo da AGM, então o
caminho obtido é no máximo duas vezes o custo do caminho ótimo.
CaminhoP CV < 2OtimoP CV .
• Restrição: algumas cidades são visitadas mais de uma vez.
• Para contornar o problema, usamos a desigualdade triangular.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 66

Limite Superior de Aproximação para o PCV -


Desigualdade Triangular

• Introduzimos curto-circuitos que nunca aumentam o comprimento total


do caminho.

• Inicie em uma folha da AGM, mas sempre que a busca em


profundidade for voltar para uma cidade já visitada, salte para a
próxima ainda não visitada.

• A rota direta não é maior do que a anterior indireta, em razão da


desigualdade triangular.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 67

Limite Superior de Aproximação para o PCV -


Desigualdade Triangular

• Se todas as cidades tiverem sido visitadas, volte para o ponto de


partida.

(a) (b) (c)

• O algoritmo constrói um caminho solução para o PCV porque cada


cidade é visitada apenas uma vez, exceto a cidade de partida.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 68

Limite Superior de Aproximação para o PCV -


Desigualdade Triangular
• O caminho obtido não é maior que o caminho obtido em uma busca em
profundidade, cujo comprimento é no máximo duas vezes o do caminho
ótimo.
• Os principais passos do algoritmo são:
1. Obtenha a árvore geradora mínima para o conjunto de n cidades, com
custo O(n2 ).
2. Aplique a busca em profundidade na AGM obtida com custo O(n):
– Inicie em uma folha (grau 1).
– Siga uma aresta não utilizada.
– Se for retornar para uma cidade já visitada, salte para a próxima ainda
não visitada (rota direta menor que a indireta pela desigualdade
triangular).
– Se todas as cidades tiverem sido visitadas, volte à cidade de origem.
• Assim, obtivemos um algoritmo polinomial de custo O(n2 ), com uma razão de
aproximação garantida para o pior caso de RA ≤ 2.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 69

Como Melhorar o Limite Superior a Partir da AGM


• No algoritmo anterior um caminho para o caixeiro viajante pode ser
obtido dobrando os arcos da AGM, o que leva a um pior caso para a
razão de aproximação no máximo igual a 2.
• Melhora-se a garantia de um fator 2 para o pior caso, utilizando o
conceito de grafo Euleriano.
• Um grafo Euleriano é um grafo conectado no qual todo vértice tem
grau par.
• Um grafo Euleriano possui um caminho Euleriano, um ciclo que
passa por todas as arestas exatamente uma vez.
• O caminho Euleriano em um grafo Euleriano, pode ser obtido em
tempo O(n), usando a busca em profundidade.
• Podemos obter um caminho para o PCV a partir de uma AGM, usando
o caminho Euleriano e a técnica de curto-circuito.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 70

Como Melhorar o Limite Superior a Partir da AGM

• Passos do algoritmo:
– Suponha uma AGM que tenha cidades do PCV como vértices.
– Dobre suas arestas para obter um grafo Euleriano.
– Encontre um caminho Euleriano para esse grafo.
– Converta-o em um caminho do caixeiro viajante usando
curto-circuitos.

• Pela desigualdade triangular, o caminho do caixeiro viajante não pode


ser mais longo do que o caminho Euleriano e, conseqüentemente, de
comprimento no máximo duas vezes o comprimento da AGM.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 71

Casamento Mínimo com Pesos

• Christophides propôs uma melhoria no algoritmo anterior utilizando o


conceito de casamento mínimo com pesos em grafos.

• Dado um conjunto contendo um número par de cidades, um


casamento é uma coleção de arestas M tal que cada cidade é a
extremidade de exatamente um arco em M .

• Um casamento mínimo é aquele para o qual o


comprimento total das arestas é mínimo.
• Todo vértice é parte de exatamente uma aresta
do conjunto M .
• Pode ser encontrado com custo O(n3 ).
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 72

Casamento Mínimo com Pesos


• Considere a AGM T de um grafo.
• Alguns vértices em T já possuem grau par, assim não precisariam receber
mais arestas se quisermos transformar a árvore em um grafo Euleriano.
• Os únicos vértices com que temos de nos preocupar são os vértices de grau
ímpar.
• Existe sempre um número par de vértices de grau ímpar, desde que a soma
dos graus de todos os vértices tenha de ser par porque cada aresta é
contada exatamente uma vez.
• Uma maneira de construir um grafo Euleriano que inclua T é simplesmente
obter um casamento para os vértices de grau ímpar.
• Isto aumenta de um o grau de cada vértice de grau ímpar. Os de de grau par
não mudam.
• Se adicionamos em T um casamento mínimo para os vértices de grau ímpar,
obtemos um grafo Euleriano que tem comprimento mínimo dentre aqueles
que contêm T .
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 73

Casamento Mínimo com Pesos - Exemplo

a. Uma árvore geradora mí-


nima T .
(a) (b)
b. T mais um casamento mí-
nimo dos vértices de grau
ímpar.
c. Caminho de Euler em (b).
d. Busca em profundidade
com curto-circuito.

(c) (d)
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 74

Casamento Mínimo com Pesos

• Basta agora determinar o comprimento do grafo de Euler.

• Caminho do caixeiro viajante em que podem ser vistas seis cidades


correspondentes aos vértices de grau ímpar enfatizadas.

Ótimo PVC

• O caminho determina os casamentos M e M ′ .


Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 75

Casamento Mínimo com Pesos


• Seja I uma instância do PCV, e Comp(T ), Comp(M ) e Comp(M ′ ),
respectivamente, a soma dos comprimentos de T , M e M ′ .
• Pela desigualdade triangular devemos ter que:
Comp(M ) + Comp(M ′ ) ≤ Otimo(I),
• Assim, ou M ou M ′ têm de ter comprimento menor ou igual a
Otimo(I)/2.
• Logo, o comprimento de um casamento mínimo para os vértices de
grau ímpar de T tem também de ter comprimento no máximo
Otimo(I)/2.
• Desde que o comprimento de M é menor do que o caminho do
caixeiro viajante ótimo, podemos concluir que o comprimento do grafo
Euleriano construído é:
3
Comp(I) < Otimo(I).
2
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 76

Casamento Mínimo com Pesos - Algoritmo de


Christophides

• Os principais passos do algoritmo de Christophides são:


1. Obtenha a AGM T para o conjunto de n cidades, com custo O(n2 ).
2. Construa um casamento mínimo M para o conjunto de vértices de
grau ímpar em T , com custo O(n3 ).
3. Encontre um caminho de Euler para o grafo Euleriano obtido com a
união de T e M , e converta o caminho de Euler em um caminho do
caixeiro viajante usando curto-circuitos, com um custo de O(N ).

• Assim obtivemos um algoritmo polinomial de custo O(n3 ), com uma


razão de aproximação garantida para o pior caso de RA < 3/2.
Projeto de Algoritmos – Cap.9 Problemas N P -Completo e Algoritmos Aproximados – Seção 9.2.3 77

Algoritmo de Christophides - Pior Caso


• Exemplo de pior caso do algoritmo de Christofides:
5

1 1 1 1 1

1 1 1 1 1 1 1 1 1 1
1 1 1 1

• A AGM e o caminho ótimo são:

AGM 1 1 1 1 1 1 1 1 1 1

1 1 1 1 1
Ótimo 1

1 1 1 1

• Neste caso, para uma instância I:


3
C(I) = [Otimo(I) − 1],
2
onde o Otimo(I) = 11, C(I) = 15, e AGM = 10.

Você também pode gostar