OK, donc je ne ressemble pas à un idiot, je vais énoncer le problème/les exigences plus explicitement:
NULL
si aucune correspondance n'est trouvée.... ainsi que ce que je veux dire par "le plus rapide":
O(n)
où n
= longueur de la botte de foin. (Mais il est possible d'utiliser des idées d'algorithmes qui sont normalement O(nm)
(par exemple, un hachage en roulis) si elles sont combinées avec un algorithme plus robuste pour donner des résultats déterministes O(n)
).if (!needle[1])
etc. sont correctes) pire que l'algorithme naïf de la force brute, en particulier sur les aiguilles très courtes qui sont probablement le cas le plus fréquent. (Il est inutile de prendre beaucoup de temps pour le prétraitement sans condition, tout comme d’essayer d’améliorer le coefficient linéaire pour les aiguilles pathologiques aux dépens des aiguilles probables.)Ma mise en œuvre actuelle est environ 10% plus lente et 8 fois plus rapide (selon l'entrée) que la mise en œuvre de Two-Way par la glibc.
Mise à jour: Mon algorithme optimal actuel est le suivant:
strchr
.Les grandes questions qui me restent à l'esprit sont les suivantes:
O(m)
(où m
est la longueur de l'aiguille) dans les algorithmes spatiaux pourraient être utilisés pour m<100
ou alors. Il serait également possible d’utiliser des algorithmes quadratiques dans le cas le plus défavorable s’il est facile de tester des aiguilles qui ne nécessitent que le temps linéaire.Points bonus pour:
Note: Je connais bien la plupart des algorithmes existants, mais je ne sais pas dans quelle mesure ils fonctionnent correctement. Voici une bonne référence pour que les gens ne me donnent pas de références sur les algorithmes sous forme de commentaires/réponses: http://www-igm.univ-mlv.fr/~lecroq/string/index.html
Créez une bibliothèque de tests sur les aiguilles et les meules de foin. Profil des tests sur plusieurs algorithmes de recherche, y compris la force brute. Choisissez celui qui fonctionne le mieux avec vos données.
Boyer-Moore utilise une table de caractères incorrects avec une bonne table de suffixes.
Boyer-Moore-Horspool utilise une table de caractères incorrects.
Knuth-Morris-Pratt utilise une table de correspondance partielle.
Rabin-Karp utilise des hachages en cours d'exécution.
Ils négocient tous des frais généraux pour des comparaisons réduites à un degré différent, de sorte que la performance réelle dépendra de la longueur moyenne de l'aiguille et de la botte de foin. Plus la surcharge initiale est importante, mieux c'est avec des entrées plus longues. Avec des aiguilles très courtes, la force brute peut l'emporter.
Modifier:
Un algorithme différent peut être préférable pour trouver des paires de bases, des phrases anglaises ou des mots simples. S'il y avait un meilleur algorithme pour toutes les entrées, il aurait été rendu public.
Pensez à la petite table suivante. Chaque point d'interrogation peut avoir un meilleur algorithme de recherche différent.
short needle long needle
short haystack ? ?
long haystack ? ?
Cela devrait vraiment être un graphique, avec une gamme d'entrées plus courtes à plus longues sur chaque axe. Si vous tracez chaque algorithme sur un tel graphique, chacun aura une signature différente. Certains algorithmes souffrent de nombreuses répétitions dans le motif, ce qui peut affecter des utilisations telles que la recherche de gènes. Certains autres facteurs qui affectent les performances globales recherchent plusieurs fois le même motif et recherchent différents motifs en même temps.
Si j'avais besoin d'un ensemble d'échantillons, je pense que je pourrais gratter un site tel que Google ou Wikipédia, puis supprimer le code HTML de toutes les pages de résultats. Pour un site de recherche, tapez un mot puis utilisez l’une des expressions de recherche suggérées. Choisissez quelques langues différentes, le cas échéant. En utilisant des pages Web, tous les textes seraient courts à moyens, alors fusionnez suffisamment de pages pour obtenir des textes plus longs. Vous pouvez également trouver des livres du domaine public, des archives légales et d'autres corps de texte volumineux. Ou simplement générer du contenu aléatoire en choisissant des mots dans un dictionnaire. Mais le but du profilage est de tester le type de contenu que vous allez rechercher, utilisez donc des échantillons réels si possible.
Je suis parti court et long vague. Pour l’aiguille, je pense à moins de 8 caractères, à moins de 64 caractères et à moins de 1k. Pour la meule de foin, je pense à court comme moins de 2 ^ 10, moyen comme moins de 2 ^ 20, et long que jusqu'à 2 ^ 30 caractères.
Publié en 2011, je pense que cela pourrait très bien être le "Alignement de chaînes en temps réel simple en temps réel" par Dany Breslauer, Roberto Grossi et Filippo Mignosi.
En 2014, les auteurs ont publié cette amélioration: Vers une correspondance optimale des chaînes compactées .
Le lien http://www-igm.univ-mlv.fr/~lecroq/string/index.html vous pointez est une excellente source et un résumé des meilleures correspondances de chaînes connues et documentées algorithmes.
Les solutions à la plupart des problèmes de recherche impliquent des compromis en ce qui concerne les frais généraux de pré-traitement, les contraintes de temps et d'espace. Aucun algorithme ne sera optimal ou pratique dans tous les cas.
Si vous avez pour objectif de concevoir un algorithme spécifique pour la recherche de chaînes, ignorez le reste de ce que j'ai à dire. Si vous souhaitez développer une routine de service de recherche de chaînes généralisée, procédez comme suit:
Passez un peu de temps à examiner les forces et les faiblesses des algorithmes que vous avez déjà mentionnés. Effectuez la révision avec l'objectif de trouver un ensemble d'algorithmes qui couvrent l'étendue et la portée des recherches de chaînes qui vous intéressent. Ensuite, créez un sélecteur de recherche frontal basé sur une fonction de classificateur afin de cibler le meilleur algorithme pour les entrées données. De cette façon, vous pouvez utiliser l'algorithme le plus efficace pour faire le travail. Ceci est particulièrement efficace lorsqu'un algorithme est très bon pour certaines recherches mais se dégrade peu. Par exemple, la force brute est probablement la meilleure solution pour les aiguilles de longueur 1, mais se dégrade rapidement à mesure que la longueur d’aiguille augmente. Le sustik-moore algoritim peut devenir plus efficace (sur de petits alphabets), puis pour les aiguilles plus longues plus grands alphabets, les algorithmes KMP ou Boyer-Moore peuvent être meilleurs. Ce ne sont que des exemples pour illustrer une stratégie possible.
L'approche à algorithmes multiples n'est pas une nouvelle idée. Je crois qu'il a été utilisé par quelques logiciels de tri/recherche commerciaux (par exemple, SYNCSORT couramment utilisé sur les ordinateurs centraux implémente plusieurs algorithmes de tri et utilise des méthodes heuristiques pour choisir la "meilleure" solution pour les entrées fournies).
Chaque algorithme de recherche se décline en plusieurs variantes qui peuvent avoir un impact significatif sur ses performances, comme le montre par exemple paper .
Analysez votre service pour classer les domaines dans lesquels des stratégies de recherche supplémentaires sont nécessaires ou pour ajuster plus efficacement votre fonction de sélecteur. Cette approche n’est ni rapide ni facile, mais si elle est bien menée, elle peut produire de très bons résultats.
J'ai été surpris de voir notre rapport technique cité dans cette discussion; Je suis l'un des auteurs de l'algorithme nommé Sustik-Moore ci-dessus. (Nous n'avons pas utilisé ce terme dans notre document.)
Je voulais souligner ici que pour moi la caractéristique la plus intéressante de l’algorithme est qu’il est assez simple de prouver que chaque lettre est examinée au plus une fois. Pour les versions antérieures de Boyer-Moore, ils ont prouvé que chaque lettre était examinée au plus 3 fois et plus tard au plus 2 fois, et que ces épreuves étaient plus compliquées (voir les citations sur papier). Par conséquent, je vois aussi une valeur didactique à présenter/étudier cette variante.
Nous décrivons également dans cet article d’autres variantes axées sur l’efficacité tout en assouplissant les garanties théoriques. Il s’agit d’un court article dont le contenu devrait être compréhensible pour un diplômé moyen du secondaire, à mon avis.
Notre objectif principal était de porter cette version à l'attention de ceux qui pourraient encore l'améliorer. La recherche de chaînes comporte de nombreuses variantes et nous ne pouvons pas à lui seul imaginer où cette idée pourrait apporter des avantages. (Texte fixe et changement de modèle, modèle différent de modèle fixe, prétraitement possible/impossible, exécution parallèle, recherche de sous-ensembles correspondants dans des textes volumineux, erreurs possibles, quasi-correspondances, etc.)
L'algorithme de recherche de sous-chaîne le plus rapide dépendra du contexte:
Le document de 2010 "Le problème exact de correspondance des chaînes: une évaluation expérimentale complète" fournit des tableaux avec des exécutions de 51 algorithmes (avec différentes tailles d’alphabet et de longueurs d’aiguilles), afin que vous puissiez choisir le meilleur algorithme pour votre contexte.
Tous ces algorithmes ont des implémentations C, ainsi qu'une suite de tests, ici:
Je sais que c'est une vieille question, mais la plupart des mauvaises tables sont à un seul caractère. Si cela convient à votre ensemble de données (par exemple, en particulier si ce sont des mots écrits) et si vous disposez de l'espace disponible, vous pouvez obtenir une accélération spectaculaire en utilisant une mauvaise table de décalage composée de n-grammes plutôt que de caractères uniques.
Une très bonne question. Ajoutez juste quelques petits morceaux ...
Quelqu'un parlait de correspondance de séquence d'ADN. Mais pour les séquences d’ADN, nous avons généralement pour tâche de construire une structure de données (tableau de suffixes, arborescence de suffixes ou index FM) pour la botte de foin et de faire correspondre de nombreuses aiguilles à cette dernière. C'est une question différente.
Ce serait vraiment bien si quelqu'un souhaitait analyser différents algorithmes. Il existe de très bons points de repère sur la compression et la construction de tableaux de suffixes, mais je n'ai pas encore vu de point de repère sur la correspondance des chaînes. Les candidats potentiels de la botte de foin pourraient provenir du repère SACA .
Il y a quelques jours, je testais l'implémentation de Boyer-Moore depuis la page que vous avez recommandée (EDIT: j'ai besoin d'un appel de fonction comme memmem (), mais ce n'est pas une fonction standard. J'ai donc décidé d'implémenter il). Mon programme d'analyse comparative utilise une botte de foin aléatoire. Il semble que l’implémentation de Boyer-Moore dans cette page soit plus rapide que celle de memmem () de glibc et de strnstr () de Mac. Si cela vous intéresse, la mise en oeuvre est ici et le code de benchmarking est ici . Ce n'est certainement pas un repère réaliste, mais c'est un début.
J'ai récemment découvert un outil de Nice permettant de mesurer les performances des différents algos disponibles: http://www.dmi.unict.it/~faro/smart/index.php
Vous pourriez le trouver utile. De plus, si je devais répondre rapidement à un algorithme de recherche de sous-chaînes, je choisirais Knuth-Morris-Pratt.
Un algorithme plus rapide "Rechercher un seul caractère correspondant" (ala strchr
).
Notes importantes:
Ces fonctions utilisent un "nombre/nombre de zéros (en tête)" "gcc
compilateur intrinsèque- __builtin_ctz
. Ces fonctions ne sont susceptibles d’être rapides que sur les machines qui ont une instruction qui exécute cette opération (c’est-à-dire x86, ppc, arm).
Ces fonctions supposent que l’architecture cible peut effectuer des charges non alignées de 32 et 64 bits. Si votre architecture cible ne prend pas cela en charge, vous devrez ajouter une logique de démarrage pour aligner correctement les lectures.
Ces fonctions sont neutres par rapport au processeur. Si le CPU cible a des instructions vectorielles, vous pourrez peut-être faire (beaucoup) mieux. Par exemple, la fonction strlen
ci-dessous utilise SSE3 et peut être modifiée de manière triviale en XOR les octets analysés pour rechercher un octet autre que 0
. Tests d'évaluation effectués sur un ordinateur portable Core 2 à 2,66 GHz fonctionnant sous Mac OS X 10.6 (x86_64):
strchr
findFirstByte64
strlen
... une version 32 bits:
#ifdef __BIG_ENDIAN__
#define findFirstZeroByte32(x) ({ uint32_t _x = (x); _x = ~(((_x & 0x7F7F7F7Fu) + 0x7F7F7F7Fu) | _x | 0x7F7F7F7Fu); (_x == 0u) ? 0 : (__builtin_clz(_x) >> 3) + 1; })
#else
#define findFirstZeroByte32(x) ({ uint32_t _x = (x); _x = ~(((_x & 0x7F7F7F7Fu) + 0x7F7F7F7Fu) | _x | 0x7F7F7F7Fu); (__builtin_ctz(_x) + 1) >> 3; })
#endif
unsigned char *findFirstByte32(unsigned char *ptr, unsigned char byte) {
uint32_t *ptr32 = (uint32_t *)ptr, firstByte32 = 0u, byteMask32 = (byte) | (byte << 8);
byteMask32 |= byteMask32 << 16;
while((firstByte32 = findFirstZeroByte32((*ptr32) ^ byteMask32)) == 0) { ptr32++; }
return(ptr + ((((unsigned char *)ptr32) - ptr) + firstByte32 - 1));
}
... et une version 64 bits:
#ifdef __BIG_ENDIAN__
#define findFirstZeroByte64(x) ({ uint64_t _x = (x); _x = ~(((_x & 0x7F7F7F7F7f7f7f7full) + 0x7F7F7F7F7f7f7f7full) | _x | 0x7F7F7F7F7f7f7f7full); (_x == 0ull) ? 0 : (__builtin_clzll(_x) >> 3) + 1; })
#else
#define findFirstZeroByte64(x) ({ uint64_t _x = (x); _x = ~(((_x & 0x7F7F7F7F7f7f7f7full) + 0x7F7F7F7F7f7f7f7full) | _x | 0x7F7F7F7F7f7f7f7full); (__builtin_ctzll(_x) + 1) >> 3; })
#endif
unsigned char *findFirstByte64(unsigned char *ptr, unsigned char byte) {
uint64_t *ptr64 = (uint64_t *)ptr, firstByte64 = 0u, byteMask64 = (byte) | (byte << 8);
byteMask64 |= byteMask64 << 16;
byteMask64 |= byteMask64 << 32;
while((firstByte64 = findFirstZeroByte64((*ptr64) ^ byteMask64)) == 0) { ptr64++; }
return(ptr + ((((unsigned char *)ptr64) - ptr) + firstByte64 - 1));
}
Edit 2011/06/04 L'OP signale dans les commentaires que cette solution a un "bogue insurmontable":
il peut lire après l'octet ou le terminateur null recherché, ce qui pourrait accéder à une page non mappée ou à une page sans autorisation de lecture. Vous ne pouvez simplement pas utiliser de grandes lectures dans des fonctions de chaîne à moins qu'elles ne soient alignées.
Ceci est techniquement vrai, mais s’applique à pratiquement tous les algorithmes opérant sur des fragments de plus d’un octet, y compris la méthode suggérée par le PO dans les commentaires:
Une implémentation typique de
strchr
n’est pas naïve, mais bien plus efficace que ce que vous avez donné. Voir la fin de ceci pour l'algorithme le plus largement utilisé: http://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord
De plus, cela n'a vraiment rien à voir avec alignement en soi. Certes, cela pourrait potentiellement causer le comportement décrit dans la plupart des architectures courantes utilisées, mais cela concerne davantage les détails de mise en œuvre de la microarchitecture: si la lecture non alignée chevauche une limite de 4K (encore une fois, typique), cette lecture provoquera un programme défaut de terminaison si la limite de page suivante de 4K n'est pas mappée.
Mais ce n'est pas un "bogue" dans l'algorithme donné dans la réponse, ce comportement est dû au fait que des fonctions comme strchr
et strlen
n'acceptent pas l'argument length
pour lier le taille de la recherche. Recherche en cours char bytes[1] = {0x55};
, Qui, aux fins de notre discussion, se trouve tout à fait à la fin d’un 4K VM la limite de la page et la page suivante est non mappée, avec strchr(bytes, 0xAA)
(où strchr
est une implémentation octet à la fois) plantera exactement de la même manière. Idem pour strchr
cousin apparenté strlen
.
Sans l'argument length
, il n'y a aucun moyen de savoir quand vous devez quitter l'algorithme haute vitesse et revenir à un algorithme octet par octet. Un "bogue" beaucoup plus probable serait de lire "au-delà de la taille de l'allocation", ce qui aboutit techniquement à undefined behavior
Selon les différentes normes du langage C, et serait signalé comme une erreur par quelque chose comme valgrind
.
En résumé, tout ce qui fonctionne sur des morceaux d'octet supérieurs à la vitesse va plus vite, comme cela répond au code et au code indiqué par l'OP, mais doit avoir une sémantique de lecture à l'octet près sera probablement "bogué" s'il n'y a pas length
argument pour contrôler le ou les cas (s) de la "dernière lecture".
Le code dans cette réponse est un noyau permettant de trouver rapidement le premier octet dans un bloc naturel de la taille d'un CPU du processeur si le processeur cible a une instruction rapide ctz
. Il est trivial d’ajouter des choses comme s’assurer que cela ne fonctionne que sur des limites naturelles correctement alignées, ou une certaine forme de length
bound, ce qui vous permettrait de sortir du noyau à grande vitesse et de passer à un octet plus lent. -byte chèque.
Le PO indique également dans les commentaires:
Quant à votre optimisation en ctz, elle ne fait de différence que pour l’opération de queue O(1)). Elle pourrait améliorer les performances avec des chaînes minuscules (par exemple,
strchr("abc", 'a');
mais certainement pas avec des chaînes. de toute taille majeure.
Que cette affirmation soit vraie ou non dépend beaucoup de la microarchitecture en question. En utilisant le modèle canonique de pipeline RISC à 4 étapes, il est presque certainement vrai. Mais il est extrêmement difficile de dire si cela est vrai pour un processeur super scalaire hors service dans lequel la vitesse du cœur peut totalement réduire la vitesse de la mémoire en continu. Dans ce cas, il est non seulement plausible, mais également courant, qu’il existe un écart important entre "le nombre d’instructions pouvant être retirées" et "le nombre d’octets pouvant être transmis en continu", de sorte que vous ayez "le nombre d'instructions pouvant être retirées pour chaque octet pouvant être diffusées ". Si cela est suffisant, l'instruction ctz
+ peut être effectuée "gratuitement".
Voici implémentation de la recherche de Python , utilisée dans tout le noyau. Les commentaires indiquent qu’il utilise une table compressée delta 1 de boyer-moore .
J'ai moi-même effectué des recherches approfondies sur la recherche de chaînes, mais c'était pour plusieurs chaînes de recherche. Les implémentations d'assemblage de Horspool et Bitap peuvent souvent se défendre contre des algorithmes tels que Aho-Corasick pour les faibles nombres de motifs.
Vous pouvez implémenter, par exemple, 4 algorithmes différents. Toutes les M minutes (à déterminer de manière empirique) exécutent les 4 données réelles actuelles. Accumuler des statistiques sur N courses (également à déterminer). Ensuite, utilisez uniquement le gagnant pour les prochaines minutes.
Consignez les statistiques sur les gains pour pouvoir remplacer les algorithmes qui ne gagnent jamais par de nouveaux. Concentrez vos efforts d'optimisation sur la routine la plus gagnante. Portez une attention particulière aux statistiques après toute modification apportée au matériel, à la base de données ou à la source de données. Si possible, incluez ces informations dans le journal des statistiques afin que vous n'ayez pas à les calculer à partir de la date/heure du journal.
L'algorithme bidirectionnel que vous avez mentionné dans votre question (ce qui est d'ailleurs incroyable!) A récemment été amélioré pour fonctionner efficacement sur les mots à plusieurs octets à la fois: Correspondance optimale de la chaîne de caractères .
Je n'ai pas lu le document en entier, mais il semble qu'ils s'appuient sur quelques nouvelles instructions spéciales de la CPU (incluses dans, par exemple, SSE 4.2)) étant O(1) pour leur revendication de complexité temporelle, mais s'ils ne sont pas disponibles, ils peuvent les simuler en temps O (log log w) pour les mots w-bit, ce qui ne sonne pas trop mal.
Utilisez stdlib strstr
:
char *foundit = strstr(haystack, needle);
C'était très rapide, il ne m'a fallu qu'environ 5 secondes pour taper.
Vous pouvez également avoir plusieurs repères avec plusieurs types de chaînes, ce qui peut avoir un impact important sur les performances. Les algorithmes fonctionneront différemment en recherchant le langage naturel (et même dans ce cas, il pourrait encore y avoir des distinctions fines en raison des morphologies différentes), des chaînes d'ADN ou des chaînes aléatoires, etc.
La taille de l'alphabet jouera un rôle dans de nombreux algues, tout comme la taille des aiguilles. Par exemple, Horspool fait du bon texte anglais, mais pas de l’ADN en raison de la taille différente de l’alphabet, ce qui rend la vie difficile pour la règle du caractère médiocre. L'introduction du bon suffixe allieviates ceci grandement.
Je ne sais pas si c'est le meilleur, mais j'ai une bonne expérience avec Boyer-Moore .
Cela ne répond pas directement à la question, mais si le texte est très volumineux, pourquoi ne pas le diviser en sections qui se chevauchent (chevauchement d'une longueur de motif), puis rechercher simultanément les sections à l'aide de fils. En ce qui concerne l’algorithme le plus rapide, Boyer-Moore-Horspool est selon moi l’une des variantes les plus rapides, sinon la plus rapide, de Boyer-Moore. J'ai posté quelques variantes de Boyer-Moore (je ne connais pas leur nom) dans cette rubrique Algorithme plus rapide que la recherche BMH (Boyer – Moore – Horspool) .