Você está na página 1de 8

Estruturas de Dados Captulo 13: Tabelas Hash

13.1. Introduo Uma maneira de organizar dados, que apresenta bons resultados na prtica, conhecida como hashing1, e se baseia na idia de distribuir os dados em posies aleatrias de uma tabela. Podemos apresentar esta idia atravs de um exemplo. Suponhamos que desejamos organizar o cadastro de aproximadamente 500 empregados de uma empresa usando para identificar cada empregado o seu CPF2. Por exemplo, a informao sobre cada empregado pode ser guardada em um registro INFO:
typedef struct info INFO, *PT; struct info{ int cpf; char nome[80]; char ender[120]; ... };

Como os CPFs variam entre 000 000 000 e 999 999 999, ignorando-se os dgitos verificadores, podemos guardar toda a informao em um vetor
INFO vet[1000000000];

ou para economizar algum espao, em posies apontadas por componentes do vetor


PT vet[1000000000];

(A economia de espao viria do fato de que INFO* ocupa menos espao do que INFO, e do fato de que s precisariam ser representadas em estruturas INFO as informaes associadas aos 500 empregados.) Estas duas maneiras tornariam o acesso muito simples: no primeiro caso, a informao sobre o empregado de CPF c estaria em vet[c], e, no segundo caso, em *vet[c]. Em qualquer dos dois casos, entretanto, a idia absurda, por causa do espao total requerido. Uma variante desta idia procura reduzir o espao usando um CPF parcial, por exemplo, os trs ltimos dgitos. Neste caso, poderamos ter um vetor
PT vet[1000];

e a informao sobre o mesmo empregado estaria em *vet[c%1000], usando-se aqui o operador % de C, que representa mdulo, ou resto da diviso. O espao foi reduzido a um total aceitvel, o acesso continua simples, mas um problema adicional surgiu: dois empregados que tem CPFs com os mesmos trs ltimos dgitos passam a ter as informaes guardadas na mesma posio do vetor. Esta situao chamada de coliso, e deve ser resolvida encontrando-se uma posio alternativa para os dados de um dos empregados.

1 2

to hash significa cortar em pedacinhos, picar; falando de comida, hash pode ser picadinho. ou, mais precisamente, seu nmero de inscrio no cadastro de pessoas fsicas da receita federal Estruturas de Dados, J. L. Rangel, 13-1

No contexto da nossa discusso neste captulo, a funo tomar os trs ltimos dgitos do CPF a funo de hash, vet uma tabela hash. Naturalmente este sistema s pode ser utilizado se tivermos uma boa soluo para o problema da coliso. Em particular, a probabilidade de coliso pode ser reduzida usando uma tabela suficiente para vrias vezes o nmero total de entradas. Por exemplo, uma tabela com 1000 entradas (como acima), no caso de uma empresa com 500 empregados (500 posies ocupadas) teria uma probabilidade de 50% de coliso, se fosse feita a insero de um novo empregado. A propriedade fundamental da funo de hash a de espalhar bem as chaves de busca (os valores pelos quais se faz a busca na tabela), para que o nmero de colises seja o menor possvel, e neste sentido que dissemos, no incio desta Introduo, que seria bom usar posies aleatrias da tabela para guardar as informaes. 13.2. Formas possveis de tratamento de colises H vrias formas possveis de tratar colises. A mais simples usar a prxima posio vazia, em caso de coliso. Por exemplo, para inserir um elemento x numa tabela, usando a funo de hash h, podemos usar a primeira das posies h(x), h(x)+1, h(x)+2, que estiver vazia. (Se o final da tabela for atingido, continuamos, circularmente, a partir do incio.) Esta forma de tratamento de colises tem uma desvantagem fundamental, que a tendncia de formao de grupos de posies ocupadas consecutivas, fazendo com que a primeira posio vazia, na prtica, possa ficar muito longe da posio original, dada pela funo de hash. Para inserir um determinado valor x na tabela, ou para concluir que o valor no se encontra na tabela, necessrio encontrar a primeira posio vazia aps a posio h(x). Para fixar as idias, suponha que inserimos u, x, v, w, y e z na tabela, nessa ordem, e que h(u)=h(v)=h(w)=423, e h(x)=h(y)=h(z)=425. Inicialmente, inserimos u na posio h(u)=423, e x na posio h(x)=425.
422 423 u 424 425 x 426 427 428

