INF01203 - Estruturas de Dados - Instituto de Informática (UFRGS) - Prof. Dennis Giovani Balreira



Aula 19 - Tabelas Hash: Funções de Hash

Vimos que árvores binárias de busca oferecem busca em O(log n), e heaps fornecem acesso ao máximo em O(1). Mas é possível buscar um elemento arbitrário em O(1)? Sim — com tabelas hash. A ideia central é usar uma função matemática para mapear uma chave diretamente para um índice do vetor, eliminando comparações por completo. Em vez de percorrer uma árvore ou fazer busca binária, calculamos onde o elemento está e vamos direto ao ponto.


1. A ideia da tabela hash

Uma tabela hash é um vetor de tamanho m. Dada uma função de hash h: U → {0, 1, ..., m−1}, onde U é o universo de chaves possíveis, a chave k é armazenada na posição h(k) do vetor. A busca por k calcula h(k) e acessa diretamente aquela posição — sem percorrer estrutura nenhuma.

Exemplo: m = 7, h(k) = k % 7

Inserindo as chaves 15, 28, 33, 4 e 20:

h(15) = 15 % 7 = 1
h(28) = 28 % 7 = 0
h(33) = 33 % 7 = 5
h( 4) =  4 % 7 = 4
h(20) = 20 % 7 = 6

Tabela (m = 7):
+-------+-------+-------+-------+-------+-------+-------+
| [0]   | [1]   | [2]   | [3]   | [4]   | [5]   | [6]   |
+-------+-------+-------+-------+-------+-------+-------+
|  28   |  15   |  --   |  --   |   4   |  33   |  20   |
+-------+-------+-------+-------+-------+-------+-------+

Busca por 33: calcula h(33) = 5, acessa tabela[5] diretamente. Custo: O(1).

Problema: colisões. Duas chaves diferentes podem produzir o mesmo índice. Por exemplo, se inseríssemos a chave 12 nessa tabela: h(12) = 12 % 7 = 5, que já está ocupado por 33. Esse fenômeno é chamado de colisão e é inevitável sempre que o universo de chaves for maior que m. As técnicas para tratar colisões são tema da Aula 20.

Três grandezas fundamentais:

  • m: tamanho da tabela (número de posições).
  • n: número de elementos armazenados.
  • λ = n/m: fator de carga (load factor) — mede o quão "cheia" está a tabela. Um λ baixo significa poucas colisões; um λ alto aumenta a chance de colisão.

2. Propriedades de uma boa função de hash

Nem toda função que mapeia chaves a índices é útil na prática. As propriedades desejáveis são:

Propriedade Descrição
Determinismo A mesma chave sempre produz o mesmo índice. Sem isso, um elemento inserido não poderia ser encontrado.
Uniformidade As chaves devem ser distribuídas o mais uniformemente possível entre os m slots, minimizando colisões. Idealmente, cada slot recebe aproximadamente n/m elementos.
Eficiência O cálculo deve ser rápido: O(1) para chaves inteiras, O(|chave|) para strings. Uma função lenta anula o ganho da tabela hash.
O que NÃO é necessário: a função de hash não precisa ser invertível. Não precisamos recuperar a chave a partir do índice — só precisamos ir da chave ao índice. Funções de hash criptográficas (SHA-256, por exemplo) adicionam requisitos extras de segurança que não são necessários em estruturas de dados comuns.

3. Hash por divisão (método simples)

h(k) = k mod m

Intuição do módulo

Pense no módulo como um relógio. Um relógio de 12 horas "dá a volta" a cada 12 unidades: depois das 11 vem o 0, depois vem o 1, e assim por diante. O módulo faz exatamente isso: conta de 0 até m−1 e recomeça.

m = 5  →  os possíveis índices são 0, 1, 2, 3, 4

k:    0  1  2  3  4  5  6  7  8  9  10  11  12 ...
h(k): 0  1  2  3  4  0  1  2  3  4   0   1   2 ...
                      ↑ "dá a volta" aqui

O módulo garante que qualquer chave, por maior que seja, produz um índice válido em [0, m−1]. O custo é O(1): uma única divisão inteira.

O problema é que o módulo descarta a parte inteira de k/m e conserva apenas o resto. Se as chaves tiverem padrão regular, o resto também terá — e isso causa colisões sistemáticas.

A escolha de m importa muito

Por que m primo ajuda?

