• Aucun résultat trouvé

8.4 Conclusions

Nous venons de voir comment stocker des informations pr´esentant des liens entre elles. Ce n’est qu’une partie de l’histoire. Dans les bases de donn´ees, on stocke des informations pouvant avoir des liens compliqu´es entre elles. Pensez `a une carte des villes de France et des routes entre elles, par exemple. Des structures de donn´ees plus complexes seront d´ecrites dans les autres cours (graphes par exemple) qui permettront de r´esoudre des probl`emes plus complexes : comment aller de Paris `a Toulouse en le moins de temps possible, etc.

Recherche exhaustive

Ce que l’ordinateur sait faire de mieux, c’est traiter tr`es rapidement une quantit´e gigantesque de donn´ees. Cela dit, il y a des limites `a tout, et le but de ce chapitre est d’expliquer sur quelques cas ce qu’il est raisonnable d’attendre comme temps de r´esolution d’un probl`eme. Cela nous permettra d’insister sur le coˆut des algorithmes et de la fa¸con de les mod´eliser.

9.1 Rechercher dans du texte

Commen¸cons par un probl`eme pour lequel de bonnes solutions existent.

Rechercher une phrase dans un texte est une tˆache que l’on demande `a n’importe quel programme de traitement de texte, `a un navigateur, un moteur de recherche, etc. C’est ´egalement une part importante du travail accompli r´eguli`erement en bio-informatique.

Vues les quantit´es de donn´ees gigantesques que l’on doit parcourir, il est crucial de faire cela le plus rapidement possible. Le but de cette section est de pr´esenter quelques algorithmes qui accomplissent ce travail.

Pour mod´eliser le probl`eme, nous supposons que nous travaillons sur un texteT (un tableau de caract`ereschar[], plutˆot qu’un objet de typeStringpour all´eger un peu les programmes) de longueurndans lequel nous recherchons un motifM (un autre tableau de caract`eres) de longueur m que nous supposerons plus petit que n. Nous appelerons occurence en position i>0 la propri´et´e queT[i]=M[0], . . . ,T[i+m-1]=M[m-1].

Recherche na¨ıve

C’est l’id´ee la plus naturelle : on essaie toutes les positions possibles du motif en dessous du texte. Comment tester qu’il existe une occurrence en position i? Il suffit d’utiliser un indice j qui va servir `a comparerM[j] `aT[i+j]de proche en proche : static boolean occurrence(char[] T, char[] M, int i){

for(int j = 0; j < M.length; j++) if(T[i+j] != M[j]) return false;

return true;

}

Nous utilisons cette primitive dans la fonction suivante, qui teste toutes les occu-rences possibles :

101

static void naif(char[] T, char[] M){

System.out.print("Occurrences en position :");

for(int i = 0; i < T.length-M.length; i++) if(occurrence(T, M, i))

System.out.print(" "+i+",");

System.out.println("");

}

Si T contient les caract`eres de la chaˆıne "il fait beau aujourd’hui"et M ceux de

"au", le programme affichera

Occurrences en position: 10, 13,

Le nombre de comparaisons de caract`eres effectu´ees est (n−m)m, puisque chacun desn−mtests en demandem. Simest n´egligeable devantn, on obtient un nombre de l’ordre de nm. Le but de la section qui suit est de donner un algorithme faisant moins de comparaisons.

Algorithme lin´eaire de Karp-Rabin

Supposons que S soit une fonction (non n´ecessairement injective) qui donne une valeur num´erique `a une chaˆıne de caract`eres quelconque, que nous appeleronssignature: nous en donnons deux exemples ci-dessous. Si deux chaˆınes de caract`eres C1 et C2sont identiques, alors S(C1) =S(C2). R´eciproquement, siS(C1)6=S(C2), alorsC1 ne peut ˆetre ´egal `aC2.

Le principe de l’algorithme de Karp-Rabin utilise cette id´ee de la fa¸con suivante : on remplace le test d’occurrenceT[i..i+m−1] =M[0..m−1] par S(T[i..i+m−1]) = S(M[0..m−1]). Le membre de droite de ce test est constant, on le pr´ecalcule donc et il ne reste plus qu’`a effectuer n−mcalculs deS et comparer la valeurS(T[i..i+m−1]) `a cette constante. En cas d’´egalit´e, on soup¸conne une occurrence et on la v´erifie `a l’aide de la fonction occurrence pr´esent´ee ci-dessus. Le nombre de calculs `a effectuer est simplement 1 +n−m ´evaluations deS.

Voici la fonction qui implante cette id´ee. Nous pr´eciserons la fonction de signature S plus loin (cod´ee ici sous la forme d’une fonctionsignature) :

static void KR(char[] T, char[] M){ int n, m;

}

System.out.println("");

}

La fonction de signature est critique. Il est difficile de fabriquer une fonction qui soit `a la fois injective et rapide `a calculer. On se contente d’approximations. Soit Xun texte de longueur m. En Java ou d’autres langages proches, il est g´en´eralement facile de convertir un caract`ere en nombre. Le langage unicode repr´esente un caract`ere sur 16 bits et le passage du caract`ere c`a l’entier est simplement(int)c. La premi`ere fonction

`a laquelle on peut penser est celle qui se contente de faire la somme des caract`eres repr´esent´es par des entiers :

static long signature(char[] X, int m, int i){ long s = 0;

for(int j = i; j < i+m; j++) s += (long)X[j];

return s;

}

Avec cette fonction, le programme affichera : Occurrences en position: 10, 13, [18],

o`u on a indiqu´e les fausses occurrences par des crochets. On verra plus loin comment diminuer ce nombre.

Pour acc´el´erer le calcul de la signature, on remarque que l’on peut faire cela de mani`ere incr´ementale. Plus pr´ecis´ement :

S(X[1..m]) =S(X[0..m−1])−X[0] +X[m],

ce qui remplace m additions par 1 addition et 1 soustraction `a chaque ´etape (on a confondu X[i] et sa valeur en tant que caract`ere).

