Algorithme de Rabin-Karp
Lâalgorithme de Rabin-Karp ou algorithme de Karp-Rabin est un algorithme de recherche de sous-chaĂźne crĂ©Ă© par Richard M. Karp et Michael O. Rabin (1987). Cette mĂ©thode recherche un ensemble de motifs donnĂ©s (câest-Ă -dire des sous-chaĂźnes) dans un texte grĂące Ă une fonction de hachage. Lâalgorithme nâest pas beaucoup employĂ© pour les recherches dâune unique sous-chaĂźne mais a une importance thĂ©orique et sâavĂšre trĂšs efficace pour des recherches de multiples sous-chaĂźnes.
Pour un texte dâune longueur de n caractĂšres, et p sous-chaĂźnes dâune longueur totale m, sa complexitĂ© moyenne est en O(n+m) pour une utilisation mĂ©moire en O(p). Mais, dans le pire des cas, sa complexitĂ© est de O(nm), ce qui explique son utilisation relativement limitĂ©e. En comparaison, lâalgorithme de recherche de chaĂźnes dâAho-Corasick a une complexitĂ© asymptotique, dans le pire des cas, en O(n+m) pour une utilisation mĂ©moire en O(m).
L'algorithme de Karp-Rabin a lâavantage dâĂȘtre capable de trouver dans un texte une sous-chaĂźne prĂ©sente dans un ensemble de k chaĂźnes, avec une complexitĂ© moyenne de O(n+m) indĂ©pendamment de la taille de k. Ainsi, son efficacitĂ© pour les recherches de multiples sous-chaĂźnes, sans tenir compte de la casse ou de la ponctuation, le rend utile pour la dĂ©tection de plagiat.
Description
Contrairement Ă lâalgorithme naĂŻf qui compare directement le motif Ă toutes les sous-chaĂźnes du texte caractĂšre par caractĂšre, lâalgorithme de Rabin-Karp va comparer des empreintes du motif aux empreintes des sous-chaĂźnes du texte, de maniĂšre Ă faire des comparaisons et des sauts de sous-chaĂźne plutĂŽt que de simples caractĂšres.
Cet algorithme fonctionne bien dans de nombreux cas rĂ©els, mais peut atteindre des temps relativement longs dans certains cas marginaux comme la recherche dâun motif de 10000 "A" suivis dâun unique "B" dans un texte de 10 millions de "A" â dans ce cas, le temps atteint la pire complexitĂ© en O(mn).
Comparaison avec les autres algorithmes
Lâalgorithme naĂŻf compare toutes les positions possibles :
algo_naif(texte, motif) 1. n â longueur(texte) 2. m â longueur(motif) 3. pour i de 1 Ă n-m+1 faire 4. si texte[i..i+m-1] = motif[1..m] 5. motif trouvĂ© dans le texte Ă la position i 6. motif non trouvĂ©
Lâalgorithme de KnuthâMorrisâPratt utilise un prĂ©calcul pour examiner une seule fois chaque Ă©lĂ©ment du texte (l'augmentation de i d'une unitĂ© est remplacĂ©e par une augmentation de i Ă une position non encore examinĂ©e), avec une complexitĂ© en O(n).
Lâalgorithme de BoyerâMoore saute autant de caractĂšres que possible Ă chaque Ă©chec de comparaison, ne faisant que n/m comparaisons dans le meilleur des cas.
Lâalgorithme de RabinâKarp, lui, vise Ă accĂ©lĂ©rer la comparaison des lignes 5 et 6.
Pseudo-code
La comparaison des empreintes, Ă©voquĂ©e ci-dessus, nâest pas suffisante, car limiter le nombre dâempreintes possibles (pour amĂ©liorer leur comparaison) conduit Ă ce que plusieurs motifs peuvent correspondre Ă la mĂȘme empreinte. Il faut donc, quand les empreintes correspondent, comparer ensuite les chaĂźnes elles-mĂȘmes, ce qui augmente le temps global. Lâobjectif est donc de limiter ces comparaisons de chaĂźnes, grĂące Ă la fonction de hachage (voir ci-dessous).
En supposant que le texte T et le motif M sont reprĂ©sentĂ©s comme des tableaux de caractĂšres, que la longueur de T est supĂ©rieure Ă celle de M, et en se donnant une fonction de hachage hach, lâalgorithme de Rabin-Karp est le suivant :
rabin_karp(texte, motif) 1. n â longueur(texte) 2. m â longueur(motif) 3. hn â hach(texte[1..m]) 4. hm â hach(motif[1..m]) 5. pour i de 0 Ă n-m+1 faire 6. si hn = hm 7. si texte[i..i+m-1] = motif[1..m] 8. motif trouvĂ© dans le texte Ă la position i 9. hn â hach(texte[i+1..i+m]) 10. motif non trouvĂ©
Complexité
Les lignes 3, 4, 7 et 9 sont toutes en O(m). Mais les lignes 3 et 4 ne sont exĂ©cutĂ©es quâune seule fois, et la ligne 7 uniquement quand les empreintes correspondent, ce qui devrait ĂȘtre rare. La ligne 6 est exĂ©cutĂ©e n fois, mais requiert un temps constant. La ligne coĂ»teuse est donc la ligne 9.
Recalculer naĂŻvement lâempreinte pour la sous-chaĂźne T[i+1..i+m] nĂ©cessiterait un temps en O(m), et comme cela est fait Ă chaque boucle, lâalgorithme serait en O(mn), comme lâalgorithme naĂŻf. Lâastuce ici est de remarquer que la variable hn contient dĂ©jĂ lâempreinte de T[i..i+m-1]. Sâil est possible de lâutiliser pour calculer la prochaine empreinte avec un temps fixe, alors le problĂšme serait rĂ©solu.
Pour cela, il faut utiliser une fonction de hachage dĂ©roulante (en). Il sâagit dâune fonction de hachage spĂ©cialement conçue pour permettre une telle opĂ©ration. Un exemple simple est lâaddition des valeurs de chaque caractĂšre de la sous-chaĂźne. Ainsi, il est possible dâutiliser cette formule pour calculer la prochaine empreinte, dans un temps constant :
T[i+1..i+m] = T[i..i+m-1] - T[i] + T[i+m]
Cette fonction simpliste fonctionne, mais risque de produire un passage plus fréquent dans la ligne 7 que des fonctions plus sophistiquées, comme celles présentées dans le chapitre suivant.
Notez bien quâen cas de malchance, ou de trĂšs mauvaise fonction de hachage, la ligne 7 pourrait ĂȘtre exĂ©cutĂ©e n fois, Ă chaque passage dans la boucleâŻ; et comme elle est en O(m), lâalgorithme sera finalement en O(nm).
Choix dâune bonne fonction de hachage
Exemple dâune bonne fonction de hachage
Si lâon reprĂ©sente les caractĂšres comme des nombres dans une base b donnĂ©e (en pratique si lâencodage des caractĂšres se fait sur 8 bits, ce qui donne 256 caractĂšres possibles, on utilisera une base 256) et que lâon choisit un nombre entier q appropriĂ©, une fonction de hachage est :
hash(t) = t modulo q
oĂč t est la reprĂ©sentation du texte comme un nombre dans la base b.
Par exemple, prenons le texte suivant composé de chiffres décimaux :
6 5 8 2 4 6 9 1 3
On choisit la base 10 et le représentant du texte dans cette base sera naturellement le nombre :
658246913
Si lâon choisit le nombre 11, la fonction de hachage sera :
hash(t) = t modulo 11
soit
hash(658246913) = 658246913 modulo 11 = 5
Cette fonction de hachage a la propriĂ©tĂ© de pouvoir calculer facilement lâempreinte de T[i+1..j+1] en fonction de celle de T[i..j]. Dans lâexemple prĂ©cĂ©dent, si lâon veut exprimer hash(582) en fonction de hash(658), on peut constater que
582 = 58Ă10 + 2 = (658 - 600) Ă 10 + 2
dâoĂč
hash(582) = [ (hash(658) - hash(600)) Ă 10 + 2 ] modulo 11
Cet exemple se gĂ©nĂ©ralise dans une base quelconque avec un nombre entier q quelconque. Ainsi, dans le pseudo-code prĂ©cĂ©dent, pour i â©Ÿ 1, on peut procĂ©der ainsi pour mettre Ă jour la variable hT avec lâempreinte de T[i+1..i+lM] en utilisant celle de la sous-chaĂźne prĂ©cĂ©dente, dĂ©jĂ calculĂ©e :
hT â (hT - T[i]ĂblM-1) Ă b + T[i+lM] modulo q
Empreinte de Rabin
Lâempreinte de Rabin est une fonction de hachage roulante populaire et efficace pour lâalgorithme de Rabin-Karp, dont la performance est directement liĂ©e Ă lâefficacitĂ© du calcul des empreintes des sous-chaĂźnes successives du texte. Cette fonction traite chaque sous-chaĂźne comme un nombre dans une base donnĂ©e, en gĂ©nĂ©ral un grand nombre premier. Par exemple, si la sous-chaĂźne est «âŻhaâŻÂ» et la base 101, lâempreinte sera 104 Ă 1011 + 97 Ă 1010 = 10601 (ASCII de 'h' est 104 et celui de 'a' est 97).
LâintĂ©rĂȘt essentiel dâutiliser une telle fonction est quâelle permet de calculer lâempreinte de la sous-chaĂźne suivante Ă partir de la prĂ©cĂ©dente en un nombre constant dâopĂ©rations, indĂ©pendamment de la longueur des sous-chaĂźnes.
Par exemple, avec le texte «âŻabracadabraâŻÂ» et la recherche dâun motif de 3 caractĂšres, lâempreinte de la premiĂšre sous-chaĂźne, «âŻabrâŻÂ», en utilisant la base 101, est :
// ASCII a = 97, b = 98, r = 114. hach(«âŻabrâŻÂ») = (97 Ă 1012) + (98 Ă 1011) + (114 Ă 1010) = 999509
Lâempreinte de la sous-chaĂźne suivante, «âŻbraâŻÂ», depuis lâempreinte de «âŻabrâŻÂ», se calcule en soustrayant le nombre ajoutĂ© pour le premier 'a' de «âŻabrâŻÂ», soit 97 Ă 1012, en multipliant par la base et en ajoutant le dernier 'a' de «âŻbraâŻÂ», soit 97 Ă 1010, de cette façon :
// base empreinte ancien 'a' nouveau 'a' hach(«âŻbraâŻÂ») = [101 Ă (999509 - (97 Ă 1012))] + (97 Ă 1010) = 1011309
Si les sous-chaĂźnes en question sont longues, cet algorithme Ă©conomise beaucoup de temps par rapport Ă dâautres fonctions de hachage.
En thĂ©orie, il existe dâautres algorithmes qui peuvent permettre un recalcul facile, comme multiplier entre elles les valeurs ASCII de chaque caractĂšre de maniĂšre que le dĂ©calage de la sous-chaĂźne nâentraĂźne quâune division par le premier caractĂšre et une multiplication par le dernier. La limite vient alors de la restriction de taille du type entier et de la nĂ©cessitĂ© dâutiliser lâarithmĂ©tique modulaire pour rĂ©duire les rĂ©sultats des empreintes (voir lâarticle fonction de hachage). A contrario, les fonctions de hachage simples ne produisent pas trĂšs vite de grands nombres mais, comme lâaddition simple des valeurs ASCII, risquent de provoquer de nombreuses collisions dâempreinte et donc de ralentir lâalgorithme. DâoĂč la prĂ©fĂ©rence pour la fonction dĂ©crite ci-dessus pour lâalgorithme RabinâKarp.
Extension de lâalgorithme Ă la recherche de multiples sous-chaĂźnes
Lâalgorithme de RabinâKarp est infĂ©rieur Ă celui de KnuthâMorrisâPratt, de BoyerâMoore ou dâautres, pour la recherche dâun motif unique, du fait de son comportement dans les pires cas. Cependant, câest un algorithme Ă privilĂ©gier pour la recherche de motifs multiples.
Ainsi, pour rechercher un grand nombre k de motifs de longueur fixe dans un texte, on peut crĂ©er une variante simple de lâalgorithme RabinâKarp qui utilise un filtre de Bloom ou une structure de donnĂ©es Ensemble pour vĂ©rifier si lâempreinte dâune chaĂźne donnĂ©e appartient Ă un ensemble dâempreintes des motifs recherchĂ©s, avec le texte T, un ensemble de motifs M de longueur m :
rabin_karp_ensemble(T, M, m) 1. n â longueur(T) 2. hm â ensemble vide 3. pour chaque S de lâensemble M faire 4. ajoute hach(S[1..m]) Ă hm 5. ht â hach(T[1..m]) 6. pour i de 1 Ă n-m+1 faire 7. si ht â hm et T[i..i+m-1] â M 8. rĂ©sultat trouvĂ© : i 9. ht â hach(T[i+1..i+m]) 10. rĂ©sultat non trouvĂ©
Par rapport Ă la façon naĂŻve de rechercher k motifs en rĂ©pĂ©tant une recherche mono-motif en O(n), ce qui aboutit Ă une recherche en O(n k), la variante de lâalgorithme ci-dessus peut trouver tous les k motifs en O(n+k) attendus, car une table dâempreintes permet de vĂ©rifier si lâempreinte dâune sous-chaĂźne est celle dâun des motifs en O(1).
Notes et références
- Richard M. Karp et Michael O. Rabin, « Efficient randomized pattern-matching algorithms », IBM Journal of Research and Development, vol. 31, no 2,â , p. 249â260 (DOI 10.1147/rd.312.0249, lire en ligne, consultĂ© le )
- (en) Cormen, Thomas H., Leiserson, Charles E., Rivest, Ronald L. et Stein, Clifford, Introduction to Algorithms, Cambridge, Massachusetts, MIT Press, , 2nd Ă©d. (1re Ă©d. 1990), 1180 p. (ISBN 978-0-262-03293-3), « The RabinâKarp algorithm », p. 911â916.