A intuição não é que números primos sejam magicamente melhores. O ponto é mais sutil: quando as chaves seguem um padrão periódico (são múltiplos de algum número d, por exemplo), o conjunto de índices possíveis fica restrito a múltiplos de mdc(d, m). Se mdc(d, m) > 1, muitos slots ficam inacessíveis e as chaves se amontoam nos que sobram.

Um m primo resolve isso: mdc(d, m) = 1 para qualquer d que não seja múltiplo de m, então todos os m slots podem ser atingidos — o padrão das chaves não restringe os índices usados.

Comparação com as mesmas chaves: m = 10 vs m = 11

Chaves: 100, 200, 300, 123, 456, 789, 50

Repare que 100, 200, 300 e 50 são todos múltiplos de 10 — um padrão periódico com d = 10.

m = 10 (composto — mdc(10, 10) = 10):

h(100) = 100 % 10 = 0
h(200) = 200 % 10 = 0   ← colisão
h(300) = 300 % 10 = 0   ← colisão
h(123) = 123 % 10 = 3
h(456) = 456 % 10 = 6
h(789) = 789 % 10 = 9
h( 50) =  50 % 10 = 0   ← colisão

100, 200, 300 e 50 — quatro chaves diferentes, todas para o slot [0].
Os outros 9 slots mal são usados.
Por quê? Todos terminam em 0; k % 10 captura só o último dígito decimal.
m = 11 (primo — mdc(10, 11) = 1):

h(100) = 100 % 11 = 1
h(200) = 200 % 11 = 2
h(300) = 300 % 11 = 3
h(123) = 123 % 11 = 2   ← colisão com 200
h(456) = 456 % 11 = 5
h(789) = 789 % 11 = 8
h( 50) =  50 % 11 = 6

Apenas 1 colisão — e ela não tem nada a ver com o padrão das chaves.
100, 200, 300 e 50 ficaram em slots diferentes: o padrão foi quebrado.
O que mudou? Com m = 10, o padrão "termina em 0" das chaves se alinha perfeitamente com o módulo, restringindo todos esses elementos ao slot 0. Com m = 11, não existe esse alinhamento — 11 não divide 10 nem é divisível por ele, então o padrão das chaves não consegue "encaixar" no tamanho da tabela, e os restos ficam espalhados. Isso é o que mdc(d, m) = 1 garante: nenhum padrão regular nas chaves consegue restringir sistematicamente os slots utilizados.

Exemplo ruim: m = 8 (potência de 2)

Chaves: 8, 16, 24, 32   (múltiplos de 8)
h( 8) =  8 % 8 = 0
h(16) = 16 % 8 = 0   ← colisão
h(24) = 24 % 8 = 0   ← colisão
h(32) = 32 % 8 = 0   ← colisão

Todos vão para [0] — a tabela degenera para uma lista.

Por quê? Em binário:
   8 = 0...01000   últimos 3 bits: 000
  16 = 0...10000   últimos 3 bits: 000
  24 = 0...11000   últimos 3 bits: 000
  32 = 0...100000  últimos 3 bits: 000

k % 8 captura apenas os 3 bits finais — e todos são iguais.
Os bits que diferenciam as chaves (os mais significativos) são ignorados.

4. Hash multiplicativo

O hash por divisão usa apenas o resto de k ÷ m — o que ignora boa parte da informação contida em k. O hash multiplicativo extrai informação de todos os bits da chave, em três etapas:

  1. Multiplica a chave por uma constante A ∈ (0, 1): obtém k · A.
  2. Descarta a parte inteira de k · A, ficando apenas com a parte fracionária frac(k · A) — um número em [0, 1).
  3. Escala para o intervalo [0, m): multiplica por m e arredonda para baixo.

h(k) = ⌊ m · frac(k · A) ⌋

onde frac(x) = x − ⌊x⌋ (parte fracionária).

Por que funciona?

A parte inteira de k · A cresce com k e é descartada — ela carregaria o padrão regular das chaves. A parte fracionária, por outro lado, não segue um padrão simples quando A é irracional: multiplicar por um número irracional "quebra" qualquer regularidade das chaves e espalha os valores por [0, 1) de forma quase uniforme.

Knuth recomenda usar a razão áurea conjugada:

A = (√5 − 1) / 2 ≈ 0,6180339887

Essa constante tem uma propriedade matemática especial: as partes fracionárias de 1·A, 2·A, 3·A, ... se distribuem no intervalo [0,1) de forma maximamente uniforme — cada novo ponto cai no maior espaço ainda vazio entre os pontos anteriores. É a constante que minimiza a chance de colisões sistemáticas.

