Algoritmos de pesquisa de strings para textos grandes
1. Introdução
Neste artigo, mostraremos vários algoritmos para pesquisar um padrão em um texto grande. Descreveremos cada algoritmo com o código fornecido e uma base matemática simples.
Observe que os algoritmos fornecidos não são a melhor maneira de fazer uma pesquisa de texto completo em aplicativos mais complexos. Para fazer a pesquisa de texto completo corretamente, podemos usarSolr ouElasticSearch.
2. Algoritmos
Começaremos com um algoritmo de pesquisa de texto ingênuo que é o mais intuitivo e ajuda a descobrir outros problemas avançados associados a essa tarefa.
2.1. Métodos auxiliares
Antes de começar, vamos definir métodos simples para calcular números primos que usamos no algoritmo Rabin Karp:
public static long getBiggerPrime(int m) {
BigInteger prime = BigInteger.probablePrime(getNumberOfBits(m) + 1, new Random());
return prime.longValue();
}
private static int getNumberOfBits(int number) {
return Integer.SIZE - Integer.numberOfLeadingZeros(number);
}
2.2. Pesquisa de texto simples
O nome desse algoritmo o descreve melhor do que qualquer outra explicação. É a solução mais natural:
public static int simpleTextSearch(char[] pattern, char[] text) {
int patternSize = pattern.length;
int textSize = text.length;
int i = 0;
while ((i + patternSize) <= textSize) {
int j = 0;
while (text[i + j] == pattern[j]) {
j += 1;
if (j >= patternSize)
return i;
}
i += 1;
}
return -1;
}
A ideia desse algoritmo é direta: repita o texto e, se houver uma correspondência para a primeira letra do padrão, verifique se todas as letras do padrão correspondem ao texto.
Sem é um número de letras no padrão, en é o número de letras no texto, a complexidade de tempo desses algoritmos éO(m(n-m + 1)).
O pior cenário ocorre no caso de umString com muitas ocorrências parciais:
Text: baeldunbaeldunbaeldunbaeldun
Pattern: example
2.3. Algoritmo Rabin Karp
Como mencionado acima, o algoritmo Simple Text Search é muito ineficiente quando os padrões são longos e quando há muitos elementos repetidos do padrão.
A idéia do algoritmo Rabin Karp é usar o hash para encontrar um padrão em um texto. No início do algoritmo, precisamos calcular um hash do padrão que é usado posteriormente no algoritmo. Este processo é chamado de cálculo de impressão digital, e podemos encontrar uma explicação detalhadahere.
O importante sobre a etapa de pré-processamento é que sua complexidade de tempo éO(m)e a iteração através do texto levaráO(n), o que dá a complexidade de tempo de todo o algoritmoO(m+n).
Código do algoritmo:
public static int RabinKarpMethod(char[] pattern, char[] text) {
int patternSize = pattern.length;
int textSize = text.length;
long prime = getBiggerPrime(patternSize);
long r = 1;
for (int i = 0; i < patternSize - 1; i++) {
r *= 2;
r = r % prime;
}
long[] t = new long[textSize];
t[0] = 0;
long pfinger = 0;
for (int j = 0; j < patternSize; j++) {
t[0] = (2 * t[0] + text[j]) % prime;
pfinger = (2 * pfinger + pattern[j]) % prime;
}
int i = 0;
boolean passed = false;
int diff = textSize - patternSize;
for (i = 0; i <= diff; i++) {
if (t[i] == pfinger) {
passed = true;
for (int k = 0; k < patternSize; k++) {
if (text[i + k] != pattern[k]) {
passed = false;
break;
}
}
if (passed) {
return i;
}
}
if (i < diff) {
long value = 2 * (t[i] - r * text[i]) + text[i + patternSize];
t[i + 1] = ((value % prime) + prime) % prime;
}
}
return -1;
}
No pior cenário, a complexidade de tempo para este algoritmo éO(m(n-m+1)). No entanto, em média, esse algoritmo tem complexidade de tempo deO(n+m).
Além disso, existe a versão Monte Carlo deste algoritmo, que é mais rápida, mas pode resultar em correspondências incorretas (falsos positivos).
2.4 Knuth-Morris-Pratt Algorithm
No algoritmo Simple Text Search, vimos como o algoritmo pode ser lento se houver muitas partes do texto que correspondam ao padrão.
A idéia do algoritmo de Knuth-Morris-Pratt é o cálculo da tabela de deslocamento que nos fornece as informações nas quais devemos procurar nossos candidatos a padrões. Podemos ler mais sobre a tabela de deslocamentohere.
Implementação em Java do algoritmo KMP:
public static int KnuthMorrisPrattSearch(char[] pattern, char[] text) {
int patternSize = pattern.length;
int textSize = text.length;
int i = 0, j = 0;
int[] shift = KnuthMorrisPrattShift(pattern);
while ((i + patternSize) <= textSize) {
while (text[i + j] == pattern[j]) {
j += 1;
if (j >= patternSize)
return i;
}
if (j > 0) {
i += shift[j - 1];
j = Math.max(j - shift[j - 1], 0);
} else {
i++;
j = 0;
}
}
return -1;
}
E aqui está como calculamos a tabela de turnos:
public static int[] KnuthMorrisPrattShift(char[] pattern) {
int patternSize = pattern.length;
int[] shift = new int[patternSize];
shift[0] = 1;
int i = 1, j = 0;
while ((i + j) < patternSize) {
if (pattern[i + j] == pattern[j]) {
shift[i + j] = i;
j++;
} else {
if (j == 0)
shift[i] = i + 1;
if (j > 0) {
i = i + shift[j - 1];
j = Math.max(j - shift[j - 1], 0);
} else {
i = i + 1;
j = 0;
}
}
}
return shift;
}
A complexidade de tempo desse algoritmo também éO(m+n).
2.5. Algoritmo Boyer-Moore Simples
Dois cientistas, Boyer e Moore, tiveram outra ideia. Por que não comparar o padrão com o texto da direita para a esquerda em vez de da esquerda para a direita, mantendo a mesma direção de mudança:
public static int BoyerMooreHorspoolSimpleSearch(char[] pattern, char[] text) {
int patternSize = pattern.length;
int textSize = text.length;
int i = 0, j = 0;
while ((i + patternSize) <= textSize) {
j = patternSize - 1;
while (text[i + j] == pattern[j]) {
j--;
if (j < 0)
return i;
}
i++;
}
return -1;
}
Como esperado, isso será executado emO(m * n). Mas esse algoritmo levou à implementação de ocorrência e às heurísticas de correspondência que aceleram significativamente o algoritmo. Podemos encontrar maishere.
2.6. Algoritmo Boyer-Moore-Horspool
Existem muitas variações da implementação heurística do algoritmo de Boyer-Moore, e a mais simples é a variação de Horspool.
Essa versão do algoritmo é chamada Boyer-Moore-Horspool, e essa variação resolveu o problema das mudanças negativas (podemos ler sobre o problema das mudanças negativas na descrição do algoritmo de Boyer-Moore).
Como o algoritmo de Boyer-Moore, a complexidade de tempo do pior caso éO(m * n), enquanto a complexidade média é O (n). O uso do espaço não depende do tamanho do padrão, mas apenas do tamanho do alfabeto, que é 256, pois esse é o valor máximo do caractere ASCII no alfabeto inglês:
public static int BoyerMooreHorspoolSearch(char[] pattern, char[] text) {
int shift[] = new int[256];
for (int k = 0; k < 256; k++) {
shift[k] = pattern.length;
}
for (int k = 0; k < pattern.length - 1; k++){
shift[pattern[k]] = pattern.length - 1 - k;
}
int i = 0, j = 0;
while ((i + pattern.length) <= text.length) {
j = pattern.length - 1;
while (text[i + j] == pattern[j]) {
j -= 1;
if (j < 0)
return i;
}
i = i + shift[text[i + pattern.length - 1]];
}
return -1;
}
4. Conclusão
Neste artigo, apresentamos vários algoritmos para pesquisa de texto. Como vários algoritmos exigem um fundo matemático mais forte, tentamos representar a idéia principal sob cada algoritmo e fornecê-la de uma maneira simples.
E, como sempre, o código-fonte pode ser encontradoover on GitHub.