Vous êtes sur la page 1sur 124

e ments dAlgorithmique El Ensta - in101

Franc oise Levy-dit-Vehel & Matthieu Finiasz Ann ee 2011-2012

Table des mati` eres


1 Complexit e 1.1 D enitions . . . . . . . . . . . . . . . . . . . . . 1.1.1 Comparaison asymptotique de fonctions 1.1.2 Complexit e dun algorithme . . . . . . . 1.1.3 Complexit e spatiale, complexit e variable 1.2 Un seul probl` eme, quatre algorithmes . . . . . . 1.3 Un premier algorithme pour le tri . . . . . . . . 2 R ecursivit e 2.1 Conception des algorithmes . . . . . . . . . 2.2 Probl` eme des tours de Hano . . . . . . . . . 2.3 Algorithmes de tri . . . . . . . . . . . . . . 2.3.1 Le tri fusion . . . . . . . . . . . . . . 2.3.2 Le tri rapide . . . . . . . . . . . . . . 2.3.3 Complexit e minimale dun algorithme 2.4 R esolution d equations de r ecurrence . . . . 2.4.1 R ecurrences lin eaires . . . . . . . . . 2.4.2 R ecurrences de partitions . . . . . 2.5 Compl ements . . . . . . . . . . . . . . . . 2.5.1 R ecursivit e terminale . . . . . . . 2.5.2 D er ecursication dun programme 2.5.3 Ind ecidabilit e de la terminaison . . 1 1 1 2 3 6 9 11 11 12 15 15 17 21 22 22 24 28 28 28 30 35 35 36 39 40 41 43 44 44 45

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . . . . . . . . . . . . . . . . de tri . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

3 Structures de Donn ees 3.1 Tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.1 Allocation m emoire dun tableau . . . . . . . . . 3.1.2 Compl ement : allocation dynamique de tableau 3.2 Listes cha n ees . . . . . . . . . . . . . . . . . . . . . . . . 3.2.1 Op erations de base sur une liste . . . . . . . . . . 3.2.2 Les variantes : doublement cha n ees, circulaires... 3.2.3 Conclusion sur les listes . . . . . . . . . . . . . . 3.3 Piles & Files . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.1 Les piles . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

3.3.2

Les les . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

46 49 49 50 51 53 55 57 59 59 59 61 62 63 64 66 72 77 78 78 79 85 85 87 87 88 89 92 93 93 97 99 99 100 102

4 Recherche en table 4.1 Introduction . . . . . . . . . . . . . . 4.2 Table ` a adressage direct . . . . . . . 4.3 Recherche s equentielle . . . . . . . . 4.4 Recherche dichotomique . . . . . . . 4.5 Tables de hachage . . . . . . . . . . . 4.6 Tableau r ecapitulatif des complexit es

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

5 Arbres 5.1 Pr eliminaires . . . . . . . . . . . . . . . . . . . . . . 5.1.1 D enitions et terminologie . . . . . . . . . . . 5.1.2 Premi` eres propri et es . . . . . . . . . . . . . . 5.1.3 Repr esentation des arbres . . . . . . . . . . . 5.2 Utilisation des arbres . . . . . . . . . . . . . . . . . . 5.2.1 Evaluation dexpressions & parcours darbres . 5.2.2 Arbres Binaires de Recherche . . . . . . . . . 5.2.3 Tas pour limpl ementation de les de priorit e 5.2.4 Tri par tas . . . . . . . . . . . . . . . . . . . . 5.3 Arbres equilibr es . . . . . . . . . . . . . . . . . . . . 5.3.1 R e equilibrage darbres . . . . . . . . . . . . . 5.3.2 Arbres AVL . . . . . . . . . . . . . . . . . . . 6 Graphes 6.1 D enitions et terminologie . . . . . . . . . . . . . . . 6.2 Repr esentation des graphes . . . . . . . . . . . . . . . 6.2.1 Matrice dadjacence . . . . . . . . . . . . . . . 6.2.2 Liste de successeurs . . . . . . . . . . . . . . . 6.3 Existence de chemins & fermeture transitive . . . . . 6.4 Parcours de graphes . . . . . . . . . . . . . . . . . . 6.4.1 Arborescences . . . . . . . . . . . . . . . . . . 6.4.2 Parcours en largeur . . . . . . . . . . . . . . . 6.4.3 Parcours en profondeur . . . . . . . . . . . . . 6.5 Applications des parcours de graphes . . . . . . . . . 6.5.1 Tri topologique . . . . . . . . . . . . . . . . . 6.5.2 Calcul des composantes fortement connexes 6.5.3 Calcul de chemins optimaux . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

7 Recherche de motifs 109 7.1 D enitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 7.2 Lalgorithme de Rabin-Karp . . . . . . . . . . . . . . . . . . . . . . . . . . 110 7.3 Automates pour la recherche de motifs . . . . . . . . . . . . . . . . . . . . 112

7.3.1 7.3.2 7.3.3

Automates nis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 Construction dun automate pour la recherche de motifs . . . . . . 114 Reconnaissance dexpression r eguli` eres . . . . . . . . . . . . . . . 116

Chapitre 1 Complexit e
La notion de complexit e est centrale en algorithmique : cest gr ace ` a elle que lon peut d enir ce quest un bon algorithme. La recherche dalgorithmes ayant une complexit e plus petite que les meilleurs algorithmes connus est un th` eme de recherche important dans toutes les branches de linformatique et des math ematiques appliqu ees. Dans ce chapitre nous voyons comment est d enie cette notion de complexit e.

1.1
1.1.1

D enitions
Comparaison asymptotique de fonctions

Commen cons par un rappel de quelques notations : pour deux fonctions r eelles f (n) et g (n), on ecrira : f (n) = O(g (n)) (1.1) si et seulement sil existe deux constantes strictement positives n0 et c telles que : n > n0 , 0 f (n) c g (n).

Inversement, quand g (n) = O(f (n)), on utilise la notation : f (n) = (g (n)) et, quand on a ` a la fois les propri et es (1.1) et (1.2) : f (n) = (g (n)). Plus formellement, f (n) = (g (n)) si :
2 (c, c ) (R +) ,

(1.2)

(1.3)

n0 N,

n n0 ,

c g (n) f (n) c g (n).

Notons que la formule (1.3) ne signie pas que f (n) est equivalente ` a g (n) (not e f (n) g (n)), qui se d enit comme : f (n) g (n) = 0. lim n g (n) 1

1.1 D enitions

ENSTA cours IN101

Cependant, si f (n) g (n) on a f (n) = (g (n)). Enn, il est clair que f (n) = (g (n)) si et seulement si g (n) = (f (n)). Intuitivement, la notation revient ` a oublier le coecient multiplicatif constant de g (n). Voici quelques exemples de comparaisons de fonctions : n2 + 3n + 1 = (n2 ) = (50n2 ), n/ log(n) = O(n), 50n10 = O(n10,01 ), 2n = O(exp(n)), exp(n) = O(n!), n/ log(n) = ( n), log2 (n) = (log(n)) = (ln(n)). On peut ainsi etablir une hi erarchie (non exhaustive) entre les fonctions : log(n) n n n n log(n) n2 n3 2n exp(n) n! nn 22

1.1.2

Complexit e dun algorithme

On appelle complexit e dun algorithme est le nombre asymptotique dop erations de base quil doit eectuer en fonction de la taille de lentr ee quil a ` a traiter. Cette complexit e est ind ependante de la vitesse de la machine sur laquelle est ex ecut e lalgorithme : la vitesse de la machine (ou la qualit e de limpl ementation) peut faire changer le temps dex ecution dune op eration de base, mais ne change pas le nombre dop erations ` a eectuer. Une optimisation qui fait changer le nombre dop erations de base (et donc la complexit e) doit etre vue comme un changement dalgorithme. Etant donn ee la d enition pr ec edente, il convient donc de d enir convenablement ce quest une op eration de base et comment lon mesure la taille de lentr ee avant de pouvoir parler de la complexit e dun algorithme. Prenons par exemple le code suivant, qui calcule la somme des carr es de 1 ` an:
1 2 3 4 5 6 7 8

unsigned int sum_of_squares(unsigned int n) { int i; unsigned int sum = 0; for (i=1; i<n+1; i++) { sum += i*i; } return sum; }

Pour un tel algorithme on consid` ere que la taille de lentr ee est n et on cherche ` a compter le nombre dop erations de base en fonction de n. Lalgorithme eectue des multiplications et des additions, donc il convient de consid erer ces op erations comme op erations de base. Il y a au total n multiplications et n additions, donc lalgorithme a une complexit e (n). Le choix de lop eration de base semble ici tout ` a fait raisonnable car dans un processeur 2 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 1. Complexit e

moderne laddition et la multiplication sont des op erations qui prennent (1) cycles pour etre eectu es. Imaginons maintenant que lon modie lalgorithme pour quil puisse manipuler des grands entiers (plus grands que ce que le processeur peut manipuler dun seul coup) : le co ut dune addition ou dune multiplication va augmenter quand la taille des entiers va augmenter, et ces op erations nont alors plus un co ut constant. Il convient alors de changer dop eration de base et de consid erer non plus des multiplications/additions complexes mais des op erations binaires el ementaires. Le co ut de laddition de deux nombre entre 0 et n est alors (log n) (le nombre de bits n ecessaires pour ecrire n) et le co ut dune multiplication 2 (en utilisant un algorithme basique) est ((log n) ). Calculer la somme des carr es de 1 ` a n quand n devient grand a donc une complexit e (n(log n)2 ) op erations binaires. Comme vous le verrez tout au long de ce poly, selon le contexte, lop eration de base ` a choisir peut donc changer, mais elle reste en g en eral lop eration la plus naturelle. Ordres de grandeurs de complexit es. Jusquici nous avons parl e de complexit e asymptotique des algorithmes, mais pour une complexit e donn ee, il est important davoir une id ee des ordres de grandeurs des param` etres que lalgorithme pourra traiter. Un algorithme exponentiel sera souvent trop co uteux pour de grandes entr ees, mais pourra en g en eral tr` es bien traiter des petites entr ees. Voici quelques chires pour se faire une id ee : eectuer 230 (environ un milliard) op erations binaires sur un PC standard prend de lordre de la seconde. le record actuel de puissance de calcul fourni pour r esoudre un probl` eme donn e est de 60 lordre de 2 op erations binaires. aujourdhui en cryptographie, on consid` ere quun probl` eme dont la r esolution n ecessite 80 2 op erations binaires est impossible ` a r esoudre. 128 une complexit e de 2 op erations binaires sera a priori toujours inatteignable dici quelques dizaines dann ees (cest pour cela que lon utilise aujourdhui des clefs de 128 bits en cryptographie sym etrique, alors qu` a la n des ann ees 70 les clefs de 56 bits du DES semblaient susamment solides). Dans la pratique, un algorithme avec une complexit e (n) pourra traiter des entr ees jusqu` a n = 230 en un temps raisonnable (cela d epend bien s ur des constantes), et un algorithme cubique (de complexit e (n3 )), comme par exemple un algorithme basique pour linversion dune matrice nn, pourra traiter des donn ees jusqu` a une taille n = 210 = 1024. 20 20 En revanche, inverser une matrice 2 2 n ecessitera des mois de calcul ` a plusieurs milliers de machines (` a moins dutiliser un meilleur algorithme...).

1.1.3

Complexit e spatiale, complexit e variable

