Sunteți pe pagina 1din 124

Elements dAlgorithmique Ensta - in101

Francoise Levy-dit-Vehel & Matthieu Finiasz Anne 2010-2011 e

Table des mati`res e


1 Complexit e 1.1 Dnitions . . . . . . . . . . . . . . . . . . . . . e 1.1.1 Comparaison asymptotique de fonctions 1.1.2 Complexit dun algorithme . . . . . . . e 1.1.3 Complexit spatiale, complexit variable e e 1.2 Un seul probl`me, quatre algorithmes . . . . . . e 1.3 Un premier algorithme pour le tri . . . . . . . . 2 Rcursivit e e 2.1 Conception des algorithmes . . . . . . . . . 2.2 Probl`me des tours de Hano . . . . . . . . . e 2.3 Algorithmes de tri . . . . . . . . . . . . . . 2.3.1 Le tri fusion . . . . . . . . . . . . . . 2.3.2 Le tri rapide . . . . . . . . . . . . . . 2.3.3 Complexit minimale dun algorithme e 2.4 Rsolution dquations de rcurrence . . . . e e e 2.4.1 Rcurrences linaires . . . . . . . . . e e 2.4.2 Rcurrences de partitions . . . . . e 2.5 Complments . . . . . . . . . . . . . . . . e 2.5.1 Rcursivit terminale . . . . . . . e e 2.5.2 Drcursication dun programme ee 2.5.3 Indcidabilit de la terminaison . . e e 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 Donnes e 3.1 Tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.1 Allocation mmoire dun tableau . . . . . . . . . e 3.1.2 Complment : allocation dynamique de tableau e 3.2 Listes cha ees . . . . . . . . . . . . . . . . . . . . . . . . n 3.2.1 Oprations de base sur une liste . . . . . . . . . . e 3.2.2 Les variantes : doublement cha ees, circulaires... n 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 ` adressage direct . . . . . . . a 4.3 Recherche squentielle . . . . . . . . e 4.4 Recherche dichotomique . . . . . . . 4.5 Tables de hachage . . . . . . . . . . . 4.6 Tableau rcapitulatif des complexits e e

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

5 Arbres 5.1 Prliminaires . . . . . . . . . . . . . . . . . . . . . . e 5.1.1 Dnitions et terminologie . . . . . . . . . . . e 5.1.2 Premi`res proprits . . . . . . . . . . . . . . e ee 5.1.3 Reprsentation des arbres . . . . . . . . . . . e 5.2 Utilisation des arbres . . . . . . . . . . . . . . . . . . 5.2.1 Evaluation dexpressions & parcours darbres . 5.2.2 Arbres Binaires de Recherche . . . . . . . . . 5.2.3 Tas pour limplmentation de les de priorit e e 5.2.4 Tri par tas . . . . . . . . . . . . . . . . . . . . 5.3 Arbres quilibrs . . . . . . . . . . . . . . . . . . . . e e 5.3.1 Rquilibrage darbres . . . . . . . . . . . . . ee 5.3.2 Arbres AVL . . . . . . . . . . . . . . . . . . . 6 Graphes 6.1 Dnitions et terminologie . . . . . . . . . . . . . . . e 6.2 Reprsentation des graphes . . . . . . . . . . . . . . . e 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 Dnitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 e 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 rguli`res . . . . . . . . . . . . . . . 116 e e

Chapitre 1 Complexit e
La notion de complexit est centrale en algorithmique : cest grce ` elle que lon peut e a a dnir ce quest un bon algorithme. La recherche dalgorithmes ayant une complexit plus e e petite que les meilleurs algorithmes connus est un th`me de recherche important dans toutes e les branches de linformatique et des mathmatiques appliques. Dans ce chapitre nous e e voyons comment est dnie cette notion de complexit. e e

1.1
1.1.1

Dnitions e
Comparaison asymptotique de fonctions

Commenons par un rappel de quelques notations : pour deux fonctions relles f (n) et c e g(n), on crira : e 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 ` la fois les proprits (1.1) et (1.2) : a ee f (n) = (g(n)). Plus formellement, f (n) = (g(n)) si : (c, c ) (R )2 , + n0 N, n n0 , c g(n) f (n) c g(n). (1.3) (1.2)

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

1.1 Dnitions e

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 ` oublier le a 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 tablir une hirarchie (non exhaustive) entre les fonctions : e e log(n) n n n n log(n) n2 n3 2n exp(n) n! nn 22

1.1.2

Complexit dun algorithme e

On appelle complexit dun algorithme est le nombre asymptotique doprations de e e quil doit eectuer en fonction de la taille de lentre quil a ` traiter. Cette combase e a plexit est indpendante de la vitesse de la machine sur laquelle est excut lalgorithme : e e e e la vitesse de la machine (ou la qualit de limplmentation) peut faire changer le temps e e dexcution dune opration de base, mais ne change pas le nombre doprations ` eectuer. e e e a Une optimisation qui fait changer le nombre doprations de base (et donc la complexit) e e doit tre vue comme un changement dalgorithme. e Etant donne la dnition prcdente, il convient donc de dnir convenablement ce e e e e e quest une opration de base et comment lon mesure la taille de lentre avant de pouvoir e e parler de la complexit dun algorithme. Prenons par exemple le code suivant, qui calcule e la somme des carrs de 1 ` n : e a
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`re que la taille de lentre est n et on cherche ` compter e e a le nombre doprations de base en fonction de n. Lalgorithme eectue des multiplications e et des additions, donc il convient de considrer ces oprations comme oprations de base. e e e Il y a au total n multiplications et n additions, donc lalgorithme a une complexit (n). e Le choix de lopration de base semble ici tout ` fait raisonnable car dans un processeur e a 2 F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 1. Complexit e

