web-dev-qa-db-fra.com

L'algorithme C ++ le plus rapide pour les tests de chaînes par rapport à une liste de graines prédéfinies (insensible à la casse)

J'ai une liste de chaînes de graines, environ 100 chaînes prédéfinies. Toutes les chaînes ne contiennent que ASCII caractères.

std::list<std::wstring> seeds{ L"google", L"yahoo", L"stackoverflow"};

Mon application reçoit constamment de nombreuses chaînes pouvant contenir n'importe quel caractère. J'ai besoin de vérifier chaque ligne reçue et de décider si elle contient des graines ou non. La comparaison doit être insensible à la casse.

J'ai besoin de l'algorithme le plus rapide possible pour tester la chaîne reçue.

En ce moment, mon application utilise cet algo:

std::wstring testedStr;
for (auto & seed : seeds)
{
    if (boost::icontains(testedStr, seed))
    {
        return true;
    }
}
return false;

Cela fonctionne bien, mais je ne suis pas sûr que ce soit le moyen le plus efficace.

Comment est-il possible de mettre en œuvre l'algorithme afin d'obtenir de meilleures performances?

Ceci est une application Windows. L'application reçoit un std::wstring chaînes.


Mise à jour

Pour cette tâche, j'ai implémenté l'algo Aho-Corasick. Si quelqu'un pouvait réviser mon code, ce serait formidable - je n'ai pas une grande expérience avec de tels algorithmes. Lien vers l'implémentation: Gist.github.com

53
Victor Mezrin

Vous pouvez utiliser algorithme Aho – Corasick

Il construit un trie/automate où certains sommets marqués comme terminaux, ce qui signifie que la chaîne a des graines.

Il est construit dans O(sum of dictionary Word lengths) et donne la réponse dans O(test string length)

Avantages:

  • Il fonctionne spécifiquement avec plusieurs mots du dictionnaire et le temps de vérification ne dépend pas du nombre de mots (si nous ne considérons pas les cas où il ne rentre pas dans la mémoire, etc.)
  • L'algorithme n'est pas difficile à implémenter (en comparaison avec les structures de suffixes au moins)

Vous pouvez le rendre insensible à la casse en abaissant chaque symbole s'il est ASCII (non ASCII ne correspondent pas de toute façon)

50
RiaD

S'il existe une quantité finie de chaînes correspondantes, cela signifie que vous pouvez construire un arbre tel que, lu de la racine aux feuilles, des chaînes similaires occuperont des branches similaires.

Ceci est également connu sous le nom d'un trie , ou Radix Tree .

Par exemple, nous pourrions avoir les chaînes cat, coach, con, conch aussi bien que dark, dad, dank, do. Leur trie pourrait ressembler à ceci:

enter image description here

Une recherche de l'un des mots dans l'arbre cherchera l'arbre, à partir d'une racine. Le faire à une feuille correspondrait à une correspondance avec une graine. Quoi qu'il en soit, chaque caractère de la chaîne doit correspondre à l'un de leurs enfants. Si ce n'est pas le cas, vous pouvez terminer la recherche (par exemple, vous ne considéreriez aucun mot commençant par "g" ou tout mot commençant par "cu").

Il existe différents algorithmes pour construire l'arbre, le rechercher et le modifier à la volée, mais j'ai pensé donner un aperçu conceptuel de la solution au lieu d'un spécifique car je ne connais pas le meilleur algorithme pour il.

Conceptuellement, un algorithme que vous pourriez utiliser pour rechercher dans l'arbre serait lié à l'idée derrière tri radix d'une quantité fixe de catégories ou de valeurs qu'un caractère d'une chaîne pourrait prendre à un moment donné à l'heure.

Cela vous permet de comparer un mot à votre liste de mots . Puisque vous recherchez cette liste de mots comme sous-chaînes de votre chaîne d'entrée, il y aura plus que cela.