Vantagem sobre o hash por divisão

O método funciona bem para qualquer valor de m, inclusive potências de 2. No hash por divisão, m = 2p é problemático porque a operação k % 2p considera apenas os p bits menos significativos de k. No hash multiplicativo, a multiplicação por A mistura todos os bits antes de extrair o índice.

Conveniência de m = 2p: quando m é potência de 2, a multiplicação por m é um deslocamento de bits (shift), muito eficiente em hardware. Por isso muitas implementações práticas combinam hash multiplicativo com m = potência de 2.

Exemplo passo a passo: m = 8, A = 0,618, chaves 10, 20, 30

k = 10:
  Passo 1 — multiplica:  10 · 0,618 = 6,18
  Passo 2 — frac:        frac(6,18) = 0,18
  Passo 3 — escala:      ⌊8 · 0,18⌋ = ⌊1,44⌋ = 1   →  h(10) = 1

k = 20:
  Passo 1 — multiplica:  20 · 0,618 = 12,36
  Passo 2 — frac:        frac(12,36) = 0,36
  Passo 3 — escala:      ⌊8 · 0,36⌋ = ⌊2,88⌋ = 2   →  h(20) = 2

k = 30:
  Passo 1 — multiplica:  30 · 0,618 = 18,54
  Passo 2 — frac:        frac(18,54) = 0,54
  Passo 3 — escala:      ⌊8 · 0,54⌋ = ⌊4,32⌋ = 4   →  h(30) = 4

Resultado: 10→1, 20→2, 30→4  (espalhado, sem colisões)

Compare com h(k) = k % 8:
  h(10) = 2,  h(20) = 4,  h(30) = 6  (padrão regular: sempre pares)
  Para k = 8, 16, 24: todos vão para [0] — degeneração.
  O hash multiplicativo não sofre esse problema.

5. Hash para strings — hash polinomial

O problema com strings

Strings não são inteiros — não dá para aplicar divisão ou multiplicação diretamente. A abordagem mais simples seria somar os valores ASCII de cada caractere. Mas somas ignoram a ordem: "gato" e "tago" teriam o mesmo hash, e anagramas colidiriam sempre.

A solução é dar um peso diferente a cada posição. Trata-se a string como um polinômio em que os coeficientes são os valores ASCII e a variável é uma base B:

h(s) = (s[0] · BL-1 + s[1] · BL-2 + ... + s[L-1] · B0) mod m

(L = comprimento da string; s[i] = valor ASCII do i-ésimo caractere; B = base, tipicamente 31)

O primeiro caractere é multiplicado pela maior potência de B — ele tem mais influência no resultado. O último caractere é multiplicado por B0 = 1. Assim, a mesma letra em posições diferentes contribui valores diferentes, e a ordem importa.

A base B é tipicamente 31 (para letras minúsculas) ou 37. Valores primos reduzem colisões entre anagramas.

Regra de Horner — calculando sem potências explícitas

A fórmula acima tem um problema prático: calcular BL-1, BL-2, ... exigiria L exponenciações separadas, e para strings longas as potências crescem sem limite — overflow garantido. A Regra de Horner reorganiza o polinômio algebricamente para evitar isso.

A fórmula geral é a recorrência:

h0 = 0
hi = hi−1 · B + s[i],   i = 1, ..., L

Para ver que é o mesmo polinômio, expanda passo a passo:

h1=s[1]
h2=s[1]·B + s[2]
h3=s[1]·B² + s[2]·B + s[3]
hL=s[1]·BL−1 + s[2]·BL−2 + ··· + s[L]·B0

É exatamente o polinômio original — mas calculado com apenas uma multiplicação e uma adição por passo, sem potências.

Considere uma string de 4 caracteres para ver como a fatoração funciona. O polinômio original:

s[0]·B³ + s[1]·B² + s[2]·B + s[3]

pode ser reescrito fatorando B progressivamente da esquerda para a direita:

((s[0]·B + s[1])·B + s[2])·B + s[3]

São expressões idênticas — experimente expandir os parênteses. O ganho é que agora basta uma multiplicação e uma adição por passo, sem potência alguma. O algoritmo fica:

início:h0 = 0
passo i:hi = hi−1 · B + s[i]
resultado:h(s) = hL mod m