moderne laddition et la multiplication sont des oprations qui prennent (1) cycles pour e tre eectus. e e 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 cot dune addition ou dune multiplication va augmenter quand la taille des entiers va u augmenter, et ces oprations nont alors plus un cot constant. Il convient alors de changer e u dopration de base et de considrer non plus des multiplications/additions complexes mais e e des oprations binaires lmentaires. Le cot de laddition de deux nombre entre 0 et n est e ee u alors (log n) (le nombre de bits ncessaires pour crire n) et le cot dune multiplication e e u 2 (en utilisant un algorithme basique) est ((log n) ). Calculer la somme des carrs de 1 ` e a n quand n devient grand a donc une complexit (n(log n)2 ) oprations binaires. e e Comme vous le verrez tout au long de ce poly, selon le contexte, lopration de base ` e a choisir peut donc changer, mais elle reste en gnral lopration la plus naturelle. e e e Ordres de grandeurs de complexits. Jusquici nous avons parl de complexit e e e asymptotique des algorithmes, mais pour une complexit donne, il est important davoir e e une ide des ordres de grandeurs des param`tres que lalgorithme pourra traiter. Un ale e gorithme exponentiel sera souvent trop coteux pour de grandes entres, mais pourra en u e gnral tr`s bien traiter des petites entres. Voici quelques chires pour se faire une ide : e e e e e eectuer 230 (environ un milliard) oprations binaires sur un PC standard prend de e lordre de la seconde. le record actuel de puissance de calcul fourni pour rsoudre un probl`me donn est de e e e 60 lordre de 2 oprations binaires. e aujourdhui en cryptographie, on consid`re quun probl`me dont la rsolution ncessite e e e e 80 2 oprations binaires est impossible ` rsoudre. e a e 128 une complexit de 2 e oprations binaires sera a priori toujours inatteignable dici e quelques dizaines dannes (cest pour cela que lon utilise aujourdhui des clefs de 128 e bits en cryptographie symtrique, alors qu` la n des annes 70 les clefs de 56 bits du e a e DES semblaient susamment solides). Dans la pratique, un algorithme avec une complexit (n) pourra traiter des entres e e jusqu` n = 230 en un temps raisonnable (cela dpend bien sr des constantes), et un a e u algorithme cubique (de complexit (n3 )), comme par exemple un algorithme basique pour e linversion dune matrice nn, pourra traiter des donnes jusqu` une taille n = 210 = 1024. e a En revanche, inverser une matrice 220 220 ncessitera des mois de calcul ` plusieurs milliers e a de machines (` moins dutiliser un meilleur algorithme...). a

1.1.3

Complexit spatiale, complexit variable e e

Complexit spatiale. Jusquici, la seule complexit dont il a t question est la come e ee plexit temporelle : le temps mis pour excuter un algorithme. Cependant, il peut aussi tre e e e intressant de mesurer dautres aspects, et en particulier la complexit spatiale : la taille e e mmoire ncessaire ` lexcution dun algorithme. Comme pour la complexit temporelle, e e a e e & M. Finiasz 3

1.1 Dnitions e

ENSTA cours IN101

seule nous intresse la complexit spatiale asymptotique, toujours en fonction de la taille e e de lentre. Des exemples dalgorithmes avec des complexits spatiales direntes seront e e e donns dans la section 1.2. e Il est ` noter que la complexit spatiale est ncessairement toujours infrieure (ou a e e e gale) ` la complexit temporelle dun algorithme : on suppose quune opration dcriture e a e e e en mmoire prend un temps (1), donc crire une mmoire de taille M prend un temps e e e (M ). Comme pour la complexit temporelle, des bornes existent sur les complexits spatiales e e 20 atteignables en pratique. Un programme qui utilise moins de 2 entiers (4Mo sur une machine 32-bits) ne pose aucun probl`me sur une machine standard, en revanche 230 est ` e a la limite, 240 est dicile ` atteindre et 250 est ` peu pr`s hors de porte. a a e e ` Des Modeles de Memoire Alternatifs Vous pouvez tre amens ` rencontrer des mod`les de mmoire alternatifs dans lesquels e e a e e les temps dacc`s/criture ne sont plus (1) mais dpendent de la complexit spatiale e e e e totale de lalgorithme. Supposons que la complexit spatiale soit (M ). e Dans un syst`me informatique standard, pour accder ` une capacit mmoire de e e a e e taille M , il faut pouvoir adresser lensemble de cet espace. cela signie quune adresse mmoire (un pointeur en C), doit avoir une taille minimale de log2 M bits, et lire un e pointeur a donc une complexit (log M ). Plus un algorithme utilise de mmoire, plus e e le temps dacc`s ` cette mmoire est grand. Dans ce mod`le, la complexit temporelle e a e e e dun algorithme est toujours au moins gale ` (M log M ). e a Certains mod`les vont encore plus loin et prennent aussi en compte des contraintes e physiques : dans des puces mmoires planaires (le nombre de couches superposes de e e transistors est (1)) comme lon utilise ` lheure actuelle, le temps pour quune infora mation circule dun bout de la puce mmoire jusquau processeur est au moins gal ` e a e ( M ). Dans ce mod`le on peut borner la complexit temporelle par (M M ). La e e constante (linverse de la vitesse de la lumi`re) dans le est en revanche tr`s petite. e e Dans ce cours, nous considrerons toujours le mod`le mmoire standard avec des temps e e e dacc`s constants en (1). e

Complexit dans le pire cas. Comme nous avons vu, la complexit dun algorithme e e sexprime en fonction de la taille de lentre ` traiter. Cependant, certains algorithmes e a peuvent avoir une complexit tr`s variable pour des entres de mme taille : factoriser un e e e e nombre va par exemple dpendre plus de la taille du plus grand facteur que de la taille e totale du nombre. Une technique dtude des performances dun tel algorithme ` complexit variable e a e consiste ` examiner la complexit en temps du pire cas. Le temps de calcul dans le pire a e cas pour les entres x de taille n xe est dni par e e e T (n) = sup{x, |x|=n} T (x). T (n) fournit donc une borne suprieure sur le temps de calcul sur nimporte quelle e 4 F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 1. Complexit e

entre de taille n. Pour calculer T (n), on nglige les constantes dans lanalyse, an de e e pouvoir exprimer comment lalgorithme dpend de la taille des donnes. Pour cela, on e e utilise la notation O ou , qui permet de donner un ordre de grandeur sans caractriser e plus nement. La complexit dans le pire cas est donc indpendante de limplmentation e e e choisie. Complexit moyenne. Une autre faon dtudier les performances dun algorithme e c e consiste ` en dterminer la complexit moyenne : cest la moyenne du temps de calcul sur a e e toutes les donnes dune taille n xe, en supposant connue une distribution de probabilit e e e (p(n)) sur lensemble des entres de taille n : e Tm (n) =
x, |x|=n

pn (x)T (x).

On remarque que Tm (n) nest autre que lesprance de la variable alatoire temps de cale e cul . Le cas le plus ais pour dterminer Tm (n) est celui o` les donnes sont quidistribues e e u e e e dans leur ensemble de dnition : on peut alors dans un premier temps valuer le temps e e de calcul T (x) de lalgorithme sur une entre x choisie alatoirement dans cet ensemble, e e le calcul de la moyenne des complexits sur toutes les entres de lensemble sen dduisant e e e facilement 1 . Lorsque la distribution (p(n)) nest pas la distribution uniforme, la complexit moyenne e dun algorithme est en gnral plus dicile ` calculer que la complexit dans le pire cas ; e e a e de plus, lhypoth`se duniformit peut ne pas reter la situation pratique dutilisation de e e e lalgorithme.

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


nombre moyen (selon une distribution de probabilit sur les entres) de fois que chaque e e instruction est excute, en le multipliant par le temps propre ` chaque instruction, et en e e a faisant la somme globale de ces quantits. Elle est donc dpendante de limplmentation e e e choisie. Trois dicults se prsentent lorsque lon veut mener ` bien un calcul de complexit e e a e en moyenne : dabord, lvaluation prcise du temps ncessaire ` chaque instruction peut e e e a savrer dicile, essentiellement ` cause de la variabilit de ce temps dune machine ` e a e a lautre. Ensuite, le calcul du nombre moyen de fois que chaque instruction est excute peut tre e e e dlicat, les calculs de borne suprieure tant gnralement plus simples. En consquence, e e e e e e la complexit en moyenne de nombreux algorithmes reste inconnue ` ce jour. e a Enn, le mod`le de donnes choisi nest pas forcment reprsentatif des ensembles de e e e e donnes rencontrs en pratique. Il se peut mme que, pour certains algorithmes, il nexiste e e e pas de mod`le connu. e
1. Cette hypoth`se dquidistribution rend en eet le calcul de la complexit sur une donne choisie e e e e alatoirement reprsentatif de la complexit pour toutes les donnes de lensemble. e e e e

& M. Finiasz

1.2 Un seul probl`me, quatre algorithmes e

ENSTA cours IN101

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

1.2

Un seul probl`me, quatre algorithmes de come plexits tr`s direntes e e e

Pour comprendre limportance du choix de lalgorithme pour rsoudre un probl`me e e donn, prenons lexemple du calcul des nombres de Fibonacci dnis de la mani`re suivante : e e e F0 = 0, F1 = 1, et, n 2, Fn = Fn1 + Fn2 . Un premier algorithme pour calculer le n-i`me nombre de Fibonacci Fn reprend exactement e la dnition par rcurrence ci-dessus : e e
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 rcursif (cf. chapitre 2). Si lon note Cn , le e nombre dappels ` fibo1 ncessaires au calcul de Fn , on a, C0 = C1 = 1, et, pour n 2, a e Cn = 1 + Cn1 + Cn2 . Si lon pose Dn = (Cn + 1)/2, on observe que Dn suit exactement la relation de rcurrence e dnissant la suite de Fibonacci (seules les conditions initiales di`rent). e e La rsolution de cette rcurrence linaire utilise une technique dalg`bre classique (cf. e e e e chapitre 2) : on calcule les solutions de lquation caractristique de cette rcurrence - ici e e e 1+ 5 2 = 1 5 . Dn scrit alors x x 1 = 0 - qui sont = 2 et e 2 Dn = n + n . On dtermine et ` laide des conditions initiales. On obtient nalement e a 1 Dn = (n+1 n+1 ), n 0. 5 Etant donn que Cn = 2Dn 1, la complexit - en termes de nombre dappels ` fibo1 e e a n du calcul de Fn par cet algorithme est donc ( ), cest-`-dire exponentiel en n. a On observe que lalgorithme prcdent calcule plusieurs fois les valeurs Fk , pour k < n, e e ce qui est bien videmment inutile. Il est plus judicieux de calculer les valeurs Fk , 2 k n e ` partir des deux valeurs Fk1 et Fk2 , de les stocker dans un tableau, et de retourner Fn . a 6 F. Levy-dit-Vehel

Anne 2010-2011 e

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 de mesure de complexit le temps de calcul dune opration e e e daddition, de soustraction ou de multiplication. La complexit de lalgorithme Fibo2 est e alors (n), i.e. linaire en n (en eet, la boucle for comporte n 1 itrations, chaque e e itration consistant en une addition). La complexit est donc drastiquement rduite par e e e rapport ` lalgorithme prcdent : le prix ` payer ici est une complexit en espace linaire a e e a e e ((n) pour stocker le tableau fib). On remarque maintenant que les n 2 valeurs Fk , 0 k n 3 nont pas besoin dtre e stockes pour le calcul de Fn . On peut donc revenir ` une complexit en espace en (1) en e a e ne conservant que les deux derni`res valeurs courantes Fk1 et Fk2 ncessaires au calcul e e de Fk : on obtient le troisi`me algorithme suivant : e
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 en temps de (n). e Un dernier algorithme permet datteindre une complexit logarithmique en n. Il est bas e e sur lcriture suivante de (Fn , Fn1 ), pour n 2 : e ) ) ( )( ( 1 1 Fn1 Fn . = Fn2 Fn1 1 0 En itrant, on obtient : e ( Fn Fn1 ) = ( 1 1 1 0 )n1 ( F1 F0 ) . 7

& M. Finiasz

1.2 Un seul probl`me, quatre algorithmes e

ENSTA cours IN101 (

) 1 1 Ainsi, calculer Fn revient ` mettre ` la puissance (n 1) la matrice a a . 1 0 La complexit en temps de lalgorithme qui en dcoule est ici (log(n)) multiplications e e de matrices carres 2 2 : en eet, cest le temps requis par lalgorithme dexponentiation e square-and-multiply (cf. TD 01) pour calculer la matrice. Lespace ncessaire est celui e du stockage de quelques matrices carres 2 2, soit (1). Lalgorithme fibo4 peut scrire e e 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 rsultat avec la matrice */ e res[0][0] = 1; res[0][1] = 1; res[1][0] = 1; res[1][1] = 0; /* cas particulier n==0 ` traiter tout de suite */ a if (n == 0) { return 0; } /* on doit trouver le un le plus ` gauche de n-1 */ a tmp = n-1; while (tmp != 0) { i++; tmp = tmp >> 1; } /* on dcrmente i car la premi`re multiplication e e e a dj` t faite en initialisant res e a e e */ i--; while (i > 0) { /* on l`ve au carr */ e e 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`me bit de n-1 e 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

Anne 2010-2011 e
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 tude algorithmique pralable du probl`me de dpart conduit ` une rduction e e e e a e parfois drastique de la complexit de sa rsolution, ce qui a pour consquence de permettre e e e datteindre des tailles de param`tres inenvisageables avec un algorithme irrchi... La e e e Table 1.1 ci-dessous illustre les temps de calcul des quatre algorithmes prcdents. e e fibo1 fibo2 fibo3 fibo4 n (n ) (n) (n) (log(n)) 40 31s 0s 0s 0s 228 231 calcul irralisable e 18s Segmentation fault 4s 25s 195s 0s 0s 0s 225

Table 1.1 Complexits et temps dexcution de dirents algorithmes pour le calcul e e e 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`s vite un temps trop lev pour pouvoir tre utilis. Une autre e e e e e limitation de fibo1 que lon ne voit pas dans le tableau est la limite lie au nombre maximal e de niveaux de rcursivit autoriss dans un programme : en C cette valeur est souvent e e e autour de quelques dizaines de milliers 2 . On ne peut donc pas avoir plus que ce nombre dappels rcursifs imbriqus au sein dun algorithme. Pour lalgorithme fibo2 la limite ne e e vient pas du temps de calcul, mais de la taille mmoire ncessaire : quand on essaye dallouer e e 28 un tableau de 2 entiers (soit 1Go de mmoire dun seul bloc) le syst`me dexploitation e e narrive pas ` satisfaire notre requte et nalloue donc pas de mmoire, et puisque lon ne a e e teste pas si la mmoire a bien t alloue avant dcrire dans notre tableau, un probl`me ` e ee e e e a lallocation se traduit immdiatement par une erreur de segmentation mmoire ` lcriture. e e a e

1.3

Un premier algorithme pour le tri

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

& 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 : ` chaque tape de la boucle for, les i a e premiers lments de tab sont tris par ordre croissant. Quand i = 1 cest vrai, et ` chaque ee e a fois que lon augmente i de 1, la boucle while se charge de replacer le nouvel lment ` sa ee a place en le remontant lment par lment vers les petits indice du tableau : si le nouvel ee ee lment est plus petit que celui juste avant, on inverse les deux lments que lon vient de ee ee comparer et on recommence jusqu` avoir atteint la bonne position. a Cet algorithme a un cot tr`s variable en fonction de ltat initial du tableau : u e e pour un tableau dj` tri, lalgorithme va simplement parcourir tous les lments et les ea e ee comparer ` llment juste avant eux, mais ne fera jamais dchange. Le cot est alors a ee e u de n comparaisons. pour un tableau tri ` lenvers (le cas le pire), lalgorithme va remonter chaque nouvel ea lment tout au dbut du tableau. Il y a alors au total exactement n(n1) comparaisons ee e 2 et changes. Lalgorithme de tri par insertion ` donc une complexit (n2 ) dans le pire e a e des cas. en moyenne, il faudra remonter chaque nouvel lment sur la moiti de la longueur, donc ee e i1 comparaisons et change par nouvel lment. Au nal la complexit en moyenne du e ee e 2 2 tri par insertion est (n ), la mme que le cas le pire (on perd le facteur 1 dans le ). e 2 Gnralisations du tri par insertion. Il existe plusieurs variantes du tri par insertion e e visant ` amliorer sa complexit en moyenne. Citons en particulier le tri de Shell (cf. http: a e e //fr.wikipedia.org/wiki/Tri_de_Shell) qui compare non pas des lments voisins, ee mais des lments plus distants an doptimiser le nombre de comparaisons ncessaires. ee e

10

F. Levy-dit-Vehel

Chapitre 2 Rcursivit e e
Les dnitions rcursives sont courantes en mathmatiques. Nous avons vu au chapitre e e e prcdent lexemple de la suite de Fibonacci, dnie par une relation de rcurrence. En e e e e informatique, la notion de rcursivit joue un rle fondamental. Nous voyons dans ce e e o chapitre la puissance de la rcursivit au travers essentiellement de deux algorithmes de e e tri ayant les meilleures performances asymptotiques pour des algorithmes gnriques. Nous e e terminons par une tude des solutions dquations de rcurrence entrant en jeu lors de e e e lanalyse de complexit de tels algorithmes. e

2.1

Conception des algorithmes

Il existe de nombreuses faons de concevoir un algorithme. On peut par exemple adopter c une approche incrmentale ; cest le cas du tri par insertion : apr`s avoir tri le sous-tableau e e e tab[0]...tab[j-1], on ins`re llment tab[j] au bon emplacement pour produire le e ee sous-tableau tri tab[0]...tab[j]. e Une approche souvent tr`s ecace et lgante est lapproche rcursive : un algorithme e ee e rcursif est un algorithme dni en rfrence ` lui-mme (cest la cas par exemple de e e ee a e lalgorithme fibo1 vu au chapitre prcdent). Pour viter de boucler indniment lors de sa e e e e mise en oeuvre, il est ncessaire de rajouter ` cette dnition une condition de terminaison, e a e qui autorise lalgorithme ` ne plus tre dni ` partir de lui-mme pour certaines valeurs a e e a e de lentre (pour fibo1, nous avons par exemple dni fibo1(0)=0 et fibo1(1)=1). e e Un tel algorithme suit gnralement le paradigme diviser pour rgner : il spare le e e e e probl`me en plusieurs sous-probl`mes semblables au probl`me initial mais de taille moindre, e e e rsout les sous-probl`mes de faon rcursive, puis combine toutes les solutions pour produire e e c e la solution du probl`me initial. La mthode diviser pour rgner implique trois tapes ` e e e e a chaque niveau de la rcursivit : e e Diviser le probl`me en un certain nombre de sous-probl`mes. On notera D(n) la come e plexit de cette tape. e e Rgner sur les sous-probl`mes en les rsolvant de mani`re rcursive (ou directe si le e e e e e sous-probl`me est susamment rduit, i.e. une condition de terminaison est atteinte). e e 11

2.2 Probl`me des tours de Hano e

ENSTA cours IN101

On notera R(n) la complexit de cette tape. e e Combiner les solutions des sous-probl`mes pour trouver la solution du probl`me initial. e e On notera C(n) la complexit de cette tape. e e Lien avec la rcursivit en mathmatiques. On rencontre en gnral la notion e e e e e de rcursivit en mathmatiques essentiellement dans deux domaines : les preuves par e e e rcurrence et les suites rcurrentes. La conception dalgorithmes rcursifs se rapproche e e e plus des preuves par rcurrence, mais comme nous allons le voir, le calcul de la complexit e e dun algorithme rcursif se rapproche beaucoup des suites rcurrentes. e e Lors dune preuve par rcurrence, on prouve quune proprit est juste pour des condie ee tions initiales, et on tend cette proprit ` lensemble du domaine en prouvant que la e ee a proprit est juste au rang n si elle est vrai au rang n 1. Dans un algorithme rcursif la ee e condition de terminaison correspond exactement au conditions initiales de la preuve par rcurrence, et lalgorithme va ramener le calcul sur une entre ` des calculs sur des entres e e a e plus petites. Attention ! Lors de la conception dun algorithme rcursif il faut bien faire attention ` ce que e a tous les appels rcursifs eectus terminent bien sur une condition de terminaison. e e Dans le cas contraire lalgorithme peut partir dans une pile dappels rcursifs innie e (limite uniquement par le nombre maximum dappels rcursifs autoriss). De mme, e e e e en mathmatique, lors dune preuve par rcurrence, si la condition rcurrente ne se e e e ram`ne pas toujours ` une condition initiale, la preuve peut tre fausse. e a e

2.2

Probl`me des tours de Hano e

Le probl`me des tours de Hano peut se dcrire sous la forme dun jeu (cf. Figure 2.1) : e e on dispose de trois piquets numrots 1,2,3, et de n rondelles, toutes de diam`tre dirent. e e e e Initialement, toutes les rondelles se trouvent sur le piquet 1, dans lordre dcroissant des e diam`tres (elle forment donc une pyramide). Le but du jeu est de dplacer toutes les e e rondelles sur un piquet de destination choisi parmi les deux piquets vides, en respectant les r`gles suivantes : e on ne peut dplacer quune seule rondelle ` la fois dun sommet de pile vers un autre e a piquet ; on ne peut pas placer une rondelle au-dessus dune rondelle de plus petit diam`tre. e Ce probl`me admet une rsolution rcursive lgante qui comporte trois tapes : e e e ee e 1. dplacement des n 1 rondelles suprieures du piquet origine vers le piquet ine e termdiaire par un appel rcursif ` lalgorithme. e e a 2. dplacement de la plus grande rondelle du piquet origine vers le piquet destination. e 3. dplacement des n 1 rondelles du piquet intermdiaire vers le piquet destination e e par un appel rcursif ` lalgorithme. e a 12 F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 2. Rcursivit e e

Figure 2.1 Le jeu des tours de Hano. Dplacement dune rondelle du piquet 1 vers e le piquet 2. En pointills : lobjectif nal. e Cet algorithme, que lon appelle Hano suit lapproche diviser pour rgner : la phase de , e division consiste toujours ` diviser le probl`me de taille n en deux sous-probl`mes de taille a e e n 1 et 1 respectivement. La phase de r`gne consiste ` appeler rcursivement lalgorithme e a e Hano sur le sous-probl`me de taille n 1. Ici, il y a deux sries dappels rcursifs e e e par sous-probl`me de taille n 1 (tapes 1. et 3.). La phase de combinaison des solutions e e des sous-probl`mes est inexistante ici. On donne ci-apr`s le programme C implmentant e e e 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 rcursifs que si n > 0) et pour cette valeur lalgorithme ne fait rien. Le lecteur est invit ` e ea vrier que lexcution de Hanoi(3,1,3) donne : e e
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`me des tours de Hano e

ENSTA cours IN101

Complexit de lalgorithme. Calculer la complexit dun algorithme rcursif peut e e e parfois sembler compliqu, mais il sut en gnral de se ramener ` une relation dnissant e e e a e une suite rcurrente. Cest ce que lon fait ici. Soit T (n) la complexit (ici, le nombre de e e dplacements de disques) ncessaire ` la rsolution du probl`me sur une entre de taille n e e a e e e par lalgorithme Hano En dcomposant la complexit de chaque tape on trouve : ltape . e e e e de division ne cote rien, ltape de r`gne cote le prix de deux mouvements de taille n 1 u e e u et ltape de combinaison cote le mouvement du grand disque, soit 1. On a T (0) = 0, e u T (1) = 1, et, pour n 2, T (n) = D(n) + R(n) + C(n) = 0 + 2 T (n 1) + 1. En dveloppant cette expression, on trouve e T (n) = 2n T (0) + 2n1 + . . . + 2 + 1, soit T (n) = 2n1 + . . . + 2 + 1 = 2n 1. Ainsi, la complexit de cet algorithme est exponentielle en la taille de lentre. En fait, e e ce caract`re exponentiel ne dpend pas de lalgorithme en lui-mme, mais est intrins`que e e e e au probl`me des tours de Hano : montrons en eet que le nombre minimal minn de e mouvements de disques ` eectuer pour rsoudre le probl`me sur une entre de taille a e e e n est exponentiel en n. Pour cela, nous observons que le plus grand des disques doit ncessairement tre dplac au moins une fois. Au moment du premier mouvement de ce e e e e grand disque, il doit tre seul sur un piquet, son piquet de destination doit tre vide, donc e e tous les autres disques doivent tre rangs, dans lordre, sur le piquet restant. Donc, avant e e le premier mouvement du grand disque, on aura d dplacer une pile de taille n 1. De u e mme, apr`s le dernier mouvement du grand disque, on devra dplacer les n 1 autres e e e disques ; ainsi, minn 2minn1 +1. Or, dans lalgorithme Hano le nombre de mouvements , de disques vrie exactement cette galit (on a T (n) = minn ). Ainsi, cet algorithme est e e e optimal, et la complexit exponentielle est intrins`que au probl`me. e e e Complexite Spatiale des Appels Recursifs Notons que limplmentation que nous donnons de lalgorithme Hano nest pas stane dard : lalgorithme est en gnral programm ` laide de trois piles (cf. section 3.3.1), e e ea mais comme nous navons pas encore tudi cette structure de donne, notre algorithme e e e se contente dacher les oprations quil eectuerait normalement. En utilisant des e piles, la complexit spatiale serait (n) (correspondant ` lespace ncessaire pour stoe a e cker les n disques). Ici il ny a pas dallocation mmoire, mais la complexit spatiale est e e quand mme (n) : en eet, chaque appel rcursif ncessite dallouer de la mmoire e e e e (ne serait-ce que pour conserver ladresse de retour de la fonction) qui nest libre que e e lorsque la fonction appele rcursivement se termine. Ici le programme utilise jusqu` e e a n appels rcursifs imbriqus donc sa complexit spatiale est (n). Il nest pas courant e e e de prendre en compte la complexit spatiale dappels rcursifs imbriqus, car en gnral e e e e e ce nombre dappels est toujours relativement faible.

14

F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 2. Rcursivit e e

2.3

Algorithmes de tri

Nous continuons ici ltude de mthodes permettant de trier des donnes indexes par e e e e des clefs (ces clefs sont munies dune relation dordre et permettent de conduire le tri). Il sagit de rorganiser les donnes de telle sorte que les clefs apparaissent dans un ordre bien e e dtermin (alphabtique ou numrique le plus souvent). e e e e Les algorithmes simples (comme le tri par insertion, mais aussi le tri par slection ou e le tri ` bulle) sont ` utiliser pour des petites quantits de donnes (de lordre de moins de a a e e 100 clefs), ou des donnes prsentant une structure particuli`re (donnes compl`tement ou e e e e e presque tries, ou comportant beaucoup de clefs identiques). e Pour de grandes quantits de donnes alatoires, ou si lalgorithme doit tre utilis un e e e e e grand nombre de fois, on a plutt recours ` des mthodes plus sophistiques. Nous en o a e e prsentons deux ici : le tri fusion et le tri rapide. Tous deux sont de nature rcursive et e e suivent lapproche diviser pour rgner. e

2.3.1

Le tri fusion

Cet algorithme repose sur le fait que fusionner deux tableaux tris est plus rapide e que de trier un grand tableau directement. Supposons que lalgorithme prenne en entre e un tableau de n lments dans un ordre quelconque. Lalgorithme commence par diviser ee le tableau des n lments en deux sous-tableaux de n lments chacun (tape diviser). ee ee e 2 1 Les deux sous-tableaux sont tris de mani`re rcursive en utilisant toujours le tri fusion e e e (rgner) ; ils sont ensuite fusionns pour produire le tableau tri (combiner). e e e 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 procdure fusion (la fonction merge dcrite ci-dessous) commence par recopier les e e deux sous-tableaux tris tab[p]...tab[q-1] et tab[q]...tab[r-1] dos-`-dos 2 dans un e a tableau auxiliaire tmp. Lintrt de cette faon de recopier est que lon na alors pas besoin ee c de rajouter de tests de n de sous-tableaux, ni de case supplmentaire contenant le symbole e par exemple ` chacun des sous-tableaux. Ensuite, merge remet dans tab les lments a ee du tableau tmp tri, mais ici le tri a une complexit linaire ((n)) puisque tmp provient e e e
1. La rcursion sarrte lorsque les sous-tableaux sont de taille 1, donc trivialement tris. e e e 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 dj` tris ; le procd est alors le suivant : on part de k = p, i = p ea e e e 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 incrmente i, sinon, on dcrmente j ; dans tous les cas, on e e e incrmente k. On continue jusqu` ce que k vaille r. e a

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 dbut de chaque itration de la boucle pour k, les k p lments du souse e ee tableau tab[p]...tab[k-1] sont tris. Pour trier le tableau tab de taille n, on appelle e merge sort(tab,0,n). Note sur la Reallocation de Memoire Lalgorithme merge sort tel quil est crit ne g`re pas bien sa mmoire. En eet, e e e chaque appel ` la fonction merge commence par allouer un tableau tmp de taille r a p, et les oprations dallocation mmoire sont des opration relativement coteuses e e e u (longues ` excuter en pratique). Allouer une mmoire de taille n a une complexit a e e e (n), et rallouer de la mmoire ` chaque appel de merge ne change donc pas la e e a complexit de lalgorithme. En revanche, en pratique cela ralentit beaucoup lexcution e e 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 supplmentaire (celle que lutilisateur va appeler en pratique), qui alloue e de la mmoire avant dappeler la fonction rcursive merge sort. e e

16

F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 2. Rcursivit e e

Complexit du tri fusion. Evaluons ` prsent la complexit en temps de lalgorithme e a e e e merge sort, en termes de nombre de comparaisons de clefs. On note T (n), cette complexit pour une entre de taille n. e La phase de division ne ncessite aucune comparaison. La phase de r`gne requiert deux e e fois le temps de lalgorithme merge sort sur un tableau de taille deux fois moindre, i.e. e e 2 T ( n ). La phase de recombinaison est la procdure merge. Lorsquappele avec les 2 param`tres (p, q, r), elle ncessite r p comparaisons. On a donc : e e 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 rcurrence, on construit un arbre de rcursivit : cest un arbre binaire dont e e e chaque nud reprsente le cot dun sous-probl`me individuel, invoqu ` un moment de e u e ea la rcursion. Le noeud racine contient le cot de la phase de division+recombinaison au e u niveau n de la rcursivit (ici n), et ses deux sous-arbres reprsentent les cots des souse e e u n n probl`mes au niveau 2 . En dveloppant ces cots pour 2 , on obtient deux sous-arbres, e e u et ainsi de suite jusqu` arriver au cot pour les sous-probl`mes de taille 1. Partant de a u e 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 T (n), il sut ` prsent dadditionner les cots de e a e u chaque noeud. On proc`de en calculant le cot total par niveau : le niveau de profondeur 0 e u (racine) a un cot total gal ` n, le niveau de profondeur 1 a un cot total gal ` n + n ,... u e a u e a 2 2 n i le niveau de profondeur i pour 0 i k 1 a un cot total de 2 2i = n (ce niveau u comporte 2i nuds). Le dernier niveau correspond aux 2k sous-probl`mes de taille 1 ; aucun e ne contribue ` la complexit en termes de nombre de comparaisons, donc le cot au niveau a e u k est nul. Ainsi, chaque niveau, sauf le dernier, contribue pour n ` la complexit totale. Il a e en rsulte que T (n) = k n = n log(n). e Lorsque n nest pas ncessairement une puissance de 2, on encadre n entre deux e puissances de 2 conscutives : 2k n < 2k+1 . La fonction T (n) tant croissante, on a e e k k+1 k k+1 T (2 ) T (n) T (2 ), soit k2 T (n) (k + 1)2 . Comme log(n) = k, on obtient une complexit en (n log(n)). e Remarques : Le tri fusion ne se fait pas en place 3 : en eet, la procdure merge sort ncessite e e un espace mmoire supplmentaire sous la forme dun tableau (tmp) de taille n. e e Le calcul de complexit prcdent est indpendant de la distribution des entiers ` e e e e a trier : le tri fusion sexcute en (n log(n)) pour toutes les distributions dentiers. La e complexit en moyenne est donc gale ` la complexit dans le pire cas. e e a e

2.3.2

Le tri rapide

Le deuxi`me algorithme de tri que nous tudions suit galement la mthode diviser e e e e pour rgner. Par rapport au tri fusion, il prsente lavantage de trier en place . En e e
3. Un tri se fait en place lorsque la quantit de mmoire supplmentaire - la cas chant - est une e e e e e petit constante (indpendante de la taille de lentre). e e

& M. Finiasz

17

2.3 Algorithmes de tri

ENSTA cours IN101

revanche, son comportement dpend de la distribution de son entre : dans le pire cas, e e il poss`de une complexit quadratique, cependant, ses performances en moyenne sont en e e (n log(n)), et il constitue souvent le meilleur choix en pratique 4 . Le fonctionnement de lalgorithme sur un tableau tab ` trier entre les indices p et r est le suivant : a
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 procdure partition dtermine un indice q tel que, ` lissue de la procdure, pour e e a e 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] ntant pas eux-mmes tris). Elle e e e utilise un lment x = tab[p] appel pivot autour duquel se fera le partitionnement. ee e
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

` A la n de chacune des itrations de la boucle for, le tableau est divis en trois parties : e e i, p i q, tab[i] x i, q + 1 i j 1, tab[i] > x
4. Lordre de grandeur de complexit en termes de nombre de comparaisons, mais aussi de nombre e doprations lmentaires (aectations, incrmentations) est le mme que pour le tri fusion, mais les e ee e e constantes caches dans la notation sont plus petites. e

18

F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 2. Rcursivit e e

Pour j i r, les clefs tab[i] nont pas de lien x avec le pivot x (lments non encore e ee traits). e Si le test en ligne 6 est vrai, alors llment en position j est infrieur ou gal ` x, donc ee e e a on le dcale le plus ` gauche possible (lignes 8 ` 10) ; mais on incrmente dabord q (ligne e a a e ` 7) de faon ` pouvoir insrer cet lment entre tab[p] et tab[q] strictement. A lissue de c a e ee la boucle for sur j, tous les lments de tab[p]...tab[r-1] infrieurs ou gaux ` x ont t ee e e a ee placs ` gauche de tab[q] (et ` droite de tab[p]) ; il ne reste donc plus qu` mettre x ` e a a a a la place de tab[q] (lignes 13 ` 15). a Complexit du tri rapide. La complexit de lalgorithme quick sort dpend du cae e e ract`re quilibr ou non du partitionnement, qui lui-mme dpend de la valeur du pivot e e e e e choisi au dpart. En eet, si x = tab[p] est infrieur ` tous les autres lments du tableau, e e a ee la procdure partition dcoupera le tableau initial en deux sous-tableaux extrmement e e e dsquilibrs : lun de taille 0 et lautre de taille n 1. En particulier si le tableau est dj` ee e ea tri (ou inversement tri), cette conguration surviendra ` chaque appel rcursif. Dans ce e e a e cas, le temps dexcution T (n) de lalgorithme satisfait la rcurrence e e T (n) = T (n 1) + D(n), (la procdure quick sort sur une entre de taille 0 ne ncessitant pas de comparaison) e e e o` D(n) est le cot de la procdure partition sur une entre de taille n. Il est clair que u u e e partition(tab, p, r) ncessite r p 1 comparaisons, donc D(n) = n 1. Larbre rcursif e e correspondant ` T (n) = T (n 1) + n 1 est de profondeur n 1, le niveau de profondeur a i ayant un cot de n (i + 1). En cumulant les cots par niveau, on obtient u u T (n) =
n1 i=0

n (i + 1) =

n1 i=0

i=

n(n 1) . 2

Ainsi, la complexit de lalgorithme dans le pire cas est en (n2 ), i.e. pas meilleure que e celle du tri par insertion. Le cas le plus favorable est celui o` la procdure partition dcoupe toujours le tau e e bleau courant en deux sous-tableaux de taille presque gale ( n et n 1, donc toujours e 2 2 infrieure ou gale ` n ). Dans ce cas, la complexit de lalgorithme est donn par e e a 2 e e T (n) = 2 T ( n ) + n 1. 2 Cette rcurrence est similaire ` celle rencontre lors de ltude du tri fusion. La solution e a e e 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 est aussi celle du cas moyen. Pour e cela, il est ncessaire de faire une hypoth`se sur la distribution des entiers en entre de e e e lalgorithme. On suppose donc quils suivent une distribution alatoire uniforme. Cest e le rsultat de partition qui, ` chaque appel rcursif, conditionne le dcoupage en souse a e e tableaux et donc la complexit. On ne peut pas supposer que partition fournit un indice e q uniformment distribu dans [p, ..., r 1] : en eet, on choisit toujours comme pivot le e e & M. Finiasz 19

2.3 Algorithmes de tri

ENSTA cours IN101

premier lment du sous-tableau courant, donc on ne peut faire dhypoth`se duniformit ee e e 5 quant ` la distribution de cet lment tout au long de lalgorithme . Pour pouvoir faire une a ee hypoth`se raisonnable duniformit sur q, il est ncessaire de modier un tant soit peut e e e partition de faon ` choisir non plus le premier lment comme pivot, mais un lment c a ee ee alatoirement choisi dans le sous-tableau considr. La procdure devient alors : e ee e
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); }

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

e e En eet, n (T (q 1)+T (nq)) reprsente la somme des complexits correspondant aux q=1 n dcoupages possibles du tableau tab[0, ..., n 1] en deux sous-tableaux. La complexit e e moyenne T (n) est donc la moyenne de ces complexits. Par symtrie, on a : e e 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 ` cette galit la mme galit au rang n 1, on obtient une expression a e e e e e 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, mme si les entiers en entre sont uniformment distribus : en eet, la conguration de ces e e e e entiers dans le tableau est modie dun appel de partition ` lautre. e a

20

F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 2. Rcursivit e e

En dveloppant la mme formule pour T (n 1) et en la rinjectant ci-dessus, on obtient : e e e T (n) T (n 2) 2(n 2) 2(n 1) = + + . n+1 n1 (n 1)n n(n + 1) En itrant ce processus : e
n n 2(k 1) 2(k 1) T (n) = , 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 (k 1) 1 1 n dx , 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 en (n log(n)) pour le cas e moyen.

2.3.3

Complexit minimale dun algorithme de tri e

Nous pouvons nous demander si les complexits en (n log(n)) obtenues ci-dessus sont e les meilleures que lon puisse esprer pour un algorithme de tri gnrique (i.e. qui ne fait e e e aucune hypoth`se sur les donnes ` trier). Pour rpondre ` cette question, nous allons e e a e a calculer une borne infrieure sur la complexit (en termes de nombre de comparaisons) e e de tout algorithme de tri par comparaison. Prcisons tout dabord que lon appelle tri e par comparaison un algorithme de tri qui, pour obtenir des informations sur lordre de la squence dentre, utilise seulement des comparaisons. Les algorithmes de tri fusion et e e rapide vus prcdemment sont des tris par comparaison. La borne calcule est valable dans e e e le pire cas (contexte gnral de la thorie de la complexit). e e e e Tout algorithme de tri par comparaison peut tre modlis par un arbre de dcision e e e e (un arbre binaire comme dni dans le chapitre 5). Chaque comparaison que lalgorithme e eectue reprsente un nud de larbre, et en fonction du rsultat de la comparaison, e e lalgorithme peut sengager soit dans le sous-arbre gauche, soit dans le sous-arbre droit. Lalgorithme fait une premi`re comparaison qui est la racine de larbre, puis sengage dans e lun des deux sous-arbres ls et fait une deuxi`me comparaison, et ainsi de suite... Quand e lalgorithme sarrte cest quil a ni de trier lentre : il ne fera plus dautres comparaisons e e et une feuille de larbre est atteinte. Chaque ordre des lments dentre m`ne ` une ee e e a feuille dirente, et le nombre de comparaisons ` eectuer pour atteindre cette feuille est e a la profondeur de la feuille dans larbre. Larbre explorant toutes les comparaisons, il en rsulte que le nombre de ses feuilles pour des entres de taille n est au moins gal ` n!, e e e a cardinal de lensemble de toutes les permutations des n positions. Soit h, la hauteur de larbre. Dans le pire cas, il est ncessaire de faire h comparaisons e pour trier les n lments (autrement dit, la feuille correspondant ` la permutation correcte ee a & M. Finiasz 21

2.4 Rsolution dquations de rcurrence e e e

ENSTA cours IN101

se trouve sur un chemin de longueur 6 h). Le nombre de feuilles dun arbre binaire de hauteur h tant au plus 2h , on a e 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 nonons ce rsultat en : e c e Thor`me 2.3.1. Tout algorithme de tri par comparaison ncessite (n log(n)) compae e e raisons dans le pire cas. Les tris fusion et rapide sont donc des tris par comparaison asymptotiquement optimaux.

2.4

Rsolution dquations de rcurrence e e e

Lanalyse des performances dun algorithme donne en gnral des quations o` le temps e e e u de calcul, pour une taille des donnes, est exprim en fonction du temps de calcul pour e e des donnes de taille moindre. Il nest pas toujours possible de rsoudre ces quations. e e e Dans cette section, nous donnons quelques techniques permettant de trouver des solutions exactes ou approches de rcurrences intervenant classiquement dans les calculs de cot. e e u

2.4.1

Rcurrences linaires e e

Commenons par rappeler la mthode de rsolution des rcurrences linaires homog`nes c e e e e e ` coecients constants. Il sagit dquations du type : a e un+h = ah1 un+h1 + . . . + a0 un , a0 = 0, h N , n N. Cette suite est enti`rement dtermine par ses h premi`res valeurs u0 , . . . , uh1 (la suite e e e e est dordre h). Son polynme caractristique est : o e G(x) = xh ah1 xh1 . . . a1 x a0 . Soit 1 , . . . , r , ses racines, et soit ni , la multiplicit de i , 1 i r. En considrant la e e n srie gnratrice formelle associe ` un , soit U (X) = n=0 un X , et en multipliant cette e e e e a
6. Toutes les feuilles ne se trouvant pas sur le dernier niveau.

22

F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 2. Rcursivit e e

srie par le polynme B(X) = X h G(1/X) (polynme rciproque de G(X)), on obtient un e o o e polynme A(X) de degr au plus h 1. Lexpression : o e U (X) = A(X) B(X)

montre alors que U (X) est en fait une srie rationnelle, i.e. une fraction rationnelle. Sa e dcomposition en lments simples donne lexpression suivante 7 pour le terme gnral de e ee e e la suite : r n un = Pi (n)i ,
i=1

o` Pi (n) est un polynme de degr au plus ni . Une expression de cette forme pour un est u o e appele polynme exponentiel. e o La suite de Fibonacci Fn = Fn1 + Fn2 , avec u0 = 0 et u1 = 1, est un exemple de 1 n n ), o` = 1+ 5 . La suite u telle rcurrence, trait dans le chapitre 1. On a Fn = 5 ( e e 2 de Fibonacci intervient dans le calcul de cot de nombreux algorithmes. Considrons par u e 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 scrit x % y */ e return euclide(y,x % y); } }

Notons n(x, y), le nombre de divisions avec reste eectues par lalgorithme. Alors n(x, y) = e 0 si y = 0, et n(x, y) = 1 + n(y, x mod y) sinon. Pour valuer le cot de lalgorithme (en e u 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 ` prsent k 2, a e et la proprit ci-dessus vraie pour tout j k 1, i.e. si n(u, v) = j, alors u Fj+2 , pour ee u, v N, u v > 0. Supposons n(x, y) = k, et considrons les divisions euclidiennes : e 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 mme, z Fk et par suite Fk+1 + Fk = Fk+2 . e x 1 n 1, n N, on a Fn (n 1). Do` 5x + 1 k+2 . Donc, Dautre part, comme u 5 pour x > y 0, n(x, y) log ( 5x + 1) 2,
e 7. Le dtail des calculs peut tre trouv dans Elments dalgorithmique, de Beauquier, Berstel, e e e Chrtienne, d. Masson. e e

& M. Finiasz

23

2.4 Rsolution dquations de rcurrence e e e

ENSTA cours IN101

autrement dit, n(x, y) = O(log(x)). Il existe de nombreux autres types dquations de rcurrence. Parmi eux, les rcurrences e e e linaires avec second membre constituent une classe importante de telles rcurrences ; elles e e peuvent tre rsolues par une technique similaire ` celle prsente pour les quations hoe e a e e e mog`nes. Des mthodes adaptes existent pour la plupart des types de rcurrences rene e e e contres ; une prsentation de ces direntes techniques peut tre trouve dans le livre e e e e e ements dalgorithmique de Berstel et al. Nous allons dans la suite nous concentrer sur les El quations de rcurrence que lon rencontre typiquement dans les algorithmes diviser pour e e rgner. e

2.4.2

Rcurrences de partitions e
(2.1)

Nous considrons ici des relations de rcurrence de la forme : e e T (1) = d T (n) = aT ( n ) + f (n), n > 1. b

o` a R+ , b N, b 2. u 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`te par n ou n . e b b b La rcurrence (2.1) correspond au cot du calcul eectu par un algorithme rcursif e u e e du type diviser pour rgner, dans lequel on remplace le probl`me de taille n par a souse e probl`mes, chacun de taille n . Le temps de calcul T (n) est donc aT ( n ) auquel il faut e b b ajouter le temps f (n) ncessaire ` la combinaison des solutions des probl`mes partiels en e a e une solution du probl`me total. En gnral, on value une borne suprieure sur le cot (n) e e e e e u de lalgorithme, ` savoir (1) = d, et (n) a ( n ) + f (n), n > 1. On a donc (n) T (n) a b pour tout n, et donc la fonction T constitue une majoration du cot de lalgorithme. u Lors de lanalyse de la complexit du tri fusion, nous avons vu une mthode de rsolution e e e dquations de type (2.1), dans le cas o` a = b = 2 : nous avons construit un arbre e u binaire dont les nuds reprsentent des cots, tel que la somme des nuds du niveau de e u n profondeur i de larbre correspond au cot total des 2i sous-probl`mes de taille 2i . Pour a u e et b quelconques, larbre construit est a-aire, de hauteur logb (n), le niveau de profondeur i n correspondant au cot des ai sous-probl`mes, chacun de taille bi , 0 i logb (n). Comme u e pour le tri fusion, laddition des cots ` chaque niveau de larbre donne le cot total T (n) u a u pour une donne de taille n. Cette mthode est appele mthode de larbre rcursif. Elle e e e e e donne lieu ` une expression gnrique pour T (n) en fonction de n. a e e Lemme 2.4.1. Soit f : N R+ , une fonction dnie sur les puissances exactes de b, et e + soit T : N R , la fonction dnie par e T (1) = d T (n) = aT ( n ) + f (n), n = bp , p N , b o` b 2 est entier, a > 0 et d > 0 sont rels. Alors u e
logb (n)1

(2.2)

T (n) = (n 24

logb (a)

)+

i=0

n ai f ( b i )

(2.3) F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 2. Rcursivit e e

Preuve. Lexpression ci-dessus provient exactement de laddition des cots ` chaque u a niveau de larbre rcursif correspondant ` la rcurrence (2.2) : larbre est a-aire, de hauteur e a e p = logb (n) ; le niveau de profondeur i correspond au cot des ai sous-probl`mes, chacun u e n n i de taille bi ; le cot total du niveau i, 0 i logb (n) 1, est donc a f ( bi ). Le dernier u niveau (i = logb (n)) a un cot de ap d. Ainsi : u T (n) = a d + On a : ap = alogb (n) = blogb (n)logb (a) = (bplogb (a) ) = (nlogb (a) ), do` ap d = (nlogb (a) ). u Dans le cas o` f (n) = nk , le temps de calcul est donn par le thor`me suivant. u e e e Thor`me 2.4.1. Soit T : N R+ , une fonction croissante, telle quil existe des entiers e e b 2, et des rels a > 0, c > 0, d > 0, k 0, pour lesquels e 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 ( bi )

(2.5)

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

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

Si Si

a bk a bk

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

& M. Finiasz

25

2.4 Rsolution dquations de rcurrence e e e avec =


1 . a/bk 1

ENSTA cours IN101

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

donc et cnk (n) cap = (nlogb (a) ) do` T (n) = (nlogb (a) ). u 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` T (n) = (g(n)). u Remarques : 1. On compare k ` logb (a). Le comportement asymptotique de T (n) suit nmax(k,logb (a)) , a sauf pour k = logb (a), auquel cas on multiplie la complexit par un facteur correce tif logb (n). 2. Ce thor`me couvre les rcurrences du type (2.1), avec f (n) = cnk , c > 0, k 0. e e e k Si f (n) = O(n ), le thor`me reste valable en remplaant les par des O. Mais e e c 8 la borne obtenue peut alors ne pas tre aussi ne que celle obtenue en appliquant e directement la formule (2.3) (i.e. la mthode de larbre rcursif). e e Par exemple, si lon a une rcurrence de la forme : e T (1) = 0 T (n) = 2T (n/2) + nlog(n), n = 2p , p N , la fonction f (n) = n log(n) vrie f (n) = O(n3/2 ). Le thor`me prcdent donne T (n) = e e e e e 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


p1 p1 i

log(n)(log(n) 1) . 2 Ainsi, T (n) = (n(log(n))2 ). On obtient le mme rsultat lorsque n nest pas une puissance e e de 2 en encadrant n entre deux puissances conscutives de 2. e T (n) = n(log(n))2 En revanche, la rcurrence e T (1) = 1 T (n) = 2T (n/4) + n2 n, n = 4p , p N ,
8. Elle dpend de la nesse de lapproximation de f comme O(nk ). e

(2.6)

26

F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 2. Rcursivit e e

est bien adapte au thor`me prcdent, avec a = 2, b = 4, c = 1, k = 3/2. On a a < bk = 8 e e e e e 3/2 donc T (n) = (n ). Enn, de faon plus gnrale, nous pouvons noncer le thor`me suivant. c e e e e e Thor`me 2.4.2. Soit T : N R+ , une fonction croissante telle quil existe des entiers e e b 2, des rels a > 0, d > 0, et une fonction f : N R+ pour lesquels e 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 rels c > 0, k 0 et q. Alors : e (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) ) + Soit T (n) = 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

T (n) =

pi

f (b ) = c
bk

p i=1

pi ik

do` 9 , en notant (n) = u

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`de comme dans la preuve du thor`me e e e prcdent. e e
9. cf. preuve du lemme 2.4.1.

& M. Finiasz

27

2.5

Complments e Complments e Rcursivit terminale e e

ENSTA cours IN101

2.5
2.5.1

La rcursivit terminale (tail recursivity en anglais) est un cas particulier de rcursivit : e e e e il sagit du cas o` un algorithme rcursif eectue son appel rcursif comme toute derni`re u e e e instruction. Cest le cas par exemple de lalgorithme dEuclide vu page 23 : lappel rcursif e se fait dans le return, et aucune opration nest eectue sur le rsultat retourn par e e e e lappel rcursif, il est juste pass au niveau du dessus. Dans ce cas, le compilateur a la e e possibilit doptimiser lappel rcursif : au lieu deectuer lappel rcursif, rcuprer le e e e e e rsultat ` ladresse de retour quil a x pour lappel rcursif, et recopier le rsultat ` e a e e e a sa propre adresse de retour, lalgorithme peut directement choisir de redonner sa propre adresse de retour ` lappel rcursif. Cette optimisation prsente lavantage de ne pas avoir a e e une pile rcapitulant les appels rcursifs aux direntes sous-fonctions : lutilisation de e e e rcursion terminale supprime toute limite sur le nombre dappels rcursifs imbriqus. e e e Bien sr, pour que la rcursion terminale prsente un intrt, il faut quelle soit gre u e e ee ee par le compilateur. Cest le cas par exemple du compilateur Caml, ou (pour les cas simples de rcursivit terminale) de gcc quand on utilise les options doptimisation -O2 ou -O3. e e Attention ! Si lappel rcursif nest pas la seule instruction dans le return, il devient impossible e 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 rcursivit terminale. e e

2.5.2

Drcursication dun programme e e

Il est toujours possible de supprimer la rcursion dun programme an den obtenir une e version itrative. Cest en fait ce que fait un compilateur lorsquil traduit un programme e rcursif en langage machine. En eet, pour tout appel de procdure, un compilateur ene e gendre une srie gnrique dinstructions : placer les valeurs des variables locales et ladresse e e e de la prochaine instruction sur la pile, dnir les valeurs des param`tres de la procdure e e e et aller au dbut de celle-ci ; et de mme ` la n dune procdure : dpiler ladresse de e e a e e retour et les valeurs des variables locales, mettre ` jour les variables et aller ` ladresse de a a retour. Lorsque lon veut drcursier un programme, la technique employe par le ee e compilateur est la technique la plus gnrique. Cependant, dans certains cas, il est possible e e de faire plus simple, mme si lutilisation dune pile est presque toujours ncessaire. Nous e e donnons ici deux exemples de drcursication, pour lalgorithme dEuclide, et pour un ee parcours darbre binaire (cf. chapitre 5). Drcursication de lalgorithme dEuclide. Lcriture la plus simple de lalgoe e e rithme dEuclide est rcursive : e 28 F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 2. Rcursivit e 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 scrit x % y */ e return euclide(y,x % y); } }

Toutefois, comme nous lavons vu dans la section prcdente, il sagit ici de rcursion e e e terminale. Dans ce cas, de la mme faon que le compilateur arrive ` se passer de pile, e c a nous pouvons aussi nous passer dune pile, et rcrire le programme avec une simple boucle e while. On conserve la mme condition de terminaison (sauf que dans la boucle while il e 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 ` chaque tour de la boucle. Cela a donne la version itrative de lalgorithme dEuclide : e
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 ncessaire pour e "changer" les valeurs x et y e */ tmp = x; x = y; y = tmp % y; } return x; }

Drcursication dun parcours darbre. Prenons lalgorithme rcursif de parcours e e e prxe darbre binaire suivant : e
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 ` un nud de larbre. Cette structure contient a les deux ls du nud left et right et toute autre donne que peut avoir ` contenir le e a nud. La fonction explore est la fonction que lon veut appliquer ` tous les nuds de a larbre (cela peut-tre une comparaison dans le cas dune recherche dans larbre). e & M. Finiasz 29

2.5

Complments e

ENSTA cours IN101

Ici nous sommes en prsence dune rcursion double, il est donc ncessaire dutiliser e e e une pile pour grer lensemble des appels rcursifs imbriqus. On suppose donc quune e e e pile est implmente (cf. section 3.3.1) et que les fonction push et pop nous permettent e e respectivement dajouter ou de rcuprer un lment dans cette pile. On suppose que la e e ee fonction stack is empty renvoie 1 quand la pile est vide, 0 autrement. Ce qui va rendre cette drcursication plus facile que le cas gnral est quici les dirents appels ` la ee e e e a fonction sont indpendants les uns des autres : la fonction explore ne renvoie rien, et lon e nutilise pas son rsultat pour modier la faon dont le parcours va se passer. On obtient e c alors le code itratif suivant : e
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, et que tous ses e ls le soient aussi. Il sut donc de mettre la racine sur la pile au dpart, et ensuite, tant e que la pile nest pas vide dexplorer les nuds qui sont dessus et ` chaque fois dajouter a leurs ls. Il faut faire attention ` ajouter les ls dans lordre inverse des appels rcursifs a e pour que le parcours se fasse bien dans le mme ordre. En eet, le dernier nud ajout sur e e la pile sera le premier explor ensuite. e

2.5.3

Indcidabilit de la terminaison e e

Un exemple de preuve de terminaison. Comme nous lavons vu, dans un algorithme rcursif, il est indispensable de vrier que les conditions de terminaison seront atteintes e e pour tous les appels rcursifs, et cela quelle que soit lentre. La faon la plus simple de e e c prouver que cest le cas est de dnir une distance aux conditions de terminaison et e de montrer que cette distance est strictement dcroissante lors de chaque appel rcursif 10 . e e Prenons par exemple le cas dun calcul rcursif de coecients binomiaux (bas sur la e e construction du triangle de Pascal) :
10. Dans le cas ou cette distance est discr`te (si par exemple cette distance est toujours enti`re), une e e dcroissance stricte est susante, mais ce nest pas le cas si la distance est continue (ce qui narrive jamais e en informatique !).

30

F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 2. Rcursivit e 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 sarrte quand lun des deux bords e 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 dcro e t strictement ` chaque appel rcursif. Cela prouve donc que cet algorithme termine. a e Notons toutefois que cette faon de calculer les coecients binomiaux est tr`s mauvaise, c e il est bien plus rapide dutiliser un algorithme itratif faisant le calcul du dveloppement e e en factoriels du coecient binomial.

Un exemple dalgorithme sans preuve de terminaison. Jusqu` prsent, tous les a e algorithmes rcursifs que lon a vu terminent, et on peut de plus prouver quils terminent. e Cependant dans certains cas, aucune preuve nest connue pour dire que lalgorithme termine. Dans ce cas on parle dindcidabilit de la terminaison : pour une entre donne, on e e e e ne peut pas savoir si lalgorithme va terminer, et la seule faon de savoir sil termine est c dexcuter lalgorithme sur lentre en question, puis dattendre quil termine (ce qui peut e e bien sr ne jamais arriver). Regardons par exemple lalgorithme suivant : u

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` atteindre 1 a lalgorithme renvoie le nombre dtapes ncessaires avant datteindre 1 e e 31

& M. Finiasz

2.5

Complments e

ENSTA cours IN101

La conjecture de Collatz (aussi appele conjecture de Syracuse 11 ) dit que cet algorithme e termine toujours. Cependant, ce nest quune conjecture, et aucune preuve nest connue. Nous somme donc dans un contexte o`, pour un entier donn, la seule faon de savoir si lalu e c gorithme termine est dexcuter lalgorithme et dattendre : la terminaison est indcidable. e e Un exemple dalgorithme pour lequel on peut prouver que la terminaison est indcidable. Maintenant nous arrivons dans des concepts un peu plus abstraits : avec la e 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 entres, mais dcider a priori sil termine ou e e non pour une entre donne est impossible. La seule faon de savoir si lalgorithme termine e e c est de lexcuter et dattendre, sachant que pour certaines entres lattente sera innie. e e Lalgorithme en question est un prouveur automatique pour des propositions logiques. Il prend en entre un proposition A et cherche ` prouver soit A, soit non-A. Il commence e a par explorer toutes les preuves de longueur 1, puis celles de longueur 2, puis 3 et ainsi de suite. Si la proposition A est dcidable, cest-`-dire que A ou non-A admet une preuve, e a cette preuve est de longueur nie, et donc notre algorithme va la trouver et terminer. En revanche, nous savons que certaines propositions sont indcidables : cest ce quarme e le thor`me dincompltude de Gdel (cf. http://en.wikipedia.org/wiki/Kurt_Godel, e e e o suivre le lien Gdels incompleteness theorems). Donc nous savons que cet algorithme peut o ne pas terminer, et nous ne pouvons pas dcider sil terminera pour une entre donne. e e e Nous pouvons donc prouver que la terminaison de cet algorithme est indcidable, e contrairement ` lexemple prcdent ou la terminaison tait indcidable uniquement parce a e e e e quon ne pouvait pas prouver la terminaison. Le probl`me de larrt de Turing. D`s 1936, Alan Turing sest intress au probl`me e e e e e e de la terminaison dun algorithme, quil a formul sous la forme du probl`me de larrt e e e (halting problem en anglais) : Etant donne la description dun programme et une entre de taille nie, dcider e e e si le programme va nir ou va sexcuter indniment pour cette entre. e e e Il a prouv quaucun algorithme gnrique ne peut rsoudre ce probl`me pour tous les e e e e e couples programme/entre possibles. Cela signie en particulier quil existe des couples e programme/entre pour lesquels le probl`me de larrt est indcidable. e e e e La preuve de Turing est assez simple et fonctionne par labsurde. Supposons que lon e e ait un algorithme halt or not(A,i) prenant en entre un algorithme A et une entre i et qui retourne vrai si A(i) termine, et faux si A(i) ne termine pas. On cre alors lalgorithme e suivant :
11. Pour plus de dtails sur cette conjecture, allez voir la page http://fr.wikipedia.org/wiki/ e e Conjecture_de_Collatz, ou la version anglaise un peu plus compl`te http://en.wikipedia.org/wiki/ Collatz_conjecture.

32

F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 2. Rcursivit e 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 mme, e si lappel ` halt or not(halt err,halt err) renvoie vrai, halt err(halt err) va toura ner indniment, ce qui signie que halt or not(halt err,halt err) aurait du renvoyer e faux. Nous avons donc une contradiction : un programme ne peut donc pas rsoudre le e probl`me de larrt pour tous les couples programme/entre. e e e

& M. Finiasz

33

Chapitre 3 Structures de Donnes e


Linformatique a rvolutionn le monde moderne grce ` sa capacit ` traiter de grandes e e a a ea quantits de donnes, des quantits beaucoup trop grandes pour tre traites ` la main. Cee e e e e a pendant, pour pouvoir manipuler ecacement des donnes de grande taille il est en gnral e e e ncessaire de bien les structurer : tableaux, listes, piles, arbres, tas, graphes... Une multie tude de structures de donnes existent, et une multitude de variantes de chaque structure, e chaque fois mieux adapte ` un algorithme en particulier. Dans ce chapitre nous voyons e a les principales structures de donnes ncessaires pour optimiser vos premiers algorithmes. e e

3.1

Tableaux

Les tableaux sont la structure de donne la plus simple. Ils sont en gnral implments e e e e e nativement dans la majorit des langages de programmation et sont donc simples ` utiliser. e a Un tableau reprsente une zone de mmoire conscutive dun seul bloc (cf. Figure 3.1) e e e ce qui prsente ` la fois des avantages et des inconvnients : e a e la mmoire est en un seul bloc conscutif avec des lments de taille constante ` e e ee a lintrieur (la taille de chaque lment est dni par le type du tableau), donc il est e ee e e tr`s facile daccder au i`me lment du tableau. Linstruction tab[i] se contente de e e ee prendre ladresse mmoire sur laquelle pointe tab et dy ajouter i fois la taille dun e lment. ee il faut xer la taille du tableau avant de commencer ` lutiliser. Les syst`mes dexa e ploitation modernes gardent une trace des processus auxquels les direntes zones de e mmoire appartiennent : si un processus va crire dans une zone mmoire qui ne lui e e e appartient pas (une zone que le noyau ne lui a pas allou) il y a une erreur de segmene tation. Quand vous programmez, avant dcrire (ou de lire) ` la case i dun tableau e a il est ncessaire de vrier que i est plus petit que la taille alloue au tableau (car le e e e compilateur ne le vriera pas pour vous). e 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 Reprsentation en mmoire dun tableau simple. e e

3.1.1

Allocation mmoire dun tableau e

Il existe deux faons dallouer de la mmoire ` un tableau. c e a 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 mme int tab2[4][4]; va allouer e un tableau ` deux dimensions de taille 4 4. En mmoire ce tableau bidimensionnel a e peut ressemblera ` ce quon voit dans la Figure 3.2. Attention, un tableau allou a e statiquement ne se trouve pas dans la mme zone mmoire quun tableau allou avec e e e lune des mthodes dallocation dynamique : ici il se trouve dans la mme zone que e e toutes les variables de type int . On appelle cela de lallocation statique car on ne peut pas modier la taille du tableau en cours dexcution. e la deuxi`me faon utilise soit la commande new (syntaxe C++), soit la commande e c malloc (syntaxe C) et permet une allocation dynamique (dont la taille dpend des e entres par exemple). Lallocation du tableau scrit alors int* tab = new int [100]; e e ou int* tab = (int* ) malloc(100*sizeof(int ));. En revanche cette technique ne permet pas dallouer directement un tableau ` deux dimensions. Il faut pour cela eeca tuer une boucle qui scrit alors : e

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

Anne 2010-2011 e
Mmoire
tab

Chapitre 3. Structures de Donnes e


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 Reprsentation en mmoire dun tableau ` deux dimensions. e e a Lutilisation de malloc (ou new en C++) est donc beaucoup plus lourde, dautant plus que la mmoire alloue ne sera pas libre delle mme et lutilisation de la commande free e e ee e (ou delete en C++) sera ncessaire, mais prsente deux avantages : e e le code est beaucoup plus proche de ce qui se passe rellement en mmoire (surtout e e dans le cas ` deux dimensions) ce qui permet de mieux se rendre compte des oprations a e rellement eectues. En particulier, une commande qui para simple comme int e e t tab[1000][1000][1000]; demande en ralit dallouer un million de fois 1000 entiers, e e ce qui est tr`s long et occupe 4Go de mmoire. e e cela laisse beaucoup plus de souplesse pour les allocations en deux dimensions (ou plus) : contrairement ` la notation simplie, rien noblige ` avoir des tableaux carrs ! a e a e Pour les cas simples, la notations [100] est donc susante, mais d`s que cela devient e compliqu, lutilisation de malloc devient ncessaire. e e Exemple dallocation non carre. Le programme suivant permet de calculer tous les e coecients binomiaux de faon rcursive en utilisant la construction du triangle de Pascal. c e La mthode rcursive simple vue au chapitre prcdent (cf. page 30) est tr`s inecace car e e e e e elle recalcule un grand nombre de fois les mme coecients. Pour lamliorer, on utilise e e 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 intressant est que le tableau est triangulaire, et lutilisation de malloc e (ou new) permet de ne pas allouer plus de mmoire que ncessaire (on gagne un facteur 2 e e sur loccupation mmoire ici). e
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); } /* insrer ici les instructions qui e 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 mme quavant, mis ` part quelle vrie dabord dans le cache si la e a e valeur a dj` t calcule, et quelle stocke la valeur avant de la retourner. On arrte ici eaee e e le calcul de binomiaux ` n = 33 car au-del` les coecients binomiaux sont trop grands a a pour tenir dans un int de 32 bits. Notez ici lutilisation de calloc qui alloue la mmoire e et linitialise ` 0, contrairement ` malloc qui laisse la mmoire non initialise. La fonction a a e e calloc est donc plus lente, mais linitialisation est ncessaire pour pouvoir utiliser la e technique de mise en cache. Cette technique de programmation dynamique est tr`s utilise e e quand lon veut programmer vite et ecacement un algorithme qui se dcrit mieux de e faon rcursive quitrative. Cela sera souvent le cas dans des probl`mes combinatoires ou c e e e de dnombrement. e 38 F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 3. Structures de Donnes e

Liberation de la Memoire Allouee Notez la prsence des free ` la n de la fonction main : ces free ne sont pas ncessaire e a e si le programme est termin ` cet endroit l` car la mmoire est de toute faon libre ea a e c e e par le syst`me dexploitation quand le processus sarrte, mais si dautres instructions e e doivent suivre, la mmoire alloue sera dj` libre. De plus, il est important de prendre e e ea e e lhabitude de toujours librer de la mmoire alloue (` chaque malloc doit correspondre e e e a un free) car cela permet de localiser plus facilement une fuite de mmoire (de la e mmoire alloue mais non libre, typiquement dans une boucle) lors du debuggage. e e e e

3.1.2

Complment : allocation dynamique de tableau e

Nous avons vu que la structure de tableau a une taille xe avant lutilisation : il faut e toujours allouer la mmoire en premier. Cela rend cette structure relativement peu adapte e e aux algorithmes qui ajoutent dynamiquement des donnes sans que lon puisse borner e ` lavance la quantit quil faudra ajouter. Pourtant, les tableaux prsentent lavantage a e e `me e davoir un acc`s instantan ` la i e ea case, ce qui peut tre tr`s utile dans certains cas. On e e a alors envie davoir des tableaux de taille variable. Cest ce quimplmente par exemple la e classe Vector en java (sauf que la classe Vector le fait mal...). Lide de base est assez simple : on veut deux fonctions insert et get(i) qui permettent e e dajouter un lment ` la n du tableau et de lire le i`me lment en temps constant (en ee a ee O(1) en moyenne). Il sut donc de garder en plus du tableau tab deux entiers qui indiquent le nombre dlments dans le tableau, et la taille totale du tableau (lespace allou). Lire le ee e e i`me lment peut se faire directement avec tab[i] (on peut aussi implmenter une fonction ee e get(i) qui vrie en plus que lon ne va pas lire au-del` de la n du tableau). En revanche, e a ajouter un lment est plus compliqu : ee e soit le nombre dlments dans le tableau est strictement plus petit que la taille totale ee et il sut dincrmenter le nombre dlments et dinsrer le nouvel lment, e ee e ee soit le tableau est dj` rempli et il faut rallouer de la mmoire. Si on veut conserver ea e e un acc`s en temps constant il est ncessaire de conserver un tableau dun seul bloc, il e e faut donc allouer un nouvel espace mmoire, plus grand que le prcdent, y recopier le e e e contenu de tab, librer la mmoire occupe par tab, mettre ` jour tab pour pointer e e e a vers le nouvel espace mmoire et on est alors ramen au cas simple vu prcdemment. e e e e Cette technique marche bien, mais pose un probl`me : recopier le contenu de tab est e une opration longue et son cot dpend de la taille totale de tab. Recopier un tableau e u e de taille n a une complexit de (n). Heureusement, cette opration nest pas eectue ` e e e 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 ` linitialisation de choisir le nombre dlments ` a ee a ajouter au tableau ` chaque fois quil faut le faire grandir. Crer un Vector de taille n en a e augmentant de t ` chaque fois va donc ncessiter de recopier le contenu du tableau une a e & M. Finiasz 39

3.2 Listes cha ees n


Mmoire
L data data

ENSTA cours IN101

data data

NULL

Figure 3.3 Reprsentation en mmoire dune liste simplement cha ee. e e n fois toute les t insertions. La complexit pour insrer n lments est donc : e e ee K =n+
t
n

tit

n n ( t t

i=1

+ 1) + n = (n2 ). 2

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

K =n+

i=0

2i 2n = (n).

Ce qui donne en moyenne une complexit par insertion de K = (1). Il est donc possible e n de conserver toutes les bonnes proprits des tableaux et dajouter une taille variable sans ee rien perdre sur les complexits asymptotiques. e La rallocation dynamique de tableau a donc un cot assez faible si elle est bien faite, e u mais elle ncessite en revanche dutiliser plus de mmoire que les autres structures de e e donnes : un tableau contient toujours de lespace allou mais non utilis, et la phase de e e e rallocation ncessite dallouer en mme temps le tableau de taille n et celui de taille n . e e e 2

3.2

Listes cha ees n

Contrairement ` un tableau, une liste cha ee nest pas constitue dun seul bloc de a n e mmoire : chaque lment (ou nud) de la liste est allou indpendamment des autres e ee e e et contient dune part des donnes et dautre part un pointeur vers llment suivant (cf. e ee 40 F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 3. Structures de Donnes e

Figure 3.3). Une liste est donc en fait simplement un pointeur vers le premier lment de ee la liste et pour accder ` un autre lment, il sut ensuite de suivre la cha de pointeurs. e a ee ne En gnral le dernier lment de la liste pointe vers NULL, ce qui signie aussi quune liste e e ee vide est simplement un pointeur vers NULL. En C, lutilisation de liste ncessite la cration e e dune structure correspondant ` un nud de la liste. Le type list en lui mme doit ensuite a e tre dni comme un pointeur vers un nud. Cela donne le code suivant : e e

1 2 3 4 5

struct cell { /* insrer ici toutes les donnes que doit contenir un noeud */ e e cell* next; }; typedef cell* list;

3.2.1

Oprations de base sur une liste e

Insertion. Linsertion dun lment ` la suite dun lment donn se fait en deux tapes ee a ee e e illustres sur le dessin suivant : e
Liste initiale
Cration du nud

data

Insertion dans la liste data

data

data

data

data

data

data

On cre le nud en lui donnant le bon nud comme nud suivant. Il sut ensuite e de faire pointer llment apr`s lequel on veut linsrer vers ce nouvel lment. Le dessin ee e e ee reprsente une insertion en milieu de liste, mais en gnral, lajout dun lment ` une liste e e e ee a se fait toujours par le dbut : dans ce cas lopration est la mme, mais le premier lment e e e ee de liste est un simple pointeur (sans champ data).

Suppression. Pour supprimer un nud cest exactement lopration inverse, il sut de e faire attention ` bien sauvegarder un pointeur vers llment que lon va supprimer pour a ee pouvoir librer la mmoire quil utilise. e e & M. Finiasz 41

3.2 Listes cha ees n


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 reprsente une suppression en milieux de liste, mais le cas le plus e courant sera la suppression du premier lment dune liste. ee Parcours. Pour parcourir une liste on utilise un curseur qui pointe sur llment que ee lon est en train de regarder. On initialise le curseur sur le premier lment et on suit les ee pointeurs jusqu` arriver sur NULL. Il est important de ne jamais perdre le pointeur vers a le premier lment de la liste (sinon les lments deviennent dnitivement inaccessibles) : ee ee e cest pour cela que lon utilise une autre variable comme curseur. Voila le code C dune fonction qui recherche une valeur (passe en argument) dans une liste dentiers, et remplace e 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 passe en argument, ce qui cre automatiquement une nouvelle e e variable locale ` la fonction cleaner. Il ntait donc pas ncessaire de crer la variable cur. a e e e Cela ayant de toute faon un cot ngligeable par rapport ` un appel de fonction, il nest c u e a pas gnant de prendre lhabitude de toujours avoir une variable ddie pour le curseur. e e e 42 F. Levy-dit-Vehel

Anne 2010-2011 e
Mmoire
L data

Chapitre 3. Structures de Donnes e

L_end

data

data NULL

NULL

Figure 3.4 Reprsentation en mmoire dune liste doublement cha ee. e e n

3.2.2

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

La structure de liste est une structure de base tr`s souvent utilise pour construire e e des structures plus complexes comme les piles ou les les que nous verrons ` la section a suivante. Selon les cas, un simple pointeur vers llment suivant peut ne pas sure, on ee peut chercher ` avoir directement acc`s au dernier lment... Une multitude de variations a e ee existent et seules les plus courantes sont prsentes ici. e e Listes doublement cha ees. Une liste doublement cha ee (cf. Figure 3.4) poss`de n n e en plus des listes simples un pointeur vers llment prcdent dans la liste. Cela saccomee e e pagne aussi en gnral dune deuxi`me variable L end pointant vers le dernier lment de e e e ee la liste et permet ainsi un parcours dans les deux sens et un ajout simple dlments en n ee de liste. Le seul surcot est la prsence du pointeur en plus qui ajoute quelques oprations u e e de plus ` chaque insertion/suppression et qui occupe un peu despace mmoire. a e Listes circulaires. Les listes circulaires sont des listes cha ees (simplement ou doun blement) dont le dernier lment ne pointe pas vers NULL, mais vers le premier lment ee ee (cf. Figure 3.5). Il ny a donc plus de relle notion de dbut et n de liste, il y a juste e e une position courante indique par un curseur. Le probl`me est quune telle liste ne peux e e jamais tre vide : an de pouvoir grer ce cas particulier il est ncessaire dutiliser ce que e e e lon appelle une sentinelle. Sentinelles. Dans une liste, une sentinelle est un nud particulier qui doit pouvoir tre e reconnaissable en fonction du contenu de son champ data (une mthode classique est e dajouter un entier au champ data qui est non-nul uniquement pour la sentinelle) et qui sert juste ` simplier la programmation de certaines listes mais ne reprsente pas un rel a e e lment de la liste. Une telle sentinelle peut avoir plusieurs usages : ee & M. Finiasz 43

3.3 Piles & Files


Mmoire
cur data

ENSTA cours IN101

data

data data

Figure 3.5 Reprsentation en mmoire dune liste circulaire simplement cha ee. e e n reprsenter une liste circulaire vide : il est possible de dcider quune liste circulaire vide e e sera reprsente en mmoire par une liste circulaire ` un seul lment (qui est donc son e e e a ee propre successeur) qui serait une sentinelle. terminer une liste non-circulaire : il peut tre pratique dajouter une sentinelle ` la n e a de toute liste cha ee pour que lorsque la liste est vide des fonctions comme retourner n le premier lment ou retourner le dernier lment aient toujours quelque chose ee ee ` renvoyer. Dans la plupart des cas on peut leur demander de retourner NULL, mais une a sentinelle peut rendre un programme plus lisible. On peut aussi envisager davoir plusieurs types de sentinelles, par exemple une qui marquerait un dbut de liste et une autre une n de liste. e

3.2.3

Conclusion sur les listes

Par rapport ` un tableau la liste prsente deux principaux avantages : a e il ny a pas de limitation de longueur dune liste (` part la taille de la mmoire) a e il est tr`s facile dinsrer ou de supprimer un lment au milieu de la liste sans pour e e ee autant devoir tout dcaler ou laisser un trou. e En revanche, pour un mme nombre dlments, une liste occupera toujours un peu plus e ee despace mmoire quun tableau car il faut stocker les pointeurs (de plus le syst`me dexe e ploitation conserve aussi des traces de tous les segments de mmoire allous pour pouvoir e e e les dsallouer quand le processus sarrte) et lacc`s au i`me lment de la liste cote en e e e ee u moyenne (n) pour une liste de longueur n.

3.3

Piles & Files

Les piles et les les sont des structures tr`s proches des listes et tr`s utilises en infore e e matique. Ce sont des structures de donnes dynamiques (comme les listes) sur lesquelles e 44 F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 3. Structures de Donnes e

on veut pouvoir excuter deux instructions principales : ajouter un lment et retirer un e ee lment (en le retournant an de pouvoir lire son contenu). La seule dirence entre la pile ee e et la liste est que dans le cas de la pile llment que lon retire est le dernier ` avoir t ee a ee ajout (on parle alors de LIFO comme Last In First Out), et dans le cas de la le on retire e en premier les lment ajouts en premier (cest un FIFO comme First In First Out). Les ee e noms de pile et le ne sont bien sr pas choisis au hasard : on peut penser ` une pile de u a livres sur une table ou ` une le dattente ` la boulangerie qui se comportent de la mme a a e faon. c

3.3.1

Les piles

Les piles sont en gnral implmentes ` laide dune liste simplement cha ee, la seule e e e e a n dirence est que lon utilise beaucoup moins doprations : en gnral on ne cherche jamais e e e e ` aller lire ce qui se trouve au fond dune pile, on ne fait quajouter et retirer des lments. a ee Une pile vide est alors reprsente par une liste vide, ajouter un lment revient ` ajouter e e ee a un lment en tte de liste, et le retirer ` retourner son contenu et ` la supprimer de la ee e a a liste. Voici en C ce que pourrait tre limplmentation dune pile dentiers : e e
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 ` la pile et le retire de la pile. Pour lutilisateur qui nutilise que les fonctions push ea et pop, le fait que la pile est implmente avec une liste L est transparent. e e Exemples dutilisation de piles. Les piles sont une structure de donne tr`s basique (il e e ny a que deux oprations possibles), mais elles sont utilises tr`s souvent en informatique. e e e & M. Finiasz 45

3.3 Piles & Files

ENSTA cours IN101

Depuis le dbut de ce poly nous avons dj` vu deux exemples dutilisation : e ea Les tours de Hano : programmer rellement les tours de Hano ncessite trois piles (une e e pour chaque piquet) contenant des entiers reprsentant les rondelles. Dans ces piles on e peut uniquement ajouter une rondelle (cest ce que fait push) ou retirer la rondelle du dessus (avec pop). Dans la drcursication : lorsquun compilateur drcursie un programme, il utilise ee ee une pile pour stocker les appels rcursifs. De faon plus gnral, tout appel ` une e c e e a fonction se traduit par lajout de plusieurs lments (adresse de retour...) dans la pile ee qui sert ` lexcution du processus. a e Une autre utilisation courante de pile est lvaluation dexpressions mathmatiques. e e La notation polonaise inverse (que lon retrouve sur les calculatrice HP) est parfaitement adapte ` lusage dune pile : chaque nombre est ajout ` la pile et chaque opration e a e a e prend des lments dans la pile et retourne le rsultat sur la pile. Par exemple, lexpression ee e 5 (3 + 4) scrit en notation polonaise inverse 4 3 + 5 ce qui reprsente les oprations : e e e push(4) push(3) push(pop() + pop()) push(5) push(pop() * pop()) ` A la n il reste donc juste 35 sur la pile.

3.3.2

Les les

Les les sont elles aussi en gnrale implmentes ` laide dune liste. En revanche, les e e e e a deux oprations push et pop que lon veut implmenter doivent lune ajouter un lment e e ee en n de liste et lautre retirer un lment en dbut de liste (ainsi les lments sortent bien ee e ee dans le bon ordre : First In, First Out). Il est donc ncessaire davoir un pointeur sur la n e de la liste pour pouvoir facilement (en temps constant) y insrer des lments. Pour cela e ee on utilise donc une liste simplement cha ee, mais avec deux pointeurs : lun sur le dbut n e et lautre sur la n. Si on suppose que L pointe vers le premier lment de la liste et L end ee 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 ` jour le premier et le dernier lment */ a e e if (L == NULL) { L = nw; L_end = nw; } else { L_end->next = nw;

46

F. Levy-dit-Vehel

Anne 2010-2011 e
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

Chapitre 3. Structures de Donnes e

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 utilises partout o` lon a des donnes e u e ` traiter de faon asynchrone avec leur arrive, mais o` lordre de traitement de ces a c e u donnes est important (sinon on prf`re en gnral utiliser une pile qui est plus lg`re e ee e e e e ` implmenter). Cest le cas par exemple pour le routage de paquets rseau : le routeur a e e reoit des paquets qui sont mis dans une le au fur et ` mesure quils arrivent, le routeur c a 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 mmoire tampon et permet ainsi de e mieux utiliser les ressources du routeur : si le trac dentre est irrgulier il sut grce au e e a tampon de pouvoir traiter un ux gal au dbit dentre moyen, alors que sans tampon il e e e faudrait pouvoir traiter un ux gal au dbit maximum en entre. e e e

& M. Finiasz

47

Chapitre 4 Recherche en table


Le probl`me auquel on sintresse dans ce chapitre est celui de la mise en uvre ine e formatique dun dictionnaire ; autrement dit, il sagit dtudier les mthodes permettant de e e retrouver le plus rapidement possible une information en mmoire, accessible ` partir dune e a clef (par exemple, trouver une dnition ` partir dun mot). La mthode la plus naturelle e a e est limplmentation par tableau (table ` adressage direct : pour grer un dictionnaire avec e a e des mots jusqu` 30 lettres on cre une table assez grande pour contenir les 2630 mots a e possibles, et on place les dnitions dans la case correspondant ` chaque mot), mais bien e a que sa complexit temporelle soit optimale, cette mthode devient irraliste en terme de e e e complexit spatiale d`s lors que lon doit grer un grand nombre de clefs. Une deuxi`me e e e e mthode consiste ` ne considrer que les clefs eectivement prsentes en table (recherche e a e e squentielle) : on obtient alors des performances analogues aux oprations de base sur les e e listes chanes, la recherche dun lment tant linaire en la taille de la liste. Une approche e ee e e plus ecace est la recherche dichotomique, dans laquelle on utilise un tableau contenant les clefs tries (comme cest le cas dans un vrai dictionnaire) ; la complexit de lopration de e e e recherche devient alors en log(n), est rend cette mthode tr`s intressante pour de gros e e e chiers auxquels on acc`de souvent mais que lon modie peu. Enn, une quatri`me mthode e e e aborde est lutilisation de tables de hachage, qui permet datteindre en moyenne les perfore mances de ladressage direct tout en saranchissant du nombre potentiellement tr`s grand e de clefs possible.

4.1

Introduction

Un probl`me tr`s frquent en informatique est celui de la recherche de linformation e e e stocke en mmoire. Cette information est en gnral accessible via une clef. La consultae e e e tion dun annuaire lectronique est lexemple type dune telle recherche. Un autre exemple e est celui dun compilateur, qui doit grer une table des symboles, dans laquelle les clefs sont e les identicateurs des donns ` traiter. Les fonctionnalits 1 dun dictionnaire sont exactee a e ment celles quil convient de mettre en oeuvre lorsque lon souhaite grer linformation en e
1. Pour la cration, lutilisation et la mise ` jour. e a

49

4.2 Table ` adressage direct a

ENSTA cours IN101

mmoire. En eet, les oprations que lon dsire eectuer sont : e e e la recherche de linformation correspondant ` une clef donne, a e linsertion dune information ` lendroit spci par la clef, a e e la suppression dune information. ` noter que si la clef correspondant ` une information existe dj`, linsertion est une A a ea modication de linformation. On dispose donc de paires de la forme (clef,information), auxquelles on doit appliquer ces oprations. Pour cela, lutilisation dune structure de e donnes dynamique simpose. e Nous allons ici tudier les structures de donnes permettant limplmentation ecace e e e de dictionnaires. Nous supposerons que les clefs sont des entiers dans lintervalle [0, m 1], o` m est un entier qui peut tre tr`s grand (en gnral il sera souvent tr`s grand). Si les u e e e e e clefs ont des valeurs non enti`res, on peut appliquer une bijection de lensemble des clefs e vers (un sous-ensemble de) [0, m 1]. Nous nous intresserons ` la complexit spatiale e a e et temporelle des trois oprations ci-dessus, complexits dpendantes de la structure de e e e donnes utilise. e e

4.2

Table ` adressage direct a

La mthode la plus naturelle pour rechercher linformation est dutiliser un tableau e 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 opration daectation tab[i] = info) et la suppression (tab[i] = NULL, e avec la convention que NULL correspond ` une case vide). Le stockage du tableau ncessite a e un espace mmoire de taille (m). e En gnral, une table ` adressage direct ne contient pas directement linformation, mais e e a des pointeurs vers linformation. Cela prsente deux avantages : dune part cela permet e davoir une information par case de taille variable et dautre part, la mmoire ` allouer e a 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 dnition, on allouera la mmoire avec type info** tab e e = new type info* [m] pour un tableau avec pointeurs. Par convention tab[i] = NULL si la case est vide (aucune information ne correspond ` la clef i), sinon tab[i] = &info. a Dans le cas o` linformation est de petite taille, lutilisation dun tableau sans pointeurs u peut toutefois tre intressante. Chaque case du tableau doit alors contenir deux champs : e e le premier champ est de type boolen et indique la prsence de la clef (si la clef nest pas e e prsente en table cest que linformation associe nest pas disponible), lautre est de type e e type info et contient linformation proprement dite.
1 2

struct cell { bool key_exists;

50

F. Levy-dit-Vehel

Anne 2010-2011 e
3 4 5

Chapitre 4. Recherche en table

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

Toute ces variantes de structures de donnes conduisent ` la complexit donne ci-dessus. e a e e Cette reprsentation en tableau (ou variantes) est bien adapte lorsque m est petit, e e mais si m est tr`s grand, une complexit spatiale linaire devient rapidement irraliste. e e e e 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 squentielle e

En gnral, lespace des clefs possibles peut tre tr`s grand, mais le nombre de clefs e e e e prsentes dans la table est bien moindre (cest le cas pour les mots de huit lettres). Lide de e e 2 la recherche squentielle est de mettre les donnes (les couples (clef,info)) dans un tableau e e de taille n, o` n est le nombre maximal 3 de clefs susceptibles de se trouver simultanment en u e table. On utilise aussi un indice p indiquant la premi`re case libre du tableau. Les donnes e e sont insres en n de tableau (tab[p] reoit (clef,info) et p est incrment). On eectue ee c e e une recherche en parcourant le tableau squentiellement jusqu` trouver lenregistrement e a correspondant ` la clef cherche, ou arriver ` lindice n de n de tableau (on suppose que le a e a tableau est enti`rement rempli). Si lon suppose que toutes les clefs ont la mme probabilit e e e dtre recherches, le nombre moyen de comparaisons est : e e
n 1 n+1 i= . n 2 i=1

En eet, si llment cherch se trouve en tab[i], on parcourra le tableau jusqu` la ee e a position i (ce qui correspond ` i + 1 comparaisons). Les clefs susceptibles dtre cherches a e e tant supposes quiprobables, i peut prendre toute valeur entre 0 et n 1. En cas de e e e recherche infructueuse, on a ` eectuer n comparaisons. La complexit de lopration de a e e recherche est donc en temps (n). Linsertion dun enregistrement seectue en temps (1), sauf si la clef est dj` prsente ea e en table, auquel cas la modication doit tre prcde dune recherche de cette clef. e e e e Lopration de suppression ncessitant toujours une recherche pralable, elle seectue en e e e temps (n) (il est galement ncessaire de dcaler les lments restant dans le tableau apr`s e e e ee e suppression, ce qui a aussi une complexit (n)). La complexit de stockage du tableau e e est en (n). Si lon dispose dinformation supplmentaires sur les clefs, on peut amliorer la come e plexit des oprations ci-dessus, par exemple en plaant les clefs les plus frquemment lues e e c e en dbut de tableau. e
2. On parle aussi de recherche linaire. e 3. Ou une borne suprieure sur ce nombre si le nombre maximal nest pas connu davance. e

& M. Finiasz

51

4.3 Recherche squentielle e

ENSTA cours IN101

Voici comment simplmente les oprations de recherche, insertion, suppression dans e e 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 ^tre faite dans le main */ e 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 insre deux fois */ e e 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("lment inexistant, suppression impossible.\n"); E e }

On peut galement utiliser une liste cha ee ` la place dun tableau. Un avantage e n a est alors que lon na plus de limitation sur la taille. Les complexits de recherche et de e suppression sont les mmes que dans limplmentation par tableau de taille n. Linsertion e e conserve elle aussi sa complexit de (1) sauf que les nouveaux lments sont insrs au e ee ee dbut au lieu d` la n. La modication ncessite toujours un temps en (n) (le cot dune e a e u recherche). Asymptotiquement les complexits sont les mmes, mais dans la pratique la e e recherche sera un peu plus lente (un parcours de tableau est plus rapide quun parcours 52 F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 4. Recherche en table

de liste) et la suppression un peu plus rapide (on na pas ` dcaler tous les lments du a e ee tableau). Avec une liste, les oprations sont alors exactement les mmes que celles dcrites e e e dans le chapitre sur les listes.

4.4

Recherche dichotomique

Une mthode ecace pour diminuer la complexit de lopration de recherche est dutie e e liser un tableau (ou une liste) de clefs tries par ordre croissant (ou dcroissant). On peut e e alors utiliser lapproche diviser-pour-rgner pour raliser la recherche : on compare e e la clef val cherche ` celle situe au milieu du tableau. Si elles sont gales, on retourne e a e e linformation correspondante. Sinon, si val est suprieure ` cette clef, on recommence la e a recherche dans la partie suprieure du tableau. Si val est infrieure, on explore la partie e e infrieure. On obtient lalgorithme rcursif suivant (on recherche entre les indices p (inclus) e e 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 de lopration de recherche. La complexit est ici calcule en nombre e e e e dappels rcursifs ` dicho search, ou de mani`re quivalente en nombre de comparaisons e a e e de clefs ` eectuer (mme si lalgorithme comporte deux comparaisons, tant que ce nombre a e est constant il va dispara dans le de la complexit). tre e On note n = r p, la taille du tableau et Cn , la complexit cherche. Pour une donne e e e de taille n, on fait une comparaison de clef, puis (si la clef nest pas trouve) on appelle la e procdure rcursivement sur un donne de taille n , donc Cn C n + 1. De plus, C1 = 1. e e e 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 de lopration e e de recherche avec cette reprsentation est donc bien meilleure quavec les reprsentations e e prcdentes. e e Malheureusement ce gain sur la recherche fait augmenter les cot des autres oprations. u e Linsertion dun lment doit maintenant se faire ` la bonne place (et non pas ` la n du ee a a tableau). Si lon consid`re linsertion dun lment choisi alatoirement, on devra dcaler en e ee e e moyenne n/2 lments du tableau pour le placer correctement. Ainsi, lopration dinsertion ee e est en (n). Le cot de lopration de suppression lui ne change pas et ncessite toujours u e e le dcalage den moyenne n/2 lments. e ee En pratique, cette mthode est ` privilgier dans le cas o` lon a un grand nombre de e a e u clef sur lesquels on peut faire de nombreuses requtes, mais que lon modie peu. On peut e alors, dans une phase initiale, construire la table par une mthode de tri du type tri rapide. e Recherche par interpolation. La recherche dichotomique ne modlise pas notre faon e c intuitive de rechercher dans un dictionnaire. En eet, si lon cherche un mot dont la premi`re lettre se trouve ` la n de lalphabet, on ouvrira le dictionnaire plutt vers e a o la n. Une faon de modliser ce comportement est ralise par la recherche par interpoc e e e lation. Elle consiste simplement ` modier lalgorithme prcdent en ne considrant plus a e e e systmatiquement le milieu du tableau, mais plutt une estimation de lendroit o` la clef e o u devrait se trouver dans le tableau. Soit encore val, la clef ` rechercher. Etant donne que a e le tableau est tri par ordre croissant des clefs, la valeur de val donne une indication de e lendroit o` elle se trouve ; cette estimation est donne par : u e q=p+ val tab[p] (r p). tab[r 1] tab[p]

Il sav`re que, dans le cas o` les clefs ont une distribution alatoire, cette amlioration e u e e rduit drastiquement le cot de la recherche, comme le montre le rsultat suivant (que nous e u e admettrons) : Thor`me 4.4.1. La recherche par interpolation sur un ensemble de clefs distribues e e e alatoirement ncessite moins de log(log(n)) + 1 comparaisons, sur un tableau de taille e e n. Attention, ce rsultat tr`s intressant nest valable que si les clef sont rparties unie e e e formment dans lensemble des clefs possibles. Une distribution biaise peut grandement e e dgrader cette complexit. e e 54 F. Levy-dit-Vehel

Anne 2010-2011 e
Mmoire
m tab

Chapitre 4. Recherche en table

key

info

key

info

key key info

info = pointeur vers NULL

Figure 4.1 Reprsentation en mmoire dune table de hachage. La huiti`me case e e e du tableau contient une collision : deux clef qui ont la mme valeur hache. e e

4.5

Tables de hachage

Lorsque lensemble des clefs eectivement stockes est beaucoup plus petit que lespace e de toutes les clefs possibles, on peut sinspirer de ladressage direct - tr`s ecace lorsque e est petit - pour implmenter un dictionnaire. Soit n le nombre de clefs rellement prsentes. e e e Lide est dutiliser une fonction surjective h de dans [0, m 1] (o` on choisit en gnral e u e e m n). Il sut alors davoir une table de taille m (comme prcdemment) et de placer une e e clef c dans la case k = h(c) de cette table. La fonction h est appele fonction de hachage, e la table correspondante est appele table de hachage et lentier k = h(c) la valeur hache e e de la clef c. Les fonctions utilises habituellement sont tr`s rapides, et lon peut considrer e e e que leur temps dexcution est en (1) quelle que soit la taille de lentre. e e Le principal probl`me qui se pose est lexistence de plusieurs clefs possdant la mme e e e valeur hache. Si lon consid`re par exemple deux clefs c1 , c2 , telles que h(c1 ) = h(c2 ), e e le couple (c1 , c2 ) est appel une collision. Etant donn que || m, les collisions sont e e nombreuses. Une solution pour grer les collisions consiste ` utiliser un tableau tab de e a listes (cha ees) de couples (clef,info) : le tableau est indic par les entiers de 0 ` m 1 n e a et, pour chaque indice i, tab[i] est (un pointeur sur) la liste cha ee de tous les couples n (clef,info) tels que h(clef) = i (i.e. la liste de toutes les collisions de valeur hache i). Une e telle table de hachage est reprsente en Figure 4.1. Les oprations de recherche, insertion, e e e suppression sont alors les mmes que lorsque lon implmente une recherche squentielle e e e avec des listes, sauf quici les listes sont beaucoup plus petites. Lopration dinsertion a une complexit en (1) (insertion de (clef,info) au dbut de e e e la liste tab[i] avec i = h(clef)). Pour dterminer la complexit - en nombre dlments ` e e ee a examiner (comparer) - des autres oprations (recherche et suppression), on doit dabord e n conna la taille moyenne des listes. Pour cela, on dnit = m , le facteur de remplissage tre e ` de la table, i.e. le nombre moyen dlments stocks dans une mme liste. A noter que ee e e peut prendre des valeurs arbitrairement grandes, i.e. il ny a pas de limite a priori sur le nombre dlments susceptibles de se trouver en table. ee & M. Finiasz 55

4.5 Tables de hachage

ENSTA cours IN101

Dans le pire cas, les n clefs ont toute la mme valeur hache, et on retombe sur la e e recherche squentielle sur une liste de taille n, en (n) (suppression en (n) galement). e e En revanche, si la fonction de hachage rpartit les n clefs uniformment dans lintervalle e e [0, m 1] - on parle alors de hachage uniforme simple - chaque liste sera de taille en moyenne, et donc la recherche dun lment (comme sa suppression) ncessitera au plus ee e +1 comparaisons. Une estimation pralable de la valeur de n permet alors de dimensionner e la table de faon ` avoir une recherche en temps constant (en choisissant m = O(n)). La c a complexit spatiale de cette mthode est en (m+n), pour stocker la table et les n lments e e ee de liste.

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


par table de hachage dpendent de la mani`re dont la fonction h rpartit (en moyenne) e e e lensemble des clefs ` stocker parmi les m premiers entiers. Nous avons vu que, pour une a complexit moyenne optimale, h doit raliser un hachage uniforme simple. e e En gnral, on ne conna pas la distribution initiale des clefs, et il est donc dicile e e t de vrier si une fonction de hachage poss`de cette proprit. En revanche, plus les clefs e e ee sont rparties uniformment dans lespace des clefs possibles, moins il sera ncessaire e e e dutiliser une bonne fonction de hachage. Si les clefs sont rparties de faon parfaitement e c uniforme, on peut choisir m = 2 et simplement prendre comme valeur hache les premiers e bits de la clef. Cela sera tr`s rapide, mais la moindre rgularit dans les clefs peut tre e e e e catastrophique. Il est donc en gnral recommand davoir une fonction de hachage dont e e e la sortie dpend de tous les bits de la clef. e Si lon suppose que les clefs sont des entiers naturels (si ce nest pas le cas, on peut en gnral trouver un codage qui permette dinterprter chaque clef comme un entier), la e e e fonction modulo : h(c) = c mod m est rapide et poss`de de bonnes proprits de rpartition statistique d`s lors que m est un e ee e e nombre premier assez loign dune puissance de 2 ; (si m = 2 , cela revient ` prendre e e a bits de la clef). Une autre fonction frquemment utilise est la fonction e e h(c) = m(cx cx), o` x est une constante relle appartenant ` ]0, 1[. Le choix de x = 51 conduit ` de bonnes u e a a 2 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

Anne 2010-2011 e

Chapitre 4. Recherche en table

4.6

Tableau rcapitulatif des complexits e e


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)

mthode e adressage direct recherche squentielle e recherche dichotomique recherche par interpolation tables de hachage avec m n

Note : les complexits ci-dessus sont celles dans le pire cas, sauf celles suivies de pour e lesquelles il sagit du cas moyen. On constate quune table de hachage de taille bien adapte et munie dune bonne e fonction de hachage peut donc tre tr`s ecace. Cependant cela demande davoir une e e bonne ide de la quantit de donnes ` grer ` lavance. e e e a e a

& M. Finiasz

57

Chapitre 5 Arbres
La notion darbre modlise, au niveau des structures de donnes, la notion de rcursivit e e e e pour les fonctions. Il est ` noter que la reprsentation par arbre est tr`s courante dans a e e la vie quotidienne : arbres gnalogiques, organisation de comptitions sportives, organie e e gramme dune entreprise... Dans ce chapitre, apr`s avoir introduit la terminologie et les e principales proprits des arbres, nous illustrons leur utilisation ` travers trois exemples ee a fondamentaux en informatique : lvaluation dexpressions, la recherche dinformation, et e limplmentation des les de priorit. Nous terminons par la notion darbre quilibr, dont e e e e la structure permet de garantir une complexit algorithmique optimale quelle que soit la e distribution de lentre. e

5.1
5.1.1

Prliminaires e
Dnitions et terminologie e

Un arbre est une collection (ventuellement vide) de nuds et dartes assujettis ` e e a certaines conditions : un nud peut porter un nom et un certain nombre dinformations pertinentes (les donnes contenues dans larbre) ; une arte est un lien entre deux nuds. e e Une branche (ou chemin) de larbre est une suite de nuds distincts, dans laquelle deux nuds successifs sont relis par une arte. La longueur dune branche est le nombre de ses e e artes (ou encore le nombre de nuds moins un). Un nud est spciquement dsign e e e e 1 ee e comme tant la racine de larbre . La proprit fondamentale dnissant un arbre est e alors que tout nud est reli ` la racine par une et une seule branche 2 (i.e. il existe un e a unique chemin dun nud donn ` la racine). ea Comme on peut le voir sur la Figure 5.1, chaque nud poss`de un lien (descendant, e selon la reprsentation graphique) vers chacun de ses ls (ou descendants), ventuellement e e 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 spcie, on parle plutt darborescence. e e o e a 2. Dans le cas ou certains nuds sont relis par plus dune branche (ou pas de branche du tout) ` la racine on parle alors de graphe.

59

5.1 Prliminaires e
Racine

ENSTA cours IN101


= Nud = Arte Nuds internes

Figure 5.1 Reprsentation dun arbre. On place toujours la racine en haut. e la racine - poss`de un p`re (ou anctre) et un seul, qui est le nud immdiatement aue e e e dessus de lui dans la reprsentation graphique. Les nuds sans descendance sont appels e e feuilles, les nuds avec descendance tant des nuds internes. Tout nud est la racine du e sous-arbre constitu par sa descendance et lui-mme. e e Voici une liste du vocabulaire couramment utilis pour les arbres : e Une clef est une information contenue dans un nud. Un ensemble darbres est une fort. e Un arbre est ordonn si lordre des ls de chacun de ses nuds est spci. Cest e e e gnralement le cas dans les arbres que lon va considrer dans la suite. e e e Un arbre est de plus numrot si chacun de ses ls est numrot par un entier strictement e e e e positif (deux nuds ne possdant pas le mme numro). e e e Les nuds dun arbre se rpartissent en niveaux. Le niveau dun nud est la longueur e de la branche qui le relie ` la racine. a La hauteur dun arbre est le niveau maximum de larbre (i.e. plus grande distance dun nud ` la racine). a La longueur totale dun arbre est la somme des longueurs de tous les chemins menant des nuds ` la racine. a Le degr dun nud est le nombre de ls quil poss`de. e e Larit dun arbre est le degr maximal de ses nuds. e e Lorsque lon a un arbre dont les ls de chaque nud sont numrots par des entiers tous e e compris dans lintervalle [1, . . . , k], on parle darbre k-aire. Un arbre k-aire est donc un arbre numrot, darit infrieure ou gale ` k. e e e e e a Comme avec les sentinelles pour les listes, on peut dnir un type particulier de nud e dit nud vide : cest un nud factice dans le sens o` il ne contient pas dinformation ; u il peut juste servir ` remplir la descendance des nuds qui contiennent moins de k ls. a Lexemple le plus important darbre m-aire est larbre binaire. Chaque nud poss`de e 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

Anne 2010-2011 e

Chapitre 5. Arbres

galement dnir la notion de ls gauche et ls droit dune feuille : il sut de reprsenter e e e chacun deux par le nud vide. De cette mani`re, tout nud non vide peut tre e e considr comme un nud interne. ee Un arbre binaire est complet si les nuds remplissent compl`tement tous les niveaux, e sauf ventuellement le dernier, pour lequel les nuds apparaissent alors tous le plus ` e a gauche possible (notez que larbre binaire de hauteur 0 est complet).

5.1.2

Premi`res proprits e e e

La meilleure dnition des arbres est sans doute rcursive : un arbre est soit larbre e e vide, soit un nud appel racine reli ` un ensemble (ventuellement vide) darbres appels e ea e e ses ls. Cette dnition rcursive se particularise trivialement au cas des arbres binaires comme e e suit : un arbre binaire est soit larbre vide, soit un nud racine reli ` un arbre binaire ea gauche (appel sous-arbre gauche) et un arbre binaire droit (appel sous-arbre droit). e e Un arbre binaire est videmment un arbre ; mais rciproquement, tout arbre peut tre e e e reprsent par un arbre binaire (cf. plus loin la reprsentation des arbres). Cette vision e e e rcursive des arbres nous permet de dmontrer les proprits suivantes. e e ee Proprit 5.1.1. Il existe une branche unique reliant deux nuds quelconques dun arbre. e e Proprit 5.1.2. Un arbre de N nuds contient N 1 artes. e e e Preuve. Cest une consquence directe du fait que tout nud, sauf la racine, poss`de un e e p`re et un seul, et que chaque arte relie un nud ` son p`re. Il y a donc autant dartes e e a e e que de nuds ayant un p`re, soit N 1. e Proprit 5.1.3. Un arbre binaire complet possdant N nuds internes contient N + 1 e e e 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 rsultat est e vrai pour larbre binaire de hauteur 0 (il est rduit ` une feuille). Considrons un arbre e a e binaire complet A = (r, Ag , Ad ), r dsignant la racine de larbre, et Ag et Ad ses souse arbres gauche et droit respectivement. Les feuilles de A tant celles de Ag et de Ad , on a e 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 tant des arbres complets, la rcurrence sapplique, e e 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. Proprit 5.1.4. La hauteur h dun arbre binaire contenant N nuds vrie h + 1 e e e log2 (N + 1). Preuve. Un arbre binaire contient au plus 2 nuds au premier niveau, 22 nuds au deuxi`me niveau,..., 2h nuds au niveau h. Le nombre maximal de nuds pour un arbre e binaire de hauteur h est donc 1 + 2 + . . . + 2h = 2h+1 1, i.e. N + 1 2h+1 . & M. Finiasz 61

5.1 Prliminaires e

ENSTA cours IN101

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

5.1.3

Reprsentation des arbres e

Arbres binaires. Pour reprsenter un arbre binaire, on utilise une structure similaire ` e a celle dune liste cha ee, mais avec deux pointeurs au lieu dun : lun vers le ls gauche, n lautre vers le ls droit. On dnit alors un nud par : e
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 faon tr`s similaire (o` k est un c e u entier x une fois pour toute dans le programme) : e
1 2 3 4 5 6

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

