Dicionários e Tabelas de Dispersão
Igor Machado Coelho
03/11/2026
São requisitos para essa aula:
Agradecimentos especiais ao prof. Fabiano Oliveira, cujo material sobre Tabelas de Dispersão forma a base desses slides.
O Dicionário (do inglês Dictionary) ou Mapa (do inglês Map) é um Tipo Abstrato de Dado (TAD) que associa chaves a valores. Também é conhecido como mapeamento ou tabela de símbolos.
Supondo um dicionário M de caractere para
float, por exemplo:
B com valor
100.0, e a chave C com valor
150.0B, recebendo o valor
100.0B para
120.0 (adicionando novamente!)CM: B -> 120.0
C -> 150.0 (removida)
Cada chave aparece no máximo uma vez: é como um Conjunto de chaves, cada uma com um valor associado.
Dicionários são estruturas fundamentais na própria computação. Algumas linguagens (como Python) oferecem suporte nativo:
dict (tabela de
dispersão)HashMap (tabela de dispersão) e
TreeMap (árvore balanceada)std::unordered_map (tabela de
dispersão) e std::map (árvore balanceada)Assim como vetores, guardam dados de um mesmo tipo. A diferença é que o índice (a chave) pode ser de um tipo arbitrário: um caractere, um número grande, um nome…
Um Dicionário possui 5 operações básicas:
tem(c): a chave c existe? (do inglês
contains)consulta(c): retorna o valor associado à chave
c, que deve existir (do inglês
at)adiciona(c, v): associa o valor v à chave
c, inserindo ou atualizando (do inglês
put)remove(c): remove a chave c, se existir, e
diz se removeu (do inglês remove)tamanho(): número de chavesComo no Conjunto, operações que podem não fazer nada devolvem
bool.
O conceito precisa de dois tipos: o da chave
K e o do valor V.
// Exemplo de uso
DicionarioTAD<char, float> auto d = ...;
d.cria();
d.adiciona('A', 100.0);
d.adiciona('B', 200.0);
d.adiciona('A', 150.0); // atualiza!
println("{}", d.consulta('A')); // 150
println("{}", d.tem('C')); // false
println("{}", d.remove('B')); // true
println("{}", d.tamanho()); // 1
d.libera();Cuidado: consulta(c) tem uma
pré-condição: tem(c) deve ser verdadeiro.
Em C++26: pre(tem(c)).
Já conhecemos algumas opções, com n chaves:
| Implementação | adiciona(c,v) |
consulta(c) |
remove(c) |
|---|---|---|---|
| vetor não ordenado (como o Saco) | O(n) (verifica se existe!) | O(n) | O(n) |
| vetor ordenado (como o Conjunto Ordenado) | O(n) | O(logn) | O(n) |
| árvore balanceada (próxima aula) | O(logn) | O(logn) | O(logn) |
| “estrutura dos sonhos” | O(1) | O(1) | O(1) |
Existe a “estrutura dos sonhos”? Quase! Veremos hoje as Tabelas de Dispersão (do inglês hash tables), com custo O(1) em média.
Se as chaves são pequenas, por exemplo um char (256
valores possíveis), basta um vetor com uma posição por chave
possível:
chave: ... 'A' 'B' 'C' 'D' ...
valores: ... 100.0 ---- 150.0 ---- ...
ocupado: ... true false true false ...
O valor da chave c fica em valores[c]. Não
há busca: o acesso é direto!
Essa é uma Tabela de Acesso Direto: todas as operações custam O(1), e o espaço é O(M), onde M é o número de chaves possíveis (não as presentes!).
float consulta(char c) {
return valores[c]; // pre: tem(c)
}
void adiciona(char c, float v) {
if (!ocupado[c]) { ocupado[c] = true; N++; }
valores[c] = v;
}
bool remove(char c) {
if (!ocupado[c]) return false;
ocupado[c] = false; N--;
return true;
}
};
static_assert(
DicionarioTAD<DicionarioDireto,
char, float>);Desafio: o código tem erro com
caracteres negativos! Explore unsigned char.
char, dias do mês, notas de 0 a 10…Exemplo: duas palavras são anagramas se uma
é um rearranjo das letras da outra. Basta contar as
letras de cada uma em duas tabelas de acesso direto (int
para cada char) e comparar: tempo
O(n)!
Mas e se as chaves forem números de 32 bits? Seriam
M=232
posições (16 GB para float), mesmo com só 10 chaves
presentes!
E se forem nomes? O número de chaves possíveis é praticamente infinito…
Solução: usar um vetor pequeno e uma função que espalhe as chaves nele: uma Tabela de Dispersão!
Uma Tabela de Dispersão (do inglês Hash Table) usa
um vetor H de tamanho
M
e uma função de dispersão
h
que leva cada chave a uma posição de
0
a
M−1:
a chave c fica na posic¸a˜o H[h(c)]
Exemplo com M=10 e h(c)=cmod10:
chaves: 100 30 1 49 11 36 644 77 18
H: | 100,30 | 1,11 | | | 644 | | 36 | 77 | 18 | 49 |
0 1 2 3 4 5 6 7 8 9
Note que h(100)=h(30)=0, e h(1)=h(11)=1: são colisões!
Toda tabela de dispersão precisa decidir: (1) qual função de dispersão usar e (2) como tratar as colisões.
Uma boa função de dispersão deve ser:
Método da Divisão: h(c)=cmodM. É fácil de calcular, mas nem sempre uniforme:
Para outros tipos de chave (textos, por exemplo), primeiro
transformamos a chave em um número. Em C++, std::hash faz
isso: std::hash<std::string>{}("ana").
Desafio: ver outras técnicas, como método da dobra.
Colisões são inevitáveis: se há mais chaves possíveis que posições, alguma posição recebe mais de uma chave (princípio da casa dos pombos).
O fator de carga mede quão cheia a tabela está:
α=Mn(chaves por posic¸a˜o, em meˊdia)
Duas famílias de tratamento de colisões:
std::unordered_map e no HashMap do Javadict do PythonCada posição H[i] é uma lista encadeada
com as chaves
c
tais que
h(c)=i:
H[0] -> [100] -> [30] -> 0
H[1] -> [ 11] -> [ 1] -> 0
H[2] -> 0
...
H[9] -> [ 49] -> 0
Todas as operações começam calculando
i=h(c)
e depois trabalham só na lista H[i]:
tem e consulta: percorre a lista
H[i] procurando
cadiciona: se
c
já está na lista, atualiza o valor; senão, insere no
início da lista (como no Saco!)remove: procura
c
na lista e o desligastruct NoHash {
int chave;
float valor;
NoHash* proximo;
};
constexpr int M = 13; // primo!
struct DicionarioHash {
NoHash* H[M]; // uma lista por posição
int N; // num. de chaves
int h(int c) { return (c % M + M) % M; }
void cria() {
N = 0;
for (int i = 0; i < M; i++) H[i] = 0;
}
int tamanho() { return N; }
...
};Desafio: por que (c % M + M) % M, e não
só c % M? (Dica: chaves negativas!)
O libera() percorre as
M
listas liberando os nós, como no Saco. Exercício!
O custo de cada operação é: calcular h(c), em O(1), mais percorrer uma lista.
Se mantivermos M proporcional a n (por exemplo, α≤1), temos α=O(1) e:
todas as operac¸o˜es custam O(1) em meˊdia
Atenção: é um custo médio, que depende de a função de dispersão espalhar bem as chaves. Uma escolha ruim (ou um adversário que conhece a função!) pode levar ao pior caso.
Se as chaves continuarem chegando, α cresce e as listas ficam longas. A solução é a mesma do Conjunto Dinâmico:
Reinserir custa O(n), mas, como dobramos o tamanho, isso acontece raramente: o custo amortizado da inserção continua O(1) (mesmo argumento do vetor dinâmico!).
Na STL, std::unordered_map faz isso automaticamente:
max_load_factor() é
1,0
por padrão.
Desafio: implemente o DicionarioHash
com H alocado dinamicamente e redimensionamento.
E se não quisermos listas (nem ponteiros)? No endereçamento aberto, todas as chaves ficam no próprio vetor, e α≤1.
Em caso de colisão, tentamos outras posições, em uma ordem definida por h(c,k), onde k=0,1,2,… é a tentativa:
A tentativa linear é a mais simples, mas forma agrupamentos: quanto maior um bloco ocupado, mais chaves caem nele, e mais ele cresce. As outras espalham melhor.
h(c,k)=(h(c)+k)modMk=0,1,2,…
Em caso de colisão, tenta a próxima posição. Exemplo
com
M=11,
h(c)=cmod11,
inserindo 22, 33, 44 e
1:
22: h = 0, tenta 0 -> posição 0
33: h = 0, tenta 0, 1 -> posição 1
44: h = 0, tenta 0, 1, 2 -> posição 2
1: h = 1, tenta 1, 2, 3 -> posição 3 (nem colidiu com 22, 33, 44!)
H: | 22 | 33 | 44 | 1 | | | | | | | |
0 1 2 3 4 5 6 7 8 9 10
Agrupamento primário: chaves de posições diferentes passam a disputar o mesmo bloco. Quanto maior o bloco, maior a chance de uma nova chave cair nele, e mais ele cresce!
Vantagem: visita todas as M posições, e percorre memória contígua (ótimo para cache).
h(c,k)=(h(c)+k2)modM
Com as mesmas chaves, os saltos crescem (+1, +4, +9, …) e o bloco se desfaz:
33: h = 0, tenta 0, 1 -> posição 1
44: h = 0, tenta 0, 1, 4 -> posição 4
1: h = 1, tenta 1, 2 -> posição 2
H: | 22 | 33 | 1 | | 44 | | | | | | |
0 1 2 3 4 5 6 7 8 9 10
Cuidado: a sequência pode não visitar todas as posições! Com M=11, k2mod11 só alcança 6 posições. Com M primo, as primeiras (M+1)/2 tentativas são distintas: mantendo α≤1/2, sempre há lugar.
Ainda resta o agrupamento secundário: chaves com o mesmo h(c) fazem a mesma sequência de saltos.
Dispersão dupla:
h(c,k)=(h1(c)+k⋅h2(c))modM,
em que o tamanho do salto depende da chave. Com
h2(c)=7−(cmod7),
33 salta de 2 em 2 e 44 de 5 em 5, mesmo
partindo da mesma posição! Com
M
primo e
h2(c)=0,
visita todas as posições.
Considere h(a)=h(b)=2, com tentativa linear: a ocupa a posição 2, e b a posição 3.
inserir a, b: | . | . | a | b | . | remover a: | . | . | ? | b | . |
0 1 2 3 4 0 1 2 3 4
Se a posição 2 voltar a ficar vazia, a busca por b começa em 2, vê uma posição vazia e para: b “some”!
Solução: marcar a posição como removida (uma lápide). Cada posição tem um de três estados:
VAZIO: a busca para aquiOCUPADO: compara a chaveREMOVIDO: a busca continua, mas a
inserção pode reaproveitar a posiçãoenum Estado { VAZIO, OCUPADO, REMOVIDO };
struct DicionarioAberto {
int chaves[M]; float valores[M];
Estado estado[M];
int N;
// posição de c, ou -1 se não existe
int posicao(int c) {
for (int k = 0; k < M; k++) {
int i = (h(c) + k) % M; // linear
if (estado[i] == VAZIO) return -1;
if (estado[i] == OCUPADO &&
chaves[i] == c) return i;
} // REMOVIDO: continua procurando!
return -1;
}
...
};void adiciona(int c, float v) { // pre: N < M
int p = posicao(c);
if (p != -1) { valores[p] = v; return; }
for (int k = 0; k < M; k++) {
int i = (h(c) + k) % M;
if (estado[i] != OCUPADO) { // reaproveita
chaves[i] = c; valores[i] = v;
estado[i] = OCUPADO; N++;
return;
}
}
}
bool remove(int c) {
int p = posicao(c);
if (p == -1) return false;
estado[p] = REMOVIDO; N--; // lápide
return true;
}| Encadeamento Exterior | Endereçamento Aberto | |
|---|---|---|
| Onde ficam as colisões | listas fora do vetor | no próprio vetor |
| Fator de carga α | pode passar de 1 | sempre α≤1 |
| Memória extra | um ponteiro por chave | nenhuma (mas sobra espaço livre) |
| Remoção | simples (desliga da lista) | precisa de lápides |
| Desempenho com α alto | degrada suavemente | degrada rapidamente |
| Uso de cache | ruim (nós espalhados) | ótimo (memória contígua) |
Ambos têm custo médio O(1) com α limitado, e pior caso O(n).
Curiosidade: existe ainda o encadeamento interior, um meio-termo em que as listas usam posições do próprio vetor.
As tabelas de dispersão espalham as chaves de
propósito: chaves vizinhas, como 100 e 101,
vão para posições sem nenhuma relação.
Por isso, perguntas sobre a ordem das chaves ficam caras:
10 e 20?
O(M+n)Lembra do Saco Ordenado? Lá, essas perguntas
custavam
O(logn),
com buscaMaiorIgual e buscaMaior, mas inserir
custava
O(n)…
Para onde vamos: nas próximas aulas, as Árvores Balanceadas darão O(logn) para tudo, e com elas construiremos o Dicionário Ordenado, que responde às perguntas de ordem.
import std;
int main() {
std::unordered_map<std::string, int> idade; // tabela de dispersão
idade["ana"] = 20; // adiciona
idade["bia"] = 22;
idade["ana"] = 21; // atualiza
println("{}", idade.contains("ana")); // true
println("{}", idade.at("ana")); // 21
idade.erase("bia"); // remove
println("{}", idade.size()); // 1
std::map<std::string, int> ordenado; // árvore balanceada
// mesmas operações, mas percorre as chaves em ordem!
return 0;
}std::unordered_map |
std::map |
|
|---|---|---|
| Implementação | tabela de dispersão | árvore rubro-negra |
| Busca, inserção, remoção | O(1) em média | O(logn) no pior caso |
| Percurso em ordem | não | sim |
(Adaptados do material do prof. Fabiano Oliveira.)
libera() do DicionarioHash, e
uma versão do DicionarioAberto com tentativa
quadrática.Desafio: no exercício 2, como resolver em O(nlogn) sem tabela de dispersão? E em O(n), se o vetor já estiver ordenado?
Além da bibliografia do curso, recomendamos para esse tópico:
Bibliografia Adicional:
Em especial, agradeço aos colegas que elaboraram bons materiais, como o prof. Fabiano Oliveira (IME-UERJ), cujo material de Tabelas de Dispersão forma a base desta aula, e o prof. Jayme Szwarcfiter cujos conceitos formam o cerne desses slides.
Estendo os agradecimentos aos demais colegas que colaboraram com a elaboração do material do curso de Pesquisa Operacional, que abriu caminho para verificação prática dessa tecnologia de slides.
Esse material de curso só é possível graças aos inúmeros projetos de código-aberto que são necessários a ele, incluindo:
Agradecimento especial a empresas que suportam projetos livres envolvidos nesse curso:
Esses slides foram escritos utilizando pandoc, segundo o tutorial ilectures:
Exceto expressamente mencionado (com as devidas ressalvas ao material cedido por colegas), a licença será Creative Commons.
Licença: CC-BY 4.0 2020
Igor Machado Coelho