Complexit e spatiale. Jusquici, la seule complexit e dont il a et e question est la complexit e temporelle : le temps mis pour ex ecuter un algorithme. Cependant, il peut aussi etre int eressant de mesurer dautres aspects, et en particulier la complexit e spatiale : la taille m emoire n ecessaire ` a lex ecution dun algorithme. Comme pour la complexit e temporelle, & M. Finiasz 3

1.1 D enitions

ENSTA cours IN101

seule nous int eresse la complexit e spatiale asymptotique, toujours en fonction de la taille de lentr ee. Des exemples dalgorithmes avec des complexit es spatiales di erentes seront donn es dans la section 1.2. Il est ` a noter que la complexit e spatiale est n ecessairement toujours inf erieure (ou egale) ` a la complexit e temporelle dun algorithme : on suppose quune op eration d ecriture en m emoire prend un temps (1), donc ecrire une m emoire de taille M prend un temps (M ). Comme pour la complexit e temporelle, des bornes existent sur les complexit es spatiales 20 atteignables en pratique. Un programme qui utilise moins de 2 entiers (4Mo sur une machine 32-bits) ne pose aucun probl` eme sur une machine standard, en revanche 230 est ` a la limite, 240 est dicile ` a atteindre et 250 est ` a peu pr` es hors de port ee. `les de Me moire Alternatifs Des Mode Vous pouvez etre amen es ` a rencontrer des mod` eles de m emoire alternatifs dans lesquels les temps dacc` es/ ecriture ne sont plus (1) mais d ependent de la complexit e spatiale totale de lalgorithme. Supposons que la complexit e spatiale soit (M ). Dans un syst` eme informatique standard, pour acc eder ` a une capacit e m emoire de taille M , il faut pouvoir adresser lensemble de cet espace. cela signie quune adresse m emoire (un pointeur en C), doit avoir une taille minimale de log2 M bits, et lire un pointeur a donc une complexit e (log M ). Plus un algorithme utilise de m emoire, plus le temps dacc` es ` a cette m emoire est grand. Dans ce mod` ele, la complexit e temporelle dun algorithme est toujours au moins egale ` a (M log M ). Certains mod` eles vont encore plus loin et prennent aussi en compte des contraintes physiques : dans des puces m emoires planaires (le nombre de couches superpos ees de transistors est (1)) comme lon utilise ` a lheure actuelle, le temps pour quune information circule dun bout de la puce m e moire jusquau processeur est au moins egal ` a ( M ). Dans ce mod` ele on peut borner la complexit e temporelle par (M M ). La constante (linverse de la vitesse de la lumi` ere) dans le est en revanche tr` es petite. Dans ce cours, nous consid ererons toujours le mod` ele m emoire standard avec des temps dacc` es constants en (1).

Complexit e dans le pire cas. Comme nous avons vu, la complexit e dun algorithme sexprime en fonction de la taille de lentr ee ` a traiter. Cependant, certains algorithmes peuvent avoir une complexit e tr` es variable pour des entr ees de m eme taille : factoriser un nombre va par exemple d ependre plus de la taille du plus grand facteur que de la taille totale du nombre. Une technique d etude des performances dun tel algorithme ` a complexit e variable consiste ` a examiner la complexit e en temps du pire cas. Le temps de calcul dans le pire cas pour les entr ees x de taille n x ee est d eni par T (n) = sup{x, |x|=n} T (x). T (n) fournit donc une borne sup erieure sur le temps de calcul sur nimporte quelle 4 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 1. Complexit e

entr ee de taille n. Pour calculer T (n), on n eglige les constantes dans lanalyse, an de pouvoir exprimer comment lalgorithme d epend de la taille des donn ees. Pour cela, on utilise la notation O ou , qui permet de donner un ordre de grandeur sans caract eriser plus nement. La complexit e dans le pire cas est donc ind ependante de limpl ementation choisie. Complexit e moyenne. Une autre fa con d etudier les performances dun algorithme consiste ` a en d eterminer la complexit e moyenne : cest la moyenne du temps de calcul sur toutes les donn ees dune taille n x ee, en supposant connue une distribution de probabilit e (p(n)) sur lensemble des entr ees de taille n : Tm (n) =
x, |x|=n

pn (x)T (x).

On remarque que Tm (n) nest autre que lesp erance de la variable al eatoire temps de calcul . Le cas le plus ais e pour d eterminer Tm (n) est celui o` u les donn ees sont equidistribu ees dans leur ensemble de d enition : on peut alors dans un premier temps evaluer le temps de calcul T (x) de lalgorithme sur une entr ee x choisie al eatoirement dans cet ensemble, le calcul de la moyenne des complexit es sur toutes les entr ees de lensemble sen d eduisant facilement 1 . Lorsque la distribution (p(n)) nest pas la distribution uniforme, la complexit e moyenne dun algorithme est en g en eral plus dicile ` a calculer que la complexit e dans le pire cas ; de plus, lhypoth` ese duniformit e peut ne pas re eter la situation pratique dutilisation de lalgorithme.

Analyse en moyenne. Lanalyse en moyenne dun algorithme consiste ` a calculer le


nombre moyen (selon une distribution de probabilit e sur les entr ees) de fois que chaque instruction est ex ecut ee, en le multipliant par le temps propre ` a chaque instruction, et en faisant la somme globale de ces quantit es. Elle est donc d ependante de limpl ementation choisie. Trois dicult es se pr esentent lorsque lon veut mener ` a bien un calcul de complexit e en moyenne : dabord, l evaluation pr ecise du temps n ecessaire ` a chaque instruction peut sav erer dicile, essentiellement ` a cause de la variabilit e de ce temps dune machine ` a lautre. Ensuite, le calcul du nombre moyen de fois que chaque instruction est ex ecut ee peut etre d elicat, les calculs de borne sup erieure etant g en eralement plus simples. En cons equence, la complexit e en moyenne de nombreux algorithmes reste inconnue ` a ce jour. Enn, le mod` ele de donn ees choisi nest pas forc ement repr esentatif des ensembles de donn ees rencontr es en pratique. Il se peut m eme que, pour certains algorithmes, il nexiste pas de mod` ele connu.
1. Cette hypoth` ese d equidistribution rend en eet le calcul de la complexit e sur une donn ee choisie al eatoirement repr esentatif de la complexit e pour toutes les donn ees de lensemble.

& M. Finiasz

1.2 Un seul probl` eme, quatre algorithmes

ENSTA cours IN101

Pour ces raisons, le temps de calcul dans le pire cas est bien plus souvent utilis e comme mesure de la complexit e.

1.2

Un seul probl` eme, quatre algorithmes de complexit es tr` es di erentes

Pour comprendre limportance du choix de lalgorithme pour r esoudre un probl` eme donn e, prenons lexemple du calcul des nombres de Fibonacci d enis de la mani` ere suivante : F0 = 0, F1 = 1, et, n 2, Fn = Fn1 + Fn2 . Un premier algorithme pour calculer le n-i` eme nombre de Fibonacci Fn reprend exactement la d enition par r ecurrence ci-dessus :
1 2 3 4 5 6

unsigned int fibo1(unsigned int n) { if (n < 2) { return n; } return fibo1(n-1) + fibo1(n-2); }

Lalgorithme obtenu est un algorithme r ecursif (cf. chapitre 2). Si lon note Cn , le nombre dappels ` a fibo1 n ecessaires au calcul de Fn , on a, C0 = C1 = 1, et, pour n 2, Cn = 1 + Cn1 + Cn2 . Si lon pose Dn = (Cn + 1)/2, on observe que Dn suit exactement la relation de r ecurrence d enissant la suite de Fibonacci (seules les conditions initiales di` erent). La r esolution de cette r ecurrence lin eaire utilise une technique dalg` ebre classique (cf. chapitre 2) : on calcule les solutions de l equation caract eristique de cette r ecurrence - ici 1+ 5 1 5 2 x x 1 = 0 - qui sont = 2 et = 2 . Dn s ecrit alors n . Dn = n + On d etermine et ` a laide des conditions initiales. On obtient nalement 1 n+1 ), n 0. Dn = (n+1 5 Etant donn e que Cn = 2Dn 1, la complexit e - en termes de nombre dappels ` a fibo1 n du calcul de Fn par cet algorithme est donc ( ), cest-` a-dire exponentiel en n. On observe que lalgorithme pr ec edent calcule plusieurs fois les valeurs Fk , pour k < n, ce qui est bien evidemment inutile. Il est plus judicieux de calculer les valeurs Fk , 2 k n ` a partir des deux valeurs Fk1 et Fk2 , de les stocker dans un tableau, et de retourner Fn . 6 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 1. Complexit e

1 2 3 4 5 6 7 8 9 10 11 12

unsigned int fibo2(unsigned int n) { unsigned int* fib = (unsigned int* ) malloc((n+1)*sizeof(unsigned int )); int i,res; fib[0] = 0; fib[1] = 1; for (i=2; i<n+1; i++) { fib[i] = fib[i-1] + fib[i-2]; } res = fib[n]; free(fib); return res; }

On prend ici comme unit e de mesure de complexit e le temps de calcul dune op eration daddition, de soustraction ou de multiplication. La complexit e de lalgorithme Fibo2 est alors (n), i.e. lin eaire en n (en eet, la boucle for comporte n 1 it erations, chaque it eration consistant en une addition). La complexit e est donc drastiquement r eduite par rapport ` a lalgorithme pr ec edent : le prix ` a payer ici est une complexit e en espace lin eaire ((n) pour stocker le tableau fib). On remarque maintenant que les n 2 valeurs Fk , 0 k n 3 nont pas besoin d etre stock ees pour le calcul de Fn . On peut donc revenir ` a une complexit e en espace en (1) en ne conservant que les deux derni` eres valeurs courantes Fk1 et Fk2 n ecessaires au calcul de Fk : on obtient le troisi` eme algorithme suivant :
1 2 3 4 5 6 7 8 9 10

unsigned int fibo3(unsigned int n) { unsigned int fib0 = 0; unsigned int fib1 = 1; int i; for (i=2; i<n+1; i++) { fib1 = fib0 + fib1; fib0 = fib1 - fib0; } return fib1; }

Cet algorithme admet toutefois encore une complexit e en temps de (n). Un dernier algorithme permet datteindre une complexit e logarithmique en n. Il est bas e sur l ecriture suivante de (Fn , Fn1 ), pour n 2 : ) ) ( )( ( 1 1 Fn1 Fn . = Fn2 Fn1 1 0 En it erant, on obtient : ( Fn Fn1 ) = ( 1 1 1 0 )n1 ( F1 F0 ) . 7

& M. Finiasz

1.2 Un seul probl` eme, quatre algorithmes

ENSTA cours IN101 (

) 1 1 Ainsi, calculer Fn revient ` a mettre ` a la puissance (n 1) la matrice . 1 0 La complexit e en temps de lalgorithme qui en d ecoule est ici (log(n)) multiplications de matrices carr ees 2 2 : en eet, cest le temps requis par lalgorithme dexponentiation square-and-multiply (cf. TD 01) pour calculer la matrice. Lespace n ecessaire est celui du stockage de quelques matrices carr ees 2 2, soit (1). Lalgorithme fibo4 peut s ecrire ainsi :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

unsigned int fibo4(unsigned int n) { unsigned int res[2][2]; unsigned int mat_tmp[2][2]; int i = 0; unsigned int tmp; /* on initialise le r esultat avec la matrice */ res[0][0] = 1; res[0][1] = 1; res[1][0] = 1; res[1][1] = 0; /* cas particulier n==0 ` a traiter tout de suite */ if (n == 0) { return 0; } /* on doit trouver le un le plus ` a gauche de n-1 */ tmp = n-1; while (tmp != 0) { i++; tmp = tmp >> 1; } /* on d ecr emente i car la premi` ere multiplication a d ej` a et e faite en initialisant res */ i--; while (i > 0) { /* on el` eve au carr e */ mat_tmp[0][0] = res[0][0]*res[0][0] + res[0][1]*res[1][0]; mat_tmp[0][1] = res[0][0]*res[0][1] + res[0][1]*res[1][1]; mat_tmp[1][0] = res[1][0]*res[0][0] + res[1][1]*res[1][0]; mat_tmp[1][1] = res[1][0]*res[0][1] + res[1][1]*res[1][1]; /* on regarde la valeur du i` eme bit de n-1 pour savoir si on doit faire une multiplication */ if (((n-1) & (1<<(i-1))) != 0) { res[0][0] = mat_tmp[0][0] + mat_tmp[0][1]; res[0][1] = mat_tmp[0][0]; res[1][0] = mat_tmp[1][0] + mat_tmp[1][1]; res[1][1] = mat_tmp[1][0]; } else { /* on replace la matrice dans res */ res[0][0] = mat_tmp[0][0];

F. Levy-dit-Vehel

Ann ee 2011-2012
43 44 45 46 47 48 49 50

Chapitre 1. Complexit e

res[0][1] = mat_tmp[0][1]; res[1][0] = mat_tmp[1][0]; res[1][1] = mat_tmp[1][1]; } i--; } return res[0][0]; }

Ainsi, une etude algorithmique pr ealable du probl` eme de d epart conduit ` a une r eduction parfois drastique de la complexit e de sa r esolution, ce qui a pour cons equence de permettre datteindre des tailles de param` etres inenvisageables avec un algorithme irr e echi... La Table 1.1 ci-dessous illustre les temps de calcul des quatre algorithmes pr ec edents. fibo1 fibo2 fibo3 fibo4 n (n ) (n) (n) (log(n)) 40 31s 0s 0s 0s 228 231 calcul irr ealisable 18s Segmentation fault 4s 25s 195s 0s 0s 0s 225

Table 1.1 Complexit es et temps dex ecution de di erents algorithmes pour le calcul de la suite de Fibonacci. Comme on le voit dans la Table 1.1, lalgorithme fibo4 qui a la meilleure complexit e est beaucoup plus rapide que les autres, et peut traiter des valeurs de n plus grandes. Lalgorithme fibo1 prend tr` es vite un temps trop elev e pour pouvoir etre utilis e. Une autre limitation de fibo1 que lon ne voit pas dans le tableau est la limite li ee au nombre maximal de niveaux de r ecursivit e autoris es dans un programme : en C cette valeur est souvent autour de quelques dizaines de milliers 2 . On ne peut donc pas avoir plus que ce nombre dappels r ecursifs imbriqu es au sein dun algorithme. Pour lalgorithme fibo2 la limite ne vient pas du temps de calcul, mais de la taille m emoire n ecessaire : quand on essaye dallouer 28 un tableau de 2 entiers (soit 1Go de m emoire dun seul bloc) le syst` eme dexploitation narrive pas ` a satisfaire notre requ ete et nalloue donc pas de m emoire, et puisque lon ne teste pas si la m emoire a bien et e allou ee avant d ecrire dans notre tableau, un probl` eme ` a lallocation se traduit imm ediatement par une erreur de segmentation m emoire ` a l ecriture.

1.3

Un premier algorithme pour le tri

Nous abordons un premier exemple dalgorithme permettant de trier des el ements munis dune relation dordre (des entiers par exemple) : il sagit du tri par insertion, qui mod elise notre fa con de trier des cartes ` a jouer. Voici le code dun tel algorithme triant un tableau dentier tab de taille n :
2. Le nombre maximal de niveaux de r ecursivit e est de lordre de 218 avec la version 4.1.2 de gcc sur une gentoo 2007.0, avec un processeur Intel Pentium D et les options par d efaut.

& M. Finiasz

1.3 Un premier algorithme pour le tri

ENSTA cours IN101

1 2 3 4 5 6 7 8 9 10 11 12

void insertion_sort(int* tab, int n) { int i,j,tmp; for (i=1; i<n; i++) { tmp = tab[i]; j = i-1; while ((j > 0) && (tab[j] > tmp)) { tab[j+1] = tab[j]; j--; } tab[j+1] = tmp; } }

Le principe de cet algorithme est assez simple : ` a chaque etape de la boucle for, les i premiers el ements de tab sont tri es par ordre croissant. Quand i = 1 cest vrai, et ` a chaque fois que lon augmente i de 1, la boucle while se charge de replacer le nouvel el ement ` a sa place en le remontant el ement par el ement vers les petits indice du tableau : si le nouvel el ement est plus petit que celui juste avant, on inverse les deux el ements que lon vient de comparer et on recommence jusqu` a avoir atteint la bonne position. Cet algorithme a un co ut tr` es variable en fonction de l etat initial du tableau : pour un tableau d ej` a tri e, lalgorithme va simplement parcourir tous les el ements et les comparer ` a l el ement juste avant eux, mais ne fera jamais d echange. Le co ut est alors de n comparaisons. pour un tableau tri e` a lenvers (le cas le pire), lalgorithme va remonter chaque nouvel 1) el ement tout au d ebut du tableau. Il y a alors au total exactement n(n2 comparaisons 2 et echanges. Lalgorithme de tri par insertion ` a donc une complexit e (n ) dans le pire des cas. en moyenne, il faudra remonter chaque nouvel el ement sur la moiti e de la longueur, donc i1 comparaisons et e change par nouvel e l e ment. Au nal la complexit e en moyenne du 2 2 tri par insertion est (n ), la m eme que le cas le pire (on perd le facteur 1 dans le ). 2 G en eralisations du tri par insertion. Il existe plusieurs variantes du tri par insertion visant ` a am eliorer sa complexit e en moyenne. Citons en particulier le tri de Shell (cf. http: //fr.wikipedia.org/wiki/Tri_de_Shell) qui compare non pas des el ements voisins, mais des el ements plus distants an doptimiser le nombre de comparaisons n ecessaires.

10

F. Levy-dit-Vehel

Chapitre 2 R ecursivit e
Les d enitions r ecursives sont courantes en math ematiques. Nous avons vu au chapitre pr ec edent lexemple de la suite de Fibonacci, d enie par une relation de r ecurrence. En informatique, la notion de r ecursivit e joue un r ole fondamental. Nous voyons dans ce chapitre la puissance de la r ecursivit e au travers essentiellement de deux algorithmes de tri ayant les meilleures performances asymptotiques pour des algorithmes g en eriques. Nous terminons par une etude des solutions d equations de r ecurrence entrant en jeu lors de lanalyse de complexit e de tels algorithmes.

2.1

Conception des algorithmes

Il existe de nombreuses fa cons de concevoir un algorithme. On peut par exemple adopter une approche incr ementale ; cest le cas du tri par insertion : apr` es avoir tri e le sous-tableau tab[0]...tab[j-1], on ins` ere l el ement tab[j] au bon emplacement pour produire le sous-tableau tri e tab[0]...tab[j]. Une approche souvent tr` es ecace et el egante est lapproche r ecursive : un algorithme r ecursif est un algorithme d eni en r ef erence ` a lui-m eme (cest la cas par exemple de lalgorithme fibo1 vu au chapitre pr ec edent). Pour eviter de boucler ind eniment lors de sa mise en oeuvre, il est n ecessaire de rajouter ` a cette d enition une condition de terminaison, qui autorise lalgorithme ` a ne plus etre d eni ` a partir de lui-m eme pour certaines valeurs de lentr ee (pour fibo1, nous avons par exemple d eni fibo1(0)=0 et fibo1(1)=1). Un tel algorithme suit g en eralement le paradigme diviser pour r egner : il s epare le probl` eme en plusieurs sous-probl` emes semblables au probl` eme initial mais de taille moindre, r esout les sous-probl` emes de fa con r ecursive, puis combine toutes les solutions pour produire la solution du probl` eme initial. La m ethode diviser pour r egner implique trois etapes ` a chaque niveau de la r ecursivit e: Diviser le probl` eme en un certain nombre de sous-probl` emes. On notera D(n) la complexit e de cette etape. R egner sur les sous-probl` emes en les r esolvant de mani` ere r ecursive (ou directe si le sous-probl` eme est susamment r eduit, i.e. une condition de terminaison est atteinte). 11

2.2 Probl` eme des tours de Hano

ENSTA cours IN101

On notera R(n) la complexit e de cette etape. Combiner les solutions des sous-probl` emes pour trouver la solution du probl` eme initial. On notera C (n) la complexit e de cette etape. Lien avec la r ecursivit e en math ematiques. On rencontre en g en eral la notion de r ecursivit e en math ematiques essentiellement dans deux domaines : les preuves par r ecurrence et les suites r ecurrentes. La conception dalgorithmes r ecursifs se rapproche plus des preuves par r ecurrence, mais comme nous allons le voir, le calcul de la complexit e dun algorithme r ecursif se rapproche beaucoup des suites r ecurrentes. Lors dune preuve par r ecurrence, on prouve quune propri et e est juste pour des conditions initiales, et on etend cette propri et e ` a lensemble du domaine en prouvant que la propri et e est juste au rang n si elle est vrai au rang n 1. Dans un algorithme r ecursif la condition de terminaison correspond exactement au conditions initiales de la preuve par r ecurrence, et lalgorithme va ramener le calcul sur une entr ee ` a des calculs sur des entr ees plus petites. Attention ! Lors de la conception dun algorithme r ecursif il faut bien faire attention ` a ce que tous les appels r ecursifs eectu es terminent bien sur une condition de terminaison. Dans le cas contraire lalgorithme peut partir dans une pile dappels r ecursifs innie (limit ee uniquement par le nombre maximum dappels r ecursifs autoris es). De m eme, en math ematique, lors dune preuve par r ecurrence, si la condition r ecurrente ne se ram` ene pas toujours ` a une condition initiale, la preuve peut etre fausse.

2.2

Probl` eme des tours de Hano

Le probl` eme des tours de Hano peut se d ecrire sous la forme dun jeu (cf. Figure 2.1) : on dispose de trois piquets num erot es 1,2,3, et de n rondelles, toutes de diam` etre di erent. Initialement, toutes les rondelles se trouvent sur le piquet 1, dans lordre d ecroissant des diam` etres (elle forment donc une pyramide). Le but du jeu est de d eplacer toutes les rondelles sur un piquet de destination choisi parmi les deux piquets vides, en respectant les r` egles suivantes : on ne peut d eplacer quune seule rondelle ` a la fois dun sommet de pile vers un autre piquet ; on ne peut pas placer une rondelle au-dessus dune rondelle de plus petit diam` etre. Ce probl` eme admet une r esolution r ecursive el egante qui comporte trois etapes : 1. d eplacement des n 1 rondelles sup erieures du piquet origine vers le piquet interm ediaire par un appel r ecursif ` a lalgorithme. 2. d eplacement de la plus grande rondelle du piquet origine vers le piquet destination. 3. d eplacement des n 1 rondelles du piquet interm ediaire vers le piquet destination par un appel r ecursif ` a lalgorithme. 12 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 2. R ecursivit e

Figure 2.1 Le jeu des tours de Hano . D eplacement dune rondelle du piquet 1 vers le piquet 2. En pointill es : lobjectif nal. Cet algorithme, que lon appelle Hano , suit lapproche diviser pour r egner : la phase de division consiste toujours ` a diviser le probl` eme de taille n en deux sous-probl` emes de taille n 1 et 1 respectivement. La phase de r` egne consiste ` a appeler r ecursivement lalgorithme Hano sur le sous-probl` eme de taille n 1. Ici, il y a deux s eries dappels r ecursifs par sous-probl` eme de taille n 1 ( etapes 1. et 3.). La phase de combinaison des solutions des sous-probl` emes est inexistante ici. On donne ci-apr` es le programme C impl ementant lalgorithme Hano :
1 2 3 4 5 6 7 8 9 10 11

void Hanoi(int n, int i, int j) { int intermediate = 6-(i+j); if (n > 0) { Hanoi(n-1,i,intermediate); printf("Mouvement du piquet %d vers le piquet %d.\n",i,j); Hanoi(n-1,intermediate,j); } } int main(int argc, char* argv[]) { Hanoi(atoi(argv[1]),1,3); }

On constate quici la condition de terminaison est n = 0 (lalgorithme ne fait des appels r ecursifs que si n > 0) et pour cette valeur lalgorithme ne fait rien. Le lecteur est invit e` a v erier que lex ecution de Hanoi(3,1,3) donne :
Mouvement Mouvement Mouvement Mouvement Mouvement Mouvement Mouvement du du du du du du du piquet piquet piquet piquet piquet piquet piquet 1 1 3 1 2 2 1 vers vers vers vers vers vers vers le le le le le le le piquet piquet piquet piquet piquet piquet piquet 3 2 2 3 1 3 3

& M. Finiasz

13

2.2 Probl` eme des tours de Hano

ENSTA cours IN101

Complexit e de lalgorithme. Calculer la complexit e dun algorithme r ecursif peut parfois sembler compliqu e, mais il sut en g en eral de se ramener ` a une relation d enissant une suite r ecurrente. Cest ce que lon fait ici. Soit T (n) la complexit e (ici, le nombre de d eplacements de disques) n ecessaire ` a la r esolution du probl` eme sur une entr ee de taille n par lalgorithme Hano . En d ecomposant la complexit e de chaque etape on trouve : l etape de division ne co ute rien, l etape de r` egne co ute le prix de deux mouvements de taille n 1 et l etape de combinaison co ute le mouvement du grand disque, soit 1. On a T (0) = 0, T (1) = 1, et, pour n 2, T (n) = D(n) + R(n) + C (n) = 0 + 2 T (n 1) + 1. En d eveloppant cette expression, on trouve T (n) = 2n T (0) + 2n1 + . . . + 2 + 1, soit T (n) = 2n1 + . . . + 2 + 1 = 2n 1.

Ainsi, la complexit e de cet algorithme est exponentielle en la taille de lentr ee. En fait, ce caract` ere exponentiel ne d epend pas de lalgorithme en lui-m eme, mais est intrins` eque au probl` eme des tours de Hano : montrons en eet que le nombre minimal minn de mouvements de disques ` a eectuer pour r esoudre le probl` eme sur une entr ee de taille n est exponentiel en n. Pour cela, nous observons que le plus grand des disques doit n ecessairement etre d eplac e au moins une fois. Au moment du premier mouvement de ce grand disque, il doit etre seul sur un piquet, son piquet de destination doit etre vide, donc tous les autres disques doivent etre rang es, dans lordre, sur le piquet restant. Donc, avant le premier mouvement du grand disque, on aura d u d eplacer une pile de taille n 1. De m eme, apr` es le dernier mouvement du grand disque, on devra d eplacer les n 1 autres disques ; ainsi, minn 2minn1 +1. Or, dans lalgorithme Hano , le nombre de mouvements de disques v erie exactement cette egalit e (on a T (n) = minn ). Ainsi, cet algorithme est optimal, et la complexit e exponentielle est intrins` eque au probl` eme. Spatiale des Appels Re cursifs Complexite Notons que limpl ementation que nous donnons de lalgorithme Hano nest pas standard : lalgorithme est en g en eral programm e` a laide de trois piles (cf. section 3.3.1), mais comme nous navons pas encore etudi e cette structure de donn ee, notre algorithme se contente dacher les op erations quil eectuerait normalement. En utilisant des piles, la complexit e spatiale serait (n) (correspondant ` a lespace n ecessaire pour stocker les n disques). Ici il ny a pas dallocation m emoire, mais la complexit e spatiale est quand m eme (n) : en eet, chaque appel r ecursif n ecessite dallouer de la m emoire (ne serait-ce que pour conserver ladresse de retour de la fonction) qui nest lib er ee que lorsque la fonction appel ee r ecursivement se termine. Ici le programme utilise jusqu` a n appels r ecursifs imbriqu es donc sa complexit e spatiale est (n). Il nest pas courant de prendre en compte la complexit e spatiale dappels r ecursifs imbriqu es, car en g en eral ce nombre dappels est toujours relativement faible.

14

F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 2. R ecursivit e

2.3

Algorithmes de tri

Nous continuons ici l etude de m ethodes permettant de trier des donn ees index ees par des clefs (ces clefs sont munies dune relation dordre et permettent de conduire le tri). Il sagit de r eorganiser les donn ees de telle sorte que les clefs apparaissent dans un ordre bien d etermin e (alphab etique ou num erique le plus souvent). Les algorithmes simples (comme le tri par insertion, mais aussi le tri par s election ou le tri ` a bulle) sont ` a utiliser pour des petites quantit es de donn ees (de lordre de moins de 100 clefs), ou des donn ees pr esentant une structure particuli` ere (donn ees compl` etement ou presque tri ees, ou comportant beaucoup de clefs identiques). Pour de grandes quantit es de donn ees al eatoires, ou si lalgorithme doit etre utilis e un grand nombre de fois, on a plut ot recours ` a des m ethodes plus sophistiqu ees. Nous en pr esentons deux ici : le tri fusion et le tri rapide. Tous deux sont de nature r ecursive et suivent lapproche diviser pour r egner.

2.3.1

Le tri fusion

Cet algorithme repose sur le fait que fusionner deux tableaux tri es est plus rapide que de trier un grand tableau directement. Supposons que lalgorithme prenne en entr ee un tableau de n el ements dans un ordre quelconque. Lalgorithme commence par diviser le tableau des n el ements en deux sous-tableaux de n el ements chacun ( etape diviser). 2 1 Les deux sous-tableaux sont tri es de mani` ere r ecursive en utilisant toujours le tri fusion (r egner) ; ils sont ensuite fusionn es pour produire le tableau tri e (combiner). Lalgorithme de tri fusion (merge sort en anglais) du tableau dentiers tab entre les indices p et r est le suivant :
1 2 3 4 5 6 7 8 9

void merge_sort(int* tab, int p, int r) { int q; if (r-p > 1) { q = (p+r)/2; merge_sort(tab,p,q); merge_sort(tab,q,r); merge(tab,p,q,r); } }

La proc edure fusion (la fonction merge d ecrite ci-dessous) commence par recopier les deux sous-tableaux tri es tab[p]...tab[q-1] et tab[q]...tab[r-1] dos-` a-dos 2 dans un tableau auxiliaire tmp. Lint er et de cette fa con de recopier est que lon na alors pas besoin de rajouter de tests de n de sous-tableaux, ni de case suppl ementaire contenant le symbole par exemple ` a chacun des sous-tableaux. Ensuite, merge remet dans tab les el ements du tableau tmp tri e, mais ici le tri a une complexit e lin eaire ((n)) puisque tmp provient
1. La r ecursion sarr ete lorsque les sous-tableaux sont de taille 1, donc trivialement tri es. 2. Ainsi, tmp est le tableau [tab[p],...,tab[q-1],tab[r-1],...,tab[q]].

& M. Finiasz

15

2.3 Algorithmes de tri

ENSTA cours IN101

de deux sous-tableaux d ej` a tri es ; le proc ed e est alors le suivant : on part de k = p, i = p et j = r 1, et on compare tmp[i-p] avec tmp[j-p]. On met le plus petit des deux dans tab[k] ; si cest tmp[i-p], on incr emente i, sinon, on d ecr emente j ; dans tous les cas, on incr emente k . On continue jusqu` a ce que k vaille r.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

void merge(int* tab, int p, int q, int r) { int* tmp = (int* ) malloc((r-p)*sizeof(int )); int i,j,k; for (i=p; i<q; i++) { tmp[i-p] = tab[i]; } for (i=q; i<r; i++) { tmp[r-p-1-(i-q)] = tab[i]; } i=p; j=r-1; for (k=p; k<r; k++) { if (tmp[i-p] < tmp[j-p]) { tab[k] = tmp[i-p]; i++; } else { tab[k] = tmp[j-p]; j--; } } free(tmp); }

Ainsi, au d ebut de chaque it eration de la boucle pour k , les k p el ements du soustableau tab[p]...tab[k-1] sont tri es. Pour trier le tableau tab de taille n, on appelle merge sort(tab,0,n). allocation de Me moire Note sur la Re Lalgorithme merge sort tel quil est ecrit ne g` ere pas bien sa m emoire. En eet, chaque appel ` a la fonction merge commence par allouer un tableau tmp de taille r p, et les op erations dallocation m emoire sont des op eration relativement co uteuses (longues ` a ex ecuter en pratique). Allouer une m emoire de taille n a une complexit e (n), et r eallouer de la m emoire ` a chaque appel de merge ne change donc pas la complexit e de lalgorithme. En revanche, en pratique cela ralentit beaucoup lex ecution du programme. Il serait donc plus ecace dallouer une fois pour toutes un tableau de taille n au premier appel de la fonction de tri et de passer ce tableau en argument ` a toutes les fonctions an quelles puissent lutiliser. Cela demande cependant dajouter une fonction suppl ementaire (celle que lutilisateur va appeler en pratique), qui alloue de la m emoire avant dappeler la fonction r ecursive merge sort.

16

F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 2. R ecursivit e

Complexit e du tri fusion. Evaluons ` a pr esent la complexit e en temps de lalgorithme e merge sort, en termes de nombre de comparaisons de clefs. On note T (n), cette complexit pour une entr ee de taille n. La phase de division ne n ecessite aucune comparaison. La phase de r` egne requiert deux fois le temps de lalgorithme merge sort sur un tableau de taille deux fois moindre, i.e. ). La phase de recombinaison est la proc edure merge. Lorsquappel ee avec les 2 T (n 2 param` etres (p, q, r), elle n ecessite r p comparaisons. On a donc : n T (n) = D(n) + R(n) + C (n) = 0 + 2 T ( ) + n. 2 Supposons dabord que n est une puissance de 2, soit n = 2k . Pour trouver la solution de cette r ecurrence, on construit un arbre de r ecursivit e : cest un arbre binaire dont chaque nud repr esente le co ut dun sous-probl` eme individuel, invoqu e` a un moment de la r ecursion. Le noeud racine contient le co ut de la phase de division+recombinaison au niveau n de la r ecursivit e (ici n), et ses deux sous-arbres repr esentent les co uts des sousn n probl` emes au niveau 2 . En d eveloppant ces co uts pour 2 , on obtient deux sous-arbres, et ainsi de suite jusqu` a arriver au co ut pour les sous-probl` emes de taille 1. Partant de n = 2k , le nombre de niveaux de cet arbre est exactement log(n) + 1 = k + 1 (la hauteur de larbre est k ). Pour trouver la complexit e T (n), il sut ` a pr esent dadditionner les co uts de chaque noeud. On proc` ede en calculant le co ut total par niveau : le niveau de profondeur 0 (racine) a un co ut total egal ` a n, le niveau de profondeur 1 a un co ut total egal ` a n +n ,... 2 2 n i le niveau de profondeur i pour 0 i k 1 a un co ut total de 2 2i = n (ce niveau comporte 2i nuds). Le dernier niveau correspond aux 2k sous-probl` emes de taille 1 ; aucun ne contribue ` a la complexit e en termes de nombre de comparaisons, donc le co ut au niveau k est nul. Ainsi, chaque niveau, sauf le dernier, contribue pour n ` a la complexit e totale. Il en r esulte que T (n) = k n = n log(n). Lorsque n nest pas n ecessairement une puissance de 2, on encadre n entre deux puissances de 2 cons ecutives : 2k n < 2k+1 . La fonction T (n) etant croissante, on a k k+1 k k+1 T (2 ) T (n) T (2 ), soit k 2 T (n) (k + 1)2 . Comme log(n) = k , on obtient une complexit e en (n log(n)). Remarques : Le tri fusion ne se fait pas en place 3 : en eet, la proc edure merge sort n ecessite un espace m emoire suppl ementaire sous la forme dun tableau (tmp) de taille n. Le calcul de complexit e pr ec edent est ind ependant de la distribution des entiers ` a trier : le tri fusion sex ecute en (n log(n)) pour toutes les distributions dentiers. La complexit e en moyenne est donc egale ` a la complexit e dans le pire cas.

2.3.2

Le tri rapide

Le deuxi` eme algorithme de tri que nous etudions suit egalement la m ethode diviser pour r egner. Par rapport au tri fusion, il pr esente lavantage de trier en place . En
3. Un tri se fait en place lorsque la quantit e de m emoire suppl ementaire - la cas ech eant - est une petit constante (ind ependante de la taille de lentr ee).

& M. Finiasz

17

2.3 Algorithmes de tri

ENSTA cours IN101

revanche, son comportement d epend de la distribution de son entr ee : dans le pire cas, il poss` ede une complexit e quadratique, cependant, ses performances en moyenne sont en (n log(n)), et il constitue souvent le meilleur choix en pratique 4 . Le fonctionnement de lalgorithme sur un tableau tab ` a trier entre les indices p et r est le suivant :
void quick_sort(int* tab, int p, int r) { int q; if (r-p > 1) { q = partition(tab,p,r); quick_sort(tab,p,q); quick_sort(tab,q+1,r); } }

1 2 3 4 5 6 7 8

La proc edure partition d etermine un indice q tel que, ` a lissue de la proc edure, pour p i q 1, tab[i] tab[q], et pour q + 1 i r 1, tab[i] > tab[q] (les soustableaux tab[i]...tab[q-1] et tab[q+1]...tab[r-1] n etant pas eux-m emes tri es). Elle utilise un el ement x = tab[p] appel e pivot autour duquel se fera le partitionnement.
int partition(int* tab, int p, int r) { int x = tab[p]; int q = p; int i,tmp; for (i=p+1; i<r; i++) { if (tab[i] <= x) { q++; tmp = tab[q]; tab[q] = tab[i]; tab[i] = tmp; } } tmp = tab[q]; tab[q] = tab[p]; tab[p] = tmp; return q; }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

` la n de chacune des it A erations de la boucle for, le tableau est divis e en trois parties : i, p i q, tab[i] x i, q + 1 i j 1, tab[i] > x
4. Lordre de grandeur de complexit e en termes de nombre de comparaisons, mais aussi de nombre dop erations el ementaires (aectations, incr ementations) est le m eme que pour le tri fusion, mais les constantes cach ees dans la notation sont plus petites.

18

F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 2. R ecursivit e

Pour j i r, les clefs tab[i] nont pas de lien x e avec le pivot x ( el ements non encore trait es). Si le test en ligne 6 est vrai, alors l el ement en position j est inf erieur ou egal ` a x, donc on le d ecale le plus ` a gauche possible (lignes 8 ` a 10) ; mais on incr emente dabord q (ligne ` lissue de 7) de fa con ` a pouvoir ins erer cet el ement entre tab[p] et tab[q] strictement. A la boucle for sur j , tous les el ements de tab[p]...tab[r-1] inf erieurs ou egaux ` a x ont et e plac es ` a gauche de tab[q] (et ` a droite de tab[p]) ; il ne reste donc plus qu` a mettre x ` a la place de tab[q] (lignes 13 ` a 15). Complexit e du tri rapide. La complexit e de lalgorithme quick sort d epend du caract` ere equilibr e ou non du partitionnement, qui lui-m eme d epend de la valeur du pivot choisi au d epart. En eet, si x = tab[p] est inf erieur ` a tous les autres el ements du tableau, la proc edure partition d ecoupera le tableau initial en deux sous-tableaux extr emement d es equilibr es : lun de taille 0 et lautre de taille n 1. En particulier si le tableau est d ej` a tri e (ou inversement tri e), cette conguration surviendra ` a chaque appel r ecursif. Dans ce cas, le temps dex ecution T (n) de lalgorithme satisfait la r ecurrence T (n) = T (n 1) + D(n), (la proc edure quick sort sur une entr ee de taille 0 ne n ecessitant pas de comparaison) o` u D(n) est le co ut de la proc edure partition sur une entr ee de taille n. Il est clair que partition(tab, p, r) n ecessite r p 1 comparaisons, donc D(n) = n 1. Larbre r ecursif correspondant ` a T (n) = T (n 1) + n 1 est de profondeur n 1, le niveau de profondeur i ayant un co ut de n (i + 1). En cumulant les co uts par niveau, on obtient T (n) =
n1 i=0

n (i + 1) =

n 1 i=0

i=

n(n 1) . 2

Ainsi, la complexit e de lalgorithme dans le pire cas est en (n2 ), i.e. pas meilleure que celle du tri par insertion. Le cas le plus favorable est celui o` u la proc edure partition d ecoupe toujours le tan bleau courant en deux sous-tableaux de taille presque egale ( n et 1, donc toujours 2 2 n inf erieure ou egale ` a 2 ). Dans ce cas, la complexit e de lalgorithme est donn e par T (n) = 2 T ( n ) + n 1. 2 Cette r ecurrence est similaire ` a celle rencontr ee lors de l etude du tri fusion. La solution est T (n) = (n 1) log(n) si n est une puissance de 2, soit T (n) = (n log(n)) pour tout n. Nous allons maintenant voir que cette complexit e est aussi celle du cas moyen. Pour cela, il est n ecessaire de faire une hypoth` ese sur la distribution des entiers en entr ee de lalgorithme. On suppose donc quils suivent une distribution al eatoire uniforme. Cest le r esultat de partition qui, ` a chaque appel r ecursif, conditionne le d ecoupage en soustableaux et donc la complexit e. On ne peut pas supposer que partition fournit un indice q uniform ement distribu e dans [p, ..., r 1] : en eet, on choisit toujours comme pivot le & M. Finiasz 19

2.3 Algorithmes de tri

ENSTA cours IN101

premier el ement du sous-tableau courant, donc on ne peut faire dhypoth` ese duniformit e 5 quant ` a la distribution de cet el ement tout au long de lalgorithme . Pour pouvoir faire une hypoth` ese raisonnable duniformit e sur q , il est n ecessaire de modier un tant soit peut partition de fa con ` a choisir non plus le premier el ement comme pivot, mais un el ement al eatoirement choisi dans le sous-tableau consid er e. La proc edure devient alors :
1 2 3 4 5 6 7 8

int random_partition(int* tab, int p, int r) { int i,tmp; i = (double) rand()/RAND_MAX * (r-p) + p; tmp = tab[p]; tab[p] = tab[i]; tab[i] = tmp; partition(tab,p,r); }

L echange a pour eet de placer le pivot tab[i] en position p, ce qui permet dex ecuter partition normalement). En supposant ` a pr esent que lentier q retourn e par random partition est uniform ement distribu e dans [p, r 1], on obtient la formule suivante pour T (n) : 1 T (n) = n 1 + (T (q 1) + T (n q )). n q=1
n

esente la somme des complexit es correspondant aux En eet, n q =1 (T (q 1)+ T (n q )) repr n d ecoupages possibles du tableau tab[0, ..., n 1] en deux sous-tableaux. La complexit e moyenne T (n) est donc la moyenne de ces complexit es. Par sym etrie, on a : 2 T (n) = n 1 + T (q 1), n q=1
n

ou nT (n) = n(n 1) + 2

n q =1

T (q 1).

En soustrayant ` a cette egalit e la m eme egalit e au rang n 1, on obtient une expression faisant seulement intervenir T (n) et T (n 1) : nT (n) = (n + 1)T (n 1) + 2(n 1), ou T (n) T (n 1) 2(n 1) = + . n+1 n n(n + 1)

5. Et ce, m eme si les entiers en entr ee sont uniform ement distribu es : en eet, la conguration de ces entiers dans le tableau est modi ee dun appel de partition ` a lautre.

20

F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 2. R ecursivit e

En d eveloppant la m eme formule pour T (n 1) et en la r einjectant ci-dessus, on obtient : T (n) T (n 2) 2(n 2) 2(n 1) = + + . n+1 n1 (n 1)n n(n + 1) En it erant ce processus :
n n T (n) 2(k 1) 2(k 1) = , ou T (n) = (n + 1) . n + 1 k=2 k (k + 1) k (k + 1) k=2

Pour n susamment grand, on a n n n n dx (k 1) 1 1 , et = ln(n). k ( k + 1) k k x 1 k=2 k=1 k=1 Ainsi, T (n) 2(n + 1)ln(n) et on retrouve bien une complexit e en (n log(n)) pour le cas moyen.

2.3.3

Complexit e minimale dun algorithme de tri

Nous pouvons nous demander si les complexit es en (n log(n)) obtenues ci-dessus sont les meilleures que lon puisse esp erer pour un algorithme de tri g en erique (i.e. qui ne fait aucune hypoth` ese sur les donn ees ` a trier). Pour r epondre ` a cette question, nous allons calculer une borne inf erieure sur la complexit e (en termes de nombre de comparaisons) de tout algorithme de tri par comparaison. Pr ecisons tout dabord que lon appelle tri par comparaison un algorithme de tri qui, pour obtenir des informations sur lordre de la s equence dentr ee, utilise seulement des comparaisons. Les algorithmes de tri fusion et rapide vus pr ec edemment sont des tris par comparaison. La borne calcul ee est valable dans le pire cas (contexte g en eral de la th eorie de la complexit e). Tout algorithme de tri par comparaison peut etre mod elis e par un arbre de d ecision (un arbre binaire comme d eni dans le chapitre 5). Chaque comparaison que lalgorithme eectue repr esente un nud de larbre, et en fonction du r esultat de la comparaison, lalgorithme peut sengager soit dans le sous-arbre gauche, soit dans le sous-arbre droit. Lalgorithme fait une premi` ere comparaison qui est la racine de larbre, puis sengage dans lun des deux sous-arbres ls et fait une deuxi` eme comparaison, et ainsi de suite... Quand lalgorithme sarr ete cest quil a ni de trier lentr ee : il ne fera plus dautres comparaisons et une feuille de larbre est atteinte. Chaque ordre des el ements dentr ee m` ene ` a une feuille di erente, et le nombre de comparaisons ` a eectuer pour atteindre cette feuille est la profondeur de la feuille dans larbre. Larbre explorant toutes les comparaisons, il en r esulte que le nombre de ses feuilles pour des entr ees de taille n est au moins egal ` a n!, cardinal de lensemble de toutes les permutations des n positions. Soit h, la hauteur de larbre. Dans le pire cas, il est n ecessaire de faire h comparaisons pour trier les n el ements (autrement dit, la feuille correspondant ` a la permutation correcte & M. Finiasz 21

2.4 R esolution d equations de r ecurrence

ENSTA cours IN101

se trouve sur un chemin de longueur 6 h). Le nombre de feuilles dun arbre binaire de hauteur h etant au plus 2h , on a 2h n!, soit h log(n!). La formule de Stirling n! = donne 1 n 2n( )n (1 + ( )), e n

n log(n!) > log(( )n ) = nlog(n) nlog(e), e

et par suite, h = (n log(n)). Nous enon cons ce r esultat en : Th eor` eme 2.3.1. Tout algorithme de tri par comparaison n ecessite (n log(n)) comparaisons dans le pire cas. Les tris fusion et rapide sont donc des tris par comparaison asymptotiquement optimaux.

2.4

R esolution d equations de r ecurrence

Lanalyse des performances dun algorithme donne en g en eral des equations o` u le temps de calcul, pour une taille des donn ees, est exprim e en fonction du temps de calcul pour des donn ees de taille moindre. Il nest pas toujours possible de r esoudre ces equations. Dans cette section, nous donnons quelques techniques permettant de trouver des solutions exactes ou approch ees de r ecurrences intervenant classiquement dans les calculs de co ut.

2.4.1

R ecurrences lin eaires

Commen cons par rappeler la m ethode de r esolution des r ecurrences lin eaires homog` enes ` a coecients constants. Il sagit d equations du type : un+h = ah1 un+h1 + . . . + a0 un , a0 = 0, h N , n N. Cette suite est enti` erement d etermin ee par ses h premi` eres valeurs u0 , . . . , uh1 (la suite est dordre h). Son polyn ome caract eristique est : G(x) = xh ah1 xh1 . . . a1 x a0 . Soit 1 , . . . , r , ses racines, et soit ni , la multiplicit e de i , 1 i r. En consid erant la n s erie g en eratrice formelle associ ee ` a un , soit U (X ) = n=0 un X , et en multipliant cette
6. Toutes les feuilles ne se trouvant pas sur le dernier niveau.

22

F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 2. R ecursivit e

s erie par le polyn ome B (X ) = X h G(1/X ) (polyn ome r eciproque de G(X )), on obtient un polyn ome A(X ) de degr e au plus h 1. Lexpression : U (X ) = A(X ) B (X )

montre alors que U (X ) est en fait une s erie rationnelle, i.e. une fraction rationnelle. Sa d ecomposition en el ements simples donne lexpression suivante 7 pour le terme g en eral de la suite : r n , un = Pi (n)i
i=1

o` u Pi (n) est un polyn ome de degr e au plus ni . Une expression de cette forme pour un est appel ee polyn ome exponentiel. La suite de Fibonacci Fn = Fn1 + Fn2 , avec u0 = 0 et u1 = 1, est un exemple de 1+ 1 n n u = 2 5 . La suite telle r ecurrence, trait e dans le chapitre 1. On a Fn = 5 ( ), o` de Fibonacci intervient dans le calcul de co ut de nombreux algorithmes. Consid erons par exemple lalgorithme dEuclide de calcul du pgcd de deux entiers x et y non tous les deux nuls (on suppose x y ) :
1 2 3 4 5 6 7 8

int euclide(int x, int y) { if (y == 0) { return x; } else { /* en C, x modulo y s ecrit x % y */ return euclide(y,x % y); } }

Notons n(x, y ), le nombre de divisions avec reste eectu ees par lalgorithme. Alors n(x, y ) = 0 si y = 0, et n(x, y ) = 1 + n(y, x mod y ) sinon. Pour evaluer le co ut de lalgorithme (en fonction de x), nous allons dabord prouver que, pour x > y 0, n(x, y ) = k x Fk+2 . Pour k = 0, on a x 1 = F2 , et, pour k = 1, on a x 2 = F3 . Supposons ` a pr esent k 2, et la propri et e ci-dessus vraie pour tout j k 1, i.e. si n(u, v ) = j , alors u Fj +2 , pour u, v N, u v > 0. Supposons n(x, y ) = k , et consid erons les divisions euclidiennes : x = qy + z, 0 z < y, y = q z + u, 0 u < z. On a n(y, z ) = k 1 donc y Fk+1 ; de m eme, z Fk et par suite x Fk+1 + Fk = Fk+2 . 1 n n Dautre part, comme 1, n N, on a Fn 5 ( 1). Do` u 5x + 1 k+2 . Donc, pour x > y 0, n(x, y ) log ( 5x + 1) 2,
ements dalgorithmique, de Beauquier, Berstel, 7. Le d etail des calculs peut etre trouv e dans El Chr etienne, ed. Masson.

& M. Finiasz

23

2.4 R esolution d equations de r ecurrence

ENSTA cours IN101

autrement dit, n(x, y ) = O(log(x)). Il existe de nombreux autres types d equations de r ecurrence. Parmi eux, les r ecurrences lin eaires avec second membre constituent une classe importante de telles r ecurrences ; elles peuvent etre r esolues par une technique similaire ` a celle pr esent ee pour les equations homog` enes. Des m ethodes adapt ees existent pour la plupart des types de r ecurrences rencontr ees ; une pr esentation de ces di erentes techniques peut etre trouv ee dans le livre El ements dalgorithmique de Berstel et al. Nous allons dans la suite nous concentrer sur les equations de r ecurrence que lon rencontre typiquement dans les algorithmes diviser pour r egner.

2.4.2

R ecurrences de partitions
(2.1)

Nous consid erons ici des relations de r ecurrence de la forme : T (1) = d T (n) = aT ( n ) + f (n), n > 1. b

o` u a R+ , b N, b 2. Notre but est de trouver une expression de T (n) en fonction de n. Dans lexpression ci-dessus, lorsque n nest pas entier, on linterpr` ete par n ou n . b b b La r ecurrence (2.1) correspond au co ut du calcul eectu e par un algorithme r ecursif du type diviser pour r egner, dans lequel on remplace le probl` eme de taille n par a sousprobl` emes, chacun de taille n . Le temps de calcul T ( n ) est donc aT ( n ) auquel il faut b b ajouter le temps f (n) n ecessaire ` a la combinaison des solutions des probl` emes partiels en une solution du probl` eme total. En g en eral, on evalue une borne sup erieure sur le co ut (n) de lalgorithme, ` a savoir (1) = d, et (n) a ( n ) + f ( n ), n > 1. On a donc ( n ) T (n) b pour tout n, et donc la fonction T constitue une majoration du co ut de lalgorithme. Lors de lanalyse de la complexit e du tri fusion, nous avons vu une m ethode de r esolution d equations de type (2.1), dans le cas o` u a = b = 2 : nous avons construit un arbre binaire dont les nuds repr esentent des co uts, tel que la somme des nuds du niveau de n profondeur i de larbre correspond au co ut total des 2i sous-probl` emes de taille 2 i . Pour a et b quelconques, larbre construit est a-aire, de hauteur logb (n), le niveau de profondeur i n correspondant au co ut des ai sous-probl` emes, chacun de taille b i , 0 i logb (n). Comme pour le tri fusion, laddition des co uts ` a chaque niveau de larbre donne le co ut total T (n) pour une donn ee de taille n. Cette m ethode est appel ee m ethode de larbre r ecursif. Elle donne lieu ` a une expression g en erique pour T (n) en fonction de n. Lemme 2.4.1. Soit f : N R+ , une fonction d enie sur les puissances exactes de b, et + soit T : N R , la fonction d enie par T (1) = d ) + f (n), n = bp , p N , T (n) = aT ( n b o` u b 2 est entier, a > 0 et d > 0 sont r eels. Alors
logb (n)1

(2.2)

T (n) = (n 24

logb (a)

)+

i=0

n ai f ( b i)

(2.3) F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 2. R ecursivit e

Preuve. Lexpression ci-dessus provient exactement de laddition des co uts ` a chaque niveau de larbre r ecursif correspondant ` a la r ecurrence (2.2) : larbre est a-aire, de hauteur p = logb (n) ; le niveau de profondeur i correspond au co ut des ai sous-probl` emes, chacun n n i de taille bi ; le co ut total du niveau i, 0 i logb (n) 1, est donc a f ( bi ). Le dernier niveau (i = logb (n)) a un co ut de ap d. Ainsi : T (n) = a d + On a : ap = alogb (n) = blogb (n)logb (a) = (bplogb (a) ) = (nlogb (a) ), do` u ap d = (nlogb (a) ). Dans le cas o` u f (n) = nk , le temps de calcul est donn e par le th eor` eme suivant. Th eor` eme 2.4.1. Soit T : N R+ , une fonction croissante, telle quil existe des entiers b 2, et des r eels a > 0, c > 0, d > 0, k 0, pour lesquels T (1) = d T (n) = aT ( n ) + cnk , n = bp , p N . b Alors : (nk ) (nk logb (n)) T (n) = (nlogb (a) ) si a < bk si a = bk si a > bk . (2.4)
p p1 i=0 n ai f ( b i)

(2.5)

Preuve. On suppose dabord que n est une puissance de b, soit n = bp , pour un entier p 1. Par le lemme pr ec edent, T (n) = (n
logb (a) p1 a ) + cn ( k )i . b i=0 k

1 a i Notons (n) = p i=0 ( bk ) . a Si bk = 1, (n) = p logb (n) et donc T (n) = ap d + cnk (n) = (nk logb (n)). Supposons ` a pr esent ba k = 1.
p1 p 1 ( ba a k) . (n) = ( k )i = b 1 ( ba k) i=0

Si Si

a bk a bk

1 p k < 1, alors limp ( ba k ) = 0 donc (n) 1(a/bk ) , et T (n) = (n ). > 1, p (a a k) 1 = (( k )p 1), (n) = b a ( bk ) 1 b

& M. Finiasz

25

2.4 R esolution d equations de r ecurrence avec =


1 . a/bk 1

ENSTA cours IN101

p (n) ( ba 1 1 k) = lim = 0, a p a p et p ( a ( bk ) ( bk ) )p bk p (n) ( ba k) ,

donc et cnk (n) cap = (nlogb (a) ) do` u T (n) = (nlogb (a) ). Maintenant, soit n susamment grand, et soit p N , tel que bp n < bp+1 . On a T (bp ) T (n) T (bp+1 ). Or, g (bn) = (g (n)) pour chacune des trois fonctions g intervenant au second membre de (2.5) ; do` u T (n) = (g (n)). Remarques : 1. On compare k ` a logb (a). Le comportement asymptotique de T (n) suit nmax(k,logb (a)) , sauf pour k = logb (a), auquel cas on multiplie la complexit e par un facteur correctif logb (n). 2. Ce th eor` eme couvre les r ecurrences du type (2.1), avec f (n) = cnk , c > 0, k 0. Si f (n) = O(nk ), le th eor` eme reste valable en rempla cant les par des O. Mais 8 la borne obtenue peut alors ne pas etre aussi ne que celle obtenue en appliquant directement la formule (2.3) (i.e. la m ethode de larbre r ecursif). Par exemple, si lon a une r ecurrence de la forme : T (1) = 0 T (n) = 2T (n/2) + nlog(n), n = 2p , p N , la fonction f (n) = n log(n) v erie f (n) = O(n3/2 ). Le th eor` eme pr ec edent donne T (n) = 3/2 O(n ) (a = b = 2, k = 3/2). Supposons dabord que n est une puissance de 2, soit n = 2p . Par lexpression (2.3) : T (n) = soit
p1 i=0

n n n p(p 1) 2 i log( i ) = n log( i ) = np log(n) i = np log(n) , 2 2 2 2 i=0 i=0


i

p1

p1

log(n)(log(n) 1) . 2 Ainsi, T (n) = (n(log(n))2 ). On obtient le m eme r esultat lorsque n nest pas une puissance de 2 en encadrant n entre deux puissances cons ecutives de 2. T (n) = n(log(n))2 En revanche, la r ecurrence T (1) = 1 T (n) = 2T (n/4) + n2 n, n = 4p , p N ,
8. Elle d epend de la nesse de lapproximation de f comme O(nk ).

(2.6)

26

F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 2. R ecursivit e

est bien adapt ee au th eor` eme pr ec edent, avec a = 2, b = 4, c = 1, k = 3/2. On a a < bk = 8 donc T (n) = (n3/2 ). Enn, de fa con plus g en erale, nous pouvons enoncer le th eor` eme suivant. Th eor` eme 2.4.2. Soit T : N R+ , une fonction croissante telle quil existe des entiers b 2, des r eels a > 0, d > 0, et une fonction f : N R+ pour lesquels T (1) = d T (n) = aT (n/b) + f (n), n = bp , p N . Supposons de plus que f (n) = cnk (logb (n))q pour des r eels c > 0, k 0 et q . Alors : (nk ) (nk logb (n)1+q ) (nk logb (logb (n))) T (n) = (nlogb (a) ) (nlogb (a) ) Preuve. Soit n = bp . La formule (2.3) donne T (n) = (nlogb (a) ) + (n) = Soit T p1
i=0 p1 i=0

(2.7)

si si si si si

a < bk et a = bk et a = bk et a = bk et a > bk .

q q q q

=0 > 1 = 1 < 1

(2.8)

ai f (bpi ).

ai f (bpi ).
p i=1

(n) = T

pi

f (b ) = c
bk

p i=1

pi ik

do` u 9 , en notant (n) =

p bk b (logb (b) ) = ca ( )i iq , a i=1 i q p

i=1 ( a

)i iq : T (n) = (nlogb (a) (n)).

Si a = bk , alors

si q > 1 (p1+q ) = ((logb (n))1+q ) (logb (p)) = (logb (logb (n))) si q = 1 (n) = (1) si q < 1. p (Ces estimations sont obtenues en approchant (n) par 1 xq dx quand p ). Si a > bk , alors (n) = (1). Si a < bk et q = 0, on a (n) = (nk /nlogb (a) ). Lorsque n nest pas une puissance de b, on proc` ede comme dans la preuve du th eor` eme pr ec edent.
9. cf. preuve du lemme 2.4.1.

& M. Finiasz

27

2.5

Compl ements Compl ements R ecursivit e terminale

ENSTA cours IN101

2.5
2.5.1

La r ecursivit e terminale (tail recursivity en anglais) est un cas particulier de r ecursivit e: il sagit du cas o` u un algorithme r ecursif eectue son appel r ecursif comme toute derni` ere instruction. Cest le cas par exemple de lalgorithme dEuclide vu page 23 : lappel r ecursif se fait dans le return, et aucune op eration nest eectu ee sur le r esultat retourn e par lappel r ecursif, il est juste pass e au niveau du dessus. Dans ce cas, le compilateur a la possibilit e doptimiser lappel r ecursif : au lieu deectuer lappel r ecursif, r ecup erer le r esultat ` a ladresse de retour quil a x e pour lappel r ecursif, et recopier le r esultat ` a sa propre adresse de retour, lalgorithme peut directement choisir de redonner sa propre adresse de retour ` a lappel r ecursif. Cette optimisation pr esente lavantage de ne pas avoir une pile r ecapitulant les appels r ecursifs aux di erentes sous-fonctions : lutilisation de r ecursion terminale supprime toute limite sur le nombre dappels r ecursifs imbriqu es. Bien s ur, pour que la r ecursion terminale pr esente un int er et, il faut quelle soit g er ee par le compilateur. Cest le cas par exemple du compilateur Caml, ou (pour les cas simples de r ecursivit e terminale) de gcc quand on utilise les options doptimisation -O2 ou -O3. Attention ! Si lappel r ecursif nest pas la seule instruction dans le return, il devient impossible pour le compilateur de faire loptimisation : par exemple, un algorithme se terminant par une ligne du type return n*recursif(n-1) ne fait pas de r ecursivit e terminale.

2.5.2

D er ecursication dun programme

Il est toujours possible de supprimer la r ecursion dun programme an den obtenir une version it erative. Cest en fait ce que fait un compilateur lorsquil traduit un programme r ecursif en langage machine. En eet, pour tout appel de proc edure, un compilateur engendre une s erie g en erique dinstructions : placer les valeurs des variables locales et ladresse de la prochaine instruction sur la pile, d enir les valeurs des param` etres de la proc edure et aller au d ebut de celle-ci ; et de m eme ` a la n dune proc edure : d epiler ladresse de retour et les valeurs des variables locales, mettre ` a jour les variables et aller ` a ladresse de retour. Lorsque lon veut d er ecursier un programme, la technique employ ee par le compilateur est la technique la plus g en erique. Cependant, dans certains cas, il est possible de faire plus simple, m eme si lutilisation dune pile est presque toujours n ecessaire. Nous donnons ici deux exemples de d er ecursication, pour lalgorithme dEuclide, et pour un parcours darbre binaire (cf. chapitre 5). D er ecursication de lalgorithme dEuclide. L ecriture la plus simple de lalgorithme dEuclide est r ecursive : 28 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 2. R ecursivit e

1 2 3 4 5 6 7 8

int euclide(int x, int y) { if (y == 0) { return x; } else { /* en C, x modulo y s ecrit x % y */ return euclide(y,x % y); } }

Toutefois, comme nous lavons vu dans la section pr ec edente, il sagit ici de r ecursion terminale. Dans ce cas, de la m eme fa con que le compilateur arrive ` a se passer de pile, nous pouvons aussi nous passer dune pile, et r ecrire le programme avec une simple boucle while. On conserve la m eme condition de terminaison (sauf que dans la boucle while il sagit dune condition de continuation quil faut donc inverser), les deux variables x et y, et on se contente de mettre les bonnes valeurs dans x et y ` a chaque tour de la boucle. Cela donne la version it erative de lalgorithme dEuclide :
1 2 3 4 5 6 7 8 9 10 11

int iterative_euclide(int x, int y) { int tmp; while (y != 0) { /* une variable temporaire est n ecessaire pour " echanger" les valeurs x et y */ tmp = x; x = y; y = tmp % y; } return x; }

D er ecursication dun parcours darbre. Prenons lalgorithme r ecursif de parcours pr exe darbre binaire suivant :
1 2 3 4 5 6 7 8

void depth_first_traversing(node n) { if ( n != NULL ) { explore(n); depth_first_traversing(n->left); depth_first_traversing(n->right); } return; }

Ici node est une structure correspondant ` a un nud de larbre. Cette structure contient les deux ls du nud left et right et toute autre donn ee que peut avoir ` a contenir le nud. La fonction explore est la fonction que lon veut appliquer ` a tous les nuds de larbre (cela peut- etre une comparaison dans le cas dune recherche dans larbre). & M. Finiasz 29

2.5

Compl ements

ENSTA cours IN101

Ici nous sommes en pr esence dune r ecursion double, il est donc n ecessaire dutiliser une pile pour g erer lensemble des appels r ecursifs imbriqu es. On suppose donc quune pile est impl ement ee (cf. section 3.3.1) et que les fonction push et pop nous permettent respectivement dajouter ou de r ecup erer un el ement dans cette pile. On suppose que la fonction stack is empty renvoie 1 quand la pile est vide, 0 autrement. Ce qui va rendre cette d er ecursication plus facile que le cas g en eral est quici les di erents appels ` a la fonction sont ind ependants les uns des autres : la fonction explore ne renvoie rien, et lon nutilise pas son r esultat pour modier la fa con dont le parcours va se passer. On obtient alors le code it eratif suivant :
void iterative_depth_first_traversing(node n) { node current; push(n); while ( !stack_is_empty() ) { current = pop(); if (current != NULL) { explore(current); push(current->right); push(current->left); } } return; }

1 2 3 4 5 6 7 8 9 10 11 12 13

Le but est que chaque nud qui est mis sur la pile soit un jour explor e, et que tous ses ls le soient aussi. Il sut donc de mettre la racine sur la pile au d epart, et ensuite, tant que la pile nest pas vide dexplorer les nuds qui sont dessus et ` a chaque fois dajouter leurs ls. Il faut faire attention ` a ajouter les ls dans lordre inverse des appels r ecursifs pour que le parcours se fasse bien dans le m eme ordre. En eet, le dernier nud ajout e sur la pile sera le premier explor e ensuite.

2.5.3

Ind ecidabilit e de la terminaison

Un exemple de preuve de terminaison. Comme nous lavons vu, dans un algorithme r ecursif, il est indispensable de v erier que les conditions de terminaison seront atteintes pour tous les appels r ecursifs, et cela quelle que soit lentr ee. La fa con la plus simple de prouver que cest le cas est de d enir une distance aux conditions de terminaison et de montrer que cette distance est strictement d ecroissante lors de chaque appel r ecursif 10 . Prenons par exemple le cas dun calcul r ecursif de coecients binomiaux (bas e sur la construction du triangle de Pascal) :
10. Dans le cas ou cette distance est discr` ete (si par exemple cette distance est toujours enti` ere), une d ecroissance stricte est susante, mais ce nest pas le cas si la distance est continue (ce qui narrive jamais en informatique !).