Arbres gnraux. Dans le cas dun arbre gnral il ny a pas de limite sur le nombre e e e e de ls dun nud. Il faut donc utiliser une structure dynamique pour stocker tous les ls dun mme nud. Pour cela on utilise une liste cha ee. Chaque nud contient donc un e n pointeur vers son ls le plus ` gauche (le premier lment de la liste), qui lui contient un a ee pointeur vers sont fr`re juste ` sa droite (la queue de la liste). Chaque nud contient donc e a un pointeur vers un ls et un pointeur vers un fr`re. On obtient une structure de la forme : e
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

Anne 2010-2011 e

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

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

5.2

Utilisation des arbres

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

5.2 Utilisation des arbres

ENSTA cours IN101

2 4 3

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

5.2.1

Evaluation dexpressions & parcours darbres

Nous nous intressons ici ` la reprsentation par arbre des expressions arithmtiques. e a e e Dans cette reprsentation, les nuds de larbre sont les oprateurs, et les feuilles les e e oprandes. Sil ny a que des oprateurs darit 2 (comme + ou ) on obtient alors un e e e arbre binaire. Si on consid`re des oprateurs darit suprieur on obtient un arbre gnral, e e e e e e mais on a vu que tout arbre est quivalent ` un arbre binaire. On ne consid`re donc ici que e a e des arbres binaires et des oprateurs darit deux, mais les algorithmes seront les mmes e e e pour des oprateurs darit quelconque. e e On ne sintresse pas ici ` la construction dun arbre dvaluation, mais uniquement e a e ` son valuation. On part donc dune arbre comme celui reprsent sur la Figure 5.3. a e e e Lvaluation dune telle expression se fait en parcourant larbre, cest-`-dire en visitant e a 3 chaque nud de mani`re systmatique. Il existe trois faons de parcourir un arbre. Chae e c cune correspond ` une criture dirente de lexpression. Nous dtaillons ci-apr`s ces trois a e e e e parcours, qui sont essentiellement rcursifs, et font tous parti des parcours dits en profone deur (depth-rst en anglais). Parcours prxe. Ce parcours consiste ` visiter la racine dabord, puis le sous-arbre e a gauche, et enn le sous-arbre droit. Il correspond ` lcriture dune expression dans laquelle a e les oprateurs sont placs avant les oprandes, par exemple + 4 3 2 pour lexpression de e e e la Figure 5.3. Cette notation est aussi appele notation polonaise. Lalgorithme suivant e eectue un parcours prxe de larbre A et pour chaque nud visit ache son contenu e e (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 galement parcours en largeur (i.e. parcours du haut vers le bas, ee en visitant tous les nuds dun mme niveau de gauche ` droite avant de passer au niveau suivant. Ce e a parcours nest pas rcursif, et est utilis par exemple dans la structure de tas (cf. section 5.2.3), mais ne e e permet pas dvaluer une expression. e

64

F. Levy-dit-Vehel

Anne 2010-2011 e
4 5 6 7

Chapitre 5. Arbres

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

Parcours inxe. Le parcours inxe correspond ` lcriture habituelle des expressions a e arithmtiques, par exemple (4 + 3) 2 (sous rserve que lon rajoute les parenth`ses e e e ncessaires). Algorithmiquement, on visite dabord le sous-arbre gauche, puis la racine, e 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 dajouter des parenth`ses pour lever les ambigu es, ce qui rend lexpression e e t un peu plus lourde : lalgorithme achera ((4) + (3)) (2). Parcours postxe. Ce parcours consiste ` visiter le sous-arbre gauche dabord, puis a le droit, et enn la racine. Il correspond ` lcriture dune expression dans laquelle les a e oprateurs sont placs apr`s les oprandes, par exemple 4 3 + 2 (cest ce que lon appel e e e e 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. Pour les trois parcours ci-dessus, il est clair que lon visite une fois chaque e nud, donc n appels rcursifs pour un arbre ` n nuds, et ce quel que soit le parcours e a considr. La complexit de lopration de parcours est donc en (n). ee e e Cas des arbres gnraux. Les parcours prxes et postxes sont aussi bien dnis e e e e pour les arbres quelconques. Dans ce cas, la loi de parcours prxe devient : visiter la e 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 tre bien dni si le e e nombre de ls de chaque nud est variable. Notons aussi que le parcours postxe dun arbre gnralis est quivalent au parcours e e e e inxe de larbre binaire quivalent (comme vu sur la Figure 5.2) et que le parcours prxe e e dun arbre gnralis est quivalant au parcours prxe de larbre binaire quivalent. e e e e e e Parcours en largeur. Le parcours en largeur (breadth-rst en anglais) est tr`s dirent e e 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 rcursif mais doit e tre fait sur larbre tout entier. La technique la plus simple utilise deux les A et B. On e initialise A avec la racine et B ` vide. Puis ` chaque tape du parcours on pop un nud de a a e la le A, on eectue lopration que lon veut dessus (par exemple acher la clef), et on e ajoute tous les ls de ce nud ` B. On recommence ainsi jusqu` avoir vider A, et quand A a a et vide on inverse les les A et B et on recommence. On sarrte quand les deux les sont e vides. ` A chaque tape la le A contient les nuds du niveau que lon est en train de parcourir, e 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`s frquent sur des arbres et sera plus e e souvent rencontr dans le contexte plus gnral des graphes. e e e

5.2.2

Arbres Binaires de Recherche

Nous avons vu dans le chapitre 4 comment implmenter un dictionnaire ` laide dune e a table. Nous avons vu que la mthode la plus ecace est lutilisation dune table de hachage. e Cependant, pour quune telle table ore des performances optimales, il est ncessaire de e conna sa taille nale ` lavance. Dans la plus part des contextes, cela nest pas possible. tre a 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 infrieures ` celle de ce nud, et e a son sous-arbre droit des valeurs suprieures ou gales. Un tel arbre est reprsent en Fie e e e gure 5.4. Cette proprit est susante pour pouvoir armer quun parcours inxe de ee larbre va visiter les nuds dans lordre croissant des clefs. Un ABR est donc un arbre ordonn dans lequel on veut pouvoir facilement eectuer des oprations de recherche, dine e sertion et de suppression, et cela sans perturber lordre. Recherche dans un ABR. Pour chercher une clef dans un ABR on utilise naturellement une mthode analogue ` la recherche dichotomique dans une table. Pour trouver un nud e a de clef v, on commence par comparer v ` la clef de la racine, note ici r. Si v < r, alors a e on se dirige vers le sous-arbre gauche de la racine. Si v = r, on a termin, et on retourne e linformation associe ` la racine. Si v > vr , on consid`re le sous-arbre droit de la racine. e a e On applique cette mthode rcursivement sur les sous-arbres. e e 66 F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 5. Arbres

5 2 4 6 7 8 9

Figure 5.4 Un exemple darbre binaire de recherche (ABR). Il est ` noter que la taille du sous-arbre courant diminue ` chaque appel rcursif. La a a e procdure sarrte donc toujours : soit parce quun nud de clef v a t trouv, soit parce e e ee e quil nexiste pas de nud ayant la clef v dans larbre, et le sous-arbre courant est alors vide. Cela peut se programmer de faon rcursive ou itrative : c e e
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.\n"); e 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.\n"); e 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 dj` prsente dans un ABR. Trois ea e emplacements sont possibles. Insertion dans un ABR. Linsertion dans un ABR nest pas une opration complique : e e il sut de faire attention ` bien respecter lordre au moment de linsertion. Pour cela, la a mthode la plus simple et de parcourir larbre de la mme faon que pendant la recherche e e c dune clef et lorsque lon atteint une feuille, on peut insrer le nouveau nud, soit ` gauche, e a soit ` droite de cette feuille, selon la valeur de la clef. On est alors certain de conserver a lordre dans lABR. Cette technique fonctionne bien quand on veut insrer un nud dont e la clef nest pas prsente dans lABR. Si la clef est dj` prsente deux choix sont possibles e ea e (cf. Figure 5.5) : soit on ddouble le nud en insrant le nouveau nud juste ` ct de e e a oe celui qui poss`de la mme clef (en dessus ou en dessous), soit on descend quand mme e e e jusqu` une feuille dans le sous-arbre droit du nud. a Encore une fois, linsertion peut se programmer soit de faon rcursive, soit de faon c e c itrative. Dans les deux cas, on choisit dinsrer un nud dj` prsent dans larbre comme e e ea e sil ntait pas prsent : on lins`re comme ls dune feuille (troisi`me possibilit dans la e e e e e 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 ncessaire de le modier que quand e larbre est initialement vide) il est ncessaire de le passer en argument par pointeur. De ce e fait, les appels rcursifs utilisent une criture un peu lourde &((*A)->left) : e e on commence par prendre larbre *A. 68 F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 5. Arbres

on regarde son ls gauche (*A)->left et on veut pouvoir modier ce ls gauche : on rcup`re donc le pointeur vers le ls gauche e e &((*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 itrative, on retrouve lcriture un peu lourde de la version rcursive e e e avec cur = &((*cur)->left). On pourrait tre tent de remplacer cette ligne par la ligne e e (*cur) = (*cur)->left, mais cela ne ferait pas ce quil faut ! On peut voir la dirence e entre ces deux commandes sur la Figure 5.6. Suppression dans un ABR. La suppression de nud est lopration la plus dlicate e e 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, si on le supprime cela va crer un trou e e dans larbre : il faut donc dplacer un autre nud de larbre pour reboucher le trou. En e pratique, on peut distinguer trois cas dirents : e 1. Si n ne poss`de aucun ls (n est une feuille), alors on peut le supprimer de larbre e directement, i.e. on fait pointer son p`re vers NULL au lieu de n. e 2. Si n ne poss`de quun seul ls : on supprime n de larbre et on fait pointer le p`re de e e n vers le ls de n (ce qui revient ` remplacer n par son ls). a 3. Si n poss`de deux ls, on commence par calculer le successeur de n, cest-`-dire le e a nud suivant n lorsque lon num`re les nuds avec un parcours inxe de larbre : e e 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 ` gauche du a sous-arbre droit de n. Ce nud tant tout ` gauche du sous-arbre, il a forcment son e a e ls gauche vide. On peut alors supprimer le nud n en le remplaant par le nud c s (on remplace la clef et linformation contenue dans le nud), et en supprimant le nud s de son emplacement dorigine avec la mthode vue au cas 2 (ou au cas 1 si e s poss`de deux ls vides). Une telle suppression est reprsente sur la Figure 5.7. e e e & 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 mmoire lors de linsertion dun nud n dans un e ABR. On part dun arbre ayant juste une racine et un ls droit et on doit insrer e le nouveau nud comme ls gauche de la racine. On initialise cur, puis on descend dans larbre dune faon ou dune autre : ` gauche la bonne mthode qui marche, ` c a e a droite la mauvaise. Dans les deux cas, on trouve un pointeur vers NULL et on ins`re le e nouveau nud n. Avec la mthode de droite larbre original est modi et deux nuds e e sont perdus.

70

F. Levy-dit-Vehel

Anne 2010-2011 e
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 itrative de la suppression (la version rcursive est plus complique et e e e 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 le noeud ` supprimer, on cherche son successeur */ e a s = &((*cur)->right); while ((*s)->left != NULL) { s = &((*s)->left); } /* on a trouv le successeur */ e 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 des direntes oprations. Soit n le nombre de nuds de larbre, appel e e e e aussi taille de larbre, et h la hauteur de larbre. Les algorithmes ci-dessus permettent de se convaincre aisment du fait que la complexit des oprations de recherche, dinsertion e e e & M. Finiasz 71

5.2 Utilisation des arbres

ENSTA cours IN101

et de suppression, sont en (h) : pour les version rcursives, chaque appel fait descendre e dun niveau dans larbre et pour les version itratives chaque tape de la boucle while fait e e aussi descendre dun niveau. Pour calculer la complexit, il faut donc conna la valeur e tre de h. Il existe des arbres de taille n et de hauteur n1 : ce sont les arbres dont tous les nuds ont au plus un ls non vide. Ce type darbre est alors quivalent ` une liste, et les oprations e a e ci-dessus ont donc une complexit analogue ` celle de la recherche squentielle (i.e. en e a e (h) = (n)). Noter que lon obtient ce genre de conguration lorsque lon doit insrer n e clefs dans lordre (croissant ou dcroissant), ou bien par exemple les lettres A,Z,B,Y,C,X,... e dans cet ordre. En revanche, dans le cas le plus favorable, i.e. celui dun arbre complet o` tous les u 4 niveaux sont remplis, larbre a une hauteur environ gale ` log2 (n). Dans ce cas, les trois e a oprations ci-dessus ont alors une complexit en (h) = (log2 (n)). e e La question est maintenant de savoir ce quil se passe en moyenne. Si les clefs sont insres de mani`re alatoire, on a le rsultat suivant : ee e e e Proposition 5.2.1. La hauteur moyenne dun arbre binaire de recherche construit ale atoirement ` partir de n clefs distinctes est en (log2 (n)). a 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 adapts pour e limplmentation de dictionnaires. En outre, an de ne pas tomber dans le pire cas, e on dispose de mthodes de rquilibrage darbres pour rester le plus pr`s possible de la e ee e 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 mthode de tri se dduit naturellement de cette structure e e de donnes : e Soient n entiers ` trier a Construire lABR dont les nuds ont pour clefs ces entiers Imprimer un parcours inxe de cet ABR. La complexit de cette mthode de tri est en (n log2 (n)) ((nlog2 (n)) pour linsertion e e 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 dcroissant). e

5.2.3

Tas pour limplmentation de les de priorit e e

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

72

F. Levy-dit-Vehel

Anne 2010-2011 e

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 Reprsentation dun tas ` laide dun tableau. e a nanceur dun syst`me dexploitation qui doit excuter en priorit les instructions venant e e e dun processus de priorit leve. Les lments sortiront de cette le prcisment selon leur ee e ee e e rang : llment de rang le plus lev sortira en premier. Les primitives ` implmenter pour ee e e a e mettre en oeuvre et manipuler des les de priorit sont : e une une une une fonction fonction fonction fonction dinitialisation dune le (cration dune le vide), e dinsertion dun lment dans la le (avec son rang), ee qui retourne llment de plus haut rang, ee de suppression de llment de plus haut rang. ee

Le codage dune le de priorit peut tre ralis au moyen dune liste : alors linsertion e e e e se fait en temps constant, mais lopration qui retourne ou supprime llment de plus e ee haut rang ncessite une recherche pralable de celui-ci, et se fait donc en (n), o` n est e e u le nombre dlments de la le. Autrement, on peut aussi choisir davoir une opration ee e dinsertion plus lente qui ins`re directement llment ` la bonne position en fonction de e ee a sa priorit (cette opration cote alors (n)) et dans ce cas les oprations de recherche et e e u e suppression peuvent se faire en (1). Une mthode beaucoup plus ecace pour reprsenter une le de priorit est obtenue au e e e moyen dun arbre binaire de structure particuli`re, appel tas (heap en anglais) ou (arbre) e e maximier. Un tas est un arbre binaire complet, qui poss`de en plus la proprit que la clef e ee de tout nud est suprieure ou gale ` celle de ses descendants. Un exemple de tas est e e a donn ` la Figure 5.8. ea La faon la plus naturelle dimplmenter un tas semble tre dutiliser une structure c e e darbre similaire ` celle utilises pour les ABR. Pourtant limplmentation la plus ecace a e e utilise en fait un tableau. On commence par numroter les nuds de 0 (racine) ` n-1 e a (nud le plus ` droite du dernier niveau) par un parcours par niveau. On place chaque a nud dans la case du tableau tab correspondant ` son numro (cf. Figure 5.8) : la racine a e dans tab[0], le ls gauche de la racine dans tab[1]... Avec cette numrotation le p`re du e e 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 proprit de maximier (ordre sur les clefs) se traduit sur le tableau par : ee & M. Finiasz 73

5.2 Utilisation des arbres

ENSTA cours IN101

1 i n 1, tab[i] tab[ i1 ]. 2 Dtaillons ` prsent les oprations de base (celles listes ci-dessus) sur les tas reprsents e a e e e e e par des tableaux. Initialisation. Pour coder un tas avec un tableau il faut au moins trois variables : le tableau tab o` ranger les nuds, le nombre maximum max de nuds que lon peut mettre u dans ce tas, et le nombre de nuds dj` prsents n. Le plus simple est de coder cela au ea e moyen de la structure suivante :
1 2 3 4 5

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

Pour crer un tas (vide) de taille maximale m donne en param`tre, on cre un objet de e e e e 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 linconvnient de lutilisation dun tableau : le nombre tre e maximum dlments ` mettre dans le tas doit tre dni ` lavance, d`s linitialisation. ee a e e a e Cette limitation peut toutefois tre contourne en utilisant une rallocation dynamique du e e e tableau : on peut dans ce cas ajouter un niveau au tableau ` chaque fois que le niveau a prcdent est plein. On ralloue alors un nouveau tableau (qui sera de taille double) et e e e on peut recopier le tableau prcdent au dbut de celui l`. Comme vu dans la section sur e e e a les tableau, la taille tant double ` chaque rallocation, cela naecte pas la complexit e e a e e moyenne dune insertion dlment. ee Insertion. Lopration dinsertion comme celle de suppression comporte deux phases : la e premi`re vise ` insrer (resp. supprimer) la clef considre (resp. la clef de la racine), tout e a e ee en prservant la proprit de compltude de larbre, la deuxi`me est destine ` rtablir la e ee e e e a e proprit de maximier, i.e. lordre sur les clefs des nuds de larbre. ee Pour insrer un nud de clef v, on cre un nouveau nud dans le niveau de profondeur le e e e plus lev de larbre, et le plus ` gauche possible 5 . Ensuite, on op`re une ascension dans e e a larbre (plus prcisment dans la branche reliant le nouveau nud ` la racine) de faon ` e e a c a
5. Si larbre est compl`tement rempli sur le dernier niveau, on cre un niveau supplmentaire. e e e

74

F. Levy-dit-Vehel

Anne 2010-2011 e
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`re. Si v est suprieure ` celle-ci, on permute ces deux clefs. On recommence jusqu` ce e e a a que v soit infrieure ou gale ` la clef de son nud p`re ou que v soit ` la racine de larbre e e a e a (en cas 0 de tab). Les tapes dune insertion sont reprsentes sur la Figure 5.9. e e e
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`re v ` la fin de tab */ e a 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 (note v) du nud situ au niveau de profondeur le plus lev et le plus ` droite e e e e a possible (le dernier nud du tableau), nud que lon supprime alors aisment tant donn e e e quil na pas de ls. Ensuite, pour rtablir lordre sur les clefs de larbre, i.e. mettre v ` e 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 infrieure ou gale ` au moins une de ces deux clefs, e e a on permute v avec la plus grande des deux clefs. On recommence cette opration jusqu` e a ce que v soit suprieure ou gale ` la clef de chacun de ses nuds ls, ou jusqu` arriver ` e e a a a une feuille. Les tapes dune suppression sont reprsentes sur la Figure 5.9. e e e Voici comment peut se programmer lopration de suppression qui renvoie au passage e llment le plus grand du tas que lon vient de supprimer. ee
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 ` la fin */ a int max = h->tab[0]; int i = 0; bool cont = true; h->n--; /* on met la clef du dernier noeud ` la racine */ a 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 ncessaire */ e 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

Anne 2010-2011 e
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 dchanger deux cases du tableau (ou deux entiers situs e e nimporte o` dans la mmoire). On est oblig de passer en arguments des pointeurs vers u e e les entiers ` changer car si lon passe directement les entiers, ils seront recopis dans des ae e variables locales de la fonction, et ce sont ces variable locale qui seront changes, les entiers e e dorigine restant bien tranquillement ` leur place. a Complexit des oprations sur les tas. Comme souvent dans les arbres, la complexit e e e des direntes opration dpend essentiellement de la hauteur totale de larbre. Ici, avec e e e 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 oprations dinsertion ou de suppression sont excutes au maxie e e mum un nombre de fois gal ` la hauteur du tas. Chacune de ces excution contenant un e a e nombre constant dopration la complexit total des oprations dinsertion et de supprese e e sion est donc (h) = (log(n)).

5.2.4

Tri par tas

La reprsentation prcdente dun ensemble dentiers (clefs) donne lieu de mani`re e e e e naturelle ` un algorithme de tri appel tri pas tas (heapsort en anglais). Soient n entiers a e ` trier, on les ins`re dans un tas, puis on rp`te n fois lopration de suppression. La a e e e e complexit de ce tri est n (log(n)) pour les n insertions de clefs plus n (log(n)) e pour les n suppressions, soit (n log(n)) au total. Il est ` noter que, de par la dnition a e des oprations dinsertion/suppression, cette complexit ne dpend pas de la distribution e e e initiale des entiers ` trier, i.e. elle est la mme en moyenne ou dans le pire cas. Voici une a e implmentation dun tel tri : on part dun tableau tab que lon re-remplit avec les entiers e tris (par ordre croissant). e
1 2 3

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

& M. Finiasz

77

5.3 Arbres quilibrs e e


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 quilibrs e e

Les algorithmes sur les arbres binaires de recherche donnent de bons rsultats dans le e cas moyen, mais ils ont de mauvaises performances dans le pire cas. Par exemple, lorsque les donnes sont dj` tries, ou en ordre inverse, ou contiennent alternativement des grandes e ea e et des petites clefs, le tri par ABR a un tr`s mauvais comportement. e Avec le tri rapide, le seul rem`de envisageable tait le choix alatoire dun lment pivot, e e e ee dont on esprait quil liminerait le pire cas. Heureusement pour la recherche par ABR, e e il est possible de faire beaucoup mieux, car il existe des techniques gnrales dquilibrage e e e darbres permettant de garantir que le pire cas narrive jamais. Ces oprations de transformation darbres sont plus ou moins simples, mais sont peu e coteuses en temps. Elles permettent de rendre larbre le plus rgulier possible, dans un sens u e qui est mesur par un param`tre dpendant en gnral de sa hauteur. Une famille darbres e e e e e satisfaisant une telle condition de rgularit est appele une famille darbres quilibrs. e e e e e Il existe plusieurs familles darbres quilibrs : les arbres AVL, les arbres rouge-noir, les e e arbres a-b... Nous verrons ici essentiellement les arbres AVL.

5.3.1

Rquilibrage darbres ee

Nous prsentons ici une opration dquilibrage appele rotation, qui sapplique ` tous e e e e a les arbres binaires. Soit donc A, un arbre binaire non vide. On crira A = (x, Z, T ) pour e 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). Lopration de rotation e gauche de A est lopration : e 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) pointe sur le sous-arbre Z, et le ls droit e du nud x est mis ` pointer sur le sous-arbre Y . Cette opration est reprsente sur la a e e e Figure 5.10. La rotation droite est lopration inverse : e A = (y, (x, X, Y ), Z) D(A) = (x, X, (y, Y, Z)). 78 F. Levy-dit-Vehel

Anne 2010-2011 e
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 Opration de rotation pour le rquilibrage darbre binaire. e ee On remplace le nud racine y par le nud x, le ls gauche du nud x (inchang) pointe e sur le sous-arbre X, son ls droit pointe sur le nud y, et le ls gauche du nud y est mis ` pointer sur le sous-arbre Y . a Utilisation des rotations. Le but des rotations est de pouvoir rquilibrer un ABR. ee On op`re donc une rotation gauche lorsque larbre est dsquilibr ` droite , i.e. son e ee ea sous-arbre droit est plus haut que son sous-arbre gauche. On op`re une rotation droite dans e le cas contraire. Il est ais de vrier que les rotations prservent la condition sur lordre e e e des clefs dun ABR. On a alors : Proposition 5.3.1. Si A est un ABR, et si la rotation gauche (resp. droite) est dnie e sur A, alors G(A) (resp. D(A)) est encore un ABR. On peut galement dnir des doubles rotations (illustres sur la Figure 5.11) comme e e e suit : la rotation gauche-droite associe ` larbre A = (x, Ag , Ad ), larbre D(x, G(Ag ), Ad ). a De mani`re analogue, la rotation droite-gauche associe ` larbre A = (x, Ag , Ad ), larbre e a G(x, Ag , D(Ad )). Ces oprations prservent galement lordre des ABR. Une proprit ime e e ee portante des rotations et doubles rotations est quelles simplmentent en temps constant : e en eet, lorsquun ABR est reprsent par une structure de donnes du type binary tree e e e dnie plus haut dans ce chapitre, une rotation consiste essentiellement en la mise ` jour e a dun nombre x (i.e. indpendant du nombre de nuds de larbre ou de sa hauteur) de e e pointeurs.

5.3.2

Arbres AVL

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

5.3 Arbres quilibrs e e


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`re informelle, un ABR est un arbre AVL si, pour tout nud de larbre, les e hauteurs de ses sous-arbres gauche et droit di`rent dau plus 1. Plus prcisment, posons e e e (A) = 0 si A est larbre vide, et dans le cas gnral : e e (A) = h(Ag ) h(Ad ) o` Ag et Ad sont les sous-arbres gauche et droit de A, et h(Ag ) la hauteur de larbre Ag u (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 lquilibre de A. Pour plus de simplicit, la notation (x) o` x est un e e e u nud dsignera lquilibre de larbre dont x est la racine. Une dnition plus rigoureuse e e e dun arbre AVL est alors : Dnition 5.1. Un ABR est un arbre AVL si, pour tout nud x de larbre, (x) e {1, 0, 1}. La proprit fondamentale des AVL est que lon peut borner leur hauteur en fonction ee du log du nombre de nuds dans larbre. Plus prcisment : e e Proposition 5.3.2. Soit A un arbre AVL possdant n sommets et de hauteur h. Alors e log2 (1 + n) 1 + h c log2 (2 + n), 5)/2) 1, 44.

avec c = 1/ log2 ((1 +

Preuve. Pour une hauteur h donne, larbre possdant le plus de nuds est larbre complet, e e h+1 h+1 `2 a 1 nuds. Donc n 2 1 et log2 (1 + n) 1 + h. 80 F. Levy-dit-Vehel

Anne 2010-2011 e

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`me sous arbre est petit, moins il aura de nud, mais la proprit dAVL fait e ee quil ne peut pas tre de hauteur plus petite que h 2. Donc larbre AVL de hauteur h e contenant le moins de nuds est constitu dun nud racine et de deux sous-arbres AVL e 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` Fk est le k-i`me nombre de Fibonacci. Pour tout arbre AVL ` n u e a sommets et de hauteur h, on a par consquent : e 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 ` 100 000 nuds a une hauteur comprise entre 17 et 25. Le a nombre de comparaisons ncessaires ` une recherche, insertion ou suppression dans un tel e a arbre sera alors de cet ordre. Arbres de Fibonacci La borne suprieure de la proposition prcdente est essentiellement atteinte pour les e e e arbres de Fibonacci dnis comme suit : (0) est larbre vide, (1) est rduit ` une e e a feuille, et, pour k 2, larbre k+2 a un sous-arbre gauche gal ` k+1 , et un sous-arbre e a doit gal ` k . La hauteur de k est k 1, et k poss`de Fk+2 nuds. e a e Limplmentation des AVL est analogue ` celle des ABR, ` ceci pr`s que lon rajoute ` e a a e 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 opration dinsertion et de suppression e un peu plus compliques : il est ncessaire de remettre ` jour ces hauteurs chaque fois que e e a cela est ncessaire. e Insertion dans un AVL. Lopration dinsertion dans un AVL se fait de la mme e e mani`re que dans un ABR : on descend dans larbre ` partir de la racine pour rechercher la e a feuille o` mettre le nouveau nud. Ensuite il sut de remonter dans larbre pour remettre u & M. Finiasz 81

5.3 Arbres quilibrs e e


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 Rquilibrage apr`s insertion dans un AVL. ee e ` jour les hauteurs de tous les sous-arbres (dans une version rcursive de linsertion, cela a e se fait aisment en abandonnant la rcursion terminale et en remettant ` jour les hauteur e e a juste apr`s lappel rcursif pour linsertion). e e Toutefois, cette opration peut dsquilibrer larbre, i.e. larbre rsultant nest plus e ee e AVL. Pour rtablir la proprit sur les hauteurs, il sut de rquilibrer larbre par des e ee ee rotations (ou doubles rotations) le long du chemin qui m`ne de la racine ` la feuille o` e a u sest fait linsertion. Dans la pratique, cela se fait juste apr`s avoir remis ` jour les hauteurs e a des sous-arbres : si on constate un dsquilibre entre les deux ls de 2 ou 2 on eectue ee une rotation, ou une double rotation. Supposons que le nud x ait deux ls G et D et quapr`s insertion (x) = h(G) h(D) = 2. Le sous-arbre G est donc plus haut que D. e 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 (nots Gg et Gd ). Si (G) > 1 alors on fait une e rotation droite sur x qui sut ` rquilibrer larbre. Si (G) = h(Gg ) h(Gd ) = 1 alors a ee il faut faire une double rotation : on commence par une rotation gauche sur G an que (G) > 1, puis on est ramen au cas prcdent et une rotation droite sur x sut. Cette e e e opration est illustre sur la Figure 5.12. e e Dans le cas o` le sous-arbre D est le plus haut, il sut de procder de faon symtrique. u e c e Il est important de noter quapr`s une telle rotation (ou double rotation), larbre qui tait e e dsquilibr apr`s linsertion retrouve sa hauteur initiale. Donc la proprit dAVL est ee e e ee ncessairement prserver pour les nuds qui se trouvent plus haut dans larbre. e e Proposition 5.3.3. Apr`s une insertion dans un arbre AVL, il sut dune seule rotation e ou double rotation pour rquilibrer larbre. Lopration dinsertion/rquilibrage dans un ee e ee AVL ` n nuds se ralise donc en O(log2 (n)). a e 82 F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 5. Arbres

Suppression dans un AVL. Lopration de suppression dans un AVL se fait de la mme e e mani`re que dans un ABR : on descend dans larbre ` partir de la racine pour rechercher e a le nud contenant la clef ` supprimer. Sil sagit dune feuille, on supprime celle-ci ; sinon, a on remplace le nud par son nud successeur, et on supprime le successeur. Comme dans le cas de linsertion, cette opration peut dsquilibrer larbre. Pour le rquilibrer, on e ee ee op`re galement des rotations ou doubles rotations le long du chemin qui m`ne de la racine e e e ` la feuille o` sest fait la suppression ; mais ici, le rquilibrage peut ncessiter plusieurs a u ee e rotations ou doubles rotations mais le nombre de rotations (simples ou doubles) ncessaires e est au plus gal ` la hauteur de larbre (on en fait au maximum une par niveau), et donc : e a Proposition 5.3.4. Lopration de suppression/rquilibrage dans un AVL ` n nuds se e ee a ralise en O(log2 (n)). e Conclusion sur les AVL. Les AVL sont donc des arbres binaires de recherches vriant e juste une proprit dquilibrage supplmentaire. Le maintien de cette proprit naugee e e ee mente pas le cot des oprations de recherche, dinsertion ou de suppression dans larbre, u e 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 ` implmenter quun ABR classique mais nore que des avantages. Dautres structures a e darbres quilibrs permettent dobtenir des rsultats similaires, mais ` chaque fois, le e e e a maintien de la proprit dquilibrage rend les opration dinsertion/suppression un peu ee e e plus lourdes ` implmenter. a e

& M. Finiasz

83

Chapitre 6 Graphes
Nous nous intressons ici essentiellement aux graphes orients. Apr`s un rappel de la e e e terminologie de base associe aux graphes et des principales reprsentations de ceux-ci, nous e e prsentons un algorithme testant lexistence de chemins, qui nous conduit ` la notion de e a fermeture transitive. Nous nous intressons ensuite aux parcours de graphes : le parcours en e largeur est de nature itrative, et nous permettra dintroduire la notion darborescence des e plus courts chemins ; le parcours en profondeur - essentiellement rcursif - admet plusieurs e applications, parmi lesquelles le tri topologique. Nous terminons par le calcul de chemins optimaux dans un graphe (algorithme de Aho-Hopcroft-Ullman).

6.1

Dnitions et terminologie e

Dnition 6.1. Un graphe orient G = (S, A) est la donne dun ensemble ni S de e e e sommets, et dun sous-ensemble A du produit S S, appel ensemble des arcs de G. e Un arc a = (x, y) a pour origine le sommet x, et pour extrmit le sommet y. On note e e org(a) = x, ext(a) = y. Le sommet y est un successeur de x, x tant un prdcesseur de e e e
5 1 4 8 7 0 3

6 2

Figure 6.1 Exemple de graphe orient. e 85

6.1 Dnitions et terminologie e


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 boucle. e Un arc reprsente une liaison oriente entre son origine et son extrmit. Lorsque loriene e e e tation des liaisons nest pas une information pertinente, la notion de graphe non orient e permet de sen aranchir. Un graphe non orient G = (S, A) est la donne dun ensemble e e ni S de sommets, et dune famille de paires de S dont les lments sont appels artes. ee e e Etant donn un graphe orient G, sa version non oriente est obtenue en supprimant les e e e boucles, et en remplaant chaque arc restant (x, y) par la paire {x, y}. c Soit G = (S, A), un graphe orient, et T , un sous-ensemble de S. Lensemble e (T ) = {a = (x, y) A, (x T, y S \ T ) ou (y T, x S \ T )}, est appel le cocycle associ ` T . Lensemble e ea B(T ) = {x S \ T, y T, (x, y) ou (y, x) A}, est appel bordure de T (voir Figure 6.2). Si le sommet u appartient ` B(T ), on dit aussi e a que u est adjacent ` T . Le graphe G est biparti sil existe T S, tel que A = (T ). a Dnition 6.2. Soit G = (S, A), un graphe orient. Un chemin f du graphe est une suite e e darcs < a1 , . . . , ap >, telle que org(ai+1 ) = ext(ai ). Lorigine du chemin f , note aussi org(f ), est celle de son premier arc a1 , et son extrmit, e e e ext(f ), est celle de ap . La longueur du chemin est gale au nombre darcs qui le composent, e i.e. p. Un chemin f tel que org(f ) = ext(f ) est appel un circuit. e Dans un graphe non orient la terminologie est parfois un peu dirente : un chemin e e est appel une chane, et un circuit un cycle. Un graphe sans cycle est un graphe acyclique. e Soit G = (S, A), un graphe orient, et soit x, un sommet de S. Un sommet y est un e ascendant (resp. descendant) de x, sil existe un chemin de y ` x (resp. de x ` y). a a 86 F. Levy-dit-Vehel

Anne 2010-2011 e
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 associe ` un graphe. e a Un graphe non orient G est connexe si, pour tout couple de sommets, il existe une e cha ayant ces deux sommets pour extrmits. Par extension, un graphe orient est ne e e e connexe si sa version non oriente est connexe. La relation dnie sur lensemble des some e mets dun graphe non orient par x y sil existe une cha reliant x ` y, est une relation e ne a dquivalence dont les classes sont appeles composantes connexes du graphe. Cette noe e tion est tr`s similaire ` la notion de composantes connexe en topologie : on regarde les e a composantes connexe du dessin du graphe. Soit G = (S, A), un graphe orient. Notons G , la relation dquivalence dnie sur S e e e par x G y si x = y ou sil existe un chemin joignant x ` y et un chemin joignant y ` x. Les a a classes dquivalence pour cette relation sont appeles composantes fortement connexes de e e G.

6.2

Reprsentation des graphes e

Il existe essentiellement deux faons de reprsenter un graphe. Dans la suite, G = (S, A) c e dsignera un graphe orient. e e

6.2.1

Matrice dadjacence

Dans cette reprsentation, on commence par numroter de faon arbitraire les sommets e e c du graphe : S = {x1 , . . . , xn }. On dnit ensuite une matrice carre M , dordre n par : e e { 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 dextrmit xj e e (voir Figure 6.3). M est la matrice dadjacence de G. Bien entendu, on peut galement e reprsenter un graphe non orient ` laide de cette structure : la matrice M sera alors e e a symtrique, avec des 0 sur la diagonale. e La structure de donne correspondante - ainsi que son initialisation - est : e & M. Finiasz 87

6.2 Reprsentation des graphes e


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 Reprsentation dun graphe par liste de successeurs. e

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 cot en espace dun tel codage de la matrice est clairement en (n2 ). Ce codage deu vient donc inutilisable d`s que n dpasse quelques centaines de milliers. En outre, lorsque le e e graphe est peu dense (i.e. le rapport |A|/|S|2 est petit et donc la matrice M est creuse) il est trop coteux. Cependant, la matrice dadjacence poss`de de bonnes proprits algbriques u e ee e qui nous serviront dans ltude de lexistence de chemins (cf. section 6.3). e

6.2.2

Liste de successeurs

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

Anne 2010-2011 e

Chapitre 6. Graphes

que tab[i] contienne la liste des successeurs du sommet i, pour tout 1 i n. Cette reprsentation est en (n + |A|), donc poss`de une complexit en espace bien meilleure e e e que la prcdente (` noter que cette complexit est optimale dun point de vue thorique). e e a e e Les structures de donnes correspondantes sont : e
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, on parle de liste de voisins, mais le codage reste e le mme. Contrairement ` la reprsentation par matrice dadjacence, ici, les sommets du e a e graphe ont une vritable reprsentation et peuvent contenir dautres donnes quun simple e e e entier. Avec la matrice dadjacence, dventuelles donnes supplmentaires doivent tre e e e e stockes dans une structure annexe. e

6.3

Existence de chemins & fermeture transitive

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

Or, tout chemin de longueur p entre xi et xj se dcompose en un chemin de longueur p 1 e p1 entre xi et un certain xk , suivi dun arc entre xk et xj . Par hypoth`se, Mi,k est le nombre e 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 intermdiaire xk , 1 k n, de Mi,k e multipli par le nombre darcs (0 ou 1) reliant xk ` xj . e a La longueur dun chemin entre deux sommets tant au plus n, on en dduit : e e

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` les fonction copy mat(A,B) et mult mat(&C,A,B) permettent respectivement de copier u 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 ` eectuer n produits de matrices pour a conna lexistence dun chemin entre deux sommets donns (cest le cas pour trouver tre e un chemin de longueur n ou pour tre certain quaucun chemin nexiste). Le produit de e e deux matrices carres dordre n requrant n3 oprations 1 , la complexit de recherche de e e e lexistence dun chemin entre deux sommets par cet algorithme est en (n4 ). Cest aussi bien entendu la complexit du calcul de N . e 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`rent une complexit en (n2.5 ), mais les constantes dans le sont e e telles que pour des tailles infrieurs ` quelques milliers lalgorithme cubique de base est souvent le plus e a rapide.

90

F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 6. Graphes

tel que (x, y) A si et seulement sil existe un chemin dans G dorigine x et dextrmit e e y. La matrice N dnie prcdemment calcule la fermeture transitive du graphe G. En e e e eet, la matrice dadjacence M de G est obtenue ` partir de N en posant Mi,j = 0 si a Ni,j = 0, Mi,j = 1 si Ni,j = 0. Une fois calcule G , on peut rpondre en temps constant ` e e 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 ` chaque foncea tion : cest le graphe des dpendances (entre les variables) de la fonction et les sommets e de ce graphe reprsentent les variables, un arc entre deux sommets x et y indique que le e 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 donne. e Dans la suite, nous prsentons un algorithme de calcul de la fermeture transitive A dun e graphe G = (S, A), qui admet une complexit en (n3 ). Soit x un sommet du graphe G et e notons x (A), lopration qui ajoute ` A tous les arcs (y, z) tels que y est un prdcesseur e a e e de x, et z un successeur : x (A) = A {(y, z), (y, x) A et (x, z) A}. Cette opration vrie les proprits suivantes, que nous admettrons : e e ee Proprit 6.3.1. e e x (x (A)) = x (A), et, pour tout couple de sommets (x, y) : x (y (A)) = y (x (A)). Si lon consid`re litre de laction des xi sur A, on voit aisment que e ee e x1 (x2 (. . . (xn (A)) . . .)) A . Mieux : Proposition 6.3.1. La fermeture transitive A de G est donne par e A = x1 (x2 (. . . (xn (A)) . . .)). Preuve. Il reste ` montrer A x1 (x2 (. . . (xn (A)) . . .)). Soit f , un chemin joignant deux a 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`s la proprit ci-dessus, on peut permuter e ee lordre des yi dans cette criture, donc on peut considrer par exemple que les yi sont e e & M. Finiasz 91

6.4 Parcours de graphes

ENSTA cours IN101

ordonns suivant leurs numros croissants. Pour (i1 , . . . , ik ) {1, . . . , n}, avec, si 1 e e < 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 ` un A(i1 ,...,ip ) , pour un p-uplet (i1 , . . . , ip ) a dlments de {1, . . . , n}. Or, pour tout tel p-uplet, on a : ee A(i1 ,...,ip ) A(1,...,n) = x1 (x2 (. . . (xn (A)) . . .)). Donc tout chemin dans G (et tout arc dans A ) appartient ` x1 (x2 (. . . (xn (A)) . . .)). a Cette expression de la fermeture transitive dun graphe donne lieu ` lalgorithme de a 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 implmentent exactement lopration e e xk (A) ; en eet, la matrice dadjacence M de G est construite ` partir de celle de G a 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 au milieu de lalgorithme. Clairement, la complexit e e 3 de lalgorithme de Roy-Warshall est en (n ) oprations boolennes. e e Remarque : on obtient la fermeture rexive-transitive de G en ajoutant ` la matrice e a dadjacence de G la matrice Identit dordre n. e

6.4

Parcours de graphes

Nous tudions ici les algorithmes permettant de parcourir un graphe quelconque G, e cest-`-dire de visiter tous les sommets de G une seule fois. Il existe essentiellement deux a mthodes de parcours de graphes que nous exposons ci-apr`s. Chacune delles utilise la e e 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

Anne 2010-2011 e

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 dextrmit x. La longueur dun tel chemin e e est appel la profondeur de x dans larborescence. Dans une arborescence, tout sommet, e sauf la racine, admet un unique prdcesseur. On a donc |A| = |S| 1. Par analogie avec e e la terminologie employe pour les arbres, le prdcesseur dun sommet est appel son p`re, e e e e e les successeurs tant alors appels ses ls. La dirence entre une arborescence et un arbre e e e tient seulement au fait que, dans un arbre, les ls dun sommet sont ordonns. e On peut montrer quun graphe connexe sans cycle est une arborescence. Donc si G est un graphe sans cycle, G est aussi la fort constitue par ses composantes connexes. e e Une arborescence est reprsente par son vecteur p`re, [n], o` n est le nombre de e e e u sommets de larborescence, tel que [i] est le p`re du sommet i. Par convention, [r] = NULL. e

6.4.2

Parcours en largeur

Une arborescence souvent associe ` un graphe quelconque est larborescence des plus e a courts chemins. Cest le graphe dans lequel ne sont conserves que les artes appartenant e e ` un plus court chemin entre la racine et un autre sommet. On peut la construire grce ` a a a un parcours en largeur dabord du graphe (breadth-rst traversal en anglais). Le principe est le suivant : on parcourt le graphe ` partir du sommet choisi comme racine en visitant a tous les sommets situs ` distance (i.e profondeur) k de ce sommet, avant tous les sommets e a situs ` distance k + 1. e a Dnition 6.3. Dans un graphe G = (S, A), pour chaque sommet x, une arborescence des e plus courts chemins de racine x est une arborescence (Y, B, x) telle que un sommet y appartient ` Y si, et seulement si, il existe un chemin dorigine x et a dextrmit y. e e la longueur du plus court chemin de x ` y dans G est gale ` la profondeur de y dans a e a 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. Thor`me 6.4.1. Pour tout graphe G = (S, A), et tout sommet x de G, il existe une e e 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`re la suite {Yi }i densembles de sommets suivante : e Y0 = {x}. Y1 = Succ(x), lensemble des successeurs 2 de x.
2. Si (x, x) A, alors on enl`ve x de cette liste de successeurs. e

& M. Finiasz

93

6.4 Parcours de graphes

ENSTA cours IN101

Pour i 1, Yi+1 est lensemble obtenu en considrant tous les successeurs des sommets e i de Yi qui nappartiennent pas ` j=1 Yj . a Pour chaque Yi , i > 0, on dnit de plus lensemble Bi des arcs dont lextrmit est dans e e e Yi et lorigine dans Yi1 . Attention Bi ne contient pas tous les arcs possibles, mais un seul arc par lment Yi : on veut que chaque lment nait quun seul p`re. On pose ensuite de ee e ee 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`s la remarque ci-dessus. e Cette preuve donne un algorithme de construction de larborescence des plus courts chemins dun sommet donn : comme pour le parcours en largeur dun arbre on utilise e une le qui g`re les ensembles Yi , i.e. les sommets ` traiter (ce sont les sommets qui, ` un e a a moment donn de lalgorithme, ont t identis comme successeurs, mais qui nont pas e ee e encore t parcourus ; autrement dit, ce sont les sommets en attente ). Par rapport ` un ee a parcours darbre, la prsence ventuelle de cycles fait que lon utilise en plus un coloriage e e des sommets avec trois couleurs : les sommets en blanc sont ceux qui nont pas encore t ee traits (au dpart, tous les sommets, sauf le sommet x choisi comme racine, sont blancs). e e Les sommets en gris sont ceux de la le, i.e. en attente de traitement, et les sommets en noir sont ceux dj` traits (ils ont donc, ` un moment, t dls). On dnit aussi un ea e a ee e e e tableau d qui indique la distance du sommet considr ` x ; en dautres termes, d[v] est la eea profondeur du sommet v dans larborescence en cours de construction (on initialise chaque composante de d ` ). a Selon la structure de donne utilise pour reprsenter le graphe, la faon de programmer e e e c cet algorithme peut varier et la complexit de lalgorithme aussi ! On se place ici dans le e cas le plus favorable : on a une structure de donne similaire ` un arbre, o` chaque sommet e a u du graphe contient un pointeur vers la liste de ses successeurs, et en plus, les sommets du graphe sont tous indexs par des entiers compris entre 0 et n. e
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 t initialis */ e e e color = (int* ) malloc(n*sizeof(int ));

94

F. Levy-dit-Vehel

Anne 2010-2011 e
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`re */ e } } 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 t atteint, on fixe son e e p`re et on le met dans la file de noeuds ` traiter. */ e a 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 ` t rduite ` son minimum : on a en fait deux les, aee e a lune dans laquelle on ne fait que retirer des lments avec le fonction pop et lautre dans ee laquelle on ajoute des lments avec la fonction push. La fonction swap queues permet ee dchanger ces deux les et la fonction queue is empty retourne vrai quand la premi`re e e le est vide (on na plus de sommets ` retirer). a Chaque parcours en largeur ` partir dun sommet fournissant une seule composante a connexe du graphe, si le graphe en poss`de plusieurs il faudra ncessairement appeler la e e a fonction minimum spanning tree plusieurs fois avec comme argument ` chaque fois un sommet x non encore visit. Si on se donne un tableau vertices contenant des poine teurs 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 ` un graphe. Les sommets 0 et 5 a sont des racines. Les arcs en pointills ne font pas partie dune arborescence. e
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 t colori en noir */ e e e minimum_spanning_tree(vertices[i], nb); } } }

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

96

F. Levy-dit-Vehel

Anne 2010-2011 e

Chapitre 6. Graphes

6.4.3

Parcours en profondeur

Alors que le parcours en largeur est essentiellement itratif (implmentation par le), le e e parcours en profondeur est de nature rcursive 3 . Le principe du parcours en profondeur est e de visiter tous les sommets en allant dabord le plus profondment possible dans le graphe. e Un syst`me de datation (utilisant deux tableaux beg[] et end[]) permet de mmoriser e e les dates de dbut et de n de traitement dun sommet. Lalgorithme de parcours cre e e 4 une fort par un procd rcursif. Chaque arborescence de la fort est cre ` partir e e e e e ee a dun sommet x par lalgorithme suivant (le code couleur utilis pour colorier les sommets e a la mme signication que dans le cas du parcours en largeur - les sommets gris tant e e 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 implmentation du parcours en profondeur, reprenant les mmes structures que e e limplmentation du parcours en largeur. e
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 t initialis */ e e 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`re */ e 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 itrative de lalgorithme peut tre obtenue en utilisant une pile. e e 4. Cest la fort correspondant aux arcs eectivement utiliss pour explorer les sommets, donc, mme e e e si le graphe est connexe, il se peut quun parcours en profondeur de celui-ci produise une fort (cf. Fie gure 6.6). En revanche, si le graphe est fortement connexe, on est certain davoir un seul arbre dans cette fort. e

& 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 Fort engendre lors du parcours en profondeur dun graphe. Le e e graphe de dpart est connexe, mais deux arbres sont ncessaires pour le reprsenter e e e enti`rement. Les arcs en pointills sont ceux qui ne font partie daucune arborescence. e e Ici, les sommets ont t parcourus dans lordre : 0, 3, 4, 1, 6, 7, 2 puis 5, 8. ee
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 a t trouv */ e e e e father[tmp->vert->num] = x->num; depth_first_spanning_tree(tmp->vert); } tmp = tmp->next; } /* une fois tous les successeurs traits e 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 fort. On utilise donc la fonction suivante : e
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 t colori en noir */ e e e depth_first_spanning_tree(vertices[i]); }

98

F. Levy-dit-Vehel

Anne 2010-2011 e
9 10

Chapitre 6. Graphes

} }

Le parcours en profondeur passe une seule fois par chaque sommet et fait un nombre doprations proportionnel au nombre darcs. La complexit dun tel parcours est donc en e e (n + |A|). Application au labyrinthe. Il est possible de reprsenter un labyrinthe par un graphe, e dont les sommets sont les embranchements, ceux-ci tant relis selon les chemins autoriss. e e e 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 la sortie. Ce retour sur e ses pas est pris en charge par la rcursivit. e e Le parcours en largeur quand ` lui modliserait plutt un groupe de personnes dans un a e o labyrinthe : au dpart, le groupe est au point dentre du labyrinthe, puis il se rpartit de e e e faon ` ce que chaque personne explore un embranchement adjacent ` lembranchement o` c a a u il se trouve. Il modlise galement un parcours de labyrinthe eectu par un ordinateur, e e e dans lequel le sommet destination (la sortie) est connu, et pour lequel il sagit de trouver le plus court chemin de lorigine (entre du labyrinthe) ` la destination, i.e. un chemin e a particulier dans larborescence des plus courts chemins de racine lorigine.

6.5
6.5.1

Applications des parcours de graphes


Tri topologique

Les graphes orients sans circuit sont utiliss dans de nombreuses applications pour e e reprsenter des prcdences entre v`nements (on parle alors de graphe des dpendances). Le e e e e e e tri topologique dun graphe consiste ` ordonnancer les tches reprsentes par les sommets a a e e du graphe selon les dpendances modlises par ses arcs. Par convention, une tache doit e e e tre accomplie avant une autre, si un arc pointe du sommet correspondant ` cette tache e a vers le sommet correspondant ` lautre. Autrement dit, tant donn un graphe orient a e e 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 extrmit. Il peut tre vu comme un alignement e e e des sommets de G le long dune ligne horizontale, de mani`re que tous les arcs soient e orients de gauche ` droite. e a Le parcours en profondeur dun graphe permet de rsoudre le probl`me du tri topoloe e gique. Lide est deectuer un parcours en profondeur du graphe, puis de retourner la liste e des sommets dans lordre dcroissant de leur date de n de traitement. En eet, si (u, v) e est un arc (et donc si la tche reprsente par u doit tre eectue avant celle reprsente a e e e e e e par v) alors, dans lalgorithme de parcours en profondeur, le sommet v aura ncessairement e ni dtre trait avant le sommet u : un sommet na jamais ni dtre trait avant que tous e e e e ses successeurs naient t traits. Lalgorithme de tri topologique est donc le suivant : ee e & 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 linaire : on utilise e 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 (|S| + |A|), i.e. linaire en la taille du e e graphe. Test de Cyclicite dun Graphe De faon similaire, on peut tester si un graphe est cyclique par un parcours en proc fondeur : en eet, si un graphe poss`de un cycle, cela veut dire que, dans une arboe rescence de recouvrement, un sommet x est lorigine dun arc dont lextrmit est un e e anctre de x dans cette arborescence. Dans le parcours en profondeur, cela veut dire e que ` un moment, le successeur dun sommet gris sera gris. Il sut donc de modia er lg`rement lalgorithme de parcours en profondeur pour ne pas seulement tester si e e la couleur dun sommet est blanche, mais aussi tester si elle est grise et acher un message en consquence. e

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 dcomposer en composantes fortement connexe disjointes : un ensemble de e 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 composes dun seul e 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

Anne 2010-2011 e

Chapitre 6. Graphes

fondeur permet de retrouver toutes les composantes fortement connexes. Cest lalgorithme de Tarjan, invent en 1972. Le principe de lalgorithme est le suivant : e on eectue un parcours en profondeur du graphe, ` chaque fois que lon traite un nouveau sommet on lui attribue un index (attribu par a e ordre croissant) et un lowlink initialis ` la valeur de lindex et on ajoute le sommet ea ` une pile, a le lowlink correspond au plus petit index accessible par un chemin partant du sommet en question, donc ` la n du traitement dun sommet on met ` jour son lowlink pour a a tre le minimum entre les lowlink de tous ses successeurs et lindex du sommet courant, e chaque fois quun successeur dun sommet est dj` colori en gris on met ` jour le ea e a lowlink de ce sommet en consquence, e ` chaque fois que lon a ni de traiter un sommet dont le lowlink est gal ` lindex on a a e a trouv une composante fortement connexe. On peut retrouver lensemble des lments e ee de cette composante en retirant des lments de la pile jusqu` atteindre le sommet ee a courant. Cet algorithme est donc un peu plus compliqu que les prcdents, mais reste relativee e e ment simple ` comprendre. Si on a un graphe acyclique, ` aucun moment un successeur a a dun sommet naura un index plus petit que le sommet courant. Du coup, les lowlink restent toujours gaux ` lindex du sommet et ` la n du traitement de chaque sommet une e a a nouvelle composant connexe a t trouve : cette composante contient juste le sommet couee e rant et on retrouve bien le rsultat attendu : un graphe acyclique contient n composantes e fortement connexes composes dun seul sommet. e Maintenant, si le graphe contient un cycle, tous les lments de ce cycle vont avoir leur ee lowlink gal ` lindex du premier sommet visit. Donc, un seul sommet du cycle aura un e a e son index gal ` son lowlink : le premier visit, qui est donc aussi le plus profond dans e a e la pile. Tous les lments qui sortiront de la pile avant lui sont alors les autres sommets ee de la composante fortement connexe forme par le cycle. Voici le code correspondant ` e a lalgorithme de Tarjan (un exemple dexcution est visible sur la Figure 6.7) : e
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; /* initialiss ` 0 */ e a /* initialiss ` -1 */ e a /* initialiss ` -1 */ e a

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 t visit, e e e on le visite */ Tarjan(tmp->vert); /* et on met ` jour le lowlink de x */ a 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 un cycle */ e 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 dpile jusqu` retrouver x. */ e a printf("CFC : "); do { tmp2 = pop(); color[tmp2->num] = 2; printf("%d,", tmp2->num); while (tmp2 != x) printf("\n"); } }

Notons que index joue exactement le mme rle que la variable beg dans le parcours en e o 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 forcment atteindre e 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`re ici un graphe G = (S, A) pondr, i.e. ` chaque arc est associe une e ee a e valeur appele poids. On appellera poids dun chemin la somme des poids des arcs qui le e composent. Le probl`me est alors, tant donn deux sommets, de trouver un chemin de e e e poids minimal reliant ces deux sommets (sil en existe un). La rsolution de ce probl`me ` beaucoup dapplications, par exemple pour le calcul du e e a plus court chemin dune ville ` une autre (en passant par des routes dont on conna la a t longueur) ou le calcul dun chemin de capacit maximale dans un rseau de communication e e (dans lequel le taux de transmission dun chemin est gal au minimum des taux de transe mission de chaque liaison intermdiaire) sont des exemples de situations dans lesquelles ce e probl`me intervient. e An de traiter de mani`re uniforme les dirents ensembles de dnition pour les e e e poids, induits par lapplication (lensemble des nombres rels, lensemble {vrai, f aux}...), e on consid`re la situation gnrale o` les poids appartiennent ` un semi-anneau S(, , , ), e e e u a cest-`-dire vriant les proprits suivantes : a e ee 102 F. Levy-dit-Vehel

Anne 2010-2011 e

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 dexcution de lalgorithme de Tarjan sur un petit graphe ` e a trois composantes fortement connexes. On explore dabord le sommet 0 et on voit ensuite lvolution des direntes variables au fur et ` mesure de lexploration des e e a 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), dlment neutre . On impose de plus qutant donne une squence ee e e e innie dnombrable s1 , . . . , si , . . . dlments de S, s1 s2 . . . S. e ee est une loi de composition interne associative, dlment neutre . ee est absorbant pour . Pour tout x S, on note x , llment de S dni par ee e x = x x x x x x . . . Par exemple, dans le cas du probl`me du plus court chemin reliant une ville ` une e a 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 tant le minimum e () des longueurs des chemins existants ; ` noter que, pour tout x, on a ici x = 0). a Soit donc S(, , , ), un semi-anneau, et soit p, une fonction de A dans S, qui associe un poids ` chaque arc du graphe. On prolonge p ` SS par p(i, j) = si (i, j) A. On tend a a e galement p aux chemins en dnissant, pour tout chemin (s1 , . . . , sk ), p((s1 , . . . , sk )) = e e p((s1 , s2 )) p((s2 , s3 )) . . . p((sk1 , sk )). On cherche ` dterminer le cot minimal (en a e u 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 ` dterminer a e i,j = chemin {chemin de i a j} p(chemin). Il est clair que, pour un graphe quelconque, il nest en gnral pas possible dnumrer e e e e tous les chemins reliant deux sommets. Pour calculer la matrice = (i,j )i,j , il existe un algorithme d ` Aho, Hopcroft et Ullman, qui admet une complexit en (n3 ), n tant le ua e e nombre de sommets de G. Si on appelle Best(x,y) la fonction qui calcule xy, Join(x,y) la fonction x y, Star(x) la fonction x et elem le type des lments de S, on peut alors ee crire le code de lalgorithme AHU (qui ressemble beaucoup ` lalgorithme de Roy-Warshall e a vu prcdemment). Il prend en argument la matrice M telle que M[i][j] est le poids de e e larte reliant le sommet i au somment j : e
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])); } } } }

