LNCC
Parte 2: Estruturas de dados para pesquisa em memória primária
GA-024
Antônio Tadeu A. Gomes, D.Sc.
[email protected] http://wiki.martin.lncc.br/atagomes-cursos-lncc-ga024
Sala 2C-01
LNCC
Estruturas de dados para pesquisa em memória primária
l Primórdios: pesquisa sequencial e binária
l Árvores binárias
l Árvores Trie e Patricia
l Tabelas de dispersão (hash)
LNCC
Primórdios: pesquisa sequencial e binária (I)
l Pesquisa: encontrar em uma estrutura uma ou mais ocorrências de registros de dados com chaves iguais a uma chave de pesquisa
- Ex. de registro:
l Grande quantidade de métodos de pesquisa, cuja eficiência varia conforme:
- Quantidade dos dados envolvidos - Frequência de inserções e retiradas de dados
l Problema associado: ordenação de registros - Estruturas pré-ordenadas podem auxiliar no processo de
pesquisa
struct registry { int id; /* chave do registro */ ... /* outros campos do registro */ } typedef struct registry Registry;
LNCC
Primórdios: pesquisa sequencial e binária (II)
l Pesquisa sequencial: - Método para vetores e listas encadeadas - Método mais simples - Independe da estrutura estar pré-ordenada - Melhor solução para pequenas quantidades de dados
l Tempo médio de pesquisa para n registros: (n+1)/2
l Pesquisa binária: - Método para vetores - Depende da estrutura estar pré-ordenada
l Custo de manutenção da ordenação em vetores é alto - Boa solução para dados estáveis
l Tempo médio de pesquisa para n registros: ≈ log n
LNCC
Primórdios: pesquisa sequencial e binária (III)
/* tamanho do vetor = n */ /* função retorna posição do registro no vetor v, ou -1 se não encontrou */ int binsearch( Registry *v, int id ) { if( n <= 0 ) return -1; int esq = 0; int dir = n-1; int i; do { i = (esq+dir)/2; if( id > v[i].id ) esq = i+1; else dir = i-1; } while( ( id != v[i].id ) && ( esq <= dir ) ); if( id == v[i].id ) return i; return -1; }
l Ex. (pesquisa binária em vetor de registros): - Versão recursiva?
l Ex. (busca de G):
LNCC
Árvores binárias (I)
l Vetores e listas encadeadas: estruturas de dados lineares - Inadequadas para representar hierarquias
l Ex.: diretórios de arquivos - Vários trade-offs a serem considerados entre as duas
estruturas no que se refere a ordenação e pesquisa
l Árvores: estruturas de dados hierárquicas - Boa solução de compromisso para armazenamento:
l Eficiência no acesso direto e sequencial a registros l Eficiência na inserção e retirada de registros l Boa taxa de utilização de memória
LNCC
Árvores binárias (II)
l Árvores: conjunto de nós tal que existe um nó r, denominado raiz, com zero ou mais sub-árvores, cujas raízes estão ligadas a r - os nós raízes destas sub-árvores são os filhos de r - os nós internos da árvore são os nós com filhos - as folhas ou nós externos da árvore são os nós sem
filhos
LNCC
Árvores binárias (III)
l Árvores binárias: - cada nó tem zero, um ou dois filhos
l Definição recursiva – uma árvore binária é:
- uma árvore vazia; ou - um nó raiz com duas sub-árvores:
l a sub-árvore da direita (sad) l a sub-árvore da esquerda (sae)
LNCC
Árvores binárias (IV)
l Propriedade fundamental: - só existe um caminho da raiz para qualquer nó (cf.
grafos)
l Altura de uma árvore: - comprimento do caminho mais
longo da raiz até uma das folhas l a altura de uma árvore com um único
nó raiz é zero l a altura de uma árvore vazia é,
por convenção, -1
- Ex.: altura = 2
LNCC
Árvores binárias (V)
l Nível de um nó - a raiz está no nível 0, seus filhos diretos no nível 1, ... - o último nível da árvore é a altura da árvore
LNCC
Árvores binárias (VI)
l Árvore cheia: - todos os seus nós internos têm duas sub-árvores
associadas - número n de nós de uma árvore cheia de altura h
n = 2h+1 -1
LNCC
Árvores binárias (VII)
l Árvore degenerada: - todos os seus nós internos têm uma única sub-árvore
associada - número n de nós de uma árvore degenerada de altura h
n = h+1
LNCC
Árvores binárias (VIII)
l Esforço computacional necessário para alcançar qualquer nó da árvore - Proporcional à altura da árvore
l Altura de uma árvore binária com n nós:
- mínima: proporcional a log n (caso da árvore cheia) - máxima: proporcional a n (caso da árvore degenerada)
LNCC
Árvores binárias (IX)
l Ex. (avaliação de expressões aritméticas): - nós folhas representam operandos - nós internos representam operadores
- (3+6)*(4-1)+5
l Como avaliar essa expressão?
LNCC
Árvores binárias (X)
l TAD “BTree” em C (elemento ponteiro p/ Registry): - Operações básicas:
l BTree* btree_create(void) l BTree* btree_createfrom(Registry *r, BTree* sae, BTree* sad) l BTree* btree_destroy(BTree* t) l int btree_isempty(BTree* t) l Registry* btree_getelem(BTree* t, int id) l void btree_print(BTree* t)
- Estrutura: struct btree { Registry *info; struct btree* sae; struct btree* sad; }; typedef struct btree BTree;
LNCC
Árvores binárias (XI)
l Operações em geral implementadas recursivamente
BTree* btree_create(void) { return NULL; }
BTree* btree_createfrom(Registry *r, BTree* sae, BTree* sad) { BTree* p=(BTree*)malloc(sizeof(BTree)); p->info = r; p->sae = sae; p->sad = sad; return p; }
BTree* btree_destroy(BTree* t) { if (!btree_isempty(t)) { btree_destroy(t->sae); btree_destroy(t->sad); free(t); } return NULL; }
LNCC
Árvores binárias (XII) l Ex. (criação de árvore):
- Para onde aponta t? - Como pesquisar nessa árvore
(btree_getelem())?
BTree* t = btree_createfrom(a, btree_createfrom(b, btree_create(), btree_createfrom(d, btree_create(), btree_create()) ), btree_createfrom(c, btree_createfrom(e, btree_create(), btree_create()), btree_createfrom(f, btree_create(), btree_create()) ) );
LNCC
Árvores binárias (XIII) l Árvore binária de pesquisa:
- Todos os registros com chaves menores que o da raiz estão na sae e os com chaves maiores na sad
l Tempo médio de pesquisa para n registros: ≈ log n
- Como imprimir os registros da árvore em ordem crescente das chaves (btree_print())?
a
b
c
d
e
f
Registry* btree_getelem(BTree* t, int id) { if( btree_isempty(t) ) return NULL; else if( id < t->info->id ) return btree_getelem( t->sae, id ); else if( id > t->info->id ) return btree_getelem( t->sad, id ); else return t->info; }
LNCC
Árvores binárias (XIV) l Ordens de percurso:
- pré-ordem:
l trata raiz, percorre sae, percorre sad l Ex.: c a b e d f
- ordem simétrica (ou
caminhamento central): l percorre sae, trata raiz, percorre sad l Ex.: a b c d e f
- pós-ordem:
l percorre sae, percorre sad, trata raiz l Ex.: b a d f e c
a
b
c
d
e
f
LNCC
Árvores binárias (XV)
l Inserção/remoção de nós em/de uma árvore binária de pesquisa envolve tratamento especial
l Inserção: - Tempo médio: ≈ log n (!!!)
BTree* btree_insert(BTree* t, Registry* r) { if (btree_isempty(t)) { t = (BTree*)malloc(sizeof(BTree)); t->info = r; t->sae = t->sad = NULL; } else if (r->id < t->info->id) t->sae = btree_insert(t->sae,r); else t->sad = btree_insert(t->sad,r); return t; }
LNCC
Árvores binárias (XVI)
l Remoção: - Segue o mesmo princípio recursivo da inserção, sendo
necessário tratar 3 casos de remoção de nó: 1) nó que é folha 2) nó possui um único filho 3) nó possui dois filhos
l Algoritmo em C: [Celes et al, 2004, pág. 269]
- Tempo médio: > log n, mas << n (!!)
LNCC
Árvores binárias (XVII)
l Remoção – caso 1: nó é folha - libere a memória alocada
LNCC
Árvores binárias (XVIII)
l Remoção – caso 2: nó possui um único filho - libere a memória alocada - o filho do nó removido passa a ser filho do pai desse nó
LNCC
Árvores binárias (XIX) l Remoção – caso 3: nó possui dois filhos
- encontre o nó N que precede o nó removido na ordenação (o elemento mais à direita da sub-árvore à esquerda)
- troque o registro do nó com o registro de N - retire N da sub-árvore à esquerda (que agora contém o
registro do nó que se deseja retirar) l retirar o nó N agora é trivial, pois N é um nó folha ou N é um nó
com um único filho (no caso, o filho da direita nunca existe)
LNCC
Árvores binárias (XX)
l Considerações: - O maior esforço computacional de busca ocorre quando
os registros são inseridos em ordem crescente ou decrescente de chaves
l Neste caso a árvore resultante é uma árvore degenerada
l Árvore binária de pesquisa randômica: - Árvore com n chaves, construída através de n inserções
randômicas sucessivas em uma árvore inicialmente vazia l Nesse caso, número esperado de comparações para recuperar
um registro qualquer é cerca de 1,39 log n l Como conseguir log n comparações (melhor caso)?
LNCC
Árvores binárias (XXI) l Árvore bin. de pesq. completamente balanceada:
- Subárvores vazias aparecem em no máximo dois níveis adjacentes
- Árvore cheia ≠ completamente balanceada! - Minimiza tempo médio de pesquisa para uma distribuição
uniforme das chaves (cada chave igualmente provável de ser usada em uma pesquisa)
- Problema: custo para manter a árvore completamente balanceada após cada inserção é muito alto l Ex. (inserção
de registro com chave 1):
LNCC
Árvores binárias (XXII) l Soluções intermediárias:
- Princípio básico: manter árvore “quase-balanceada”, ao invés de tentar manter a árvore completamente balanceada
- Objetivo: Procurar obter bons tempos de pesquisa, próximos do tempo ótimo da árvore completamente balanceada (≈ log n), mas sem custo muito alto para inserir ou retirar da árvore
- Algumas abordagens: l Árvores AVL (Adelson-Velskki e Landis, 1962) l Árvores 2-3 (Bayer, 1971) l Árvores Rubro-Negras (Guibas e Sedgewick, 1978)
LNCC
Árvores binárias (XXIII)
l Árvores AVL: - Árvore construída de tal modo que a altura de sua
subárvore direita difere da altura da subárvore esquerda de no máximo 1
- Definição recursiva!
LNCC
Árvores binárias (XXIV) l Inserção em árvores AVL:
- Dada uma raiz r com subárvores sae e sad, e supondo que a inserção deve ser feita na sub-árvore sae, pode-se distinguir 3 casos:
l Se h(sae) = h(sad), então sae e sad ficam com alturas diferentes mas continuam balanceadas
l Se h(sae) < h(sad), então sae e sad ficam com alturas iguais e balanceamento foi melhorado (Ex.: inserção dos nós 9 e 11)
l Se h(sae) > h(sad), então sae fica ainda maior e balanceamento foi violado (Ex.: inserção dos nós 3, 5 e 7)
l Remoção em árvores AVL também pode implicar em desbalanceamento (Ex.: remoção do nó 10)
LNCC
Árvores binárias (XXV) l Rebalanceamento de uma árvore AVL após uma
inserção/remoção pode ser feito através de operações de rotação sobre a árvore - Critério: Fator de Balanceamento (FB) dos nós
l Altura da subárvore direita do nó menos a altura da subárvore esquerda do nó
- Árvore está balanceada se FB = -1, 0 ou 1 (Ex.: FB(8) = -1, FB(4) = 0, FB(10) = 0)
- FB deve ser reavaliado (e rotação efetuada, se necessário) antes/após inserção/remoção de nó, de modo ascendente em relação ao ponto de modificação da árvore
LNCC
Árvores binárias (XXVI)
l Rotação: mudança do arranjo topológico da árvore em relação a um nó pivô - Rotação à direita X Rotação à esquerda
l Ex.: inserção do nó 5
Rotação de 4 à esquerda (pivô = 6)
Rotação de 8 à direita (pivô = 6)
LNCC
Árvores binárias (XXVII) l Rebalanceamento
de árvores AVL: - 2 tipos, 4 casos:
- Tipo 1: o nó raiz
de uma subárvore tem FB = +-2 e tem um filho com FB = +-1, com mesmo sinal que FB do nó pai
- Tipo 2: o nó raiz de uma subárvore tem FB = +-2 e tem um filho com FB = +-1, com sinal oposto ao FB do nó pai
LNCC
Árvores binárias (XXVIII) l Árvore com número variável de filhos:
- Representação: “lista de filhos” l Um nó aponta apenas
para seu primeiro filho
l Cada um de seus filhos aponta para o próximo irmão
LNCC
Árvores Trie e Patricia (I) l Algoritmos de pesquisa para registros cujas chaves
são representadas por sequências de caracteres ou de dígitos - Conhecidos como algoritmos de pesquisa digital - Particularmente vantajosos quando as chaves são
grandes e de tamanho variável
l Possibilitam localizar todas as ocorrências de uma determinada chave em um conjunto de registros, com tempo de resposta ≈ log n - Uso em casamento de padrões, construção e consulta de
tabelas de rotas em redes de computadores etc.
LNCC
Árvores Trie e Patricia (II)
l Trie – árvore M-ária cujos: - nós internos são conjuntos (p.ex. vetores) de M
componentes com campos correspondentes aos dígitos ou caracteres que formam as chaves
- nós folhas armazenam chave completa e registro
l Cada nó interno 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
LNCC
Árvores Trie e Patricia (III)
l Considerando as chaves como sequência de bits (M = 2), o algoritmo de pesquisa digital é semelhante ao de pesquisa em árvore binária, exceto que caminha-se na árvore de acordo com os bits de chave - Ex. (chaves de 6 bits)
LNCC
Árvores Trie e Patricia (IV)
l Inserção de novas chaves na Trie binária: - Caso 1: busca pela nova chave interrompida em um nó
interno; cria-se novo nó folha abaixo desse nó interno l Ex.: W = 110110
- Caso 2: busca pela nova chave interrompida em um nó folha; cria-se nós internos cujos descendentes conterão a chave já existente e a nova chave
l Ex.: K = 100010
LNCC
Árvores Trie e Patricia (V)
l O formato das Tries binárias, 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
l Desvantagem: - Formação de árvores degeneradas para chaves com um
grande número de bits em comum l Ex. (caminho gerado
pelas chaves B e C):
LNCC
Árvores Trie e Patricia (VI)
l Patricia (Practical Algorithm to Retrieve Information Coded In Alphanumeric) – problema dos caminhos de uma só direção eliminado através da inserção, em cada nó interno da árvore, do índice do bit a ser testado para decidir qual ramo da árvore tomar - Ex.:
0 1
1 1
1
0
0
0
LNCC
Árvores Trie e Patricia (VII)
0 1
1 1
1
0
0
0
1 0
0
0
0 1
1
1
1 0
0
0 1
1
l Inserção de novas chaves na árvore Patricia: - Caso 1: chave no nó folha de parada no nível i tem mesmo prefixo de
comprimento x ≥ i que nova chave (ex.: K = 100010; i = 3, x = 4)
- Caso 2: chave no nó folha de parada no nível i tem mesmo prefixo de comprimento x < i que nova chave (ex.: W = 110110; i = 5, x = 1)
0 1
1 1
1
0
0
0
1 0
K = 100010
5
0 1
1 1
1
0
0
0
J = 100001
3
LNCC
Tabelas de dispersão (I) l Tabelas “hash” – registros armazenados em uma tabela
(ex.: vetor) são diretamente endereçados a partir de uma transformação aritmética da chave de pesquisa em um índice para a tabela (ex.: posição do registro no vetor)
- Tempo de resposta: ≈ constante (!) - Ex.:
LNCC
Tabelas de dispersão (II)
l TAD “Hash” em C (chave de pesquisa é inteiro e tabela é representada por um vetor de ponteiros): - Estrutura:
- Operações básicas: l Registry* hash_get(Hash tab, int key); l Registry* hash_put(Hash tab, int key, ... /* outras infos */);
struct registry { int key; ... /* informações do registro */ }; typedef struct registry Registry; #define M 193 typedef Registry* Hash[M]; /* lê-se: array com M elementos do tipo ponteiro para Registry!!! */
LNCC
Tabelas de dispersão (III) l Transformação aritmética de duas chaves distintas
pode levar a um mesmo índice: colisão - Assume-se transformações aritméticas como “funções
não injetoras”, senão a tabela teria que ser extremamente grande
- Desse modo, mesmo que se use uma função de transformação que distribua os registros uniformemente entre as entradas da tabela, colisões fatalmente ocorrem e têm de ser resolvidas de alguma forma
l Ex.: chave 26
LNCC
Tabelas de dispersão (IV) l Propriedades desejáveis da função de
transformação: - ser eficientemente avaliada (para tempo de resposta
constante e rápido) - “dispersar” bem as chaves de busca (para minimizar
colisões) – daí “função de dispersão” (ou “função hash”)
l A probabilidade p de se inserir N itens consecutivos sem colisão em uma tabela de tam. M é: - Paradoxo do
aniversário (Feller,1968): l Para N = 23 e M = 365, p ≈ 0.49 l Para N=23 e M=23, p=0,000000001238096 l Para N=37 e M=37, p=0,000000000000001
LNCC
Tabelas de dispersão (V) l Função hash mais comum (método da divisão):
h(K) = K mod M, onde K é a chave e M o tamanho da tabela - Para essa função, recomenda-se que M seja número
primo distante de potências de 2 l Senão, as chaves com padrões de bits/dígitos semelhantes nos
bits/dígitos menos significativos tendem a ser mapeadas para a mesma posição (colisão)
l Há, claro, alternativas: - Método da multiplicação - Funções hash universais
32 64 5364 128 97128 256 193
...
Potência de 2lower bound
Potência de 2upper bound
Número primo
static int hash( int key ) { return key % M; }
LNCC
Tabelas de dispersão (VI)
l Como as transformações sobre chaves são aritméticas, deve-se transformar chaves não-numéricas em números - Ex.:
l n: número de caracteres da chave l chave[i]: código de caractere (ASCII, Unicode etc.) do i-ésimo
caractere da chave l p[i]: pesos gerados aleatoriamente para 0 ≤ i ≤ n – 1
- Dois conjuntos de pesos p1 e p2 geram funções hash h1 e h2 diferentes
LNCC
Tabelas de dispersão (VII) l Independente da qualidade da função hash, taxa de
ocupação da tabela não deve: - ser muito alta (> 75%), para evitar colisões e manter
eficiência da busca
- ser muito baixa (< 25%), para evitar desperdício de memória na tabela
l Estratégias de expansão/redução da tabela podem ser usadas, mas seu custo computacional depende das estratégias de tratamento de colisão adotadas
LNCC
Tabelas de dispersão (VIII)
l Estratégias mais comuns para tratamento de colisão: - Encadeamento separado: cada elemento da tabela
representa um ponteiro para uma lista encadeada, onde os registros são armazenados
- Endereçamento aberto: registros podem ser armazenados diretamente na tabela
- Hash coalescente: estratégia híbrida
LNCC
Tabelas de dispersão (IX) l Encadeamento separado:
- Cada registro armazenado na tabela hash será um nó de
uma lista encadeada
- Registros devem prever um ponteiro adicional para o próximo nó da lista
struct registry { int key; ... /* outras infos */ struct registry* next; }; typedef struct registry Registry;
LNCC
Tabelas de dispersão (X) l Encadeamento separado: Registry* hash_put(Hash tab, int key, ... /* outras infos */) { int h = hash(key); Registry* r = tab[h]; while(r != NULL ) { if(r->key == key) break; r = r->next; } if (r==NULL) { /* não encontrou o elemento */ /* insere novo elemento no início da lista */ r = (Registry*) malloc(sizeof(Registry)); r->key = key; r->next = tab[h]; tab[h] = r; } /* atribui ou modifica informação */ ... return r; }
Registry* hash_get(Hash tab, int key) { int h = hash(key); Registry* r = tab[h]; while(r != NULL ) { if(r->key == key) break; r = r->next; } return r; }
LNCC
Tabelas de dispersão (XI)
l Endereçamento aberto: - Útil quando o número de registros a armazenar na tabela
puder ser previamente estimado
- Hash linear: uso da primeira posição consecutiva livre na tabela hash
- Hash duplo: uso de uma segunda função hash para determinar posição livre
LNCC
Tabelas de dispersão (XII)
l Hash linear: se houver colisão, é procurado o próximo índice livre da tabela (usando incremento circular) para armazenar o novo elemento
LNCC
Tabelas de dispersão (XIII)
l Hash linear: Registry* hash_put(Hash tab, int key, ... /* outras infos */) { int h = hash(key); while (tab[h] != NULL) { if (tab[h]->key == key) break; h = (h+1) % M; } if (tab[h]==NULL) {/* não encontrou o elemento */ tab[h] = (Registry*) malloc(sizeof(Registry)); tab[h]->key = key; } /* atribui/modifica informação */ ... return tab[h]; }
Registry* hash_get(Hash tab, int key) { int h = hash(key); while (tab[h] != NULL) { if (tab[h]->key == key) return tab[h]; h = (h+1) % M; } return NULL; }
LNCC
Tabelas de dispersão (XIV) l Hash linear sofre de um problema chamado agrupamento
(clustering) (Knuth, 1973), que piora à medida que a taxa de ocupação aumenta
l Hash duplo: se houver colisão, é procurada uma posição livre na tabela com incrementos dados por uma segunda função hash h’(k)
- em lugar de tentar-se (h(k)+1) mod M, tenta-se (h(k)+h’(k)) mod M
- Ex.: h'(k) = (M-2) - k mod (M-2)
static int hash2 (int key) { return M - 2 - key%(M-2); } Registry* hash_get(Hash tab, int key) { int h = hash(key); int h2 = hash2(key); while (tab[h] != NULL) { if (tab[h]->key == key) return tab[h]; h = (h+h2) % M; } return NULL; }
LNCC
Tabelas de dispersão (XV)
l Cuidados na escolha da segunda função hash: - Nunca pode retornar zero, pois isso não faria com que o
índice fosse incrementado - No caso de função baseada no método da divisão, não
deve retornar um número divisor da dimensão da tabela, pois isso limitaria a procura de uma posição livre a um subconjunto restrito dos índices da tabela
l Se a dimensão da tabela for um número primo, garante-se automaticamente que o resultado da função não será um divisor
LNCC
Tabelas de dispersão (XVI)
l Hash coalescente: usa uma técnica similar ao encadeamento separado, mas ao invés de alocar novos nós em uma lista encadeada separada por entrada na tabela, são usadas essas próprias entradas como nós no encadeamento - Compromisso entre eficiência na busca e ocupação de
memória
LNCC
Tabelas de dispersão (XVII)
l Hash coalescente: - No exemplo,
procura-se primeira posição livre
- E hash_get()?
Registry* hash_put(Hash tab, int key, ... /* outras infos */) { int h = hash(key); int cursor = 0; if(tab[h] == NULL) { cursor = h; tab[cursor] = (Registry*) malloc(sizeof(Registry)); } else { Registry *r = tab[h]; /* procura próx. entrada livre */ while (cursor < M && tab[cursor] != NULL) ++cursor; /* se tabela está cheia, retorna NULL */ if(cursor == M) return NULL; tab[cursor] = (Registry*) malloc(sizeof(Registry)) /* Procura último nó na lista e aponta para o mesmo */ while(r->next != NULL) r = r->next; r->next = tab[cursor]; } /* atribui/modifica informação na posição “cursor” */ ... return tab[cursor]; }
LNCC
Tabelas de dispersão (XVIII) l Tabelas hash podem ser usadas na representação
de matrizes esparsas: l Basta definir uma função hash(int i, int j, …)
l Problema com tabelas hash:
l Custo computacional potencialmente alto para: l recuperar os registros na ordem lexicográfica das chaves l remover itens da tabela (em particular, nos casos de hash linear,
duplo e coalescente) l Redução da “localidade de referência”
l Dispersão dos registros impede ganhos de desempenho associados ao uso de memória cache (como em operações com matrizes esparsas, p.ex. multiplicação)
LNCC
Tabelas de dispersão (XIX) l Hash perfeito
- Função que permite construção de tabela sem colisões - Possível se o conjunto de chaves for pré-determinado
l Para cada conjunto de chaves, uma função hash perfeita específica é necessária
- Problema passa a ser obter a função hash perfeita! l Complicadores: hash perfeito mínimo (N=M) e com ordem
lexicográfica preservada (k1 < k2 implica h(k1) < h(k2)) l Campo fértil para pesquisas...