30

F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 2. R ecursivit e

1 2 3 4 5 6

int binomial(int n, int p) { if ((p==0) || (n==p)) { return 1; } return binomial(n-1,p) + binomial(n-1,p-1); }

Ici la condition de terminaison est double : lalgorithme sarr ete quand lun des deux bords du triangle de Pascal est atteint. Pour prouver que lun des bords est atteint on peut utiliser la mesure D : (n, p) D(n, p) = p (n p). Ainsi, on est sur un bord quand la mesure vaut 0 et on a D(n 1, p) < D(n, p) et D(n 1, p 1) < D(n, p). Donc la distance d ecro t strictement ` a chaque appel r ecursif. Cela prouve donc que cet algorithme termine. Notons toutefois que cette fa con de calculer les coecients binomiaux est tr` es mauvaise, il est bien plus rapide dutiliser un algorithme it eratif faisant le calcul du d eveloppement en factoriels du coecient binomial.

Un exemple dalgorithme sans preuve de terminaison. Jusqu` a pr esent, tous les algorithmes r ecursifs que lon a vu terminent, et on peut de plus prouver quils terminent. Cependant dans certains cas, aucune preuve nest connue pour dire que lalgorithme termine. Dans ce cas on parle dind ecidabilit e de la terminaison : pour une entr ee donn ee, on ne peut pas savoir si lalgorithme va terminer, et la seule fa con de savoir sil termine est dex ecuter lalgorithme sur lentr ee en question, puis dattendre quil termine (ce qui peut bien s ur ne jamais arriver). Regardons par exemple lalgorithme suivant :

1 2 3 4 5 6 7 8 9

int collatz(int n) { if (n==1) { return 0; } else if ((n%2) == 0) { return 1 + collatz(n/2); } else { return 1 + collatz(3*n+1); } }

Cet algorithme fait la chose suivante : on part dun entier n si cet entier est pair on le divise par 2 sil est impair on le multiplie par 3 et on ajoute 1 on recommence ainsi jusqu` a atteindre 1 lalgorithme renvoie le nombre d etapes n ecessaires avant datteindre 1 31

& M. Finiasz

2.5

Compl ements

ENSTA cours IN101

