Vous êtes sur la page 1sur 124

e

ments dAlgorithmique
El
Ensta - in101
Franc
oise Levy-dit-Vehel & Matthieu Finiasz
Annee 2011-2012

Table des mati`


eres
1 Complexit
e
1.1 Denitions . . . . . . . . . . . . . . . . . . . . .
1.1.1 Comparaison asymptotique de fonctions
1.1.2 Complexite dun algorithme . . . . . . .
1.1.3 Complexite spatiale, complexite variable
1.2 Un seul probl`eme, quatre algorithmes . . . . . .
1.3 Un premier algorithme pour le tri . . . . . . . .
2 R
ecursivit
e
2.1 Conception des algorithmes . . . . . . . . .
2.2 Probl`eme des tours de Hano . . . . . . . . .
2.3 Algorithmes de tri . . . . . . . . . . . . . .
2.3.1 Le tri fusion . . . . . . . . . . . . . .
2.3.2 Le tri rapide . . . . . . . . . . . . . .
2.3.3 Complexite minimale dun algorithme
2.4 Resolution dequations de recurrence . . . .
2.4.1 Recurrences lineaires . . . . . . . . .
2.4.2 Recurrences de partitions . . . . .
2.5 Complements . . . . . . . . . . . . . . . .
2.5.1 Recursivite terminale . . . . . . .
2.5.2 Derecursication dun programme
2.5.3 Indecidabilite de la terminaison . .

.
.
.
.
.
.

.
.
.
.
.
.

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

.
.
.
.
.
.

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

.
.
.
.
.
.

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

.
.
.
.
.
.

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

3 Structures de Donn
ees
3.1 Tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.1.1 Allocation memoire dun tableau . . . . . . . . .
3.1.2 Complement : allocation dynamique de tableau
3.2 Listes chanees . . . . . . . . . . . . . . . . . . . . . . . .
3.2.1 Operations de base sur une liste . . . . . . . . . .
3.2.2 Les variantes : doublement chanees, circulaires...
3.2.3 Conclusion sur les listes . . . . . . . . . . . . . .
3.3 Piles & Files . . . . . . . . . . . . . . . . . . . . . . . . .
3.3.1 Les piles . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.

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

3.3.2

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

4 Recherche en table
4.1 Introduction . . . . . . . . . . . . . .
4.2 Table `a adressage direct . . . . . . .
4.3 Recherche sequentielle . . . . . . . .
4.4 Recherche dichotomique . . . . . . .
4.5 Tables de hachage . . . . . . . . . . .
4.6 Tableau recapitulatif des complexites

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

5 Arbres
5.1 Preliminaires . . . . . . . . . . . . . . . . . . . . . .
5.1.1 Denitions et terminologie . . . . . . . . . . .
5.1.2 Premi`eres proprietes . . . . . . . . . . . . . .
5.1.3 Representation des arbres . . . . . . . . . . .
5.2 Utilisation des arbres . . . . . . . . . . . . . . . . . .

5.2.1 Evaluation
dexpressions & parcours darbres .
5.2.2 Arbres Binaires de Recherche . . . . . . . . .
5.2.3 Tas pour limplementation de les de priorite
5.2.4 Tri par tas . . . . . . . . . . . . . . . . . . . .
5.3 Arbres equilibres . . . . . . . . . . . . . . . . . . . .
5.3.1 Reequilibrage darbres . . . . . . . . . . . . .
5.3.2 Arbres AVL . . . . . . . . . . . . . . . . . . .
6 Graphes
6.1 Denitions et terminologie . . . . . . . . . . . . . . .
6.2 Representation des graphes . . . . . . . . . . . . . . .
6.2.1 Matrice dadjacence . . . . . . . . . . . . . . .
6.2.2 Liste de successeurs . . . . . . . . . . . . . . .
6.3 Existence de chemins & fermeture transitive . . . . .
6.4 Parcours de graphes . . . . . . . . . . . . . . . . . .
6.4.1 Arborescences . . . . . . . . . . . . . . . . . .
6.4.2 Parcours en largeur . . . . . . . . . . . . . . .
6.4.3 Parcours en profondeur . . . . . . . . . . . . .
6.5 Applications des parcours de graphes . . . . . . . . .
6.5.1 Tri topologique . . . . . . . . . . . . . . . . .
6.5.2 Calcul des composantes fortement connexes
6.5.3 Calcul de chemins optimaux . . . . . . . . . .

.
.
.
.
.
.

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

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

.
.
.
.
.
.

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

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

.
.
.
.
.
.

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

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

.
.
.
.
.
.

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

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

.
.
.
.
.
.

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

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

.
.
.
.
.
.

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

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

.
.
.
.
.
.

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

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

.
.
.
.
.
.

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

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

.
.
.
.
.
.

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

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

.
.
.
.
.
.

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

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

.
.
.
.
.
.

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

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

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

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

7.3.1
7.3.2
7.3.3

Automates nis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113


Construction dun automate pour la recherche de motifs . . . . . . 114
Reconnaissance dexpression reguli`eres . . . . . . . . . . . . . . . 116

Chapitre 1
Complexit
e
La notion de complexite est centrale en algorithmique : cest grace `a elle que lon peut
denir ce quest un bon algorithme. La recherche dalgorithmes ayant une complexite plus
petite que les meilleurs algorithmes connus est un th`eme de recherche important dans toutes
les branches de linformatique et des mathematiques appliquees. Dans ce chapitre nous
voyons comment est denie cette notion de complexite.

1.1
1.1.1

D
enitions
Comparaison asymptotique de fonctions

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

0 f (n) c g(n).

Inversement, quand g(n) = O(f (n)), on utilise la notation :


f (n) = (g(n))

(1.2)

et, quand on a `a la fois les proprietes (1.1) et (1.2) :


f (n) = (g(n)).

(1.3)

Plus formellement, f (n) = (g(n)) si :


(c, c ) (R+ )2 ,

n0 N,

n n0 ,

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

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

1.1 Denitions

ENSTA cours IN101

Cependant, si f (n) g(n) on a f (n) = (g(n)). Enn, il est clair que f (n) = (g(n))
si et seulement si g(n) = (f (n)). Intuitivement, la notation revient `a oublier le
coecient multiplicatif constant de g(n).
Voici quelques exemples de comparaisons de fonctions :
n2 + 3n + 1 = (n2 ) = (50n2 ),
n/ log(n) = O(n),
50n10 = O(n10,01 ),
2n = O(exp(n)),
exp(n) = O(n!),

n/ log(n) = ( n),
log2 (n) = (log(n)) = (ln(n)).
On peut ainsi etablir une hierarchie (non exhaustive) entre les fonctions :
log(n)

1.1.2

n
n n n log(n) n2 n3 2n exp(n) n! nn 22

Complexit
e dun algorithme

On appelle complexite dun algorithme est le nombre asymptotique doperations de


base quil doit eectuer en fonction de la taille de lentree quil a `a traiter. Cette complexite est independante de la vitesse de la machine sur laquelle est execute lalgorithme :
la vitesse de la machine (ou la qualite de limplementation) peut faire changer le temps
dexecution dune operation de base, mais ne change pas le nombre doperations `a eectuer.
Une optimisation qui fait changer le nombre doperations de base (et donc la complexite)
doit etre vue comme un changement dalgorithme.

Etant
donnee la denition precedente, il convient donc de denir convenablement ce
quest une operation de base et comment lon mesure la taille de lentree avant de pouvoir
parler de la complexite dun algorithme. Prenons par exemple le code suivant, qui calcule
la somme des carres de 1 `a n :
1
2
3
4
5
6
7
8

unsigned int sum_of_squares(unsigned int n) {


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

Pour un tel algorithme on consid`ere que la taille de lentree est n et on cherche `a compter
le nombre doperations de base en fonction de n. Lalgorithme eectue des multiplications
et des additions, donc il convient de considerer ces operations comme operations de base.
Il y a au total n multiplications et n additions, donc lalgorithme a une complexite (n).
Le choix de loperation de base semble ici tout `a fait raisonnable car dans un processeur
2

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 1. Complexite

