AccueilđŸ‡«đŸ‡·Chercher

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

    Cet article est issu de wikipedia. Text licence: CC BY-SA 4.0, Des conditions supplĂ©mentaires peuvent s’appliquer aux fichiers multimĂ©dias.