Algorithmes de recherche de chaînes pour les gros textes

Algorithmes de recherche de chaînes pour les gros textes

1. introduction

Dans cet article, nous allons montrer plusieurs algorithmes de recherche d'un modèle dans un texte volumineux. Nous décrirons chaque algorithme avec le code fourni et des connaissances mathématiques simples.

Notez que les algorithmes fournis ne sont pas le meilleur moyen d'effectuer une recherche en texte intégral dans des applications plus complexes. Pour effectuer correctement une recherche en texte intégral, nous pouvons utiliserSolr ouElasticSearch.

2. Algorithmes

Nous allons commencer par un algorithme de recherche de texte naïf qui est le plus intuitif et qui aide à découvrir d'autres problèmes avancés associés à cette tâche.

2.1. Méthodes d'assistance

Avant de commencer, définissons des méthodes simples de calcul des nombres premiers que nous utilisons dans l'algorithme de 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);
}

Le nom de cet algorithme le décrit mieux que toute autre explication. C’est la solution la plus naturelle:

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

L'idée de cet algorithme est simple: parcourez le texte et s'il y a correspondance pour la première lettre du motif, vérifiez si toutes les lettres du motif correspondent au texte.

Sim est un nombre de lettres dans le modèle etn est le nombre de lettres dans le texte, la complexité temporelle de cet algorithme estO(m(n-m + 1)).

Le pire des cas se produit dans le cas d'unString ayant de nombreuses occurrences partielles:

Text: baeldunbaeldunbaeldunbaeldun
Pattern: example

2.3. Algorithme de Rabin Karp

Comme mentionné ci-dessus, l'algorithme Simple Text Search est très inefficace lorsque les modèles sont longs et qu'il y a beaucoup d'éléments répétés dans le modèle.

L'algorithme de Rabin Karp consiste à utiliser le hachage pour trouver un motif dans un texte. Au début de l'algorithme, nous devons calculer un hachage du motif qui sera utilisé plus tard dans l'algorithme. Ce processus s'appelle le calcul des empreintes digitales, et nous pouvons trouver une explication détailléehere.

La chose importante à propos de l'étape de prétraitement est que sa complexité temporelle est deO(m) et l'itération à travers le texte prendraO(n), ce qui donne la complexité temporelle de tout l'algorithmeO(m+n).

Code de l'algorithme:

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;

}

Dans le pire des cas, la complexité temporelle de cet algorithme est deO(m(n-m+1)). Cependant, en moyenne, cet algorithme a une complexité temporelle deO(n+m).

De plus, il existe une version Monte Carlo de cet algorithme qui est plus rapide, mais il peut en résulter des correspondances erronées (faux positifs).

2.4 Knuth-Morris-Pratt Algorithm

Dans l'algorithme de recherche de texte simple, nous avons vu à quel point l'algorithme pouvait être lent s'il existe de nombreuses parties du texte correspondant au motif.

L'idée de l'algorithme de Knuth-Morris-Pratt est le calcul de la table de décalage qui nous fournit les informations où nous devrions rechercher nos candidats modèles. Nous pouvons en savoir plus sur la table de décalagehere.

Implémentation Java de l'algorithme 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;
}

Et voici comment nous calculons la table de décalage:

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

La complexité temporelle de cet algorithme est égalementO(m+n).

2.5. Algorithme de Boyer-Moore simple

Deux scientifiques, Boyer et Moore, ont eu une autre idée. Pourquoi ne pas comparer le motif au texte de droite à gauche au lieu de gauche à droite, tout en conservant la même direction de décalage:

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

Comme prévu, cela fonctionnera dans le tempsO(m * n). Mais cet algorithme a conduit à l'implémentation de l'occurrence et à l'heuristique de correspondance, ce qui a considérablement accéléré l'algorithme. Nous pouvons trouver plus dehere.

2.6. Algorithme de Boyer-Moore-Horspool

Il existe de nombreuses variantes de mise en œuvre heuristique de l’algorithme de Boyer-Moore, la plus simple étant la variante de Horspool.

Cette version de l'algorithme s'appelle Boyer-Moore-Horspool, et cette variation a résolu le problème des décalages négatifs (on peut en savoir plus sur le problème des décalages négatifs dans la description de l'algorithme de Boyer-Moore).

Comme l'algorithme de Boyer-Moore, la complexité temporelle du pire des cas estO(m * n) tandis que la complexité moyenne est O (n). L’utilisation de l’espace ne dépend pas de la taille du motif, mais uniquement de la taille de l’alphabet qui est de 256 car c’est la valeur maximale du caractère ASCII dans l’alphabet anglais:

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. Conclusion

Dans cet article, nous avons présenté plusieurs algorithmes pour la recherche de texte. Étant donné que plusieurs algorithmes nécessitent une base mathématique plus solide, nous avons essayé de représenter l'idée principale sous chaque algorithme et de la fournir de manière simple.

Et, comme toujours, le code source peut être trouvéover on GitHub.