Algoritmo Rabin-Karp em PHP
Implementação otimizada e explicação do algoritmo de busca de string Rabin-Karp, focado em performance e hashing.
🔍 Algoritmo Rabin-Karp em PHP
O Rabin-Karp é um algoritmo de busca de string que utiliza hashing para encontrar padrões dentro de um texto. Diferente da busca força bruta, que compara caractere por caractere, o Rabin-Karp compara o hash do padrão com o hash de trechos do texto, tornando a verificação inicial muito mais rápida.
💡 Por que e Quando Usar?
O Rabin-Karp é aplicado em cenários onde a velocidade e a eficiência na análise de grandes volumes de texto são cruciais, especialmente quando envolvem múltiplas buscas ou verificação de integridade.
Embora o PHP forneça funções nativas otimizadas para busca de string (como strpos()), a aplicabilidade do Rabin-Karp em projetos PHP de grande escala reside em contextos muito específicos:
💻 1. Aplicabilidade em Ferramentas e Processamento de Dados
Análise de Logs e Parsing de Arquivos Grandes
Cenário: Processar arquivos de log de servidor (gigabytes de texto) ou arquivos de dados grandes (CSV, XML, etc.) para buscar padrões de erro, endereços IP ou sequências específicas. Vantagem do RK: Usar uma implementação otimizada do RK pode ser significativamente mais rápido do que a busca naive em laços, garantindo uma complexidade de tempo de $O(N+M)$ (linear) em média.
Compiladores, Parsers e Linguagens de Template
Cenário: Ferramentas que precisam identificar tokens (palavras-chave, variáveis, operadores) dentro de um código-fonte ou template. Vantagem do RK: É excelente para identificar múltiplos padrões simultaneamente (busca de múltiplos padrões), pois podemos armazenar os hashes dos padrões em um conjunto e verificar cada janela do texto contra esse conjunto.
🛡️ 2. Aplicações de Segurança e Integridade de Dados
O Rabin-Karp é particularmente poderoso devido à sua técnica de hashing rolante.
Detecção de Plágio e Similaridade
Cenário: Comparar grandes blocos de texto (documentos, códigos-fonte) para identificar seções que foram copiadas. Vantagem do RK: O hashing rolante permite comparar rapidamente os hashes de janelas de texto de tamanho fixo em diferentes documentos. Se os hashes coincidirem, é provável que o texto seja idêntico. Isso acelera enormemente o processo de detecção de similaridade.
Assinaturas Digitais e Verificação de Integridade de Arquivos
Cenário: Usar o hashing de string para gerar identificadores rápidos e únicos para partes de um arquivo. Vantagem do RK: A técnica de hashing rolante pode ser adaptada para gerar fingerprints (impressões digitais) rápidos de diferentes seções de um arquivo, permitindo verificar integridade sem reprocessar todo o arquivo a cada mudança mínima.
🚀 Implementação em PHP
Abaixo, uma implementação robusta do algoritmo Rabin-Karp utilizando Rolling Hash. Esta técnica permite recalcular o hash da próxima janela de texto em tempo constante $O(1)$, aproveitando o cálculo da janela anterior.
<?php
class RabinKarp {
private $prime = 101; // Um número primo para o hash
private $d = 256; // Número de caracteres no alfabeto de entrada
/**
* Busca todas as ocorrências de um padrão em um texto.
*
* @param string $pattern O padrão a ser buscado
* @param string $text O texto onde buscar
* @return array Lista de índices onde o padrão foi encontrado
*/
public function search(string $pattern, string $text): array {
$m = strlen($pattern);
$n = strlen($text);
$i = 0;
$j = 0;
$p = 0; // Hash do padrão
$t = 0; // Hash do texto
$h = 1;
$occurrences = [];
if ($m > $n) return [];
// O valor de h seria "pow(d, m-1) % prime"
for ($i = 0; $i < $m - 1; $i++) {
$h = ($h * $this->d) % $this->prime;
}
// Calcula o hash inicial do padrão e da primeira janela do texto
for ($i = 0; $i < $m; $i++) {
$p = ($this->d * $p + ord($pattern[$i])) % $this->prime;
$t = ($this->d * $t + ord($text[$i])) % $this->prime;
}
// Desliza o padrão sobre o texto
for ($i = 0; $i <= $n - $m; $i++) {
// Se os hashes batem, verifica os caracteres um a um
if ($p == $t) {
$match = true;
for ($j = 0; $j < $m; $j++) {
if ($text[$i + $j] != $pattern[$j]) {
$match = false;
break;
}
}
if ($match) {
$occurrences[] = $i;
}
}
// Calcula o hash da próxima janela de texto:
// Remove o dígito líder, adiciona o dígito final
if ($i < $n - $m) {
$t = ($this->d * ($t - ord($text[$i]) * $h) + ord($text[$i + $m])) % $this->prime;
// Se obtivermos um valor negativo, convertemos para positivo
if ($t < 0) {
$t = ($t + $this->prime);
}
}
}
return $occurrences;
}
}
// --- Testes e Exemplos ---
$rk = new RabinKarp();
// Exemplo 1: Busca simples
$texto = "ABABDABACDABABCABAB";
$padrao = "ABABCABAB";
$resultado = $rk->search($padrao, $texto);
echo "Padrão encontrado nos índices: " . implode(", ", $resultado) . "\n";
// Exemplo 2: Múltiplas ocorrências
$texto2 = "AABAACAADAABAABA";
$padrao2 = "AABA";
$resultado2 = $rk->search($padrao2, $texto2);
echo "Ocorrências de 'AABA': " . implode(", ", $resultado2) . "\n";
// Saída esperada
// Padrão encontrado nos índices: 10
// Ocorrências de 'AABA': 0, 9, 12
🧠 Entendendo o Rolling Hash (O Pulo do Gato)
O segredo da performance está na atualização do hash. Em vez de recalcular tudo do zero para cada nova janela (o que seria lento), nós "deslizamos" a janela.
Imagine que estamos calculando o hash de números para simplificar.
Texto: 1 2 3 4 5
Janela (tamanho 3): [1 2 3]
- Hash Inicial: $1 \times 100 + 2 \times 10 + 3 \times 1 = 123$
- Deslizar para direita: Sai o
1, entra o4. Nova janela:[2 3 4] - Cálculo Rápido:
- Pegamos o 123.
- Removemos o
1(o dígito mais à esquerda): $123 - (1 \times 100) = 23$. - Deslocamos para a esquerda (multiplicamos pela base): $23 \times 10 = 230$.
- Adicionamos o
4(o novo dígito): $230 + 4 = 234$.
Pronto! Calculamos o hash da nova janela 234 sem ter que iterar por todos os itens novamente.
Fórmula simplificada: $$ H{prox} = (H{atual} - \text{val}(char{sai}) \times h) \times d + \text{val}(char{entra}) \pmod q $$
🕵️ Exemplo Prático: Detector de Plágio (Similaridade)
Vamos sair da teoria e ir para um caso real. Imagine que você tem dois textos e quer saber se eles compartilham sentenças comuns. O Rabin-Karp é perfeito para isso porque podemos gerar hashes de todas as frases de um texto e verificar rapidamente se existem no outro.
<?php
class PlagiarismDetector {
private $prime = 101;
private $d = 256;
/**
* Verifica a porcentagem de similaridade baseada em subsequências comuns.
*
* @param string $original Texto original
* @param string $suspect Texto suspeito
* @param int $windowSize Tamanho da janela de comparação (em caracteres)
* @return float Porcentagem de similaridade (0 a 100)
*/
public function checkSimilarity(string $original, string $suspect, int $windowSize = 10): float {
$n = strlen($original);
$m = strlen($suspect);
if ($n < $windowSize || $m < $windowSize) return 0.0;
// 1. Mapear todos os hashes de janelas do texto original
$originalHashes = [];
$h = 1;
$t = 0; // Hash atual
// Pré-calcula h = pow(d, windowSize-1) % prime
for ($i = 0; $i < $windowSize - 1; $i++) {
$h = ($h * $this->d) % $this->prime;
}
// Calcula hash da primeira janela do original
for ($i = 0; $i < $windowSize; $i++) {
$t = ($this->d * $t + ord($original[$i])) % $this->prime;
}
$originalHashes[$t] = true; // Armazena o hash
// Calcula o restante dos hashes do original (Rolling Hash)
for ($i = 0; $i < $n - $windowSize; $i++) {
$t = ($this->d * ($t - ord($original[$i]) * $h) + ord($original[$i + $windowSize])) % $this->prime;
if ($t < 0) $t += $this->prime;
$originalHashes[$t] = true;
}
// 2. Verificar quantas janelas do texto suspeito coincidem
$matches = 0;
$totalWindows = $m - $windowSize + 1;
$p = 0;
// Hash da primeira janela do suspeito
for ($i = 0; $i < $windowSize; $i++) {
$p = ($this->d * $p + ord($suspect[$i])) % $this->prime;
}
if (isset($originalHashes[$p])) $matches++;
// Rolling hash no suspeito
for ($i = 0; $i < $m - $windowSize; $i++) {
$p = ($this->d * ($p - ord($suspect[$i]) * $h) + ord($suspect[$i + $windowSize])) % $this->prime;
if ($p < 0) $p += $this->prime;
if (isset($originalHashes[$p])) {
$matches++;
}
}
return ($matches / $totalWindows) * 100;
}
}
// --- Teste Prático ---
$detector = new PlagiarismDetector();
$textoOriginal = "O rato roeu a roupa do rei de Roma e a rainha riu.";
$textoSuspeito = "Sabemos que o rato roeu a roupa do rei de Roma ontem.";
// Vamos usar uma janela de 10 caracteres para considerar "cópia"
$similaridade = $detector->checkSimilarity($textoOriginal, $textoSuspeito, 10);
echo "Similaridade detectada: " . round($similaridade, 2) . "%\n";
// Saída esperada
// Similaridade detectada: 72.73%
Nota: Em um sistema real de produção, você usaria um hash mais forte (como polinomial com módulo $10^9 + 7$) para evitar colisões, mas a lógica permanece idêntica.
🎩 Trick: Fingerprinting de Arquivos
Para ver como aplicar conceitos de hashing para detecção eficiente de mudanças em arquivos (similar ao rsync), confira nosso trick dedicado:
👉 Fingerprinting de Arquivos com Hashing em PHP
⚠️ Considerações Finais
A maior aplicabilidade no mundo real não é substituir as funções nativas como strpos, mas sim entender a lógica por trás delas para criar soluções customizadas de busca, diff e análise de dados.
Para ensino, o Rabin-Karp é excelente para demonstrar como alcançar complexidade $O(N+M)$ na prática através de hashing, elevando o nível de compreensão sobre algoritmos de texto.