La conjecture de Collatz (aussi appel ee conjecture de Syracuse 11 ) dit que cet algorithme termine toujours. Cependant, ce nest quune conjecture, et aucune preuve nest connue. Nous somme donc dans un contexte o` u, pour un entier donn e, la seule fa con de savoir si lalgorithme termine est dex ecuter lalgorithme et dattendre : la terminaison est ind ecidable. Un exemple dalgorithme pour lequel on peut prouver que la terminaison est ind ecidable. Maintenant nous arrivons dans des concepts un peu plus abstraits : avec la conjecture de Collatz nous avions un algorithme pour lequel aucune preuve de terminaison nexiste, mais nous pouvons aussi imaginer un algorithme pour lequel il est possible de prouver quil ne termine pas pour certaines entr ees, mais d ecider a priori sil termine ou non pour une entr ee donn ee est impossible. La seule fa con de savoir si lalgorithme termine est de lex ecuter et dattendre, sachant que pour certaines entr ees lattente sera innie. Lalgorithme en question est un prouveur automatique pour des propositions logiques. Il prend en entr ee un proposition A et cherche ` a prouver soit A, soit non-A. Il commence par explorer toutes les preuves de longueur 1, puis celles de longueur 2, puis 3 et ainsi de suite. Si la proposition A est d ecidable, cest-` a-dire que A ou non-A admet une preuve, cette preuve est de longueur nie, et donc notre algorithme va la trouver et terminer. En revanche, nous savons que certaines propositions sont ind ecidables : cest ce quarme le th eor` eme dincompl etude de G odel (cf. http://en.wikipedia.org/wiki/Kurt_Godel, suivre le lien G odels incompleteness theorems ). Donc nous savons que cet algorithme peut ne pas terminer, et nous ne pouvons pas d ecider sil terminera pour une entr ee donn ee. Nous pouvons donc prouver que la terminaison de cet algorithme est ind ecidable, contrairement ` a lexemple pr ec edent ou la terminaison etait ind ecidable uniquement parce quon ne pouvait pas prouver la terminaison. Le probl` eme de larr et de Turing. D` es 1936, Alan Turing sest int eress e au probl` eme de la terminaison dun algorithme, quil a formul e sous la forme du probl` eme de larr et (halting problem en anglais) : Etant donn ee la description dun programme et une entr ee de taille nie, d ecider si le programme va nir ou va sex ecuter ind eniment pour cette entr ee. Il a prouv e quaucun algorithme g en erique ne peut r esoudre ce probl` eme pour tous les couples programme/entr ee possibles. Cela signie en particulier quil existe des couples programme/entr ee pour lesquels le probl` eme de larr et est ind ecidable. La preuve de Turing est assez simple et fonctionne par labsurde. Supposons que lon ee un algorithme A et une entr ee i et ait un algorithme halt or not(A,i) prenant en entr qui retourne vrai si A(i) termine, et faux si A(i) ne termine pas. On cr ee alors lalgorithme suivant :
11. Pour plus de d etails sur cette conjecture, allez voir la page http://fr.wikipedia.org/wiki/ ete http://en.wikipedia.org/wiki/ Conjecture_de_Collatz, ou la version anglaise un peu plus compl` Collatz_conjecture.

32

F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 2. R ecursivit e

1 2 3 4 5 6 7 8

void halt_err(program A) { if (halt_or_not(A,A)) { while (1) { printf("Boucle infinie\n"); } } return; }

Cet algorithme halt err va donc terminer uniquement si halt or not(A,A) renvoie faux, sinon, il part dans une boucle innie. Maintenant si on appel halt err(halt err), le programme ne termine que si halt or not(halt err,halt err) renvoie faux, mais sil termine cela signie que halt or not(halt err,halt err) devrait renvoyer vrai. De m eme, si lappel ` a halt or not(halt err,halt err) renvoie vrai, halt err(halt err) va tourner ind eniment, ce qui signie que halt or not(halt err,halt err) aurait du renvoyer faux. Nous avons donc une contradiction : un programme ne peut donc pas r esoudre le probl` eme de larr et pour tous les couples programme/entr ee.

& M. Finiasz

33

Chapitre 3 Structures de Donn ees


Linformatique a r evolutionn e le monde moderne gr ace ` a sa capacit e` a traiter de grandes quantit es de donn ees, des quantit es beaucoup trop grandes pour etre trait ees ` a la main. Cependant, pour pouvoir manipuler ecacement des donn ees de grande taille il est en g en eral n ecessaire de bien les structurer : tableaux, listes, piles, arbres, tas, graphes... Une multitude de structures de donn ees existent, et une multitude de variantes de chaque structure, chaque fois mieux adapt ee ` a un algorithme en particulier. Dans ce chapitre nous voyons les principales structures de donn ees n ecessaires pour optimiser vos premiers algorithmes.

3.1

Tableaux

Les tableaux sont la structure de donn ee la plus simple. Ils sont en g en eral impl ement es nativement dans la majorit e des langages de programmation et sont donc simples ` a utiliser. Un tableau repr esente une zone de m emoire cons ecutive dun seul bloc (cf. Figure 3.1) ce qui pr esente ` a la fois des avantages et des inconv enients : la m emoire est en un seul bloc cons ecutif avec des el ements de taille constante ` a lint erieur (la taille de chaque el ement est d eni par le type du tableau), donc il est eme tr` es facile dacc eder au i` el ement du tableau. Linstruction tab[i] se contente de prendre ladresse m emoire sur laquelle pointe tab et dy ajouter i fois la taille dun el ement. il faut xer la taille du tableau avant de commencer ` a lutiliser. Les syst` emes dexploitation modernes gardent une trace des processus auxquels les di erentes zones de m emoire appartiennent : si un processus va ecrire dans une zone m emoire qui ne lui appartient pas (une zone que le noyau ne lui a pas allou e) il y a une erreur de segmentation. Quand vous programmez, avant d ecrire (ou de lire) ` a la case i dun tableau il est n ecessaire de v erier que i est plus petit que la taille allou ee au tableau (car le compilateur ne le v eriera pas pour vous). 35

3.1 Tableaux
Mmoire
tab

ENSTA cours IN101

espace non allou

tab[0] tab[1] tab[2] tab[3] tab[4] tab[5] tab[6] sizeof(tab[0])

Figure 3.1 Repr esentation en m emoire dun tableau simple.

3.1.1

Allocation m emoire dun tableau

Il existe deux fa cons dallouer de la m emoire ` a un tableau. la plus simple permet de faire de lallocation statique. Par exemple int tab[100]; qui va allouer un tableau de 100 entiers pour tab. De m eme int tab2[4][4]; va allouer un tableau ` a deux dimensions de taille 4 4. En m emoire ce tableau bidimensionnel peut ressemblera ` a ce quon voit dans la Figure 3.2. Attention, un tableau allou e statiquement ne se trouve pas dans la m eme zone m emoire quun tableau allou e avec lune des m ethodes dallocation dynamique : ici il se trouve dans la m eme zone que toutes les variables de type int . On appelle cela de lallocation statique car on ne peut pas modier la taille du tableau en cours dex ecution. la deuxi` eme fa con utilise soit la commande new (syntaxe C++), soit la commande malloc (syntaxe C) et permet une allocation dynamique (dont la taille d epend des entr ees par exemple). Lallocation du tableau s ecrit alors int* tab = new int [100]; ou int* tab = (int* ) malloc(100*sizeof(int ));. En revanche cette technique ne permet pas dallouer directement un tableau ` a deux dimensions. Il faut pour cela eectuer une boucle qui s ecrit alors :

1 2 3 4 5 6 7 8 9 10 11

int i; int** tab2; tab2 = (int** ) malloc(4*sizeof(int* )); for (i=0; i<4; i++) { tab2[i] = (int* ) malloc(4*sizeof(int )); } /* ou en utilisant new */ tab2 = new int* [4]; for (i=0; i<4; i++) { tab2[i] = new int [4]; }

36

F. Levy-dit-Vehel

Ann ee 2011-2012
Mmoire
tab

Chapitre 3. Structures de Donn ees


tab[3][0] tab[3][1] tab[3][2] tab[3][3] tab[2][0] tab[2][1] tab[2][2] tab[2][3] tab[1][0] tab[1][1] tab[1][2] tab[1][3]

Figure 3.2 Repr esentation en m emoire dun tableau ` a deux dimensions. Lutilisation de malloc (ou new en C++) est donc beaucoup plus lourde, dautant plus que la m emoire allou ee ne sera pas lib er ee delle m eme et lutilisation de la commande free (ou delete en C++) sera n ecessaire, mais pr esente deux avantages : le code est beaucoup plus proche de ce qui se passe r eellement en m emoire (surtout dans le cas ` a deux dimensions) ce qui permet de mieux se rendre compte des op erations r eellement eectu ees. En particulier, une commande qui para t simple comme int tab[1000][1000][1000]; demande en r ealit e dallouer un million de fois 1000 entiers, ce qui est tr` es long et occupe 4Go de m emoire. cela laisse beaucoup plus de souplesse pour les allocations en deux dimensions (ou plus) : contrairement ` a la notation simpli ee, rien noblige ` a avoir des tableaux carr es ! Pour les cas simples, la notations [100] est donc susante, mais d` es que cela devient compliqu e, lutilisation de malloc devient n ecessaire. Exemple dallocation non carr ee. Le programme suivant permet de calculer tous les coecients binomiaux de fa con r ecursive en utilisant la construction du triangle de Pascal. La m ethode r ecursive simple vue au chapitre pr ec edent (cf. page 30) est tr` es inecace car elle recalcule un grand nombre de fois les m eme coecients. Pour lam eliorer, on utilise un tableau bidimensionnel qui va servir de cache : chaque fois quun coecient est calcul e on le met dans le tableau, et chaque fois que lon a besoin dun coecient on regarde dabord dans le tableau avant de la calculer. Cest ce que lon appelle la programmation dynamique. Le point int eressant est que le tableau est triangulaire, et lutilisation de malloc (ou new) permet de ne pas allouer plus de m emoire que n ecessaire (on gagne un facteur 2 sur loccupation m emoire ici).
int** tab; int binomial(int n, int p) { if (tab[n][p] == 0) {

1 2 3 4

& M. Finiasz

tab[0][0] tab[0][1] tab[0][2] tab[0][3]

tab[0] tab[1] tab[2] tab[3]

37

3.1 Tableaux
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

ENSTA cours IN101

if ((p==0) || (n==p) { tab[n][p] = 1; } else { tab[n][p] = binomial(n-1,p) + binomial(n-1,p-1); } } return tab[n][p]; } int main(int argc, char** argv) { int i; tab = (int** ) malloc(33*sizeof(int* )); for (i=0; i<34; i++) { tab[i] = (int* ) calloc((i+1),sizeof(int )); } for (i=0; i<34; i++) { binomial(33,i); } /* ins erer ici les instructions qui utilisent la table de binomiaux for (i=0; i<33; i++) { free(tab[i]); } free(tab); }

*/

On utilise donc la variable globale tab comme table de cache et la fonction binomiale est exactement la m eme quavant, mis ` a part quelle v erie dabord dans le cache si la valeur a d ej` a et e calcul ee, et quelle stocke la valeur avant de la retourner. On arr ete ici le calcul de binomiaux ` a n = 33 car au-del` a les coecients binomiaux sont trop grands pour tenir dans un int de 32 bits. Notez ici lutilisation de calloc qui alloue la m emoire et linitialise ` a 0, contrairement ` a malloc qui laisse la m emoire non initialis ee. La fonction calloc est donc plus lente, mais linitialisation est n ecessaire pour pouvoir utiliser la technique de mise en cache. Cette technique de programmation dynamique est tr` es utilis ee quand lon veut programmer vite et ecacement un algorithme qui se d ecrit mieux de fa con r ecursive quit erative. Cela sera souvent le cas dans des probl` emes combinatoires ou de d enombrement. 38 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 3. Structures de Donn ees

ration de la Me moire Alloue e Libe Notez la pr esence des free ` a la n de la fonction main : ces free ne sont pas n ecessaire si le programme est termin e` a cet endroit l` a car la m emoire est de toute fa con lib er ee par le syst` eme dexploitation quand le processus sarr ete, mais si dautres instructions doivent suivre, la m emoire allou ee sera d ej` a lib er ee. De plus, il est important de prendre lhabitude de toujours lib erer de la m emoire allou ee (` a chaque malloc doit correspondre un free) car cela permet de localiser plus facilement une fuite de m emoire (de la m emoire allou ee mais non lib er ee, typiquement dans une boucle) lors du debuggage.

3.1.2

Compl ement : allocation dynamique de tableau

Nous avons vu que la structure de tableau a une taille x ee avant lutilisation : il faut toujours allouer la m emoire en premier. Cela rend cette structure relativement peu adapt ee aux algorithmes qui ajoutent dynamiquement des donn ees sans que lon puisse borner ` a lavance la quantit e quil faudra ajouter. Pourtant, les tableaux pr esentent lavantage ` eme davoir un acc` es instantan e` a la i case, ce qui peut etre tr` es utile dans certains cas. On a alors envie davoir des tableaux de taille variable. Cest ce quimpl emente par exemple la classe Vector en java (sauf que la classe Vector le fait mal...). Lid ee de base est assez simple : on veut deux fonctions insert et get(i) qui permettent eme dajouter un el ement ` a la n du tableau et de lire le i` el ement en temps constant (en O(1) en moyenne). Il sut donc de garder en plus du tableau tab deux entiers qui indiquent le nombre d el ements dans le tableau, et la taille totale du tableau (lespace allou e). Lire le eme i` el ement peut se faire directement avec tab[i] (on peut aussi impl ementer une fonction get(i) qui v erie en plus que lon ne va pas lire au-del` a de la n du tableau). En revanche, ajouter un el ement est plus compliqu e: soit le nombre d el ements dans le tableau est strictement plus petit que la taille totale et il sut dincr ementer le nombre d el ements et dins erer le nouvel el ement, soit le tableau est d ej` a rempli et il faut r eallouer de la m emoire. Si on veut conserver un acc` es en temps constant il est n ecessaire de conserver un tableau dun seul bloc, il faut donc allouer un nouvel espace m emoire, plus grand que le pr ec edent, y recopier le contenu de tab, lib erer la m emoire occup ee par tab, mettre ` a jour tab pour pointer vers le nouvel espace m emoire et on est alors ramen e au cas simple vu pr ec edemment. Cette technique marche bien, mais pose un probl` eme : recopier le contenu de tab est une op eration longue et son co ut d epend de la taille totale de tab. Recopier un tableau de taille n a une complexit e de (n). Heureusement, cette op eration nest pas eectu ee ` a chaque fois, et comme nous allons le voir, il est donc possible de conserver une complexit e en moyenne de O(1). La classe Vector de java permet ` a linitialisation de choisir le nombre d el ements ` a ajouter au tableau ` a chaque fois quil faut le faire grandir. Cr eer un Vector de taille n en augmentant de t ` a chaque fois va donc n ecessiter de recopier le contenu du tableau une & M. Finiasz 39

3.2 Listes cha n ees


Mmoire
L data data

ENSTA cours IN101

data data

NULL

Figure 3.3 Repr esentation en m emoire dune liste simplement cha n ee. fois toute les t insertions. La complexit e pour ins erer n el ements est donc : K =n+
t
n

tit

n n ( t t

i=1

+ 1) + n = (n2 ). 2

= (n). Cest beaucoup Donc en moyenne, la complexit e de linsertion dun el ement est K n trop ! La bonne solution consiste ` a doubler la taille du tableau ` a chaque r eallocation (ou la multiplier par nimporte quelle constante plus grande que 2). Ainsi, pour ins erer n el ements n n dans le tableau il faudra avoir recopi e une fois 2 el ements, le coup davant 4 , celui davant n ... Au total la complexit e est donc : 8
log2 n

K =n+

i=0

2i 2n = (n).

= (1). Il est donc possible Ce qui donne en moyenne une complexit e par insertion de K n de conserver toutes les bonnes propri et es des tableaux et dajouter une taille variable sans rien perdre sur les complexit es asymptotiques. La r eallocation dynamique de tableau a donc un co ut assez faible si elle est bien faite, mais elle n ecessite en revanche dutiliser plus de m emoire que les autres structures de donn ees : un tableau contient toujours de lespace allou e mais non utilis e, et la phase de . r eallocation n ecessite dallouer en m eme temps le tableau de taille n et celui de taille n 2

3.2

Listes cha n ees

Contrairement ` a un tableau, une liste cha n ee nest pas constitu ee dun seul bloc de m emoire : chaque el ement (ou nud) de la liste est allou e ind ependamment des autres et contient dune part des donn ees et dautre part un pointeur vers l el ement suivant (cf. 40 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 3. Structures de Donn ees

Figure 3.3). Une liste est donc en fait simplement un pointeur vers le premier el ement de la liste et pour acc eder ` a un autre el ement, il sut ensuite de suivre la cha ne de pointeurs. En g en eral le dernier el ement de la liste pointe vers NULL, ce qui signie aussi quune liste vide est simplement un pointeur vers NULL. En C, lutilisation de liste n ecessite la cr eation dune structure correspondant ` a un nud de la liste. Le type list en lui m eme doit ensuite etre d eni comme un pointeur vers un nud. Cela donne le code suivant :

1 2 3 4 5

struct cell { /* ins erer ici toutes les donn ees que doit contenir un noeud */ cell* next; }; typedef cell* list;

3.2.1

Op erations de base sur une liste

Insertion. Linsertion dun el ement ` a la suite dun el ement donn e se fait en deux etapes illustr ees sur le dessin suivant :
Liste initiale
Cration du nud

data

Insertion dans la liste data

data

data

data

data

data

data

On cr ee le nud en lui donnant le bon nud comme nud suivant. Il sut ensuite de faire pointer l el ement apr` es lequel on veut lins erer vers ce nouvel el ement. Le dessin repr esente une insertion en milieu de liste, mais en g en eral, lajout dun el ement ` a une liste se fait toujours par le d ebut : dans ce cas lop eration est la m eme, mais le premier el ement de liste est un simple pointeur (sans champ data).

Suppression. Pour supprimer un nud cest exactement lop eration inverse, il sut de faire attention ` a bien sauvegarder un pointeur vers l el ement que lon va supprimer pour pouvoir lib erer la m emoire quil utilise. & M. Finiasz 41

3.2 Listes cha n ees


Liste initiale

ENSTA cours IN101


Sauvegarde du pointeur tmp

data data data data

data data

Suppression dans la liste tmp

Libration de la mmoire tmp

data data data data

data data

Ici encore, le dessin repr esente une suppression en milieux de liste, mais le cas le plus courant sera la suppression du premier el ement dune liste. Parcours. Pour parcourir une liste on utilise un curseur qui pointe sur l el ement que lon est en train de regarder. On initialise le curseur sur le premier el ement et on suit les pointeurs jusqu` a arriver sur NULL. Il est important de ne jamais perdre le pointeur vers le premier el ement de la liste (sinon les el ements deviennent d enitivement inaccessibles) : cest pour cela que lon utilise une autre variable comme curseur. Voila le code C dune fonction qui recherche une valeur (pass ee en argument) dans une liste dentiers, et remplace toutes les occurrences de cette valeur par des 0.
1 2 3 4 5 6 7 8 9 10

void cleaner(int n, list L) { list cur = L; while (cur != NULL) { if (cur->data == n) { cur->data = 0; } cur = cur->next; } return; }

Notons quici, la liste L est pass ee en argument, ce qui cr ee automatiquement une nouvelle variable locale ` a la fonction cleaner. Il n etait donc pas n ecessaire de cr eer la variable cur. Cela ayant de toute fa con un co ut n egligeable par rapport ` a un appel de fonction, il nest pas g enant de prendre lhabitude de toujours avoir une variable d edi ee pour le curseur. 42 F. Levy-dit-Vehel

Ann ee 2011-2012
Mmoire
L data

Chapitre 3. Structures de Donn ees

L_end

data

data NULL

NULL

Figure 3.4 Repr esentation en m emoire dune liste doublement cha n ee.

3.2.2

Les variantes : doublement cha n ees, circulaires...

La structure de liste est une structure de base tr` es souvent utilis ee pour construire des structures plus complexes comme les piles ou les les que nous verrons ` a la section suivante. Selon les cas, un simple pointeur vers l el ement suivant peut ne pas sure, on peut chercher ` a avoir directement acc` es au dernier el ement... Une multitude de variations existent et seules les plus courantes sont pr esent ees ici. Listes doublement cha n ees. Une liste doublement cha n ee (cf. Figure 3.4) poss` ede en plus des listes simples un pointeur vers l el ement pr ec edent dans la liste. Cela saccompagne aussi en g en eral dune deuxi` eme variable L end pointant vers le dernier el ement de la liste et permet ainsi un parcours dans les deux sens et un ajout simple d el ements en n de liste. Le seul surco ut est la pr esence du pointeur en plus qui ajoute quelques op erations de plus ` a chaque insertion/suppression et qui occupe un peu despace m emoire. Listes circulaires. Les listes circulaires sont des listes cha n ees (simplement ou doublement) dont le dernier el ement ne pointe pas vers NULL, mais vers le premier el ement (cf. Figure 3.5). Il ny a donc plus de r eelle notion de d ebut et n de liste, il y a juste une position courante indiqu ee par un curseur. Le probl` eme est quune telle liste ne peux jamais etre vide : an de pouvoir g erer ce cas particulier il est n ecessaire dutiliser ce que lon appelle une sentinelle. Sentinelles. Dans une liste, une sentinelle est un nud particulier qui doit pouvoir etre reconnaissable en fonction du contenu de son champ data (une m ethode classique est dajouter un entier au champ data qui est non-nul uniquement pour la sentinelle) et qui sert juste ` a simplier la programmation de certaines listes mais ne repr esente pas un r eel el ement de la liste. Une telle sentinelle peut avoir plusieurs usages : & M. Finiasz 43

3.3 Piles & Files


Mmoire
cur data

ENSTA cours IN101

data

data data

Figure 3.5 Repr esentation en m emoire dune liste circulaire simplement cha n ee. repr esenter une liste circulaire vide : il est possible de d ecider quune liste circulaire vide sera repr esent ee en m emoire par une liste circulaire ` a un seul el ement (qui est donc son propre successeur) qui serait une sentinelle. terminer une liste non-circulaire : il peut etre pratique dajouter une sentinelle ` a la n de toute liste cha n ee pour que lorsque la liste est vide des fonctions comme retourner le premier el ement ou retourner le dernier el ement aient toujours quelque chose ` a renvoyer. Dans la plupart des cas on peut leur demander de retourner NULL, mais une sentinelle peut rendre un programme plus lisible. On peut aussi envisager davoir plusieurs types de sentinelles, par exemple une qui marquerait un d ebut de liste et une autre une n de liste.

3.2.3

Conclusion sur les listes

Par rapport ` a un tableau la liste pr esente deux principaux avantages : il ny a pas de limitation de longueur dune liste (` a part la taille de la m emoire) il est tr` es facile dins erer ou de supprimer un el ement au milieu de la liste sans pour autant devoir tout d ecaler ou laisser un trou. En revanche, pour un m eme nombre d el ements, une liste occupera toujours un peu plus despace m emoire quun tableau car il faut stocker les pointeurs (de plus le syst` eme dexploitation conserve aussi des traces de tous les segments de m emoire allou es pour pouvoir eme les d esallouer quand le processus sarr ete) et lacc` es au i` el ement de la liste co ute en moyenne (n) pour une liste de longueur n.

3.3

Piles & Files

Les piles et les les sont des structures tr` es proches des listes et tr` es utilis ees en informatique. Ce sont des structures de donn ees dynamiques (comme les listes) sur lesquelles 44 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 3. Structures de Donn ees

on veut pouvoir ex ecuter deux instructions principales : ajouter un el ement et retirer un el ement (en le retournant an de pouvoir lire son contenu). La seule di erence entre la pile et la liste est que dans le cas de la pile l el ement que lon retire est le dernier ` a avoir et e ajout e (on parle alors de LIFO comme Last In First Out ), et dans le cas de la le on retire en premier les el ement ajout es en premier (cest un FIFO comme First In First Out ). Les noms de pile et le ne sont bien s ur pas choisis au hasard : on peut penser ` a une pile de livres sur une table ou ` a une le dattente ` a la boulangerie qui se comportent de la m eme fa con.

3.3.1

Les piles

Les piles sont en g en eral impl ement ees ` a laide dune liste simplement cha n ee, la seule di erence est que lon utilise beaucoup moins dop erations : en g en eral on ne cherche jamais ` a aller lire ce qui se trouve au fond dune pile, on ne fait quajouter et retirer des el ements. Une pile vide est alors repr esent ee par une liste vide, ajouter un el ement revient ` a ajouter un el ement en t ete de liste, et le retirer ` a retourner son contenu et ` a la supprimer de la liste. Voici en C ce que pourrait etre limpl ementation dune pile dentiers :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

list L; void push(int n) { cell* nw = (cell* ) malloc(sizeof(cell )); nw->data = n; nw->next = L; L = nw; } int pop() { int val; cell* tmp; /* on teste dabord si la pile est vide */ if (L == NULL) { return -1; } val = L->data; tmp = L; L = L->next; free(tmp); return val; }

La fonction push ajoute un entier dans la pile et la fonction pop retourne le dernier entier ajout e` a la pile et le retire de la pile. Pour lutilisateur qui nutilise que les fonctions push et pop, le fait que la pile est impl ement ee avec une liste L est transparent. Exemples dutilisation de piles. Les piles sont une structure de donn ee tr` es basique (il ny a que deux op erations possibles), mais elles sont utilis ees tr` es souvent en informatique. & M. Finiasz 45

3.3 Piles & Files

ENSTA cours IN101

Depuis le d ebut de ce poly nous avons d ej` a vu deux exemples dutilisation : Les tours de Hano : programmer r eellement les tours de Hano n ecessite trois piles (une pour chaque piquet) contenant des entiers repr esentant les rondelles. Dans ces piles on peut uniquement ajouter une rondelle (cest ce que fait push) ou retirer la rondelle du dessus (avec pop). Dans la d er ecursication : lorsquun compilateur d er ecursie un programme, il utilise une pile pour stocker les appels r ecursifs. De fa con plus g en eral, tout appel ` a une fonction se traduit par lajout de plusieurs el ements (adresse de retour...) dans la pile qui sert ` a lex ecution du processus. Une autre utilisation courante de pile est l evaluation dexpressions math ematiques. La notation polonaise inverse (que lon retrouve sur les calculatrice HP) est parfaitement adapt ee ` a lusage dune pile : chaque nombre est ajout e ` a la pile et chaque op eration prend des el ements dans la pile et retourne le r esultat sur la pile. Par exemple, lexpression 5 (3 + 4) s ecrit en notation polonaise inverse 4 3 + 5 ce qui repr esente les op erations : push(4) push(3) push(pop() + pop()) push(5) push(pop() * pop()) ` la n il reste donc juste 35 sur la pile. A

3.3.2

Les les

Les les sont elles aussi en g en erale impl ement ees ` a laide dune liste. En revanche, les deux op erations push et pop que lon veut impl ementer doivent lune ajouter un el ement en n de liste et lautre retirer un el ement en d ebut de liste (ainsi les el ements sortent bien dans le bon ordre : First In, First Out ). Il est donc n ecessaire davoir un pointeur sur la n de la liste pour pouvoir facilement (en temps constant) y ins erer des el ements. Pour cela on utilise donc une liste simplement cha n ee, mais avec deux pointeurs : lun sur le d ebut et lautre sur la n. Si on suppose que L pointe vers le premier el ement de la liste et L end vers le dernier, voici comment peuvent se programmer les fonctions push et pop :
1 2 3 4 5 6 7 8 9 10 11 12

list L, L_end; void push(int n) { cell* nw = (cell* ) malloc(sizeof(cell )); nw->data = n; nw->next = NULL; /* si la file est vide, il faut mettre ` a jour le premier et le dernier el ement */ if (L == NULL) { L = nw; L_end = nw; } else { L_end->next = nw;

46

F. Levy-dit-Vehel

Ann ee 2011-2012
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

Chapitre 3. Structures de Donn ees

L_end = nw; } } int pop() { int val; cell* tmp; /* on teste dabord si la file est vide */ if (L == NULL) { return -1; } val = L->data; tmp = L; L = L->next; free(tmp); return val; }

Encore une fois, pour lutilisateur de cette le, le fait que nous utilisons une liste est transparent. Exemples dutilisation de les. Les les sont utilis ees partout o` u lon a des donn ees ` a traiter de fa con asynchrone avec leur arriv ee, mais o` u lordre de traitement de ces donn ees est important (sinon on pr ef` ere en g en eral utiliser une pile qui est plus l eg` ere ` a impl ementer). Cest le cas par exemple pour le routage de paquets r eseau : le routeur re coit des paquets qui sont mis dans une le au fur et ` a mesure quils arrivent, le routeur sort les paquets de cette le un par un et les traite en fonction de leur adresse de destination pour les renvoyer. La le sert dans ce cas de m emoire tampon et permet ainsi de mieux utiliser les ressources du routeur : si le trac dentr ee est irr egulier il sut gr ace au tampon de pouvoir traiter un ux egal au d ebit dentr ee moyen, alors que sans tampon il faudrait pouvoir traiter un ux egal au d ebit maximum en entr ee.

& M. Finiasz

47

Chapitre 4 Recherche en table


Le probl` eme auquel on sint eresse dans ce chapitre est celui de la mise en uvre informatique dun dictionnaire ; autrement dit, il sagit d etudier les m ethodes permettant de retrouver le plus rapidement possible une information en m emoire, accessible ` a partir dune clef (par exemple, trouver une d enition ` a partir dun mot). La m ethode la plus naturelle est limpl ementation par tableau (table ` a adressage direct : pour g erer un dictionnaire avec des mots jusqu` a 30 lettres on cr ee une table assez grande pour contenir les 2630 mots possibles, et on place les d enitions dans la case correspondant ` a chaque mot), mais bien que sa complexit e temporelle soit optimale, cette m ethode devient irr ealiste en terme de complexit e spatiale d` es lors que lon doit g erer un grand nombre de clefs. Une deuxi` eme m ethode consiste ` a ne consid erer que les clefs eectivement pr esentes en table (recherche s equentielle) : on obtient alors des performances analogues aux op erations de base sur les listes cha n ees, la recherche dun el ement etant lin eaire en la taille de la liste. Une approche plus ecace est la recherche dichotomique, dans laquelle on utilise un tableau contenant les clefs tri ees (comme cest le cas dans un vrai dictionnaire) ; la complexit e de lop eration de recherche devient alors en log(n), est rend cette m ethode tr` es int eressante pour de gros chiers auxquels on acc` ede souvent mais que lon modie peu. Enn, une quatri` eme m ethode abord ee est lutilisation de tables de hachage, qui permet datteindre en moyenne les performances de ladressage direct tout en saranchissant du nombre potentiellement tr` es grand de clefs possible.

4.1

Introduction

Un probl` eme tr` es fr equent en informatique est celui de la recherche de linformation stock ee en m emoire. Cette information est en g en eral accessible via une clef. La consultation dun annuaire electronique est lexemple type dune telle recherche. Un autre exemple est celui dun compilateur, qui doit g erer une table des symboles, dans laquelle les clefs sont les identicateurs des donn es ` a traiter. Les fonctionnalit es 1 dun dictionnaire sont exactement celles quil convient de mettre en oeuvre lorsque lon souhaite g erer linformation en
1. Pour la cr eation, lutilisation et la mise ` a jour.

49

4.2 Table ` a adressage direct

ENSTA cours IN101

m emoire. En eet, les op erations que lon d esire eectuer sont : la recherche de linformation correspondant ` a une clef donn ee, linsertion dune information ` a lendroit sp eci e par la clef, la suppression dune information. ` A noter que si la clef correspondant ` a une information existe d ej` a, linsertion est une modication de linformation. On dispose donc de paires de la forme (clef,information), auxquelles on doit appliquer ces op erations. Pour cela, lutilisation dune structure de donn ees dynamique simpose. Nous allons ici etudier les structures de donn ees permettant limpl ementation ecace de dictionnaires. Nous supposerons que les clefs sont des entiers dans lintervalle [0, m 1], o` u m est un entier qui peut etre tr` es grand (en g en eral il sera souvent tr` es grand). Si les clefs ont des valeurs non enti` eres, on peut appliquer une bijection de lensemble des clefs vers (un sous-ensemble de) [0, m 1]. Nous nous int eresserons ` a la complexit e spatiale et temporelle des trois op erations ci-dessus, complexit es d ependantes de la structure de donn ees utilis ee.

4.2

Table ` a adressage direct

La m ethode la plus naturelle pour rechercher linformation est dutiliser un tableau tab de taille m : tab[i] contiendra linformation de clef i. La recherche de linformation de clef i se fait alors en (1) (il sut de retourner tab[i]), tout comme linsertion (qui est une simple op eration daectation tab[i] = info) et la suppression (tab[i] = NULL, avec la convention que NULL correspond ` a une case vide). Le stockage du tableau n ecessite un espace m emoire de taille (m). En g en eral, une table ` a adressage direct ne contient pas directement linformation, mais des pointeurs vers linformation. Cela pr esente deux avantages : dune part cela permet davoir une information par case de taille variable et dautre part, la m emoire ` a allouer pour le tableau est m fois la taille dun pointeur au lieu de m fois la taille dune information. Dans un tableau dinformations (qui nutiliserait pas de pointeurs) il faut allouer m fois la taille de linformation la plus longue, mais avec des pointeurs on nalloue que m fois la taille dun pointeur, plus la taille totale de toutes les informations. En pratique, si on appelle type info le type dune d enition, on allouera la m emoire avec type info** tab = new type info* [m] pour un tableau avec pointeurs. Par convention tab[i] = NULL si la case est vide (aucune information ne correspond ` a la clef i), sinon tab[i] = &info. Dans le cas o` u linformation est de petite taille, lutilisation dun tableau sans pointeurs peut toutefois etre int eressante. Chaque case du tableau doit alors contenir deux champs : le premier champ est de type bool een et indique la pr esence de la clef (si la clef nest pas pr esente en table cest que linformation associ ee nest pas disponible), lautre est de type type info et contient linformation proprement dite.
1 2

struct cell { bool key_exists;

50

F. Levy-dit-Vehel

Ann ee 2011-2012
3 4 5

Chapitre 4. Recherche en table

type_info info; }; cell* tab = (cell* ) malloc(m*sizeof(cell ));

Toute ces variantes de structures de donn ees conduisent ` a la complexit e donn ee ci-dessus. Cette repr esentation en tableau (ou variantes) est bien adapt ee lorsque m est petit, mais si m est tr` es grand, une complexit e spatiale lin eaire devient rapidement irr ealiste. Par exemple si lon souhaite stocker tous les mots de huit lettres (minuscules sans accents), nous avons m = 268 237.6 mots possibles, soit un espace de stockage de 100 Go rien que pour le tableau de pointeurs.

4.3

Recherche s equentielle

En g en eral, lespace des clefs possibles peut etre tr` es grand, mais le nombre de clefs pr esentes dans la table est bien moindre (cest le cas pour les mots de huit lettres). Lid ee de 2 la recherche s equentielle est de mettre les donn ees (les couples (clef,info)) dans un tableau de taille n, o` u n est le nombre maximal 3 de clefs susceptibles de se trouver simultan ement en table. On utilise aussi un indice p indiquant la premi` ere case libre du tableau. Les donn ees sont ins er ees en n de tableau (tab[p] re coit (clef,info) et p est incr ement e). On eectue une recherche en parcourant le tableau s equentiellement jusqu` a trouver lenregistrement correspondant ` a la clef cherch ee, ou arriver ` a lindice n de n de tableau (on suppose que le tableau est enti` erement rempli). Si lon suppose que toutes les clefs ont la m eme probabilit e d etre recherch ees, le nombre moyen de comparaisons est :
n 1 n+1 i= . n 2 i=1

En eet, si l el ement cherch e se trouve en tab[i], on parcourra le tableau jusqu` a la position i (ce qui correspond ` a i + 1 comparaisons). Les clefs susceptibles d etre cherch ees etant suppos ees equiprobables, i peut prendre toute valeur entre 0 et n 1. En cas de recherche infructueuse, on a ` a eectuer n comparaisons. La complexit e de lop eration de recherche est donc en temps (n). Linsertion dun enregistrement seectue en temps (1), sauf si la clef est d ej` a pr esente en table, auquel cas la modication doit etre pr ec ed ee dune recherche de cette clef. Lop eration de suppression n ecessitant toujours une recherche pr ealable, elle seectue en temps (n) (il est egalement n ecessaire de d ecaler les el ements restant dans le tableau apr` es suppression, ce qui a aussi une complexit e (n)). La complexit e de stockage du tableau est en (n). Si lon dispose dinformation suppl ementaires sur les clefs, on peut am eliorer la complexit e des op erations ci-dessus, par exemple en pla cant les clefs les plus fr equemment lues en d ebut de tableau.
2. On parle aussi de recherche lin eaire. 3. Ou une borne sup erieure sur ce nombre si le nombre maximal nest pas connu davance.

& M. Finiasz

51

4.3 Recherche s equentielle

ENSTA cours IN101

Voici comment simpl emente les op erations de recherche, insertion, suppression dans une telle structure.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38

struct cell { int key; type_info info; }; cell* tab; /* lallocation doit ^ etre faite dans le main */ int p=0; type_info search(int val) { int i; for (i=0; i<p; i++) { if (tab[i].key == val) { return tab[i].info; } } printf("Recherche infructueuse.\n"); return NULL; } /* on suppose quune clef nest jamais ins er ee deux fois */ void insert(cell nw) { tab[p] = nw; p++; } void delete(int val) { int i,j; for (i=0; i<p; i++) { if (tab[i].key == val) { for (j=i; j<p-1; j++) { tab[j] = tab[j+1]; } tab[p-1] = NULL; p--; return; } } printf(" El ement inexistant, suppression impossible.\n"); }

On peut egalement utiliser une liste cha n ee ` a la place dun tableau. Un avantage est alors que lon na plus de limitation sur la taille. Les complexit es de recherche et de suppression sont les m emes que dans limpl ementation par tableau de taille n. Linsertion conserve elle aussi sa complexit e de (1) sauf que les nouveaux el ements sont ins er es au d ebut au lieu d` a la n. La modication n ecessite toujours un temps en (n) (le co ut dune recherche). Asymptotiquement les complexit es sont les m emes, mais dans la pratique la recherche sera un peu plus lente (un parcours de tableau est plus rapide quun parcours 52 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 4. Recherche en table

de liste) et la suppression un peu plus rapide (on na pas ` a d ecaler tous les el ements du tableau). Avec une liste, les op erations sont alors exactement les m emes que celles d ecrites dans le chapitre sur les listes.

4.4

Recherche dichotomique

Une m ethode ecace pour diminuer la complexit e de lop eration de recherche est dutiliser un tableau (ou une liste) de clefs tri ees par ordre croissant (ou d ecroissant). On peut alors utiliser lapproche diviser-pour-r egner pour r ealiser la recherche : on compare la clef val cherch ee ` a celle situ ee au milieu du tableau. Si elles sont egales, on retourne linformation correspondante. Sinon, si val est sup erieure ` a cette clef, on recommence la recherche dans la partie sup erieure du tableau. Si val est inf erieure, on explore la partie inf erieure. On obtient lalgorithme r ecursif suivant (on recherche entre les indices p (inclus) et r (exclus) du tableau) :

1 2 3 4 5 6 7 8 9 10 11 12 13 14

type info dicho_search(cell* tab, int val, int p, int r) { int q = (p+r)/2; if (p == r) { /* la recherche est infructueuse */ return NULL; } if (val == tab[q].key) { return tab[q].info; } else if (val < tab[q].key) { return dicho_search(tab, val, p, q); } else { return dicho_search(tab, val, q+1, r); } }