Em seguida, como h(v)=423 est ocupada, v inserido na posio seguinte, 424.


422 423 u 424 v 425 x 426 427 428

De forma semelhante, w vai ser inserido na primeira posio vazia aps h(w)=423, ou sejam na posio 426.
422 423 u 424 v 425 x 426 w 427 428

Idem, y e z, nas posies 427 e 428.


422 423 u 424 v 425 x 426 w 427 y 428 z

Estruturas de Dados, J. L. Rangel, 13-2

Suponha agora que vamos procurar n na tabela, e que h(n)=424. Para concluir que este valor no se encontra na tabela, devemos visitar 6 posies da tabela, de 424 a 429. Normalmente no se utilizam tabelas hash em situaes em que elementos devem ser removidos, pelas dificuldades impostas pelos esquemas de tratamento de colises. Continuando o exemplo anterior, suponha que x simplesmente removido:
422 423 u 424 v 425 426 w 427 y 428 z

De todos os elementos mencionados, apenas u e v continuam acessveis: o acesso a w, y e z foi perdido. Para remover x, de forma correta, seria necessrio mudar diversos outros elementos de posio na tabela. Um esquema um pouco mais complicado utiliza uma segunda funo r, a funo de re-hash, para resolver colises. Assim, um elemento x seria inserido na primeira das posies vazias entre h(x), h(x)+r(x), h(x)+2*r(x), A vantagem que se tivermos h(x)=h(y)=h(z)=i, ser pouco provvel que tenhamos tambm r(y)=r(z). Assim, x seria inserido na posio i, y seria inserido na posio i+r(y), e z seria inserido numa posio diferente i+r(z). Com isso, a busca de z no passaria pela posio ocupada por y. Uma possibilidade adicional para o problema das colises ter uma lista de entradas associada a uma posio na tabela. No exemplo acima, a posio 423 da tabela conteria um apontador para uma lista encadeada com trs ns, correspondentes a u, v e w; idem 425, para x, y e z. Esta lista pode tambm ser implementada em uma rea de overflow na prpria tabela, onde so feitas as inseres em caso de coliso. Uma implementao de uma tabela com esta organizao pode ser encontrada na ltima seo deste captulo. Considera-se que, em uma tabela hash bem dimensionada, devemos ter 1,5 acessos tabela, em mdia, para encontrar um elemento. Isto corresponde a uma situao em que metade dos acessos feita diretamente, e, para a outra metade, ocorre uma coliso. Voltando aos nmeros do exemplo da Introduo, a tabela com um bilho de entradas garantia um custo de 1,0 acessos; se o preo a pagar por uma tabela de tamanho 1000 for o adicional de 0,5 acesso, em mdia, pode ser considerado razovel. 13.3. Estruturas possveis para tabelas de hash Para definir uma tabela de hash, precisamos definir uma chave de busca para a informao. Esta chave pode ser um nmero, como um CPF, pode ser uma cadeia de smbolos, como um nome, ou pode ser a combinao de vrias informaes. Os registros que contm a informao podem ser armazenados em memria ou em arquivos, dependendo basicamente do tamanho e do nmero de registros a ser considerado. Assim, se a informao sobre cada elemento pode ser representada por uma estrutura INFO, a tabela de hash pode ter a forma
INFO vet[max];

mas o normal seria


INFO *vet[max];

Estruturas de Dados, J. L. Rangel, 13-3

para reduzir o custo de manter posies vazias na tabela. No caso de informao armazenada em um arquivo, poderamos ter como apontador um nmero inteiro, o nmero do registro correspondente no arquivo. Neste caso, seria interessante fazer com que a tabela tivesse duas colunas, uma para conter o valor da chave de busca, e outra para o nmero do registro correspondente no arquivo, para evitar que fosse necessrio consultar o arquivo apenas para concluir que no se trata do elemento desejado, como acontece na resoluo de colises. Poderamos ter
typedef struct linha { int CPF; /* ou outra chave de busca */ int reg; /* nmero do registro no arquivo */ } LINHA; LINHA vet[max];