Percorra os caracteres da esquerda para a direita, atualizando h a cada passo. Ao final, aplique mod m. Por segurança, aplique % m em cada iteração — assim os valores intermediários ficam sempre em [0, m−1] e não há risco de overflow mesmo em strings longas.

Exemplo: B = 31, m = 13, hash("gato")

Valores ASCII: 'g'=103, 'a'=97, 't'=116, 'o'=111

h₀ =  0
h₁ = ( 0 * 31 + 103) % 13  →  103 % 13 = 12   (processou 'g')
h₂ = (12 * 31 +  97) % 13  →  469 % 13 =  1   (processou 'a')
h₃ = ( 1 * 31 + 116) % 13  →  147 % 13 =  4   (processou 't')
h₄ = ( 4 * 31 + 111) % 13  →  235 % 13 =  1   (processou 'o')

h("gato") = h₄ = 1

A ordem importa — comparando com o anagrama "tago"

Valores ASCII: 't'=116, 'a'=97, 'g'=103, 'o'=111

h₀ =  0
h₁ = (  0 * 31 + 116) % 13  →  116 % 13 =  7   (processou 't')
h₂ = (  7 * 31 +  97) % 13  →  314 % 13 =  1   (processou 'a')
h₃ = (  1 * 31 + 103) % 13  →  134 % 13 =  4   (processou 'g')
h₄ = (  4 * 31 + 111) % 13  →  235 % 13 =  1   (processou 'o')

h("tago") = h₄ = 1

Neste caso houve colisão — ambas produziram 1.
Isso acontece porque m = 13 é pequeno demais: há apenas 13 slots possíveis
para infinitas strings. Com m primo grande (ex.: 100003) e B = 31,
a chance de dois anagramas distintos colidirem é muito menor.
Por que a soma simples dos ASCII falha? Se usássemos h = (s[0] + s[1] + ... + s[L-1]) % m, qualquer anagrama produziria exatamente o mesmo hash — a soma não depende da ordem. A Regra de Horner resolve isso: cada caractere é multiplicado por uma potência diferente de B antes de ser somado, então a posição de cada letra influencia o resultado de forma distinta.

Custo: O(L) onde L é o comprimento da string — cada caractere é processado exatamente uma vez.


6. Código C das funções de hash

/* Hash por divisão.
   O (+m)%m garante resultado não-negativo para chaves negativas. */
int hashDivisao(int chave, int m) {
    return ((chave % m) + m) % m;
}

/* Hash multiplicativo de Knuth. */
int hashMultiplicativo(int chave, int m) {
    double A    = 0.6180339887;
    double frac = chave * A - (int)(chave * A);
    return (int)(m * frac);
}

/* Hash polinomial para strings (Regra de Horner). */
int hashString(const char *s, int m) {
    int h = 0;
    int B = 31;
    for (int i = 0; s[i] != '\0'; i++)
        h = (h * B + s[i]) % m;
    return h;
}

7. Colisões são inevitáveis

Por melhor que seja a função de hash, colisões não podem ser completamente eliminadas quando o universo de chaves é maior que m. Isso é uma consequência direta do princípio da casa dos pombos: se mais de m chaves distintas precisam ser mapeadas para m posições, alguma posição receberá pelo menos duas chaves.

O problema do aniversário

A situação é ainda mais delicada: a primeira colisão ocorre muito mais cedo do que se imagina. O problema do aniversário mostra que, em um grupo de apenas 23 pessoas, a probabilidade de duas delas compartilharem o mesmo dia de aniversário supera 50% — mesmo havendo 365 dias possíveis.

Para uma tabela de tamanho m, a primeira colisão ocorre com
probabilidade > 50% após aproximadamente √m inserções:

  m = 365   (dias do ano)    → ~23 inserções para >50% de colisão
  m = 1024  (tabela pequena) → ~38 inserções para >50% de colisão
  m = 10000 (tabela média)   → ~118 inserções para >50% de colisão

Mesmo com m = 1.000.000, a primeira colisão é esperada
em torno de ~1177 inserções — muito antes de a tabela
estar cheia.
Funções de hash perfeitas: existem funções que não produzem colisão alguma, mas apenas quando o conjunto de chaves é fixo e conhecido em tempo de compilação. Elas precisam ser construídas especificamente para esse conjunto. Em aplicações dinâmicas (onde chaves chegam em tempo de execução), a colisão é inevitável e deve ser tratada — o que é tema da Aula 20.

8. Complexidade