Complexit e de lop eration de recherche. La complexit e est ici calcul ee en nombre dappels r ecursifs ` a dicho search, ou de mani` ere equivalente en nombre de comparaisons de clefs ` a eectuer (m eme si lalgorithme comporte deux comparaisons, tant que ce nombre est constant il va dispara tre dans le de la complexit e). On note n = r p, la taille du tableau et Cn , la complexit e cherch ee. Pour une donn ee de taille n, on fait une comparaison de clef, puis (si la clef nest pas trouv ee) on appelle la n + 1. De plus, C = 1. , donc C C proc edure r ecursivement sur un donn ee de taille n n 1 2 2 Si n = 2k , on a alors C2k C2k1 + 1 C2k2 + 2 . . . C20 + k = k + 1, autrement dit, C2k = O(k ). & M. Finiasz 53

4.4 Recherche dichotomique

ENSTA cours IN101

Lorsque n nest pas une puissance de deux, notons k lentier tel que 2k n < 2k+1 . Cn est une fonction croissante, donc C2k Cn C2k+1 , soit k + 1 Cn k + 2. Ainsi, Cn = O(k ) = O(log(n)). La complexit e de lop eration de recherche avec cette repr esentation est donc bien meilleure quavec les repr esentations pr ec edentes. Malheureusement ce gain sur la recherche fait augmenter les co ut des autres op erations. Linsertion dun el ement doit maintenant se faire ` a la bonne place (et non pas ` a la n du tableau). Si lon consid` ere linsertion dun el ement choisi al eatoirement, on devra d ecaler en moyenne n/2 el ements du tableau pour le placer correctement. Ainsi, lop eration dinsertion est en (n). Le co ut de lop eration de suppression lui ne change pas et n ecessite toujours le d ecalage den moyenne n/2 el ements. En pratique, cette m ethode est ` a privil egier dans le cas o` u lon a un grand nombre de clef sur lesquels on peut faire de nombreuses requ etes, mais que lon modie peu. On peut alors, dans une phase initiale, construire la table par une m ethode de tri du type tri rapide. Recherche par interpolation. La recherche dichotomique ne mod elise pas notre fa con intuitive de rechercher dans un dictionnaire. En eet, si lon cherche un mot dont la premi` ere lettre se trouve ` a la n de lalphabet, on ouvrira le dictionnaire plut ot vers la n. Une fa con de mod eliser ce comportement est r ealis ee par la recherche par interpolation. Elle consiste simplement ` a modier lalgorithme pr ec edent en ne consid erant plus syst ematiquement le milieu du tableau, mais plut ot une estimation de lendroit o` u la clef devrait se trouver dans le tableau. Soit encore val, la clef ` a rechercher. Etant donn ee que le tableau est tri e par ordre croissant des clefs, la valeur de val donne une indication de lendroit o` u elle se trouve ; cette estimation est donn ee par : q=p+ val tab[p] (r p). tab[r 1] tab[p]

Il sav` ere que, dans le cas o` u les clefs ont une distribution al eatoire, cette am elioration r eduit drastiquement le co ut de la recherche, comme le montre le r esultat suivant (que nous admettrons) : Th eor` eme 4.4.1. La recherche par interpolation sur un ensemble de clefs distribu ees al eatoirement n ecessite moins de log(log(n)) + 1 comparaisons, sur un tableau de taille n. Attention, ce r esultat tr` es int eressant nest valable que si les clef sont r eparties uniform ement dans lensemble des clefs possibles. Une distribution biais ee peut grandement d egrader cette complexit e. 54 F. Levy-dit-Vehel

Ann ee 2011-2012
Mmoire
m tab

Chapitre 4. Recherche en table

key

info

key

info

key key info

info = pointeur vers NULL

Figure 4.1 Repr esentation en m emoire dune table de hachage. La huiti` eme case du tableau contient une collision : deux clef qui ont la m eme valeur hach ee.

4.5

Tables de hachage

Lorsque lensemble des clefs eectivement stock ees est beaucoup plus petit que lespace de toutes les clefs possibles, on peut sinspirer de ladressage direct - tr` es ecace lorsque est petit - pour impl ementer un dictionnaire. Soit n le nombre de clefs r eellement pr esentes. Lid ee est dutiliser une fonction surjective h de dans [0, m 1] (o` u on choisit en g en eral m n). Il sut alors davoir une table de taille m (comme pr ec edemment) et de placer une clef c dans la case k = h(c) de cette table. La fonction h est appel ee fonction de hachage, la table correspondante est appel ee table de hachage et lentier k = h(c) la valeur hach ee de la clef c. Les fonctions utilis ees habituellement sont tr` es rapides, et lon peut consid erer que leur temps dex ecution est en (1) quelle que soit la taille de lentr ee. Le principal probl` eme qui se pose est lexistence de plusieurs clefs poss edant la m eme valeur hach ee. Si lon consid` ere par exemple deux clefs c1 , c2 , telles que h(c1 ) = h(c2 ), le couple (c1 , c2 ) est appel e une collision. Etant donn e que || m, les collisions sont nombreuses. Une solution pour g erer les collisions consiste ` a utiliser un tableau tab de listes (cha n ees) de couples (clef,info) : le tableau est indic e par les entiers de 0 ` a m1 et, pour chaque indice i, tab[i] est (un pointeur sur) la liste cha n ee de tous les couples (clef,info) tels que h(clef) = i (i.e. la liste de toutes les collisions de valeur hach ee i). Une telle table de hachage est repr esent ee en Figure 4.1. Les op erations de recherche, insertion, suppression sont alors les m emes que lorsque lon impl emente une recherche s equentielle avec des listes, sauf quici les listes sont beaucoup plus petites. Lop eration dinsertion a une complexit e en (1) (insertion de (clef,info) au d ebut de la liste tab[i] avec i = h(clef)). Pour d eterminer la complexit e - en nombre d el ements ` a examiner (comparer) - des autres op erations (recherche et suppression), on doit dabord n , le facteur de remplissage conna tre la taille moyenne des listes. Pour cela, on d enit = m ` noter que de la table, i.e. le nombre moyen d el ements stock es dans une m eme liste. A peut prendre des valeurs arbitrairement grandes, i.e. il ny a pas de limite a priori sur le nombre d el ements susceptibles de se trouver en table. & M. Finiasz 55

4.5 Tables de hachage

ENSTA cours IN101

Dans le pire cas, les n clefs ont toute la m eme valeur hach ee, et on retombe sur la recherche s equentielle sur une liste de taille n, en (n) (suppression en (n) egalement). En revanche, si la fonction de hachage r epartit les n clefs uniform ement dans lintervalle [0, m 1] - on parle alors de hachage uniforme simple - chaque liste sera de taille en moyenne, et donc la recherche dun el ement (comme sa suppression) n ecessitera au plus +1 comparaisons. Une estimation pr ealable de la valeur de n permet alors de dimensionner la table de fa con ` a avoir une recherche en temps constant (en choisissant m = O(n)). La complexit e spatiale de cette m ethode est en (m + n), pour stocker la table et les n el ements de liste.

Choix de la fonction de hachage. Les performances moyennes de la recherche


par table de hachage d ependent de la mani` ere dont la fonction h r epartit (en moyenne) lensemble des clefs ` a stocker parmi les m premiers entiers. Nous avons vu que, pour une complexit e moyenne optimale, h doit r ealiser un hachage uniforme simple. En g en eral, on ne conna t pas la distribution initiale des clefs, et il est donc dicile de v erier si une fonction de hachage poss` ede cette propri et e. En revanche, plus les clefs sont r eparties uniform ement dans lespace des clefs possibles, moins il sera n ecessaire dutiliser une bonne fonction de hachage. Si les clefs sont r eparties de fa con parfaitement uniforme, on peut choisir m = 2 et simplement prendre comme valeur hach ee les premiers bits de la clef. Cela sera tr` es rapide, mais la moindre r egularit e dans les clefs peut etre catastrophique. Il est donc en g en eral recommand e davoir une fonction de hachage dont la sortie d epend de tous les bits de la clef. Si lon suppose que les clefs sont des entiers naturels (si ce nest pas le cas, on peut en g en eral trouver un codage qui permette dinterpr eter chaque clef comme un entier), la fonction modulo : h(c) = c mod m est rapide et poss` ede de bonnes propri et es de r epartition statistique d` es lors que m est un nombre premier assez eloign e dune puissance de 2 ; (si m = 2 , cela revient ` a prendre bits de la clef). Une autre fonction fr equemment utilis ee est la fonction h(c) = m(cx cx), o` u x est une constante r eelle appartenant ` a ]0, 1[. Le choix de x = 521 conduit ` a de bonnes performances en pratique (hachage de Fibonacci), mais le passage par des calculs ottants rend le hachage un peu plus lent. 56 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 4. Recherche en table

4.6

Tableau r ecapitulatif des complexit es


stockage (m) (n) (n) (n) (m + n) (n) recherche (1) (n) (log(n)) (log log(n)) n ( m ) (1) insertion (1) (1) (n) (log log(n)) (1) (1) modif. (1) (n) (n) (n) n ( m ) (1) suppr. (1) (n) (n) (n) n ( m ) (1)

m ethode adressage direct recherche s equentielle recherche dichotomique recherche par interpolation tables de hachage avec m n

Note : les complexit es ci-dessus sont celles dans le pire cas, sauf celles suivies de pour lesquelles il sagit du cas moyen. On constate quune table de hachage de taille bien adapt ee et munie dune bonne fonction de hachage peut donc etre tr` es ecace. Cependant cela demande davoir une bonne id ee de la quantit e de donn ees ` a g erer ` a lavance.

& M. Finiasz

57

Chapitre 5 Arbres
La notion darbre mod elise, au niveau des structures de donn ees, la notion de r ecursivit e pour les fonctions. Il est ` a noter que la repr esentation par arbre est tr` es courante dans la vie quotidienne : arbres g en ealogiques, organisation de comp etitions sportives, organigramme dune entreprise... Dans ce chapitre, apr` es avoir introduit la terminologie et les principales propri et es des arbres, nous illustrons leur utilisation ` a travers trois exemples fondamentaux en informatique : l evaluation dexpressions, la recherche dinformation, et limpl ementation des les de priorit e. Nous terminons par la notion darbre equilibr e, dont la structure permet de garantir une complexit e algorithmique optimale quelle que soit la distribution de lentr ee.

5.1
5.1.1

Pr eliminaires
D enitions et terminologie

Un arbre est une collection ( eventuellement vide) de nuds et dar etes assujettis ` a certaines conditions : un nud peut porter un nom et un certain nombre dinformations pertinentes (les donn ees contenues dans larbre) ; une ar ete est un lien entre deux nuds. Une branche (ou chemin) de larbre est une suite de nuds distincts, dans laquelle deux nuds successifs sont reli es par une ar ete. La longueur dune branche est le nombre de ses ar etes (ou encore le nombre de nuds moins un). Un nud est sp eciquement d esign e 1 et e fondamentale d enissant un arbre est comme etant la racine de larbre . La propri alors que tout nud est reli e ` a la racine par une et une seule branche 2 (i.e. il existe un unique chemin dun nud donn e` a la racine). Comme on peut le voir sur la Figure 5.1, chaque nud poss` ede un lien (descendant, selon la repr esentation graphique) vers chacun de ses ls (ou descendants), eventuellement un lien vers le vide, ou pas de lien si le nud na pas de ls ; inversement, tout nud - sauf
1. Si la racine nest pas sp eci ee, on parle plut ot darborescence. es par plus dune branche (ou pas de branche du tout) ` a la 2. Dans le cas ou certains nuds sont reli racine on parle alors de graphe.

59

5.1 Pr eliminaires
Racine

ENSTA cours IN101


= Nud = Arte Nuds internes

Figure 5.1 Repr esentation dun arbre. On place toujours la racine en haut. la racine - poss` ede un p` ere (ou anc etre) et un seul, qui est le nud imm ediatement audessus de lui dans la repr esentation graphique. Les nuds sans descendance sont appel es feuilles, les nuds avec descendance etant des nuds internes. Tout nud est la racine du sous-arbre constitu e par sa descendance et lui-m eme. Voici une liste du vocabulaire couramment utilis e pour les arbres : Une clef est une information contenue dans un nud. Un ensemble darbres est une for et. Un arbre est ordonn e si lordre des ls de chacun de ses nuds est sp eci e. Cest g en eralement le cas dans les arbres que lon va consid erer dans la suite. Un arbre est de plus num erot e si chacun de ses ls est num erot e par un entier strictement positif (deux nuds ne poss edant pas le m eme num ero). Les nuds dun arbre se r epartissent en niveaux. Le niveau dun nud est la longueur de la branche qui le relie ` a la racine. La hauteur dun arbre est le niveau maximum de larbre (i.e. plus grande distance dun nud ` a la racine). La longueur totale dun arbre est la somme des longueurs de tous les chemins menant des nuds ` a la racine. Le degr e dun nud est le nombre de ls quil poss` ede. Larit e dun arbre est le degr e maximal de ses nuds. Lorsque lon a un arbre dont les ls de chaque nud sont num erot es par des entiers tous compris dans lintervalle [1, . . . , k ], on parle darbre k -aire. Un arbre k -aire est donc un arbre num erot e, darit e inf erieure ou egale ` a k. Comme avec les sentinelles pour les listes, on peut d enir un type particulier de nud dit nud vide : cest un nud factice dans le sens o` u il ne contient pas dinformation ; il peut juste servir ` a remplir la descendance des nuds qui contiennent moins de k ls. Lexemple le plus important darbre m-aire est larbre binaire. Chaque nud poss` ede deux ls et on parle alors de ls gauche et de ls droit dun nud interne. On peut 60 F. Levy-dit-Vehel

Fe u

ill e

Ann ee 2011-2012

Chapitre 5. Arbres

egalement d enir la notion de ls gauche et ls droit dune feuille : il sut de repr esenter chacun deux par le nud vide. De cette mani` ere, tout nud non vide peut etre consid er e comme un nud interne. Un arbre binaire est complet si les nuds remplissent compl` etement tous les niveaux, sauf eventuellement le dernier, pour lequel les nuds apparaissent alors tous le plus ` a gauche possible (notez que larbre binaire de hauteur 0 est complet).

5.1.2

Premi` eres propri et es

La meilleure d enition des arbres est sans doute r ecursive : un arbre est soit larbre vide, soit un nud appel e racine reli e` a un ensemble ( eventuellement vide) darbres appel es ses ls. Cette d enition r ecursive se particularise trivialement au cas des arbres binaires comme suit : un arbre binaire est soit larbre vide, soit un nud racine reli e` a un arbre binaire gauche (appel e sous-arbre gauche) et un arbre binaire droit (appel e sous-arbre droit). Un arbre binaire est evidemment un arbre ; mais r eciproquement, tout arbre peut etre repr esent e par un arbre binaire (cf. plus loin la repr esentation des arbres). Cette vision r ecursive des arbres nous permet de d emontrer les propri et es suivantes. Propri et e 5.1.1. Il existe une branche unique reliant deux nuds quelconques dun arbre. Propri et e 5.1.2. Un arbre de N nuds contient N 1 ar etes. Preuve. Cest une cons equence directe du fait que tout nud, sauf la racine, poss` ede un p` ere et un seul, et que chaque ar ete relie un nud ` a son p` ere. Il y a donc autant dar etes que de nuds ayant un p` ere, soit N 1. Propri et e 5.1.3. Un arbre binaire complet poss edant N nuds internes contient N + 1 feuilles. Preuve. Pour un arbre binaire complet A, notons f (A), son nombre de feuilles et n(A), son nombre de nuds internes. On doit montrer que f (A) = n(A) + 1. Le r esultat est vrai pour larbre binaire de hauteur 0 (il est r eduit ` a une feuille). Consid erons un arbre binaire complet A = (r, Ag , Ad ), r d esignant la racine de larbre, et Ag et Ad ses sousarbres gauche et droit respectivement. Les feuilles de A etant celles de Ag et de Ad , on a f (A) = f (Ag ) + f (Ad ) ; les nuds internes de A sont ceux de Ag , ceux de Ad , et r, do` u n(A) = n(Ag ) + n(Ad ) + 1. Ag et Ad etant des arbres complets, la r ecurrence sapplique, et f (Ag ) = n(Ag ) + 1, f (Ad ) = n(Ad ) + 1. On obtient donc f (A) = f (Ag ) + f (Ad ) = n(Ag ) + 1 + n(Ad ) + 1 = n(A) + 1. Propri et e 5.1.4. La hauteur h dun arbre binaire contenant N nuds v erie h + 1 log2 (N + 1). Preuve. Un arbre binaire contient au plus 2 nuds au premier niveau, 22 nuds au deuxi` eme niveau,..., 2h nuds au niveau h. Le nombre maximal de nuds pour un arbre binaire de hauteur h est donc 1 + 2 + . . . + 2h = 2h+1 1, i.e. N + 1 2h+1 . & M. Finiasz 61

5.1 Pr eliminaires

ENSTA cours IN101

Pour un arbre binaire complet de hauteur h contenant N nuds, tous les niveaux (sauf le dernier sont compl` etement remplis. On a donc N 1 + 2 + . . . + 2h1 = 2h 1 , i.e. log2 (N + 1) h. La hauteur dun arbre binaire complet ` a N nuds est donc toujours de lordre de log2 (N ).

5.1.3

Repr esentation des arbres

Arbres binaires. Pour repr esenter un arbre binaire, on utilise une structure similaire ` a celle dune liste cha n ee, mais avec deux pointeurs au lieu dun : lun vers le ls gauche, lautre vers le ls droit. On d enit alors un nud par :
1 2 3 4 5 6 7

struct node { int key; type_info info; node* left; node* right; }; typedef node* binary_tree;

Arbres k-aires. On peut construire un arbre k -aire de fa con tr` es similaire (o` u k est un entier x e une fois pour toute dans le programme) :
1 2 3 4 5 6

struct node { int key; type_info info; node sons[k]; }; typedef node* k_ary_tree;

Arbres g en eraux. Dans le cas dun arbre g en eral il ny a pas de limite sur le nombre de ls dun nud. Il faut donc utiliser une structure dynamique pour stocker tous les ls dun m eme nud. Pour cela on utilise une liste cha n ee. Chaque nud contient donc un pointeur vers son ls le plus ` a gauche (le premier el ement de la liste), qui lui contient un pointeur vers sont fr` ere juste ` a sa droite (la queue de la liste). Chaque nud contient donc un pointeur vers un ls et un pointeur vers un fr` ere. On obtient une structure de la forme :
1 2 3 4 5 6 7

struct node { int key; type_info info; node* brother; node* son; }; typedef node* tree;

62

F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 5. Arbres
= Nud = Arte = Fils = Frre = NULL

Figure 5.2 Arbre g en eral : on acc` ede aux ls dun nud en suivant une liste cha n ee. Dans cette repr esentation, on voit que tout nud poss` ede deux liens : elle est donc identique ` a la repr esentation dun arbre binaire. Ainsi, on peut voir un arbre quelconque comme un arbre binaire (avec, pour tout nud, le lien gauche pointant sur son ls le plus ` a gauche, le lien droit vers son fr` ere imm ediatement ` a droite). Nous verrons cependant les modications ` a apporter lors du parcours des arbres g en eraux. Remonter dans un arbre Dans les applications o` u il est seulement n ecessaire de remonter dans larbre, et pas de descendre, la repr esentation par lien-p` ere dun arbre est susante. Cette repr esentation consiste ` a stocker, pour chaque nud, un lien vers son p` ere. On peut alors utiliser deux tableaux pour repr esenter un tel arbre : on prend soin d etiqueter les nuds de larbre au pr ealable (de 1 ` a k sil y a k nuds). Le premier tableau tab info de taille k contiendra linformation contenue dans chaque nud (tab info[i] = info du nud i), le deuxi` eme tableau tab father de taille k lui aussi contiendra les liens p` ere, de telle sorte que tab father[i] = clef du p` ere du nud i. Linformation relative au p` ere du nud i se trouve alors dans tab info[tab father[i]]. Lorsquon doit juste remonter dans larbre, une repr esentation par tableaux est donc susante. On pourrait aussi utiliser une liste de tous les nud dans laquelle chaque nud contiendrait en plus un lien vers son p` ere.

5.2

Utilisation des arbres

Il existe une multitude dalgorithmes utilisant des arbres. Souvent, lutilisation darbres ayant une structure bien pr ecise permet dobtenir des complexit es asymptotiques meilleures quavec des structures lin eaires (liste ou tableau par exemple). Nous voyons ici quelques exemples typiques dutilisation darbres. & M. Finiasz 63

5.2 Utilisation des arbres

ENSTA cours IN101

2 4 3

Figure 5.3 Un exemple darbre d evaluation, correspondant ` a lexpression (4+3) 2.

5.2.1

Evaluation dexpressions & parcours darbres

Nous nous int eressons ici ` a la repr esentation par arbre des expressions arithm etiques. Dans cette repr esentation, les nuds de larbre sont les op erateurs, et les feuilles les op erandes. Sil ny a que des op erateurs darit e 2 (comme + ou ) on obtient alors un arbre binaire. Si on consid` ere des op erateurs darit e sup erieur on obtient un arbre g en eral, mais on a vu que tout arbre est equivalent ` a un arbre binaire. On ne consid` ere donc ici que des arbres binaires et des op erateurs darit e deux, mais les algorithmes seront les m emes pour des op erateurs darit e quelconque. On ne sint eresse pas ici ` a la construction dun arbre d evaluation, mais uniquement ` a son evaluation. On part donc dune arbre comme celui repr esent e sur la Figure 5.3. L evaluation dune telle expression se fait en parcourant larbre, cest-` a-dire en visitant 3 chaque nud de mani` ere syst ematique. Il existe trois fa cons de parcourir un arbre. Chacune correspond ` a une ecriture di erente de lexpression. Nous d etaillons ci-apr` es ces trois parcours, qui sont essentiellement r ecursifs, et font tous parti des parcours dits en profondeur (depth-rst en anglais). Parcours pr exe. Ce parcours consiste ` a visiter la racine dabord, puis le sous-arbre gauche, et enn le sous-arbre droit. Il correspond ` a l ecriture dune expression dans laquelle les op erateurs sont plac es avant les op erandes, par exemple + 4 3 2 pour lexpression de la Figure 5.3. Cette notation est aussi appel ee notation polonaise. Lalgorithme suivant eectue un parcours pr exe de larbre A et pour chaque nud visit e ache son contenu (pour acher au nal + 4 3 2).
1 2 3

void preorder_traversal(tree A) { if (A != NULL) { printf("%d ", A->key);

3. Plus le parcours par niveau, appel e egalement parcours en largeur (i.e. parcours du haut vers le bas, en visitant tous les nuds dun m eme niveau de gauche ` a droite avant de passer au niveau suivant. Ce parcours nest pas r ecursif, et est utilis e par exemple dans la structure de tas (cf. section 5.2.3), mais ne permet pas d evaluer une expression.

64

F. Levy-dit-Vehel

Ann ee 2011-2012
4 5 6 7

Chapitre 5. Arbres

preorder_traversal(A->left); preorder_traversal(A->right); } }

Parcours inxe. Le parcours inxe correspond ` a l ecriture habituelle des expressions arithm etiques, par exemple (4 + 3) 2 (sous r eserve que lon rajoute les parenth` eses n ecessaires). Algorithmiquement, on visite dabord le sous-arbre gauche, puis la racine, et enn le sous-arbre droit :
1 2 3 4 5 6 7 8 9

void inorder_traversal(tree A) { if (A != NULL) { printf("("); inorder_traversal(A->left); printf("%d", A->key); inorder_traversal(A->right); printf(")"); } }

On est oblig e dajouter des parenth` eses pour lever les ambigu t es, ce qui rend lexpression un peu plus lourde : lalgorithme achera ((4) + (3)) (2). Parcours postxe. Ce parcours consiste ` a visiter le sous-arbre gauche dabord, puis le droit, et enn la racine. Il correspond ` a l ecriture dune expression dans laquelle les op erateurs sont plac es apr` es les op erandes, par exemple 4 3 + 2 (cest ce que lon appel la notation polonaise inverse, bien connue des utilisateurs de calculatrices HP).
1 2 3 4 5 6 7

void postorder_traversal(tree A) { if (A != NULL) { postorder_traversal(A->left); postorder_traversal(A->right); printf("%d ", A->key); } }

Complexit e. Pour les trois parcours ci-dessus, il est clair que lon visite une fois chaque nud, donc n appels r ecursifs pour un arbre ` a n nuds, et ce quel que soit le parcours consid er e. La complexit e de lop eration de parcours est donc en (n). Cas des arbres g en eraux. Les parcours pr exes et postxes sont aussi bien d enis pour les arbres quelconques. Dans ce cas, la loi de parcours pr exe devient : visiter la racine, puis chacun des sous-arbres ; la loi de parcours postxe devient : visiter chacun des & M. Finiasz 65

5.2 Utilisation des arbres

ENSTA cours IN101

sous-arbres, puis la racine. Le parcours inxe ne peut en revanche pas etre bien d eni si le nombre de ls de chaque nud est variable. Notons aussi que le parcours postxe dun arbre g en eralis e est equivalent au parcours inxe de larbre binaire equivalent (comme vu sur la Figure 5.2) et que le parcours pr exe dun arbre g en eralis e est equivalant au parcours pr exe de larbre binaire equivalent. Parcours en largeur. Le parcours en largeur (breadth-rst en anglais) est tr` es di erent des parcours en profondeur car il se fait par niveaux : on visite dabord tous les nuds du niveau 0 (la racine), puis ceux du niveau 1... Un tel parcours nest pas r ecursif mais doit etre fait sur larbre tout entier. La technique la plus simple utilise deux les A et B. On initialise A avec la racine et B ` a vide. Puis ` a chaque etape du parcours on pop un nud de la le A, on eectue lop eration que lon veut dessus (par exemple acher la clef), et on ajoute tous les ls de ce nud ` a B. On recommence ainsi jusqu` a avoir vider A, et quand A et vide on inverse les les A et B et on recommence. On sarr ete quand les deux les sont vides. ` chaque A etape la le A contient les nuds du niveau que lon est en train de parcourir, et la le B se remplit des nuds du niveau suivant. On eectue donc bien un parcours par niveaux de larbre. Ce type de parcours nest pas tr` es fr equent sur des arbres et sera plus souvent rencontr e dans le contexte plus g en eral des graphes.

5.2.2

Arbres Binaires de Recherche

Nous avons vu dans le chapitre 4 comment impl ementer un dictionnaire ` a laide dune table. Nous avons vu que la m ethode la plus ecace est lutilisation dune table de hachage. Cependant, pour quune telle table ore des performances optimales, il est n ecessaire de conna tre sa taille nale ` a lavance. Dans la plus part des contextes, cela nest pas possible. Dans ce cas, lutilisation dun arbre binaire de recherche est souvent le bon choix. Un arbre binaire de recherche (ABR) est un arbre binaire tel que le sous-arbre gauche de tout nud contienne des valeurs de clef strictement inf erieures ` a celle de ce nud, et son sous-arbre droit des valeurs sup erieures ou egales. Un tel arbre est repr esent e en Figure 5.4. Cette propri et e est susante pour pouvoir armer quun parcours inxe de larbre va visiter les nuds dans lordre croissant des clefs. Un ABR est donc un arbre ordonn e dans lequel on veut pouvoir facilement eectuer des op erations de recherche, dinsertion et de suppression, et cela sans perturber lordre. Recherche dans un ABR. Pour chercher une clef dans un ABR on utilise naturellement une m ethode analogue ` a la recherche dichotomique dans une table. Pour trouver un nud de clef v , on commence par comparer v ` a la clef de la racine, not ee ici r. Si v < r, alors on se dirige vers le sous-arbre gauche de la racine. Si v = r, on a termin e, et on retourne linformation associ ee ` a la racine. Si v > vr , on consid` ere le sous-arbre droit de la racine. On applique cette m ethode r ecursivement sur les sous-arbres. 66 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 5. Arbres

5 2 4 6 7 8 9

Figure 5.4 Un exemple darbre binaire de recherche (ABR). Il est ` a noter que la taille du sous-arbre courant diminue ` a chaque appel r ecursif. La proc edure sarr ete donc toujours : soit parce quun nud de clef v a et e trouv e, soit parce quil nexiste pas de nud ayant la clef v dans larbre, et le sous-arbre courant est alors vide. Cela peut se programmer de fa con r ecursive ou it erative :
1 2 3 4 5 6 7 8 9 10 11 12 13 14

type_info ABR_search(int v, tree A) { if (A == NULL) { printf("Recherche infructueuse.\n"); return NULL; } if (v < A->key) { return ABR_search(v, A->left); } else if (v == A->key) { printf("Noeud trouv e.\n"); return A->info; } else { return ABR_search(v,A->right); } }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

type_info ABR_search_iter(int v, tree A) { node* cur = A; while (cur != NULL) { if (v < cur->key) { cur = cur->left; } else if (v == cur->key) { printf("Noeud trouv e.\n"); return cur->info; } else { cur = cur->right; } } printf("Recherche infructueuse.\n"); return NULL; }

& M. Finiasz

67

5.2 Utilisation des arbres

ENSTA cours IN101

5 8 2 4 6 7 8 9 2 4

5 8 8 6 7 9 4 2

5 8 6 7 8 9

Figure 5.5 Insertion dun nud dont la clef est d ej` a pr esente dans un ABR. Trois emplacements sont possibles. Insertion dans un ABR. Linsertion dans un ABR nest pas une op eration compliqu ee : il sut de faire attention ` a bien respecter lordre au moment de linsertion. Pour cela, la m ethode la plus simple et de parcourir larbre de la m eme fa con que pendant la recherche dune clef et lorsque lon atteint une feuille, on peut ins erer le nouveau nud, soit ` a gauche, soit ` a droite de cette feuille, selon la valeur de la clef. On est alors certain de conserver lordre dans lABR. Cette technique fonctionne bien quand on veut ins erer un nud dont la clef nest pas pr esente dans lABR. Si la clef est d ej` a pr esente deux choix sont possibles (cf. Figure 5.5) : soit on d edouble le nud en ins erant le nouveau nud juste ` a c ot e de celui qui poss` ede la m eme clef (en dessus ou en dessous), soit on descend quand m eme jusqu` a une feuille dans le sous-arbre droit du nud. Encore une fois, linsertion peut se programmer soit de fa con r ecursive, soit de fa con it erative. Dans les deux cas, on choisit dins erer un nud d ej` a pr esent dans larbre comme sil n etait pas pr esent : on lins` ere comme ls dune feuille (troisi` eme possibilit e dans la Figure 5.5).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

void ABR_insert(int v, tree* A) { if ((*A) == NULL) { node* n = (node* ) malloc(sizeof(node )); n->key = v; n->left = NULL; n->right = NULL; (*A) = n; return; } if (v < (*A)->key) { ABR_insert(v, &((*A)->left)); } else { ABR_insert(v, &((*A)->right)); } }

Notons que an de pouvoir modier larbre A (il nest n ecessaire de le modier que quand larbre est initialement vide) il est n ecessaire de le passer en argument par pointeur. De ce fait, les appels r ecursifs utilisent une ecriture un peu lourde &((*A)->left) : on commence par prendre larbre *A. 68 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 5. Arbres

on regarde son ls gauche (*A)->left et on veut pouvoir modier ce ls gauche : on r ecup` ere donc le pointeur vers le ls gauche &((*A)->left) an de pouvoir faire pointer ce pointeur vers un nouveau nud.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

void ABR_insert_iter(int v, tree* A) { node** cur = A; while ((*cur) != NULL) { if (v < (*cur)->key) { cur = &((*cur)->left); } else { cur = &((*cur)->right); } } node* n = (node* ) malloc(sizeof(node )); n->key = v; n->left = NULL; n->right = NULL; (*cur) = n; return; }

Dans cette version it erative, on retrouve l ecriture un peu lourde de la version r ecursive avec cur = &((*cur)->left). On pourrait etre tent e de remplacer cette ligne par la ligne (*cur) = (*cur)->left, mais cela ne ferait pas ce quil faut ! On peut voir la di erence entre ces deux commandes sur la Figure 5.6. Suppression dans un ABR. La suppression de nud est lop eration la plus d elicate dans un arbre binaire de recherche. Nous avons vu quil est facile de trouver le nud n ` a supprimer. En revanche, une fois le nud n trouv e, si on le supprime cela va cr eer un trou dans larbre : il faut donc d eplacer un autre nud de larbre pour reboucher le trou. En pratique, on peut distinguer trois cas di erents : 1. Si n ne poss` ede aucun ls (n est une feuille), alors on peut le supprimer de larbre directement, i.e. on fait pointer son p` ere vers NULL au lieu de n. 2. Si n ne poss` ede quun seul ls : on supprime n de larbre et on fait pointer le p` ere de n vers le ls de n (ce qui revient ` a remplacer n par son ls). 3. Si n poss` ede deux ls, on commence par calculer le successeur de n, cest-` a-dire le nud suivant n lorsque lon enum` ere les nuds avec un parcours inxe de larbre : le successeur est le nud ayant la plus petite clef plus grande que la clef de n. Si on appelle s ce nud, il est facile de voir que s sera le nud le plus ` a gauche du sous-arbre droit de n. Ce nud etant tout ` a gauche du sous-arbre, il a forc ement son ls gauche vide. On peut alors supprimer le nud n en le rempla cant par le nud s (on remplace la clef et linformation contenue dans le nud), et en supprimant le nud s de son emplacement dorigine avec la m ethode vue au cas 2 (ou au cas 1 si s poss` ede deux ls vides). Une telle suppression est repr esent ee sur la Figure 5.7. & M. Finiasz 69

5.2 Utilisation des arbres

ENSTA cours IN101

tat initial A A key *A key cur cur cur = &((*cur)->left); A A (*cur) = (*cur)->left; *A key key node** cur = A;

key *A key cur (*cur) = &n; n.key cur *A

key key

(*cur) = &n; n.key

key *A key cur cur *A

key key

Insertion russie

Insertion rate

Figure 5.6 Ce quil se passe en m emoire lors de linsertion dun nud n dans un ABR. On part dun arbre ayant juste une racine et un ls droit et on doit ins erer le nouveau nud comme ls gauche de la racine. On initialise cur, puis on descend dans larbre dune fa con ou dune autre : ` a gauche la bonne m ethode qui marche, ` a droite la mauvaise. Dans les deux cas, on trouve un pointeur vers NULL et on ins` ere le nouveau nud n. Avec la m ethode de droite larbre original est modi e et deux nuds sont perdus.

70

F. Levy-dit-Vehel

Ann ee 2011-2012
Arbre initial
nud supprimer
5 2 4 6 7 5 6 8 4 6 7 9 2 4

Chapitre 5. Arbres
Mouvements de nuds Arbre final

sous-a rb re 8

dr o
it
9

8 7 9

successeur

Figure 5.7 Suppression dun nud dun ABR. En C, la version it erative de la suppression (la version r ecursive est plus compliqu ee et napporte pas grand chose) peut se programmer ainsi :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

void ABR_del(int v, tree* A) { node** cur = A; node** s; node* tmp; while ((*cur) != NULL) { if (v < (*cur)->key) { cur = &((*cur)->left); } else if (v > (*n)->key){ cur = &((*cur)->right); } else { /* on a trouv e le noeud ` a supprimer, on cherche son successeur */ s = &((*cur)->right); while ((*s)->left != NULL) { s = &((*s)->left); } /* on a trouv e le successeur */ tmp = (*s); /* on garde un lien vers le successeur */ (*s) = tmp->right; tmp->left = (*cur)->left; tmp->right = (*cur)->right; (*cur) = tmp; return; } } printf("Noeud introuvable. Suppression impossible."); return; }

Complexit e des di erentes op erations. Soit n le nombre de nuds de larbre, appel e aussi taille de larbre, et h la hauteur de larbre. Les algorithmes ci-dessus permettent de se convaincre ais ement du fait que la complexit e des op erations de recherche, dinsertion & M. Finiasz 71

5.2 Utilisation des arbres

ENSTA cours IN101

et de suppression, sont en (h) : pour les version r ecursives, chaque appel fait descendre dun niveau dans larbre et pour les version it eratives chaque etape de la boucle while fait aussi descendre dun niveau. Pour calculer la complexit e, il faut donc conna tre la valeur de h. Il existe des arbres de taille n et de hauteur n 1 : ce sont les arbres dont tous les nuds ont au plus un ls non vide. Ce type darbre est alors equivalent ` a une liste, et les op erations ci-dessus ont donc une complexit e analogue ` a celle de la recherche s equentielle (i.e. en (h) = (n)). Noter que lon obtient ce genre de conguration lorsque lon doit ins erer n clefs dans lordre (croissant ou d ecroissant), ou bien par exemple les lettres A,Z,B,Y,C,X,... dans cet ordre. En revanche, dans le cas le plus favorable, i.e. celui dun arbre complet o` u tous les 4 niveaux sont remplis, larbre a une hauteur environ egale ` a log2 (n). Dans ce cas, les trois op erations ci-dessus ont alors une complexit e en (h) = (log2 (n)). La question est maintenant de savoir ce quil se passe en moyenne. Si les clefs sont ins er ees de mani` ere al eatoire, on a le r esultat suivant : Proposition 5.2.1. La hauteur moyenne dun arbre binaire de recherche construit al eatoirement ` a partir de n clefs distinctes est en (log2 (n)). Ainsi, on a un comportement en moyenne qui est logarithmique en le nombre de nuds de larbre (et donc proche du cas le meilleur), ce qui rend les ABR bien adapt es pour limpl ementation de dictionnaires. En outre, an de ne pas tomber dans le pire cas, on dispose de m ethodes de r e equilibrage darbres pour rester le plus pr` es possible de la conguration darbre complet (cf. section 5.3). Tri par Arbre Binaire de Recherche Comme nous lavons vu, le parcours inxe dun ABR ache les clefs de larbre dans lordre croissant. Ainsi, une m ethode de tri se d eduit naturellement de cette structure de donn ees : Soient n entiers ` a trier Construire lABR dont les nuds ont pour clefs ces entiers Imprimer un parcours inxe de cet ABR. La complexit e de cette m ethode de tri est en (n log2 (n)) ((nlog2 (n)) pour linsertion de n clefs, chaque insertion se faisant en (log2 (n)), plus (n) pour le parcours inxe) en moyenne et dans le cas le plus favorable. Elle est en revanche en (n2 ) dans le pire cas (entiers dans lordre croissant ou d ecroissant).

5.2.3

Tas pour limpl ementation de les de priorit e

Une le de priorit e est un ensemble d el ements, chacun muni dun rang ou priorit e (attribu ee avant quil ne rentre dans la le). Un exemple fondamental de le est lordon4. On trouve dans ce cas 1 nud de profondeur 0, 2 nuds de profondeur 1, ...., 2h nuds de profondeur h i h et le nombre total de nuds est alors i=0 2 = 2h+1 1.

72

F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 5. Arbres

55 0 24 1 14 3 3 7 12 8 6 9 11 4 1 10 7 11 55 24 18 14 11 16 9 3 12 6 1 7 16 5 18 2 9 6

Figure 5.8 Repr esentation dun tas ` a laide dun tableau. nanceur dun syst` eme dexploitation qui doit ex ecuter en priorit e les instructions venant dun processus de priorit e elev ee. Les el ements sortiront de cette le pr ecis ement selon leur rang : l el ement de rang le plus elev e sortira en premier. Les primitives ` a impl ementer pour mettre en oeuvre et manipuler des les de priorit e sont : une une une une fonction fonction fonction fonction dinitialisation dune le (cr eation dune le vide), dinsertion dun el ement dans la le (avec son rang), qui retourne l el ement de plus haut rang, de suppression de l el ement de plus haut rang.

Le codage dune le de priorit e peut etre r ealis e au moyen dune liste : alors linsertion se fait en temps constant, mais lop eration qui retourne ou supprime l el ement de plus haut rang n ecessite une recherche pr ealable de celui-ci, et se fait donc en (n), o` u n est le nombre d el ements de la le. Autrement, on peut aussi choisir davoir une op eration dinsertion plus lente qui ins` ere directement l el ement ` a la bonne position en fonction de sa priorit e (cette op eration co ute alors (n)) et dans ce cas les op erations de recherche et suppression peuvent se faire en (1). Une m ethode beaucoup plus ecace pour repr esenter une le de priorit e est obtenue au moyen dun arbre binaire de structure particuli` ere, appel e tas (heap en anglais) ou (arbre) maximier. Un tas est un arbre binaire complet, qui poss` ede en plus la propri et e que la clef de tout nud est sup erieure ou egale ` a celle de ses descendants. Un exemple de tas est donn e` a la Figure 5.8. La fa con la plus naturelle dimpl ementer un tas semble etre dutiliser une structure darbre similaire ` a celle utilis ees pour les ABR. Pourtant limpl ementation la plus ecace utilise en fait un tableau. On commence par num eroter les nuds de 0 (racine) ` a n-1 (nud le plus ` a droite du dernier niveau) par un parcours par niveau. On place chaque nud dans la case du tableau tab correspondant ` a son num ero (cf. Figure 5.8) : la racine dans tab[0], le ls gauche de la racine dans tab[1]... Avec cette num erotation le p` ere du i1 nud i est le nud 2 , et les ls du nud i sont les nuds 2i + 1 (ls gauche) et 2i + 2 (ls droit). La propri et e de maximier (ordre sur les clefs) se traduit sur le tableau par : & M. Finiasz 73

5.2 Utilisation des arbres

ENSTA cours IN101

1 1 i n 1, tab[i] tab[ i ]. 2 D etaillons ` a pr esent les op erations de base (celles list ees ci-dessus) sur les tas repr esent es par des tableaux.

Initialisation. Pour coder un tas avec un tableau il faut au moins trois variables : le tableau tab o` u ranger les nuds, le nombre maximum max de nuds que lon peut mettre dans ce tas, et le nombre de nuds d ej` a pr esents n. Le plus simple est de coder cela au moyen de la structure suivante :
1 2 3 4 5

struct heap { int max; int n; int* tab; };

Pour cr eer un tas (vide) de taille maximale m donn ee en param` etre, on cr ee un objet de type tas pour lequel tab est un tableau fra chement allou e:
1 2 3 4 5 6 7

heap* init_heap(int m) { heap* h = (heap* ) malloc(sizeof(heap )); h->max = m; h->n = 0; h->tab = (int* ) malloc(m*sizeof(int )); return h; }

Nous voyons alors appara tre linconv enient de lutilisation dun tableau : le nombre maximum d el ements ` a mettre dans le tas doit etre d eni ` a lavance, d` es linitialisation. Cette limitation peut toutefois etre contourn ee en utilisant une r eallocation dynamique du tableau : on peut dans ce cas ajouter un niveau au tableau ` a chaque fois que le niveau pr ec edent est plein. On r ealloue alors un nouveau tableau (qui sera de taille double) et on peut recopier le tableau pr ec edent au d ebut de celui l` a. Comme vu dans la section sur les tableau, la taille etant doubl ee ` a chaque r eallocation, cela naecte pas la complexit e moyenne dune insertion d el ement. Insertion. Lop eration dinsertion comme celle de suppression comporte deux phases : la premi` ere vise ` a ins erer (resp. supprimer) la clef consid er ee (resp. la clef de la racine), tout en pr eservant la propri et e de compl etude de larbre, la deuxi` eme est destin ee ` a r etablir la propri et e de maximier, i.e. lordre sur les clefs des nuds de larbre. Pour ins erer un nud de clef v , on cr ee un nouveau nud dans le niveau de profondeur le ere une ascension dans plus elev e de larbre, et le plus ` a gauche possible 5 . Ensuite, on op` larbre (plus pr ecis ement dans la branche reliant le nouveau nud ` a la racine) de fa con ` a
5. Si larbre est compl` etement rempli sur le dernier niveau, on cr ee un niveau suppl ementaire.

74

F. Levy-dit-Vehel

Ann ee 2011-2012
Insertion du nud 43
55 24 14 3 12 6 11 1 7 16 43 18 9 3 14 12 6 24 11 1 7 43 16 55 18 9 3 14 12 6 24

Chapitre 5. Arbres

55 43 11 1 7 18 16 9

Suppression dun nud


55 24 14 3 12 6 11 1 7 18 16 43 9 3 14 12 6 24 11 1 7 18 16 43 9

43 24 14 3 12 6 11 1 7 18 16 9 3 14 12 6 24 11 1

43 18 16 7 9

Figure 5.9 Insertion et suppression dun nud dun tas. placer ce nouveau nud au bon endroit : pour cela, on compare v avec la clef de son nud p` ere. Si v est sup erieure ` a celle-ci, on permute ces deux clefs. On recommence jusqu` a ce que v soit inf erieure ou egale ` a la clef de son nud p` ere ou que v soit ` a la racine de larbre (en cas 0 de tab). Les etapes dune insertion sont repr esent ees sur la Figure 5.9.
void heap_insert(int v, heap* h) { if (h->n == h->max) { printf("Tas plein."); return; } int tmp; int i = h->n; h->tab[i] = v; /* on ins` ere v ` a la fin de tab */ while ((i>0) && (h->tab[i] > h->tab[(i-1)/2])) { /* tant que lordre nest pas bon et que la racine nest pas atteinte */ tmp = h->tab[i]; h->tab[i] = h->tab[(i-1)/2]; h->tab[(i-1)/2] = tmp; i=(i-1)/2; } h->n++; }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

& M. Finiasz

75

5.2 Utilisation des arbres

ENSTA cours IN101

Suppression. Pour supprimer la clef contenue dans la racine, on remplace celle-ci par la clef (not ee v ) du nud situ e au niveau de profondeur le plus elev e et le plus ` a droite possible (le dernier nud du tableau), nud que lon supprime alors ais ement etant donn e quil na pas de ls. Ensuite, pour r etablir lordre sur les clefs de larbre, i.e. mettre v ` a la bonne place, on eectue une descente dans larbre : on compare v aux clefs des ls gauche et droit de la racine et si v est inf erieure ou egale ` a au moins une de ces deux clefs, on permute v avec la plus grande des deux clefs. On recommence cette op eration jusqu` a ce que v soit sup erieure ou egale ` a la clef de chacun de ses nuds ls, ou jusqu` a arriver ` a une feuille. Les etapes dune suppression sont repr esent ees sur la Figure 5.9. Voici comment peut se programmer lop eration de suppression qui renvoie au passage l el ement le plus grand du tas que lon vient de supprimer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

int heap_del(heap* h) { if (h->n == 0) { printf("Erreur : le tas est vide."); return -1; } /* on sauvegarde le max pour le retourner ` a la fin */ int max = h->tab[0]; int i = 0; bool cont = true; h->n--; /* on met la clef du dernier noeud ` a la racine */ h->tab[0] = h->tab[h->n]; while (cont) { if (2*i+2 > h->n) { /* si le noeud i na pas de fils */ cont = false; } else if (2*i+2 == h->n) { /* si le noeud i a un seul fils (gauche) on inverse les deux si n ecessaire */ if (h->tab[i] < h->tab[2*i+1]) { swap(&(h->tab[i]),&(h->tab[2*i+1])); } cont = false; } else { /* si le noeud i a deux fils on regarde si lun des deux est plus grand */ if ((h->tab[i] < h->tab[2*i+1]) || (h->tab[i] < h->tab[2*i+2])) { /* on cherche le fils le plus grand */ int greatest; if (h->tab[2*i+1] > h->tab[2*i+2]) { greatest = 2*i+1; } else { greatest = 2*i+2; } /* on inverse et on continue la boucle */ swap(&(h->tab[i]),&(h->tab[greatest]));

76

F. Levy-dit-Vehel

Ann ee 2011-2012
38 39 40 41 42 43 44 45 46 47 48 49 50 51

Chapitre 5. Arbres

i = greatest; } else { cont = false; } } } return max; } void swap(int* a, int* b) { int tmp = (*a); (*a)=(*b); (*b)=tmp; }