A exata estrutura de uma tabela vai depender da aplicao pretendida. 13.4. Critrios para escolha de uma funo de hash A primeira propriedade desejvel para uma funo de hash (ou de re-hash) a facilidade de sua avaliao. Por essa razo, normalmente as operaes utilizadas em uma funo de hash so as operaes correspondentes s instrues mais rpidas de um computador: and (e), or (ou), xor (ou exclusivo) e os deslocamentos de bits. Operaes como a soma podem ser utilizadas, embora sejam mais lentas que as outras mencionadas, mas funes matemticas como senos ou logaritmos normalmente no chegam a ser consideradas. Por exemplo, para acelerar o clculo da funo de hash, em vez de usar o resto da diviso por 1000, como proposto na Introduo, seria prefervel usar o resto da diviso pela potncia de 2 mais prxima, 1024, uma vez que o valor x%1024 pode ser calculado de forma mais rpida (em C) como x&1023. Isto acontece porque 1023 em binrio 00...0111111111, de maneira que x&1023 tem todos os bits iguais a zero, com exceo dos ltimos 9 bits, que so iguais aos bits correspondentes de x. (O operador & de C faz o and lgico de dois nmeros bit a bit. Mesmo neste caso simples, a escolha dos bits do CPF que sero usados na funo de hash precisa ser feita com cuidado. No nosso caso, escolhemos os ltimos porque os CPFs so atribudos, em princpio, consecutivamente, o que faz com que os valores dos trs ltimos dgitos possam, na prtica, ser considerados aleatrios. Se usssemos os trs primeiros, provavelmente teramos mais colises, entre empregados com a mesma idade aproximada. 13.5. Um exemplo Neste exemplo, vamos montar uma tabela hash, cuja finalidade seria armazenar identificadores, (por exemplo, nomes de variveis) durante a compilao de um programa. Suporemos que os identificadores so formados de letras e dgitos, e que o tipo da informao sobre cada identificador j foi definido, de forma que dispomos de declaraes
typedef struct info INFO, *PT; struct info { ... };
Estruturas de Dados, J. L. Rangel, 13-4

Suporemos tambm que a tabela deve ter 512 linhas, e que dessas, as primeiras 256 devem ser acessveis atravs da funo hash, ficando as demais para ser usadas em caso de coliso. Vamos apresentar uma funo de hash relativamente simples, mas que permite mostrar alguns dos aspectos que podem (devem) ser levados em considerao. A funo de hash Os caracteres que podem aparecer num identificador so letras e dgitos, cujos cdigos ASCII ocorrem entre 48 e 122. Consultando uma tabela ASCII completa, ou a tabela resumida apresentada na Fig. 1, verificamos que, para todos estes caracteres, o primeiro bit ser sempre 0, e na maioria das vezes, o segundo ser 1. Por essa razo, a funo de hash aqui apresentada ignora estes dois primeiros bits.
Decimal ... 48 49 ... 57 ... 65 66 ... 90 ... 97 98 ... 122 ... Hexadecimal ... 30 31 ... 39 ... 41 42 ... 5A ... 61 62 ... 7A ... Binrio ... 0011 0000 0011 0001 ... 0011 1001 ... 0100 0001 0100 0010 ... 0101 1010 ... 0110 0001 0110 0010 ... 0111 1010 ... Caracter ... 0 1 ... 9 ... A B ... Z ... a b ... z ...

Fig.1 Tabela ASCII resumida Exerccio: Escreva um programa que imprima uma tabela ASCII completa. Como a funo de hash deve fornecer um nmero entre 0 e 255, precisamos de 8 bits. Para no complicar muito, vamos considerar apenas 4 caracteres do identificador, os dois primeiros e os dois ltimos. Ignorados os 2 primeiros bits, cada caracter vai fornecer 6 bits. Com 4 caracteres, teremos 24 bits, ou seja, 3 bytes. O resultado da funo ser o ou exclusivo desses 3 bytes. A idia de usar tambm caracteres do incio e do fim do identificador procura distinguir identificadores como matriz00, matriz01, vetor00, vetor01. Claro que, ainda assim, teramos uma coliso entre vetor00 e vetorx00. Outro valor que pode ser considerado pela funo de hash o comprimento da cadeia. A funo hash pode ser:

Estruturas de Dados, J. L. Rangel, 13-5

char hash(char *s) { char c1,c2=0,c3=0,c4=0; char b1,b2,b3; int l=strlen(s); c1=s[0]; if (l>=2) c2=s[1]; if (l>=3) c3=s[l-1]; if (l>=4) c4=s[l-2]; b1=((c1&63)<<2) | ((c2&48)>>4); b2=((c2&15)<<4) | ((c3&60)>>2); b3=((c3& 3)<<6) | (c4&63); return b1 ^ b2 ^ b3; }

Observaes: 1. para identificadores com menos de 4 caracteres, alguns caracteres so zero. 2. os valores binrios dos inteiros considerados so:
63=00111111 48=00110000 15=00001111 60=00111100 3=00000011

3. & (e) usado para extrair os bits desejados, ainda nas posies originais. 4. os deslocamentos >> e << servem para arrumar os bits retirados de c1, c2, c3 e c4 nas posies desejadas em b1, b2 e b3. 5. | (ou) usado para juntar os bits de vrias procedncias em b1, b2 e b3. 6. ^ (ou exclusivo) usado para reunir os trs bytes b1, b2 e b3 em um nico byte de resultado. 7. podemos eliminar algumas variveis, e escrever
return (((c1&63)<<2) | ((c2&48)>>4)) ^ (((c2&15)<<4) | ((c3&60)>>2)) ^ (((c3& 3)<<6) | (c4&63));

8. naturalmente, tudo isso supe sizeof(char)=1. A estrutura da tabela Como mencionado anteriormente, a tabela ser composta de 512 linhas, sendo as primeiras 256 alcanadas diretamente, atravs da funo de hash, e as demais utilizadas como rea de overflow para tratamento de colises. A tabela tem trs colunas: nome, para os nomes dos identificadores, pinf, que aponta para a informao sobre aquele identificador, e prox, que encadeia a lista usada para tratamento de colises. Uma linha vazia indicada por nome==NULL. Declaramos

Estruturas de Dados, J. L. Rangel, 13-6

struct sts{ char *nome; PT pinf; int prox; }; struct sts ts[512]; int vazio=256;

/* primeira posio vazia */

Para este exemplo, teremos apenas uma tabela de smbolos ts. Para algumas situaes, seria conveniente definir um tipo tabela de smbolos TS, e, assim, usar tabelas ts1, ts2, . Por exemplo, em C, poderamos ter uma tabela para variveis globais e, para cada funo, uma tabela de variveis locais. Para acesso tabela, teremos uma operao
int busca(char *n);

Esta operao devolve como resultado o nmero da linha na tabela que tem o valor n no campo nome. Se esta linha no existir, uma linha para n aberta na tabela. Este tipo de operao interessante quando procuramos um valor em uma tabela, para verificar se j existe, mas, se no existir, a prxima ao ser a insero do valor na tabela. Por exemplo, tratando a declarao de uma varivel x, procuramos inicialmente x na tabela; se encontrarmos, trata-se de um erro varivel x redeclarada; se no encontrarmos, vamos inserir x na tabela com o tipo apropriado. A implementao de busca usa strcmp (compara cadeias) e assert (se condio falhar, aborta):
int busca(char *n) { int h=hash(n),p; if (ts[h].nome==NULL) p=h; else { do { p=h; if (strcmp(ts[h].nome,n)==0) return h; /* achou: nome==n */ h=ts[h].prox; } while (h!=0); h=vazio++; assert(h<512); /* ainda na tabela? */ ts[p].prox=h; } ts[h].nome=n; ts[h].prox=0; ts[h].pinf=NULL; return h; }

O exemplo completo pode ser encontrado nos arquivos


hash.h, hash.c a funo hash info.h, info.c definio da informao associada a um identificador tabhash.h, tabhash.c tabela hash ts e as operaes xhash.c um exemplo de aplicao
Estruturas de Dados, J. L. Rangel, 13-7

Este exemplo procura apenas demonstrar a forma de utilizao de tabelas hash, e, por essa razo, o modelo de tabela de smbolos apresentado extremamente simplificado. Maiores detalhes sobre tabelas de smbolos podem ser vistos na literatura sobre Compiladores.

(junho 1999) Estruturas de Dados, J. L. Rangel, 13-8