Vous êtes sur la page 1sur 41

Algoritmos e Estruturas de

Dados II

Luiz Gustavo Bizarro Mirisola


Gustavo Sousa Pavani

Universidade Federal do ABC (UFABC)


1º Trimestre - 2011

Aulas 8 e 9

1
Busca em tabelas

Para se resolver os problemas de busca, inserção e


remoção em uma tabela com N elementos, há várias
maneiras:
Busca seqüencial.
● Acesso em O(N)

Busca Binária
● Acesso em O(log2N)
● Ainda muito lento se N é grande ((log21M)=20!

Uso de árvores B.
● Acesso em O(logkN), onde k é o tamanho da folha.
● Acesso bem melhor do que na busca seqüencial!

2
Busca em tabelas

Não é possível achar um método mais rápido?


O(n) não é ruim em memória, mas mesmo um método
logaritmico em seeks é custoso.
Lembra: acesso disco 1.000.000 vezes mais lento
Tabelas de dispersão (Hash Tables).
Busca com acesso médio igual a O(1).
O(1) significa “constante”
● Não é necessariamente 1 único acesso
● Alem disso, é O(1) na média! O pior caso é O(N)
● Na medida do possível, uma constante pequena!
● Idéia: tempo de acesso não deve variar com N!!

3
Armazenamento em tabelas

Suponha que existam n chaves a serem armazenadas em


uma tabela T, seqüencial e de dimensão m. As posições da
tabela se situam no intervalo [0,m-1].
Tabela é particionada em m compartimentos “buckets”, cada
um correspondendo a um endereço e podendo armazenar r
registros distintos.
● Se a tabela está na memória: comum r=1.
● Se a tabela está armazenada no disco:
– número de acessos é mais importante do que o uso eficiente
da memória.
– cada compartimento ocupa, tipicamente, um bloco de disco.
● Vamos considerar somente o primeiro caso (r=1).

Buckets
4
Acesso direto

Suponha que o número de chaves n seja igual ao


número de compartimentos m. Considere que os valores
das chaves sejam, respectivamente, 0,1,...,(m-1).
Pode-se utilizar então diretamente o valor de cada chave
como seu índice na tabela!
● Cada chave x é armazenada no compartimento x.

Técnica denominada de acesso direto.


Essa técnica também pode ser utilizada quando n < m,
porém (m-n) é pequeno.
● As chaves se distribuiriam entre 0,1,...,(m-1).
● Haveriam (m-n) compartimentos vazios ou espaços.

5
Problemas com o acesso direto

As chaves nem sempre são valores numéricos.


Exemplo: chaves como nome de pessoas.
Solução: construa representação numérica das chaves.
● Função f (chave) 
→[0...n − 1]
O problema dos espaços é mais grave!
Exemplo: duas chaves 0 e 999999.
Acesso direto: tabela de 1 M buckets, sendo que somente dois
seriam ocupados.
Solução: o uso de tabelas de dispersão.
Precisamos de um bucket para cada chave possível?

6
Tabela de dispersão

Em lugar de organizar a tabela segundo o valor


relativo de cada chave em relação às demais, a
tabela de dispersão leva em conta somente o seu
valor absoluto, interpretado como um valor
numérico.
Através da aplicação de uma função conveniente
(função de dispersão ou função hash), a chave
é transformada em um endereço da tabela
(endereço base)

hash(chave) 
→[0...m − 1]
Note: m, não n !
7
Tabela de dispersão

Em lugar de organizar a tabela segundo o valor


relativo de cada chave em relação às demais, a
tabela de dispersão leva em conta somente o seu
valor absoluto, interpretado como um valor
numérico.
Através da aplicação de uma função conveniente
(função de dispersão ou função hash), a chave
é transformada em um endereço da tabela
(endereço base).
O método aproveita a possibilidade de acesso
randômico a memória para alcançar uma
complexidade média O(1), sendo o pior caso,
entretanto O(N).
8
Diferenças das tabelas de dispersão

Hashing: o endereço base gerado pela função de


dispersão parece ser aleatório.
Não há relação óbvia entre a chave e a localização
do registro correspondente, mesmo que a chave
seja usada para determinar a localização do
registro.
Hashing: duas chaves diferentes podem ser
transformadas para o mesmo endereço base, de
forma que dois registros podem ser enviados para
um mesmo local no arquivo.
Quando isso ocorre, dizemos que acontece uma
colisão, que deve ser apropriadamente tratada.
9
Exemplo

Tabela, m = 11, Inicializada vazia

10
Exemplo

Adicionado a chave “Steve”.


h(Steve) = 3
l Para buscar “Steve” na tabela:
l Calcule h(Steve), obtenha 3
l Vá diretamente à posição 3 e
encontre “Steve”
l Hash table pode ser índice.
l RNN ou byte offset armazenado
junto com a chave.

11
Exemplo

Adicionado “Sparck”:
OK, h(Spark)=6
l Calcule h(Zé)=9
l Leia a posição 9, não há nada:
l Conclusão:
l Zé não está na tabela

12
Exemplo

Adicionado “Notes”:
Problema: h(Notes)=3
l h(Notes)=h(Steve)
l Colisão!!
l Há mais que 11 strings no mundo
l Com função hash uniforme, qual a
chance de colisão?
l Depende:
l tamanho da tabela
l quantas chaves devem ser
armazenadas.
13
Exemplo

Adicionado “Notes”:
Problema: h(Notes)=3
l h(Notes)=h(Steve)
l Colisão!!
l Várias formas de tratar colisões
l Onde armazenar e como
encontrar Notes e Steve agora.
l Importante:
l Colisões diminuem a eficiência.

14
Função de dispersão

Uma função de dispersão h transforma uma chave x em um


endereço base h(x) da tabela de dispersão.
Idealmente, deve satisfazer as seguintes condições:
Produzir um número baixo de colisões;
● Algum conhecimento é necessário sobre as chaves.
● Se m < n, há sempre colisões com n chaves armazenadas.

Ser facilmente computável;


● Se a função hash for O(n), qual a vantagem?

Ser uniforme.
● Igual probabilidade de escolha para todos os buckets

15
Função de dispersão perfeita

Quando uma função de dispersão é capaz de evitar


qualquer colisão tal função é dita perfeita.
Supondo-se determinados n e m.

Entretanto, é muito difícil se obter uma função de dispersão


perfeita.
Suponha que eu gostaria de guardar 4000 registros em 5000
compartimentos.
Foi mostrado que de todas as funções de dispersão possíveis, somente
uma em 10120000 é capaz de evitar todas as colisões.
Para um número pequeno de chaves que mudam com pouca
freqüência é possível achar com mais facilidade uma função de
dispersão perfeita.

Foco: minimizar o número de colisões a um valor aceitável.


16
Hash Function: Método da divisão

Fácil, eficiente, largamente empregado.


A chave x é dividida pela dimensão da tabela m e o
resto da divisão é o endereço base.
H(x) = x mod m.
Resulta em endereços no intervalo [0, m-1].
Cuidado na escolha do valor de m!
● Números pares são ruins.
● Potências de 2 são ainda piores!
– Ignora os bits mais significativos da chave.
● Sugestão: escolher m tal que não possua divisores primos menores
que 20.
● Ou um número primo de tamanho apropriado.

17
Hash Function: Método da dobra
Suponha a chave como uma seqüência de dígitos escritos em
um pedaço de papel. O método consiste em “dobrar” esse
papel de maneira que os dígitos se superponham. Estes
devem ser somados, sem levar em consideração o “vai um”.
Suponha que os dígitos decimais da chave sejam d1,...,dk e que uma dobra
seja realizada após o j-ésimo dígito da esquerda.
Isto implica transformar a chave em d’1,..., d’j, d2j+1, ..., dk, onde d’i é o
dígito menos significativo da soma di+d2j-i+1,1  i  j.
O processo é repetido. O número total de dobras e posição j de cada uma
devem ser definidos de forma que o resultado final contenha o número de
dígitos desejado para formar o endereço base.
Outra maneira de se ober um endereço base de k bits para uma chave
qualquer é separar a chave em diversos campos de k bits e operá-los com
o ou exclusivo. Há várias variações dependendo da chave.

18
Hash Function: Multiplicação

Apresenta algumas variações, sendo a mais conhecida


o método do “meio quadrado”.
A chave é multiplicada por ela mesma (ou
alternativamente por uma constante) e o resultado é
armazenado em uma palavra de memória de b bits.
O número de bits necessário para formar o endereço
base é então retirado dos b bits, descartando-se os bits
excessivos da extrema direita e da extrema esquerda
da palavra.
O número b bits determina o número possível de
endereços base.

19
Hash Function: Aleatória x Uniforme

Idéia: um liquidificador de bits, com distribuição da


saída mais uniforme possível
Outro exemplo: criptografar a chave
Usar chave cifrada como índice no hash

Pergunta: Uniforme = Aleatória ?

Distribuções melhores que aleatória:


Uniformidade com função específica
para as chaves, caso a caso.
20
Método da análise dos dígitos
Ao contrário dos demais, necessita de conhecimento prévio do tipo
de chave de busca. É usado em geral para chaves decimais.
A função de dispersão consiste em selecionar, de forma conveniente, alguns
dígitos decimais que formam a chave para compor o seu endereço base.
Suponha que os valores das chaves sejam uniformemente distribuídos.
Inicialmente, observa-se o primeiro dígito de cada chave. Se existem n chaves
diferentes, então, em média, n/10 devem ter o primeiro dígito zero, n/10 devem ter
o primeiro dígito 1 e assim sucessivamente. Suponha que existem, na verdade, ni
chaves com primeiro dígito i, 0  i  9.
Deve-se calcular o desvio da distribuição do primeiro dígito.
9

∑ i
( n
i =0
− n / 10 ) 2

Repita a análise para cada dígito e encontre os k melhores, isto é, aqueles que
possuem o menor desvio. O endereço base é obtido eliminando-se todos os
dígitos da chave, exceto os escolhidos entre os k melhores.

21
Memória extra para reduzir colisões
Fator de carga
(# registros armazenados)/(# de compartimentos) = r / m.
Medida da quantidade de memória ou espaço em disco que é
realmente usado.
Fórmula de Poisson
( r / m) x e − r / m
Vale para distr. aleatória p( x) =
x!
● Quantos compartimentos ficaram vazios = N p(0)
● Quantos compartimentos terão um registro = N p(1)
● Quantos compartimentos terão mais de um registro alocado = N [p(2) + p(3) + p(4) +
p(5)]. Para x  6, o valor é desprezível.
● Supondo um registro por compartimento, quantos registros fora do endereço base são
esperados = N [p(2) + 2p(3) + 3p(4) + 4p(5)]
Para p(2), um no compartimento e outro fora. Para p(3), um no
compartimento e dois fora. E assim por diante...

22
Efeito do fator de carga
Efeito do fator de carga na proporção de registros não armazenados
no endereço base, supondo um registro por compartimento.
Note: esse resultado não conta toda a estória, pois um registro fora do seu
endereço base pode ocupar o endereço base de outro registro, provocando outra
colisão no futuro! (Depende de como colisões são tratadas)
Efeito acumulativo
Colisões secundárias
Fator de carga (%) % de registros não armazenados
10 4,8
20 9,4
30 13,6
40 17,6
50 21,4
60 24,8
70 28,1
80 31,2
90 34,1
100 36,8
23
Tratamento de colisões

Mesmo que uma função de dispersão seja muito boa,


sempre existe a possibilidade da ocorrência de
colisões.
A diminuição de fator de carga não elimina todas as
colisões, apenas as torna mais raras
Outro problema: desperdício de espaço para baixos
fatores de carga.
Necessidade: método de tratamento de colisões.
Armazenar chaves sinônimas em listas encadeadas
Ou fora da tabela (sem provocar novas colisões)
Ou compartilhando o espaço da tabela.

24
Encadeamento exterior
Consiste em manter m listas encadeadas, uma para cada
possível endereço base.
Campo de encadeamento deve ser acrescentado a cada registro.
Os registros correspondentes aos endereços base serão os nós
cabeça para essas listas.
Implementação: simples aplicação de lista encadeada.

25
Encadeamento exterior
O comprimento de uma lista encadeada pode ser O(N), logo esta é a
complexidade de pior caso.
No caso médio, entretanto, temos para uma função de dispersão
uniforme:
● Busca sem sucesso: número médio de comparações efetuadas é igual
ao fator de carga (α).
● Busca com sucesso: 1 + α/2 – 1/2m.

Desvantagem:
Espaço para ponteiros

26
Encadeamento interior

Em algumas aplicações, não é desejável a


manutenção de uma estrutura exterior à tabela de
dispersão, como uma lista encadeada.
Neste caso, a lista encadeada deve compartilhar o
mesmo espaço de memória da tabela de dispersão.

27
Encadeamento interior

O encadeamento interior prevê a divisão da tabela


T em duas zonas, uma de endereços base, de
tamanho p, e outra reservada aos sinônimos, de
tamanho s. Naturalmente, p + s = m. Os valores de
p e s são fixos. Assim, a função de dispersão deve
obter endereços base na faixa [0,p-1].
● Dois campos no compartimento. Um para o armazenamento da
chave e outro um ponteiro que indica o próximo elemento da
lista de sinônimos correspondentes ao endereço base em
questão.

28
Encadeamento interior - problemas

A zona de colisão tem tamanho limitado, o que pode causar


“overflow” caso ela já tenha sido preenchida.
Entretanto, aumentando-se o tamanho da zona de colisão pela
correspondente diminuição da zona de endereços base, ao diminuir a
probabilidade de overflow, causa também uma diminuição da eficiência
da tabela de dispersão.
No caso limite em que p = 1 e s = m – 1, a tabela de dispersão de um
bucket se reduz a uma lista encadeada, cujo tempo médio de busca é
O(N).
Uma saída é não se diferenciar as duas zonas da tabela, mas essa
técnica produz o efeito indesejado de colisões secundárias, isto é,
aquelas provenientes da coincidência de endereços para chaves que
não são sinônimas.
● Diminuição da eficiência.

29
Busca por encadeamento interior

Tratamento da remoção de uma chave.


Está fora de cogitação re-organizar a tabela T.
Uso de três estados: vazio, ocupado e liberado.
Supondo que cada registro contém três campos: chave,
estado e ponteiro.
Procedimento busca(x, end, a)
● a  0; end  h(x); j  nil;
● enquanto a = 0 faça
● se T[end].estado = “não ocupado” então j  end;
● se T[end].chave = x e T[end].estado = “ocupado” então
● a  1; % chave encontrada
● senão end  T[end].ponteiro
● se end = h(x) então
● a  2; end  j; % chave não encontrada
30
Inserção por encadeamento interior
busca(x, end, a)
se a  1 então
se end  nil então j  end;
senão i  1; j  h(x);
enquanto i  m faça
se T[j] estado = “ocupado” então
j  (j+1) mod m;
i  i + 1;
senão i  m + 2 %compartimento não ocupado
se i = (m + 1) então “inserção inválida. overflow”; pare
temp  T[j].ponteiro; % fusão das listas
T[j].ponteiro  T[h(x)].ponteiro;
T[h(x)].ponteiro  temp;
T[j].chave  x; % inserção de x
T[j].estado  “ocupado”
Senão “inserção inválida; chave já existente”.

31
Remoção por encadeamento interior

busca(x, end, a)
se a = 1 então T[end].estado  “liberado”
senão “exclusão inválida: chave não existente.”

A busca, inserção e remoção possuem pior caso igual a


O(N).

32
Endereçamento aberto
O tratamento de colisões por encadeamento utiliza
listas encadeadas para armazenar chaves sinônimas.
Desperdício de espaço com ponteiros.
Solução: endereçamento aberto. As chaves sinônimas
são também armazenadas na tabela, sem qualquer
informação adicional.
Quando houver colisão, determina-se por cálculo qual
o próximo compartimento a ser examinado. Se ocorrer
nova colisão, um novo compartimento é escolhido por
cálculo, e assim por diante. A busca com sucesso se
encerra quando o compartimento for encontrado
contendo a chave procurada. A busca sem sucesso
ocorre quando o cálculo indica um compartimento
vazio ou a exaustão da tabela.
33
Endereçamento aberto - busca
A função de dispersão h(x) deve ser capaz de fornecer até m
endereços base.
Nova forma: h(x, k), onde k = 0,...,(m-1).
A seqüência de endereços base h(x, k), para k = 0,...,(m-1), é chamada de
seqüência de tentativas.

Mecanismo de busca
procedimento busca-aberto(x, end, a)
a = 3; k = 0;
enquanto k < m faça
end = h(x, k);
se T[h(x,k)].chave = x então
a = 1; k = m; % chave encontrada
senão se T[h(x,k)].chave = nil então
a = 2; k = m; % posição livre
senão k = k + 1
34
Endereçamento aberto - remoção

Busca e inserção tem pior caso igual a O(N)


Na média, para α < 1, busca sem sucesso faz 1/(1- α)
tentativas e busca com sucesso faz

1 1 1
log +
α 1−α α
Como no caso do encadeamento interior, cada
compartimento da tabela deve ter três estados: vazio,
ocupado e liberado para permitir a remoção de elementos.
Liberado: marcador (tombstone) indica que
o registro já foi ocupado, e a busca deve continuar

35
Tentativa linear

Suponha que o endereço base da chave x é h’(x) e


que exista uma chave x’ que ocupa o compartimento
h’(x).
Solução: armazenar o novo registro, de chave x, no
endereço consecutivo h’(x) + 1. Se este estiver
ocupado, tenta-se h’(x) + 2, ..., etc. até uma posição
vazia, considerando-se a tabela circular.
h(x, k) = (h’(x) + k) mod m, 0 <= k <= (m – 1)
Possui o inconveniente de produzir longos trechos consecutivos de
memória ocupados, o que se denomina agrupamento primário.
● Quanto maior for o tamanho do agrupamento primário, maior a
probabilidade de aumentá-lo ainda mais!
36
Tentativa linear – porque a tombstone

1- registros abaixo foram inseridos na ordem dada


2- Morris foi apagado
3- agora se busca por Smith, mas há um espaço livre
entre o slot 5 e a posição de Smith.
A busca não pode parar no slot 7 só poque está livre

37
Tentativa quadrática

Tentativa de se obter seqüências de endereços diversos para


endereços base próximos, porém diferentes.
O incremento é dado por uma função quadrática de k.
h(x, k) = (h’(x) + c1 k +c2 k2) mod m, c1 e c2 são constantes, c2  0 e 0  k  (m –
1).
Entretanto, se duas chaves possuírem a mesma tentativa inicial, as duas
seqüências de tentativas serão idênticas. Esse fato produz uma outra forma de
concentração de chaves na tabela, denominada de agrupamento secundário. A
degradação é menor que no agrupamento primário.
m, c1 e c2 devem ser escolhidos de forma que consigam varrer toda a tabela.
Forma recorrente
● h(x,0) = h’(x)
● h(x,k) = (h(x,k-1)+k) mod m
● Se m for potência de 2, é possível varrer toda a tabela.

38
Dispersão dupla
A seqüência de alternativas é calculada como:
h(x, k) = (h’(x)+ k h’’(x)) mod m, 0 <= k < m.
Esse método tende a distribuir as chaves na tabela de forma mais
conveniente que a tentativa linear ou a quadrática.
● Se x e y são duas chaves distintas tais que h’(x) = h’(y), então as
seqüências obtidas por esses métodos serão idênticas, o que
ocasiona a concentração de chaves em trechos da tabela.
● Na dispersão dupla, isso somente acontece se h’(x) = h’(y) e h’’(x) =
h’’(y) são idênticas.
● Também é capaz de gerar um maior número de tentativas distintas.
● Para que os endereços bases obtidos correspondam a varredura de
toda a tabela, é necessário que h’’(x) e m sejam primos entre si.
– Exemplo: m como potência de 2 e h’’(x) gerando números
ímpares ou, mais fácil ainda, m primo.

39
Tabela de dispersão dinâmica

Até o momento, o número de compartimentos da tabela não


variava. Pode não ser possível avaliar de antemão o número de
elementos da tabela
Solução simples: método da dispersão linear.
● Alterar apenas parte dos endereços já alocados.
Expansão de um compartimento p com a definição de um novo
compartimento q no final de tabela, denominado de expansão de p.
O conjunto de chaves sinônimas, originalmente com endereço base p, é
distribuído entre os compartimentos p e q.
Quando todos os compartimentos tiverem sido expandidos, o tamanho da
tabela terá sido dobrado e o processo, quando necessário, poderá ser
recomeçado.
Quando se alcançar determinado limiar no fator de carga, a tabela pode ser
expandida.
Normalmente o tratamento de colisões é por encadeamento exterior, pela
facilidade de ajustes nos ponteiros.
40
Processo de expansão

Dada uma chave x, computa-se h0(x). Seja p o menor


compartimento ainda não expandido. Se h0(x) < p, o
compartimento já foi expandido; o endereço correto é
então recalculado com a função h1(x)
Note que quando toda a tabela tiver sido expandida,
todos os endereços serão calculados por h1(x).
É necessário conhecer l, que é o número de vezes que a
tabela foi duplicada.
A função de dispersão é então fornecida por:
● hl(x) = x mod (2l m)

41

Vous aimerez peut-être aussi