moderne laddition et la multiplication sont des operations qui prennent (1) cycles pour
etre eectues.
Imaginons maintenant que lon modie lalgorithme pour quil puisse manipuler des
grands entiers (plus grands que ce que le processeur peut manipuler dun seul coup) : le
co
ut dune addition ou dune multiplication va augmenter quand la taille des entiers va
augmenter, et ces operations nont alors plus un co
ut constant. Il convient alors de changer
doperation de base et de considerer non plus des multiplications/additions complexes mais
des operations binaires elementaires. Le co
ut de laddition de deux nombre entre 0 et n est
alors (log n) (le nombre de bits necessaires pour ecrire n) et le co
ut dune multiplication
2
(en utilisant un algorithme basique) est ((log n) ). Calculer la somme des carres de 1 `a
n quand n devient grand a donc une complexite (n(log n)2 ) operations binaires.
Comme vous le verrez tout au long de ce poly, selon le contexte, loperation de base `a
choisir peut donc changer, mais elle reste en general loperation la plus naturelle.
Ordres de grandeurs de complexit
es. Jusquici nous avons parle de complexite
asymptotique des algorithmes, mais pour une complexite donnee, il est important davoir
une idee des ordres de grandeurs des param`etres que lalgorithme pourra traiter. Un algorithme exponentiel sera souvent trop co
uteux pour de grandes entrees, mais pourra en
general tr`es bien traiter des petites entrees. Voici quelques chires pour se faire une idee :
eectuer 230 (environ un milliard) operations binaires sur un PC standard prend de
lordre de la seconde.
le record actuel de puissance de calcul fourni pour resoudre un probl`eme donne est de
lordre de 260 operations binaires.
aujourdhui en cryptographie, on consid`ere quun probl`eme dont la resolution necessite
280 operations binaires est impossible `a resoudre.
une complexite de 2128 operations binaires sera a priori toujours inatteignable dici
quelques dizaines dannees (cest pour cela que lon utilise aujourdhui des clefs de 128
bits en cryptographie symetrique, alors qu`a la n des annees 70 les clefs de 56 bits du
DES semblaient susamment solides).
Dans la pratique, un algorithme avec une complexite (n) pourra traiter des entrees
jusqu`a n = 230 en un temps raisonnable (cela depend bien s
ur des constantes), et un
3
algorithme cubique (de complexite (n )), comme par exemple un algorithme basique pour
linversion dune matrice nn, pourra traiter des donnees jusqu`a une taille n = 210 = 1024.
En revanche, inverser une matrice 220 220 necessitera des mois de calcul `a plusieurs milliers
de machines (`a moins dutiliser un meilleur algorithme...).

1.1.3

Complexit
e spatiale, complexit
e variable

Complexit
e spatiale. Jusquici, la seule complexite dont il a ete question est la complexite temporelle : le temps mis pour executer un algorithme. Cependant, il peut aussi etre
interessant de mesurer dautres aspects, et en particulier la complexite spatiale : la taille
memoire necessaire `a lexecution dun algorithme. Comme pour la complexite temporelle,
& M. Finiasz

1.1 Denitions

ENSTA cours IN101

seule nous interesse la complexite spatiale asymptotique, toujours en fonction de la taille


de lentree. Des exemples dalgorithmes avec des complexites spatiales dierentes seront
donnes dans la section 1.2.
Il est `a noter que la complexite spatiale est necessairement toujours inferieure (ou
egale) `a la complexite temporelle dun algorithme : on suppose quune operation decriture
en memoire prend un temps (1), donc ecrire une memoire de taille M prend un temps
(M ).
Comme pour la complexite temporelle, des bornes existent sur les complexites spatiales
atteignables en pratique. Un programme qui utilise moins de 220 entiers (4Mo sur une
machine 32-bits) ne pose aucun probl`eme sur une machine standard, en revanche 230 est `a
la limite, 240 est dicile `a atteindre et 250 est `a peu pr`es hors de portee.

Des Mode`les de Memoire Alternatifs


Vous pouvez etre amenes `a rencontrer des mod`eles de memoire alternatifs dans lesquels
les temps dacc`es/ecriture ne sont plus (1) mais dependent de la complexite spatiale
totale de lalgorithme. Supposons que la complexite spatiale soit (M ).
Dans un syst`eme informatique standard, pour acceder `a une capacite memoire de
taille M , il faut pouvoir adresser lensemble de cet espace. cela signie quune adresse
memoire (un pointeur en C), doit avoir une taille minimale de log2 M bits, et lire un
pointeur a donc une complexite (log M ). Plus un algorithme utilise de memoire, plus
le temps dacc`es `a cette memoire est grand. Dans ce mod`ele, la complexite temporelle
dun algorithme est toujours au moins egale `a (M log M ).
Certains mod`eles vont encore plus loin et prennent aussi en compte des contraintes
physiques : dans des puces memoires planaires (le nombre de couches superposees de
transistors est (1)) comme lon utilise `a lheure actuelle, le temps pour quune information
circule dun bout de la puce memoire jusquau processeur est au moins
egal `a
( M ). Dans ce mod`ele on peut borner la complexite temporelle par (M M ). La
constante (linverse de la vitesse de la lumi`ere) dans le est en revanche tr`es petite.
Dans ce cours, nous considererons toujours le mod`ele memoire standard avec des temps
dacc`es constants en (1).

Complexit
e dans le pire cas. Comme nous avons vu, la complexite dun algorithme
sexprime en fonction de la taille de lentree `a traiter. Cependant, certains algorithmes
peuvent avoir une complexite tr`es variable pour des entrees de meme taille : factoriser un
nombre va par exemple dependre plus de la taille du plus grand facteur que de la taille
totale du nombre.
Une technique detude des performances dun tel algorithme `a complexite variable
consiste `a examiner la complexite en temps du pire cas. Le temps de calcul dans le pire
cas pour les entrees x de taille n xee est deni par
T (n) = sup{x, |x|=n} T (x).
T (n) fournit donc une borne superieure sur le temps de calcul sur nimporte quelle
4

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 1. Complexite

entree de taille n. Pour calculer T (n), on neglige les constantes dans lanalyse, an de
pouvoir exprimer comment lalgorithme depend de la taille des donnees. Pour cela, on
utilise la notation O ou , qui permet de donner un ordre de grandeur sans caracteriser
plus nement. La complexite dans le pire cas est donc independante de limplementation
choisie.
Complexit
e moyenne. Une autre facon detudier les performances dun algorithme
consiste `a en determiner la complexite moyenne : cest la moyenne du temps de calcul sur
toutes les donnees dune taille n xee, en supposant connue une distribution de probabilite
(p(n)) sur lensemble des entrees de taille n :
Tm (n) =

pn (x)T (x).

x, |x|=n

On remarque que Tm (n) nest autre que lesperance de la variable aleatoire temps de calcul . Le cas le plus aise pour determiner Tm (n) est celui o`
u les donnees sont equidistribuees
dans leur ensemble de denition : on peut alors dans un premier temps evaluer le temps
de calcul T (x) de lalgorithme sur une entree x choisie aleatoirement dans cet ensemble,
le calcul de la moyenne des complexites sur toutes les entrees de lensemble sen deduisant
facilement 1 .
Lorsque la distribution (p(n)) nest pas la distribution uniforme, la complexite moyenne
dun algorithme est en general plus dicile `a calculer que la complexite dans le pire cas ;
de plus, lhypoth`ese duniformite peut ne pas reeter la situation pratique dutilisation de
lalgorithme.

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


nombre moyen (selon une distribution de probabilite sur les entrees) de fois que chaque
instruction est executee, en le multipliant par le temps propre `a chaque instruction, et en
faisant la somme globale de ces quantites. Elle est donc dependante de limplementation
choisie.
Trois dicultes se presentent lorsque lon veut mener `a bien un calcul de complexite
en moyenne : dabord, levaluation precise du temps necessaire `a chaque instruction peut
saverer dicile, essentiellement `a cause de la variabilite de ce temps dune machine `a
lautre.
Ensuite, le calcul du nombre moyen de fois que chaque instruction est executee peut etre
delicat, les calculs de borne superieure etant generalement plus simples. En consequence,
la complexite en moyenne de nombreux algorithmes reste inconnue `a ce jour.
Enn, le mod`ele de donnees choisi nest pas forcement representatif des ensembles de
donnees rencontres en pratique. Il se peut meme que, pour certains algorithmes, il nexiste
pas de mod`ele connu.
1. Cette hypoth`ese dequidistribution rend en eet le calcul de la complexite sur une donnee choisie
aleatoirement representatif de la complexite pour toutes les donnees de lensemble.

& M. Finiasz

1.2 Un seul probl`eme, quatre algorithmes

ENSTA cours IN101

Pour ces raisons, le temps de calcul dans le pire cas est bien plus souvent utilise comme
mesure de la complexite.

1.2

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

Pour comprendre limportance du choix de lalgorithme pour resoudre un probl`eme


donne, prenons lexemple du calcul des nombres de Fibonacci denis de la mani`ere suivante :
F0 = 0, F1 = 1, et, n 2, Fn = Fn1 + Fn2 .
Un premier algorithme pour calculer le n-i`eme nombre de Fibonacci Fn reprend exactement
la denition par recurrence ci-dessus :
1
2
3
4
5
6

unsigned int fibo1(unsigned int n) {


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

Lalgorithme obtenu est un algorithme recursif (cf. chapitre 2). Si lon note Cn , le
nombre dappels `a fibo1 necessaires au calcul de Fn , on a, C0 = C1 = 1, et, pour n 2,
Cn = 1 + Cn1 + Cn2 .
Si lon pose Dn = (Cn + 1)/2, on observe que Dn suit exactement la relation de recurrence
denissant la suite de Fibonacci (seules les conditions initiales di`erent).
La resolution de cette recurrence lineaire utilise une technique dalg`ebre classique (cf.
chapitre 2) : on calcule les solutions
de lequation
caracteristique de cette recurrence - ici

1+ 5
1 5
2

x x 1 = 0 - qui sont = 2 et = 2 . Dn secrit alors


Dn = n + n .
On determine et `a laide des conditions initiales. On obtient nalement
1
Dn = (n+1 n+1 ), n 0.
5

Etant
donne que Cn = 2Dn 1, la complexite - en termes de nombre dappels `a fibo1 du calcul de Fn par cet algorithme est donc (n ), cest-`a-dire exponentiel en n.
On observe que lalgorithme precedent calcule plusieurs fois les valeurs Fk , pour k < n,
ce qui est bien evidemment inutile. Il est plus judicieux de calculer les valeurs Fk , 2 k n
`a partir des deux valeurs Fk1 et Fk2 , de les stocker dans un tableau, et de retourner Fn .
6

F. Levy-dit-Vehel

Annee 2011-2012

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

Chapitre 1. Complexite

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 unite de mesure de complexite le temps de calcul dune operation
daddition, de soustraction ou de multiplication. La complexite de lalgorithme Fibo2 est
alors (n), i.e. lineaire en n (en eet, la boucle for comporte n 1 iterations, chaque
iteration consistant en une addition). La complexite est donc drastiquement reduite par
rapport `a lalgorithme precedent : le prix `a payer ici est une complexite en espace lineaire
((n) pour stocker le tableau fib).
On remarque maintenant que les n 2 valeurs Fk , 0 k n 3 nont pas besoin detre
stockees pour le calcul de Fn . On peut donc revenir `a une complexite en espace en (1) en
ne conservant que les deux derni`eres valeurs courantes Fk1 et Fk2 necessaires au calcul
de Fk : on obtient le troisi`eme algorithme suivant :
1
2
3
4
5
6
7
8
9
10

unsigned int fibo3(unsigned int n) {


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

Cet algorithme admet toutefois encore une complexite en temps de (n).


Un dernier algorithme permet datteindre une complexite logarithmique en n. Il est base
sur lecriture suivante de (Fn , Fn1 ), pour n 2 :
)
) (
)(
(
1 1
Fn1
Fn
.
=
Fn2
Fn1
1 0
En iterant, on obtient :
(

& M. Finiasz

Fn
Fn1

(
=

1 1
1 0

)n1 (

F1
F0

)
.
7

1.2 Un seul probl`eme, quatre algorithmes

ENSTA cours IN101


(

)
1 1
Ainsi, calculer Fn revient `a mettre `a la puissance (n 1) la matrice
.
1 0
La complexite en temps de lalgorithme qui en decoule est ici (log(n)) multiplications
de matrices carrees 2 2 : en eet, cest le temps requis par lalgorithme dexponentiation
square-and-multiply (cf. TD 01) pour calculer la matrice. Lespace n
ecessaire est celui
du stockage de quelques matrices carrees 2 2, soit (1). Lalgorithme fibo4 peut secrire
ainsi :
1
2
3
4
5

unsigned int fibo4(unsigned int n) {


unsigned int res[2][2];
unsigned int mat_tmp[2][2];
int i = 0;
unsigned int tmp;

6
7
8
9
10
11

/* on initialise le r
esultat avec la matrice */
res[0][0] = 1;
res[0][1] = 1;
res[1][0] = 1;
res[1][1] = 0;

12
13
14
15
16

/* cas particulier n==0 `


a traiter tout de suite */
if (n == 0) {
return 0;
}

17
18
19
20
21
22
23
24
25
26

/* on doit trouver le un le plus `


a gauche de n-1 */
tmp = n-1;
while (tmp != 0) {
i++;
tmp = tmp >> 1;
}
/* on d
ecr
emente i car la premi`
ere multiplication
a d
ej`
a
et
e faite en initialisant res
*/
i--;

27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

while (i > 0) {
/* on
el`
eve au carr
e */
mat_tmp[0][0] = res[0][0]*res[0][0] + res[0][1]*res[1][0];
mat_tmp[0][1] = res[0][0]*res[0][1] + res[0][1]*res[1][1];
mat_tmp[1][0] = res[1][0]*res[0][0] + res[1][1]*res[1][0];
mat_tmp[1][1] = res[1][0]*res[0][1] + res[1][1]*res[1][1];
/* on regarde la valeur du i`
eme bit de n-1
pour savoir si on doit faire une multiplication */
if (((n-1) & (1<<(i-1))) != 0) {
res[0][0] = mat_tmp[0][0] + mat_tmp[0][1];
res[0][1] = mat_tmp[0][0];
res[1][0] = mat_tmp[1][0] + mat_tmp[1][1];
res[1][1] = mat_tmp[1][0];
} else { /* on replace la matrice dans res */
res[0][0] = mat_tmp[0][0];

F. Levy-dit-Vehel

Annee 2011-2012
res[0][1] = mat_tmp[0][1];
res[1][0] = mat_tmp[1][0];
res[1][1] = mat_tmp[1][1];

43
44
45

}
i--;

46
47

}
return res[0][0];

48
49
50

Chapitre 1. Complexite

Ainsi, une etude algorithmique prealable du probl`eme de depart conduit `a une reduction
parfois drastique de la complexite de sa resolution, ce qui a pour consequence de permettre
datteindre des tailles de param`etres inenvisageables avec un algorithme irreechi... La
Table 1.1 ci-dessous illustre les temps de calcul des quatre algorithmes precedents.
fibo1
fibo2
fibo3
fibo4

n
(n )
(n)
(n)
(log(n))

40
31s
0s
0s
0s

225

228
231
calcul irrealisable
18s Segmentation fault
4s 25s
195s
0s 0s
0s

Table 1.1 Complexites et temps dexecution de dierents algorithmes pour le calcul


de la suite de Fibonacci.
Comme on le voit dans la Table 1.1, lalgorithme fibo4 qui a la meilleure complexite
est beaucoup plus rapide que les autres, et peut traiter des valeurs de n plus grandes.
Lalgorithme fibo1 prend tr`es vite un temps trop eleve pour pouvoir etre utilise. Une autre
limitation de fibo1 que lon ne voit pas dans le tableau est la limite liee au nombre maximal
de niveaux de recursivite autorises dans un programme : en C cette valeur est souvent
autour de quelques dizaines de milliers 2 . On ne peut donc pas avoir plus que ce nombre
dappels recursifs imbriques au sein dun algorithme. Pour lalgorithme fibo2 la limite ne
vient pas du temps de calcul, mais de la taille memoire necessaire : quand on essaye dallouer
un tableau de 228 entiers (soit 1Go de memoire dun seul bloc) le syst`eme dexploitation
narrive pas `a satisfaire notre requete et nalloue donc pas de memoire, et puisque lon ne
teste pas si la memoire a bien ete allouee avant decrire dans notre tableau, un probl`eme `a
lallocation se traduit immediatement par une erreur de segmentation memoire `a lecriture.

1.3

Un premier algorithme pour le tri

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

& M. Finiasz

1.3 Un premier algorithme pour le tri

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

ENSTA cours IN101

void insertion_sort(int* tab, int n) {


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

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

10

F. Levy-dit-Vehel

Chapitre 2
R
ecursivit
e
Les denitions recursives sont courantes en mathematiques. Nous avons vu au chapitre
precedent lexemple de la suite de Fibonacci, denie par une relation de recurrence. En
informatique, la notion de recursivite joue un role fondamental. Nous voyons dans ce
chapitre la puissance de la recursivite au travers essentiellement de deux algorithmes de
tri ayant les meilleures performances asymptotiques pour des algorithmes generiques. Nous
terminons par une etude des solutions dequations de recurrence entrant en jeu lors de
lanalyse de complexite de tels algorithmes.

2.1

Conception des algorithmes

Il existe de nombreuses facons de concevoir un algorithme. On peut par exemple adopter


une approche incrementale ; cest le cas du tri par insertion : apr`es avoir trie le sous-tableau
tab[0]...tab[j-1], on ins`ere lelement tab[j] au bon emplacement pour produire le
sous-tableau trie tab[0]...tab[j].
Une approche souvent tr`es ecace et elegante est lapproche recursive : un algorithme
recursif est un algorithme deni en reference `a lui-meme (cest la cas par exemple de
lalgorithme fibo1 vu au chapitre precedent). Pour eviter de boucler indeniment lors de sa
mise en oeuvre, il est necessaire de rajouter `a cette denition une condition de terminaison,
qui autorise lalgorithme `a ne plus etre deni `a partir de lui-meme pour certaines valeurs
de lentree (pour fibo1, nous avons par exemple deni fibo1(0)=0 et fibo1(1)=1).
Un tel algorithme suit generalement le paradigme diviser pour regner : il separe le
probl`eme en plusieurs sous-probl`emes semblables au probl`eme initial mais de taille moindre,
resout les sous-probl`emes de facon recursive, puis combine toutes les solutions pour produire
la solution du probl`eme initial. La methode diviser pour regner implique trois etapes `a
chaque niveau de la recursivite :
Diviser le probl`eme en un certain nombre de sous-probl`emes. On notera D(n) la complexite de cette etape.
R
egner sur les sous-probl`emes en les resolvant de mani`ere recursive (ou directe si le
sous-probl`eme est susamment reduit, i.e. une condition de terminaison est atteinte).
11

2.2 Probl`eme des tours de Hano

ENSTA cours IN101

On notera R(n) la complexite de cette etape.


Combiner les solutions des sous-probl`emes pour trouver la solution du probl`eme initial.
On notera C(n) la complexite de cette etape.
Lien avec la r
ecursivit
e en math
ematiques. On rencontre en general la notion
de recursivite en mathematiques essentiellement dans deux domaines : les preuves par
recurrence et les suites recurrentes. La conception dalgorithmes recursifs se rapproche
plus des preuves par recurrence, mais comme nous allons le voir, le calcul de la complexite
dun algorithme recursif se rapproche beaucoup des suites recurrentes.
Lors dune preuve par recurrence, on prouve quune propriete est juste pour des conditions initiales, et on etend cette propriete `a lensemble du domaine en prouvant que la
propriete est juste au rang n si elle est vrai au rang n 1. Dans un algorithme recursif la
condition de terminaison correspond exactement au conditions initiales de la preuve par
recurrence, et lalgorithme va ramener le calcul sur une entree `a des calculs sur des entrees
plus petites.
Attention !
Lors de la conception dun algorithme recursif il faut bien faire attention `a ce que
tous les appels recursifs eectues terminent bien sur une condition de terminaison.
Dans le cas contraire lalgorithme peut partir dans une pile dappels recursifs innie
(limitee uniquement par le nombre maximum dappels recursifs autorises). De meme,
en mathematique, lors dune preuve par recurrence, si la condition recurrente ne se
ram`ene pas toujours `a une condition initiale, la preuve peut etre fausse.

2.2

Probl`
eme des tours de Hano

Le probl`eme des tours de Hano peut se decrire sous la forme dun jeu (cf. Figure 2.1) :
on dispose de trois piquets numerotes 1,2,3, et de n rondelles, toutes de diam`etre dierent.
Initialement, toutes les rondelles se trouvent sur le piquet 1, dans lordre decroissant des
diam`etres (elle forment donc une pyramide). Le but du jeu est de deplacer toutes les
rondelles sur un piquet de destination choisi parmi les deux piquets vides, en respectant
les r`egles suivantes :
on ne peut deplacer quune seule rondelle `a la fois dun sommet de pile vers un autre
piquet ;
on ne peut pas placer une rondelle au-dessus dune rondelle de plus petit diam`etre.
Ce probl`eme admet une resolution recursive elegante qui comporte trois etapes :
1. deplacement des n 1 rondelles superieures du piquet origine vers le piquet intermediaire par un appel recursif `a lalgorithme.
2. deplacement de la plus grande rondelle du piquet origine vers le piquet destination.
3. deplacement des n 1 rondelles du piquet intermediaire vers le piquet destination
par un appel recursif `a lalgorithme.
12

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 2. Recursivite

Figure 2.1 Le jeu des tours de Hano. Deplacement dune rondelle du piquet 1 vers
le piquet 2. En pointilles : lobjectif nal.
Cet algorithme, que lon appelle Hano, suit lapproche diviser pour regner : la phase de
division consiste toujours `a diviser le probl`eme de taille n en deux sous-probl`emes de taille
n 1 et 1 respectivement. La phase de r`egne consiste `a appeler recursivement lalgorithme
Hano sur le sous-probl`eme de taille n 1. Ici, il y a deux series dappels recursifs
par sous-probl`eme de taille n 1 (etapes 1. et 3.). La phase de combinaison des solutions
des sous-probl`emes est inexistante ici. On donne ci-apr`es le programme C implementant
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


recursifs que si n > 0) et pour cette valeur lalgorithme ne fait rien. Le lecteur est invite `a
verier que lexecution de Hanoi(3,1,3) donne :
Mouvement
Mouvement
Mouvement
Mouvement
Mouvement
Mouvement
Mouvement

& M. Finiasz

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

13

2.2 Probl`eme des tours de Hano

ENSTA cours IN101

Complexit
e de lalgorithme. Calculer la complexite dun algorithme recursif peut
parfois sembler complique, mais il sut en general de se ramener `a une relation denissant
une suite recurrente. Cest ce que lon fait ici. Soit T (n) la complexite (ici, le nombre de
deplacements de disques) necessaire `a la resolution du probl`eme sur une entree de taille n
par lalgorithme Hano. En decomposant la complexite de chaque etape on trouve : letape
de division ne co
ute rien, letape de r`egne co
ute le prix de deux mouvements de taille n 1
et letape de combinaison co
ute le mouvement du grand disque, soit 1. On a T (0) = 0,
T (1) = 1, et, pour n 2,
T (n) = D(n) + R(n) + C(n) = 0 + 2 T (n 1) + 1.
En developpant cette expression, on trouve
T (n) = 2n T (0) + 2n1 + . . . + 2 + 1,
soit
T (n) = 2n1 + . . . + 2 + 1 = 2n 1.
Ainsi, la complexite de cet algorithme est exponentielle en la taille de lentree. En fait,
ce caract`ere exponentiel ne depend pas de lalgorithme en lui-meme, mais est intrins`eque
au probl`eme des tours de Hano : montrons en eet que le nombre minimal minn de
mouvements de disques `a eectuer pour resoudre le probl`eme sur une entree de taille
n est exponentiel en n. Pour cela, nous observons que le plus grand des disques doit
necessairement etre deplace au moins une fois. Au moment du premier mouvement de ce
grand disque, il doit etre seul sur un piquet, son piquet de destination doit etre vide, donc
tous les autres disques doivent etre ranges, dans lordre, sur le piquet restant. Donc, avant
le premier mouvement du grand disque, on aura d
u deplacer une pile de taille n 1. De
meme, apr`es le dernier mouvement du grand disque, on devra deplacer les n 1 autres
disques ; ainsi, minn 2minn1 +1. Or, dans lalgorithme Hano, le nombre de mouvements
de disques verie exactement cette egalite (on a T (n) = minn ). Ainsi, cet algorithme est
optimal, et la complexite exponentielle est intrins`eque au probl`eme.

Complexite Spatiale des Appels Recursifs


Notons que limplementation que nous donnons de lalgorithme Hano
nest pas standard : lalgorithme est en general programme `a laide de trois piles (cf. section 3.3.1),
mais comme nous navons pas encore etudie cette structure de donnee, notre algorithme
se contente dacher les operations quil eectuerait normalement. En utilisant des
piles, la complexite spatiale serait (n) (correspondant `a lespace necessaire pour stocker les n disques). Ici il ny a pas dallocation memoire, mais la complexite spatiale est
quand meme (n) : en eet, chaque appel recursif necessite dallouer de la memoire
(ne serait-ce que pour conserver ladresse de retour de la fonction) qui nest liberee que
lorsque la fonction appelee recursivement se termine. Ici le programme utilise jusqu`a
n appels recursifs imbriques donc sa complexite spatiale est (n). Il nest pas courant
de prendre en compte la complexite spatiale dappels recursifs imbriques, car en general
ce nombre dappels est toujours relativement faible.

14

F. Levy-dit-Vehel

Annee 2011-2012

2.3

Chapitre 2. Recursivite

Algorithmes de tri

Nous continuons ici letude de methodes permettant de trier des donnees indexees par
des clefs (ces clefs sont munies dune relation dordre et permettent de conduire le tri). Il
sagit de reorganiser les donnees de telle sorte que les clefs apparaissent dans un ordre bien
determine (alphabetique ou numerique le plus souvent).
Les algorithmes simples (comme le tri par insertion, mais aussi le tri par selection ou
le tri `a bulle) sont `a utiliser pour des petites quantites de donnees (de lordre de moins de
100 clefs), ou des donnees presentant une structure particuli`ere (donnees compl`etement ou
presque triees, ou comportant beaucoup de clefs identiques).
Pour de grandes quantites de donnees aleatoires, ou si lalgorithme doit etre utilise un
grand nombre de fois, on a plutot recours `a des methodes plus sophistiquees. Nous en
presentons deux ici : le tri fusion et le tri rapide. Tous deux sont de nature recursive et
suivent lapproche diviser pour regner.

2.3.1

Le tri fusion

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

void merge_sort(int* tab, int p, int r) {


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

La procedure fusion (la fonction merge decrite ci-dessous) commence par recopier les
deux sous-tableaux tries tab[p]...tab[q-1] et tab[q]...tab[r-1] dos-`a-dos 2 dans un
tableau auxiliaire tmp. Linteret de cette facon de recopier est que lon na alors pas besoin
de rajouter de tests de n de sous-tableaux, ni de case supplementaire contenant le symbole
par exemple `a chacun des sous-tableaux. Ensuite, merge remet dans tab les elements
du tableau tmp trie, mais ici le tri a une complexite lineaire ((n)) puisque tmp provient
1. La recursion sarrete lorsque les sous-tableaux sont de taille 1, donc trivialement tries.
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 dej`a tries ; le procede est alors le suivant : on part de k = p, i = p


et j = r 1, et on compare tmp[i-p] avec tmp[j-p]. On met le plus petit des deux dans
tab[k] ; si cest tmp[i-p], on incremente i, sinon, on decremente j ; dans tous les cas, on
incremente k. On continue jusqu`a ce que k vaille r.

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

void merge(int* tab, int p, int q, int r) {


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

Ainsi, au debut de chaque iteration de la boucle pour k, les k p elements du soustableau tab[p]...tab[k-1] sont tries. Pour trier le tableau tab de taille n, on appelle
merge sort(tab,0,n).
allocation de Me
moire
Note sur la Re
Lalgorithme merge sort tel quil est ecrit ne g`ere pas bien sa memoire. En eet,
chaque appel `a la fonction merge commence par allouer un tableau tmp de taille r
p, et les operations dallocation memoire sont des operation relativement co
uteuses
(longues `a executer en pratique). Allouer une memoire de taille n a une complexite
(n), et reallouer de la memoire `a chaque appel de merge ne change donc pas la
complexite de lalgorithme. En revanche, en pratique cela ralentit beaucoup lexecution
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 supplementaire (celle que lutilisateur va appeler en pratique), qui alloue
de la memoire avant dappeler la fonction recursive merge sort.

16

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 2. Recursivite

Complexit
e du tri fusion. Evaluons
`a present la complexite en temps de lalgorithme
merge sort, en termes de nombre de comparaisons de clefs. On note T (n), cette complexite
pour une entree de taille n.
La phase de division ne necessite aucune comparaison. La phase de r`egne requiert deux
fois le temps de lalgorithme merge sort sur un tableau de taille deux fois moindre, i.e.
2 T ( n2 ). La phase de recombinaison est la procedure merge. Lorsquappelee avec les
param`etres (p, q, r), elle necessite r p comparaisons. On a donc :
n
T (n) = D(n) + R(n) + C(n) = 0 + 2 T ( ) + n.
2
Supposons dabord que n est une puissance de 2, soit n = 2k . Pour trouver la solution
de cette recurrence, on construit un arbre de recursivite : cest un arbre binaire dont
chaque nud represente le co
ut dun sous-probl`eme individuel, invoque `a un moment de
la recursion. Le noeud racine contient le co
ut de la phase de division+recombinaison au
niveau n de la recursivite (ici n), et ses deux sous-arbres representent les co
uts des sousn
n
probl`emes au niveau 2 . En developpant ces co
uts pour 2 , on obtient deux sous-arbres,
et ainsi de suite jusqu`a arriver au co
ut pour les sous-probl`emes de taille 1. Partant de
n = 2k , le nombre de niveaux de cet arbre est exactement log(n) + 1 = k + 1 (la hauteur de
larbre est k). Pour trouver la complexite T (n), il sut `a present dadditionner les co
uts de
chaque noeud. On proc`ede en calculant le co
ut total par niveau : le niveau de profondeur 0
(racine) a un co
ut total egal `a n, le niveau de profondeur 1 a un co
ut total egal `a n2 + n2 ,...
le niveau de profondeur i pour 0 i k 1 a un co
ut total de 2i 2ni = n (ce niveau
i
comporte 2 nuds). Le dernier niveau correspond aux 2k sous-probl`emes de taille 1 ; aucun
ne contribue `a la complexite en termes de nombre de comparaisons, donc le co
ut au niveau
k est nul. Ainsi, chaque niveau, sauf le dernier, contribue pour n `a la complexite totale. Il
en resulte que T (n) = k n = n log(n).
Lorsque n nest pas necessairement une puissance de 2, on encadre n entre deux
puissances de 2 consecutives : 2k n < 2k+1 . La fonction T (n) etant croissante, on a
T (2k ) T (n) T (2k+1 ), soit k2k T (n) (k + 1)2k+1 . Comme log(n) = k, on obtient
une complexite en (n log(n)).
Remarques :
Le tri fusion ne se fait pas en place 3 : en eet, la procedure merge sort necessite
un espace memoire supplementaire sous la forme dun tableau (tmp) de taille n.
Le calcul de complexite precedent est independant de la distribution des entiers `a
trier : le tri fusion sexecute en (n log(n)) pour toutes les distributions dentiers. La
complexite en moyenne est donc egale `a la complexite dans le pire cas.

2.3.2

Le tri rapide

Le deuxi`eme algorithme de tri que nous etudions suit egalement la methode diviser
pour regner. Par rapport au tri fusion, il presente lavantage de trier en place . En
3. Un tri se fait en place lorsque la quantite de memoire supplementaire - la cas echeant - est une
petit constante (independante de la taille de lentree).

& M. Finiasz

17

2.3 Algorithmes de tri

ENSTA cours IN101

revanche, son comportement depend de la distribution de son entree : dans le pire cas,
il poss`ede une complexite quadratique, cependant, ses performances en moyenne sont en
(n log(n)), et il constitue souvent le meilleur choix en pratique 4 . Le fonctionnement de
lalgorithme sur un tableau tab `a trier entre les indices p et r est le suivant :

1
2
3
4
5
6
7
8

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);
}
}

La procedure partition determine un indice q tel que, `a lissue de la procedure, pour


p i q 1, tab[i] tab[q], et pour q + 1 i r 1, tab[i] > tab[q] (les soustableaux tab[i]...tab[q-1] et tab[q+1]...tab[r-1] netant pas eux-memes tries). Elle
utilise un element x = tab[p] appele pivot autour duquel se fera le partitionnement.

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

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;
}

` la n de chacune des iterations de la boucle for, le tableau est divise en trois parties :
A
i, p i q, tab[i] x
i, q + 1 i j 1, tab[i] > x
4. Lordre de grandeur de complexite en termes de nombre de comparaisons, mais aussi de nombre
doperations elementaires (aectations, incrementations) est le meme que pour le tri fusion, mais les
constantes cachees dans la notation sont plus petites.

18

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 2. Recursivite

Pour j i r, les clefs tab[i] nont pas de lien xe avec le pivot x (elements non encore
traites).
Si le test en ligne 6 est vrai, alors lelement en position j est inferieur ou egal `a x, donc
on le decale le plus `a gauche possible (lignes 8 `a 10) ; mais on incremente dabord q (ligne
` lissue de
7) de facon `a pouvoir inserer cet element entre tab[p] et tab[q] strictement. A
la boucle for sur j, tous les elements de tab[p]...tab[r-1] inferieurs ou egaux `a x ont ete
places `a gauche de tab[q] (et `a droite de tab[p]) ; il ne reste donc plus qu`a mettre x `a
la place de tab[q] (lignes 13 `a 15).
Complexit
e du tri rapide. La complexite de lalgorithme quick sort depend du caract`ere equilibre ou non du partitionnement, qui lui-meme depend de la valeur du pivot
choisi au depart. En eet, si x = tab[p] est inferieur `a tous les autres elements du tableau,
la procedure partition decoupera le tableau initial en deux sous-tableaux extremement
desequilibres : lun de taille 0 et lautre de taille n 1. En particulier si le tableau est dej`a
trie (ou inversement trie), cette conguration surviendra `a chaque appel recursif. Dans ce
cas, le temps dexecution T (n) de lalgorithme satisfait la recurrence
T (n) = T (n 1) + D(n),
(la procedure quick sort sur une entree de taille 0 ne necessitant pas de comparaison)
o`
u D(n) est le co
ut de la procedure partition sur une entree de taille n. Il est clair que
partition(tab, p, r) necessite r p 1 comparaisons, donc D(n) = n 1. Larbre recursif
correspondant `a T (n) = T (n 1) + n 1 est de profondeur n 1, le niveau de profondeur
i ayant un co
ut de n (i + 1). En cumulant les co
uts par niveau, on obtient
T (n) =

n1

i=0

n (i + 1) =

n1

i=0

i=

n(n 1)
.
2

Ainsi, la complexite de lalgorithme dans le pire cas est en (n2 ), i.e. pas meilleure que
celle du tri par insertion.
Le cas le plus favorable est celui o`
u la procedure partition decoupe toujours le tableau courant en deux sous-tableaux de taille presque egale ( n2 et n2 1, donc toujours
inferieure ou egale `a n2 ). Dans ce cas, la complexite de lalgorithme est donne par
T (n) = 2 T ( n2 ) + n 1.
Cette recurrence est similaire `a celle rencontree lors de letude du tri fusion. La solution
est T (n) = (n 1) log(n) si n est une puissance de 2, soit T (n) = (n log(n)) pour tout n.
Nous allons maintenant voir que cette complexite est aussi celle du cas moyen. Pour
cela, il est necessaire de faire une hypoth`ese sur la distribution des entiers en entree de
lalgorithme. On suppose donc quils suivent une distribution aleatoire uniforme. Cest
le resultat de partition qui, `a chaque appel recursif, conditionne le decoupage en soustableaux et donc la complexite. On ne peut pas supposer que partition fournit un indice
q uniformement distribue dans [p, ..., r 1] : en eet, on choisit toujours comme pivot le
& M. Finiasz

19

2.3 Algorithmes de tri

ENSTA cours IN101

premier element du sous-tableau courant, donc on ne peut faire dhypoth`ese duniformite


quant `a la distribution de cet element tout au long de lalgorithme 5 . Pour pouvoir faire une
hypoth`ese raisonnable duniformite sur q, il est necessaire de modier un tant soit peut
partition de facon `a choisir non plus le premier element comme pivot, mais un element
aleatoirement choisi dans le sous-tableau considere. La procedure devient alors :
1
2
3
4
5
6
7
8

int random_partition(int* tab, int p, int r) {


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

Lechange a pour eet de placer le pivot tab[i] en position p, ce qui permet dexecuter
partition normalement).
En supposant `a present que lentier q retourne par random partition est uniformement
distribue dans [p, r 1], on obtient la formule suivante pour T (n) :
1
T (n) = n 1 +
(T (q 1) + T (n q)).
n q=1
n

En eet, nq=1 (T (q 1)+T (nq)) represente la somme des complexites correspondant aux
n decoupages possibles du tableau tab[0, ..., n 1] en deux sous-tableaux. La complexite
moyenne T (n) est donc la moyenne de ces complexites. Par symetrie, on a :
2
T (n) = n 1 +
T (q 1),
n q=1
n

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

T (q 1).

q=1

En soustrayant `a cette egalite la meme egalite au rang n 1, on obtient une expression


faisant seulement intervenir T (n) et T (n 1) :
nT (n) = (n + 1)T (n 1) + 2(n 1),
ou

T (n)
T (n 1) 2(n 1)
=
+
.
n+1
n
n(n + 1)

5. Et ce, meme si les entiers en entree sont uniformement distribues : en eet, la conguration de ces
entiers dans le tableau est modiee dun appel de partition `a lautre.

20

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 2. Recursivite

En developpant la meme formule pour T (n 1) et en la reinjectant ci-dessus, on obtient :


T (n)
T (n 2) 2(n 2) 2(n 1)
=
+
+
.
n+1
n1
(n 1)n n(n + 1)
En iterant ce processus :
n
n

T (n)
2(k 1)
2(k 1)
=
, ou T (n) = (n + 1)
.
n + 1 k=2 k(k + 1)
k(k + 1)
k=2

Pour n susamment grand, on a


n
n
n
n

dx
(k 1)
1
1

, et

= ln(n).
k(k
+
1)
k
k
x
1
k=2
k=1
k=1
Ainsi, T (n) 2(n + 1)ln(n) et on retrouve bien une complexite en (n log(n)) pour le cas
moyen.

2.3.3

Complexit
e minimale dun algorithme de tri

Nous pouvons nous demander si les complexites en (n log(n)) obtenues ci-dessus sont
les meilleures que lon puisse esperer pour un algorithme de tri generique (i.e. qui ne fait
aucune hypoth`ese sur les donnees `a trier). Pour repondre `a cette question, nous allons
calculer une borne inferieure sur la complexite (en termes de nombre de comparaisons)
de tout algorithme de tri par comparaison. Precisons tout dabord que lon appelle tri
par comparaison un algorithme de tri qui, pour obtenir des informations sur lordre de
la sequence dentree, utilise seulement des comparaisons. Les algorithmes de tri fusion et
rapide vus precedemment sont des tris par comparaison. La borne calculee est valable dans
le pire cas (contexte general de la theorie de la complexite).
Tout algorithme de tri par comparaison peut etre modelise par un arbre de decision
(un arbre binaire comme deni dans le chapitre 5). Chaque comparaison que lalgorithme
eectue represente un nud de larbre, et en fonction du resultat de la comparaison,
lalgorithme peut sengager soit dans le sous-arbre gauche, soit dans le sous-arbre droit.
Lalgorithme fait une premi`ere comparaison qui est la racine de larbre, puis sengage dans
lun des deux sous-arbres ls et fait une deuxi`eme comparaison, et ainsi de suite... Quand
lalgorithme sarrete cest quil a ni de trier lentree : il ne fera plus dautres comparaisons
et une feuille de larbre est atteinte. Chaque ordre des elements dentree m`ene `a une
feuille dierente, et le nombre de comparaisons `a eectuer pour atteindre cette feuille est
la profondeur de la feuille dans larbre. Larbre explorant toutes les comparaisons, il en
resulte que le nombre de ses feuilles pour des entrees de taille n est au moins egal `a n!,
cardinal de lensemble de toutes les permutations des n positions.
Soit h, la hauteur de larbre. Dans le pire cas, il est necessaire de faire h comparaisons
pour trier les n elements (autrement dit, la feuille correspondant `a la permutation correcte
& M. Finiasz

21

2.4 Resolution dequations de recurrence

ENSTA cours IN101

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

1
n
2n( )n (1 + ( )),
e
n

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

et par suite, h = (n log(n)). Nous enoncons ce resultat en :


Th
eor`
eme 2.3.1. Tout algorithme de tri par comparaison necessite (n log(n)) comparaisons dans le pire cas.
Les tris fusion et rapide sont donc des tris par comparaison asymptotiquement optimaux.

2.4

R
esolution d
equations de r
ecurrence

Lanalyse des performances dun algorithme donne en general des equations o`


u le temps
de calcul, pour une taille des donnees, est exprime en fonction du temps de calcul pour
des donnees de taille moindre. Il nest pas toujours possible de resoudre ces equations.
Dans cette section, nous donnons quelques techniques permettant de trouver des solutions
exactes ou approchees de recurrences intervenant classiquement dans les calculs de co
ut.

2.4.1

R
ecurrences lin
eaires

Commencons par rappeler la methode de resolution des recurrences lineaires homog`enes


`a coecients constants. Il sagit dequations du type :
un+h = ah1 un+h1 + . . . + a0 un , a0 = 0, h N , n N.
Cette suite est enti`erement determinee par ses h premi`eres valeurs u0 , . . . , uh1 (la suite
est dordre h). Son polynome caracteristique est :
G(x) = xh ah1 xh1 . . . a1 x a0 .
Soit 1 , . . . , r , ses racines, et soit ni , la multiplicite de
i , 1 ni r. En considerant la
serie generatrice formelle associee `a un , soit U (X) = n=0 un X , et en multipliant cette
6. Toutes les feuilles ne se trouvant pas sur le dernier niveau.

22

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 2. Recursivite

serie par le polynome B(X) = X h G(1/X) (polynome reciproque de G(X)), on obtient un


polynome A(X) de degre au plus h 1. Lexpression :
U (X) =

A(X)
B(X)

montre alors que U (X) est en fait une serie rationnelle, i.e. une fraction rationnelle. Sa
decomposition en elements simples donne lexpression suivante 7 pour le terme general de
la suite :
r

un =
Pi (n)in ,
i=1

o`
u Pi (n) est un polynome de degre au plus ni . Une expression de cette forme pour un est
appelee polynome exponentiel.
La suite de Fibonacci Fn = Fn1 + Fn2 , avec u0 = 0 et u1 = 1, est un exemple de
u = 1+2 5 . La suite
telle recurrence, traite dans le chapitre 1. On a Fn = 15 (n n ), o`
de Fibonacci intervient dans le calcul de co
ut de nombreux algorithmes. Considerons par
exemple lalgorithme dEuclide de calcul du pgcd de deux entiers x et y non tous les deux
nuls (on suppose x y) :
1
2
3
4
5
6
7
8

int euclide(int x, int y) {


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

Notons n(x, y), le nombre de divisions avec reste eectuees par lalgorithme. Alors n(x, y) =
0 si y = 0, et n(x, y) = 1 + n(y, x mod y) sinon. Pour evaluer le co
ut de lalgorithme (en
fonction de x), nous allons dabord prouver que, pour x > y 0,
n(x, y) = k x Fk+2 .
Pour k = 0, on a x 1 = F2 , et, pour k = 1, on a x 2 = F3 . Supposons `a present k 2,
et la propriete ci-dessus vraie pour tout j k 1, i.e. si n(u, v) = j, alors u Fj+2 , pour
u, v N, u v > 0. Supposons n(x, y) = k, et considerons les divisions euclidiennes :
x = qy + z, 0 z < y, y = q z + u, 0 u < z.
On a n(y, z) = k 1 donc y Fk+1 ; de meme, z Fk et par suite
x Fk+1 + Fk = Fk+2 .
1
n
n

Dautre part, comme 1, n N, on a Fn 5 ( 1). Do`


u 5x + 1 k+2 . Donc,
pour x > y 0,

n(x, y) log ( 5x + 1) 2,
ements dalgorithmique, de Beauquier, Berstel,
7. Le detail des calculs peut etre trouve dans El
Chretienne, ed. Masson.

& M. Finiasz

23

2.4 Resolution dequations de recurrence

ENSTA cours IN101

autrement dit, n(x, y) = O(log(x)).


Il existe de nombreux autres types dequations de recurrence. Parmi eux, les recurrences
lineaires avec second membre constituent une classe importante de telles recurrences ; elles
peuvent etre resolues par une technique similaire `a celle presentee pour les equations homog`enes. Des methodes adaptees existent pour la plupart des types de recurrences rencontrees ; une presentation de ces dierentes techniques peut etre trouvee dans le livre
ements dalgorithmique de Berstel et al. Nous allons dans la suite nous concentrer sur les
El
equations de recurrence que lon rencontre typiquement dans les algorithmes diviser pour
regner.

2.4.2

Recurrences de partitions

Nous considerons ici des relations de recurrence de la forme :


T (1) = d
T (n) = aT ( nb ) + f (n), n > 1.

(2.1)

o`
u a R+ , b N, b 2.
Notre but est de trouver une expression de T (n) en fonction de n. Dans lexpression
ci-dessus, lorsque nb nest pas entier, on linterpr`ete par nb ou nb .
La recurrence (2.1) correspond au co
ut du calcul eectue par un algorithme recursif
du type diviser pour regner, dans lequel on remplace le probl`eme de taille n par a sousprobl`emes, chacun de taille nb . Le temps de calcul T (n) est donc aT ( nb ) auquel il faut
ajouter le temps f (n) necessaire `a la combinaison des solutions des probl`emes partiels en
une solution du probl`eme total. En general, on evalue une borne superieure sur le co
ut (n)
de lalgorithme, `a savoir (1) = d, et (n) a ( nb ) + f (n), n > 1. On a donc (n) T (n)
pour tout n, et donc la fonction T constitue une majoration du co
ut de lalgorithme.
Lors de lanalyse de la complexite du tri fusion, nous avons vu une methode de resolution
dequations de type (2.1), dans le cas o`
u a = b = 2 : nous avons construit un arbre
binaire dont les nuds representent des co
uts, tel que la somme des nuds du niveau de
profondeur i de larbre correspond au co
ut total des 2i sous-probl`emes de taille 2ni . Pour a
et b quelconques, larbre construit est a-aire, de hauteur logb (n), le niveau de profondeur i
correspondant au co
ut des ai sous-probl`emes, chacun de taille bni , 0 i logb (n). Comme
pour le tri fusion, laddition des co
uts `a chaque niveau de larbre donne le co
ut total T (n)
pour une donnee de taille n. Cette methode est appelee methode de larbre recursif. Elle
donne lieu `a une expression generique pour T (n) en fonction de n.
Lemme 2.4.1. Soit f : N R+ , une fonction denie sur les puissances exactes de b, et
soit T : N R+ , la fonction denie par
T (1) = d
T (n) = aT ( nb ) + f (n), n = bp , p N ,

(2.2)

o`
u b 2 est entier, a > 0 et d > 0 sont reels. Alors

logb (n)1
logb (a)

T (n) = (n

)+

ai f ( bni )

(2.3)

i=0

24

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 2. Recursivite

Preuve. Lexpression ci-dessus provient exactement de laddition des co


uts `a chaque
niveau de larbre recursif correspondant `a la recurrence (2.2) : larbre est a-aire, de hauteur
p = logb (n) ; le niveau de profondeur i correspond au co
ut des ai sous-probl`emes, chacun
de taille bni ; le co
ut total du niveau i, 0 i logb (n) 1, est donc ai f ( bni ). Le dernier
niveau (i = logb (n)) a un co
ut de ap d. Ainsi :
p

T (n) = a d +

p1

ai f ( bni )

i=0

On a :
ap = alogb (n) = blogb (n)logb (a) = (bplogb (a) ) = (nlogb (a) ),
do`
u ap d = (nlogb (a) ).
Dans le cas o`
u f (n) = nk , le temps de calcul est donne par le theor`eme suivant.
Th
eor`
eme 2.4.1. Soit T : N R+ , une fonction croissante, telle quil existe des entiers
b 2, et des reels a > 0, c > 0, d > 0, k 0, pour lesquels
T (1) = d
T (n) = aT ( nb ) + cnk , n = bp , p N .
Alors :

(nk )
(nk logb (n))
T (n) =

(nlogb (a) )

si a < bk
si a = bk
si a > bk .

(2.4)

(2.5)

Preuve. On suppose dabord que n est une puissance de b, soit n = bp , pour un entier
p 1. Par le lemme precedent,
logb (a)

T (n) = (n

p1

a
) + cn
( k )i .
b
i=0
k

a i
Notons (n) = p1
i=0 ( bk ) .
a
Si bk = 1, (n) = p logb (n) et donc T (n) = ap d + cnk (n) = (nk logb (n)).
Supposons `a present bak = 1.
p1

1 ( bak )p
a
.
(n) =
( k )i =
b
1 ( bak )
i=0

Si
Si

a
bk
a
bk

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

& M. Finiasz

25

2.4 Resolution dequations de recurrence


avec =

1
.
a/bk 1

ENSTA cours IN101

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

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

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

soit

log(n)(log(n) 1)
.
2
Ainsi, T (n) = (n(log(n))2 ). On obtient le meme resultat lorsque n nest pas une puissance
de 2 en encadrant n entre deux puissances consecutives de 2.
T (n) = n(log(n))2

En revanche, la recurrence
T (1) = 1

T (n) = 2T (n/4) + n2 n, n = 4p , p N ,

(2.6)

8. Elle depend de la nesse de lapproximation de f comme O(nk ).

26

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 2. Recursivite

est bien adaptee au theor`eme precedent, avec a = 2, b = 4, c = 1, k = 3/2. On a a < bk = 8


donc T (n) = (n3/2 ).
Enn, de facon plus generale, nous pouvons enoncer le theor`eme suivant.
Th
eor`
eme 2.4.2. Soit T : N R+ , une fonction croissante telle quil existe des entiers
b 2, des reels a > 0, d > 0, et une fonction f : N R+ pour lesquels
T (1) = d
T (n) = aT (n/b) + f (n), n = bp , p N .

(2.7)

Supposons de plus que


f (n) = cnk (logb (n))q
pour des reels c > 0, k 0 et q. Alors :

(nk )

(nk logb (n)1+q )


(nk logb (logb (n)))
T (n) =

(nlogb (a) )

(nlogb (a) )

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)

Preuve. Soit n = bp . La formule (2.3) donne


T (n) = (nlogb (a) ) +
Soit T(n) =

p1
i=0

p1

ai f (bpi ).

i=0

ai f (bpi ).

T(n) =

i=1

do`
u 9 , en notant (n) =

pi

f (b ) = c

i=1

bk

i=1 ( a

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

pi ik

i q

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

Si a = bk , alors

si q > 1
(p1+q ) = ((logb (n))1+q )
(logb (p)) = (logb (logb (n))) si q = 1
(n) =

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

& M. Finiasz

27

2.5

Complements

2.5
2.5.1

ENSTA cours IN101

Compl
ements
Recursivite terminale

La recursivite terminale (tail recursivity en anglais) est un cas particulier de recursivite :


il sagit du cas o`
u un algorithme recursif eectue son appel recursif comme toute derni`ere
instruction. Cest le cas par exemple de lalgorithme dEuclide vu page 23 : lappel recursif
se fait dans le return, et aucune operation nest eectuee sur le resultat retourne par
lappel recursif, il est juste passe au niveau du dessus. Dans ce cas, le compilateur a la
possibilite doptimiser lappel recursif : au lieu deectuer lappel recursif, recuperer le
resultat `a ladresse de retour quil a xe pour lappel recursif, et recopier le resultat `a
sa propre adresse de retour, lalgorithme peut directement choisir de redonner sa propre
adresse de retour `a lappel recursif. Cette optimisation presente lavantage de ne pas avoir
une pile recapitulant les appels recursifs aux dierentes sous-fonctions : lutilisation de
recursion terminale supprime toute limite sur le nombre dappels recursifs imbriques.
Bien s
ur, pour que la recursion terminale presente un interet, il faut quelle soit geree
par le compilateur. Cest le cas par exemple du compilateur Caml, ou (pour les cas simples
de recursivite terminale) de gcc quand on utilise les options doptimisation -O2 ou -O3.
Attention !
Si lappel recursif nest pas la seule instruction dans le return, il devient impossible
pour le compilateur de faire loptimisation : par exemple, un algorithme se terminant
par une ligne du type return n*recursif(n-1) ne fait pas de recursivite terminale.

2.5.2

Derecursication dun programme

Il est toujours possible de supprimer la recursion dun programme an den obtenir une
version iterative. Cest en fait ce que fait un compilateur lorsquil traduit un programme
recursif en langage machine. En eet, pour tout appel de procedure, un compilateur engendre une serie generique dinstructions : placer les valeurs des variables locales et ladresse
de la prochaine instruction sur la pile, denir les valeurs des param`etres de la procedure
et aller au debut de celle-ci ; et de meme `a la n dune procedure : depiler ladresse de
retour et les valeurs des variables locales, mettre `a jour les variables et aller `a ladresse de
retour. Lorsque lon veut derecursier un programme, la technique employee par le
compilateur est la technique la plus generique. Cependant, dans certains cas, il est possible
de faire plus simple, meme si lutilisation dune pile est presque toujours necessaire. Nous
donnons ici deux exemples de derecursication, pour lalgorithme dEuclide, et pour un
parcours darbre binaire (cf. chapitre 5).
D
er
ecursication de lalgorithme dEuclide. Lecriture la plus simple de lalgorithme dEuclide est recursive :
28

F. Levy-dit-Vehel

Annee 2011-2012

1
2
3
4
5
6
7
8

Chapitre 2. Recursivite

int euclide(int x, int y) {


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

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

int iterative_euclide(int x, int y) {


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

D
er
ecursication dun parcours darbre. Prenons lalgorithme recursif de parcours
prexe darbre binaire suivant :
1
2
3
4
5
6
7
8

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

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

29

2.5

Complements

ENSTA cours IN101

Ici nous sommes en presence dune recursion double, il est donc necessaire dutiliser
une pile pour gerer lensemble des appels recursifs imbriques. On suppose donc quune
pile est implementee (cf. section 3.3.1) et que les fonction push et pop nous permettent
respectivement dajouter ou de recuperer un element dans cette pile. On suppose que la
fonction stack is empty renvoie 1 quand la pile est vide, 0 autrement. Ce qui va rendre
cette derecursication plus facile que le cas general est quici les dierents appels `a la
fonction sont independants les uns des autres : la fonction explore ne renvoie rien, et lon
nutilise pas son resultat pour modier la facon dont le parcours va se passer. On obtient
alors le code iteratif suivant :

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

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;
}

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

2.5.3

Indecidabilite de la terminaison

Un exemple de preuve de terminaison. Comme nous lavons vu, dans un algorithme


recursif, il est indispensable de verier que les conditions de terminaison seront atteintes
pour tous les appels recursifs, et cela quelle que soit lentree. La facon la plus simple de
prouver que cest le cas est de denir une distance aux conditions de terminaison et
de montrer que cette distance est strictement decroissante lors de chaque appel recursif 10 .
Prenons par exemple le cas dun calcul recursif de coecients binomiaux (base sur la
construction du triangle de Pascal) :
10. Dans le cas ou cette distance est discr`ete (si par exemple cette distance est toujours enti`ere), une
decroissance stricte est susante, mais ce nest pas le cas si la distance est continue (ce qui narrive jamais
en informatique !).

30

F. Levy-dit-Vehel

Annee 2011-2012

1
2
3
4
5
6

Chapitre 2. Recursivite

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 sarrete quand lun des deux bords
du triangle de Pascal est atteint. Pour prouver que lun des bords est atteint on peut utiliser
la mesure D : (n, p) 7 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 decrot
strictement `a chaque appel recursif. Cela prouve donc que cet algorithme termine.
Notons toutefois que cette facon de calculer les coecients binomiaux est tr`es mauvaise,
il est bien plus rapide dutiliser un algorithme iteratif faisant le calcul du developpement
en factoriels du coecient binomial.

Un exemple dalgorithme sans preuve de terminaison. Jusqu`a present, tous les


algorithmes recursifs que lon a vu terminent, et on peut de plus prouver quils terminent.
Cependant dans certains cas, aucune preuve nest connue pour dire que lalgorithme termine. Dans ce cas on parle dindecidabilite de la terminaison : pour une entree donnee, on
ne peut pas savoir si lalgorithme va terminer, et la seule facon de savoir sil termine est
dexecuter lalgorithme sur lentree en question, puis dattendre quil termine (ce qui peut
bien s
ur ne jamais arriver). Regardons par exemple lalgorithme suivant :

1
2
3
4
5
6
7
8
9

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

Cet algorithme fait la chose suivante :

on part dun entier n


si cet entier est pair on le divise par 2
sil est impair on le multiplie par 3 et on ajoute 1
on recommence ainsi jusqu`a atteindre 1
lalgorithme renvoie le nombre detapes necessaires avant datteindre 1

& M. Finiasz

31

2.5

Complements

ENSTA cours IN101

La conjecture de Collatz (aussi appelee conjecture de Syracuse 11 ) dit que cet algorithme
termine toujours. Cependant, ce nest quune conjecture, et aucune preuve nest connue.
Nous somme donc dans un contexte o`
u, pour un entier donne, la seule facon de savoir si lalgorithme termine est dexecuter lalgorithme et dattendre : la terminaison est indecidable.
Un exemple dalgorithme pour lequel on peut prouver que la terminaison est
ind
ecidable. Maintenant nous arrivons dans des concepts un peu plus abstraits : avec la
conjecture de Collatz nous avions un algorithme pour lequel aucune preuve de terminaison
nexiste, mais nous pouvons aussi imaginer un algorithme pour lequel il est possible de
prouver quil ne termine pas pour certaines entrees, mais decider a priori sil termine ou
non pour une entree donnee est impossible. La seule facon de savoir si lalgorithme termine
est de lexecuter et dattendre, sachant que pour certaines entrees lattente sera innie.
Lalgorithme en question est un prouveur automatique pour des propositions logiques.
Il prend en entree un proposition A et cherche `a prouver soit A, soit non-A. Il commence
par explorer toutes les preuves de longueur 1, puis celles de longueur 2, puis 3 et ainsi de
suite. Si la proposition A est decidable, cest-`a-dire que A ou non-A admet une preuve,
cette preuve est de longueur nie, et donc notre algorithme va la trouver et terminer. En
revanche, nous savons que certaines propositions sont indecidables : cest ce quarme
le theor`eme dincompletude de Godel (cf. http://en.wikipedia.org/wiki/Kurt_Godel,
suivre le lien Godels incompleteness theorems). Donc nous savons que cet algorithme peut
ne pas terminer, et nous ne pouvons pas decider sil terminera pour une entree donnee.
Nous pouvons donc prouver que la terminaison de cet algorithme est indecidable,
contrairement `a lexemple precedent ou la terminaison etait indecidable uniquement parce
quon ne pouvait pas prouver la terminaison.
Le probl`
eme de larr
et de Turing. D`es 1936, Alan Turing sest interesse au probl`eme
de la terminaison dun algorithme, quil a formule sous la forme du probl`eme de larret
(halting problem en anglais) :

Etant
donnee la description dun programme et une entree de taille nie, decider
si le programme va nir ou va sexecuter indeniment pour cette entree.
Il a prouve quaucun algorithme generique ne peut resoudre ce probl`eme pour tous les
couples programme/entree possibles. Cela signie en particulier quil existe des couples
programme/entree pour lesquels le probl`eme de larret est indecidable.
La preuve de Turing est assez simple et fonctionne par labsurde. Supposons que lon
ait un algorithme halt or not(A,i) prenant en entree un algorithme A et une entree i et
qui retourne vrai si A(i) termine, et faux si A(i) ne termine pas. On cree alors lalgorithme
suivant :
11. Pour plus de details sur cette conjecture, allez voir la page http://fr.wikipedia.org/wiki/
Conjecture_de_Collatz, ou la version anglaise un peu plus compl`ete http://en.wikipedia.org/wiki/
Collatz_conjecture.

32

F. Levy-dit-Vehel

Annee 2011-2012

1
2
3
4
5
6
7
8

Chapitre 2. Recursivite

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 meme,
si lappel `a halt or not(halt err,halt err) renvoie vrai, halt err(halt err) va tourner indeniment, ce qui signie que halt or not(halt err,halt err) aurait du renvoyer
faux. Nous avons donc une contradiction : un programme ne peut donc pas resoudre le
probl`eme de larret pour tous les couples programme/entree.

& M. Finiasz

33

Chapitre 3
Structures de Donn
ees
Linformatique a revolutionne le monde moderne grace `a sa capacite `a traiter de grandes
quantites de donnees, des quantites beaucoup trop grandes pour etre traitees `a la main. Cependant, pour pouvoir manipuler ecacement des donnees de grande taille il est en general
necessaire de bien les structurer : tableaux, listes, piles, arbres, tas, graphes... Une multitude de structures de donnees existent, et une multitude de variantes de chaque structure,
chaque fois mieux adaptee `a un algorithme en particulier. Dans ce chapitre nous voyons
les principales structures de donnees necessaires pour optimiser vos premiers algorithmes.

3.1

Tableaux

Les tableaux sont la structure de donnee la plus simple. Ils sont en general implementes
nativement dans la majorite des langages de programmation et sont donc simples `a utiliser.
Un tableau represente une zone de memoire consecutive dun seul bloc (cf. Figure 3.1)
ce qui presente `a la fois des avantages et des inconvenients :
la memoire est en un seul bloc consecutif avec des elements de taille constante `a
linterieur (la taille de chaque element est deni par le type du tableau), donc il est
tr`es facile dacceder au i`eme element du tableau. Linstruction tab[i] se contente de
prendre ladresse memoire sur laquelle pointe tab et dy ajouter i fois la taille dun
element.
il faut xer la taille du tableau avant de commencer `a lutiliser. Les syst`emes dexploitation modernes gardent une trace des processus auxquels les dierentes zones de
memoire appartiennent : si un processus va ecrire dans une zone memoire qui ne lui
appartient pas (une zone que le noyau ne lui a pas alloue) il y a une erreur de segmentation. Quand vous programmez, avant decrire (ou de lire) `a la case i dun tableau
il est necessaire de verier que i est plus petit que la taille allouee au tableau (car le
compilateur ne le veriera pas pour vous).
35

3.1 Tableaux

ENSTA cours IN101

Mmoire
espace
non allou

tab

tab[0] tab[1] tab[2] tab[3] tab[4] tab[5] tab[6]


sizeof(tab[0])

Figure 3.1 Representation en memoire dun tableau simple.

3.1.1

Allocation m
emoire dun tableau

Il existe deux facons dallouer de la memoire `a un tableau.


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

1
2
3
4
5
6
7
8
9
10
11

36

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];
}

F. Levy-dit-Vehel

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

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

tab

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

Mmoire

Chapitre 3. Structures de Donnees

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

Annee 2011-2012

Figure 3.2 Representation en memoire dun tableau `a deux dimensions.


Lutilisation de malloc (ou new en C++) est donc beaucoup plus lourde, dautant plus
que la memoire allouee ne sera pas liberee delle meme et lutilisation de la commande free
(ou delete en C++) sera necessaire, mais presente deux avantages :
le code est beaucoup plus proche de ce qui se passe reellement en memoire (surtout
dans le cas `a deux dimensions) ce qui permet de mieux se rendre compte des operations
reellement eectuees. En particulier, une commande qui parat simple comme int
tab[1000][1000][1000]; demande en realite dallouer un million de fois 1000 entiers,
ce qui est tr`es long et occupe 4Go de memoire.
cela laisse beaucoup plus de souplesse pour les allocations en deux dimensions (ou plus) :
contrairement `a la notation simpliee, rien noblige `a avoir des tableaux carres !
Pour les cas simples, la notations [100] est donc susante, mais d`es que cela devient
complique, lutilisation de malloc devient necessaire.
Exemple dallocation non carr
ee. Le programme suivant permet de calculer tous les
coecients binomiaux de facon recursive en utilisant la construction du triangle de Pascal.
La methode recursive simple vue au chapitre precedent (cf. page 30) est tr`es inecace car
elle recalcule un grand nombre de fois les meme coecients. Pour lameliorer, on utilise
un tableau bidimensionnel qui va servir de cache : chaque fois quun coecient est calcule
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 interessant est que le tableau est triangulaire, et lutilisation de malloc
(ou new) permet de ne pas allouer plus de memoire que necessaire (on gagne un facteur 2
sur loccupation memoire ici).

int** tab;

2
3
4

int binomial(int n, int p) {


if (tab[n][p] == 0) {

& M. Finiasz

37

3.1 Tableaux
if ((p==0) || (n==p) {
tab[n][p] = 1;
} else {
tab[n][p] = binomial(n-1,p) + binomial(n-1,p-1);
}

5
6
7
8
9

}
return tab[n][p];

10
11
12

ENSTA cours IN101

13
14
15
16
17
18
19
20
21
22

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);
}

23

/* ins
erer ici les instructions qui
utilisent la table de binomiaux

24
25

*/

26

for (i=0; i<33; i++) {


free(tab[i]);
}
free(tab);

27
28
29
30
31

On utilise donc la variable globale tab comme table de cache et la fonction binomiale
est exactement la meme quavant, mis `a part quelle verie dabord dans le cache si la
valeur a dej`a ete calculee, et quelle stocke la valeur avant de la retourner. On arrete ici
le calcul de binomiaux `a n = 33 car au-del`a les coecients binomiaux sont trop grands
pour tenir dans un int de 32 bits. Notez ici lutilisation de calloc qui alloue la memoire
et linitialise `a 0, contrairement `a malloc qui laisse la memoire non initialisee. La fonction
calloc est donc plus lente, mais linitialisation est necessaire pour pouvoir utiliser la
technique de mise en cache. Cette technique de programmation dynamique est tr`es utilisee
quand lon veut programmer vite et ecacement un algorithme qui se decrit mieux de
facon recursive quiterative. Cela sera souvent le cas dans des probl`emes combinatoires ou
de denombrement.
38

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 3. Structures de Donnees

ration de la Me
moire Alloue
e
Libe
Notez la presence des free `a la n de la fonction main : ces free ne sont pas necessaire
si le programme est termine `a cet endroit l`a car la memoire est de toute facon liberee
par le syst`eme dexploitation quand le processus sarrete, mais si dautres instructions
doivent suivre, la memoire allouee sera dej`
a liberee. De plus, il est important de prendre
lhabitude de toujours liberer de la memoire allouee (`a chaque malloc doit correspondre
un free) car cela permet de localiser plus facilement une fuite de memoire (de la
memoire allouee mais non liberee, typiquement dans une boucle) lors du debuggage.

3.1.2

Complement : allocation dynamique de tableau

Nous avons vu que la structure de tableau a une taille xee avant lutilisation : il faut
toujours allouer la memoire en premier. Cela rend cette structure relativement peu adaptee
aux algorithmes qui ajoutent dynamiquement des donnees sans que lon puisse borner
`a lavance la quantite quil faudra ajouter. Pourtant, les tableaux presentent lavantage
davoir un acc`es instantane `a la i`eme case, ce qui peut etre tr`es utile dans certains cas. On
a alors envie davoir des tableaux de taille variable. Cest ce quimplemente par exemple la
classe Vector en java (sauf que la classe Vector le fait mal...).
Lidee de base est assez simple : on veut deux fonctions insert et get(i) qui permettent
dajouter un element `a la n du tableau et de lire le i`eme element en temps constant (en
O(1) en moyenne). Il sut donc de garder en plus du tableau tab deux entiers qui indiquent
le nombre delements dans le tableau, et la taille totale du tableau (lespace alloue). Lire le
i`eme element peut se faire directement avec tab[i] (on peut aussi implementer une fonction
get(i) qui verie en plus que lon ne va pas lire au-del`a de la n du tableau). En revanche,
ajouter un element est plus complique :
soit le nombre delements dans le tableau est strictement plus petit que la taille totale
et il sut dincrementer le nombre delements et dinserer le nouvel element,
soit le tableau est dej`a rempli et il faut reallouer de la memoire. Si on veut conserver
un acc`es en temps constant il est necessaire de conserver un tableau dun seul bloc, il
faut donc allouer un nouvel espace memoire, plus grand que le precedent, y recopier le
contenu de tab, liberer la memoire occupee par tab, mettre `a jour tab pour pointer
vers le nouvel espace memoire et on est alors ramene au cas simple vu precedemment.
Cette technique marche bien, mais pose un probl`eme : recopier le contenu de tab est
une operation longue et son co
ut depend de la taille totale de tab. Recopier un tableau
de taille n a une complexite de (n). Heureusement, cette operation nest pas eectuee `a
chaque fois, et comme nous allons le voir, il est donc possible de conserver une complexite
en moyenne de O(1).
La classe Vector de java permet `a linitialisation de choisir le nombre delements `a
ajouter au tableau `a chaque fois quil faut le faire grandir. Creer un Vector de taille n en
augmentant de t `a chaque fois va donc necessiter de recopier le contenu du tableau une
& M. Finiasz

39

3.2 Listes chanees

ENSTA cours IN101

Mmoire
L
data

data

NULL

data
data

Figure 3.3 Representation en memoire dune liste simplement chanee.


fois toute les t insertions. La complexite pour inserer n elements est donc :
t

K =n+

tit

i=1

n n
(
t t

+ 1)
+ n = (n2 ).
2

Donc en moyenne, la complexite de linsertion dun element est Kn = (n). Cest beaucoup
trop !
La bonne solution consiste `a doubler la taille du tableau `a chaque reallocation (ou la
multiplier par nimporte quelle constante plus grande que 2). Ainsi, pour inserer n elements
dans le tableau il faudra avoir recopie une fois n2 elements, le coup davant n4 , celui davant
n
... Au total la complexite est donc :
8
log2 n

K =n+

2i 2n = (n).

i=0

Ce qui donne en moyenne une complexite par insertion de Kn = (1). Il est donc possible
de conserver toutes les bonnes proprietes des tableaux et dajouter une taille variable sans
rien perdre sur les complexites asymptotiques.
La reallocation dynamique de tableau a donc un co
ut assez faible si elle est bien faite,
mais elle necessite en revanche dutiliser plus de memoire que les autres structures de
donnees : un tableau contient toujours de lespace alloue mais non utilise, et la phase de
reallocation necessite dallouer en meme temps le tableau de taille n et celui de taille n2 .

3.2

Listes chan
ees

Contrairement `a un tableau, une liste chanee nest pas constituee dun seul bloc de
memoire : chaque element (ou nud) de la liste est alloue independamment des autres
et contient dune part des donnees et dautre part un pointeur vers lelement suivant (cf.
40

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 3. Structures de Donnees

Figure 3.3). Une liste est donc en fait simplement un pointeur vers le premier element de
la liste et pour acceder `a un autre element, il sut ensuite de suivre la chane de pointeurs.
En general le dernier element de la liste pointe vers NULL, ce qui signie aussi quune liste
vide est simplement un pointeur vers NULL. En C, lutilisation de liste necessite la creation
dune structure correspondant `a un nud de la liste. Le type list en lui meme doit ensuite
etre deni comme un pointeur vers un nud. Cela donne le code suivant :

1
2
3
4
5

3.2.1

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

Op
erations de base sur une liste

Insertion. Linsertion dun element `a la suite dun element donne se fait en deux etapes
illustrees sur le dessin suivant :
Liste initiale

Cration du nud

data

data

data

Insertion dans la liste


data

data

data

data

data

On cree le nud en lui donnant le bon nud comme nud suivant. Il sut ensuite
de faire pointer lelement apr`es lequel on veut linserer vers ce nouvel element. Le dessin
represente une insertion en milieu de liste, mais en general, lajout dun element `a une liste
se fait toujours par le debut : dans ce cas loperation est la meme, mais le premier element
de liste est un simple pointeur (sans champ data).

Suppression. Pour supprimer un nud cest exactement loperation inverse, il sut de


faire attention `a bien sauvegarder un pointeur vers lelement que lon va supprimer pour
pouvoir liberer la memoire quil utilise.
& M. Finiasz

41

3.2 Listes chanees

ENSTA cours IN101

Liste initiale

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 represente une suppression en milieux de liste, mais le cas le plus
courant sera la suppression du premier element dune liste.
Parcours. Pour parcourir une liste on utilise un curseur qui pointe sur lelement que
lon est en train de regarder. On initialise le curseur sur le premier element et on suit les
pointeurs jusqu`a arriver sur NULL. Il est important de ne jamais perdre le pointeur vers
le premier element de la liste (sinon les elements deviennent denitivement inaccessibles) :
cest pour cela que lon utilise une autre variable comme curseur. Voila le code C dune
fonction qui recherche une valeur (passee en argument) dans une liste dentiers, et remplace
toutes les occurrences de cette valeur par des 0.
1
2
3
4
5
6
7
8
9
10

void cleaner(int n, list L) {


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

Notons quici, la liste L est passee en argument, ce qui cree automatiquement une nouvelle
variable locale `a la fonction cleaner. Il netait donc pas necessaire de creer la variable cur.
Cela ayant de toute facon un co
ut negligeable par rapport `a un appel de fonction, il nest
pas genant de prendre lhabitude de toujours avoir une variable dediee pour le curseur.
42

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 3. Structures de Donnees

Mmoire

L_end

L
data

data

data

NULL

NULL

Figure 3.4 Representation en memoire dune liste doublement chanee.

3.2.2

Les variantes : doublement chan


ees, circulaires...

La structure de liste est une structure de base tr`es souvent utilisee pour construire
des structures plus complexes comme les piles ou les les que nous verrons `a la section
suivante. Selon les cas, un simple pointeur vers lelement suivant peut ne pas sure, on
peut chercher `a avoir directement acc`es au dernier element... Une multitude de variations
existent et seules les plus courantes sont presentees ici.
Listes doublement chan
ees. Une liste doublement chanee (cf. Figure 3.4) poss`ede
en plus des listes simples un pointeur vers lelement precedent dans la liste. Cela saccompagne aussi en general dune deuxi`eme variable L end pointant vers le dernier element de
la liste et permet ainsi un parcours dans les deux sens et un ajout simple delements en n
de liste. Le seul surco
ut est la presence du pointeur en plus qui ajoute quelques operations
de plus `a chaque insertion/suppression et qui occupe un peu despace memoire.
Listes circulaires. Les listes circulaires sont des listes chanees (simplement ou doublement) dont le dernier element ne pointe pas vers NULL, mais vers le premier element
(cf. Figure 3.5). Il ny a donc plus de reelle notion de debut et n de liste, il y a juste
une position courante indiquee par un curseur. Le probl`eme est quune telle liste ne peux
jamais etre vide : an de pouvoir gerer ce cas particulier il est necessaire dutiliser ce que
lon appelle une sentinelle.
Sentinelles. Dans une liste, une sentinelle est un nud particulier qui doit pouvoir etre
reconnaissable en fonction du contenu de son champ data (une methode classique est
dajouter un entier au champ data qui est non-nul uniquement pour la sentinelle) et qui
sert juste `a simplier la programmation de certaines listes mais ne represente pas un reel
element de la liste. Une telle sentinelle peut avoir plusieurs usages :
& M. Finiasz

43

3.3 Piles & Files

ENSTA cours IN101

Mmoire
cur
data

data

data
data

Figure 3.5 Representation en memoire dune liste circulaire simplement chanee.


representer une liste circulaire vide : il est possible de decider quune liste circulaire vide
sera representee en memoire par une liste circulaire `a un seul element (qui est donc son
propre successeur) qui serait une sentinelle.
terminer une liste non-circulaire : il peut etre pratique dajouter une sentinelle `a la n
de toute liste chanee pour que lorsque la liste est vide des fonctions comme retourner
le premier element ou retourner le dernier element aient toujours quelque chose
`a renvoyer. Dans la plupart des cas on peut leur demander de retourner NULL, mais une
sentinelle peut rendre un programme plus lisible.
On peut aussi envisager davoir plusieurs types de sentinelles, par exemple une qui marquerait un debut de liste et une autre une n de liste.

3.2.3

Conclusion sur les listes

Par rapport `a un tableau la liste presente deux principaux avantages :


il ny a pas de limitation de longueur dune liste (`a part la taille de la memoire)
il est tr`es facile dinserer ou de supprimer un element au milieu de la liste sans pour
autant devoir tout decaler ou laisser un trou.
En revanche, pour un meme nombre delements, une liste occupera toujours un peu plus
despace memoire quun tableau car il faut stocker les pointeurs (de plus le syst`eme dexploitation conserve aussi des traces de tous les segments de memoire alloues pour pouvoir
les desallouer quand le processus sarrete) et lacc`es au i`eme element de la liste co
ute en
moyenne (n) pour une liste de longueur n.

3.3

Piles & Files

Les piles et les les sont des structures tr`es proches des listes et tr`es utilisees en informatique. Ce sont des structures de donnees dynamiques (comme les listes) sur lesquelles
44

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 3. Structures de Donnees

on veut pouvoir executer deux instructions principales : ajouter un element et retirer un


element (en le retournant an de pouvoir lire son contenu). La seule dierence entre la pile
et la liste est que dans le cas de la pile lelement que lon retire est le dernier `a avoir ete
ajoute (on parle alors de LIFO comme Last In First Out), et dans le cas de la le on retire
en premier les element ajoutes en premier (cest un FIFO comme First In First Out). Les
noms de pile et le ne sont bien s
ur pas choisis au hasard : on peut penser `a une pile de
livres sur une table ou `a une le dattente `a la boulangerie qui se comportent de la meme
facon.

3.3.1

Les piles

Les piles sont en general implementees `a laide dune liste simplement chanee, la seule
dierence est que lon utilise beaucoup moins doperations : en general on ne cherche jamais
`a aller lire ce qui se trouve au fond dune pile, on ne fait quajouter et retirer des elements.
Une pile vide est alors representee par une liste vide, ajouter un element revient `a ajouter
un element en tete de liste, et le retirer `a retourner son contenu et `a la supprimer de la
liste. Voici en C ce que pourrait etre limplementation dune pile dentiers :
1
2
3
4
5
6
7

list L;
void push(int n) {
cell* nw = (cell* ) malloc(sizeof(cell ));
nw->data = n;
nw->next = L;
L = nw;
}

8
9
10
11
12
13
14
15
16
17
18
19
20
21

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
ajoute `a la pile et le retire de la pile. Pour lutilisateur qui nutilise que les fonctions push
et pop, le fait que la pile est implementee avec une liste L est transparent.
Exemples dutilisation de piles. Les piles sont une structure de donnee tr`es basique (il
ny a que deux operations possibles), mais elles sont utilisees tr`es souvent en informatique.
& M. Finiasz

45

3.3 Piles & Files

ENSTA cours IN101

Depuis le debut de ce poly nous avons dej`a vu deux exemples dutilisation :


Les tours de Hano : programmer reellement les tours de Hano necessite trois piles (une
pour chaque piquet) contenant des entiers representant les rondelles. Dans ces piles on
peut uniquement ajouter une rondelle (cest ce que fait push) ou retirer la rondelle du
dessus (avec pop).
Dans la derecursication : lorsquun compilateur derecursie un programme, il utilise
une pile pour stocker les appels recursifs. De facon plus general, tout appel `a une
fonction se traduit par lajout de plusieurs elements (adresse de retour...) dans la pile
qui sert `a lexecution du processus.
Une autre utilisation courante de pile est levaluation dexpressions mathematiques.
La notation polonaise inverse (que lon retrouve sur les calculatrice HP) est parfaitement
adaptee `a lusage dune pile : chaque nombre est ajoute `a la pile et chaque operation
prend des elements dans la pile et retourne le resultat sur la pile. Par exemple, lexpression
5 (3 + 4) secrit en notation polonaise inverse 4 3 + 5 ce qui represente les operations :
push(4)
push(3)
push(pop() + pop())
push(5)
push(pop() * pop())
` la n il reste donc juste 35 sur la pile.
A

3.3.2

Les les

Les les sont elles aussi en generale implementees `a laide dune liste. En revanche, les
deux operations push et pop que lon veut implementer doivent lune ajouter un element
en n de liste et lautre retirer un element en debut de liste (ainsi les elements sortent bien
dans le bon ordre : First In, First Out). Il est donc necessaire davoir un pointeur sur la n
de la liste pour pouvoir facilement (en temps constant) y inserer des elements. Pour cela
on utilise donc une liste simplement chanee, mais avec deux pointeurs : lun sur le debut
et lautre sur la n. Si on suppose que L pointe vers le premier element de la liste et L end
vers le dernier, voici comment peuvent se programmer les fonctions push et pop :
1
2
3
4
5
6
7
8
9
10
11
12

46

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

F. Levy-dit-Vehel

Annee 2011-2012
L_end = nw;

13

14
15

Chapitre 3. Structures de Donnees

16
17
18
19
20
21
22
23
24
25
26
27
28
29

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 utilisees partout o`
u lon a des donnees
`a traiter de facon asynchrone avec leur arrivee, mais o`
u lordre de traitement de ces
donnees est important (sinon on pref`ere en general utiliser une pile qui est plus leg`ere
`a implementer). Cest le cas par exemple pour le routage de paquets reseau : le routeur
recoit des paquets qui sont mis dans une le au fur et `a mesure quils arrivent, le routeur
sort les paquets de cette le un par un et les traite en fonction de leur adresse de destination pour les renvoyer. La le sert dans ce cas de memoire tampon et permet ainsi de
mieux utiliser les ressources du routeur : si le trac dentree est irregulier il sut grace au
tampon de pouvoir traiter un ux egal au debit dentree moyen, alors que sans tampon il
faudrait pouvoir traiter un ux egal au debit maximum en entree.

& M. Finiasz

47

Chapitre 4
Recherche en table
Le probl`eme auquel on sinteresse dans ce chapitre est celui de la mise en uvre informatique dun dictionnaire ; autrement dit, il sagit detudier les methodes permettant de
retrouver le plus rapidement possible une information en memoire, accessible `a partir dune
clef (par exemple, trouver une denition `a partir dun mot). La methode la plus naturelle
est limplementation par tableau (table `a adressage direct : pour gerer un dictionnaire avec
des mots jusqu`a 30 lettres on cree une table assez grande pour contenir les 2630 mots
possibles, et on place les denitions dans la case correspondant `a chaque mot), mais bien
que sa complexite temporelle soit optimale, cette methode devient irrealiste en terme de
complexite spatiale d`es lors que lon doit gerer un grand nombre de clefs. Une deuxi`eme
methode consiste `a ne considerer que les clefs eectivement presentes en table (recherche
sequentielle) : on obtient alors des performances analogues aux operations de base sur les
listes chanees, la recherche dun element etant lineaire en la taille de la liste. Une approche
plus ecace est la recherche dichotomique, dans laquelle on utilise un tableau contenant les
clefs triees (comme cest le cas dans un vrai dictionnaire) ; la complexite de loperation de
recherche devient alors en log(n), est rend cette methode tr`es interessante pour de gros chiers auxquels on acc`ede souvent mais que lon modie peu. Enn, une quatri`eme methode
abordee est lutilisation de tables de hachage, qui permet datteindre en moyenne les performances de ladressage direct tout en saranchissant du nombre potentiellement tr`es grand
de clefs possible.

4.1

Introduction

Un probl`eme tr`es frequent en informatique est celui de la recherche de linformation


stockee en memoire. Cette information est en general accessible via une clef. La consultation dun annuaire electronique est lexemple type dune telle recherche. Un autre exemple
est celui dun compilateur, qui doit gerer une table des symboles, dans laquelle les clefs sont
les identicateurs des donnes `a traiter. Les fonctionnalites 1 dun dictionnaire sont exactement celles quil convient de mettre en oeuvre lorsque lon souhaite gerer linformation en
1. Pour la creation, lutilisation et la mise `a jour.

49

4.2 Table `a adressage direct

ENSTA cours IN101

memoire. En eet, les operations que lon desire eectuer sont :


la recherche de linformation correspondant `a une clef donnee,
linsertion dune information `a lendroit specie par la clef,
la suppression dune information.
`
A noter que si la clef correspondant `a une information existe dej`a, linsertion est une
modication de linformation. On dispose donc de paires de la forme (clef,information),
auxquelles on doit appliquer ces operations. Pour cela, lutilisation dune structure de
donnees dynamique simpose.
Nous allons ici etudier les structures de donnees permettant limplementation ecace
de dictionnaires. Nous supposerons que les clefs sont des entiers dans lintervalle [0, m 1],
o`
u m est un entier qui peut etre tr`es grand (en general il sera souvent tr`es grand). Si les
clefs ont des valeurs non enti`eres, on peut appliquer une bijection de lensemble des clefs
vers (un sous-ensemble de) [0, m 1]. Nous nous interesserons `a la complexite spatiale
et temporelle des trois operations ci-dessus, complexites dependantes de la structure de
donnees utilisee.

4.2

Table `
a adressage direct

La methode la plus naturelle pour rechercher linformation est dutiliser un tableau


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

50

struct cell {
bool key_exists;

F. Levy-dit-Vehel

Annee 2011-2012
3
4
5

Chapitre 4. Recherche en table

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

Toute ces variantes de structures de donnees conduisent `a la complexite donnee ci-dessus.


Cette representation en tableau (ou variantes) est bien adaptee lorsque m est petit,
mais si m est tr`es grand, une complexite spatiale lineaire devient rapidement irrealiste.
Par exemple si lon souhaite stocker tous les mots de huit lettres (minuscules sans accents),
nous avons m = 268 237.6 mots possibles, soit un espace de stockage de 100 Go rien que
pour le tableau de pointeurs.

4.3

Recherche s
equentielle

En general, lespace des clefs possibles peut etre tr`es grand, mais le nombre de clefs
presentes dans la table est bien moindre (cest le cas pour les mots de huit lettres). Lidee de
la recherche sequentielle 2 est de mettre les donnees (les couples (clef,info)) dans un tableau
de taille n, o`
u n est le nombre maximal 3 de clefs susceptibles de se trouver simultanement en
table. On utilise aussi un indice p indiquant la premi`ere case libre du tableau. Les donnees
sont inserees en n de tableau (tab[p] recoit (clef,info) et p est incremente). On eectue
une recherche en parcourant le tableau sequentiellement jusqu`a trouver lenregistrement
correspondant `a la clef cherchee, ou arriver `a lindice n de n de tableau (on suppose que le
tableau est enti`erement rempli). Si lon suppose que toutes les clefs ont la meme probabilite
detre recherchees, le nombre moyen de comparaisons est :
n

1
n+1
i=
.
n
2
i=1

En eet, si lelement cherche se trouve en tab[i], on parcourra le tableau jusqu`a la


position i (ce qui correspond `a i + 1 comparaisons). Les clefs susceptibles detre cherchees
etant supposees equiprobables, i peut prendre toute valeur entre 0 et n 1. En cas de
recherche infructueuse, on a `a eectuer n comparaisons. La complexite de loperation de
recherche est donc en temps (n).
Linsertion dun enregistrement seectue en temps (1), sauf si la clef est dej`a presente
en table, auquel cas la modication doit etre precedee dune recherche de cette clef.
Loperation de suppression necessitant toujours une recherche prealable, elle seectue en
temps (n) (il est egalement necessaire de decaler les elements restant dans le tableau apr`es
suppression, ce qui a aussi une complexite (n)). La complexite de stockage du tableau
est en (n).
Si lon dispose dinformation supplementaires sur les clefs, on peut ameliorer la complexite des operations ci-dessus, par exemple en placant les clefs les plus frequemment lues
en debut de tableau.
2. On parle aussi de recherche lineaire.
3. Ou une borne superieure sur ce nombre si le nombre maximal nest pas connu davance.

& M. Finiasz

51

4.3 Recherche sequentielle

ENSTA cours IN101

Voici comment simplemente les operations de recherche, insertion, suppression dans


une telle structure.
1
2
3
4
5
6

struct cell {
int key;
type_info info;
};
cell* tab; /* lallocation doit ^
etre faite dans le main */
int p=0;

7
8
9
10
11
12
13
14
15
16
17

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;
}

18
19
20
21
22
23

/* on suppose quune clef nest jamais ins


er
ee deux fois */
void insert(cell nw) {
tab[p] = nw;
p++;
}

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

void delete(int val) {


int i,j;
for (i=0; i<p; i++) {
if (tab[i].key == val) {
for (j=i; j<p-1; j++) {
tab[j] = tab[j+1];
}
tab[p-1] = NULL;
p--;
return;
}
}
printf("
El
ement inexistant, suppression impossible.\n");
}

On peut egalement utiliser une liste chanee `a la place dun tableau. Un avantage
est alors que lon na plus de limitation sur la taille. Les complexites de recherche et de
suppression sont les memes que dans limplementation par tableau de taille n. Linsertion
conserve elle aussi sa complexite de (1) sauf que les nouveaux elements sont inseres au
debut au lieu d`a la n. La modication necessite toujours un temps en (n) (le co
ut dune
recherche). Asymptotiquement les complexites sont les memes, mais dans la pratique la
recherche sera un peu plus lente (un parcours de tableau est plus rapide quun parcours
52

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 4. Recherche en table

de liste) et la suppression un peu plus rapide (on na pas `a decaler tous les elements du
tableau). Avec une liste, les operations sont alors exactement les memes que celles decrites
dans le chapitre sur les listes.

4.4

Recherche dichotomique

Une methode ecace pour diminuer la complexite de loperation de recherche est dutiliser un tableau (ou une liste) de clefs triees par ordre croissant (ou decroissant). On peut
alors utiliser lapproche diviser-pour-regner pour realiser la recherche : on compare
la clef val cherchee `a celle situee au milieu du tableau. Si elles sont egales, on retourne
linformation correspondante. Sinon, si val est superieure `a cette clef, on recommence la
recherche dans la partie superieure du tableau. Si val est inferieure, on explore la partie
inferieure. On obtient lalgorithme recursif suivant (on recherche entre les indices p (inclus)
et r (exclus) du tableau) :

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

type info dicho_search(cell* tab, int val, int p, int r) {


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

Complexit
e de lop
eration de recherche. La complexite est ici calculee en nombre
dappels recursifs `a dicho search, ou de mani`ere equivalente en nombre de comparaisons
de clefs `a eectuer (meme si lalgorithme comporte deux comparaisons, tant que ce nombre
est constant il va disparatre dans le de la complexite).
On note n = r p, la taille du tableau et Cn , la complexite cherchee. Pour une donnee
de taille n, on fait une comparaison de clef, puis (si la clef nest pas trouvee) on appelle la
procedure recursivement sur un donnee de taille n2 , donc Cn C n2 + 1. De plus, C1 = 1.
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 complexite de loperation
de recherche avec cette representation est donc bien meilleure quavec les representations
precedentes.
Malheureusement ce gain sur la recherche fait augmenter les co
ut des autres operations.
Linsertion dun element doit maintenant se faire `a la bonne place (et non pas `a la n du
tableau). Si lon consid`ere linsertion dun element choisi aleatoirement, on devra decaler en
moyenne n/2 elements du tableau pour le placer correctement. Ainsi, loperation dinsertion
est en (n). Le co
ut de loperation de suppression lui ne change pas et necessite toujours
le decalage den moyenne n/2 elements.
En pratique, cette methode est `a privilegier dans le cas o`
u lon a un grand nombre de
clef sur lesquels on peut faire de nombreuses requetes, mais que lon modie peu. On peut
alors, dans une phase initiale, construire la table par une methode de tri du type tri rapide.
Recherche par interpolation. La recherche dichotomique ne modelise pas notre facon
intuitive de rechercher dans un dictionnaire. En eet, si lon cherche un mot dont la
premi`ere lettre se trouve `a la n de lalphabet, on ouvrira le dictionnaire plutot vers
la n. Une facon de modeliser ce comportement est realisee par la recherche par interpolation. Elle consiste simplement `a modier lalgorithme precedent en ne considerant plus
systematiquement le milieu du tableau, mais plutot une estimation de lendroit o`
u la clef

devrait se trouver dans le tableau. Soit encore val, la clef `a rechercher. Etant
donnee que
le tableau est trie par ordre croissant des clefs, la valeur de val donne une indication de
lendroit o`
u elle se trouve ; cette estimation est donnee par :
q=p+

val tab[p]
(r p).
tab[r 1] tab[p]

Il sav`ere que, dans le cas o`


u les clefs ont une distribution aleatoire, cette amelioration
reduit drastiquement le co
ut de la recherche, comme le montre le resultat suivant (que nous
admettrons) :
Th
eor`
eme 4.4.1. La recherche par interpolation sur un ensemble de clefs distribuees
aleatoirement necessite moins de log(log(n)) + 1 comparaisons, sur un tableau de taille
n.
Attention, ce resultat tr`es interessant nest valable que si les clef sont reparties uniformement dans lensemble des clefs possibles. Une distribution biaisee peut grandement
degrader cette complexite.
54

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 4. Recherche en table

Mmoire
key

m
tab

key

key
key

info

info

info

info
= pointeur vers NULL

Figure 4.1 Representation en memoire dune table de hachage. La huiti`eme case


du tableau contient une collision : deux clef qui ont la meme valeur hachee.

4.5

Tables de hachage

Lorsque lensemble des clefs eectivement stockees est beaucoup plus petit que lespace
de toutes les clefs possibles, on peut sinspirer de ladressage direct - tr`es ecace lorsque
est petit - pour implementer un dictionnaire. Soit n le nombre de clefs reellement presentes.
Lidee est dutiliser une fonction surjective h de dans [0, m 1] (o`
u on choisit en general
m n). Il sut alors davoir une table de taille m (comme precedemment) et de placer une
clef c dans la case k = h(c) de cette table. La fonction h est appelee fonction de hachage,
la table correspondante est appelee table de hachage et lentier k = h(c) la valeur hachee
de la clef c. Les fonctions utilisees habituellement sont tr`es rapides, et lon peut considerer
que leur temps dexecution est en (1) quelle que soit la taille de lentree.
Le principal probl`eme qui se pose est lexistence de plusieurs clefs possedant la meme
valeur hachee. Si lon consid`ere par exemple deux clefs c1 , c2 , telles que h(c1 ) = h(c2 ),

le couple (c1 , c2 ) est appele une collision. Etant


donne que || m, les collisions sont
nombreuses. Une solution pour gerer les collisions consiste `a utiliser un tableau tab de
listes (chanees) de couples (clef,info) : le tableau est indice par les entiers de 0 `a m 1
et, pour chaque indice i, tab[i] est (un pointeur sur) la liste chanee de tous les couples
(clef,info) tels que h(clef) = i (i.e. la liste de toutes les collisions de valeur hachee i). Une
telle table de hachage est representee en Figure 4.1. Les operations de recherche, insertion,
suppression sont alors les memes que lorsque lon implemente une recherche sequentielle
avec des listes, sauf quici les listes sont beaucoup plus petites.
Loperation dinsertion a une complexite en (1) (insertion de (clef,info) au debut de
la liste tab[i] avec i = h(clef)). Pour determiner la complexite - en nombre delements `a
examiner (comparer) - des autres operations (recherche et suppression), on doit dabord
n
, le facteur de remplissage
connatre la taille moyenne des listes. Pour cela, on denit = m
` noter que
de la table, i.e. le nombre moyen delements stockes dans une meme liste. A
peut prendre des valeurs arbitrairement grandes, i.e. il ny a pas de limite a priori sur le
nombre delements susceptibles de se trouver en table.
& M. Finiasz

55

4.5 Tables de hachage

ENSTA cours IN101

Dans le pire cas, les n clefs ont toute la meme valeur hachee, et on retombe sur la
recherche sequentielle sur une liste de taille n, en (n) (suppression en (n) egalement).
En revanche, si la fonction de hachage repartit les n clefs uniformement dans lintervalle
[0, m 1] - on parle alors de hachage uniforme simple - chaque liste sera de taille en
moyenne, et donc la recherche dun element (comme sa suppression) necessitera au plus
+1 comparaisons. Une estimation prealable de la valeur de n permet alors de dimensionner
la table de facon `a avoir une recherche en temps constant (en choisissant m = O(n)). La
complexite spatiale de cette methode est en (m+n), pour stocker la table et les n elements
de liste.

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


par table de hachage dependent de la mani`ere dont la fonction h repartit (en moyenne)
lensemble des clefs `a stocker parmi les m premiers entiers. Nous avons vu que, pour une
complexite moyenne optimale, h doit realiser un hachage uniforme simple.
En general, on ne connat pas la distribution initiale des clefs, et il est donc dicile
de verier si une fonction de hachage poss`ede cette propriete. En revanche, plus les clefs
sont reparties uniformement dans lespace des clefs possibles, moins il sera necessaire
dutiliser une bonne fonction de hachage. Si les clefs sont reparties de facon parfaitement
uniforme, on peut choisir m = 2 et simplement prendre comme valeur hachee les premiers
bits de la clef. Cela sera tr`es rapide, mais la moindre regularite dans les clefs peut etre
catastrophique. Il est donc en general recommande davoir une fonction de hachage dont
la sortie depend de tous les bits de la clef.
Si lon suppose que les clefs sont des entiers naturels (si ce nest pas le cas, on peut
en general trouver un codage qui permette dinterpreter chaque clef comme un entier), la
fonction modulo :
h(c) = c mod m
est rapide et poss`ede de bonnes proprietes de repartition statistique d`es lors que m est un
nombre premier assez eloigne dune puissance de 2 ; (si m = 2 , cela revient `a prendre
bits de la clef).
Une autre fonction frequemment utilisee est la fonction
h(c) = m(cx cx),

o`
u x est une constante reelle appartenant `a ]0, 1[. Le choix de x = 51
conduit `a de bonnes
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

Annee 2011-2012

4.6

Chapitre 4. Recherche en table

Tableau r
ecapitulatif des complexit
es

methode
adressage direct
recherche sequentielle
recherche dichotomique
recherche par interpolation
tables de hachage
avec m n

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)

Note : les complexites ci-dessus sont celles dans le pire cas, sauf celles suivies de pour
lesquelles il sagit du cas moyen.
On constate quune table de hachage de taille bien adaptee et munie dune bonne
fonction de hachage peut donc etre tr`es ecace. Cependant cela demande davoir une
bonne idee de la quantite de donnees `a gerer `a lavance.

& M. Finiasz

57

Chapitre 5
Arbres
La notion darbre modelise, au niveau des structures de donnees, la notion de recursivite
pour les fonctions. Il est `a noter que la representation par arbre est tr`es courante dans
la vie quotidienne : arbres genealogiques, organisation de competitions sportives, organigramme dune entreprise... Dans ce chapitre, apr`es avoir introduit la terminologie et les
principales proprietes des arbres, nous illustrons leur utilisation `a travers trois exemples
fondamentaux en informatique : levaluation dexpressions, la recherche dinformation, et
limplementation des les de priorite. Nous terminons par la notion darbre equilibre, dont
la structure permet de garantir une complexite algorithmique optimale quelle que soit la
distribution de lentree.

5.1
5.1.1

Pr
eliminaires
D
enitions et terminologie

Un arbre est une collection (eventuellement vide) de nuds et daretes assujettis `a


certaines conditions : un nud peut porter un nom et un certain nombre dinformations
pertinentes (les donnees contenues dans larbre) ; une arete est un lien entre deux nuds.
Une branche (ou chemin) de larbre est une suite de nuds distincts, dans laquelle deux
nuds successifs sont relies par une arete. La longueur dune branche est le nombre de ses
aretes (ou encore le nombre de nuds moins un). Un nud est speciquement designe
comme etant la racine de larbre 1 . La propriete fondamentale denissant un arbre est
alors que tout nud est relie `a la racine par une et une seule branche 2 (i.e. il existe un
unique chemin dun nud donne `a la racine).
Comme on peut le voir sur la Figure 5.1, chaque nud poss`ede un lien (descendant,
selon la representation graphique) vers chacun de ses ls (ou descendants), eventuellement
un lien vers le vide, ou pas de lien si le nud na pas de ls ; inversement, tout nud - sauf
1. Si la racine nest pas speciee, on parle plutot darborescence.
2. Dans le cas ou certains nuds sont relies par plus dune branche (ou pas de branche du tout) `a la
racine on parle alors de graphe.

59

5.1 Preliminaires

ENSTA cours IN101


Racine

= Nud
= Arte

Fe
u

ill
e

Nuds
internes

Figure 5.1 Representation dun arbre. On place toujours la racine en haut.


la racine - poss`ede un p`ere (ou ancetre) et un seul, qui est le nud immediatement audessus de lui dans la representation graphique. Les nuds sans descendance sont appeles
feuilles, les nuds avec descendance etant des nuds internes. Tout nud est la racine du
sous-arbre constitue par sa descendance et lui-meme.
Voici une liste du vocabulaire couramment utilise pour les arbres :
Une clef est une information contenue dans un nud.
Un ensemble darbres est une foret.
Un arbre est ordonne si lordre des ls de chacun de ses nuds est specie. Cest
generalement le cas dans les arbres que lon va considerer dans la suite.
Un arbre est de plus numerote si chacun de ses ls est numerote par un entier strictement
positif (deux nuds ne possedant pas le meme numero).
Les nuds dun arbre se repartissent en niveaux. Le niveau dun nud est la longueur
de la branche qui le relie `a la racine.
La hauteur dun arbre est le niveau maximum de larbre (i.e. plus grande distance dun
nud `a la racine).
La longueur totale dun arbre est la somme des longueurs de tous les chemins menant
des nuds `a la racine.
Le degre dun nud est le nombre de ls quil poss`ede.
Larite dun arbre est le degre maximal de ses nuds.
Lorsque lon a un arbre dont les ls de chaque nud sont numerotes par des entiers tous
compris dans lintervalle [1, . . . , k], on parle darbre k-aire. Un arbre k-aire est donc un
arbre numerote, darite inferieure ou egale `a k.
Comme avec les sentinelles pour les listes, on peut denir un type particulier de nud
dit nud vide : cest un nud factice dans le sens o`
u il ne contient pas dinformation ;
il peut juste servir `a remplir la descendance des nuds qui contiennent moins de k ls.
Lexemple le plus important darbre m-aire est larbre binaire. Chaque nud poss`ede
deux ls et on parle alors de ls gauche et de ls droit dun nud interne. On peut
60

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 5. Arbres

egalement denir la notion de ls gauche et ls droit dune feuille : il sut de representer


chacun deux par le nud vide. De cette mani`ere, tout nud non vide peut etre
considere comme un nud interne.
Un arbre binaire est complet si les nuds remplissent compl`etement tous les niveaux,
sauf eventuellement le dernier, pour lequel les nuds apparaissent alors tous le plus `a
gauche possible (notez que larbre binaire de hauteur 0 est complet).

5.1.2

Premi`
eres propri
et
es

La meilleure denition des arbres est sans doute recursive : un arbre est soit larbre
vide, soit un nud appele racine relie `a un ensemble (eventuellement vide) darbres appeles
ses ls.
Cette denition recursive se particularise trivialement au cas des arbres binaires comme
suit : un arbre binaire est soit larbre vide, soit un nud racine relie `a un arbre binaire
gauche (appele sous-arbre gauche) et un arbre binaire droit (appele sous-arbre droit).
Un arbre binaire est evidemment un arbre ; mais reciproquement, tout arbre peut etre
represente par un arbre binaire (cf. plus loin la representation des arbres). Cette vision
recursive des arbres nous permet de demontrer les proprietes suivantes.
Propri
et
e 5.1.1. Il existe une branche unique reliant deux nuds quelconques dun arbre.
Propri
et
e 5.1.2. Un arbre de N nuds contient N 1 aretes.
Preuve. Cest une consequence directe du fait que tout nud, sauf la racine, poss`ede un
p`ere et un seul, et que chaque arete relie un nud `a son p`ere. Il y a donc autant daretes
que de nuds ayant un p`ere, soit N 1.
Propri
et
e 5.1.3. Un arbre binaire complet possedant N nuds internes contient N + 1
feuilles.
Preuve. Pour un arbre binaire complet A, notons f (A), son nombre de feuilles et n(A),
son nombre de nuds internes. On doit montrer que f (A) = n(A) + 1. Le resultat est
vrai pour larbre binaire de hauteur 0 (il est reduit `a une feuille). Considerons un arbre
binaire complet A = (r, Ag , Ad ), r designant la racine de larbre, et Ag et Ad ses sousarbres gauche et droit respectivement. Les feuilles de A etant celles de Ag et de Ad , on a
f (A) = f (Ag ) + f (Ad ) ; les nuds internes de A sont ceux de Ag , ceux de Ad , et r, do`
u
n(A) = n(Ag ) + n(Ad ) + 1. Ag et Ad etant des arbres complets, la recurrence sapplique,
et f (Ag ) = n(Ag ) + 1, f (Ad ) = n(Ad ) + 1. On obtient donc f (A) = f (Ag ) + f (Ad ) =
n(Ag ) + 1 + n(Ad ) + 1 = n(A) + 1.
Propri
et
e 5.1.4. La hauteur h dun arbre binaire contenant N nuds verie h + 1
log2 (N + 1).
Preuve. Un arbre binaire contient au plus 2 nuds au premier niveau, 22 nuds au
deuxi`eme niveau,..., 2h nuds au niveau h. Le nombre maximal de nuds pour un arbre
binaire de hauteur h est donc 1 + 2 + . . . + 2h = 2h+1 1, i.e. N + 1 2h+1 .
& M. Finiasz

61

5.1 Preliminaires

ENSTA cours IN101

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

5.1.3

Repr
esentation des arbres

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

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

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

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

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

62

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

F. Levy-dit-Vehel

Annee 2011-2012

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

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

5.2

Utilisation des arbres

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

63

5.2 Utilisation des arbres

ENSTA cours IN101

2
4

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

Evaluation
dexpressions & parcours darbres

5.2.1

Nous nous interessons ici `a la representation par arbre des expressions arithmetiques.
Dans cette representation, les nuds de larbre sont les operateurs, et les feuilles les
operandes. Sil ny a que des operateurs darite 2 (comme + ou ) on obtient alors un
arbre binaire. Si on consid`ere des operateurs darite superieur on obtient un arbre general,
mais on a vu que tout arbre est equivalent `a un arbre binaire. On ne consid`ere donc ici que
des arbres binaires et des operateurs darite deux, mais les algorithmes seront les memes
pour des operateurs darite quelconque.
On ne sinteresse pas ici `a la construction dun arbre devaluation, mais uniquement
`a son evaluation. On part donc dune arbre comme celui represente sur la Figure 5.3.
Levaluation dune telle expression se fait en parcourant larbre, cest-`a-dire en visitant
chaque nud de mani`ere systematique. Il existe trois 3 facons de parcourir un arbre. Chacune correspond `a une ecriture dierente de lexpression. Nous detaillons ci-apr`es ces trois
parcours, qui sont essentiellement recursifs, et font tous parti des parcours dits en profondeur (depth-rst en anglais).
Parcours pr
exe. Ce parcours consiste `a visiter la racine dabord, puis le sous-arbre
gauche, et enn le sous-arbre droit. Il correspond `a lecriture dune expression dans laquelle
les operateurs sont places avant les operandes, par exemple + 4 3 2 pour lexpression de
la Figure 5.3. Cette notation est aussi appelee notation polonaise. Lalgorithme suivant
eectue un parcours prexe de larbre A et pour chaque nud visite ache son contenu
(pour acher au nal + 4 3 2).
1
2
3

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

3. Plus le parcours par niveau, appele egalement parcours en largeur (i.e. parcours du haut vers le bas,
en visitant tous les nuds dun meme niveau de gauche `a droite avant de passer au niveau suivant. Ce
parcours nest pas recursif, et est utilise par exemple dans la structure de tas (cf. section 5.2.3), mais ne
permet pas devaluer une expression.

64

F. Levy-dit-Vehel

Annee 2011-2012
preorder_traversal(A->left);
preorder_traversal(A->right);

4
5

6
7

Chapitre 5. Arbres

Parcours inxe. Le parcours inxe correspond `a lecriture habituelle des expressions


arithmetiques, par exemple (4 + 3) 2 (sous reserve que lon rajoute les parenth`eses
necessaires). Algorithmiquement, on visite dabord le sous-arbre gauche, puis la racine,
et enn le sous-arbre droit :
1
2
3
4
5
6
7
8
9

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

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

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

Complexit
e. Pour les trois parcours ci-dessus, il est clair que lon visite une fois chaque
nud, donc n appels recursifs pour un arbre `a n nuds, et ce quel que soit le parcours
considere. La complexite de loperation de parcours est donc en (n).
Cas des arbres g
en
eraux. Les parcours prexes et postxes sont aussi bien denis
pour les arbres quelconques. Dans ce cas, la loi de parcours prexe devient : visiter la
racine, puis chacun des sous-arbres ; la loi de parcours postxe devient : visiter chacun des
& M. Finiasz

65

5.2 Utilisation des arbres

ENSTA cours IN101

sous-arbres, puis la racine. Le parcours inxe ne peut en revanche pas etre bien deni si le
nombre de ls de chaque nud est variable.
Notons aussi que le parcours postxe dun arbre generalise est equivalent au parcours
inxe de larbre binaire equivalent (comme vu sur la Figure 5.2) et que le parcours prexe
dun arbre generalise est equivalant au parcours prexe de larbre binaire equivalent.
Parcours en largeur. Le parcours en largeur (breadth-rst en anglais) est tr`es dierent
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 recursif mais doit
etre fait sur larbre tout entier. La technique la plus simple utilise deux les A et B. On
initialise A avec la racine et B `a vide. Puis `a chaque etape du parcours on pop un nud de
la le A, on eectue loperation que lon veut dessus (par exemple acher la clef), et on
ajoute tous les ls de ce nud `a B. On recommence ainsi jusqu`a avoir vider A, et quand A
et vide on inverse les les A et B et on recommence. On sarrete quand les deux les sont
vides.
` chaque etape la le A contient les nuds du niveau que lon est en train de parcourir,
A
et la le B se remplit des nuds du niveau suivant. On eectue donc bien un parcours par
niveaux de larbre. Ce type de parcours nest pas tr`es frequent sur des arbres et sera plus
souvent rencontre dans le contexte plus general des graphes.

5.2.2

Arbres Binaires de Recherche

Nous avons vu dans le chapitre 4 comment implementer un dictionnaire `a laide dune


table. Nous avons vu que la methode la plus ecace est lutilisation dune table de hachage.
Cependant, pour quune telle table ore des performances optimales, il est necessaire de
connatre sa taille nale `a lavance. Dans la plus part des contextes, cela nest pas possible.
Dans ce cas, lutilisation dun arbre binaire de recherche est souvent le bon choix.
Un arbre binaire de recherche (ABR) est un arbre binaire tel que le sous-arbre gauche
de tout nud contienne des valeurs de clef strictement inferieures `a celle de ce nud, et
son sous-arbre droit des valeurs superieures ou egales. Un tel arbre est represente en Figure 5.4. Cette propriete est susante pour pouvoir armer quun parcours inxe de
larbre va visiter les nuds dans lordre croissant des clefs. Un ABR est donc un arbre
ordonne dans lequel on veut pouvoir facilement eectuer des operations de recherche, dinsertion et de suppression, et cela sans perturber lordre.
Recherche dans un ABR. Pour chercher une clef dans un ABR on utilise naturellement
une methode analogue `a la recherche dichotomique dans une table. Pour trouver un nud
de clef v, on commence par comparer v `a la clef de la racine, notee ici r. Si v < r, alors
on se dirige vers le sous-arbre gauche de la racine. Si v = r, on a termine, et on retourne
linformation associee `a la racine. Si v > vr , on consid`ere le sous-arbre droit de la racine.
On applique cette methode recursivement sur les sous-arbres.
66

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 5. Arbres

5
2

8
4

9
7

Figure 5.4 Un exemple darbre binaire de recherche (ABR).


Il est `a noter que la taille du sous-arbre courant diminue `a chaque appel recursif. La
procedure sarrete donc toujours : soit parce quun nud de clef v a ete trouve, soit parce
quil nexiste pas de nud ayant la clef v dans larbre, et le sous-arbre courant est alors
vide. Cela peut se programmer de facon recursive ou iterative :
1
2
3
4
5
6
7
8
9
10
11
12
13
14

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

type_info ABR_search(int v, tree A) {


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

type_info ABR_search_iter(int v, tree A) {


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

& M. Finiasz

67

5.2 Utilisation des arbres

ENSTA cours IN101

8
2

8
4

9
7

9
7

9
7

Figure 5.5 Insertion dun nud dont la clef est dej`


a presente dans un ABR. Trois
emplacements sont possibles.
Insertion dans un ABR. Linsertion dans un ABR nest pas une operation compliquee :
il sut de faire attention `a bien respecter lordre au moment de linsertion. Pour cela, la
methode la plus simple et de parcourir larbre de la meme facon que pendant la recherche
dune clef et lorsque lon atteint une feuille, on peut inserer le nouveau nud, soit `a gauche,
soit `a droite de cette feuille, selon la valeur de la clef. On est alors certain de conserver
lordre dans lABR. Cette technique fonctionne bien quand on veut inserer un nud dont
la clef nest pas presente dans lABR. Si la clef est dej`a presente deux choix sont possibles
(cf. Figure 5.5) : soit on dedouble le nud en inserant le nouveau nud juste `a cote de
celui qui poss`ede la meme clef (en dessus ou en dessous), soit on descend quand meme
jusqu`a une feuille dans le sous-arbre droit du nud.
Encore une fois, linsertion peut se programmer soit de facon recursive, soit de facon
iterative. Dans les deux cas, on choisit dinserer un nud dej`a present dans larbre comme
sil netait pas present : on lins`ere comme ls dune feuille (troisi`eme possibilite dans la
Figure 5.5).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

void ABR_insert(int v, tree* A) {


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

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

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 5. Arbres

on regarde son ls gauche (*A)->left


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

void ABR_insert_iter(int v, tree* A) {


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

Dans cette version iterative, on retrouve lecriture un peu lourde de la version recursive
avec cur = &((*cur)->left). On pourrait etre tente de remplacer cette ligne par la ligne
(*cur) = (*cur)->left, mais cela ne ferait pas ce quil faut ! On peut voir la dierence
entre ces deux commandes sur la Figure 5.6.
Suppression dans un ABR. La suppression de nud est loperation la plus delicate
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 trouve, si on le supprime cela va creer un trou
dans larbre : il faut donc deplacer un autre nud de larbre pour reboucher le trou. En
pratique, on peut distinguer trois cas dierents :
1. Si n ne poss`ede aucun ls (n est une feuille), alors on peut le supprimer de larbre
directement, i.e. on fait pointer son p`ere vers NULL au lieu de n.
2. Si n ne poss`ede quun seul ls : on supprime n de larbre et on fait pointer le p`ere de
n vers le ls de n (ce qui revient `a remplacer n par son ls).
3. Si n poss`ede deux ls, on commence par calculer le successeur de n, cest-`a-dire le
nud suivant n lorsque lon enum`ere les nuds avec un parcours inxe de larbre :
le successeur est le nud ayant la plus petite clef plus grande que la clef de n. Si
on appelle s ce nud, il est facile de voir que s sera le nud le plus `a gauche du
sous-arbre droit de n. Ce nud etant tout `a gauche du sous-arbre, il a forcement son
ls gauche vide. On peut alors supprimer le nud n en le remplacant par le nud
s (on remplace la clef et linformation contenue dans le nud), et en supprimant le
nud s de son emplacement dorigine avec la methode vue au cas 2 (ou au cas 1 si
s poss`ede deux ls vides). Une telle suppression est representee sur la Figure 5.7.
& M. Finiasz

69

5.2 Utilisation des arbres

ENSTA cours IN101

tat initial
A

node** cur = A;
A
key
key

*A
key

*A
key

cur
cur
cur = &((*cur)->left);
A

(*cur) = (*cur)->left;
A

key

key

*A

*A
key

cur

key
cur

(*cur) = &n;

(*cur) = &n;

n.key

n.key

key

key

*A

*A
key

cur

key
cur

Insertion russie

Insertion rate

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

70

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 5. Arbres

Arbre initial

Mouvements de nuds

Arbre final

nud
supprimer
5

sous-a
rb
re
8

dr
o

it

successeur

9
7

Figure 5.7 Suppression dun nud dun ABR.


En C, la version iterative de la suppression (la version recursive est plus compliquee et
napporte pas grand chose) peut se programmer ainsi :

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

void ABR_del(int v, tree* A) {


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

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

71

5.2 Utilisation des arbres

ENSTA cours IN101

et de suppression, sont en (h) : pour les version recursives, chaque appel fait descendre
dun niveau dans larbre et pour les version iteratives chaque etape de la boucle while fait
aussi descendre dun niveau. Pour calculer la complexite, il faut donc connatre la valeur
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 equivalent `a une liste, et les operations
ci-dessus ont donc une complexite analogue `a celle de la recherche sequentielle (i.e. en
(h) = (n)). Noter que lon obtient ce genre de conguration lorsque lon doit inserer n
clefs dans lordre (croissant ou decroissant), ou bien par exemple les lettres A,Z,B,Y,C,X,...
dans cet ordre.
En revanche, dans le cas le plus favorable, i.e. celui dun arbre complet o`
u tous les
4
niveaux sont remplis, larbre a une hauteur environ egale `a log2 (n). Dans ce cas, les trois
operations ci-dessus ont alors une complexite en (h) = (log2 (n)).
La question est maintenant de savoir ce quil se passe en moyenne. Si les clefs sont
inserees de mani`ere aleatoire, on a le resultat suivant :
Proposition 5.2.1. La hauteur moyenne dun arbre binaire de recherche construit aleatoirement `a partir de n clefs distinctes est en (log2 (n)).
Ainsi, on a un comportement en moyenne qui est logarithmique en le nombre de nuds
de larbre (et donc proche du cas le meilleur), ce qui rend les ABR bien adaptes pour
limplementation de dictionnaires. En outre, an de ne pas tomber dans le pire cas,
on dispose de methodes de reequilibrage darbres pour rester le plus pr`es possible de la
conguration darbre complet (cf. section 5.3).
Tri par Arbre Binaire de Recherche
Comme nous lavons vu, le parcours inxe dun ABR ache les clefs de larbre dans
lordre croissant. Ainsi, une methode de tri se deduit naturellement de cette structure
de donnees :
Soient n entiers `a trier
Construire lABR dont les nuds ont pour clefs ces entiers
Imprimer un parcours inxe de cet ABR.
La complexite de cette methode de tri est en (n log2 (n)) ((nlog2 (n)) pour linsertion
de n clefs, chaque insertion se faisant en (log2 (n)), plus (n) pour le parcours inxe)
en moyenne et dans le cas le plus favorable. Elle est en revanche en (n2 ) dans le pire
cas (entiers dans lordre croissant ou decroissant).

5.2.3

Tas pour limpl


ementation de les de priorit
e

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

72

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 5. Arbres

55
0
18
2

24
1
14
3
3
7

16
5

11
4
12
8

6
9

1
10

9
6

7
11
55 24 18 14 11 16 9

3 12 6

Figure 5.8 Representation dun tas `a laide dun tableau.


nanceur dun syst`eme dexploitation qui doit executer en priorite les instructions venant
dun processus de priorite elevee. Les elements sortiront de cette le precisement selon leur
rang : lelement de rang le plus eleve sortira en premier. Les primitives `a implementer pour
mettre en oeuvre et manipuler des les de priorite sont :

une
une
une
une

fonction
fonction
fonction
fonction

dinitialisation dune le (creation dune le vide),


dinsertion dun element dans la le (avec son rang),
qui retourne lelement de plus haut rang,
de suppression de lelement de plus haut rang.

Le codage dune le de priorite peut etre realise au moyen dune liste : alors linsertion
se fait en temps constant, mais loperation qui retourne ou supprime lelement de plus
haut rang necessite une recherche prealable de celui-ci, et se fait donc en (n), o`
u n est
le nombre delements de la le. Autrement, on peut aussi choisir davoir une operation
dinsertion plus lente qui ins`ere directement lelement `a la bonne position en fonction de
sa priorite (cette operation co
ute alors (n)) et dans ce cas les operations de recherche et
suppression peuvent se faire en (1).
Une methode beaucoup plus ecace pour representer une le de priorite est obtenue au
moyen dun arbre binaire de structure particuli`ere, appele tas (heap en anglais) ou (arbre)
maximier. Un tas est un arbre binaire complet, qui poss`ede en plus la propriete que la clef
de tout nud est superieure ou egale `a celle de ses descendants. Un exemple de tas est
donne `a la Figure 5.8.
La facon la plus naturelle dimplementer un tas semble etre dutiliser une structure
darbre similaire `a celle utilisees pour les ABR. Pourtant limplementation la plus ecace
utilise en fait un tableau. On commence par numeroter les nuds de 0 (racine) `a n-1
(nud le plus `a droite du dernier niveau) par un parcours par niveau. On place chaque
nud dans la case du tableau tab correspondant `a son numero (cf. Figure 5.8) : la racine
dans tab[0], le ls gauche de la racine dans tab[1]... Avec cette numerotation le p`ere du
, et les ls du nud i sont les nuds 2i + 1 (ls gauche) et 2i + 2
nud i est le nud i1
2
(ls droit). La propriete de maximier (ordre sur les clefs) se traduit sur le tableau par :
& M. Finiasz

73

5.2 Utilisation des arbres

ENSTA cours IN101

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

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

Pour creer un tas (vide) de taille maximale m donnee en param`etre, on cree un objet de
type tas pour lequel tab est un tableau frachement alloue :
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 apparatre linconvenient de lutilisation dun tableau : le nombre


maximum delements `a mettre dans le tas doit etre deni `a lavance, d`es linitialisation.
Cette limitation peut toutefois etre contournee en utilisant une reallocation dynamique du
tableau : on peut dans ce cas ajouter un niveau au tableau `a chaque fois que le niveau
precedent est plein. On realloue alors un nouveau tableau (qui sera de taille double) et
on peut recopier le tableau precedent au debut de celui l`a. Comme vu dans la section sur
les tableau, la taille etant doublee `a chaque reallocation, cela naecte pas la complexite
moyenne dune insertion delement.
Insertion. Loperation dinsertion comme celle de suppression comporte deux phases : la
premi`ere vise `a inserer (resp. supprimer) la clef consideree (resp. la clef de la racine), tout
en preservant la propriete de completude de larbre, la deuxi`eme est destinee `a retablir la
propriete de maximier, i.e. lordre sur les clefs des nuds de larbre.
Pour inserer un nud de clef v, on cree un nouveau nud dans le niveau de profondeur le
plus eleve de larbre, et le plus `a gauche possible 5 . Ensuite, on op`ere une ascension dans
larbre (plus precisement dans la branche reliant le nouveau nud `a la racine) de facon `a
5. Si larbre est compl`etement rempli sur le dernier niveau, on cree un niveau supplementaire.

74

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 5. Arbres
Insertion du nud 43
55

55
18

24
14
3

16

11
12

18

24
14

9
43

55

43

11
12

14

9
16

43

24

18

11
12

16

Suppression dun nud


55

16
43

24
14
3

18

11
12

14

9
16

43

24

18

11
12

43

43
16

24
14
3

18

11
12

18

24
14

9
3

16

11
12

Figure 5.9 Insertion et suppression dun nud dun tas.


placer ce nouveau nud au bon endroit : pour cela, on compare v avec la clef de son nud
p`ere. Si v est superieure `a celle-ci, on permute ces deux clefs. On recommence jusqu`a ce
que v soit inferieure ou egale `a la clef de son nud p`ere ou que v soit `a la racine de larbre
(en cas 0 de tab). Les etapes dune insertion sont representees sur la Figure 5.9.

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

void heap_insert(int v, heap* h) {


if (h->n == h->max) {
printf("Tas plein.");
return;
}
int tmp;
int i = h->n;
h->tab[i] = v; /* on ins`
ere v `
a la fin de tab */
while ((i>0) && (h->tab[i] > h->tab[(i-1)/2])) {
/* tant que lordre nest pas bon et
que la racine nest pas atteinte */
tmp = h->tab[i];
h->tab[i] = h->tab[(i-1)/2];
h->tab[(i-1)/2] = tmp;
i=(i-1)/2;
}
h->n++;
}

& 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 (notee v) du nud situe au niveau de profondeur le plus eleve et le plus `a droite
possible (le dernier nud du tableau), nud que lon supprime alors aisement etant donne
quil na pas de ls. Ensuite, pour retablir lordre sur les clefs de larbre, i.e. mettre v `a
la bonne place, on eectue une descente dans larbre : on compare v aux clefs des ls
gauche et droit de la racine et si v est inferieure ou egale `a au moins une de ces deux clefs,
on permute v avec la plus grande des deux clefs. On recommence cette operation jusqu`a
ce que v soit superieure ou egale `a la clef de chacun de ses nuds ls, ou jusqu`a arriver `a
une feuille. Les etapes dune suppression sont representees sur la Figure 5.9.
Voici comment peut se programmer loperation de suppression qui renvoie au passage
lelement le plus grand du tas que lon vient de supprimer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

76

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

F. Levy-dit-Vehel

Annee 2011-2012
i = greatest;
} else {
cont = false;
}

38
39
40
41

}
}
return max;

42
43
44
45

Chapitre 5. Arbres

46
47
48
49
50
51

void swap(int* a, int* b) {


int tmp = (*a);
(*a)=(*b);
(*b)=tmp;
}

La fonction swap permet dechanger deux cases du tableau (ou deux entiers situes
nimporte o`
u dans la memoire). On est oblige de passer en arguments des pointeurs vers
les entiers `a echanger car si lon passe directement les entiers, ils seront recopies dans des
variables locales de la fonction, et ce sont ces variable locale qui seront echangees, les entiers
dorigine restant bien tranquillement `a leur place.
Complexit
e des op
erations sur les tas. Comme souvent dans les arbres, la complexite
des dierentes operation depend essentiellement de la hauteur totale de larbre. Ici, avec
les tas, nous avons de plus des arbres complets et donc la hauteur dun tas et toujours
logarithmique en son nombre de nuds : h = (log(n)).
Les boucles while des operations dinsertion ou de suppression sont executees au maximum un nombre de fois egal `a la hauteur du tas. Chacune de ces execution contenant un
nombre constant doperation la complexite total des operations dinsertion et de suppression est donc (h) = (log(n)).

5.2.4

Tri par tas

La representation precedente dun ensemble dentiers (clefs) donne lieu de mani`ere


naturelle `a un algorithme de tri appele tri pas tas (heapsort en anglais). Soient n entiers
`a trier, on les ins`ere dans un tas, puis on rep`ete n fois loperation de suppression. La
complexite de ce tri est n (log(n)) pour les n insertions de clefs plus n (log(n))
pour les n suppressions, soit (n log(n)) au total. Il est `a noter que, de par la denition
des operations dinsertion/suppression, cette complexite ne depend pas de la distribution
initiale des entiers `a trier, i.e. elle est la meme en moyenne ou dans le pire cas. Voici une
implementation dun tel tri : on part dun tableau tab que lon re-remplit avec les entiers
tries (par ordre croissant).
1
2
3

void heapsort(int* tab, int n) {


int i;
heap* h = init_heap(n);

& M. Finiasz

77

5.3 Arbres equilibres


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);

4
5
6
7
8
9
10
11
12

5.3

ENSTA cours IN101

Arbres
equilibr
es

Les algorithmes sur les arbres binaires de recherche donnent de bons resultats dans le
cas moyen, mais ils ont de mauvaises performances dans le pire cas. Par exemple, lorsque les
donnees sont dej`a triees, ou en ordre inverse, ou contiennent alternativement des grandes
et des petites clefs, le tri par ABR a un tr`es mauvais comportement.
Avec le tri rapide, le seul rem`ede envisageable etait le choix aleatoire dun element pivot,
dont on esperait quil eliminerait le pire cas. Heureusement pour la recherche par ABR,
il est possible de faire beaucoup mieux, car il existe des techniques generales dequilibrage
darbres permettant de garantir que le pire cas narrive jamais.
Ces operations de transformation darbres sont plus ou moins simples, mais sont peu
co
uteuses en temps. Elles permettent de rendre larbre le plus regulier possible, dans un sens
qui est mesure par un param`etre dependant en general de sa hauteur. Une famille darbres
satisfaisant une telle condition de regularite est appelee une famille darbres equilibres.
Il existe plusieurs familles darbres equilibres : les arbres AVL, les arbres rouge-noir, les
arbres a-b... Nous verrons ici essentiellement les arbres AVL.

5.3.1

R
e
equilibrage darbres

Nous presentons ici une operation dequilibrage appelee rotation, qui sapplique `a tous
les arbres binaires. Soit donc A, un arbre binaire non vide. On ecrira A = (x, Z, T ) pour
exprimer que x est la racine de A, et Z et T ses sous-arbres gauche et droit respectivement.
Soit A = (x, X, B) avec B non vide, et posons B = (y, Y, Z). Loperation de rotation
gauche de A est loperation :
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 (inchange) pointe sur le sous-arbre Z, et le ls droit
du nud x est mis `a pointer sur le sous-arbre Y . Cette operation est representee sur la
Figure 5.10.
La rotation droite est loperation inverse :
A = (y, (x, X, Y ), Z) D(A) = (x, X, (y, Y, Z)).
78

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 5. Arbres
Rotation droite

Rotation gauche

y
y

Z
Z

Y
X
y

Z
Y

Z
Y

Figure 5.10 Operation de rotation pour le reequilibrage darbre binaire.


On remplace le nud racine y par le nud x, le ls gauche du nud x (inchange) pointe
sur le sous-arbre X, son ls droit pointe sur le nud y, et le ls gauche du nud y est mis
`a pointer sur le sous-arbre Y .
Utilisation des rotations. Le but des rotations est de pouvoir reequilibrer un ABR.
On op`ere donc une rotation gauche lorsque larbre est desequilibre `a droite , i.e. son
sous-arbre droit est plus haut que son sous-arbre gauche. On op`ere une rotation droite dans
le cas contraire. Il est aise de verier que les rotations preservent la condition sur lordre
des clefs dun ABR. On a alors :
Proposition 5.3.1. Si A est un ABR, et si la rotation gauche (resp. droite) est denie
sur A, alors G(A) (resp. D(A)) est encore un ABR.
On peut egalement denir des doubles rotations (illustrees sur la Figure 5.11) comme
suit : la rotation gauche-droite associe `a larbre A = (x, Ag , Ad ), larbre D(x, G(Ag ), Ad ).
De mani`ere analogue, la rotation droite-gauche associe `a larbre A = (x, Ag , Ad ), larbre
G(x, Ag , D(Ad )). Ces operations preservent egalement lordre des ABR. Une propriete importante des rotations et doubles rotations est quelles simplementent en temps constant :
en eet, lorsquun ABR est represente par une structure de donnees du type binary tree
denie plus haut dans ce chapitre, une rotation consiste essentiellement en la mise `a jour
dun nombre xe (i.e. independant du nombre de nuds de larbre ou de sa hauteur) de
pointeurs.

5.3.2

Arbres AVL

Les arbres AVL ont ete introduits par Adelson, Velskii et Landis en 1962. Ils constituent
une famille dABR equilibres en hauteur.
& M. Finiasz

79

5.3 Arbres equilibres

ENSTA cours IN101


Rotation gauche-droite
x

y
z

T
X

Z
Y

z
y

T
z

Y
X

Z
Y
X

Figure 5.11 Exemple de double rotation sur un ABR.


De mani`ere informelle, un ABR est un arbre AVL si, pour tout nud de larbre, les
hauteurs de ses sous-arbres gauche et droit di`erent dau plus 1. Plus precisement, posons
(A) = 0 si A est larbre vide, et dans le cas general :
(A) = h(Ag ) h(Ad )
o`
u Ag et Ad sont les sous-arbres gauche et droit de A, et h(Ag ) la hauteur de larbre Ag
(par convention, la hauteur de larbre vide est 1 et la hauteur dune feuille est 0 et tous
deux sont des AVL).
(A) est appele lequilibre de A. Pour plus de simplicite, la notation (x) o`
u x est un
nud designera lequilibre de larbre dont x est la racine. Une denition plus rigoureuse
dun arbre AVL est alors :
D
enition 5.1. Un ABR est un arbre AVL si, pour tout nud x de larbre, (x)
{1, 0, 1}.
La propriete fondamentale des AVL est que lon peut borner leur hauteur en fonction
du log du nombre de nuds dans larbre. Plus precisement :
Proposition 5.3.2. Soit A un arbre AVL possedant n sommets et de hauteur h. Alors

avec c = 1/ log2 ((1 +

log2 (1 + n) 1 + h c log2 (2 + n),


5)/2) 1, 44.

Preuve. Pour une hauteur h donnee, larbre possedant le plus de nuds est larbre complet,
`a 2h+1 1 nuds. Donc n 2h+1 1 et log2 (1 + n) 1 + h.
80

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 5. Arbres

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

5)/2. Do`
u
h+3<

log2 (n + 2)
1
+ log ( 5)
log2 (n + 2) + 2.
log2 ()
log2 ()

Par exemple, un arbre AVL `a 100 000 nuds a une hauteur comprise entre 17 et 25. Le
nombre de comparaisons necessaires `a une recherche, insertion ou suppression dans un tel
arbre sera alors de cet ordre.
Arbres de Fibonacci
La borne superieure de la proposition precedente est essentiellement atteinte pour les
arbres de Fibonacci denis comme suit : (0) est larbre vide, (1) est reduit `a une
feuille, et, pour k 2, larbre k+2 a un sous-arbre gauche egal `a k+1 , et un sous-arbre
doit egal `a k . La hauteur de k est k 1, et k poss`ede Fk+2 nuds.
Limplementation des AVL est analogue `a celle des ABR, `a ceci pr`es que lon rajoute `a
la structure binary tree un champ qui contient la hauteur de larbre dont la racine est le
nud courant. Cette modication rend cependant le operation dinsertion et de suppression
un peu plus compliquees : il est necessaire de remettre `a jour ces hauteurs chaque fois que
cela est necessaire.
Insertion dans un AVL. Loperation dinsertion dans un AVL se fait de la meme
mani`ere que dans un ABR : on descend dans larbre `a partir de la racine pour rechercher la
feuille o`
u mettre le nouveau nud. Ensuite il sut de remonter dans larbre pour remettre
& M. Finiasz

81

5.3 Arbres equilibres

ENSTA cours IN101


Cas simple : une rotation droite suffit
x

h+1

h+2

h
h

Gg

Gd
Gg

Gd

Cas compliqu : une rotation gauche interne G


permet de se rammener au cas simple
x

Gg

Gd

h+2

h
h

h+1

h+2

Gg

Figure 5.12 Reequilibrage apr`es insertion dans un AVL.


`a jour les hauteurs de tous les sous-arbres (dans une version recursive de linsertion, cela
se fait aisement en abandonnant la recursion terminale et en remettant `a jour les hauteur
juste apr`es lappel recursif pour linsertion).
Toutefois, cette operation peut desequilibrer larbre, i.e. larbre resultant nest plus
AVL. Pour retablir la propriete sur les hauteurs, il sut de reequilibrer larbre par des
rotations (ou doubles rotations) le long du chemin qui m`ene de la racine `a la feuille o`
u
sest fait linsertion. Dans la pratique, cela se fait juste apr`es avoir remis `a jour les hauteurs
des sous-arbres : si on constate un desequilibre entre les deux ls de 2 ou 2 on eectue
une rotation, ou une double rotation. Supposons que le nud x ait deux ls G et D et
quapr`es insertion (x) = h(G) h(D) = 2. Le sous-arbre G est donc plus haut que D.
Pour savoir si lon doit faire un double rotation ou une rotation simple en x il faut regarder
les hauteurs des deux sous-arbres de G (notes Gg et Gd ). Si (G) > 1 alors on fait une
rotation droite sur x qui sut `a reequilibrer larbre. Si (G) = h(Gg ) h(Gd ) = 1 alors
il faut faire une double rotation : on commence par une rotation gauche sur G an que
(G) > 1, puis on est ramene au cas precedent et une rotation droite sur x sut. Cette
operation est illustree sur la Figure 5.12.
Dans le cas o`
u le sous-arbre D est le plus haut, il sut de proceder de facon symetrique.
Il est important de noter quapr`es une telle rotation (ou double rotation), larbre qui etait
desequilibre apr`es linsertion retrouve sa hauteur initiale. Donc la propriete dAVL est
necessairement preserver pour les nuds qui se trouvent plus haut dans larbre.
Proposition 5.3.3. Apr`es une insertion dans un arbre AVL, il sut dune seule rotation
ou double rotation pour reequilibrer larbre. Loperation dinsertion/reequilibrage dans un
AVL `a n nuds se realise donc en O(log2 (n)).
82

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 5. Arbres

Suppression dans un AVL. Loperation de suppression dans un AVL se fait de la meme


mani`ere que dans un ABR : on descend dans larbre `a partir de la racine pour rechercher
le nud contenant la clef `a supprimer. Sil sagit dune feuille, on supprime celle-ci ; sinon,
on remplace le nud par son nud successeur, et on supprime le successeur. Comme dans
le cas de linsertion, cette operation peut desequilibrer larbre. Pour le reequilibrer, on
op`ere egalement des rotations ou doubles rotations le long du chemin qui m`ene de la racine
`a la feuille o`
u sest fait la suppression ; mais ici, le reequilibrage peut necessiter plusieurs
rotations ou doubles rotations mais le nombre de rotations (simples ou doubles) necessaires
est au plus egal `a la hauteur de larbre (on en fait au maximum une par niveau), et donc :
Proposition 5.3.4. Loperation de suppression/reequilibrage dans un AVL `a n nuds se
realise en O(log2 (n)).
Conclusion sur les AVL. Les AVL sont donc des arbres binaires de recherches veriant
juste une propriete dequilibrage supplementaire. Le maintien de cette propriete naugmente pas le co
ut des operations de recherche, dinsertion ou de suppression dans larbre,
mais permet en revanche de garantir que la hauteur de larbre AVL reste toujours logarithmique en son nombre de nuds. Cette structure est donc un peu plus complexe
`a implementer quun ABR classique mais nore que des avantages. Dautres structures
darbres equilibres permettent dobtenir des resultats similaires, mais `a chaque fois, le
maintien de la propriete dequilibrage rend les operation dinsertion/suppression un peu
plus lourdes `a implementer.

& M. Finiasz

83

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

6.1

D
enitions et terminologie

D
enition 6.1. Un graphe oriente G = (S, A) est la donnee dun ensemble ni S de
sommets, et dun sous-ensemble A du produit S S, appele ensemble des arcs de G.
Un arc a = (x, y) a pour origine le sommet x, et pour extremite le sommet y. On note
org(a) = x, ext(a) = y. Le sommet y est un successeur de x, x etant un predecesseur de
1

4
3

8
7

6
2

Figure 6.1 Exemple de graphe oriente.


85

6.1 Denitions et terminologie

ENSTA cours IN101

8
7

B(T)
2

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 appele boucle.
Un arc represente une liaison orientee entre son origine et son extremite. Lorsque lorientation des liaisons nest pas une information pertinente, la notion de graphe non oriente
permet de sen aranchir. Un graphe non oriente G = (S, A) est la donnee dun ensemble
ni S de sommets, et dune famille de paires de S dont les elements sont appeles aretes.

Etant
donne un graphe oriente G, sa version non orientee est obtenue en supprimant les
boucles, et en remplacant chaque arc restant (x, y) par la paire {x, y}.
Soit G = (S, A), un graphe oriente, et T , un sous-ensemble de S. Lensemble
(T ) = {a = (x, y) A, (x T, y S \ T ) ou (y T, x S \ T )},
est appele le cocycle associe `a T . Lensemble
B(T ) = {x S \ T, y T, (x, y) ou (y, x) A},
est appele bordure de T (voir Figure 6.2). Si le sommet u appartient `a B(T ), on dit aussi
que u est adjacent `a T . Le graphe G est biparti sil existe T S, tel que A = (T ).
D
enition 6.2. Soit G = (S, A), un graphe oriente. Un chemin f du graphe est une suite
darcs < a1 , . . . , ap >, telle que
org(ai+1 ) = ext(ai ).
Lorigine du chemin f , notee aussi org(f ), est celle de son premier arc a1 , et son extremite,
ext(f ), est celle de ap . La longueur du chemin est egale au nombre darcs qui le composent,
i.e. p. Un chemin f tel que org(f ) = ext(f ) est appele un circuit.
Dans un graphe non oriente la terminologie est parfois un peu dierente : un chemin
est appele une chane, et un circuit un cycle. Un graphe sans cycle est un graphe acyclique.
Soit G = (S, A), un graphe oriente, et soit x, un sommet de S. Un sommet y est un
ascendant (resp. descendant) de x, sil existe un chemin de y `a x (resp. de x `a y).
86

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 6. Graphes
1

2
3

5
6
7

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 associee `a un graphe.


Un graphe non oriente G est connexe si, pour tout couple de sommets, il existe une
chane ayant ces deux sommets pour extremites. Par extension, un graphe oriente est
connexe si sa version non orientee est connexe. La relation denie sur lensemble des sommets dun graphe non oriente par x y sil existe une chane reliant x `a y, est une relation
dequivalence dont les classes sont appelees composantes connexes du graphe. Cette notion est tr`es similaire `a la notion de composantes connexe en topologie : on regarde les
composantes connexe du dessin du graphe.
Soit G = (S, A), un graphe oriente. Notons G , la relation dequivalence denie sur S
par x G y si x = y ou sil existe un chemin joignant x `a y et un chemin joignant y `a x. Les
classes dequivalence pour cette relation sont appelees composantes fortement connexes de
G.

6.2

Repr
esentation des graphes

Il existe essentiellement deux facons de representer un graphe. Dans la suite, G = (S, A)


designera un graphe oriente.

6.2.1

Matrice dadjacence

Dans cette representation, on commence par numeroter de facon arbitraire les sommets
du graphe : S = {x1 , . . . , xn }. On denit ensuite une matrice carree M , dordre n par :
{
1 si (xi , xj ) A
Mi,j =
0 sinon.
Autrement dit, Mi,j vaut 1 si, et seulement si, il existe un arc dorigine xi et dextremite xj
(voir Figure 6.3). M est la matrice dadjacence de G. Bien entendu, on peut egalement
representer un graphe non oriente `a laide de cette structure : la matrice M sera alors
symetrique, avec des 0 sur la diagonale.
La structure de donnee correspondante - ainsi que son initialisation - est :
& M. Finiasz

87

6.2 Representation des graphes

ENSTA cours IN101

tab

0
4

1
3

8
7

6
6
2

Figure 6.4 Representation dun graphe par liste de successeurs.

1
2
3
4

struct graph_mat {
int n;
int** mat;
};

5
6
7
8
9
10
11
12
13
14
15
16

void graph_init(int n, graph_mat* G){


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

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

6.2.2

Liste de successeurs

Une autre facon de coder la matrice dadjacence, particuli`erement adaptee dans le cas
dun graphe peu dense, consiste `a opter pour une representation creuse de la matrice au
moyen de listes chanees : cette representation est appelee liste de successeurs (adjacency
list en anglais) : chaque ligne i de la matrice est codee par la liste chanee dont chaque
cellule est constituee de j et dun pointeur vers la cellule suivante, pour tous les j tels
que Mi,j vaut 1. Autrement dit, on dispose dun tableau tab[n] de listes de sommets, tel
88

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 6. Graphes

que tab[i] contienne la liste des successeurs du sommet i, pour tout 1 i n. Cette
representation est en (n + |A|), donc poss`ede une complexite en espace bien meilleure
que la precedente (`a noter que cette complexite est optimale dun point de vue theorique).
Les structures de donnees correspondantes sont :
1
2
3
4
5
6
7
8
9
10
11
12
13

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

Dans le cas dun graphe non oriente, on parle de liste de voisins, mais le codage reste
le meme. Contrairement `a la representation par matrice dadjacence, ici, les sommets du
graphe ont une veritable representation et peuvent contenir dautres donnees quun simple
entier. Avec la matrice dadjacence, deventuelles donnees supplementaires doivent etre
stockees dans une structure annexe.

6.3

Existence de chemins & fermeture transitive

Existence de chemins. Soit G = (S, A), un graphe oriente. La matrice dadjacence de


G permet de connatre lexistence de chemins entre deux sommets de G, comme le montre
le theor`eme suivant.
Th
eor`
eme 6.3.1. Soit M p , la puissance p-i`eme de la matrice dadjacence M de G. Alors
p
est egal au nombre de chemins de longueur p de G, dont lorigine est le
le coecient Mi,j
sommet xi et lextremite xj .
Preuve. On proc`ede par recurrence sur p. Pour p = 1, le resultat est vrai puisquun chemin
de longueur 1 est un arc du graphe. Fixons un entier p 2, et supposons le theor`eme vrai
pour tout j p 1. On a :
n

p
p1
Mi,j
=
Mi,k
Mk,j .
k=1

Or, tout chemin de longueur p entre xi et xj se decompose en un chemin de longueur p 1


p1
entre xi et un certain xk , suivi dun arc entre xk et xj . Par hypoth`ese, Mi,k
est le nombre
de chemins de longueur p 1 entre xi et xk , donc le nombre de chemins de longueur p
& M. Finiasz

89

6.3 Existence de chemins & fermeture transitive

ENSTA cours IN101

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

Corollaire 6.3.1. Soit N = M + . . . + M n . Alors il existe un chemin entre les sommets


xi et xj si et seulement si, Ni,j = 0.
Un algorithme de recherche de lexistence dun chemin entre deux sommets x et y de
G est le suivant :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

int path_exists(graph_mat* G, int x, int y) {


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

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

90

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 6. Graphes

tel que (x, y) A si et seulement sil existe un chemin dans G dorigine x et dextremite
y.
La matrice N denie precedemment calcule la fermeture transitive du graphe G. En

eet, la matrice dadjacence M de G est obtenue `a partir de N en posant Mi,j


= 0 si

Ni,j = 0, Mi,j = 1 si Ni,j = 0. Une fois calculee G , on peut repondre en temps constant `a
la question de lexistence de chemins entre deux sommets x et y de G.
Exemple dApplication du Calcul de la Fermeture Transitive
Lors de la phase de compilation dun programme, un graphe est associe `a chaque fonction : cest le graphe des dependances (entre les variables) de la fonction et les sommets
de ce graphe representent les variables, un arc entre deux sommets x et y indique que le
calcul de la valeur de x fait appel au calcul de la valeur de y. Le calcul de la fermeture
transitive de ce graphe permet alors dobtenir toutes les variables intervenant dans le
calcul dune variable donnee.
Dans la suite, nous presentons un algorithme de calcul de la fermeture transitive A dun
graphe G = (S, A), qui admet une complexite en (n3 ). Soit x un sommet du graphe G et
notons x (A), loperation qui ajoute `a A tous les arcs (y, z) tels que y est un predecesseur
de x, et z un successeur :
x (A) = A {(y, z), (y, x) A et (x, z) A}.
Cette operation verie les proprietes suivantes, que nous admettrons :
Propri
et
e 6.3.1.
x (x (A)) = x (A),
et, pour tout couple de sommets (x, y) :
x (y (A)) = y (x (A)).
Si lon consid`ere literee de laction des xi sur A, on voit aisement que
x1 (x2 (. . . (xn (A)) . . .)) A .
Mieux :
Proposition 6.3.1. La fermeture transitive A de G est donnee par
A = x1 (x2 (. . . (xn (A)) . . .)).
Preuve. Il reste `a montrer A x1 (x2 (. . . (xn (A)) . . .)). Soit f , un chemin joignant deux
sommets x et y dans G. Il existe donc des sommets de G y1 , . . . yp tels que :
f = (x, y1 )(y1 , y2 ) . . . (yp , y),
donc (x, y) y1 (y2 (. . . (yp (A)) . . .)). Dapr`es la propriete ci-dessus, on peut permuter
lordre des yi dans cette ecriture, donc on peut considerer par exemple que les yi sont
& M. Finiasz

91

6.4 Parcours de graphes

ENSTA cours IN101

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

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

Dans cet algorithme, les deux boucles for internes implementent exactement loperation
xk (A) ; en eet, la matrice dadjacence M de G est construite `a partir de celle de G
par : (xi , xj ) est un arc de G si cest un arc de G ou si (xi , xk ) et (xk , xj ) sont deux arcs de
G. Cest exactement ce qui est calcule au milieu de lalgorithme. Clairement, la complexite
de lalgorithme de Roy-Warshall est en (n3 ) operations booleennes.
Remarque : on obtient la fermeture reexive-transitive de G en ajoutant `a la matrice
dadjacence de G la matrice Identite dordre n.

6.4

Parcours de graphes

Nous etudions ici les algorithmes permettant de parcourir un graphe quelconque G,


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

F. Levy-dit-Vehel

Annee 2011-2012

6.4.1

Chapitre 6. Graphes

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 dextremite x. La longueur dun tel chemin
est appele la profondeur de x dans larborescence. Dans une arborescence, tout sommet,
sauf la racine, admet un unique predecesseur. On a donc |A| = |S| 1. Par analogie avec
la terminologie employee pour les arbres, le predecesseur dun sommet est appele son p`ere,
les successeurs etant alors appeles ses ls. La dierence entre une arborescence et un arbre
tient seulement au fait que, dans un arbre, les ls dun sommet sont ordonnes.
On peut montrer quun graphe connexe sans cycle est une arborescence. Donc si G est
un graphe sans cycle, G est aussi la foret constituee par ses composantes connexes.
Une arborescence est representee par son vecteur p`ere, [n], o`
u n est le nombre de
sommets de larborescence, tel que [i] est le p`ere du sommet i. Par convention, [r] = NULL.

6.4.2

Parcours en largeur

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

& M. Finiasz

93

6.4 Parcours de graphes

ENSTA cours IN101

Pour i 1, Yi+1 est lensemble obtenu


en considerant tous les successeurs des sommets
i
de Yi qui nappartiennent pas `a j=1 Yj .
Pour chaque Yi , i > 0, on denit de plus lensemble Bi des arcs dont lextremite est dans
Yi et lorigine dans Yi1 . Attention Bi ne contient pas tous les arcs possibles, mais un seul
arc par
de Yi : on veut que chaque element nait quun seul p`ere. On pose ensuite
element
Y = i Yi , B = i Bi . Alors le graphe (Y, B) est par construction une arborescence. Cest
larborescence des plus courts chemins de racine x, dapr`es la remarque ci-dessus.
Cette preuve donne un algorithme de construction de larborescence des plus courts
chemins dun sommet donne : comme pour le parcours en largeur dun arbre on utilise
une le qui g`ere les ensembles Yi , i.e. les sommets `a traiter (ce sont les sommets qui, `a un
moment donne de lalgorithme, ont ete identies comme successeurs, mais qui nont pas
encore ete parcourus ; autrement dit, ce sont les sommets en attente ). Par rapport `a un
parcours darbre, la presence eventuelle de cycles fait que lon utilise en plus un coloriage
des sommets avec trois couleurs : les sommets en blanc sont ceux qui nont pas encore ete
traites (au depart, tous les sommets, sauf le sommet x choisi comme racine, sont blancs).
Les sommets en gris sont ceux de la le, i.e. en attente de traitement, et les sommets en
noir sont ceux dej`a traites (ils ont donc, `a un moment, ete deles). On denit aussi un
tableau d qui indique la distance du sommet considere `a x ; en dautres termes, d[v] est la
profondeur du sommet v dans larborescence en cours de construction (on initialise chaque
composante de d `a ).
Selon la structure de donnee utilisee pour representer le graphe, la facon de programmer
cet algorithme peut varier et la complexite de lalgorithme aussi ! On se place ici dans le
cas le plus favorable : on a une structure de donnee similaire `a un arbre, o`
u chaque sommet
du graphe contient un pointeur vers la liste de ses successeurs, et en plus, les sommets du
graphe sont tous indexes par des entiers compris entre 0 et n.
1
2
3
4
5
6
7
8
9
10
11
12

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;

13
14
15
16
17
18
19

94

void init(vertex* root, int nb) {


int i;
n = nb;
if (color == NULL) {
/* initialiser uniquement si cela na jamais
et
e initialis
e */
color = (int* ) malloc(n*sizeof(int ));

F. Levy-dit-Vehel

Annee 2011-2012

dist = (int* ) malloc(n*sizeof(int ));


father = (int* ) malloc(n*sizeof(int ));
for (i=0; i<n; i++) {
color[i] = 0;
/* 0 = blanc, 1 = gris, 2 = noir */
dist[i] = -1;
/* -1 = infini */
father[i] = -1; /* -1 = pas de p`
ere */
}

20
21
22
23
24
25
26

}
color[root->num] = 1;
dist[root->num] = 0;

27
28
29
30

Chapitre 6. Graphes

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

void minimum_spanning_tree(vertex* root, int num) {


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

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

& M. Finiasz

95

6.4 Parcours de graphes


0

ENSTA cours IN101

4
1

1
0

0
0

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

dist 0 3 2 1 2 0 1 1 1

Figure 6.5 Application de all spanning trees `a un graphe. Les sommets 0 et 5


sont des racines. Les arcs en pointilles ne font pas partie dune arborescence.
1
2
3
4
5
6
7
8
9
10
11

void all_spanning_trees(vertex** vertices, int nb) {


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

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

96

F. Levy-dit-Vehel

Annee 2011-2012

6.4.3

Chapitre 6. Graphes

Parcours en profondeur

Alors que le parcours en largeur est essentiellement iteratif (implementation par le), le
parcours en profondeur est de nature recursive 3 . Le principe du parcours en profondeur est
de visiter tous les sommets en allant dabord le plus profondement possible dans le graphe.
Un syst`eme de datation (utilisant deux tableaux beg[] et end[]) permet de memoriser
les dates de debut et de n de traitement dun sommet. Lalgorithme de parcours cree
une foret 4 par un procede recursif. Chaque arborescence de la foret est creee `a partir
dun sommet x par lalgorithme suivant (le code couleur utilise pour colorier les sommets
a la meme signication que dans le cas du parcours en largeur - les sommets gris etant
ceux de la pile, et correspondant ici aux sommets en cours de visite , dans le sens o`
u
le traitement dun tel sommet se termine lorsque lon a parcouru tous ses descendants).
Voici une implementation du parcours en profondeur, reprenant les memes structures que
limplementation du parcours en largeur.
1
2
3
4
5
6

int n;
int date = 0;
int* color = NULL;
int* father = NULL;
int* beg = NULL;
int* end = NULL;

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

void init(int nb) {


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

25
26
27
28

void depth_first_spanning_tree(vertex* x) {
vertices_list* tmp;
color[x->num] = 1;

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

& M. Finiasz

97

6.4 Parcours de graphes

ENSTA cours IN101

4
3

8
7

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 Foret engendree lors du parcours en profondeur dun graphe. Le


graphe de depart est connexe, mais deux arbres sont necessaires pour le representer
enti`erement. Les arcs en pointilles sont ceux qui ne font partie daucune arborescence.
Ici, les sommets ont ete parcourus dans lordre : 0, 3, 4, 1, 6, 7, 2 puis 5, 8.
beg[x->num] = date;
date++;
tmp = x->successors;
while (tmp != NULL) {
if (color[tmp->vert->num] == 0) {
/* un successeur non encore visit
e a
et
e trouv
e */
father[tmp->vert->num] = x->num;
depth_first_spanning_tree(tmp->vert);
}
tmp = tmp->next;
}
/* une fois tous les successeurs trait
es
le traitement du sommet x est fini */
color[x->num] = 2;
end[x->num] = date;
date++;

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

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

98

void all_depth_first_spanning_trees(vertex** vertices, int nb) {


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

F. Levy-dit-Vehel

Annee 2011-2012
}

9
10

Chapitre 6. Graphes

Le parcours en profondeur passe une seule fois par chaque sommet et fait un nombre
doperations proportionnel au nombre darcs. La complexite dun tel parcours est donc en
(n + |A|).
Application au labyrinthe. Il est possible de representer un labyrinthe par un graphe,
dont les sommets sont les embranchements, ceux-ci etant relies selon les chemins autorises.
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 trouve la sortie. Ce retour sur
ses pas est pris en charge par la recursivite.
Le parcours en largeur quand `a lui modeliserait plutot un groupe de personnes dans un
labyrinthe : au depart, le groupe est au point dentree du labyrinthe, puis il se repartit de
facon `a ce que chaque personne explore un embranchement adjacent `a lembranchement o`
u
il se trouve. Il modelise egalement un parcours de labyrinthe eectue par un ordinateur,
dans lequel le sommet destination (la sortie) est connu, et pour lequel il sagit de trouver
le plus court chemin de lorigine (entree du labyrinthe) `a la destination, i.e. un chemin
particulier dans larborescence des plus courts chemins de racine lorigine.

6.5
6.5.1

Applications des parcours de graphes


Tri topologique

Les graphes orientes sans circuit sont utilises dans de nombreuses applications pour
representer des precedences entre ev`enements (on parle alors de graphe des dependances). Le
tri topologique dun graphe consiste `a ordonnancer les taches representees par les sommets
du graphe selon les dependances modelisees par ses arcs. Par convention, une tache doit
etre accomplie avant une autre, si un arc pointe du sommet correspondant `a cette tache
vers le sommet correspondant `a lautre. Autrement dit, etant donne un graphe oriente
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 extremite. Il peut etre vu comme un alignement
des sommets de G le long dune ligne horizontale, de mani`ere que tous les arcs soient
orientes de gauche `a droite.
Le parcours en profondeur dun graphe permet de resoudre le probl`eme du tri topologique. Lidee est deectuer un parcours en profondeur du graphe, puis de retourner la liste
des sommets dans lordre decroissant de leur date de n de traitement. En eet, si (u, v)
est un arc (et donc si la tache representee par u doit etre eectuee avant celle representee
par v) alors, dans lalgorithme de parcours en profondeur, le sommet v aura necessairement
ni detre traite avant le sommet u : un sommet na jamais ni detre traite avant que tous
ses successeurs naient ete traites. Lalgorithme de tri topologique est donc le suivant :
& M. Finiasz

99

6.5 Applications des parcours de graphes

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

ENSTA cours IN101

void topological_sort(vertex** vertices, int nb) {


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

On obtient ainsi un algorithme de complexite (|S| + |A|), i.e. lineaire en la taille du


graphe.
dun Graphe
Test de Cyclicite
De facon similaire, on peut tester si un graphe est cyclique par un parcours en profondeur : en eet, si un graphe poss`ede un cycle, cela veut dire que, dans une arborescence de recouvrement, un sommet x est lorigine dun arc dont lextremite est un
ancetre de x dans cette arborescence. Dans le parcours en profondeur, cela veut dire
que `a un moment, le successeur dun sommet gris sera gris. Il sut donc de modier leg`erement lalgorithme de parcours en profondeur pour ne pas seulement tester si
la couleur dun sommet est blanche, mais aussi tester si elle est grise et acher un
message en consequence.

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 decomposer en composantes fortement connexe disjointes : un ensemble de
composantes fortement connexes tel que sil existe un chemin allant dun sommet dune
composante vers un sommet dune autre, le chemin inverse nexiste pas. Par exemple,
un graphe acyclique naura que des composantes fortement connexes composees dun seul
sommet.
On a vu que le parcours en profondeur permet de tester lexistence de cycles dans un
graphe. Comme nous allons le voir, une modication de lalgorithme de parcours en pro100

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 6. Graphes

fondeur permet de retrouver toutes les composantes fortement connexes. Cest lalgorithme
de Tarjan, invente en 1972. Le principe de lalgorithme est le suivant :
on eectue un parcours en profondeur du graphe,
`a chaque fois que lon traite un nouveau sommet on lui attribue un index (attribue par
ordre croissant) et un lowlink initialise `a la valeur de lindex et on ajoute le sommet
`a une pile,
le lowlink correspond au plus petit index accessible par un chemin partant du sommet
en question, donc `a la n du traitement dun sommet on met `a jour son lowlink pour
etre le minimum entre les lowlink de tous ses successeurs et lindex du sommet courant,
chaque fois quun successeur dun sommet est dej`a colorie en gris on met `a jour le
lowlink de ce sommet en consequence,
`a chaque fois que lon a ni de traiter un sommet dont le lowlink est egal `a lindex on a
trouve une composante fortement connexe. On peut retrouver lensemble des elements
de cette composante en retirant des elements de la pile jusqu`a atteindre le sommet
courant.
Cet algorithme est donc un peu plus complique que les precedents, mais reste relativement simple `a comprendre. Si on a un graphe acyclique, `a aucun moment un successeur
dun sommet naura un index plus petit que le sommet courant. Du coup, les lowlink
restent toujours egaux `a lindex du sommet et `a la n du traitement de chaque sommet une
nouvelle composant connexe a ete trouvee : cette composante contient juste le sommet courant et on retrouve bien le resultat attendu : un graphe acyclique contient n composantes
fortement connexes composees dun seul sommet.
Maintenant, si le graphe contient un cycle, tous les elements de ce cycle vont avoir leur
lowlink egal `a lindex du premier sommet visite. Donc, un seul sommet du cycle aura un
son index egal `a son lowlink : le premier visite, qui est donc aussi le plus profond dans
la pile. Tous les elements qui sortiront de la pile avant lui sont alors les autres sommets
de la composante fortement connexe formee par le cycle. Voici le code correspondant `a
lalgorithme de Tarjan (un exemple dexecution est visible sur la Figure 6.7) :
1
2
3
4

int
int
int
int

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

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

5
6
7
8
9
10
11
12
13
14
15
16

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

/* si le successeur na jamais
et
e visit
e,
on le visite */
Tarjan(tmp->vert);
/* et on met `
a jour le lowlink de x */
lowlink[x->num] = min(lowlink[x->num], lowlink[tmp->vert->num]);
} else if (color[tmp->vert->num] == 1) {
/* si le successeur est en cours de visite on a trouv
e un cycle */
lowlink[x->num] = min(lowlink[x->num], lowlink[tmp->vert->num]);
}
tmp = tmp->next;

17
18
19
20
21
22
23
24
25
26

}
if (lowlink[x->num] == index[x->num]) {
/* on a fini une composante fortement connexe
et on d
epile jusqu`
a retrouver x. */
printf("CFC : ");
do {
tmp2 = pop();
color[tmp2->num] = 2;
printf("%d,", tmp2->num);
while (tmp2 != x)
printf("\n");
}

27
28
29
30
31
32
33
34
35
36
37
38
39

ENSTA cours IN101

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

6.5.3

Calcul de chemins optimaux

On consid`ere ici un graphe G = (S, A) pondere, i.e. `a chaque arc est associee une
valeur appelee poids. On appellera poids dun chemin la somme des poids des arcs qui le
composent. Le probl`eme est alors, etant donne deux sommets, de trouver un chemin de
poids minimal reliant ces deux sommets (sil en existe un).
La resolution de ce probl`eme `a beaucoup dapplications, par exemple pour le calcul du
plus court chemin dune ville `a une autre (en passant par des routes dont on connat la
longueur) ou le calcul dun chemin de capacite maximale dans un reseau de communication
(dans lequel le taux de transmission dun chemin est egal au minimum des taux de transmission de chaque liaison intermediaire) sont des exemples de situations dans lesquelles ce
probl`eme intervient.
An de traiter de mani`ere uniforme les dierents ensembles de denition pour les
poids, induits par lapplication (lensemble des nombres reels, lensemble {vrai, f aux}...),
on consid`ere la situation generale o`
u les poids appartiennent `a un semi-anneau S(, , , ),
cest-`a-dire veriant les proprietes suivantes :
102

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 6. Graphes

pile

3
0

pile

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

index 0 1 -1 3 -1 2

lowlink -1 -1 -1 -1 -1 -1

lowlink 0 1 -1 3 -1 1
NULL

color 0 0 0 0 0 0

color 1 2 0 2 0 2

NULL

pile

3
0

pile

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

index 0 1 -1 3 4 2

lowlink 0 1 -1 3 4 1

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

color 1 0 0 0 0 0

color 1 2 0 2 1 2

NULL

0
4

pile

3
0

pile

3
0

index 0 1 -1 -1 -1 -1

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

color 1 1 0 0 0 0

NULL

index 0 1 5 3 4 2

lowlink 0 1 0 3 4 1

0
1

color 1 2 1 2 1 2

NULL

0
4
2

pile

3
0

pile

index 0 1 -1 -1 -1 2

lowlink 0 1 -1 -1 -1 1
2

color 1 1 0 0 0 1

NULL

0
1
5

index 0 1 5 3 4 2

lowlink 0 1 0 3 0 1
2

color 1 2 1 2 1 2

NULL

0
4
2

pile

3
0

pile

index 0 1 -1 -1 -1 2

index 0 1 5 3 4 2

lowlink 0 1 0 3 0 1

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

color 1 2 0 0 0 2

color 2 2 2 2 2 2

NULL

pile

3
0

index 0 1 -1 3 -1 2

lowlink 0 1 -1 3 -1 1
2

color 1 2 0 1 0 2

NULL

0
3

Figure 6.7 Exemple dexecution de lalgorithme de Tarjan sur un petit graphe `a


trois composantes fortement connexes. On explore dabord le sommet 0 et on voit
ensuite levolution des dierentes variables au fur et `a mesure de lexploration des
autres sommets.

& M. Finiasz

103

6.5 Applications des parcours de graphes

ENSTA cours IN101

est une loi de composition interne sur S, commutative, associative, idempotente


(x x = x), delement neutre . On impose de plus quetant donnee une sequence
innie denombrable s1 , . . . , si , . . . delements de S, s1 s2 . . . S.
est une loi de composition interne associative, delement neutre .
est absorbant pour .
Pour tout x S, on note x , lelement de S deni par
x = x x x x x x . . .
Par exemple, dans le cas du probl`eme du plus court chemin reliant une ville `a une
autre, S(, , , ) = {R }(min, +, , 0) (la longueur dun chemin est la somme ()
des poids des arcs qui le composent, la longueur du plus court chemin etant le minimum
() des longueurs des chemins existants ; `a noter que, pour tout x, on a ici x = 0).
Soit donc S(, , , ), un semi-anneau, et soit p, une fonction de A dans S, qui associe
un poids `a chaque arc du graphe. On prolonge p `a SS par p(i, j) = si (i, j) A. On etend
egalement p aux chemins en denissant, pour tout chemin (s1 , . . . , sk ), p((s1 , . . . , sk )) =
p((s1 , s2 )) p((s2 , s3 )) . . . p((sk1 , sk )). On cherche `a determiner le co
ut minimal (en
fait optimal, selon lapplication) des chemins reliant deux sommets quelconques du graphe.
Autrement dit, si i et j sont deux sommets de G, on cherche `a determiner
i,j = chemin {chemin de i a j} p(chemin).
Il est clair que, pour un graphe quelconque, il nest en general pas possible denumerer
tous les chemins reliant deux sommets. Pour calculer la matrice = (i,j )i,j , il existe un
algorithme d
u `a Aho, Hopcroft et Ullman, qui admet une complexite en (n3 ), n etant le
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 elements de S, on peut alors
ecrire le code de lalgorithme AHU (qui ressemble beaucoup `a lalgorithme de Roy-Warshall
vu precedemment). Il prend en argument la matrice M telle que M[i][j] est le poids de
larete reliant le sommet i au somment j :
1
2
3
4
5
6
7
8
9
10
11

void AHU(elem** M, int n) {


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

` la n de lexecution de lalgorithme AHU, la matrice M a ete modiee


Proposition 6.5.1. A
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 denie par i,j = M[i][j].
104

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 6. Graphes
(0)

(0)

Preuve. Notons i,j , la valeur de la matrice M passee en argument : i,j = p(i, j) ou .


Considerons la suite ((k) )k de matrices denie par recurrence par
(k)

(k1)

i,j = i,j

(k1)

(i,k

(k,k ) k,j
(k1)

(k1)

).

(k)

Alors nous allons prouver par recurrence que le coecient i,j est egal au co
ut minimal dun
chemin reliant le sommet i au sommet j, en ne passant que par des sommets intermediaires
de numero inferieur ou egal `a k.
(k)
En eet, cette propriete des i,j est vraie pour k = 0. Soit k 1, et supposons cette
propriete vraie pour k 1. Chaque chemin reliant le sommet i au sommet j en ne passant
que par des sommets intermediaires de numero inferieur ou egal `a k peut soit ne pas passer
(k1)
par k - auquel cas, par hypoth`ese de recurrence, son co
ut minimal est egal `a i,j - soit etre
decompose en un chemin de i `a k, deventuels circuits partant et arrivant en k, et un chemin
de k `a j. Par hypoth`ese de recurrence, les co
ut minimaux de ces chemins intermediaires
(k1)
(k1)
(k1)
sont resp. i,k , (k,k ) et k,j . Le co
ut minimal dun chemin reliant i `a j en ne passant
que par des sommets intermediaires de numero inferieur ou egal `a k est donc donne par la
formule ci-dessus.
(n)
A la n de lalgorithme, la matrice (i,j ) a ete calculee, qui correspond `a la matrice des
co
uts minimaux des chemins reliant deux sommets quelconques du graphe, en ne passant
que par des sommets intermediaires de numero inferieur ou egal `a n, i.e. par nimporte
quel sommet.
Dans limplementation de lalgorithme donnee ici on nutilise quune seule matrice, donc
en calculant les coecients de la n de la matrice `a letape k ceux du debut ont dej`a
ete modies depuis letape k 1. La matrice M ne suit donc pas exactement la formule
de recurrence de la preuve, mais le resultat nal reste quand meme identique.
Lorsque S(, , , ) = {vrai,faux}(ou, et, faux, vrai), lalgorithme ci-dessus est exactement lalgorithme de Roy-Warshall de calcul de la fermeture transitive de G (on a ici
x =vrai pour tout x {vrai,faux}).
Dans le cas specique du calcul de plus courts chemins, i.e. lorsque S(, , , ) =
{R }(min, +, , 0), lalgorithme de Aho-Hopcroft-Ullman est plus connu sous le
nom dalgorithme de Floyd-Warshall.
Lalgorithme AHU calcule le co
ut minimal dun chemin entre deux sommets quelconques
du graphe, sans fournir un chemin qui le realise. Nous presentons ci-apr`es un algorithme
permettant de trouver eectivement un tel chemin.
Calcul dun chemin de co
ut minimal. Soit G = (S, A), un graphe sans circuit
represente par sa matrice dadjacence, et soit p, une fonction (poids) denie sur S S
comme ci-dessus. On suppose ici que cette fonction verie p(i, i) = 0.
On suppose egalement que lon a calcule les co
uts minimaux des chemins entre tous
les couples de sommets, i.e. que lon dispose de la matrice = (i,j )i,j . Pour calculer ces
chemins, on denit une matrice de liaison = (i,j )i j , de la mani`ere suivante : i,j =
& M. Finiasz

105

6.5 Applications des parcours de graphes

ENSTA cours IN101

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

void print_shortest_path(int** Pi, int i, int j) {


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

Sous-Graphe de Liaison
On denit le sous-graphe de liaison de G pour i par G,i = (S,i , A,i ), o`
u:
S,i = {j S, i,j = } {i},
A,i = {(i,j , j), j S,i \ {i}}.
Dans le cas o`
u le poids est donne par la longueur, on peut montrer que G,i est une
arborescence des plus courts chemins de racine i.
Il est possible de modier lalgorithme AHU pour calculer les matrices (i,j )i,j et
en meme temps. Le principe est le meme que dans lalgorithme initial, i.e. on calcule des
suites de matrices en considerant des sommets intermediaires dun chemin de co
ut minimal.
(k)
(0)
(1)
(n)
Plus precisement, on denit la sequence , , . . . , , o`
u i,j est le predecesseur du
sommet j dans un chemin de co
ut minimal issu de i et ne passant que par des sommets
intermediaires de numero inferieur ou egal `a k. On a bien entendu (n) = , et on peut
(k)
exprimer i,j recursivement par :
{
si i = j ou p(i, j) = ,
(0)
i,j =
i sinon,
et, pour k 1,

{
(k)
i,j

(k1)

(k)

(k1)

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

Avec les notations utilisees dans la preuve de la proposition 6.5.1 : on a ici i,j = p(i, j).
Au nal, on obtient AHU link, la version modiee de AHU calculant aussi la matrice .
Comme lalgorithme AHU de depart cet algorithme prend en argument des matrices dej`a
initialisees : i,j = 1 si (i, j)
/ A et i,j = i si (i, j) A.
106

F. Levy-dit-Vehel

Annee 2011-2012

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

Chapitre 6. Graphes

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

7.1

D
enitions

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

7.2 Lalgorithme de Rabin-Karp

ENSTA cours IN101

Un premier algorithme naf. Lalgorithme le plus naf pour la recherche de motif est
deduit directement de lenonce : on essaye tous les decalages possibles, et pour chaque
decalage on regarde si le texte correspond au motif. Voici le code dun tel algorithme :
1
2
3
4
5
6
7
8
9
10
11
12
13

void basic_pattern_lookup(int* T, int n, int* P, int m) {


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

Cet algorithme a une complexite dans le pire cas en (nm) qui nest clairement pas optimale : linformation trouvee `a letape s est totalement ignoree `a letape s + 1.

7.2

Lalgorithme de Rabin-Karp

Lidee de lalgorithme de Rabin-Karp est de reprendre lalgorithme naf, mais de remplacer la comparaison de mots par une comparaison dentiers. Pour cela, lidee et de considerer
le motif recherche comme un entier code en base d, o`
u d est le nombre delements de .
Ainsi, `a un motif de longueur m est associe un entier compris entre 0 et dm 1. De meme,
pour chaque decalage, on va denir un entier correspondant au m symboles de T partant
de ce decalage. On denit donc :
m1

p=
P [i]di ,
i=0

s [0, n m],

ts =

m1

T [s + i]di .

i=0

Une fois ces entiers denis, la recherche de motif consiste simplement `a comparer p `a
chacun des ts . Cependant, meme si on consid`ere que les operations sur les entiers se font
toujours en temps constant, cette methode ne permet pas encore de gagner en complexite
car le calcul de chacun des ts a une complexite en (m) et donc calculer tous les ts co
ute
(nm). An dameliorer cela on va calculer les ts de facon recursive : entre ts et ts+1 un
symbole est ajoute et un autre enleve. On a donc :

ts
+ dm1 T [s + m].
ts+1 =
d
110

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 7. Recherche de motifs

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

void Rabin_Karp_Partic(int* T, int n, int* P, int m, int d) {


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

En pratique, cette methode est tr`es ecace quand les entiers t et p peuvent etre representes
par un int, mais d`es que m et d grandissent cela nest plus possible. Lutilisation de grands
entiers fait alors perdre lavantage gagne car un operation sur les grands entiers aura un
co
ut en (m log(d)) et la complexite totale de lalgorithme devient alors (nm log(d))
(notons que cette complexite est identique `a celle de lalgorithme naf : le facteur log(d)
correspond au co
ut de la comparaison de deux symboles de qui est neglige dans la
complexite de lalgorithme naf).
Toutefois, une solution existe pour ameliorer cela : il sut de faire tous les calculs
modulo un entier q. Chaque fois que le calcul modulo q tombe juste (quand on obtient
p = ts mod q), cela veut dire quil est possible que le bon motif soit present au decalage s,
chaque fois que le calcul tombe faux on est certain que le motif nest pas present avec un
decalage s. Quand le calcul tombe juste on peut alors verier que le motif est bien present
avec lalgorithme naf. On obtient alors lalgorithme suivant :

1
2

void Rabin_Karp(int* T, int n, int* P, int m, int d, int q) {


int i,j,h,p,t;

& M. Finiasz

111

7.3 Automates pour la recherche de motifs


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

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

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

7.3

Automates pour la recherche de motifs

Les automates nis sont des objets issus de linformatique theorique particuli`erement
adaptes `a la resolution de certains probl`emes : typiquement, la recherche de motif. Apr`es
112

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 7. Recherche de motifs

Fonction de transition
Q
Q

S0
S0
S1
S1

0
1
0
1

S1
S0
S0
S1

1
0

S0

S1
0

S0 tat initial
S1 tat final

Figure 7.1 Exemple dautomate et la table de sa fonction de transition.


avoir deni ce quest un automate ni, nous verrons comment obtenir un algorithme de
recherche de motif qui aura toujours (pas uniquement dans les cas favorables) une complexite en (n + m||), donc tr`es proche de loptimal quand n est grand par rapport `a
m.

7.3.1

Automates nis

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

113

7.3 Automates pour la recherche de motifs


a

S0

b
b

S1

ENSTA cours IN101


b

S2

a,b

S3

S4

S5

b
a

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 realiser ecacement la recherche de motifs dans un texte. Soit donc un alphabet ni, et w avec |w| = m le
mot que lon va chercher. Nous cherchons `a determiner toutes les occurrences de w dans
un texte t de longueur n. Pour cela, nous allons construire un automate M reconnaissant
le langage w .
Lensemble des etats de lautomate est Q = {S0 , S1 , . . . , Sm }, letat initial est S0 , et
il poss`ede un unique etat nal F = {Sm }. Avant de denir la fonction de transition, on
introduit la fonction : Q, denie par (u) = max{0 i m, u = wi }, o`
u wi est
le prexe de longueur i de w. Ainsi, (u) est la longueur du plus long prexe de w qui soit
de facon `a realiser les equivalences suivantes
suxe de u. On cherche `a denir (et donc )

(o`
uu ):
0 , u) = Si si et seulement si w napparat pas dans u et (u) = i.
1. Pour 0 i < m, (S
2. (S0 , u) = Sm si et seulement si w apparat dans u.
Lequivalence 2. correspond exactement `a la fonction de lautomate M : on doit atteindre letat nal si et seulement si le motif est apparu dans le texte. Lequivalence 1.
nous sert `a construire lautomate : chaque fois lon rajoute un symbole x `a u, on passe de
la case S(u) `a la case S(ux) , et d`es que lon atteint la case Sm on y reste. On obtient alors
un automate tel que celui de la Figure 7.2.
En pratique, pour construire ecacement cet automate, le plus simple est de proceder
par recurrence. Pour construire un automate qui reconnat le mot wx, on part de lautomate
qui reconnat le mot w de longueur m et on letend pour reconnatre wx de longueur m + 1.
Supposons que lautomate Mw reconnaissant w soit construit. Pour construire lautomate
Mwx on commence par ajouter le nud nal Sm+1 `a Mw . Ne reste plus ensuite qu`a calculer
tous les liens partant de la Sm (la derni`ere case de lautomate Mw , donc lavant derni`ere
de Mwx ). Un lien est simple `a calculer : la transition en ajoutant x pointe vers Sm+1 . Les
autre liens peuvent etre calcules simplement en utilisant lautomate Mw precedemment
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

Annee 2011-2012

Chapitre 7. Recherche de motifs

premier symbole de w de telle sorte que w = yv, alors on aura aussi (wt) = (vt). Or le
motif vt est de longueur m et donc (vt) peut etre calcule en utilisant Mw . Lautomate
Mw termine dans letat S(vt) quand on lui donne le motif vt en entree : il sut alors de
faire pointer le lien indexe par t de Sm au S(vt) obtenu.
Il y a || 1 tels liens `a trouver et le co
ut pour trouver son extremite est un parcours
dautomate qui a une complexite (m). Cependant, les m 1 premiers caract`eres sont
communs pour les || 1 parcours `a eectuer. Il sut donc de faire un parcours de m 1
caract`eres puis de recopier les || 1 transitions issues de letat que lon a atteint. De plus,
le parcours des m 1 premiers caract`eres correspond en fait `a lajout dun caract`ere `a la
suite des m 2 caract`eres parcourus pendant la construction de Mw . Si lon a memorise
letat auquel aboutit le parcours de ces m 2 caract`eres, il faut donc (||) operations
pour construire lautomate Mwx `a partir de lautomate Mw . Le co
ut total de la construction
dun automate Mw pour un motif de longueur m est donc (m||), mais cela demande
de programmer comme il faut ! Voici un exemple de code C pour la construction dun tel
automate : P est le motif recherche, m sa longueur et sigma la taille de lalphabet ; le
tableau d qui est retourne correspond `a la fonction de transition avec d[i][j] = (Si , j).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

int** automata_construction(int* P, int m, int sigma) {


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

16

/* on traite les
etats suivants */
for (i=1; i<m+1; i++) {
for (j=0; j<sigma; j++) {
d[i][j] = d[etat_mem][j];
}
if (i < m) {
d[i][P[i]] = i+1;
etat_mem = d[etat_mem][P[i]];
}
}
return d;

17
18
19
20
21
22
23
24
25
26
27
28

Ensuite, pour rechercher un motif w dans un texte t de longueur n il sut de construire


& M. Finiasz

115

7.3 Automates pour la recherche de motifs

ENSTA cours IN101

lautomate M reconnaissant w , puis de faire entrer les n symboles de t dans M . Le


co
ut total de la recherche est donc (n + m||).

7.3.3

Reconnaissance dexpression reguli`eres

Les automates sont un outil tr`es puissant pour la recherche de motif dans un texte, mais
leur utilisation ne se limite pas `a la recherche dun motif xe `a linterieur dun texte. Une
expression reguli`ere est un motif correspondant `a un ensemble de chanes de caract`eres.
Elle est elle meme decrite par une chane de caract`ere suivant une syntaxe bien precise
(qui change selon les langages, sinon les choses seraient trop simples !). Reconnatre une
expression reguli`ere revient `a savoir si le texte dentree fait partie des chanes de caract`ere
qui correspondent `a cette expression reguli`ere.
La description compl`ete dune syntaxe dexpression reguli`ere est un peu complexe donc
ne sont donnes ici que quelques exemples dexpressions (et les chanes de caract`eres quelles
representent), pour se faire une idee de la signication des dierents symboles.
a = la chane a uniquement
abc = le mot abc uniquement
. = nimporte quel symbole = les chanes de longueur 1
a* = toutes les chanes composees de 0 ou plusieurs a (et rien dautre)
a? = 0 ou une occurrence de a
a+ = toutes les chanes composees de au moins un a (et rien dautre)
.*a = toutes les chanes se terminant par a
[ac] = le caract`ere a ou le caract`ere c
.*coucou.* = toutes les chanes contenant le mot coucou (lobjet de ce chapitre)
(bob|love) = le mot bob ou le mot love
[^ab] = nimporte quel caract`ere autre que a ou b
Les combinaisons de ces dierentes expressions permettent de decrire des ensembles de
chanes tr`es complexes et decider si un texte correspond ou pas `a une expression peut etre
dicile. En revanche, il est toujours possible de construire un automate ni (mais des fois
tr`es grand !) qui permet de decider en temps lineaire (en la taille du texte) si un texte
est accepte par lexpression ou non. La complexite de la construction de cet automate
peut en revanche etre plus elevee. La Figure 7.3 donne deux exemples dautomates et les
expressions reguli`eres quils acceptent.

116

F. Levy-dit-Vehel

Annee 2011-2012

Chapitre 7. Recherche de motifs

Automate pour l'expression a.*ees


[^e]
a

S0

S1

[^s]
e

S2

S3

S4

[^e]
[^s]
.

[^

a]

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


.

t
[^etr]
[^etr]

S1'

S2'

t
t

.
r

e
r

S0

e
[^etr] S1

S2

.
r

S3

e
[^etr]

Figure 7.3 Exemples dautomates reconnaissant des expressions reguli`eres.

& M. Finiasz

117

Vous aimerez peut-être aussi