Une fonction de signature qui pr´esente moins de collisions s’obtient `a partir de ce qu’on appelle une fonction de hachage, dont la th´eorie ne sera pas pr´esent´ee ici. On prend p un nombre premier etB un entier. La signature est alors :

S(X[0..m−1]) = (X[0]Bm−1+· · ·+X[m−1]B0) modp.

On montre que la probabilit´e de collisions est alors 1/p. Typiquement, B = 216, p = 231−1 = 2147483647.

L’int´erˆet de cette fonction est qu’elle permet un calcul incr´emental, puisque : S(X[i+ 1..i+m]) =BS(X[i..i+m−1])−X[i]Bm+X[i+m], qui s’´evalue d’autant plus rapidement que l’on a pr´ecalcul´e Bmmodp.

Le nombre de calculs effectu´es estO(n+m), ce qui repr´esente une am´elioration notable par rapport `a la recherche na¨ıve.

Les fonctions correspondantes sont :

static long B = ((long)1) << 16, p = 2147483647;

// calcul de S(X[i..i+m-1])

static long signature2(char[] X, int i, int m){

long s = 0;

static long signatureIncr(char[] X, int m, int i, long s, long Bm){ long ss;

static void KR2(char[] T, char[] M){ int n, m;

long Bm, hT, hM;

n = T.length;

m = M.length;

System.out.print("Occurrences en position :");

hM = signature2(M, 0, m);

// calcul de Bm = B^m mod p Bm = B;

for(int i = 2; i <= m; i++) Bm = (Bm * B) % p;

hT = signature2(T, 0, m);

for(int i = 0; i < n-m; i++){ if(i > 0)

hT = signatureIncr(T, m, i-1, hT, Bm);

if(hT == hM){

Cette fois, le programme ne produit plus de collisions : Occurrences en position : 10, 13,

Remarques compl´ementaires

Des algorithmes plus rapides existent, comme par exemple ceux de Knuth-Morris-Pratt ou Boyer-Moore. Il est possible ´egalement de chercher des chaˆınes proches du motif donn´e, par exemple en cherchant `a minimiser le nombre de lettres diff´erentes entre les deux chaˆınes.

La recherche de chaˆınes est tellement importante qu’Unix poss`ede une commande grep qui permet de rechercher un motif dans un fichier. `A titre d’exemple :

unix% grep int Essai.java

affiche les lignes du fichierEssai.java qui contiennent le motifint. Pour afficher les lignes ne contenant pas int, on utilise :

unix% grep -v int Essai.java

On peut faire des recherches plus compliqu´ees, comme par exemple rechercher les lignes contenant un 0 ou un 1 :

unix% grep [01] Essai.java Le dernier exemple est :

unix% grep "int .*[0-9]" Essai.java

qui est cas d’expression r´eguli`ere. Elles peuvent ˆetre d´ecrites en termes d’automates, qui sont ´etudi´es en cours d’Informatique Fondamentale. Pour plus d’informations sur la commande grep, tapez man grep.