O hashing de strings e uma tecnica fundamental que permite comparar strings e substrings em tempo constante O(1) apos um pre-processamento linear. Em vez de comparar caractere a caractere (O(n)), calculamos um valor numerico (hash) que representa a string e comparamos os numeros. Embora colisoes sejam possiveis, com boas escolhas de parametros a probabilidade e tao baixa que a tecnica e extremamente confiavel na pratica.
As aplicacoes sao vastas: busca de padroes (Rabin-Karp), comparacao de substrings em O(1) para programacao dinamica, deteccao de palindromos, deduplicacao de strings, deteccao de anagramas e verificacao de integridade de dados. Nesta pagina, implementaremos hash polinomial com pre-processamento de prefixos, rolling hash e tecnicas para minimizar colisoes.
Como Funciona
Hash Polinomial
Tratamos cada string como um numero em uma base numerica, onde cada caractere e um “digito”:
hash(s) = s[0]*base^(n-1) + s[1]*base^(n-2) + ... + s[n-1]*base^0 (mod primo)
Exemplo: hash("abc") com base=31, primo=1000000007
hash = 97*31^2 + 98*31 + 99
= 97*961 + 98*31 + 99
= 93217 + 3038 + 99
= 96354
Na pratica usamos aritmetica modular para evitar overflow:
hash = ((97 * 31 + 98) * 31 + 99) mod 1000000007
= ((3007 + 98) * 31 + 99) mod 1000000007
= (3105 * 31 + 99) mod 1000000007
= (96255 + 99) mod 1000000007
= 96354
Pre-processamento de Prefixos
Para comparar substrings em O(1), pre-calculamos o hash de cada prefixo:
String: "abcde"
0 1 2 3 4
prefix_hash[0] = hash("") = 0
prefix_hash[1] = hash("a") = 97
prefix_hash[2] = hash("ab") = 97*31 + 98 = 3105
prefix_hash[3] = hash("abc") = 3105*31 + 99 = 96354
prefix_hash[4] = hash("abcd") = 96354*31 + 100 = 2986974
prefix_hash[5] = hash("abcde") = 2986974*31 + 101 = 92596295
Para extrair hash("bcd") = hash de s[1..4]:
hash("bcd") = prefix_hash[4] - prefix_hash[1] * base^3
= 2986974 - 97 * 29791
= 2986974 - 2889727
= 97247
Verificacao direta:
hash("bcd") = 98*31^2 + 99*31 + 100 = 98*961 + 3069 + 100 = 97247 <<<
Rolling Hash (Hash Deslizante)
Ao deslizar uma janela pelo texto, atualizamos o hash em O(1):
Texto: "abcdef" janela de tamanho 3
Janela "abc": hash = 97*31^2 + 98*31 + 99 = 96354
^sai ^entra
Janela "bcd": hash = (96354 - 97*31^2) * 31 + 100
remover 'a' deslizar adicionar 'd'
= (96354 - 93217) * 31 + 100
= 3137 * 31 + 100
= 97347
Visualizacao:
[a b c] d e f hash = H1
a [b c d] e f hash = (H1 - a*base^2) * base + d = H2
a b [c d e] f hash = (H2 - b*base^2) * base + e = H3
a b c [d e f] hash = (H3 - c*base^2) * base + f = H4
Complexidade
| Operacao | Tempo | Espaco |
|---|---|---|
| Pre-processamento (prefixos) | O(n) | O(n) |
| Hash de substring s[l..r] | O(1) | O(1) |
| Comparacao de substrings | O(1) | O(1) |
| Rolling hash (atualizar) | O(1) | O(1) |
| Construir hash completo | O(n) | O(1) |
Comparacao: verificar igualdade de duas substrings sem hashing custa O(min(len1, len2)). Com hashing, a comparacao e O(1) com probabilidade de colisao ~1/primo.
Implementacao em Rust
/// Constantes para hash polinomial.
/// Base prima e modulo primo grande para minimizar colisoes.
const BASE: u64 = 31;
const MOD: u64 = 1_000_000_007;
/// Estrutura para hashing de strings com pre-processamento de prefixos.
/// Permite consultas de hash de substrings em O(1).
struct StringHasher {
prefix_hash: Vec<u64>, // hash de cada prefixo
potencias: Vec<u64>, // base^i mod MOD pre-calculadas
n: usize,
}
impl StringHasher {
/// Constroi o hasher a partir de uma string em O(n).
fn novo(texto: &str) -> Self {
let bytes = texto.as_bytes();
let n = bytes.len();
let mut prefix_hash = vec![0u64; n + 1];
let mut potencias = vec![1u64; n + 1];
// Pre-calcula potencias de base
for i in 1..=n {
potencias[i] = potencias[i - 1].wrapping_mul(BASE) % MOD;
}
// Pre-calcula hashes de prefixos
for i in 0..n {
// Valor do caractere: usamos (byte - b'a' + 1) para evitar hash 0
// para strings de 'a'. Se a string pode conter qualquer byte, use byte + 1.
let valor = (bytes[i] as u64).wrapping_add(1);
prefix_hash[i + 1] = (prefix_hash[i].wrapping_mul(BASE) + valor) % MOD;
}
StringHasher {
prefix_hash,
potencias,
n,
}
}
/// Retorna o hash da substring s[esquerda..direita] em O(1).
/// Indices baseados em 0, intervalo semi-aberto [esquerda, direita).
fn hash_substring(&self, esquerda: usize, direita: usize) -> u64 {
assert!(esquerda <= direita && direita <= self.n);
let hash = (self.prefix_hash[direita]
+ MOD
- self.prefix_hash[esquerda]
.wrapping_mul(self.potencias[direita - esquerda])
% MOD)
% MOD;
hash
}
/// Compara duas substrings em O(1).
/// Retorna true se s[l1..r1] == s[l2..r2].
fn substrings_iguais(&self, l1: usize, r1: usize, l2: usize, r2: usize) -> bool {
if r1 - l1 != r2 - l2 {
return false;
}
self.hash_substring(l1, r1) == self.hash_substring(l2, r2)
}
}
/// Hasher com duplo hash para colisoes quase zero.
struct DuploHasher {
hasher1: StringHasher,
prefix_hash2: Vec<u64>,
potencias2: Vec<u64>,
n: usize,
}
const BASE2: u64 = 37;
const MOD2: u64 = 999_999_937;
impl DuploHasher {
fn novo(texto: &str) -> Self {
let bytes = texto.as_bytes();
let n = bytes.len();
let hasher1 = StringHasher::novo(texto);
let mut prefix_hash2 = vec![0u64; n + 1];
let mut potencias2 = vec![1u64; n + 1];
for i in 1..=n {
potencias2[i] = potencias2[i - 1].wrapping_mul(BASE2) % MOD2;
}
for i in 0..n {
let valor = (bytes[i] as u64).wrapping_add(1);
prefix_hash2[i + 1] = (prefix_hash2[i].wrapping_mul(BASE2) + valor) % MOD2;
}
DuploHasher {
hasher1,
prefix_hash2,
potencias2,
n,
}
}
/// Retorna um par (hash1, hash2) para a substring.
fn hash_substring(&self, esquerda: usize, direita: usize) -> (u64, u64) {
let h1 = self.hasher1.hash_substring(esquerda, direita);
let h2 = (self.prefix_hash2[direita]
+ MOD2
- self.prefix_hash2[esquerda]
.wrapping_mul(self.potencias2[direita - esquerda])
% MOD2)
% MOD2;
(h1, h2)
}
fn substrings_iguais(&self, l1: usize, r1: usize, l2: usize, r2: usize) -> bool {
if r1 - l1 != r2 - l2 {
return false;
}
self.hash_substring(l1, r1) == self.hash_substring(l2, r2)
}
}
fn main() {
let texto = "abcabcxyzabc";
println!("Texto: \"{}\"", texto);
println!();
let hasher = StringHasher::novo(texto);
// Compara substrings
let pares = vec![
((0, 3), (3, 6)), // "abc" vs "abc"
((0, 3), (9, 12)), // "abc" vs "abc"
((0, 3), (6, 9)), // "abc" vs "xyz"
((0, 6), (3, 9)), // "abcabc" vs "abcxyz"
];
println!("Comparacao de substrings (hash simples):");
for ((l1, r1), (l2, r2)) in &pares {
let h1 = hasher.hash_substring(*l1, *r1);
let h2 = hasher.hash_substring(*l2, *r2);
let iguais = hasher.substrings_iguais(*l1, *r1, *l2, *r2);
println!(
" \"{}\" (hash={}) vs \"{}\" (hash={}) -> {}",
&texto[*l1..*r1],
h1,
&texto[*l2..*r2],
h2,
if iguais { "IGUAIS" } else { "DIFERENTES" }
);
}
// Duplo hash
println!();
let duplo = DuploHasher::novo(texto);
println!("Duplo hash para \"abc\"[0..3]: {:?}", duplo.hash_substring(0, 3));
println!("Duplo hash para \"abc\"[3..6]: {:?}", duplo.hash_substring(3, 6));
println!("Duplo hash para \"xyz\"[6..9]: {:?}", duplo.hash_substring(6, 9));
}
Exemplo de Execucao
Texto: "abcabcxyzabc"
Comparacao de substrings (hash simples):
"abc" (hash=96354) vs "abc" (hash=96354) -> IGUAIS
"abc" (hash=96354) vs "abc" (hash=96354) -> IGUAIS
"abc" (hash=96354) vs "xyz" (hash=120234) -> DIFERENTES
"abcabc" (hash=...) vs "abcxyz" (hash=...) -> DIFERENTES
Duplo hash para "abc"[0..3]: (96354, 150904)
Duplo hash para "abc"[3..6]: (96354, 150904)
Duplo hash para "xyz"[6..9]: (120234, 186724)
Variacoes e Otimizacoes
1. Deteccao de Anagramas
Anagramas tem os mesmos caracteres em ordem diferente. Podemos usar um hash invariante a ordem:
use std::collections::HashMap;
/// Hash invariante a ordem para deteccao de anagramas.
/// Usa a soma dos hashes individuais de cada caractere.
fn hash_anagrama(s: &str) -> u64 {
// Usamos primos distintos para cada caractere para evitar colisoes
let primos: Vec<u64> = vec![
2, 3, 5, 7, 11, 13, 17, 19, 23, 29,
31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
73, 79, 83, 89, 97, 101,
];
let mut hash: u64 = 1;
for byte in s.bytes() {
let idx = (byte.to_ascii_lowercase() - b'a') as usize;
if idx < primos.len() {
hash = hash.wrapping_mul(primos[idx]) % MOD;
}
}
hash
}
/// Encontra todos os pares de anagramas em uma lista de strings.
fn encontrar_anagramas(palavras: &[&str]) -> Vec<(String, String)> {
let mut mapa: HashMap<u64, Vec<&str>> = HashMap::new();
for &palavra in palavras {
let hash = hash_anagrama(palavra);
mapa.entry(hash).or_default().push(palavra);
}
let mut pares = Vec::new();
for grupo in mapa.values() {
if grupo.len() >= 2 {
for i in 0..grupo.len() {
for j in (i + 1)..grupo.len() {
// Verifica se realmente sao anagramas (confirma contra colisao)
let mut chars1: Vec<char> = grupo[i].chars().collect();
let mut chars2: Vec<char> = grupo[j].chars().collect();
chars1.sort();
chars2.sort();
if chars1 == chars2 {
pares.push((grupo[i].to_string(), grupo[j].to_string()));
}
}
}
}
}
pares
}
/// Encontra todas as posicoes onde substrings do texto sao anagramas do padrao.
fn buscar_anagramas_substring(texto: &str, padrao: &str) -> Vec<usize> {
let texto_bytes = texto.as_bytes();
let padrao_bytes = padrao.as_bytes();
let n = texto_bytes.len();
let m = padrao_bytes.len();
if m == 0 || m > n {
return Vec::new();
}
// Conta a frequencia de cada caractere no padrao
let mut freq_padrao = [0i32; 256];
for &b in padrao_bytes {
freq_padrao[b as usize] += 1;
}
// Janela deslizante
let mut freq_janela = [0i32; 256];
let mut posicoes = Vec::new();
// Inicializa a primeira janela
for i in 0..m {
freq_janela[texto_bytes[i] as usize] += 1;
}
if freq_janela == freq_padrao {
posicoes.push(0);
}
// Desliza a janela
for i in 1..=(n - m) {
// Remove caractere que sai
freq_janela[texto_bytes[i - 1] as usize] -= 1;
// Adiciona caractere que entra
freq_janela[texto_bytes[i + m - 1] as usize] += 1;
if freq_janela == freq_padrao {
posicoes.push(i);
}
}
posicoes
}
fn main() {
// Encontrar anagramas
let palavras = vec!["roma", "amor", "mora", "ramo", "gato", "toga"];
let pares = encontrar_anagramas(&palavras);
println!("Pares de anagramas:");
for (a, b) in &pares {
println!(" \"{}\" <-> \"{}\"", a, b);
}
println!();
// Busca de anagramas como substrings
let texto = "cbaebabacd";
let padrao = "abc";
let posicoes = buscar_anagramas_substring(texto, padrao);
println!(
"Anagramas de \"{}\" em \"{}\": posicoes {:?}",
padrao, texto, posicoes
);
for pos in &posicoes {
println!(" posicao {}: \"{}\"", pos, &texto[*pos..*pos + padrao.len()]);
}
}
2. Deduplicacao de Strings
use std::collections::HashSet;
/// Remove strings duplicadas usando hashing.
/// Mais eficiente que ordenar + dedup para strings longas.
fn deduplicar(strings: &[&str]) -> Vec<String> {
let mut vistos: HashSet<u64> = HashSet::new();
let mut resultado = Vec::new();
for &s in strings {
let hasher = StringHasher::novo(s);
let hash = hasher.hash_substring(0, s.len());
if vistos.insert(hash) {
resultado.push(s.to_string());
}
}
resultado
}
/// Encontra substrings duplicadas de comprimento k em um texto.
fn substrings_duplicadas(texto: &str, k: usize) -> Vec<String> {
let n = texto.len();
if k == 0 || k > n {
return Vec::new();
}
let hasher = StringHasher::novo(texto);
let mut vistos: HashMap<u64, usize> = HashMap::new(); // hash -> primeira posicao
let mut duplicadas: HashSet<String> = HashSet::new();
for i in 0..=(n - k) {
let hash = hasher.hash_substring(i, i + k);
if let Some(&pos_anterior) = vistos.get(&hash) {
// Confirma que realmente sao iguais (contra colisoes)
if texto[i..i + k] == texto[pos_anterior..pos_anterior + k] {
duplicadas.insert(texto[i..i + k].to_string());
}
} else {
vistos.insert(hash, i);
}
}
duplicadas.into_iter().collect()
}
3. Verificacao de Palindromos com Hash
/// Estrutura que permite verificar palindromos em O(1) por consulta.
struct PalindromeHasher {
frente: StringHasher,
prefix_hash_rev: Vec<u64>,
potencias_rev: Vec<u64>,
n: usize,
}
impl PalindromeHasher {
fn novo(texto: &str) -> Self {
let n = texto.len();
let bytes = texto.as_bytes();
let frente = StringHasher::novo(texto);
// Calcula hashes da string reversa
let mut prefix_hash_rev = vec![0u64; n + 1];
let mut potencias_rev = vec![1u64; n + 1];
for i in 1..=n {
potencias_rev[i] = potencias_rev[i - 1].wrapping_mul(BASE) % MOD;
}
for i in 0..n {
let valor = (bytes[n - 1 - i] as u64).wrapping_add(1);
prefix_hash_rev[i + 1] = (prefix_hash_rev[i].wrapping_mul(BASE) + valor) % MOD;
}
PalindromeHasher {
frente,
prefix_hash_rev,
potencias_rev,
n,
}
}
/// Hash da substring reversa correspondente a s[esquerda..direita].
fn hash_reverso(&self, esquerda: usize, direita: usize) -> u64 {
let rev_esq = self.n - direita;
let rev_dir = self.n - esquerda;
(self.prefix_hash_rev[rev_dir]
+ MOD
- self.prefix_hash_rev[rev_esq]
.wrapping_mul(self.potencias_rev[rev_dir - rev_esq])
% MOD)
% MOD
}
/// Verifica se s[esquerda..direita] e um palindromo em O(1).
fn eh_palindromo(&self, esquerda: usize, direita: usize) -> bool {
self.frente.hash_substring(esquerda, direita)
== self.hash_reverso(esquerda, direita)
}
}
fn main() {
let texto = "abaacaaba";
let hasher = PalindromeHasher::novo(texto);
println!("Texto: \"{}\"", texto);
println!();
// Testa varias substrings
let testes = vec![
(0, 3, "aba"),
(0, 5, "abaac"),
(3, 6, "aca"),
(5, 9, "aaba"),
(0, 9, "abaacaaba"),
];
println!("Verificacao de palindromos em O(1):");
for (l, r, desc) in &testes {
let resultado = hasher.eh_palindromo(*l, *r);
println!(
" s[{}..{}] = \"{}\" -> {}",
l,
r,
desc,
if resultado { "PALINDROMO" } else { "NAO" }
);
}
}
Aplicacoes Praticas
Busca de Padroes com Rolling Hash (Rabin-Karp Simplificado)
/// Busca simples de padrao usando rolling hash.
fn buscar_padrao(texto: &str, padrao: &str) -> Vec<usize> {
let n = texto.len();
let m = padrao.len();
if m == 0 || m > n {
return Vec::new();
}
let hasher_texto = StringHasher::novo(texto);
let hasher_padrao = StringHasher::novo(padrao);
let hash_padrao = hasher_padrao.hash_substring(0, m);
let mut posicoes = Vec::new();
for i in 0..=(n - m) {
if hasher_texto.hash_substring(i, i + m) == hash_padrao {
// Confirma contra colisoes
if texto[i..i + m] == *padrao {
posicoes.push(i);
}
}
}
posicoes
}
Maior Prefixo Comum com Busca Binaria
/// Encontra o comprimento do maior prefixo comum entre
/// s[a..] e s[b..] em O(log n) usando hashing + busca binaria.
fn maior_prefixo_comum(hasher: &StringHasher, a: usize, b: usize, n: usize) -> usize {
let max_len = n.saturating_sub(a).min(n.saturating_sub(b));
let mut low = 0;
let mut high = max_len;
while low < high {
let mid = low + (high - low + 1) / 2;
if hasher.hash_substring(a, a + mid) == hasher.hash_substring(b, b + mid) {
low = mid;
} else {
high = mid - 1;
}
}
low
}
fn main() {
let texto = "abcabcxabc";
let hasher = StringHasher::novo(texto);
let n = texto.len();
let a = 0; // "abcabcxabc"
let b = 3; // "abcxabc"
let lcp = maior_prefixo_comum(&hasher, a, b, n);
println!(
"Maior prefixo comum entre s[{}..] e s[{}..]: {} (\"{}\")",
a,
b,
lcp,
&texto[a..a + lcp]
);
}
Analise de Colisoes
/// Analisa a taxa de colisoes para um dado conjunto de strings.
fn analisar_colisoes(strings: &[&str]) {
let mut hashes_simples: HashMap<u64, Vec<&str>> = HashMap::new();
let mut hashes_duplo: HashMap<(u64, u64), Vec<&str>> = HashMap::new();
for &s in strings {
let hasher = StringHasher::novo(s);
let h1 = hasher.hash_substring(0, s.len());
let duplo = DuploHasher::novo(s);
let h2 = duplo.hash_substring(0, s.len());
hashes_simples.entry(h1).or_default().push(s);
hashes_duplo.entry(h2).or_default().push(s);
}
let colisoes_simples = hashes_simples.values().filter(|v| v.len() > 1).count();
let colisoes_duplo = hashes_duplo.values().filter(|v| v.len() > 1).count();
println!("Total de strings: {}", strings.len());
println!("Hashes unicos (simples): {}", hashes_simples.len());
println!("Colisoes (simples): {}", colisoes_simples);
println!("Hashes unicos (duplo): {}", hashes_duplo.len());
println!("Colisoes (duplo): {}", colisoes_duplo);
// Mostra colisoes se existirem
for (hash, grupo) in &hashes_simples {
if grupo.len() > 1 {
println!(" Colisao hash={}: {:?}", hash, grupo);
}
}
}
Comparacao com Outros Algoritmos
| Tecnica | Pre-proc. | Comparacao Substring | Espaco | Colisoes |
|---|---|---|---|---|
| Comparacao direta | O(1) | O(min(n,m)) | O(1) | Nenhuma |
| Hash simples | O(n) | O(1) | O(n) | ~1/primo |
| Hash duplo | O(n) | O(1) | O(n) | ~1/primo^2 |
| Suffix Array | O(n*logn) | O(m*logn) | O(n) | Nenhuma |
| Suffix Tree | O(n) | O(m) | O(n) | Nenhuma |
O hashing de strings oferece o melhor custo-beneficio: pre-processamento linear, comparacoes em O(1) e implementacao simples. A unica desvantagem sao as colisoes, que podem ser minimizadas com duplo hashing (probabilidade ~10^-18).
Analise de Colisoes
A probabilidade de colisao entre duas strings aleatorias com um unico hash modular e:
P(colisao) = 1/MOD ~ 10^-9 (com MOD = 10^9 + 7)
Para n consultas (paradoxo do aniversario):
P(pelo menos 1 colisao) ~ n^2 / (2 * MOD)
Exemplos:
1.000 consultas: P ~ 5 * 10^-4 (raro)
100.000 consultas: P ~ 5 * 10^0 (provavel!)
Com duplo hash:
P(colisao) ~ 1/(MOD1 * MOD2) ~ 10^-18
100.000 consultas: P ~ 5 * 10^-9 (praticamente impossivel)
Recomendacao: use sempre duplo hash em competicoes e aplicacoes criticas. Para uso casual (busca em texto, deduplicacao), hash simples com primo grande e suficiente.
Exercicios Praticos
Maior substring comum: Dadas duas strings, encontre a maior substring comum usando hashing + busca binaria no comprimento. Para cada comprimento candidato, calcule todos os hashes de substrings de ambas as strings e procure correspondencias.
Contagem de substrings distintas: Conte o numero de substrings distintas de uma string. Para cada comprimento
kde 1 a n, insira os hashes de todas as substrings de comprimentokem um HashSet e some os tamanhos.Rotacao lexicograficamente minima: Dada uma string circular, encontre a rotacao que e lexicograficamente menor. Use hashing + busca binaria para comparar prefixos comuns em O(log n).
Verificacao de periodicidade: Dada uma string S, determine se ela e periodica (formada pela repeticao de um padrao menor). Para cada divisor d de n, verifique se hash(S[0..d]) gera toda a string quando repetido.
Veja Tambem
- String na Biblioteca Padrao – manipulacao de strings em Rust
- HashMap – tabela hash para armazenar hashes calculados
- HashSet – conjunto hash para deduplicacao
- Trait Hash – trait de hashing da biblioteca padrao do Rust
- Algoritmo Rabin-Karp – busca de padroes usando rolling hash
- Suffix Array – alternativa exata (sem colisoes) para operacoes em substrings