Operação Melhor caso Caso médio Pior caso
Busca O(1) O(1) O(n)
Inserção O(1) O(1) O(n)
Remoção O(1) O(1) O(n)

O caso médio O(1) assume uma boa função de hash (distribuição uniforme) e fator de carga adequado: λ < 1 para encadeamento separado, ou λ < 0,75 para endereçamento aberto. O pior caso O(n) ocorre quando todas as n chaves colidem no mesmo slot — a tabela degenera para uma lista encadeada. Na prática, com uma boa função de hash e λ controlado, o pior caso raramente acontece.

Comparação com ABP e heap:
  • ABP balanceada: busca, inserção, remoção em O(log n) no caso médio e pior caso.
  • Heap: acesso ao máximo em O(1), inserção e remoção em O(log n), mas não suporta busca por chave arbitrária eficientemente.
  • Tabela hash: busca, inserção, remoção em O(1) no caso médio, mas não mantém os elementos em ordem e não suporta operações de mínimo/máximo eficientemente.
Cada estrutura tem seu domínio: use tabela hash quando precisar de acesso por chave em O(1); use ABP quando precisar de ordem ou operações de faixa; use heap quando precisar de extremo (min/max) repetidamente.

Resumo

  • Tabela hash: vetor de tamanho m com acesso por função h(k). Busca em O(1) no caso médio.
  • Função de hash: deve ser determinística, uniforme e eficiente.
  • Hash por divisão: h(k) = k mod m. Escolher m primo e não próximo de potência de 2.
  • Hash multiplicativo (Knuth): h(k) = ⌊m · frac(k · A)⌋ com A ≈ 0,618. Funciona bem para qualquer m.
  • Hash polinomial para strings: Regra de Horner com base B (tipicamente 31). Custo O(L).
  • Colisões são inevitáveis: pelo princípio da casa dos pombos e pelo problema do aniversário (~√m inserções para >50% de chance).
  • Fator de carga λ = n/m: controla a frequência de colisões. Manter λ baixo é essencial.
  • Pior caso O(n): ocorre quando todas as chaves vão para o mesmo slot. Evitado com boa função de hash.

Exercícios

  1. O que é uma tabela hash e qual é sua principal vantagem em relação a uma árvore binária de busca balanceada?
  2. Dada a função h(k) = k % 11, insira as chaves 44, 33, 77, 55, 22 em uma tabela de tamanho 11. Em qual posição cada chave é armazenada? Há colisão?
  3. Por que m = 16 (potência de 2) é uma escolha ruim para a função h(k) = k % m? Dê um exemplo concreto de um conjunto de chaves que degrada o desempenho.
  4. Uma tabela hash tem m = 100 slots e n = 75 elementos. Qual é o fator de carga? Esse valor é aceitável para endereçamento aberto?
  5. Por que o hash polinomial para strings usa a Regra de Horner em vez de calcular diretamente o polinômio BL-1 · s[0] + BL-2 · s[1] + ...?
  6. As strings "ar" e "ra" deveriam (idealmente) ter hashes diferentes. O hash polinomial com base B garante isso? Por quê?
  7. O que é o problema do aniversário e o que ele implica para tabelas hash?
  8. Uma função de hash sempre retorna 0 para qualquer entrada. Ela é determinística e computada em O(1). Por que ainda assim é uma péssima função de hash?
Sugestão de resposta — Exercício 1

Uma tabela hash é uma estrutura que usa uma função h(k) para mapear uma chave diretamente a uma posição de um vetor, permitindo acesso em O(1) no caso médio. A vantagem principal sobre a ABP balanceada é justamente esse custo: a ABP garante O(log n) para busca, inserção e remoção nos casos médio e pior, enquanto a tabela hash realiza essas operações em O(1) no caso médio. Em troca, a tabela hash não mantém os elementos em ordem e não suporta eficientemente operações como "encontrar o menor elemento" ou "listar todos os elementos em ordem crescente", que a ABP realiza em O(n) com um percurso in-ordem.


Sugestão de resposta — Exercício 2
h(44) = 44 % 11 = 0
h(33) = 33 % 11 = 0   ← COLISÃO com 44!
h(77) = 77 % 11 = 0   ← COLISÃO com 44 e 33!
h(55) = 55 % 11 = 0   ← COLISÃO!
h(22) = 22 % 11 = 0   ← COLISÃO!

