Escolar Documentos
Profissional Documentos
Cultura Documentos
AULA 6
2
Você começa a armazenar essas informações numa estrutura de dados
unidimensional (vetor) e percebe, ao realizar testes, que necessita de uma
maneira fácil de localizar cada estado dentro da estrutura de dados.
Para isso, resolve adotar a sigla de cada estado como uma palavra-chave
para as buscas. Desse modo, cada posição do vetor conterá a sigla do estado e
todos os diversos dados coletados e que você cadastrou.
Para a inserção e manipulação dos dados nesse vetor, você adota uma
implementação denominada endereçamento direto. Nela, cada estado inserido
é colocado na primeira posição livre do vetor. Como iniciamos o vetor na posição
zero, o primeiro estado será colocado nela. O segundo estado irá na posição um,
o terceira na posição dois, e assim sucessivamente até preencher o vetor. Todos
os dados são inseridos na sequência em que foram cadastrados. Na Figura 1,
temos um exemplo de um vetor de dimensão 10 com uma sigla inserida em cada
posição sua, servindo como referência para os dados cadastrados.
3
Outra possível implementação para nosso exemplo de cadastro de
estados seriam as listas encadeadas, as quais, embora trabalhem com um
conceito diferente de vetores e usufruam do uso de ponteiros para criar um
encadeamento entre os dados, apresentam tempo de busca de um dado que é
dependente do tamanho do seu conjunto de dados, uma vez que cada elemento
da lista só conhece seu sucessor (lista simples) e em alguns casos seu
antecessor também (lista dupla). A varredura pelos elementos da lista é
necessária para a localização de um dado.
E se pudéssemos tornar esse tempo de busca aos dados sempre
constante, com complexidade 𝑂(1), e independente do tamanho do conjunto de
entrada de dados, existiria uma solução para este problema? A resposta é sim.
E, para isso, devemos utilizar uma estrutura de dados denominada de hash.
Voltemos ao exemplo de cadastro de dados de estados brasileiros em um
vetor de dimensão 10. Na Figura 1, a inserção dos dados no vetor deu-se de
forma incremental, iniciando-se na posição zero. No exemplo a seguir, vamos
inserir os dados nesse mesmo vetor, mas utilizando uma outra abordagem.
Novamente, vamos adotar a sigla de cada estado como sendo um valor-
chave, ou palavra-chave. A diferença agora é que, com base nessa chave,
aplicaremos uma equação lógica e/ou matemática para definir uma posição de
inserção no vetor. A essa função denominamos de função hash, ou algoritmo de
hash.
Como sabemos de antemão que a sigla de todos os estados brasileiros é
sempre constituída de dois caracteres alfanuméricos, vamos converter cada
caractere para seu valor em decimal seguindo o padrão da tabela ASCII. Então
vamos definir uma função hash como sendo a soma em decimal de ambas as
letras, e dividir o resultado pelo tamanho de nosso vetor (dimensão 10). A
posição de inserção no vetor será o resto dessa divisão.
Saiba mais
Para consultar a tabela ASCII, acesse:
ASCII Table and Description. Disponível em: <http://www.asciitable.com/>.
Acesso em: 6 mar. 2019.
4
tamanho do vetor (dimensão 10), e obtendo somente o resto desta divisão,
temos o valor 2, que será, portanto, a posição de inserção dos dados referentes
ao estado do Paraná. Assim, mesmo que PR seja o primeiro dado a ser
cadastrado no vetor, ele não será posicionado na posição zero do vetor, mas sim
na posição dois.
De forma análoga, podemos calcular a posição no vetor do estado do Rio
Grande do Sul, sigla RS. Temos R = 82 e S = 83. A soma de ambos valores será
165, e o resto da divisão por 10 resultará no valor 5, valor este que corresponde
a posição de inserção deste dado no vetor.
Na Tabela 1, temos o cálculo da posição de inserção para alguns estados
brasileiros utilizando a função hash.
5
É interessante analisar que, alterando o tamanho de nosso vetor,
podemos acabar alterando também a posição de inserção de cada valor. Por
exemplo, se o vetor fosse de dimensão 12, a sigla PR seria posicionada no índice
6, e não no índice 2, conforme mostrado para um tamanho 10.
Na Figura 2, observe que, caso desejarmos encontrar os dados referentes
ao estado do Rio de Janeiro (posição 6), se aplicarmos um algoritmo de busca,
estamos sujeitos a uma complexidade atrelada ao tamanho do nosso vetor.
Porém, podemos utilizar como recurso de busca a mesma função hash usada
para inserir o dado e recalcular a posição de RJ no vetor aplicando a Equação
1. Assim, encontraremos a posição 6, bastando realizar o acesso em 𝑉𝑒𝑡𝑜𝑟[6]
com tempo constante. Desse modo, o tempo de busca passa a se tornar
independente do tamanho do vetor, dependendo somente do tempo para realizar
o cálculo matemático e lógico da função de hash sempre que necessário
encontrar algum dado. Todo e qualquer valor desse vetor poderá ser encontrado
com uma complexidade 𝑂(1).
O vetor contendo os valores-chave é denominado de tabela hashing. Para
cada palavra-chave, podemos ter a quantidade que necessitarmos de dados
cadastrados referentes aquela chave, dados estes chamados de dados satélites.
Ao longo desta aula, iremos investigar um pouco mais sobre hashs.
Veremos alguns tipos de funções hash bastante comuns, bem como tipos
distintos de endereçamentos e implementações.
Acerca da aplicabilidade da estrutura de dados do tipo hash, a gama de
aplicações é bastante grande. Citamos:
6
TEMA 2 – FUNÇÕES HASH
Fácil de ser calculada. De nada valeria termos uma função com cálculos
tão complexos e lentos que todo o tempo que seria ganho no acesso a
informação com complexidade 𝑂(1), seria perdido calculando uma
custosa função de hash;
Capaz de distribuir palavras-chave o mais uniforme possível;
Capaz de minimizar colisões. Os dados devem ser inseridos de uma forma
que as colisões sejam as mínimas possíveis, reduzindo o tempo gasto
resolvendo colisões e também reavendo os dados;
Capaz de resolver qualquer colisão que ocorrer;
7
99 + 88 + 22 + 33 = 242. Este valor é usado no método da divisão.
De modo geral, quando precisamos mapear um número de chaves em m
espaços (como os de um vetor) pegando o resto da divisão entre ambos,
chamamos isso de método da divisão, conforme apresentado na Equação 2.
Esse método é bastante rápido, uma vez que requer unicamente uma divisão.
8
com palavras-chaves alfanuméricas. Por exemplo, suponhamos que temos 2000
conjuntos de caracteres para serem colocadas em uma tabela hashing. Por
projeto, definiu-se que realizar uma busca, em média, em até três posições antes
2000
de encontrar um espaço vazio é considerado aceitável. Se fizermos ≅ 666.
3
Esse método apresenta como desvantagem o fato de ser mais lento para
execução em relação ao método da divisão, pois temos mais cálculos envolvidos
no processo, porém tem a vantagem de que o valor de m não é crítico, não
importando o valor escolhido.
Em contraponto ao método de divisão, normalmente adotamos um
múltiplo de 2 para seu valor, devido à facilidade de implementação. A constante
9
A, embora possa ser qualquer valor dentro do intervalo 0 < 𝐴 < 1, é melhor com
alguns determinados valores. Segundo Knuth (1998), um ótimo valor para essa
√5−1
constante é 𝐴 = ≅ 0,618. Exemplificando, se k = 123456, e m = 16384, e a
2
sugestão para o valor de A dada por Knuth for seguida, teremos h(k) = 67.
Implementação:
Imaginemos um número primo p e um conjunto de valores 𝑍𝑝 =
{0, 1, 2 … 𝑝 − 1} e definimos que 𝑍𝑝∗ = 𝑍𝑝 − {0}, ou seja, é o conjunto 𝑍𝑝 excluindo
o valor zero.
Podemos adotar uma classe de funções H que seja dependente deste
número primo p e do seu conjunto 𝑍𝑝 (Equação 6):
ℎ𝑎,𝑏 (𝑘) = ((𝑎𝑘 + 𝑏) 𝑀𝑂𝐷 𝑝) 𝑀𝑂𝐷 𝑚 (6)
0 1 0 0 1 0 (8)
𝑀 = [1 0
0 1 1] . [ ] = [1]
1 1 0 1 1 0
1
12
Figura 3 – Pseudocódigo de implementação da hash com menu para inserção e
remoção de chave.
13
Quando uma chave k é endereçada em uma posição h(k), e esta já está
ocupada, outras posições vazias na tabela são procuradas para armazenar k.
Caso nenhuma seja encontrada, isso significa que a tabela está totalmente
preenchida e k não pode ser armazenado.
Na tentativa linear, sempre que uma colisão ocorre, tenta-se posicionar a
nova chave no próximo espaço imediatamente livre do vetor. Vamos
compreender o funcionamento por meio de um exemplo.
Queremos preencher um vetor de dimensão 10 com palavras-chave como
a sigla de cada estado brasileiro (2 caracteres). O cálculo de cada posição é feito
utilizando o método de divisão para caracteres alfanuméricos (Equação 3).
Agora, imaginemos uma situação inicial em que temos o vetor preenchido
com 3 estados, conforme a Tabela 2, as siglas PR, RS e SC, em que cada sigla
está em uma posição distinta do vetor.
14
para a posição subsequentemente livre. Após a posição 2, seguimos para a
posição 3. Esta posição está vazia e podemos inserir o estado do AM nela. O
resultado é visto na Figura 5.
15
Linha 1: cabeçalho da função que recebe como parâmetro a tabela hash
usada, a posição inicial a ser testada (isso não garante a inserção, pois
depende do espaço estar livre), já calculada pela função hashing, e o valor
que será inserido;
Linhas 5 a 9: laço de repetição que localiza uma posição para inserir no
vetor, iniciando os testes pela posição recebido como parâmetro. As
condições impostas no laço enquanto dizem que a posição é selecionada
quando o espaço está livre (L) ou ele apresenta uma chave já removida
(R);
Linhas 11 a 16: quando a posição é selecionada, definimos ela como
ocupada (O) e inseridos a chave no local. Caso o tamanho do vetor tenha
sido atingido, a inserção não é possível, e uma mensagem é informada
ao usuário.
16
Assim, com base na chave calculamos a posição pela função hash e localizamos
a posição. Em seguida, marcamos aquele índice como removido (R). Vejamos:
Linha 19: cabeçalho da função que recebe como parâmetro somente a
tabela e o valor-chave para remoção;
Linha 23: realiza a busca na hash. Nesse algoritmo, a busca está
implementada em uma outra função, apresentada na Figura 8;
Linhas 25 a 29: marca a posição encontrada como removida (R). O valor
atualmente colocado nesta posição não precisa ser removido/limpado.
Manter o valor no vetor é uma estratégia de backup caso seja necessário
reaver o valor-chave em algum momento.
Por fim, temos a busca em uma tabela hash com tentativa linear. Em
nossos pseudocódigos, a função está separada da remoção, uma vez que
podemos somente buscar sem remover simultaneamente.
Iniciamos com o cálculo da função de hash. Nela, reavemos a posição
calculada, porém o simples cálculo não representa que a chave buscada estará
naquela posição, pois ela pode ter sido inserida em uma posição subsequente
devido a colisões. A Figura 9 mostra os passos do algoritmo para realizar este
processo:
18
(𝛼 = 𝑛⁄𝑚). Em endereçamento aberto, temos sempre 𝑛 < 𝑚 e portanto 𝛼 < 1,
pois temos sempre no máximo um elemento em cada posição.
Supondo uma função de hash uniforme aplicada em uma tabela com fator
de carga 𝛼 < 1, temos:
19
Figura 10 – Vetor com dados de estados brasileiros inseridos da Tabela 3
Vamos inserir o estado do Pará (PA) nesse vetor. O cálculo pelo método
da divisão para caracteres alfanuméricos resulta na posição 5 para a sigla PA,
posição em que já temos o estado RS. Vejamos as etapas para a inserção:
20
2. Etapa 1. Verifica-se a posição 5. Como ela está ocupada,
incrementa-se o valor de i nela, e realiza o cálculo da função
hashing novamente, resultando na posição 6. Incrementa-se o
contador.
Posição 5 já está ocupada.
𝑑 = (𝑑 + 𝑖) 𝑀𝑂𝐷 10 = (5 + 1) 𝑀𝑂𝐷 10 = 6
𝑖 =𝑖+1=2
3. Etapa 2. Verifica-se a posição 6. Como ela está ocupada,
incrementa-se o valor de i nela, e realiza o cálculo da função
hashing novamente, resultando na posição 8. Incrementa-se o
contador.
Posição 6 já está ocupada.
𝑑 = (𝑑 + 𝑘) 𝑀𝑂𝐷 10 = (6 + 2) 𝑀𝑂𝐷 10 = 8
𝑖 =𝑖+1=3
4. Etapa 3.
Posição 8 está livre. Inserção realizada.
21
Linha 6: inicialização da variável incremental i;
Linhas 8 a 13: laço de repetição que localiza uma posição para inserir no
vetor, iniciando os testes pela posição recebida como parâmetro. As
condições impostas no laço enquanto dizem que a posição é selecionada
quando o espaço está livre (L) ou ele apresenta uma chave já removida
(R). O cálculo da distância é feito da mesma maneira que no exemplo
apresentado no TEMA 4;
Linhas 15 a 20: quando a posição é selecionada, nós a definimos como
ocupada (O) e é inserida a chave no local. Caso o tamanho do vetor tenha
sido atingido, a inserção não é possível, e uma mensagem é informada
ao usuário.
22
Linha 27: realiza a busca na hash. Neste algoritmo a busca está
implementada em uma outra função, apresentada na Figura 14;
Linhas 28 a 32: marca a posição encontrada como removida (R). O valor
atualmente colocado nesta posição não precisa ser removido/limpado.
Manter o valor no vetor é uma estratégia de backup caso seja necessário
reaver o valor-chave em algum momento.
Por fim, temos a busca em uma tabela hash com tentativa quadrática. Em
nossos pseudocódigos a função está separada da remoção, uma vez que
podemos somente buscar sem a necessidade de remover, simultaneamente.
Iniciamos com o cálculo da função de hash. Nela, reavemos a posição
calculada, porém o simples cálculo não representa que a chave buscada estará
naquela posição, pois ela pode ter sido inserida em uma posição subsequente
devido a colisões. A Figura 15 mostra os passos do algoritmo para realizar este
processo:
Linha 35: cabeçalho da função que recebe a tabela hashing como
parâmetro e o valor a ser buscado nela;
Linha 39: calcula a posição inicial usando a função hash definida, neste
caso, o método da divisão;
Linha 40: inicializa a variável incremental i;
Linhas 42 a 47: realiza a varredura a partir da posição recebida como
parâmetro. Lembrando que os cálculos da distâncias são dados de forma
quadrática, conforme o exemplo deste tema;
Linhas 49 a 54: quando uma posição é encontrada verifica-se se a posição
não contém um valor removido;
23
Figura 15 – Pseudocódigo de busca por tentativa quadrática
25
alocarmos o elemento colidido. Basta inseri-lo na mesma posição 5 calculada,
mas como mais um elemento da lista encadeada simples.
A inserção é dada sempre antes do Head. Assim, a sigla PA virará o novo
Head da lista da posição 5, apontando para a sigla RS que está na segunda
posição da lista. Por se tratar de uma lista não circular, o último elemento conterá
um ponteiro nulo para próximo elemento.
26
Figura 18 – Endereçamento em cadeia. Adicionando AM na posição 2
5.1 Pseudocódigo
27
O algoritmo apresentado na Figura 20 corresponde ao menu
pseudocódigo principal referente à criação da estrutura da hash, do vetor e o
menu de seleção para inserção e remoção da tabela. Vejamos alguns pontos
relevantes:
28
Na Figura 21, temos a função de inserção no vetor com endereçamento
de cadeia. A inserção se dá da mesma maneira que uma inserção no início de
uma lista simplesmente encadeada. Vejamos:
29
Linhas 20 a 22: caso a condição da linha 19 for verdadeira, isso significa
que o Head da lista é o elemento buscado. Portanto, deleta-o e transforma
o próximo elemento no novo Head;
Linhas 24 a 30: caso a condição da linha 19 for falsa, significa que
precisamos varrer a lista encadeada buscando o elemento para remover.
Essa parte do código executa esta varredura na lista até localizar o valor
correspondente;
Linha 32: verifica se a variável auxiliar de varredura está vazia;
Linha 33 e 34: se a condição da linha 32 for verdadeira, significa que a
variável não está vazia, e portanto deletamos o elemento que está nela e
rearranjamos os ponteiros da lista encadeada;
Linhas 35 e 36: se a condição da linha 32 for falsa, significa que a variável
auxiliar está vazia, e portanto o valor não foi localizado.
30
5.2 Complexidade
FINALIZANDO
31
REFERÊNCIAS
KNUTH, D. E. The art of computer programming: Sorting and searching (v. 3).
2. ed. Boston/USA: Addison-Wesley, 1998.
32