La fonction swap permet d echanger deux cases du tableau (ou deux entiers situ es nimporte o` u dans la m emoire). On est oblig e de passer en arguments des pointeurs vers les entiers ` a echanger car si lon passe directement les entiers, ils seront recopi es dans des variables locales de la fonction, et ce sont ces variable locale qui seront echang ees, les entiers dorigine restant bien tranquillement ` a leur place. Complexit e des op erations sur les tas. Comme souvent dans les arbres, la complexit e des di erentes op eration d epend essentiellement de la hauteur totale de larbre. Ici, avec les tas, nous avons de plus des arbres complets et donc la hauteur dun tas et toujours logarithmique en son nombre de nuds : h = (log(n)). Les boucles while des op erations dinsertion ou de suppression sont ex ecut ees au maximum un nombre de fois egal ` a la hauteur du tas. Chacune de ces ex ecution contenant un nombre constant dop eration la complexit e total des op erations dinsertion et de suppression est donc (h) = (log(n)).

5.2.4

Tri par tas

La repr esentation pr ec edente dun ensemble dentiers (clefs) donne lieu de mani` ere naturelle ` a un algorithme de tri appel e tri pas tas (heapsort en anglais). Soient n entiers ` a trier, on les ins` ere dans un tas, puis on r ep` ete n fois lop eration de suppression. La complexit e de ce tri est n (log(n)) pour les n insertions de clefs plus n (log(n)) pour les n suppressions, soit (n log(n)) au total. Il est ` a noter que, de par la d enition des op erations dinsertion/suppression, cette complexit e ne d epend pas de la distribution initiale des entiers ` a trier, i.e. elle est la m eme en moyenne ou dans le pire cas. Voici une impl ementation dun tel tri : on part dun tableau tab que lon re-remplit avec les entiers tri es (par ordre croissant).
1 2 3

void heapsort(int* tab, int n) { int i; heap* h = init_heap(n);

& M. Finiasz

77

5.3 Arbres equilibr es


4 5 6 7 8 9 10 11 12

ENSTA cours IN101

for (i=0; i<n; i++) { heap_insert(tab[i],h); } for (i=n-1; i>=0; i--) { tab[i] = heap_del(h); } free(h->tab); free(h); }

5.3

Arbres equilibr es

Les algorithmes sur les arbres binaires de recherche donnent de bons r esultats dans le cas moyen, mais ils ont de mauvaises performances dans le pire cas. Par exemple, lorsque les donn ees sont d ej` a tri ees, ou en ordre inverse, ou contiennent alternativement des grandes et des petites clefs, le tri par ABR a un tr` es mauvais comportement. Avec le tri rapide, le seul rem` ede envisageable etait le choix al eatoire dun el ement pivot, dont on esp erait quil eliminerait le pire cas. Heureusement pour la recherche par ABR, il est possible de faire beaucoup mieux, car il existe des techniques g en erales d equilibrage darbres permettant de garantir que le pire cas narrive jamais. Ces op erations de transformation darbres sont plus ou moins simples, mais sont peu co uteuses en temps. Elles permettent de rendre larbre le plus r egulier possible, dans un sens qui est mesur e par un param` etre d ependant en g en eral de sa hauteur. Une famille darbres satisfaisant une telle condition de r egularit e est appel ee une famille darbres equilibr es. Il existe plusieurs familles darbres equilibr es : les arbres AVL, les arbres rouge-noir, les arbres a-b... Nous verrons ici essentiellement les arbres AVL.

5.3.1

R e equilibrage darbres

Nous pr esentons ici une op eration d equilibrage appel ee rotation, qui sapplique ` a tous les arbres binaires. Soit donc A, un arbre binaire non vide. On ecrira A = (x, Z, T ) pour exprimer que x est la racine de A, et Z et T ses sous-arbres gauche et droit respectivement. Soit A = (x, X, B ) avec B non vide, et posons B = (y, Y, Z ). Lop eration de rotation gauche de A est lop eration : A = (x, X, (y, Y, Z )) G (A) = (y, (x, X, Y ), Z ). Autrement dit, on remplace le nud racine x par le nud y , le ls gauche du nud y pointe sur le nud x, son ls droit (inchang e) pointe sur le sous-arbre Z , et le ls droit du nud x est mis ` a pointer sur le sous-arbre Y . Cette op eration est repr esent ee sur la Figure 5.10. La rotation droite est lop eration inverse : A = (y, (x, X, Y ), Z ) D(A) = (x, X, (y, Y, Z )). 78 F. Levy-dit-Vehel

Ann ee 2011-2012
Rotation gauche

Chapitre 5. Arbres
Rotation droite

x y x

Z X Z Y X
y x x y

Z Y X X Z Y

Figure 5.10 Op eration de rotation pour le r e equilibrage darbre binaire. On remplace le nud racine y par le nud x, le ls gauche du nud x (inchang e) pointe sur le sous-arbre X , son ls droit pointe sur le nud y , et le ls gauche du nud y est mis ` a pointer sur le sous-arbre Y . Utilisation des rotations. Le but des rotations est de pouvoir r e equilibrer un ABR. On op` ere donc une rotation gauche lorsque larbre est d es equilibr e` a droite , i.e. son sous-arbre droit est plus haut que son sous-arbre gauche. On op` ere une rotation droite dans le cas contraire. Il est ais e de v erier que les rotations pr eservent la condition sur lordre des clefs dun ABR. On a alors : Proposition 5.3.1. Si A est un ABR, et si la rotation gauche (resp. droite) est d enie sur A, alors G (A) (resp. D(A)) est encore un ABR. On peut egalement d enir des doubles rotations (illustr ees sur la Figure 5.11) comme suit : la rotation gauche-droite associe ` a larbre A = (x, Ag , Ad ), larbre D(x, G (Ag ), Ad ). De mani` ere analogue, la rotation droite-gauche associe ` a larbre A = (x, Ag , Ad ), larbre G (x, Ag , D(Ad )). Ces op erations pr eservent egalement lordre des ABR. Une propri et e importante des rotations et doubles rotations est quelles simpl ementent en temps constant : en eet, lorsquun ABR est repr esent e par une structure de donn ees du type binary tree d enie plus haut dans ce chapitre, une rotation consiste essentiellement en la mise ` a jour dun nombre x e (i.e. ind ependant du nombre de nuds de larbre ou de sa hauteur) de pointeurs.

5.3.2

Arbres AVL

Les arbres AVL ont et e introduits par Adelson, Velskii et Landis en 1962. Ils constituent une famille dABR equilibr es en hauteur. & M. Finiasz 79

5.3 Arbres equilibr es


Rotation gauche-droite
x y z

ENSTA cours IN101

T X Z Y
y z x

Z Y X

T
z y x

Z Y X T

Figure 5.11 Exemple de double rotation sur un ABR. De mani` ere informelle, un ABR est un arbre AVL si, pour tout nud de larbre, les hauteurs de ses sous-arbres gauche et droit di` erent dau plus 1. Plus pr ecis ement, posons (A) = 0 si A est larbre vide, et dans le cas g en eral : (A) = h(Ag ) h(Ad ) o` u Ag et Ad sont les sous-arbres gauche et droit de A, et h(Ag ) la hauteur de larbre Ag (par convention, la hauteur de larbre vide est 1 et la hauteur dune feuille est 0 et tous deux sont des AVL). (A) est appel e l equilibre de A. Pour plus de simplicit e, la notation (x) o` u x est un nud d esignera l equilibre de larbre dont x est la racine. Une d enition plus rigoureuse dun arbre AVL est alors : D enition 5.1. Un ABR est un arbre AVL si, pour tout nud x de larbre, (x) {1, 0, 1}. La propri et e fondamentale des AVL est que lon peut borner leur hauteur en fonction du log du nombre de nuds dans larbre. Plus pr ecis ement : Proposition 5.3.2. Soit A un arbre AVL poss edant n sommets et de hauteur h. Alors log2 (1 + n) 1 + h c log2 (2 + n), 5)/2) 1, 44.

avec c = 1/ log2 ((1 +

Preuve. Pour une hauteur h donn ee, larbre poss edant le plus de nuds est larbre complet, h+1 h+1 ` a2 1 nuds. Donc n 2 1 et log2 (1 + n) 1 + h. 80 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 5. Arbres

Soit maintenant N (h), le nombre minimum de nuds dun arbre AVL de hauteur h. On a N (1) = 0, N (0) = 1, N (1) = 2, et, pour h 2, N (h) = 1 + N (h 1) + N (h 2). En eet, si larbre est de hauteur h, lun (au moins) de ses sous arbre est de hauteur h 1. Plus le deuxi` eme sous arbre est petit, moins il aura de nud, mais la propri et e dAVL fait quil ne peut pas etre de hauteur plus petite que h 2. Donc larbre AVL de hauteur h contenant le moins de nuds est constitu e dun nud racine et de deux sous-arbres AVL de nombre de nuds minimum, lun de hauteur h 1, lautre de hauteur h 2. Posons alors F (h) = N (h) + 1. On a F (0) = 2, F (1) = 3, et, pour h 2, F (h) = F (h 1) + F (h 2), donc F (h) = Fh+3 , o` u Fk est le k -i` eme nombre de Fibonacci. Pour tout arbre AVL ` an sommets et de hauteur h, on a par cons equent : 1 1 n + 1 F (h) = (h+3 (1 )h+3 ) > h+3 1, 5 5 avec = (1 + 5)/2. Do` u h+3< log2 (n + 2) 1 + log ( 5) log2 (n + 2) + 2. log2 () log2 ()

Par exemple, un arbre AVL ` a 100 000 nuds a une hauteur comprise entre 17 et 25. Le nombre de comparaisons n ecessaires ` a une recherche, insertion ou suppression dans un tel arbre sera alors de cet ordre. Arbres de Fibonacci La borne sup erieure de la proposition pr ec edente est essentiellement atteinte pour les arbres de Fibonacci d enis comme suit : (0) est larbre vide, (1) est r eduit ` a une feuille, et, pour k 2, larbre k+2 a un sous-arbre gauche egal ` a k+1 , et un sous-arbre doit egal ` a k . La hauteur de k est k 1, et k poss` ede Fk+2 nuds. Limpl ementation des AVL est analogue ` a celle des ABR, ` a ceci pr` es que lon rajoute ` a la structure binary tree un champ qui contient la hauteur de larbre dont la racine est le nud courant. Cette modication rend cependant le op eration dinsertion et de suppression un peu plus compliqu ees : il est n ecessaire de remettre ` a jour ces hauteurs chaque fois que cela est n ecessaire. Insertion dans un AVL. Lop eration dinsertion dans un AVL se fait de la m eme mani` ere que dans un ABR : on descend dans larbre ` a partir de la racine pour rechercher la feuille o` u mettre le nouveau nud. Ensuite il sut de remonter dans larbre pour remettre & M. Finiasz 81

5.3 Arbres equilibr es


Cas simple : une rotation droite suffit
x

ENSTA cours IN101

h+2

h+1

Gd Gg

Gg

Gd

Cas compliqu : une rotation gauche interne G permet de se rammener au cas simple
x x

Gg

Gd

h+1

h+2

h+2

Gg

Figure 5.12 R e equilibrage apr` es insertion dans un AVL. ` a jour les hauteurs de tous les sous-arbres (dans une version r ecursive de linsertion, cela se fait ais ement en abandonnant la r ecursion terminale et en remettant ` a jour les hauteur juste apr` es lappel r ecursif pour linsertion). Toutefois, cette op eration peut d es equilibrer larbre, i.e. larbre r esultant nest plus AVL. Pour r etablir la propri et e sur les hauteurs, il sut de r e equilibrer larbre par des rotations (ou doubles rotations) le long du chemin qui m` ene de la racine ` a la feuille o` u sest fait linsertion. Dans la pratique, cela se fait juste apr` es avoir remis ` a jour les hauteurs des sous-arbres : si on constate un d es equilibre entre les deux ls de 2 ou 2 on eectue une rotation, ou une double rotation. Supposons que le nud x ait deux ls G et D et quapr` es insertion (x) = h(G) h(D) = 2. Le sous-arbre G est donc plus haut que D. Pour savoir si lon doit faire un double rotation ou une rotation simple en x il faut regarder les hauteurs des deux sous-arbres de G (not es Gg et Gd ). Si (G) > 1 alors on fait une rotation droite sur x qui sut ` a r e equilibrer larbre. Si (G) = h(Gg ) h(Gd ) = 1 alors il faut faire une double rotation : on commence par une rotation gauche sur G an que (G) > 1, puis on est ramen e au cas pr ec edent et une rotation droite sur x sut. Cette op eration est illustr ee sur la Figure 5.12. Dans le cas o` u le sous-arbre D est le plus haut, il sut de proc eder de fa con sym etrique. Il est important de noter quapr` es une telle rotation (ou double rotation), larbre qui etait d es equilibr e apr` es linsertion retrouve sa hauteur initiale. Donc la propri et e dAVL est n ecessairement pr eserver pour les nuds qui se trouvent plus haut dans larbre. Proposition 5.3.3. Apr` es une insertion dans un arbre AVL, il sut dune seule rotation ou double rotation pour r e equilibrer larbre. Lop eration dinsertion/r e equilibrage dans un AVL ` a n nuds se r ealise donc en O(log2 (n)). 82 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 5. Arbres

Suppression dans un AVL. Lop eration de suppression dans un AVL se fait de la m eme mani` ere que dans un ABR : on descend dans larbre ` a partir de la racine pour rechercher le nud contenant la clef ` a supprimer. Sil sagit dune feuille, on supprime celle-ci ; sinon, on remplace le nud par son nud successeur, et on supprime le successeur. Comme dans le cas de linsertion, cette op eration peut d es equilibrer larbre. Pour le r e equilibrer, on op` ere egalement des rotations ou doubles rotations le long du chemin qui m` ene de la racine ` a la feuille o` u sest fait la suppression ; mais ici, le r e equilibrage peut n ecessiter plusieurs rotations ou doubles rotations mais le nombre de rotations (simples ou doubles) n ecessaires est au plus egal ` a la hauteur de larbre (on en fait au maximum une par niveau), et donc : Proposition 5.3.4. Lop eration de suppression/r e equilibrage dans un AVL ` a n nuds se r ealise en O(log2 (n)). Conclusion sur les AVL. Les AVL sont donc des arbres binaires de recherches v eriant juste une propri et e d equilibrage suppl ementaire. Le maintien de cette propri et e naugmente pas le co ut des op erations de recherche, dinsertion ou de suppression dans larbre, mais permet en revanche de garantir que la hauteur de larbre AVL reste toujours logarithmique en son nombre de nuds. Cette structure est donc un peu plus complexe ` a impl ementer quun ABR classique mais nore que des avantages. Dautres structures darbres equilibr es permettent dobtenir des r esultats similaires, mais ` a chaque fois, le maintien de la propri et e d equilibrage rend les op eration dinsertion/suppression un peu plus lourdes ` a impl ementer.

& M. Finiasz

83

Chapitre 6 Graphes
Nous nous int eressons ici essentiellement aux graphes orient es. Apr` es un rappel de la terminologie de base associ ee aux graphes et des principales repr esentations de ceux-ci, nous pr esentons un algorithme testant lexistence de chemins, qui nous conduit ` a la notion de fermeture transitive. Nous nous int eressons ensuite aux parcours de graphes : le parcours en largeur est de nature it erative, et nous permettra dintroduire la notion darborescence des plus courts chemins ; le parcours en profondeur - essentiellement r ecursif - admet plusieurs applications, parmi lesquelles le tri topologique. Nous terminons par le calcul de chemins optimaux dans un graphe (algorithme de Aho-Hopcroft-Ullman).

6.1

D enitions et terminologie

D enition 6.1. Un graphe orient e G = (S, A) est la donn ee dun ensemble ni S de sommets, et dun sous-ensemble A du produit S S , appel e ensemble des arcs de G. Un arc a = (x, y ) a pour origine le sommet x, et pour extr emit e le sommet y . On note org (a) = x, ext(a) = y . Le sommet y est un successeur de x, x etant un pr ed ecesseur de
5 1 4 8 7 0 3

6 2

Figure 6.1 Exemple de graphe orient e. 85

6.1 D enitions et terminologie


1 4 8 7

ENSTA cours IN101

T
0

B(T)
2 6

Arc du graphe Arc du cocycle de T

Figure 6.2 Bordure et cocycle dun sous-ensemble T dun graphe. y . Les sommets x et y sont dits adjacents. Si x = y , larc (x, x) est appel e boucle. Un arc repr esente une liaison orient ee entre son origine et son extr emit e. Lorsque lorientation des liaisons nest pas une information pertinente, la notion de graphe non orient e permet de sen aranchir. Un graphe non orient e G = (S, A) est la donn ee dun ensemble ni S de sommets, et dune famille de paires de S dont les el ements sont appel es ar etes. Etant donn e un graphe orient e G, sa version non orient ee est obtenue en supprimant les boucles, et en rempla cant chaque arc restant (x, y ) par la paire {x, y }. Soit G = (S, A), un graphe orient e, et T , un sous-ensemble de S . Lensemble (T ) = {a = (x, y ) A, (x T, y S \ T ) ou (y T, x S \ T )}, est appel e le cocycle associ e` a T . Lensemble B(T ) = {x S \ T, y T, (x, y ) ou (y, x) A}, est appel e bordure de T (voir Figure 6.2). Si le sommet u appartient ` a B (T ), on dit aussi que u est adjacent ` a T . Le graphe G est biparti sil existe T S , tel que A = (T ). D enition 6.2. Soit G = (S, A), un graphe orient e. Un chemin f du graphe est une suite darcs < a1 , . . . , ap >, telle que org (ai+1 ) = ext(ai ). Lorigine du chemin f , not ee aussi org (f ), est celle de son premier arc a1 , et son extr emit e, ext(f ), est celle de ap . La longueur du chemin est egale au nombre darcs qui le composent, i.e. p. Un chemin f tel que org (f ) = ext(f ) est appel e un circuit. Dans un graphe non orient e la terminologie est parfois un peu di erente : un chemin est appel e une cha ne, et un circuit un cycle. Un graphe sans cycle est un graphe acyclique. Soit G = (S, A), un graphe orient e, et soit x, un sommet de S . Un sommet y est un ascendant (resp. descendant) de x, sil existe un chemin de y ` a x (resp. de x ` a y ). 86 F. Levy-dit-Vehel

Ann ee 2011-2012
1 4 8 7 0 3
0 1 2 3 4 5 6 7

Chapitre 6. Graphes

6 2

0 0 0 0 0 0 0 0 1

0 0 0 0 1 0 0 0 1

0 0 0 0 0 0 0 1 0

1 0 0 0 0 0 0 0 0

0 0 0 1 0 0 0 0 0

0 0 0 0 0 0 0 0 0

1 0 0 1 0 0 0 0 0

1 0 1 0 0 0 0 0 0

0 0 0 0 0 1 0 0 0

Figure 6.3 Matrice dadjacence associ ee ` a un graphe. Un graphe non orient e G est connexe si, pour tout couple de sommets, il existe une cha ne ayant ces deux sommets pour extr emit es. Par extension, un graphe orient e est connexe si sa version non orient ee est connexe. La relation d enie sur lensemble des sommets dun graphe non orient e par x y sil existe une cha ne reliant x ` a y , est une relation d equivalence dont les classes sont appel ees composantes connexes du graphe. Cette notion est tr` es similaire ` a la notion de composantes connexe en topologie : on regarde les composantes connexe du dessin du graphe. Soit G = (S, A), un graphe orient e. Notons G , la relation d equivalence d enie sur S par x G y si x = y ou sil existe un chemin joignant x ` a y et un chemin joignant y ` a x. Les classes d equivalence pour cette relation sont appel ees composantes fortement connexes de G.

6.2

Repr esentation des graphes

Il existe essentiellement deux fa cons de repr esenter un graphe. Dans la suite, G = (S, A) d esignera un graphe orient e.

6.2.1

Matrice dadjacence