` Proposition 6.5.1. A la n de lexcution de lalgorithme AHU, la matrice M a t modie e ee e 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 dnie par i,j = M[i][j]. e 104 F. Levy-dit-Vehel

Anne 2010-2011 e
(0)

Chapitre 6. Graphes
(0)

Preuve. Notons i,j , la valeur de la matrice M passe en argument : i,j = p(i, j) ou . e (k) Considrons la suite ( )k de matrices dnie par rcurrence par e e e i,j = i,j
(k) (k1)

(i,k

(k1)

(k,k ) k,j
(k1) (k)

(k1)

).

Alors nous allons prouver par rcurrence que le coecient i,j est gal au cot minimal dun e e u chemin reliant le sommet i au sommet j, en ne passant que par des sommets intermdiaires e de numro infrieur ou gal ` k. e e e a (k) En eet, cette proprit des i,j est vraie pour k = 0. Soit k 1, et supposons cette ee proprit vraie pour k 1. Chaque chemin reliant le sommet i au sommet j en ne passant ee que par des sommets intermdiaires de numro infrieur ou gal ` k peut soit ne pas passer e e e e a (k1) par k - auquel cas, par hypoth`se de rcurrence, son cot minimal est gal ` i,j - soit tre e e u e a e dcompos en un chemin de i ` k, dventuels circuits partant et arrivant en k, et un chemin e e a e de k ` j. Par hypoth`se de rcurrence, les cot minimaux de ces chemins intermdiaires a e e u e (k1) (k1) (k1) sont resp. i,k , (k,k ) et k,j . Le cot minimal dun chemin reliant i ` j en ne passant u a que par des sommets intermdiaires de numro infrieur ou gal ` k est donc donn par la e e e e a e formule ci-dessus. (n) A la n de lalgorithme, la matrice (i,j ) a t calcule, qui correspond ` la matrice des ee e a cots minimaux des chemins reliant deux sommets quelconques du graphe, en ne passant u que par des sommets intermdiaires de numro infrieur ou gal ` n, i.e. par nimporte e e e e a quel sommet. Dans limplmentation de lalgorithme donne ici on nutilise quune seule matrice, donc e e en calculant les coecients de la n de la matrice ` ltape k ceux du dbut ont dj` a e e ea t modis depuis ltape k 1. La matrice M ne suit donc pas exactement la formule ee e e de rcurrence de la preuve, mais le rsultat nal reste quand mme identique. e e e 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 spcique du calcul de plus courts chemins, i.e. lorsque S(, , , ) = e {R }(min, +, , 0), lalgorithme de Aho-Hopcroft-Ullman est plus connu sous le nom dalgorithme de Floyd-Warshall. Lalgorithme AHU calcule le cot minimal dun chemin entre deux sommets quelconques u du graphe, sans fournir un chemin qui le ralise. Nous prsentons ci-apr`s un algorithme e e e permettant de trouver eectivement un tel chemin. Calcul dun chemin de co t minimal. Soit G = (S, A), un graphe sans circuit u reprsent par sa matrice dadjacence, et soit p, une fonction (poids) dnie sur S S e e e comme ci-dessus. On suppose ici que cette fonction vrie p(i, i) = 0. e On suppose galement que lon a calcul les cots minimaux des chemins entre tous e e u les couples de sommets, i.e. que lon dispose de la matrice = (i,j )i,j . Pour calculer ces chemins, on dnit une matrice de liaison = (i,j )i j , de la mani`re suivante : i,j = e e & 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 prdcesseur de j sur e e un chemin de cot minimal issu de i. u La connaissance de , permet dimprimer un chemin de cot minimal entre deux somu mets i et j de G avec lalgorithme rcursif suivant ( est remplac par -1) : e e
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 ` %d\n", i, j); a } else { print_shortest_path(Pi, i, Pi[i][j]); printf(",%d", j); } } }

Sous-Graphe de Liaison On dnit le sous-graphe de liaison de G pour i par G,i = (S,i , A,i ), o` : e u S,i = {j S, i,j = } {i}, A,i = {(i,j , j), j S,i \ {i}}. Dans le cas o` le poids est donn par la longueur, on peut montrer que G,i est une u e 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 mme temps. Le principe est le mme que dans lalgorithme initial, i.e. on calcule des e e suites de matrices en considrant des sommets intermdiaires dun chemin de cot minimal. e e u (k) (0) (1) (n) Plus prcisment, on dnit la squence , , . . . , , o` i,j est le prdcesseur du e e e e u e e sommet j dans un chemin de cot minimal issu de i et ne passant que par des sommets u intermdiaires de numro infrieur ou gal ` k. On a bien entendu (n) = , et on peut e e e e a (k) exprimer i,j rcursivement par : e { 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 utilises dans la preuve de la proposition 6.5.1 : on a ici i,j = p(i, j). e Au nal, on obtient AHU link, la version modie de AHU calculant aussi la matrice . e Comme lalgorithme AHU de dpart cet algorithme prend en argument des matrices dj` e ea initialises : i,j = 1 si (i, j) A et i,j = i si (i, j) A. e / 106 F. Levy-dit-Vehel

