Tabelas de dispersão Vamos considerar um arquivo de dados que armazena uma lista de alunos. Cada registro é um objeto com um número de matrícula e um nome. A tabela está sujeita a dois tipos de operação: inserção; busca. A dificuldade está em organizar a tabela de maneira que ambas as operações sejam eficientes. Em geral, uma organização que permite inserções rápidas impede buscas rápidas e vice-versa. Um registro de alunos poderia ser representado assim: typedef struct { int chave; char *nome; Aluno; A chave é o número de matrícula do aluno. As funções de inserção e busca nesse caso poderiam ter os seguintes protótipos: void insert(aluno obj) ; Aluno search(int v) ; Tipicamente, a implementação de uma estrutura de dados para conter essas informações é feita por uma tabela de símbolos ou dicionário. Tabela de símbolos ou dicionário É um conjunto de objetos dotados de uma chave (= key). As chaves podem ser qualquer tipo de dados básico que permita comparação: dadas duas chaves ch1 e ch2, deve ser possível dizer se ch1 < ch2 ou ch1 == ch2 ou ch1 > ch2. A tabela de símbolos está sujeita a dois tipos de operação: inserção (= insert = enter), que consiste em introduzir um objeto na tabela; busca (= search = lookup), que consiste em encontrar um objeto que tenha uma dada chave. Às vezes é conveniente admitir também uma operação de remoção (= delete), que consiste em retirar da tabela um ou todos os objetos que tenham uma dada chave. A dificuldade está em organizar a tabela de símbolos de maneira que ambas as operações sejam razoavelmente eficientes. Em geral, uma organização que permite inserções rápidas impede buscas rápidas e vice-versa. Implementações Vetor Lista ligada Endereçamento direto Vetor Aluno objetonulo; objetonulo.chave = 0; Aluno tabela[1000]; int N = 0; void insert(aluno obj) { tabela[n++] = obj;
Aluno search(int v) { int i; for (i = 0; i < N; i++) if (tabela[i].chave == v) break; if (i < N) return tabela[i]; Lista ligada typedef struct No *link; struct No { Aluno obj; link next; ; Aluno search(int v) { link t; for (t = lista; t!= NULL; t->next) if (t->obj.chave == v) break; if (t!= NULL) return t->obj; Essa solução não é muito boa porque as buscas são muito demoradas: elas consomem tempo proprocional a N no pior caso. Se usássemos a estrutura ordenada, o tempo de busca seria reduzido para lg(n) mas o tempo de inserção subiria para N no pior caso. Endereçamento direto A característica mais marcante dessa implementação é o uso de chaves como índices. Com endereçamento direto, as operações de inserção e busca ficam muito rápidas: elas não mais dependem do número de objetos na tabela. Os maiores defeitos dessa implementação são: Não é possível ter dois objetos diferentes com a mesma chave: isso causaria uma "colisão" na tabela (somente a inserção mais recente seria registrada); Despedício de espaço: provavelmente a maior parte da tabela ficaria vazia. Aluno tabela[n]; void insert(tipoobjeto obj) { int v = obj.chave; tabela[h] = obj; Se a tabela de símbolos tem muitos objetos é preciso recorrer a implementações mais sofisticadas como árvores de busca e tabelas de dispersão. Tabela de dispersão Tabela de dispersão ou tabela hash, é um vetor de tamanho fixo em que os elementos são colocados em uma posição determinada por um algoritmo denominado função de dispersão ou função hash. Comportamento O comportamento das tabelas de dispersão é caracterizado por: função de dispersão técnica de resolução de colisões
A característica mais marcante dessa implementação é o uso de chaves como índices. int h = funcaohash(v); int funcaohash(v) { return v - min; História Os estudos sobre tabelas de disporsão surgiram independentemente em vários momentos e locais. H. P. Luhn, em 1953, teve a idéia enquanto trabalhava na IBM G. N. Amdahl, E. M. Boehme, N. Rochester, e Arthur Samuel implementaram um programa usando tabelas de dispersão na mesma época Função hash Uma função hash é uma função matemática que transforma os dados de uma chave em um inteiro relativamente pequeno, chamado de valor de hash ou hasher. O valor de hash é normalmente usado como um índice para um vetor. A função de dispersão envolve o comprimento da tabela para assegurar que os resultados estão dentro da gama pretendida. int hash (const string & key, int tablesize) { int hashval = 0; for ( int i = 0; i < key ; i++ ) hashval = 37*hashVal + key[i]; hashval %= tablesize; if (hashval < 0 ) hashval += tablesize; return hashval; int hash (int key, int tablesize) { if ( key < 0 ) key = -key; return key%tablesize; Características da função hash A função hash deve: ser fácil de calcular ser determinista: duas entradas idênticas geram o mesmo valor de hash distribuir os objetos uniformemente pela tabela Toda tabela de símbolos tem um universo de chaves, que é o conjunto de todas as possíveis chaves. Qualquer função que leva qualquer chave no intervalo [0..M-1] de índices serve como função de dispersão. Vantagens A principal vantagem de tabelas hash sobre outras estruturas de dados é a velocidade. O tempo médio de busca é constante. Esta vantagem é mais evidente quando o número de entradas é grande, especialmente quando o número máximo de entradas pode ser previsto com antecedência e nunca redimensionada. Aplicação Tabelas hash são tipicamente utilizadas para implementar vetores ou matrizes associativos, conjuntos e caches. Comumente utilizadas para indexação de grandes volumes de dados.
Colisões A função perfeita seria a que, para quaisquer entradas A e B, fornecesse saídas diferentes. Quando as entradas A e B, passando pela função de hash, geram a mesma saída, acontece uma colisão. Exemplo de colisão A função de dispersão F(x) = lenght(x) % 10 é uma má função de dispersão, porque tem grandes probabilidades de levar a muitas colisões. Solução para colisões Separate chaining Linear probing Indexação perfeita Separate chaining Uma solução popular para resolver colisões é conhecida como separate chaining: para cada índice h da tabela há uma lista encadeada que armazena todos os objetos que a função de dispersão leva em h. Essa solução é muito boa se cada uma das "listas de colisão" resultar curta. Por causa das colisões, muitas tabelas hash são aliadas a outras estruturas de dados, como uma lista ligada ou árvores B+. Linear probing Um outro método de resolução de colisões é conhecido como linear probing. Todos os objetos são armazenados em um vetor tab[0..m-1]. Quando ocorre uma colisão, procuramos a próxima posição vaga do vetor. void STinsert(tipoObjeto obj) { int v = obj.chave; h = (h + 1) % M; tab[h] = obj; tipoobjeto STsearch(int v) { if (tab[h].chave == v) return tab[h]; else h = (h + 1) % M; tipoobjeto STsearch(int v) { if (tab[h].chave == v) return tab[h]; else h = (h + 1) % M; Os maiores defeitos dessa implementação são: não poder ter dois objetos diferentes com a mesma chave: isso causaria uma "colisão" na tabela; despedício de espaço: provavelmente a maior parte dos números que não correspondem a alunos ou ex-alunos reais ficaria vazia na tabela.
Indexação perfeita Com um conjunto fixo de registros, uma função de espalhamento perfeita pode ser criada para que indexe os itens sem que ocorra uma colisão. Desafios Tabelas hash não são eficientes para conjuntos de dados pequenos; A criação de uma função hash eficiente é custosa; Requer recriação da tabela hash se os dados aumentarem; Não permite recuperar elementos sequencialmente;