Dans cette repr esentation, on commence par num eroter de fa con arbitraire les sommets du graphe : S = {x1 , . . . , xn }. On d enit ensuite une matrice carr ee M , dordre n par : { 1 si (xi , xj ) A Mi,j = 0 sinon. Autrement dit, Mi,j vaut 1 si, et seulement si, il existe un arc dorigine xi et dextr emit e xj (voir Figure 6.3). M est la matrice dadjacence de G. Bien entendu, on peut egalement repr esenter un graphe non orient e ` a laide de cette structure : la matrice M sera alors sym etrique, avec des 0 sur la diagonale. La structure de donn ee correspondante - ainsi que son initialisation - est : & M. Finiasz 87

6.2 Repr esentation des graphes


5 1 0 4 8 7 0 3 1 2 3 4 5 6 6 2 7 8 2 0 7 4 1 8

ENSTA cours IN101

tab
3 6 7

Figure 6.4 Repr esentation dun graphe par liste de successeurs.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

struct graph_mat { int n; int** mat; }; void graph_init(int n, graph_mat* G){ int i,j; G->n = n; G->mat = (int** ) malloc(n*sizeof(int* )); for (i=0; i<n ; i++) { G->mat[i] = (int* ) malloc(n*sizeof(int )); for (j=0; j<n ; j++) { G->mat[i][j] = 0; } } }

Le co ut en espace dun tel codage de la matrice est clairement en (n2 ). Ce codage devient donc inutilisable d` es que n d epasse quelques centaines de milliers. En outre, lorsque le graphe est peu dense (i.e. le rapport |A|/|S |2 est petit et donc la matrice M est creuse) il est trop co uteux. Cependant, la matrice dadjacence poss` ede de bonnes propri et es alg ebriques qui nous serviront dans l etude de lexistence de chemins (cf. section 6.3).

6.2.2

Liste de successeurs

Une autre fa con de coder la matrice dadjacence, particuli` erement adapt ee dans le cas dun graphe peu dense, consiste ` a opter pour une repr esentation creuse de la matrice au moyen de listes cha n ees : cette repr esentation est appel ee liste de successeurs (adjacency list en anglais) : chaque ligne i de la matrice est cod ee par la liste cha n ee dont chaque cellule est constitu ee de j et dun pointeur vers la cellule suivante, pour tous les j tels que Mi,j vaut 1. Autrement dit, on dispose dun tableau tab[n] de listes de sommets, tel 88 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 6. Graphes

que tab[i] contienne la liste des successeurs du sommet i, pour tout 1 i n. Cette repr esentation est en (n + |A|), donc poss` ede une complexit e en espace bien meilleure que la pr ec edente (` a noter que cette complexit e est optimale dun point de vue th eorique). Les structures de donn ees correspondantes sont :
1 2 3 4 5 6 7 8 9 10 11 12 13

int n; /* un sommet contient une valeur et tous ses successeurs */ struct vertex { int num; vertices_list* successors; }; /* les successeurs forment une liste */ struct vertices_list { vertex* vert; vertices_list* next; }; /* on alloue ensuite le tableau de sommets */ vertex* adjacancy = (vertex* ) malloc(n*sizeof(vertex ));

Dans le cas dun graphe non orient e, on parle de liste de voisins, mais le codage reste le m eme. Contrairement ` a la repr esentation par matrice dadjacence, ici, les sommets du graphe ont une v eritable repr esentation et peuvent contenir dautres donn ees quun simple entier. Avec la matrice dadjacence, d eventuelles donn ees suppl ementaires doivent etre stock ees dans une structure annexe.

6.3

Existence de chemins & fermeture transitive

Existence de chemins. Soit G = (S, A), un graphe orient e. La matrice dadjacence de G permet de conna tre lexistence de chemins entre deux sommets de G, comme le montre le th eor` eme suivant. Th eor` eme 6.3.1. Soit M p , la puissance p-i` eme de la matrice dadjacence M de G. Alors p egal au nombre de chemins de longueur p de G, dont lorigine est le le coecient Mi,j est sommet xi et lextr emit e xj . Preuve. On proc` ede par r ecurrence sur p. Pour p = 1, le r esultat est vrai puisquun chemin de longueur 1 est un arc du graphe. Fixons un entier p 2, et supposons le th eor` eme vrai pour tout j p 1. On a : n p p1 Mi,j = Mi,k Mk,j .
k=1

Or, tout chemin de longueur p entre xi et xj se d ecompose en un chemin de longueur p 1 p1 entre xi et un certain xk , suivi dun arc entre xk et xj . Par hypoth` ese, Mi,k est le nombre de chemins de longueur p 1 entre xi et xk , donc le nombre de chemins de longueur p & M. Finiasz 89

6.3 Existence de chemins & fermeture transitive

ENSTA cours IN101

p1 entre xi et xj est la somme, sur tout sommet interm ediaire xk , 1 k n, de Mi,k multipli e par le nombre darcs (0 ou 1) reliant xk ` a xj . La longueur dun chemin entre deux sommets etant au plus n, on en d eduit :

Corollaire 6.3.1. Soit N = M + . . . + M n . Alors il existe un chemin entre les sommets xi et xj si et seulement si, Ni,j = 0. Un algorithme de recherche de lexistence dun chemin entre deux sommets x et y de G est le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

int path_exists(graph_mat* G, int x, int y) { int i; int** R = (int** ) malloc(G->n*sizeof(int* )); for (i=0; i<G->n; i++) { R[i] = (int* ) malloc(G->n*sizeof(int )); } if (G->mat[x][y] == 1) { return 1; } copy_mat(G->mat,R); for (i=1; i<G->n; i++) { mult_mat(&R,R,G->mat); if (R[x][y] == 1) { return i+1; } } } return -1;

O` u les fonction copy mat(A,B) et mult mat(&C,A,B) permettent respectivement de copier la matrice A dans B et de sauver le produit des matrices A et B dans C. Cet algorithme retourne ensuite -1 si aucun chemin nexiste entre les deux sommets et renvoies sinon la longueur du plus court chemin entre les deux sommets. Dans lalgorithme path exists, on peut avoir ` a eectuer n produits de matrices pour conna tre lexistence dun chemin entre deux sommets donn es (cest le cas pour trouver un chemin de longueur n ou pour etre certain quaucun chemin nexiste). Le produit de e de recherche de deux matrices carr ees dordre n requ erant n3 op erations 1 , la complexit lexistence dun chemin entre deux sommets par cet algorithme est en (n4 ). Cest aussi bien entendu la complexit e du calcul de N . Fermeture transitive. La fermeture transitive dun graphe G = (S, A) est la relation binaire transitive minimale contenant la relation A sur S . Il sagit dun graphe G = (S, A ),
1. Les meilleurs algorithmes requi` erent une complexit e en (n2.5 ), mais les constantes dans le sont telles que pour des tailles inf erieurs ` a quelques milliers lalgorithme cubique de base est souvent le plus rapide.

90

F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 6. Graphes

tel que (x, y ) A si et seulement sil existe un chemin dans G dorigine x et dextr emit e y. La matrice N d enie pr ec edemment calcule la fermeture transitive du graphe G. En eet, la matrice dadjacence M de G est obtenue ` a partir de N en posant Mi,j = 0 si Ni,j = 0, Mi,j = 1 si Ni,j = 0. Une fois calcul ee G , on peut r epondre en temps constant ` a la question de lexistence de chemins entre deux sommets x et y de G. Exemple dApplication du Calcul de la Fermeture Transitive Lors de la phase de compilation dun programme, un graphe est associ e` a chaque fonction : cest le graphe des d ependances (entre les variables) de la fonction et les sommets de ce graphe repr esentent les variables, un arc entre deux sommets x et y indique que le calcul de la valeur de x fait appel au calcul de la valeur de y . Le calcul de la fermeture transitive de ce graphe permet alors dobtenir toutes les variables intervenant dans le calcul dune variable donn ee. Dans la suite, nous pr esentons un algorithme de calcul de la fermeture transitive A dun graphe G = (S, A), qui admet une complexit e en (n3 ). Soit x un sommet du graphe G et notons x (A), lop eration qui ajoute ` a A tous les arcs (y, z ) tels que y est un pr ed ecesseur de x, et z un successeur : x (A) = A {(y, z ), (y, x) A et (x, z ) A}. Cette op eration v erie les propri et es suivantes, que nous admettrons : Propri et e 6.3.1. x (x (A)) = x (A), et, pour tout couple de sommets (x, y ) : x (y (A)) = y (x (A)). Si lon consid` ere lit er ee de laction des xi sur A, on voit ais ement que x1 (x2 (. . . (xn (A)) . . .)) A . Mieux : Proposition 6.3.1. La fermeture transitive A de G est donn ee par A = x1 (x2 (. . . (xn (A)) . . .)). Preuve. Il reste ` a montrer A x1 (x2 (. . . (xn (A)) . . .)). Soit f , un chemin joignant deux sommets x et y dans G. Il existe donc des sommets de G y1 , . . . yp tels que : f = (x, y1 )(y1 , y2 ) . . . (yp , y ), donc (x, y ) y1 (y2 (. . . (yp (A)) . . .)). Dapr` es la propri et e ci-dessus, on peut permuter lordre des yi dans cette ecriture, donc on peut consid erer par exemple que les yi sont & M. Finiasz 91

6.4 Parcours de graphes

ENSTA cours IN101

ordonn es suivant leurs num eros croissants. Pour (i1 , . . . , ik ) {1, . . . , n}, avec, si 1 < j n, i < ij , notons A(i1 ,...,ik ) = xi1 (xi2 (. . . (xik (A)) . . .)). On peut voir que la suite A(i1 ,...,ik ) est croissante (i.e. A(i1 ,...,ik ) A(j1 ,...,j ) si (i1 , . . . , ik ) est une sous-suite de (j1 , . . . , j )). De plus, on vient de voir que, pour 1 p n, tout chemin de longueur p joignant deux sommets x et y dans G appartient ` a un A(i1 ,...,ip ) , pour un p-uplet (i1 , . . . , ip ) d el ements de {1, . . . , n}. Or, pour tout tel p-uplet, on a : A(i1 ,...,ip ) A(1,...,n) = x1 (x2 (. . . (xn (A)) . . .)). Donc tout chemin dans G (et tout arc dans A ) appartient ` a x1 (x2 (. . . (xn (A)) . . .)). Cette expression de la fermeture transitive dun graphe donne lieu ` a lalgorithme de Roy-Warshall suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

int** roy_warshall(graph_mat* G) { int i,j,k; int** M = (int** ) malloc(G->n*sizeof(int* )); for (i=0; i<G->n; i++) { M[i] = (int* ) malloc(G->n*sizeof(int )); } copy(G->mat,M); for (k=0; k<G->n; k++) { for (i=0; i<G->n; i++) { for (j=0; j<G->n; j++) { M[i][j] = M[i][j] || (M[i][k] && M[k][j]); } } } return M; }

Dans cet algorithme, les deux boucles for internes impl ementent exactement lop eration xk (A) ; en eet, la matrice dadjacence M de G est construite ` a partir de celle de G par : (xi , xj ) est un arc de G si cest un arc de G ou si (xi , xk ) et (xk , xj ) sont deux arcs de G. Cest exactement ce qui est calcul e au milieu de lalgorithme. Clairement, la complexit e 3 de lalgorithme de Roy-Warshall est en (n ) op erations bool eennes. Remarque : on obtient la fermeture r eexive-transitive de G en ajoutant ` a la matrice dadjacence de G la matrice Identit e dordre n.

6.4

Parcours de graphes

Nous etudions ici les algorithmes permettant de parcourir un graphe quelconque G, cest-` a-dire de visiter tous les sommets de G une seule fois. Il existe essentiellement deux m ethodes de parcours de graphes que nous exposons ci-apr` es. Chacune delles utilise la notion darborescence : pour parcourir un graphe, on va en eet produire un recouvrement du graphe par une arborescence, ou plusieurs si le graphe nest pas connexe. 92 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 6. Graphes

6.4.1

Arborescences

Une arborescence (S, A, r) de racine r S est un graphe tel que, pour tout sommet x de S , il existe un unique chemin dorigine r et dextr emit e x. La longueur dun tel chemin est appel e la profondeur de x dans larborescence. Dans une arborescence, tout sommet, sauf la racine, admet un unique pr ed ecesseur. On a donc |A| = |S | 1. Par analogie avec la terminologie employ ee pour les arbres, le pr ed ecesseur dun sommet est appel e son p` ere, les successeurs etant alors appel es ses ls. La di erence entre une arborescence et un arbre tient seulement au fait que, dans un arbre, les ls dun sommet sont ordonn es. On peut montrer quun graphe connexe sans cycle est une arborescence. Donc si G est un graphe sans cycle, G est aussi la for et constitu ee par ses composantes connexes. Une arborescence est repr esent ee par son vecteur p` ere, [n], o` u n est le nombre de sommets de larborescence, tel que [i] est le p` ere du sommet i. Par convention, [r] = NULL.

6.4.2

Parcours en largeur

Une arborescence souvent associ ee ` a un graphe quelconque est larborescence des plus courts chemins. Cest le graphe dans lequel ne sont conserv ees que les ar etes appartenant ` a un plus court chemin entre la racine et un autre sommet. On peut la construire gr ace ` a un parcours en largeur dabord du graphe (breadth-rst traversal en anglais). Le principe est le suivant : on parcourt le graphe ` a partir du sommet choisi comme racine en visitant tous les sommets situ es ` a distance (i.e profondeur) k de ce sommet, avant tous les sommets situ es ` a distance k + 1. D enition 6.3. Dans un graphe G = (S, A), pour chaque sommet x, une arborescence des plus courts chemins de racine x est une arborescence (Y, B, x) telle que un sommet y appartient ` a Y si, et seulement si, il existe un chemin dorigine x et dextr emit e y. la longueur du plus court chemin de x ` a y dans G est egale ` a la profondeur de y dans larborescence (Y, B, x). Remarque : cette arborescence existe bien puisque, si (a1 , a2 , . . . , ap ) est un plus court chemin entre org (a1 ) et ext(ap ), alors le chemin (a1 , a2 , . . . , ai ) est un plus court chemin entre org (a1 ) et ext(ai ), pour tout i, 1 i p. En revanche, elle nest pas toujours unique. Th eor` eme 6.4.1. Pour tout graphe G = (S, A), et tout sommet x de G, il existe une arborescence des plus courts chemins de racine x. Preuve. Soit x, un sommet de S . Nous allons construire une arborescence des plus courts chemins de racine x. On consid` ere la suite {Yi }i densembles de sommets suivante : Y0 = {x}. Y1 = Succ(x), lensemble des successeurs 2 de x.
2. Si (x, x) A, alors on enl` eve x de cette liste de successeurs.

& M. Finiasz

93

6.4 Parcours de graphes

ENSTA cours IN101

Pour i 1, Yi+1 est lensemble obtenu en consid erant tous les successeurs des sommets i de Yi qui nappartiennent pas ` a j =1 Yj . Pour chaque Yi , i > 0, on d enit de plus lensemble Bi des arcs dont lextr emit e est dans Yi et lorigine dans Yi1 . Attention Bi ne contient pas tous les arcs possibles, mais un seul arc par el ement de Yi : on veut que chaque el ement nait quun seul p` ere. On pose ensuite Y = i Yi , B = i Bi . Alors le graphe (Y, B ) est par construction une arborescence. Cest larborescence des plus courts chemins de racine x, dapr` es la remarque ci-dessus. Cette preuve donne un algorithme de construction de larborescence des plus courts chemins dun sommet donn e : comme pour le parcours en largeur dun arbre on utilise une le qui g` ere les ensembles Yi , i.e. les sommets ` a traiter (ce sont les sommets qui, ` a un moment donn e de lalgorithme, ont et e identi es comme successeurs, mais qui nont pas encore et e parcourus ; autrement dit, ce sont les sommets en attente ). Par rapport ` a un parcours darbre, la pr esence eventuelle de cycles fait que lon utilise en plus un coloriage des sommets avec trois couleurs : les sommets en blanc sont ceux qui nont pas encore et e trait es (au d epart, tous les sommets, sauf le sommet x choisi comme racine, sont blancs). Les sommets en gris sont ceux de la le, i.e. en attente de traitement, et les sommets en noir sont ceux d ej` a trait es (ils ont donc, ` a un moment, et e d el es). On d enit aussi un tableau d qui indique la distance du sommet consid er e` a x ; en dautres termes, d[v ] est la profondeur du sommet v dans larborescence en cours de construction (on initialise chaque composante de d ` a ). Selon la structure de donn ee utilis ee pour repr esenter le graphe, la fa con de programmer cet algorithme peut varier et la complexit e de lalgorithme aussi ! On se place ici dans le cas le plus favorable : on a une structure de donn ee similaire ` a un arbre, o` u chaque sommet du graphe contient un pointeur vers la liste de ses successeurs, et en plus, les sommets du graphe sont tous index es par des entiers compris entre 0 et n.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

struct vertex { int num; vertices_list* successors; }; struct vertices_list { vertex* vert; vertices_list* next; }; int n; int* color = NULL; int* dist = NULL; int* father = NULL; void init(vertex* root, int nb) { int i; n = nb; if (color == NULL) { /* initialiser uniquement si cela na jamais et e initialis e */ color = (int* ) malloc(n*sizeof(int ));

94

F. Levy-dit-Vehel

Ann ee 2011-2012
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55

Chapitre 6. Graphes

dist = (int* ) malloc(n*sizeof(int )); father = (int* ) malloc(n*sizeof(int )); for (i=0; i<n; i++) { color[i] = 0; /* 0 = blanc, 1 = gris, 2 = noir */ dist[i] = -1; /* -1 = infini */ father[i] = -1; /* -1 = pas de p` ere */ } } color[root->num] = 1; dist[root->num] = 0; } void minimum_spanning_tree(vertex* root, int num) { vertex* cur; vertices_list* tmp; init(root,num); push(root); while (!queue_is_empty()) { while((cur=pop()) != NULL) { tmp = cur->successors; while (tmp != NULL) { if (color[tmp->vert->num] == 0) { /* si le noeud navait jamais et e atteint, on fixe son p` ere et on le met dans la file de noeuds ` a traiter. */ father[tmp->vert->num] = cur->num; dist[tmp->vert->num] = dist[cur->num]+1; color[tmp->vert->num] = 1; push(tmp->vert); } tmp = tmp->next; } color[cur->num] = 2; } swap_queues(); } }

Pour simplier, la gestion des les ` a et e r eduite ` a son minimum : on a en fait deux les, lune dans laquelle on ne fait que retirer des el ements avec le fonction pop et lautre dans laquelle on ajoute des el ements avec la fonction push. La fonction swap queues permet d echanger ces deux les et la fonction queue is empty retourne vrai quand la premi` ere le est vide (on na plus de sommets ` a retirer). Chaque parcours en largeur ` a partir dun sommet fournissant une seule composante connexe du graphe, si le graphe en poss` ede plusieurs il faudra n ecessairement appeler la a chaque fois un fonction minimum spanning tree plusieurs fois avec comme argument ` sommet x non encore visit e. Si on se donne un tableau vertices contenant des pointeurs vers tous les sommets du graphe, un algorithme de parcours en largeur de toutes les composantes connexes est alors :

& M. Finiasz

95

6.4 Parcours de graphes


0 3

ENSTA cours IN101

5
1

1
2

4
1

8
1

3
0

0
0 1 2 3 4 5 6 7 8

father -1 4 7 0 3 -1 0 0 5
1 2

dist 0 3 2 1 2 0 1 1 1

Figure 6.5 Application de all spanning trees ` a un graphe. Les sommets 0 et 5 sont des racines. Les arcs en pointill es ne font pas partie dune arborescence.
1 2 3 4 5 6 7 8 9 10 11

void all_spanning_trees(vertex** vertices, int nb) { int i; /* un premier parcours en partant du noeud 0 */ minimum_spanning_tree(vertices[0], nb); for (i=1; i<nb; i++) { if (color[i] != 2) { /* si le noeud i na jamais et e colori e en noir */ minimum_spanning_tree(vertices[i], nb); } } }

Complexit e. Dans lalgorithme minimum spanning tree, la phase dinitialisation prend un temps en (n) la premi` ere fois (on initialise des tableaux de taille n) et un temps constant les suivantes, puis, pour chaque sommet dans la le, on visite tous ses successeurs une seule fois. Chaque sommet etant visit e (trait e) une seule fois, la complexit e de lalgorithme est en (n + |A|). Maintenant, si le graphe poss` ede plusieurs composantes connexes, on peut le d ecomposer en sous-graphes Gi = (Si , Ai ). La complexit e de lalgorithme all spanning trees sera alors n pour la premi` ere initialisation, plus n pour la boucle sur les sommets, plus (|Si | + |Ai |) pour chaque sous-graphe Gi . Au total on obtient une complexit e en (n + |A|). Attention Cette complexit e est valable pour une repr esentation du graphe par listes de successeurs, mais dans le cas dune repr esentation par matrice dadjacence, lacc` es ` a tous les successeurs dun sommet n ecessite de parcourir une ligne compl` ete de la matrice. La complexit e de lalgorithme devient alors (n2 ).

96

F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 6. Graphes

6.4.3

Parcours en profondeur