Todas as chaves vão para a posição [0].
Isso ocorre porque 44, 33, 77, 55 e 22 são todos múltiplos de 11.
m = 11 é primo, mas as chaves foram escolhidas especificamente para
serem múltiplos de m — ilustrando que nenhuma função garante ausência
de colisões para qualquer conjunto de chaves.

Sugestão de resposta — Exercício 3

Se m = 16 = 24, então h(k) = k % 16 é equivalente a pegar os 4 bits menos significativos de k (em representação binária). Todos os bits mais significativos são ignorados completamente. Por exemplo, as chaves 0, 16, 32, 48, 64, 80, ... (múltiplos de 16) todas têm os 4 últimos bits iguais a zero e vão para a posição [0], causando colisão. Em geral, qualquer conjunto de chaves que diferem apenas nos bits mais significativos — algo muito comum em dados reais como endereços de memória alinhados ou identificadores sequenciais — colide sistematicamente.


Sugestão de resposta — Exercício 4

Fator de carga: λ = n/m = 75/100 = 0,75.

Para endereçamento aberto, λ = 0,75 está exatamente no limite aceitável — muitos autores e implementações práticas (como a tabela hash do Python e de Java) usam 0,75 como threshold para redimensionamento. Acima desse valor, o desempenho começa a degradar significativamente: a probabilidade de colisão aumenta, e a busca por uma posição livre pode exigir muitas sondagens. Para encadeamento separado (chaining), λ = 0,75 ainda é bastante confortável, pois cada slot pode ter múltiplos elementos e a busca ainda é O(1) no caso médio.


Sugestão de resposta — Exercício 5

Calcular o polinômio diretamente exigiria computar as potências B0, B1, ..., BL-1 explicitamente. Para uma string longa, BL-1 seria um número gigantesco que não cabe em um inteiro padrão, exigindo aritmética de precisão arbitrária (lenta) ou causando overflow silencioso. A Regra de Horner reescreve o polinômio como ((s[0] · B + s[1]) · B + s[2]) · B + ..., processando um caractere por vez com apenas uma multiplicação e uma adição por passo, sem jamais computar potências explicitamente. Aplicar o módulo a cada passo mantém os valores intermediários pequenos e evita overflow, sem alterar o resultado final.


Sugestão de resposta — Exercício 6

Sim, o hash polinomial geralmente diferencia "ar" e "ra", e isso se deve à natureza posicional do polinômio. Para "ar": h = (ord('a') · B + ord('r')) % m. Para "ra": h = (ord('r') · B + ord('a')) % m. Como ord('a') ≠ ord('r'), as duas expressões são diferentes (assumindo que B e m não criam uma coincidência numérica específica). Em geral, o hash polinomial leva em conta a posição de cada caractere: o mesmo conjunto de caracteres em ordem diferente produz hashes diferentes — ao contrário de uma soma simples dos valores ASCII, que seria idêntica para qualquer anagrama. Entretanto, com valores específicos de B e m, anagramas ainda podem colidir (como vimos com "gato"/"tago" no exemplo da aula); isso é tratado escolhendo B e m maiores e primos.


Sugestão de resposta — Exercício 7

O problema do aniversário pergunta: em um grupo de quantas pessoas a probabilidade de pelo menos duas compartilharem o mesmo aniversário supera 50%? A resposta surpreendente é apenas 23, mesmo havendo 365 dias possíveis. Para uma tabela hash de tamanho m, o análogo é: após quantas inserções a probabilidade da primeira colisão supera 50%? A resposta é aproximadamente √m. Isso tem implicação direta: mesmo com uma função de hash perfeitamente uniforme, a primeira colisão acontece muito antes de a tabela estar cheia. Com m = 1024, a primeira colisão é esperada por volta da 38ª inserção — quando a tabela está apenas ~3,7% cheia. Portanto, qualquer implementação de tabela hash precisa tratar colisões desde o início, não como um caso excepcional.


Sugestão de resposta — Exercício 8

Uma função que sempre retorna 0 é determinística e O(1), mas viola completamente a propriedade de uniformidade. Todos os n elementos seriam inseridos no slot [0], e todos os outros m−1 slots ficariam vazios. A tabela degeneraria para uma lista encadeada simples: busca, inserção e remoção custariam O(n) no pior caso, eliminando toda a vantagem da tabela hash. Na prática, essa função torna o fator de carga efetivo do slot 0 igual a n — exatamente o que se quer evitar. Uma boa função de hash distribui os elementos de forma que cada slot receba aproximadamente n/m elementos, mantendo o custo amortizado em O(1).