Algoritmos de pesquisa de strings para textos grandes

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);
}

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.