Anne 2010-2011 e

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 opration utile dans plusieurs doe maines de linformatique. Une application directe est la recherche (ecace) de squences e dacides amins dans de tr`s longues squences dADN, mais dautres applications sont e e e moins videntes : par exemple, lors de la compilation dun programme on doit identier e certains mots clef (for, if, sizeof...), identier les noms de variables et de fonctions... Tout cela revient en fait ` une recherche dune multitude de motifs en parall`le. a e

7.1

Dnitions e

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`re galement le mot vide e e not , qui ne contient aucun symbole. La longueur dun mot est le nombre de symboles e qui le compose : le mot vide est donc de longueur 0. Un mot est dit prxe dun autre e si ce mot appara au dbut de lautre (mot est prxe de motif ). De mme il est suxe t e e e dun autre sil appara ` la n (tif est suxe de motif ). t a Les mots tant de longueur nie mais quelconque il semble naturelle de les coder avec e une structure de liste, cependant, par soucis decacit, il seront en gnral cods avec e e e e des tableaux. Dans ce contexte, le probl`me de la recherche de motif (pattern-matching en e anglais) consiste simplement ` trouver toutes les occurrences dun mot P dans un texte T . a Soient donc T de longueur n cod par T[0]...T[n-1] et P de longueur m cod par e e P[0]...P[m-1]. On consid`re que les lments de lalphabet sont cods par des int. La e ee e recherche de motifs va consister ` trouver tous les dcalages s [0, n m] tels que : a e j [0, m 1], P [j] = T [s + j]. Lnonc de ce probl`me est donc tr`s simple, en revanche, les algorithmes ecaces pour e e e e y rpondre le sont un peu moins. e 109

7.2 Lalgorithme de Rabin-Karp

ENSTA cours IN101

Un premier algorithme na f. Lalgorithme le plus na pour la recherche de motif est f dduit directement de lnonc : on essaye tous les dcalages possibles, et pour chaque e e e e dcalage on regarde si le texte correspond au motif. Voici le code dun tel algorithme : e
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 ` la position %d.", i); e a } } }