Alors que le parcours en largeur est essentiellement it eratif (impl ementation par le), le parcours en profondeur est de nature r ecursive 3 . Le principe du parcours en profondeur est de visiter tous les sommets en allant dabord le plus profond ement possible dans le graphe. Un syst` eme de datation (utilisant deux tableaux beg[] et end[]) permet de m emoriser les dates de d ebut et de n de traitement dun sommet. Lalgorithme de parcours cr ee 4 une for et par un proc ed e r ecursif. Chaque arborescence de la for et est cr e ee ` a partir dun sommet x par lalgorithme suivant (le code couleur utilis e pour colorier les sommets a la m eme signication que dans le cas du parcours en largeur - les sommets gris etant ceux de la pile, et correspondant ici aux sommets en cours de visite , dans le sens o` u le traitement dun tel sommet se termine lorsque lon a parcouru tous ses descendants). Voici une impl ementation du parcours en profondeur, reprenant les m emes structures que limpl ementation du parcours en largeur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

int n; int date = 0; int* color = NULL; int* father = NULL; int* beg = NULL; int* end = NULL; void init(int nb) { int i; n = nb; if (color == NULL) { /* initialise uniquement si cela na jamais et e initialis e */ color = (int* ) malloc(n*sizeof(int )); father = (int* ) malloc(n*sizeof(int )); beg = (int* ) malloc(n*sizeof(int )); end = (int* ) malloc(n*sizeof(int )); for (i=0; i<n; i++) { color[i] = 0; /* 0 = blanc, 1 = gris, 2 = noir */ father[i] = -1; /* -1 = pas de p` ere */ beg[i] = -1; /* -1 = pas de date */ end[i] = -1; /* -1 = pas de date */ } } } void depth_first_spanning_tree(vertex* x) { vertices_list* tmp; color[x->num] = 1;

3. Une version it erative de lalgorithme peut etre obtenue en utilisant une pile. 4. Cest la for et correspondant aux arcs eectivement utilis es pour explorer les sommets, donc, m eme si le graphe est connexe, il se peut quun parcours en profondeur de celui-ci produise une for et (cf. Figure 6.6). En revanche, si le graphe est fortement connexe, on est certain davoir un seul arbre dans cette for et.

& M. Finiasz

97

6.4 Parcours de graphes

ENSTA cours IN101

1 4 8 3 0
0 1 2 3 4 5 6 7 8

father -1 4 7 0 3 -1 3 0 5 beg 0 3 10 1 2 14 6 9 15
6 2

end 13 4 11 8 5 17 7 12 16

Figure 6.6 For et engendr ee lors du parcours en profondeur dun graphe. Le graphe de d epart est connexe, mais deux arbres sont n ecessaires pour le repr esenter enti` erement. Les arcs en pointill es sont ceux qui ne font partie daucune arborescence. Ici, les sommets ont et e parcourus dans lordre : 0, 3, 4, 1, 6, 7, 2 puis 5, 8.
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

beg[x->num] = date; date++; tmp = x->successors; while (tmp != NULL) { if (color[tmp->vert->num] == 0) { /* un successeur non encore visit e a et e trouv e */ father[tmp->vert->num] = x->num; depth_first_spanning_tree(tmp->vert); } tmp = tmp->next; } /* une fois tous les successeurs trait es le traitement du sommet x est fini */ color[x->num] = 2; end[x->num] = date; date++; }

Comme pour le parcours en largeur, il faut maintenant appeler cet algorithme pour chaque arbre de la for et. On utilise donc la fonction suivante :
1 2 3 4 5 6 7 8

void all_depth_first_spanning_trees(vertex** vertices, int nb) { int i; init(nb); for (i=0; i<nb; i++) { if (color[i] != 2) { /* si le noeud i na jamais et e colori e en noir */ depth_first_spanning_tree(vertices[i]); }

98

F. Levy-dit-Vehel

Ann ee 2011-2012
9 10

Chapitre 6. Graphes

} }

Le parcours en profondeur passe une seule fois par chaque sommet et fait un nombre dop erations proportionnel au nombre darcs. La complexit e dun tel parcours est donc en (n + |A|). Application au labyrinthe. Il est possible de repr esenter un labyrinthe par un graphe, dont les sommets sont les embranchements, ceux-ci etant reli es selon les chemins autoris es. Le parcours dun graphe en profondeur dabord correspond au cheminement dune personne dans un labyrinthe : elle parcourt un chemin le plus en profondeur possible, et reviens sur ses pas pour en emprunter un autre, tant quelle na pas trouv e la sortie. Ce retour sur ses pas est pris en charge par la r ecursivit e. Le parcours en largeur quand ` a lui mod eliserait plut ot un groupe de personnes dans un labyrinthe : au d epart, le groupe est au point dentr ee du labyrinthe, puis il se r epartit de fa con ` a ce que chaque personne explore un embranchement adjacent ` a lembranchement o` u il se trouve. Il mod elise egalement un parcours de labyrinthe eectu e par un ordinateur, dans lequel le sommet destination (la sortie) est connu, et pour lequel il sagit de trouver le plus court chemin de lorigine (entr ee du labyrinthe) ` a la destination, i.e. un chemin particulier dans larborescence des plus courts chemins de racine lorigine.

6.5
6.5.1

Applications des parcours de graphes


Tri topologique

Les graphes orient es sans circuit sont utilis es dans de nombreuses applications pour repr esenter des pr ec edences entre ev` enements (on parle alors de graphe des d ependances). Le tri topologique dun graphe consiste ` a ordonnancer les t aches repr esent ees par les sommets du graphe selon les d ependances mod elis ees par ses arcs. Par convention, une tache doit etre accomplie avant une autre, si un arc pointe du sommet correspondant ` a cette tache vers le sommet correspondant ` a lautre. Autrement dit, etant donn e un graphe orient e acyclique G = (S, A), le tri topologique de G ordonne les sommets de G en une suite telle que lorigine dun arc apparaisse avant son extr emit e. Il peut etre vu comme un alignement des sommets de G le long dune ligne horizontale, de mani` ere que tous les arcs soient orient es de gauche ` a droite. Le parcours en profondeur dun graphe permet de r esoudre le probl` eme du tri topologique. Lid ee est deectuer un parcours en profondeur du graphe, puis de retourner la liste des sommets dans lordre d ecroissant de leur date de n de traitement. En eet, si (u, v ) est un arc (et donc si la t ache repr esent ee par u doit etre eectu ee avant celle repr esent ee par v ) alors, dans lalgorithme de parcours en profondeur, le sommet v aura n ecessairement ni d etre trait e avant le sommet u : un sommet na jamais ni d etre trait e avant que tous ses successeurs naient et e trait es. Lalgorithme de tri topologique est donc le suivant : & M. Finiasz 99

6.5 Applications des parcours de graphes

ENSTA cours IN101

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

void topological_sort(vertex** vertices, int nb) { all_depth_first_spanning_trees(vertices, nb); /* on doit faire un tri en temps lin eaire : on utilise le fait que les valeurs de end sont entre 0 et 2*nb */ int rev_end[2*nb]; int i; for (i=0; i<2*nb; i++) { rev_end[i] = -1; } for (i=0; i<nb; i++) { rev_end[end[i]] = i; } for (i=2*nb-1; i>=0; i--) { if (rev_end[i] != -1) { printf("%d", rev_end[i]); } } }

On obtient ainsi un algorithme de complexit e (|S | + |A|), i.e. lin eaire en la taille du graphe. dun Graphe Test de Cyclicite De fa con similaire, on peut tester si un graphe est cyclique par un parcours en profondeur : en eet, si un graphe poss` ede un cycle, cela veut dire que, dans une arborescence de recouvrement, un sommet x est lorigine dun arc dont lextr emit e est un anc etre de x dans cette arborescence. Dans le parcours en profondeur, cela veut dire que ` a un moment, le successeur dun sommet gris sera gris. Il sut donc de modier l eg` erement lalgorithme de parcours en profondeur pour ne pas seulement tester si la couleur dun sommet est blanche, mais aussi tester si elle est grise et acher un message en cons equence.

6.5.2

Calcul des composantes fortement connexes

Une composante fortement connexe dun graphe est un ensemble de sommets tel quun chemin existe de nimporte quel sommet de cette composante vers nimporte quel autre. Typiquement, un cycle dans un graphe forme une composante fortement connexe. Tout graphe peut se d ecomposer en composantes fortement connexe disjointes : un ensemble de composantes fortement connexes tel que sil existe un chemin allant dun sommet dune composante vers un sommet dune autre, le chemin inverse nexiste pas. Par exemple, un graphe acyclique naura que des composantes fortement connexes compos ees dun seul sommet. On a vu que le parcours en profondeur permet de tester lexistence de cycles dans un graphe. Comme nous allons le voir, une modication de lalgorithme de parcours en pro100 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 6. Graphes

fondeur permet de retrouver toutes les composantes fortement connexes. Cest lalgorithme de Tarjan, invent e en 1972. Le principe de lalgorithme est le suivant : on eectue un parcours en profondeur du graphe, ` a chaque fois que lon traite un nouveau sommet on lui attribue un index (attribu e par ordre croissant) et un lowlink initialis e` a la valeur de lindex et on ajoute le sommet ` a une pile, le lowlink correspond au plus petit index accessible par un chemin partant du sommet en question, donc ` a la n du traitement dun sommet on met ` a jour son lowlink pour etre le minimum entre les lowlink de tous ses successeurs et lindex du sommet courant, chaque fois quun successeur dun sommet est d ej` a colori e en gris on met ` a jour le lowlink de ce sommet en cons equence, ` a chaque fois que lon a ni de traiter un sommet dont le lowlink est egal ` a lindex on a trouv e une composante fortement connexe. On peut retrouver lensemble des el ements de cette composante en retirant des el ements de la pile jusqu` a atteindre le sommet courant. Cet algorithme est donc un peu plus compliqu e que les pr ec edents, mais reste relativement simple ` a comprendre. Si on a un graphe acyclique, ` a aucun moment un successeur dun sommet naura un index plus petit que le sommet courant. Du coup, les lowlink restent toujours egaux ` a lindex du sommet et ` a la n du traitement de chaque sommet une nouvelle composant connexe a et e trouv ee : cette composante contient juste le sommet courant et on retrouve bien le r esultat attendu : un graphe acyclique contient n composantes fortement connexes compos ees dun seul sommet. Maintenant, si le graphe contient un cycle, tous les el ements de ce cycle vont avoir leur lowlink egal ` a lindex du premier sommet visit e. Donc, un seul sommet du cycle aura un son index egal ` a son lowlink : le premier visit e, qui est donc aussi le plus profond dans la pile. Tous les el ements qui sortiront de la pile avant lui sont alors les autres sommets de la composante fortement connexe form ee par le cycle. Voici le code correspondant ` a lalgorithme de Tarjan (un exemple dex ecution est visible sur la Figure 6.7) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

int int int int

cur_index = color[n]; index[n]; lowlink[n];

0; /* initialis es ` a 0 */ /* initialis es ` a -1 */ /* initialis es ` a -1 */

void Tarjan(vertex* x) { vertices_list* tmp; vertex* tmp2; index[x->num] = cur_index; lowlink[x->num] = cur_index; color[x->num] = 1; cur_index++; push(x); tmp = x->successors; while (tmp != NULL) { if (index[tmp->vert->num] == -1) {

& M. Finiasz

101

6.5 Applications des parcours de graphes


17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

ENSTA cours IN101

/* si le successeur na jamais et e visit e, on le visite */ Tarjan(tmp->vert); /* et on met ` a jour le lowlink de x */ lowlink[x->num] = min(lowlink[x->num], lowlink[tmp->vert->num]); } else if (color[tmp->vert->num] == 1) { /* si le successeur est en cours de visite on a trouv e un cycle */ lowlink[x->num] = min(lowlink[x->num], lowlink[tmp->vert->num]); } tmp = tmp->next; } if (lowlink[x->num] == index[x->num]) { /* on a fini une composante fortement connexe et on d epile jusqu` a retrouver x. */ printf("CFC : "); do { tmp2 = pop(); color[tmp2->num] = 2; printf("%d,", tmp2->num); while (tmp2 != x) printf("\n"); } }

Notons que index joue exactement le m eme r ole que la variable beg dans le parcours en profondeur, en revanche le coloriage change un peu : on ne colorie un sommet en noir que lorsquil sort de la pile, pas directement quand on a ni de traiter ses successeurs. Comme pour le parcours en profondeur, cette algorithme ne va pas forc ement atteindre tous les sommets du graphe, il faut donc lappeler plusieurs fois, exactement comme avec lalgorithme all depth first spanning trees.

6.5.3

Calcul de chemins optimaux

On consid` ere ici un graphe G = (S, A) pond er e, i.e. ` a chaque arc est associ ee une valeur appel ee poids. On appellera poids dun chemin la somme des poids des arcs qui le composent. Le probl` eme est alors, etant donn e deux sommets, de trouver un chemin de poids minimal reliant ces deux sommets (sil en existe un). La r esolution de ce probl` eme ` a beaucoup dapplications, par exemple pour le calcul du plus court chemin dune ville ` a une autre (en passant par des routes dont on conna t la longueur) ou le calcul dun chemin de capacit e maximale dans un r eseau de communication (dans lequel le taux de transmission dun chemin est egal au minimum des taux de transmission de chaque liaison interm ediaire) sont des exemples de situations dans lesquelles ce probl` eme intervient. An de traiter de mani` ere uniforme les di erents ensembles de d enition pour les poids, induits par lapplication (lensemble des nombres r eels, lensemble {vrai, f aux}...), on consid` ere la situation g en erale o` u les poids appartiennent ` a un semi-anneau S (, , , ), cest-` a-dire v eriant les propri et es suivantes : 102 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 6. Graphes

3
0 1 2 3 4 5

pile
0 4

3
0 1 2 3 4 5

pile
0 4

index -1 -1 -1 -1 -1 -1
1

index 0 1 -1 3 -1 2 lowlink 0 1 -1 3 -1 1 NULL

lowlink -1 -1 -1 -1 -1 -1
2 2

color 0 0 0 0 0 0
5

NULL
5

color 1 2 0 2 0 2

3
0 1 2 3 4 5

pile
0 4

3
0 1 2 3 4 5

pile
0 4

index 0 -1 -1 -1 -1 -1
1

index 0 1 -1 3 4 2 lowlink 0 1 -1 3 4 1 NULL

lowlink 0 -1 -1 -1 -1 -1 NULL
2 2

color 1 0 0 0 0 0
5

0
5

color 1 2 0 2 1 2

0 4

3
0 1 2 3 4 5

pile
0 4

3
0 1 2 3 4 5

pile
0 4

index 0 1 -1 -1 -1 -1 lowlink 0 1 -1 -1 -1 -1 NULL


1

index 0 1 5 3 4 2 lowlink 0 1 0 3 4 1

NULL

1 2

color 1 1 0 0 0 0
5

0 1
5

color 1 2 1 2 1 2

0 4 2

3
0 1 2 3 4 5

pile
0 4

3
0 1 2 3 4 5

pile
0 4

index 0 1 -1 -1 -1 2 lowlink 0 1 -1 -1 -1 1

NULL

index 0 1 5 3 4 2 lowlink 0 1 0 3 0 1

NULL

1 2

color 1 1 0 0 0 1
5

0 1 5

1 2

color 1 2 1 2 1 2
5

0 4 2

3
0 1 2 3 4 5

pile
0 4

3
0 1 2 3 4 5

pile
0 4

index 0 1 -1 -1 -1 2
1

index 0 1 5 3 4 2 lowlink 0 1 0 3 0 1

lowlink 0 1 -1 -1 -1 1 NULL
2 2

color 1 2 0 0 0 2
5

0
5

color 2 2 2 2 2 2

NULL

3
0 1 2 3 4 5

pile
0 4

index 0 1 -1 3 -1 2 lowlink 0 1 -1 3 -1 1 NULL

1 2

color 1 2 0 1 0 2
5

0 3

Figure 6.7 Exemple dex ecution de lalgorithme de Tarjan sur un petit graphe ` a trois composantes fortement connexes. On explore dabord le sommet 0 et on voit ensuite l evolution des di erentes variables au fur et ` a mesure de lexploration des autres sommets.

& M. Finiasz

103

6.5 Applications des parcours de graphes

ENSTA cours IN101

est une loi de composition interne sur S , commutative, associative, idempotente (x x = x), d el ement neutre . On impose de plus qu etant donn ee une s equence innie d enombrable s1 , . . . , si , . . . d el ements de S , s1 s2 . . . S . est une loi de composition interne associative, d el ement neutre . est absorbant pour . Pour tout x S , on note x , l el ement de S d eni par x = x x x x x x . . . Par exemple, dans le cas du probl` eme du plus court chemin reliant une ville ` a une autre, S (, , , ) = {R }(min, +, , 0) (la longueur dun chemin est la somme () des poids des arcs qui le composent, la longueur du plus court chemin etant le minimum () des longueurs des chemins existants ; ` a noter que, pour tout x, on a ici x = 0). Soit donc S (, , , ), un semi-anneau, et soit p, une fonction de A dans S , qui associe un poids ` a chaque arc du graphe. On prolonge p ` a S S par p(i, j ) = si (i, j ) A. On etend egalement p aux chemins en d enissant, pour tout chemin (s1 , . . . , sk ), p((s1 , . . . , sk )) = p((s1 , s2 )) p((s2 , s3 )) . . . p((sk1 , sk )). On cherche ` a d eterminer le co ut minimal (en fait optimal, selon lapplication) des chemins reliant deux sommets quelconques du graphe. Autrement dit, si i et j sont deux sommets de G, on cherche ` a d eterminer i,j = chemin {chemin de i a j } p(chemin). Il est clair que, pour un graphe quelconque, il nest en g en eral pas possible d enum erer tous les chemins reliant deux sommets. Pour calculer la matrice = (i,j )i,j , il existe un algorithme d u` a Aho, Hopcroft et Ullman, qui admet une complexit e en (n3 ), n etant le nombre de sommets de G. Si on appelle Best(x,y) la fonction qui calcule x y , Join(x,y) la fonction x y , Star(x) la fonction x et elem le type des el ements de S , on peut alors ecrire le code de lalgorithme AHU (qui ressemble beaucoup ` a lalgorithme de Roy-Warshall vu pr ec edemment). Il prend en argument la matrice M telle que M[i][j] est le poids de lar ete reliant le sommet i au somment j :
1 2 3 4 5 6 7 8 9 10 11

void AHU(elem** M, int n) { int i,j,k; for (k=0; k<n; k++) { for (i=0; i<n; i++) { for (j=0; j<n; j++) { M[i][j] = Optimal(M[i][j], Join(Join(M[i][k],Star(M[k][k])),M[k][j])); } } } }

` la n de lex Proposition 6.5.1. A ecution de lalgorithme AHU, la matrice M a et e modi ee et contient en M[i][j] le poids du chemin optimal reliant le sommet i au sommet j. La matrice que lon voulait calculer est donc d enie par i,j = M[i][j]. 104 F. Levy-dit-Vehel

Ann ee 2011-2012
(0)

Chapitre 6. Graphes
(0)

Preuve. Notons i,j , la valeur de la matrice M pass ee en argument : i,j = p(i, j ) ou . (k ) Consid erons la suite ( )k de matrices d enie par r ecurrence par i,j = i,j
(k) (k1)

(i,k

(k1)

(k,k ) k,j
(k )

(k1)

(k1)

).

Alors nous allons prouver par r ecurrence que le coecient i,j est egal au co ut minimal dun chemin reliant le sommet i au sommet j , en ne passant que par des sommets interm ediaires de num ero inf erieur ou egal ` a k. (k ) En eet, cette propri et e des i,j est vraie pour k = 0. Soit k 1, et supposons cette propri et e vraie pour k 1. Chaque chemin reliant le sommet i au sommet j en ne passant que par des sommets interm ediaires de num ero inf erieur ou egal ` a k peut soit ne pas passer (k1) par k - auquel cas, par hypoth` ese de r ecurrence, son co ut minimal est egal ` a i,j - soit etre d ecompos e en un chemin de i ` a k , d eventuels circuits partant et arrivant en k , et un chemin de k ` a j . Par hypoth` ese de r ecurrence, les co ut minimaux de ces chemins interm ediaires (k1) (k1) (k1) sont resp. i,k , (k,k ) et k,j . Le co ut minimal dun chemin reliant i ` a j en ne passant que par des sommets interm ediaires de num ero inf erieur ou egal ` a k est donc donn e par la formule ci-dessus. (n) A la n de lalgorithme, la matrice (i,j ) a et e calcul ee, qui correspond ` a la matrice des co uts minimaux des chemins reliant deux sommets quelconques du graphe, en ne passant que par des sommets interm ediaires de num ero inf erieur ou egal ` a n, i.e. par nimporte quel sommet. Dans limpl ementation de lalgorithme donn ee ici on nutilise quune seule matrice, donc en calculant les coecients de la n de la matrice ` a l etape k ceux du d ebut ont d ej` a et e modi es depuis l etape k 1. La matrice M ne suit donc pas exactement la formule de r ecurrence de la preuve, mais le r esultat nal reste quand m eme identique. Lorsque S (, , , ) = {vrai,faux}(ou, et, faux, vrai), lalgorithme ci-dessus est exactement lalgorithme de Roy-Warshall de calcul de la fermeture transitive de G (on a ici x =vrai pour tout x {vrai,faux}). Dans le cas sp ecique du calcul de plus courts chemins, i.e. lorsque S (, , , ) = {R }(min, +, , 0), lalgorithme de Aho-Hopcroft-Ullman est plus connu sous le nom dalgorithme de Floyd-Warshall. Lalgorithme AHU calcule le co ut minimal dun chemin entre deux sommets quelconques du graphe, sans fournir un chemin qui le r ealise. Nous pr esentons ci-apr` es un algorithme permettant de trouver eectivement un tel chemin. Calcul dun chemin de co ut minimal. Soit G = (S, A), un graphe sans circuit repr esent e par sa matrice dadjacence, et soit p, une fonction (poids) d enie sur S S comme ci-dessus. On suppose ici que cette fonction v erie p(i, i) = 0. On suppose egalement que lon a calcul e les co uts minimaux des chemins entre tous les couples de sommets, i.e. que lon dispose de la matrice = (i,j )i,j . Pour calculer ces chemins, on d enit une matrice de liaison = (i,j )i j , de la mani` ere suivante : i,j = & M. Finiasz 105

6.5 Applications des parcours de graphes

ENSTA cours IN101

si i = j ou sil nexiste aucun chemin entre i et j . Sinon, i,j est le pr ed ecesseur de j sur un chemin de co ut minimal issu de i. La connaissance de , permet dimprimer un chemin de co ut minimal entre deux sommets i et j de G avec lalgorithme r ecursif suivant ( est remplac e par -1) :
1 2 3 4 5 6 7 8 9 10 11 12

void print_shortest_path(int** Pi, int i, int j) { if (i==j) { printf("%d\n", i) } else { if (Pi[i][j] == -1) { printf("Pas de chemin de %d ` a %d\n", i, j); } else { print_shortest_path(Pi, i, Pi[i][j]); printf(",%d", j); } } }

Sous-Graphe de Liaison On d enit le sous-graphe de liaison de G pour i par G,i = (S,i , A,i ), o` u: S,i = {j S, i,j = } {i}, A,i = {(i,j , j ), j S,i \ {i}}. Dans le cas o` u le poids est donn e par la longueur, on peut montrer que G,i est une arborescence des plus courts chemins de racine i. Il est possible de modier lalgorithme AHU pour calculer les matrices (i,j )i,j et en m eme temps. Le principe est le m eme que dans lalgorithme initial, i.e. on calcule des suites de matrices en consid erant des sommets interm ediaires dun chemin de co ut minimal. (k) (0) (1) ( n) Plus pr ecis ement, on d enit la s equence , , . . . , , o` u i,j est le pr ed ecesseur du sommet j dans un chemin de co ut minimal issu de i et ne passant que par des sommets interm ediaires de num ero inf erieur ou egal ` a k . On a bien entendu (n) = , et on peut (k) exprimer i,j r ecursivement par : { si i = j ou p(i, j ) = , (0) i,j = i sinon, et, pour k 1,
(k) i,j

{ =

si i,j = i,j , i,j (k1) (k1) (k1) (k ) k,j si i,j = i,k k,j .
(0)

(k1)

(k )

(k1)

Avec les notations utilis ees dans la preuve de la proposition 6.5.1 : on a ici i,j = p(i, j ). Au nal, on obtient AHU link, la version modi ee de AHU calculant aussi la matrice . Comme lalgorithme AHU de d epart cet algorithme prend en argument des matrices d ej` a initialis ees : i,j = 1 si (i, j ) / A et i,j = i si (i, j ) A. 106 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 6. Graphes

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

void AHU_link(elem** M, int** Pi, int n) { int i,j,k; elem tmp; for (k=0; k<n; k++) { for (i=0; i<n; i++) { for (j=0; j<n; j++) { tmp = Optimal(M[i][j],Join(M[i][k],M[k][j])); if (M[i][j] != tmp) { M[i][j] = tmp; Pi[i][j] = Pi[k][j]; } } } } }

& M. Finiasz

107

Chapitre 7 Recherche de motifs


La recherche de motifs dans un texte est une autre op eration utile dans plusieurs domaines de linformatique. Une application directe est la recherche (ecace) de s equences dacides amin es dans de tr` es longues s equences dADN, mais dautres applications sont moins evidentes : par exemple, lors de la compilation dun programme on doit identier certains mots clef (for, if, sizeof...), identier les noms de variables et de fonctions... Tout cela revient en fait ` a une recherche dune multitude de motifs en parall` ele.

7.1

D enitions

Un alphabet est un ensemble ni de symboles. Un mot sur un alphabet est une suite nie de symboles de . Par exemple, pour lalphabet latin = {a, b, c, ..., z }, les symboles sont des lettres et un mot est une suite nie de lettres. On consid` ere egalement le mot vide not e , qui ne contient aucun symbole. La longueur dun mot est le nombre de symboles qui le compose : le mot vide est donc de longueur 0. Un mot est dit pr exe dun autre si ce mot appara t au d ebut de lautre (mot est pr exe de motif ). De m eme il est suxe dun autre sil appara t ` a la n (tif est suxe de motif ). Les mots etant de longueur nie mais quelconque il semble naturelle de les coder avec une structure de liste, cependant, par soucis decacit e, il seront en g en eral cod es avec des tableaux. Dans ce contexte, le probl` eme de la recherche de motif (pattern-matching en anglais) consiste simplement ` a trouver toutes les occurrences dun mot P dans un texte T . Soient donc T de longueur n cod e par T[0]...T[n-1] et P de longueur m cod e par P[0]...P[m-1]. On consid` ere que les el ements de lalphabet sont cod es par des int. La recherche de motifs va consister ` a trouver tous les d ecalages s [0, n m] tels que : j [0, m 1], P [j ] = T [s + j ]. L enonc e de ce probl` eme est donc tr` es simple, en revanche, les algorithmes ecaces pour y r epondre le sont un peu moins. 109

7.2 Lalgorithme de Rabin-Karp

ENSTA cours IN101

Un premier algorithme na f. Lalgorithme le plus na f pour la recherche de motif est d eduit directement de l enonc e : on essaye tous les d ecalages possibles, et pour chaque d ecalage on regarde si le texte correspond au motif. Voici le code dun tel algorithme :
1 2 3 4 5 6 7 8 9 10 11 12 13

void basic_pattern_lookup(int* T, int n, int* P, int m) { int i,j; for (i=0; i<=n-m; i++) { for (j=0; j<m; j++) { if (T[i+j] != P[j]) { break; } } if (j == m) { printf("Motif trouv e ` a la position %d.", i); } } }

Cet algorithme a une complexit e dans le pire cas en (nm) qui nest clairement pas optimale : linformation trouv ee ` a l etape s est totalement ignor ee ` a l etape s + 1.

7.2

Lalgorithme de Rabin-Karp

Lid ee de lalgorithme de Rabin-Karp est de reprendre lalgorithme na f, mais de remplacer la comparaison de mots par une comparaison dentiers. Pour cela, lid ee et de consid erer le motif recherch e comme un entier cod e en base d, o` u d est le nombre d el ements de . Ainsi, ` a un motif de longueur m est associ e un entier compris entre 0 et dm 1. De m eme, pour chaque d ecalage, on va d enir un entier correspondant au m symboles de T partant de ce d ecalage. On d enit donc : m 1 p= P [i]di ,
i=0 m 1 i=0

s [0, n m],

ts =

T [s + i]di .

Une fois ces entiers d enis, la recherche de motif consiste simplement ` a comparer p ` a chacun des ts . Cependant, m eme si on consid` ere que les op erations sur les entiers se font toujours en temps constant, cette m ethode ne permet pas encore de gagner en complexit e car le calcul de chacun des ts a une complexit e en (m) et donc calculer tous les ts co ute (nm). An dam eliorer cela on va calculer les ts de fa con r ecursive : entre ts et ts+1 un symbole est ajout e et un autre enlev e. On a donc : ts + dm1 T [s + m]. ts+1 = d 110 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 7. Recherche de motifs

Si les calculs sur les entiers se font en temps constant, le calcul de tous les ts a alors une complexit e en (n + m) et le co ut total de la recherche de motif est aussi (n + m). Voici le code de cet algorithme :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

void Rabin_Karp_Partic(int* T, int n, int* P, int m, int d) { int i,h,p,t; /* on calcule d^(m-1) une fois pour toute */ h = pow(d,m-1); /* on calcule p */ p=0; for (i=m-1; i>=0; i--) { p = P[i] + d*p; } /* on calcule t_0 */ t=0; for (i=m-1; i>=0; i--) { t = T[i] + d*t; } /* on teste tous les d ecalages */ for (i=0; i<n-m; i++) { if (t == p) { printf("Motif trouv e ` a la position %d.", i); } t = t/d + h*T[i+m]; } if (t == p) { printf("Motif trouv e ` a la position %d.", n-m); } }

En pratique, cette m ethode est tr` es ecace quand les entiers t et p peuvent etre repr esent es par un int, mais d` es que m et d grandissent cela nest plus possible. Lutilisation de grands entiers fait alors perdre lavantage gagn e car un op eration sur les grands entiers aura un co ut en (m log(d)) et la complexit e totale de lalgorithme devient alors (nm log(d)) (notons que cette complexit e est identique ` a celle de lalgorithme na f : le facteur log(d) correspond au co ut de la comparaison de deux symboles de qui est n eglig e dans la complexit e de lalgorithme na f). Toutefois, une solution existe pour am eliorer cela : il sut de faire tous les calculs modulo un entier q . Chaque fois que le calcul modulo q tombe juste (quand on obtient p = ts mod q ), cela veut dire quil est possible que le bon motif soit pr esent au d ecalage s, chaque fois que le calcul tombe faux on est certain que le motif nest pas pr esent avec un d ecalage s. Quand le calcul tombe juste on peut alors v erier que le motif est bien pr esent avec lalgorithme na f. On obtient alors lalgorithme suivant :
void Rabin_Karp(int* T, int n, int* P, int m, int d, int q) { int i,j,h,p,t;

1 2

& M. Finiasz

111

7.3 Automates pour la recherche de motifs


3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38

ENSTA cours IN101

/* on calcule d^(m-1) mod q une fois pour toute */ h = ((int) pow(d,m-1)) % q; /* on calcule p mod q*/ p=0; for (i=m-1; i>=0; i--) { p = (P[i] + d*p) % q; } /* on calcule t_0 mod q */ t=0; for (i=m-1; i>=0; i--) { t = (T[i] + d*t) % q; } /* on teste tous les d ecalages */ for (i=0; i<n-m; i++) { if (t == p) { /* on v erifie avec lalgorithme na f */ for (j=0; j<m; j++) { if (T[j+i] != P[j]) { break; } } if (j == m) { printf("Motif trouv e ` a la position %d.", i); } } t = (t/d + h*T[i+m]) % q; } for (j=0; j<m; j++) { if (T[j+n-m] != P[j]) { break; } } if (j == m) { printf("Motif trouv e ` a la position %d.", n-m); } }

Si q est bien choisi (typiquement une puissance de 2 plus petite que 232 ), la r eduction modulo q peut se faire en temps constant, et tous les calcul dentiers se font aussi en temps constant. La complexit e de lalgorithme d epend alors du nombre de fausse alertes (les d ecalages pour lesquels le calcul modulo q est bon, mais le motif nest pas pr esent) et du nombre de fois que le motif est r eellement pr esent. En pratique, la complexit e sera souvent tr` es proche de loptimal (n + m).

7.3

Automates pour la recherche de motifs

Les automates nis sont des objets issus de linformatique th eorique particuli` erement adapt es ` a la r esolution de certains probl` emes : typiquement, la recherche de motif. Apr` es 112 F. Levy-dit-Vehel

Ann ee 2011-2012
Fonction de transition Q Q

Chapitre 7. Recherche de motifs

1 0

S0 S0 S1 S1

0 1 0 1

S1 S0 S0 S1

S0
0

S1

S0 tat initial S1 tat final

Figure 7.1 Exemple dautomate et la table de sa fonction de transition. avoir d eni ce quest un automate ni, nous verrons comment obtenir un algorithme de recherche de motif qui aura toujours (pas uniquement dans les cas favorables) une complexit e en (n + m||), donc tr` es proche de loptimal quand n est grand par rapport ` a m.

7.3.1

Automates nis

Un automate ni (nite state machine en anglais) est une machine pouvant se trouver dans un nombre ni de congurations internes ou etats. Lautomate re coit une suite discr` ete de signaux, chaque signal provoquant un changement d etat ou transition. En ce sens, un automate est un graphe dont les sommets sont les etats possibles et les arcs les transitions. Il existe aussi des automates ayant un nombre inni d etats, mais nous ne consid erons ici que des automates nis. Dans la suite, le terme automate d esignera donc toujours un automate ni. Plus formellement, un automate M est la donn ee de : un alphabet ni , un ensemble ni non vide d etats Q, une fonction de transition : Q Q, un etat de d epart not e q0 , un ensemble F d etats naux, F Q. On notera M = (, Q, , q0 , F ). Un exemple de tel automate est dessin e en Figure 7.1. Fonctionnement. Lautomate fonctionne de la mani` ere suivante : etant dans l etat q Q et recevant le signal , lautomate va passer ` a l etat (q, ). Notons , le mot vide : Q Q prenant en argument un de . On etend en une fonction etat et un mot (vide ou non) et retournant un etat : (q, ) = q, et (q, wa) = ( (q, w), a), q Q, w , a . On d enit alors le langage reconnu par M comme etant le sous-ensemble de des mots dont la lecture par M conduit ` a un etat nal ; autrement dit (q0 , w) F }. L(M ) = {w , & M. Finiasz 113

7.3 Automates pour la recherche de motifs


a b b b a b a

ENSTA cours IN101


a,b

S0

S1

S2

S3

S4

S5

Figure 7.2 Exemple dautomate reconnaissant le motif baaba. Si w L(M ), on dit que M accepte le mot w. Par exemple, lautomate de la Figure 7.1 accepte tous les mots binaires qui contiennent un nombre impaire de 0.

7.3.2

Construction dun automate pour la recherche de motifs

Nous allons ici montrer que les automates permettent de r ealiser ecacement la recherche de motifs dans un texte. Soit donc un alphabet ni, et w avec |w| = m le mot que lon va chercher. Nous cherchons ` a d eterminer toutes les occurrences de w dans un texte t de longueur n. Pour cela, nous allons construire un automate M reconnaissant le langage w . Lensemble des etats de lautomate est Q = {S0 , S1 , . . . , Sm }, l etat initial est S0 , et il poss` ede un unique etat nal F = {Sm }. Avant de d enir la fonction de transition, on introduit la fonction : Q, d enie par (u) = max{0 i m, u = wi }, o` u wi est le pr exe de longueur i de w. Ainsi, (u) est la longueur du plus long pr exe de w qui soit ) de fa suxe de u. On cherche ` a d enir (et donc con ` a r ealiser les equivalences suivantes (o` uu ): (S0 , u) = Si si et seulement si w nappara 1. Pour 0 i < m, t pas dans u et (u) = i. 2. (S0 , u) = Sm si et seulement si w appara t dans u. L equivalence 2. correspond exactement ` a la fonction de lautomate M : on doit atteindre l etat nal si et seulement si le motif est apparu dans le texte. L equivalence 1. nous sert ` a construire lautomate : chaque fois lon rajoute un symbole x ` a u, on passe de la case S(u) ` a la case S(ux) , et d` es que lon atteint la case Sm on y reste. On obtient alors un automate tel que celui de la Figure 7.2. En pratique, pour construire ecacement cet automate, le plus simple est de proc eder par r ecurrence. Pour construire un automate qui reconna t le mot wx, on part de lautomate qui reconna t le mot w de longueur m et on l etend pour reconna tre wx de longueur m + 1. Supposons que lautomate Mw reconnaissant w soit construit. Pour construire lautomate Mwx on commence par ajouter le nud nal Sm+1 ` a Mw . Ne reste plus ensuite qu` a calculer tous les liens partant de la Sm (la derni` ere case de lautomate Mw , donc lavant derni` ere de Mwx ). Un lien est simple ` a calculer : la transition en ajoutant x pointe vers Sm+1 . Les autre liens peuvent etre calcul es simplement en utilisant lautomate Mw pr ec edemment construit. En eet, si t = x, alors (wt) m (en ajoutant une mauvaise transition on ne peut jamais avancer dans lautomate, au mieux on reste sur place). Si on appelle y le 114 F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 7. Recherche de motifs

premier symbole de w de telle sorte que w = yv , alors on aura aussi (wt) = (vt). Or le motif vt est de longueur m et donc (vt) peut etre calcul e en utilisant Mw . Lautomate Mw termine dans l etat S(vt) quand on lui donne le motif vt en entr ee : il sut alors de faire pointer le lien index e par t de Sm au S(vt) obtenu. Il y a || 1 tels liens ` a trouver et le co ut pour trouver son extr emit e est un parcours dautomate qui a une complexit e (m). Cependant, les m 1 premiers caract` eres sont communs pour les || 1 parcours ` a eectuer. Il sut donc de faire un parcours de m 1 caract` eres puis de recopier les || 1 transitions issues de l etat que lon a atteint. De plus, le parcours des m 1 premiers caract` eres correspond en fait ` a lajout dun caract` ere ` a la suite des m 2 caract` eres parcourus pendant la construction de Mw . Si lon a m emoris e l etat auquel aboutit le parcours de ces m 2 caract` eres, il faut donc (||) op erations pour construire lautomate Mwx ` a partir de lautomate Mw . Le co ut total de la construction dun automate Mw pour un motif de longueur m est donc (m||), mais cela demande de programmer comme il faut ! Voici un exemple de code C pour la construction dun tel automate : P est le motif recherch e, m sa longueur et sigma la taille de lalphabet ; le tableau d qui est retourn e correspond ` a la fonction de transition avec d[i][j] = (Si , j ).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

int** automata_construction(int* P, int m, int sigma) { int i,j; int etat_mem; /* on initialise le tableau de transitions */ int** d; d = (int** ) malloc((m+1) * sizeof(int* )); for (i=0; i<m+1; i++) { d[i] = (int* ) malloc(sigma * sizeof(int )); } /* on d efinit les transitions de l etat 0 */ for (i=0; i<sigma; i++) { d[0][i] = 0; } d[0][P[0]] = 1; etat_mem = 0; /* on traite les etats suivants */ for (i=1; i<m+1; i++) { for (j=0; j<sigma; j++) { d[i][j] = d[etat_mem][j]; } if (i < m) { d[i][P[i]] = i+1; etat_mem = d[etat_mem][P[i]]; } } return d; }

Ensuite, pour rechercher un motif w dans un texte t de longueur n il sut de construire & M. Finiasz 115

7.3 Automates pour la recherche de motifs

ENSTA cours IN101

lautomate M reconnaissant w , puis de faire entrer les n symboles de t dans M . Le co ut total de la recherche est donc (n + m||).

7.3.3

Reconnaissance dexpression r eguli` eres

Les automates sont un outil tr` es puissant pour la recherche de motif dans un texte, mais leur utilisation ne se limite pas ` a la recherche dun motif x e` a lint erieur dun texte. Une expression r eguli` ere est un motif correspondant ` a un ensemble de cha nes de caract` eres. Elle est elle m eme d ecrite par une cha ne de caract` ere suivant une syntaxe bien pr ecise (qui change selon les langages, sinon les choses seraient trop simples !). Reconna tre une expression r eguli` ere revient ` a savoir si le texte dentr ee fait partie des cha nes de caract` ere qui correspondent ` a cette expression r eguli` ere. La description compl` ete dune syntaxe dexpression r eguli` ere est un peu complexe donc ne sont donn es ici que quelques exemples dexpressions (et les cha nes de caract` eres quelles repr esentent), pour se faire une id ee de la signication des di erents symboles. a = la cha ne a uniquement abc = le mot abc uniquement . = nimporte quel symbole = les cha nes de longueur 1 a* = toutes les cha nes compos ees de 0 ou plusieurs a (et rien dautre) a? = 0 ou une occurrence de a a+ = toutes les cha nes compos ees de au moins un a (et rien dautre) .*a = toutes les cha nes se terminant par a [ac] = le caract` ere a ou le caract` ere c .*coucou.* = toutes les cha nes contenant le mot coucou (lobjet de ce chapitre) (bob|love) = le mot bob ou le mot love [^ab] = nimporte quel caract` ere autre que a ou b Les combinaisons de ces di erentes expressions permettent de d ecrire des ensembles de cha nes tr` es complexes et d ecider si un texte correspond ou pas ` a une expression peut etre dicile. En revanche, il est toujours possible de construire un automate ni (mais des fois tr` es grand !) qui permet de d ecider en temps lin eaire (en la taille du texte) si un texte est accept e par lexpression ou non. La complexit e de la construction de cet automate peut en revanche etre plus elev ee. La Figure 7.3 donne deux exemples dautomates et les expressions r eguli` eres quils acceptent.

116

F. Levy-dit-Vehel

Ann ee 2011-2012

Chapitre 7. Recherche de motifs

Automate pour l'expression a.*ees [^e] [^s] e [^e] [^s] .

S0

S1

S2

S3

S4

[^

a]

Automate pour l'expression [^r]*(et?r|te.*) t [^etr] [^etr] t t e r e [^etr] S1 e [^etr] t r . r . e

S1 '

S2 '
r

S0

S2

S3

Figure 7.3 Exemples dautomates reconnaissant des expressions r eguli` eres.

& M. Finiasz

117

Vous aimerez peut-être aussi