Edit: Comme d'autres réponses l'ont mentionné, l'algorithme Aho-Corasick pour la correspondance de chaînes est un algorithme sophistiqué pour effectuer la correspondance de chaînes, composé d'un trie avec des liens supplémentaires pour prendre des "raccourcis" à travers l'arbre et avoir un modèle de recherche différent pour l'accompagner. (Comme une note intéressante, Alfred Aho est également un contributeur au manuel de compilation populaire, Compilateurs: principes, techniques et outils ainsi que le manuel d'algorithmes, The Design And Analyse d'algorithmes informatiques. Il est également un ancien membre des Bell Labs. Margaret J. Corasick ne semble pas avoir trop d'informations publiques sur elle-même.)

56
VermillionAzure

Vous devriez essayer un utilitaire regex préexistant, il peut être plus lent que votre algorithme roulé à la main, mais regex consiste à faire correspondre plusieurs possibilités, il est donc probable qu'il sera déjà plusieurs fois plus rapide qu'une table de hachage ou une simple comparaison à toutes les chaînes. Je pense que les implémentations regex peuvent déjà utiliser l'algorithme Aho-Corasick mentionné par RiaD, donc en gros vous aurez à votre disposition une implémentation bien testée et rapide.

Si vous avez C++ 11, vous avez déjà une bibliothèque regex standard

#include <string>
#include <regex>

int main(){
     std::regex self_regex("google|yahoo|stackoverflow");
     regex_match(input_string ,self_regex);
}

Je m'attends à ce que cela génère le meilleur arbre de correspondance minimum possible, donc je m'attends à ce qu'il soit vraiment rapide (et fiable!)

16
GameDeveloper

L'un des moyens les plus rapides consiste à utiliser l'arborescence des suffixes https://en.wikipedia.org/wiki/Suffix_tree , mais cette approche présente d'énormes inconvénients - il s'agit d'une structure de données difficile avec une construction difficile. Cet algorithme permet de construire un arbre à partir d'une chaîne en complexité linéaire https://en.m.wikipedia.org/wiki/Ukkonen%27s_algorithm

10
LmTinyToon

Edit: Comme l'a souligné Matthieu M., l'OP a demandé si une chaîne contient un mot-clé. Ma réponse ne fonctionne que lorsque la chaîne est égale au mot clé ou si vous pouvez diviser la chaîne, par exemple par le caractère spatial.

Surtout avec un nombre élevé de candidats possibles et les connaître au moment de la compilation en utilisant fonction de hachage parfaite avec un outil comme gperf vaut la peine d'être essayé. Le principe principal est que vous semez un générateur avec votre graine et qu'elle génère une fonction qui contient une fonction de hachage qui n'a pas de collisions pour toutes les valeurs de graine. Au moment de l'exécution, vous donnez à la fonction une chaîne et elle calcule le hachage puis vérifie si c'est le seul candidat possible correspondant à la valeur de hachage.

Le coût d'exécution est le hachage de la chaîne, puis la comparaison avec le seul candidat possible (O (1) pour la taille de la graine et O(1) pour la longueur de la chaîne).

Pour rendre le cas de comparaison insensible, il vous suffit d'utiliser tolower sur la graine et sur votre chaîne.

6
H. Idden

En variante de la réponse de DarioOO, vous pourriez obtenir une implémentation peut-être plus rapide d'une correspondance d'expression régulière, en codant un Lex parser pour vos chaînes. Bien que normalement utilisé avec yacc , il s'agit d'un cas où Lex seul fait le travail, et les analyseurs Lex sont généralement très efficaces.

Cette approche peut tomber si toutes vos chaînes sont longues, comme alors un algorithme tel que Aho-Corasick , Commentz-Walter ou Rabin-Karp offrirait probablement des améliorations significatives, et je doute que les implémentations de Lex utilisent un tel algorithme.

Cette approche est plus difficile si vous devez pouvoir configurer les chaînes sans reconfiguration, mais puisque flex est open source, vous pouvez cannibaliser son code.

3
PJTraill

Parce que le nombre de chaînes n'est pas grand (~ 100), vous pouvez utiliser algo suivant:

  1. Calculez la longueur maximale de Word que vous avez. Que ce soit N.
  2. Créer int checks[N]; tableau de somme de contrôle.
  3. Supposons que la somme de contrôle soit la somme de tous les caractères de la phrase de recherche. Ainsi, vous pouvez calculer une telle somme de contrôle pour chaque mot de votre liste (connue au moment de la compilation) et créer std::map<int, std::vector<std::wstring>>, où int est la somme de contrôle de la chaîne, et le vecteur doit contenir toutes vos chaînes avec cette somme de contrôle. Créez un tableau de ces cartes pour chaque longueur (jusqu'à N), cela peut également être fait au moment de la compilation.
  4. Maintenant, passez sur une grosse chaîne par pointeur. Lorsque le pointeur pointe sur le caractère X, vous devez ajouter la valeur de X char à tous les entiers checks, et pour chacun d'eux (nombres de 1 à N) supprimer la valeur du caractère (XK), où K est le nombre d'entiers dans le tableau checks. Ainsi, vous aurez toujours la somme de contrôle correcte pour toute la longueur stockée dans le tableau checks. Après cette recherche sur la carte, existe-t-il des chaînes avec une telle paire (longueur et somme de contrôle), et si elle existe - comparez-la.

Il devrait donner un résultat faussement positif (lorsque la somme de contrôle et la longueur sont égales, mais la phrase ne l'est pas) très rare.

Donc, disons que R est la longueur d'une grosse chaîne. En boucle, il faudra O (R). A chaque étape, vous effectuerez N opérations avec "+" petit nombre (valeur char), N opérations avec "-" petit nombre (valeur char), c'est très rapide. À chaque étape, vous devrez rechercher le compteur dans le tableau checks, et c'est O (1), car il s'agit d'un bloc de mémoire.

De plus, à chaque étape, vous devrez trouver la carte dans le tableau de la carte, qui sera également O (1), car il s'agit également d'un bloc de mémoire. Et à l'intérieur de la carte, vous devrez rechercher une chaîne avec une somme de contrôle correcte pour le journal (F), où F est la taille de la carte, et elle ne contiendra généralement pas plus de 2-3 chaînes, de sorte que nous pouvons en général prétendre que c'est également O (1).

Vous pouvez également vérifier, et s'il n'y a pas de chaînes avec la même somme de contrôle (cela devrait se produire avec de grandes chances avec seulement 100 mots), vous pouvez supprimer la carte du tout, en stockant des paires au lieu de la carte.

Donc, finalement, cela devrait donner O (R), avec un O assez petit. Cette façon de calculer checksum peut être changée, mais c'est assez simple et complètement rapide, avec de très rares réactions faussement positives.

3
Arkady