Cet algorithme a une complexit dans le pire cas en (nm) qui nest clairement pas optie male : linformation trouve ` ltape s est totalement ignore ` ltape s + 1. e a e e a e

7.2

Lalgorithme de Rabin-Karp

Lide de lalgorithme de Rabin-Karp est de reprendre lalgorithme na mais de remplae f, cer la comparaison de mots par une comparaison dentiers. Pour cela, lide et de considrer e e le motif recherch comme un entier cod en base d, o` d est le nombre dlments de . e e u ee Ainsi, ` un motif de longueur m est associ un entier compris entre 0 et dm 1. De mme, a e e pour chaque dcalage, on va dnir un entier correspondant au m symboles de T partant e e de ce dcalage. On dnit donc : e e m1 p= P [i]di ,
i=0 m1 i=0

s [0, n m],

ts =

T [s + i]di .

Une fois ces entiers dnis, la recherche de motif consiste simplement ` comparer p ` e a a chacun des ts . Cependant, mme si on consid`re que les oprations sur les entiers se font e e e toujours en temps constant, cette mthode ne permet pas encore de gagner en complexit e e car le calcul de chacun des ts a une complexit en (m) et donc calculer tous les ts cote e u (nm). An damliorer cela on va calculer les ts de faon rcursive : entre ts et ts+1 un e c e symbole est ajout et un autre enlev. On a donc : e e ts + dm1 T [s + m]. ts+1 = d 110 F. Levy-dit-Vehel

Anne 2010-2011 e

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 en (n + m) et le cot total de la recherche de motif est aussi (n + m). e u 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 dcalages */ e for (i=0; i<n-m; i++) { if (t == p) { printf("Motif trouv ` la position %d.", i); e a } t = t/d + h*T[i+m]; } if (t == p) { printf("Motif trouv ` la position %d.", n-m); e a } }

En pratique, cette mthode est tr`s ecace quand les entiers t et p peuvent tre reprsents e e e e e par un int, mais d`s que m et d grandissent cela nest plus possible. Lutilisation de grands e entiers fait alors perdre lavantage gagn car un opration sur les grands entiers aura un e e cot en (m log(d)) et la complexit totale de lalgorithme devient alors (nm log(d)) u e (notons que cette complexit est identique ` celle de lalgorithme na : le facteur log(d) e a f correspond au cot de la comparaison de deux symboles de qui est nglig dans la u e e complexit de lalgorithme na e f). Toutefois, une solution existe pour amliorer cela : il sut de faire tous les calculs e 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 prsent au dcalage s, e e chaque fois que le calcul tombe faux on est certain que le motif nest pas prsent avec un e dcalage s. Quand le calcul tombe juste on peut alors vrier que le motif est bien prsent e e e avec lalgorithme na On obtient alors lalgorithme suivant : f.
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 dcalages */ e for (i=0; i<n-m; i++) { if (t == p) { /* on vrifie avec lalgorithme naf */ e for (j=0; j<m; j++) { if (T[j+i] != P[j]) { break; } } if (j == m) { printf("Motif trouv ` la position %d.", i); e a } } 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 ` la position %d.", n-m); e a } }

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

7.3

Automates pour la recherche de motifs

Les automates nis sont des objets issus de linformatique thorique particuli`rement e e adapts ` la rsolution de certains probl`mes : typiquement, la recherche de motif. Apr`s e a e e e 112 F. Levy-dit-Vehel

Anne 2010-2011 e
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 dni ce quest un automate ni, nous verrons comment obtenir un algorithme de e recherche de motif qui aura toujours (pas uniquement dans les cas favorables) une complexit en (n + m||), donc tr`s proche de loptimal quand n est grand par rapport ` e e 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 tats. Lautomate reoit une suite discr`te e c e de signaux, chaque signal provoquant un changement dtat ou transition. En ce sens, un e automate est un graphe dont les sommets sont les tats possibles et les arcs les transitions. e Il existe aussi des automates ayant un nombre inni dtats, mais nous ne considrons e e ici que des automates nis. Dans la suite, le terme automate dsignera donc toujours un e automate ni. Plus formellement, un automate M est la donne de : e un alphabet ni , un ensemble ni non vide dtats Q, e une fonction de transition : Q Q, un tat de dpart not q0 , e e e un ensemble F dtats naux, F Q. e On notera M = (, Q, , q0 , F ). Un exemple de tel automate est dessin en Figure 7.1. e Fonctionnement. Lautomate fonctionne de la mani`re suivante : tant dans ltat q e e e Q et recevant le signal , lautomate va passer ` ltat (q, ). Notons , le mot vide a e de . On tend en une fonction : Q Q prenant en argument un tat et un mot e e (vide ou non) et retournant un tat : e (q, ) = q, et (q, wa) = ((q, w), a), q Q, w , a . On dnit alors le langage reconnu par M comme tant le sous-ensemble de des mots e e dont la lecture par M conduit ` un tat nal ; autrement dit a e L(M ) = {w , (q0 , w) F }. & 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 raliser ecacement la ree cherche de motifs dans un texte. Soit donc un alphabet ni, et w avec |w| = m le mot que lon va chercher. Nous cherchons ` dterminer toutes les occurrences de w dans a e un texte t de longueur n. Pour cela, nous allons construire un automate M reconnaissant le langage w . Lensemble des tats de lautomate est Q = {S0 , S1 , . . . , Sm }, ltat initial est S0 , et e e il poss`de un unique tat nal F = {Sm }. Avant de dnir la fonction de transition, on e e e introduit la fonction : Q, dnie par (u) = max{0 i m, u = wi }, o` wi est e u le prxe de longueur i de w. Ainsi, (u) est la longueur du plus long prxe de w qui soit e e suxe de u. On cherche ` dnir (et donc ) de faon ` raliser les quivalences suivantes a e c a e e (o` u ) : u 1. Pour 0 i < m, (S0 , u) = Si si et seulement si w nappara pas dans u et (u) = i. t 2. (S0 , u) = Sm si et seulement si w appara dans u. t Lquivalence 2. correspond exactement ` la fonction de lautomate M : on doit ate a teindre ltat nal si et seulement si le motif est apparu dans le texte. Lquivalence 1. e e nous sert ` construire lautomate : chaque fois lon rajoute un symbole x ` u, on passe de a a la case S(u) ` la case S(ux) , et d`s que lon atteint la case Sm on y reste. On obtient alors a e un automate tel que celui de la Figure 7.2. En pratique, pour construire ecacement cet automate, le plus simple est de procder e par rcurrence. Pour construire un automate qui reconna le mot wx, on part de lautomate e t qui reconna le mot w de longueur m et on ltend pour reconna wx de longueur m + 1. t e tre Supposons que lautomate Mw reconnaissant w soit construit. Pour construire lautomate Mwx on commence par ajouter le nud nal Sm+1 ` Mw . Ne reste plus ensuite qu` calculer a a tous les liens partant de la Sm (la derni`re case de lautomate Mw , donc lavant derni`re e e de Mwx ). Un lien est simple ` calculer : la transition en ajoutant x pointe vers Sm+1 . Les a autre liens peuvent tre calculs simplement en utilisant lautomate Mw prcdemment e e e e 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

Anne 2010-2011 e

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 tre calcul en utilisant Mw . Lautomate e e Mw termine dans ltat S(vt) quand on lui donne le motif vt en entre : il sut alors de e e faire pointer le lien index par t de Sm au S(vt) obtenu. e Il y a || 1 tels liens ` trouver et le cot pour trouver son extrmit est un parcours a u e e dautomate qui a une complexit (m). Cependant, les m 1 premiers caract`res sont e e communs pour les || 1 parcours ` eectuer. Il sut donc de faire un parcours de m 1 a caract`res puis de recopier les || 1 transitions issues de ltat que lon a atteint. De plus, e e le parcours des m 1 premiers caract`res correspond en fait ` lajout dun caract`re ` la e a e a suite des m 2 caract`res parcourus pendant la construction de Mw . Si lon a mmoris e e e ltat auquel aboutit le parcours de ces m 2 caract`res, il faut donc (||) oprations e e e pour construire lautomate Mwx ` partir de lautomate Mw . Le cot total de la construction a u 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, m sa longueur et sigma la taille de lalphabet ; le e tableau d qui est retourn correspond ` la fonction de transition avec d[i][j] = (Si , j). e a
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 dfinit les transitions de ltat 0 */ e e for (i=0; i<sigma; i++) { d[0][i] = 0; } d[0][P[0]] = 1; etat_mem = 0; /* on traite les tats suivants */ e 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 cot total de la recherche est donc (n + m||). u

7.3.3

Reconnaissance dexpression rguli`res e e

Les automates sont un outil tr`s puissant pour la recherche de motif dans un texte, mais e leur utilisation ne se limite pas ` la recherche dun motif x ` lintrieur dun texte. Une a ea e expression rguli`re est un motif correspondant ` un ensemble de cha e e a nes de caract`res. e Elle est elle mme dcrite par une cha de caract`re suivant une syntaxe bien prcise e e ne e e (qui change selon les langages, sinon les choses seraient trop simples !). Reconna une tre expression rguli`re revient ` savoir si le texte dentre fait partie des cha e e a e nes de caract`re e qui correspondent ` cette expression rguli`re. a e e La description compl`te dune syntaxe dexpression rguli`re est un peu complexe donc e e e ne sont donns ici que quelques exemples dexpressions (et les cha e nes de caract`res quelles e reprsentent), pour se faire une ide de la signication des dirents symboles. e e e a = la cha a uniquement ne abc = le mot abc uniquement . = nimporte quel symbole = les cha nes de longueur 1 a* = toutes les cha nes composes de 0 ou plusieurs a (et rien dautre) e a? = 0 ou une occurrence de a a+ = toutes les cha nes composes de au moins un a (et rien dautre) e .*a = toutes les cha nes se terminant par a [ac] = le caract`re a ou le caract`re c e e .*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`re autre que a ou b e Les combinaisons de ces direntes expressions permettent de dcrire des ensembles de e e cha nes tr`s complexes et dcider si un texte correspond ou pas ` une expression peut tre e e a e dicile. En revanche, il est toujours possible de construire un automate ni (mais des fois tr`s grand !) qui permet de dcider en temps linaire (en la taille du texte) si un texte e e e est accept par lexpression ou non. La complexit de la construction de cet automate e e peut en revanche tre plus leve. La Figure 7.3 donne deux exemples dautomates et les e e e expressions rguli`res quils acceptent. e e

116

F. Levy-dit-Vehel

Anne 2010-2011 e

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 rguli`res. e e

& M. Finiasz

117

S-ar putea să vă placă și