You are on page 1of 25

Introduction

lAlgorithmique
Conception et Analyse des algorithmes
Structures de donnes fondamentales
Problmes en thorie des graphes

Philippe Giabbanelli
Mai 2006, version 1
http://www.aqualonne.fr.st

Table des Matires


Avant-propos

Partie 1
Chapitre 1 : Les questions fondamentales

7
8

Quest-ce quun algorithme ?. . . . . . . . . . . . . . . . . . . . . . .


Quest-ce que la rcursivit ? . . . . . . . . . . . . . . . . . . . . . . .
Comment grer la rcursivit ? . . . . . . . . . . . . . . . . . . . . . .
Quand est-ce quune fonction rcursive a besoin de pile ? . . . . . . . .
Comment dcrire la performance de mon algorithme ? . . . . . . . . . .
Quels sont les diffrents critres de performance ? . . . . . . . . . . . .
Comment faire abstraction de la machine ? . . . . . . . . . . . . . . . .
Comment savoir si mon algorithme est le meilleur ? . . . . . . . . . . .
Entranement. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Chapitre 2 : Le problme des structures de donnes

8
10
11
12
13
15
16
18
20

21

Prsentation formelle du problme. . . . . . . . . . . . . . . . . . . . . 21


Un pointeur du point de vue physique. . . . . . . . . . . . . . . . . . . 22
Un pointeur du point de vue algorithmique. . . . . . . . . . . . . . . . . 24

Introduction lAlgorithmique, mai 2006

Philippe Giabbanelli

Introduction lAlgorithmique, mai 2006

Philippe Giabbanelli

Avant-Propos
Pourquoi ce livre ?
Il existe de trs nombreux ouvrages consacrs lalgorithmique, dont la plupart sappuient
sur des langages la mode (C++, Java) ou se contentent dtre des bibliothques
dalgorithmes. Notre but ici est damener le lecteur concevoir ce dont il a besoin, tout en
rsolvant certains cas bien connus de la littrature, titre dexemple.
Lutilisation de langages bien particuliers nest pas saine, car on a tendance relguer la
partie algorithmique au second plan, derrire les spcificits du langage. Pour rsoudre un
problme de faon gnrale, il nest pas utile de connatre la taille des entiers en C ou les
mthodes des diffrentes classes Java : seul le raisonnement compte, et lcriture est un dtail.
De plus, il est particulirement difficile de dmontrer la correction dun programme dans des
langages impratifs, cause de la seule affectation. Dans une dmonstration mathmatique,
les objets ne changent pas au cours du temps ! En outre, aprs deux millnaires de pratique
mathmatique, il ny a toujours pas eu de ncessit dintroduire les boucles : le raisonnement
par rcurrence suffit pour faire lessentiel des tches. Il est nest donc pas rigoureux de
vouloir penser immdiatement un algorithme en termes de for, while et daffectations.
Ainsi on utilisera essentiellement un langage pdagogique bases de rgles, permettant une
formalisation solide des diffrentes notions. Notre langage sera centr sur la rcursivit, dont
on montrera que les boucles ne sont que des cas particuliers. Une fois que le lecteur aura
compris la dmarche menant lalgorithme, alors il pourra le traduire dans un langage de son
choix ; quelques exemples seront donns en C, Scheme et Java.

A qui sadresse-t-il ?
Le lecteur na pas besoin davoir de grandes connaissances en informatique. Une pratique
rudimentaire dun langage pourra lui permettre de coder les exemples prsents dans ce livre,
ainsi que les algorithmes simples quil pourra concevoir par la suite.
Il nest pas non plus ncessaire davoir des connaissances pousses en mathmatiques. Un
lve moyen sortant dun baccalaurat scientifique devrait pouvoir saisir lessentiel des
notions. Certaines techniques un peu plus avances, comme la rsolution des quations de
rcurrence ou les sries, sont absolument ncessaires du point de vue algorithmique mais non
requises pour le reste de louvrage.
Lessentiel est davoir une certaine capacit dabstraction et un raisonnement rigoureux.
Lalgorithmique nest pas un code que lon crit rapidement, puis que lon dbugue jusqu
le faire marcher ; cest avant tout une science de la rflexion : on commence par penser,
ensuite on formalise, et si lon veut coder alors on essaye de le faire bien du premier coup.
Introduction lAlgorithmique, mai 2006

Philippe Giabbanelli

Comment le livre est-il organis ?


On adopte une division en trois parties. La premire donnera les bases de lalgorithmique, en
rpondant aux questions les plus fondamentales : quest-ce quun algorithme, comment
savoir si mon programme est rapide ? On montrera ensuite le problme des structures de
donnes, avec les exemples de pile, file et liste. On verra quelques algorithmes sur les listes,
dans le but de se familiariser avec notre criture. Enfin, on terminera cette partie en
prsentant des structures plus avances comme les tables de hachage, les arbres, et les tas.
Dans la seconde partie, on commencera ltude des techniques algorithmiques, avec
lapproche diviser pour rgner . On sen servira en particulier pour le problme du tri,
avec les algorithmes de tri par comparaison. Le chapitre qui suit sera consacr lutilisation
du tri, mettant en avant la dichotomie.
Dans la troisime et dernire partie, on propose une introduction aux graphes par la
construction darchitectures : le problme -d. Une fois prsents lintrt des graphes et
quelques problmes classiques, on met en place les structures de donnes utilises pour la
reprsentation des graphes.
On ne traitera pas dans ce livre des problmes de flot, des automates ou de la gomtrie.

Quelles sont les inspirations de ce livre ?


Certains des exemples de la premire partie sont tirs des exercices de Jean-Paul Roy1 ou du
cours de Jean-Clarence Nosmas2.
La seconde partie reprend quelques thmes des cours de Francis Avnaim3 et Fabrice Huet4.
Enfin, le problme -d de la troisime partie provient des cours de Jean-Claude Bermond5.
Tous enseignent ou ont enseign luniversit de Nice Sophia-Antipolis.
On utilise galement Elments dAlgorithmique de D. Beauquier, J. Berstel et Ph.
Chrtienne, ainsi que Graphes et Algorithmes de Michel Gondran et Michel Minoux.

Professeur agrg, responsable des cours de programmation fonctionnelle en 2nd anne.


Professeur de mathmatiques discrtes.
3
Professeur agrg, co-responsable du cours de structures de donnes en 1re anne.
4
Matre de confrence, co-responsable du cours de structures de donnes en 1re anne.
5
Directeur de recherche, auteur de plus de 120 articles sur la thorie des graphes, le calcul parallle et distribu,
les tlcommunications.
2

Introduction lAlgorithmique, mai 2006

Philippe Giabbanelli

Introduction lAlgorithmique, mai 2006

Philippe Giabbanelli

Partie 1
Formalisation et criture des algorithmes
Complexit en temps et en espace
Structures de donnes

Introduction lAlgorithmique, mai 2006

Philippe Giabbanelli

Chapitre 1

Les questions fondamentales


I.

Quest-ce quun algorithme ?

Un algorithme est un ensemble dinstructions traitant une information, ce qui prend la forme en
informatique dun programme utilis pour rsoudre un problme donn. Pour chaque donne du problme,
lalgorithme doit fournir une rponse aprs un nombre fini dtapes : le programme doit finir par sarrter !
Dun point de vue formel, on peut voir un problme abstrait comme tant lensemble < E, A, S > o E est
une entre (i.e. un des cas du problme), A lalgorithme, et S la sortie (i.e. rsultat au cas donn).
Un algorithme est vu comme < C I >, soit un ensemble de rgles (clauses) qui entranent des
traitements (instructions).
Ces rgles forment une partition de lunivers :
- (1) Si on fait lunion des rgles, on doit retrouver tous les cas possibles.
- (2) Lintersection de deux rgles doit tre vide.
Autrement dit, lalgorithme ne doit pas oublier un cas (1), et on ne peut pas tre dans deux cas la fois (2).

Exemple 1 : Dterminer le maximum dun tableau


Lentre est un tableau T dentiers. Les cases de T sont numrotes de 1 n. On dit que n est sa taille.
La sortie est le plus grand entier du tableau ; autrement dit, lentier suprieur tout lment du tableau T.
On peut noter la sortie n T[i] i[|1, n|] , o T[i] dsigne llment situ la case i du tableau T.
Avant dcrire de faon abstraite un algorithme, il faut avoir les ides claires sur le problme. On doit
commencer par traiter un exemple : tant donn une instance du problme, comment trouver la sortie ?
On prend un tableau 2 3 1 4 5 3 2 1 4 3 5 1, et on veut son maximum. On commence par considrer son
premier lment comme tant un maximum potentiel, et on regarde sil y a plus grand que lui. On voit que
3 est plus grand, le nouveau maximum est donc 3. On continue : 1 nest pas plus grand, par contre 4 est
plus grand. On a ensuite que 5 est plus grand que 4, et rien nest plus grand que 5 : cest donc le maximum.
Une fois quon a compris ce quon vient de faire, on peut labstraire :
- je considre le premier lment du tableau comme maximum
- si je trouve un lment plus grand que lui, cest mon nouveau maximum
- une fois que jai tout parcouru, je renvoie mon dernier maximum : rien trouv de plus grand
Ecrivons alors lalgorithme. Cest une fonction, on lui donne donc des objets sur lesquels travailler :
Je lui donne mon tableau, et sa
Lalgorithme attend un tableau T,
er
une taille n. Il va parcourir le tableau Max(T, n, i, max) = Max(T, |T|, 2, T[1])) taille. Comme je lui donne le 1
lment comme maximum, je
partir dun indice i, et au dpart il
parcours partir du 2nd.
a besoin dun maximum
Lalgorithme est initialis : il a toutes les donnes dentre dont il a besoin. On peut maintenant dtailler
les rgles qui vont nous conduire la solution : comment trouver le maximum de ce tableau ?
i n ^ T[i] > max
Max(T, n, i, max) = Max(T, n, i+1, T[i])
Si je nai pas parcouru tout le tableau et que llment actuel est suprieur au max, cest le nouveau max.
i n ^ T[i] max
Max(T, n, i, max) = Max(T, n, i+1, max)
Si mon lment actuel nest pas suprieur au max, alors je continue simplement de parcourir.
i>n
Max(T, n, i, max) = max
Si jai parcouru tout le tableau, je renvoie le max.
Introduction lAlgorithmique, mai 2006

Philippe Giabbanelli

On vient dcrire un algorithme comme un ensemble de rgles ( gauche) entranant des actions ( droite).
Si on fait lunion de ces rgles, on retrouve bien tous les cas possible :
(i > n) (i n ^ T[i] > max) (i n ^ T[i] max) = {i, T[i]}
Si on fait lintersection des rgles deux deux, elle est ncessairement vide. Par exemple :
(i n ^ T[i] > max) (i n ^ T[i] max) = {}, llment ne peut pas tre la fois plus petit et plus grand.
(i > n) (i n ^ T[i] max) = {}, on ne peut pas tre la fois dans le tableau et hors du tableau.
Autrement dit, tous les cas sont bien grs, et lalgorithme ne peut pas tre dans deux cas la fois.

Exemple 2 : Dterminer le nombre doccurrences dun lment dans un tableau


En entre, on a un tableau T dentiers non ncessairement distincts (i.e. un entier peut y tre plusieurs fois).
En sortie, on veut le nombre de fois que lentier est prsent ; cest son nombre doccurrences .
Exemple dinstance du problme : 2 3 1 4 5 3 2 1 4 3 5 1, on compte llment 1, il est prsent 3 fois.
La solution est simple : on parcours le tableau, et chaque fois que lon voit llment qui nous intresse,
alors son nombre doccurrences augmente de 1. Cependant, mme avec une solution nonce de faon
aussi lmentaire, il y a de nombreuses faons dcrire lalgorithme correspondant.
La premire, et peut-tre la plus habituelle pour les personnes habitues crire avec un langage impratif
ou objet (C, Java, C++), est de considrer quil nous faut le tableau T, sa taille n, lindice de parcours i,
llment , et une variable de rsultat res dans laquelle on va accumuler le nombre doccurrences.
1.
2.
3.
4.

NbOcc(T, n, i, , res) = NbOcc(T, |T|, 1, , 0)


i n ^ T[i] =
NbOcc(T, n, i, , res) = NbOcc(T, n, i+1, , res + 1)
i n ^ T[i]
NbOcc(T, n, i, , res) = NbOcc(T, n, i+1, , res)
i>n
NbOcc(T, n, i, , res) = res

Commentaires :
1. Il faut toujours initialiser lalgorithme. Il attend des donnes, on lui donne ce quon a dans notre
cas. On commence parcourir le tableau T partir de la 1re case, et il y a initialement 0
occurrences dtectes.
2. Si nous navons pas fini de parcourir le tableau et que llment courant est , alors on continue de
parcourir le tableau et on augmente le rsultat de 1.
3. Si nous navons pas fini de parcourir le tableau et que llment courant nest pas , on se
contente de continuer parcourir le tableau. Le nombre doccurrences ne bouge pas.
4. Si on a fini de parcourir le tableau, alors on renvoie le rsultat quon a accumul.
En y rflchissant, lalgorithme a-t-il ncessairement besoin dun indice de parcours i ? Quel est son rle ?
Cet indice de parcours doit regarder toutes les cases du tableau, soit de 1 n. Or, on peut tout aussi bien
manipuler lindice n : partons de n, et descendons jusqu arriver 1. Ainsi, on conomise une variable.
De mme, avons-nous besoin dune variable pour accumuler les rsultats ? Si on dtecte une occurrence,
il suffit de dire quil y en aura une de plus quau stade suivant. Ainsi :
1.
2.
3.
4.

NbOcc(T, n, )
n 1 ^ T[n] =
n 1 ^ T[n]
n<1

= NbOcc(T, |T|, )
NbOcc(T, n, ) = 1 + NbOcc(T, n-1, )
NbOcc(T, n, ) = NbOcc(T, n-1, )
NbOcc(T, n, ) = 0

Si on a notre lment, alors il y a une occurrence de plus que dans le reste du tableau.
Si ce nest pas notre lment, alors il y a le mme nombre doccurrence que dans le reste du tableau.
Si on est sorti du tableau, on ne peut pas trouver notre lment : on sarrte, et il y a 0 occurrences.
Introduction lAlgorithmique, mai 2006

Philippe Giabbanelli

II.

Quest-ce que la rcursivit ?

Quand une fonction se rappelle elle-mme, on dit quelle est rcursive. Une fonction rcursive est
caractrise par une, ou plusieurs, relation(s) de rcurrence. Par exemple, la factorielle :
n > 1 f(n) = n*f(n-1)
n = 1 f(1) = 1
La fonction rcursive se rappelle jusqu arriver sur un cas darrt (aussi appel cas de base), qui donne
la solution dans la situation la plus simple. Pour la factorielle, le cas de base est par exemple f(1) = 1.
Quand on construit des fonctions rcursives, le raisonnement adopt est en gnral :
- Comment traiter le cas le plus simple ?
- Comment me ramener dun cas plus compliqu au cas simple ?

Exemple 1 : Dfinir la multiplication


Imaginons quon ne sache pas faire la multiplication y*x. Dans le cas le plus simple, on sait faire 1*x = x.
Comment nous ramener dun cas plus compliqu ce cas l ? Il faut faire chuter y jusqu arriver 1.
Autrement dit, il nous faut une relation de rcurrence sur y. Or, on a y*x = x + (y-1)*x.
On peut alors dfinir la multiplication uniquement comme une succession dadditions :
y > 1 multiplication(x,y) = x + multiplication(x, y-1)
y = 1 multiplication(x,1) = x
Pour bien voir ce qui se passe, on droule la rcurrence , cest--dire quon regarde tape par tape :
multiplication(3, 5) = 5 + multiplication(2, 5) = 5 + (5 + multiplication(1,5)) = 5 + (5 + (5)) = 15.

Exemple 2 : Nombre de rgions dlimites par n frontires rectilignes


On a n droites en position gnrale , cest--dire quelles ne sont pas parallles et ne se coupent pas
toutes en un mme point. On veut calculer le nombre de rgions dlimites par ces n droites.
2
6
5
4
1
2
3
2
7
4
3
1
1
Par exemple, une droite dlimite 2 rgions ; 2 droites dlimitent 4 rgions, et 3 droites dlimitent 7 rgions.
Le cas darrt de la rcursivit est donc celui o on a une seule droite. On pose alors region(1) = 2.
On veut maintenant une relation de rcurrence sur le nombre de droites. Une relation entre n et n-1 est la
mme chose quune relation entre n+1 et n ; selon les cas, certains parlent plus que dautres. Ici, il est plus
vident de se dire si on a dj n droites, que se passe-t-il lorsque jen ajoute une ? .
La nouvelle droite va couper toutes les prcdentes : il y a donc n points dintersections. On remarque sur
nos exemples que sil y a n points dintersections, alors cela veut dire quon a fait n+1 rgion. Par exemple,
quand la 2nd droite vient couper la premire en un point, il y a 1 + 1 = 2 nouvelles rgions. Quand la 3me
droite vient couper les deux prcdentes en 2 points, il y a 2 + 1 = 3 nouvelles rgions.
On a donc region(n+1) = region(n) + n + 1, ce qui est quivalent region(n) = region(n-1) + n.
On obtient ainsi lalgorithme donnant le nombre de rgions que dlimitent n frontires rectilignes :
n > 1 region(n) = region(n-1) + n
n = 1 region(1) = 2
Notons que dans des algorithmes aussi simples, on peut enlever la rcursivit et exprimer directement le
rsultat en fonction de n. On verra au chapitre < ?> page XXX la rsolution des quations de rcurrence.
Introduction lAlgorithmique, mai 2006

10

Philippe Giabbanelli

III. Comment grer la rcursivit ?


Quand la fonction est (vritablement) rcursive, lordinateur est bien oblig de stocker les calculs quelque
part. Il utilise pour cela une pile ; il sagit dun objet dfini par deux oprations :
- empiler (ajouter un lment au sommet)
- dpiler (prendre llment qui est au sommet)
Faisons un exemple ci-dessous :
1
3

3
empiler 3

empiler 1

4
4
1
3

empiler 4

1
3
depiler

8
3

depiler

empiler 8

depiler

Exemple 1 : Gestion de la pile pour la factorielle


On a lalgorithme suivant pour le calcul de la factorielle :
F(n) = F(m)
(notons quil y toujours une initialisation)
n > 1 F(n) = n*F(n-1)
n = 1 F(1) = 1
Calculons F(3). On ne pourra calculer F(3) quaprs avoir calcul F(2) : on doit donc mettre une partie du
calcul en attente, pour ne pas loublier ; ainsi, on va empiler lappel F(3). On veut maintenant calculer F(2)
et on a besoin de F(1). On empile lappel F(2). On arrive F(1). On sait le calculer : F(1) = 1.

3!

3
n

?
F(n)

2!
3!

2
3
n

?
?
F(n)

1!
2!
3!

1
2
3
n

1
?
?
F(n)

Maintenant quon est arriv au bout de la rcurrence, on peut calculer tout ce quon avait mit en attente. Il
suffit de remonter. On va ainsi dpiler les appels.
1
2
3

1
?
?

2
3

2
?

F(n)

F(n)

3
n

6
F(n)

Exemple 2 : Une factorielle qui na pas besoin de pile


On dispose maintenant dun algorithme pour la factorielle utilisant une variable acc, qui accumule :
F(n,acc) = F(m, 1)
n > 1 F(n, acc) = F(n-1, n*acc)
n = 1 F(1, acc) = acc
Il ny a pas besoin ici de mettre des calculs en attente : on peut les enchaner. Ainsi, on se passe de la pile.
Regardons pour F(3) :
F(3, 1) = F(2, 3*1) = F(1, 2*3) = 6
Dans les deux exemples, notre algorithme est crit de faon rcursive : la fonction F se rappelle bien ellemme. Pourtant, il y a un cas o on a besoin dutiliser la pile, et un cas o on sen passe. Nous allons voir
quil y a ainsi deux types de rcursivit
Introduction lAlgorithmique, mai 2006

11

Philippe Giabbanelli

IV. Quand est-ce quune fonction rcursive a besoin de pile ?


Si la fonction est du type F(x1, , xn) = G(y1, , yn), on dit que cest une rcursivit terminale . Il ny
a alors pas besoin de pile pour continuer le calcul. Dans le calcul de la factorielle sans pile, on avait ainsi :
- F(n, acc) = F(n-1, n*acc).
On peut identifier y1 = x1 1, y2 = n*x2 et G = F.
- F(1, acc) = acc.
On peut identifier y1 = 0, y2 = x2 et G = y2.
Lorsquil y a quelque chose autour de la fonction G, on dit que cest une rcursivit enveloppe .
Autrement dit, il y a des oprations qui enveloppent la fonction : F(n) = n*F(n-1), o la fonction est
enveloppe par une multiplication avec n. Dans ce cas, on est oblig dutiliser une pile.
Quand est en prsence dune rcursivit terminale, la fonction est strictement quivalente des boucles
comme while ou for. En revanche, si cest une rcursivit enveloppe, alors il ny a pas dautres critures
possibles : on pourrait dire que cest la vraie rcursivit.

Exemple : Faire la somme des lments dun tableau


On dispose dun tableau T dentiers, et on veut faire la somme de tous ses lments.
Le principe est similaire lalgorithme pour compter le nombre doccurrences :
- lorsque je suis sur une case, alors la somme est la valeur de cette case laquelle on ajoute la
valeur de toutes les autres
- lorsque je suis sorti du tableau, la somme est ncessairement 0 (i.e. llment neutre de la somme)
1. (T, n)
2. n 1
3. n < 1

= (T, |T|)
(T, n) = T[n] + (T, n-1)
(T, n) = 0

On remarque la ligne 2 que lappel est envelopp par laddition avec T[n]. On a une rcursivit
enveloppe, et on va donc utiliser la pile. On aimerait sen passer, et il faudrait donc traduire ceci en une
rcursivit terminale. Le but maintenant est de faire rentrer laddition avec T[n] dans la fonction.
Pour cela, on utilise un accumulateur : au dpart on le met 0, et chaque appel on y ajoute T[n]. A la fin,
il suffira de rendre la valeur accumule :
1. (T, n, acc) = (T, |T|, 0)
2. n 1
(T, n, acc) = (T, n-1, acc + T[n])
3. n < 1
(T, n, acc) = acc
On a obtenu une rcursivit terminale. Ceci na donc pas besoin de pile et peut scrire avec une boucle.
fonction F(T, n, acc)
tant que n 1
acc acc + T[n]
n n1
retourner acc
Toute rcursivit terminale peut scrire avec des boucles (i.e. de faon itrative), mais ce
nest pas pour autant que tous les compilateurs le font. Beaucoup ne dtectent pas cette
possibilit : ils noptimisent pas la rcursivit, et vont continuer utiliser de la pile alors que
ce nest pas ncessaire ; cest notamment le cas de Java En revanche, cest souvent
optimis dans les langages de la programmation fonctionnelle (comme Scheme) car ils
sappuient sur la rcursivit. En Java, on dispose de mots cls comme while, et cest au
programmeur de fournir leffort dcriture. Lalgorithmique rsout les problmes (une
dmarche rcursive est conseille) et la programmation soccupe de lcriture de la solution.
Vouloir absolument crire des boucles est absurde : ni toujours possible, ni toujours meilleur !
Introduction lAlgorithmique, mai 2006

12

Philippe Giabbanelli

V.

Comment dcrire la performance de mon algorithme ?

Etant donn un problme, il y a de nombreuses faons darriver la solution, avec des mthodes plus ou
moins efficaces. Parmi diffrents algorithmes, il faut donc savoir lequel choisir, et dans quelles conditions.
Nous allons voir deux exemples, puis mettre en place une formalisation du cot des algorithmes.
Il faut commencer par se donner une unit de mesure. Celle-ci ne peut pas tre le temps : le temps prit par
un algorithme va varier selon les performances des ordinateurs ! On choisit donc comme unit
lopration lmentaire , propre chaque algorithme. En additionnant toutes les oprations
lmentaires, on aura une bonne estimation du cot. Plus le cot est faible, meilleur est lalgorithme.
Regardons lalgorithme suivant, qui est purement pdagogique :
1. fonction F(T, n)
2.
res 0
3.
tant que n 1
4.
res res*2 + 5
5.
res res 4
6.
retourner res
Les oprations lmentaires sont ici les oprations arithmtiques des lignes 4 et 5. A chaque tour de la
boucle, on effectue 1 multiplication, 1 addition et 1 soustraction. La boucle va tourner n fois.
Lalgorithme a donc besoin de n*(1+1+1) = 3*n oprations. On voit que la performance de lalgorithme
dpend bien de ses entres. Si le tableau contient 1 million dlments ou sil en contient 10, lalgorithme
ne sera pas aussi rapide.
Reprenons prsent lalgorithme qui donnait le maximum dun tableau. Comme il sagit dune rcursivit
terminale, alors on peut le traduire avec une boucle, ce qui est (au dpart) plus simple pour calculer.
Max(T, n, i, max) = Max(T, |T|, 2, T[1]))
1. fonction Max(T, n)
i n ^ T[i] > max Max(T, n, i, max) = Max(T, n, i+1, T[i])
2.
max T[1]
i n ^ T[i] max Max(T, n, i, max) = Max(T, n, i+1, max)
3.
pour i de 2 n
i>n
Max(T, n, i, max) = max
4.
si T[i] > max
5.
max T[i]
6.
retourner max
On veut compter le nombre exacts doprations de cet algorithme : comptons les affectations et
comparaisons. Il y a 1 comparaison chaque tour de boucle, et il y a n 2 + 1 soit n 1 tours de boucle.
Il y a un problme : max T[i] nest excut que si T[i] > max ; or, ceci nest pas une opration
facilement quantifiable. Une boucle comme i de 1 n va tourner exactement n fois, mais on ne peut
pas savoir en soit le nombre de fois quon aura T[i] > max !
La condition dpend des donnes. Il faut donc distinguer plusieurs cas :
- si le maximum est au dbut du tableau, alors la condition T[i] > max ne sera jamais vrifie. Ainsi,
on a juste une affectation (ligne 2) et n 1 comparaisons soit un total de n oprations.
- si le tableau est tri dans lordre croissant, chaque lment est plus grand que le prcdent et donc
la condition sera toujours vrifie. On a alors (n 1)*2 + 1 = 2n 1 oprations.
- si le tableau est alatoire, on peut dire quen moyenne la condition a une chance sur 2 dtre
vrifie. On a alors (n 1)*(1 + ) + 1 = (3*n 1)/2.
Ainsi, le cot de lalgorithme dpend de la taille (ici n) des donnes, mais peu aussi tre dpendant de leur
nature : comment sont-elles ? Lexamen de la nature des donnes amne en gnral distinguer trois cas :
le meilleur des cas, le pire des cas et le cas moyen ; dfinissons les de faon formelle.
Introduction lAlgorithmique, mai 2006

13

Philippe Giabbanelli

Soit I lensemble des donnes dinstances dun problme abstrait (i.e. tous les cas possibles).
Notons In les donnes de taille n, car on a vu que le cot de lalgorithme dpend de la taille des donnes.
On dsigne par c(i) le cot de lalgorithme rsolvant le problme pour une donne iIn.
Le cot dans le pire des cas est celui tel que lalgorithme est le moins performant ; cest donc le plus lev
de tous les cots possibles, sur des donnes de taille n. On le note par W, pour Worst Case (pire des cas).

Walgorithme(n) = max{c(i) | iIn}


Le cot dans le meilleur des cas est celui tel que lalgorithme est le plus performant : cest le plus faible
de tous les cots possibles sur des donnes de taille n. On le note par B, pour Best Case (meilleur des cas).

Balgorithme(n) = min{c(i) | iIn}


Le cot dans cas moyen suppose quon connat la probabilit p(i) de chaque cas i. Pour avoir le cot dun
cas, on multiplie la probabilit davoir ce cas par son cot : p(i)*c(i). Le cot total est donc la somme des
cots pour tous les cas. On note ce cot par A pour Average Case (cas moyen).

algorithme

> p 0 i1 $c 0 i1

i2I
n

En gnral, on suppose que tous les cas ont la mme probabilit dapparatre. Autrement dit, on considre
que la distribution des probabilits est uniforme. Il suffit donc de sommet le cot de tous les cas, et de
diviser par le nombre de cas, cest--dire par la taille de lensemble In des cas de taille n.

algorithme

>

= 1$
c 0 i1
:I : i 2 I
n

Il existe un autre cot, que lon nomme le cot amorti, utilis en particulier lorsquon fait des suites
doprations sur des structures de donnes (comme une pile). Pour avoir le cot total des oprations, on
fait la somme du cot pour chaque opration. Le plus simple est de majorer le cot de chaque opration,
en utilisant le pire des cas ; on obtient une estimation qui nous dit, au pire, le cot de la suite doprations.
Or, cette estimation qui consiste majorer par le pire des cas est bien trop excessive. En effet, on observe
que le pire des cas se rpte rarement. Dans de nombreuses structures, ce pire des cas rsulte dun
dsquilibre exceptionnel, mais le traitement de ce cas va rtablir lquilibre. On a donc un phnomne de
compensations entre oprations conscutives, do un amortissement des cots.
Pour illustrer les trois principaux cots prsents, reprenons lexemple de lalgorithme du maximum.
On veut dterminer sa complexit dans le pire des cas ; la question qui se pose est alors : quelle
organisation du tableau entrane le pire des cas ? Quest-ce que signifie, en pratique, ce pire des cas ?
Dans cet algorithme, le pire des cas est celui qui valide le plus souvent llment courant est suprieur
au maximum . Or, il y a une organisation du tableau telle que, si on le parcours de 2 n, llment
courant soit toujours plus grand que le maximum tabli : cest le tableau tri en ordre croissant.
Une fois le pire des cas connu, on peut calculer le cot ; nous avons vu quil est de 2n 1 oprations.
Pour le meilleur des cas, on applique le mme raisonnement : cest lorganisation du tableau telle que la
condition llment courant est suprieur au maximum soit le moins souvent valide. Or, dans un
tableau tri en ordre dcroissant, le maximum est au dbut, et sera suprieur tous les lments parcourus.
Introduction lAlgorithmique, mai 2006

14

Philippe Giabbanelli

VI. Quels sont les diffrents critres de performance ?


Comme on sen doute, le nombre dopration nest quune partie des critres pour savoir quel algorithme
choisir. Il faut non seulement valuer son cot en calculs (le nombre doprations), mais aussi la mmoire
dont il a besoin. Certains algorithmes utilisent un petit nombre de variables, dautres ont besoin de crer
des tableaux intermdiaires, ou font appel la pile car la rcursivit est enveloppe
Lefficacit dun algorithme est dsigne sous le terme de complexit . On la mesure, sur une taille
dentre n donne, par deux fonctions : le nombre doprations (complexit en temps) et la place mmoire
(complexit en espace). Dterminer ces fonctions est le domaine de lanalyse de complexit .
Il est particulirement important de mesurer la mmoire utilise, car cest une ressource qui peut-tre rare
ou dont lutilisation peut savrer trs coteuse du point de vue nergtique (imaginons les satellites).
Pour un problme donn, on peut donc tre amen choisir entre un algorithme rapide mais utilisant
beaucoup de mmoire, et un algorithme plus lent qui utilise la mmoire de faon modre. Cest ensuite
une question de cible : un algorithme qui a besoin de 1 Go de mmoire dans la plupart des cas ne sera pas
mit sur des ordinateurs personnels, mais on peut y songer sur des serveurs ou stations graphiques.
En outre, la nature des donnes quon soumet lalgorithme a une certaine importance. Dans la vie
professionnelle, les algorithmes servent rarement des donnes gnriques ; on dispose souvent
dinformations complmentaires. Par exemple, si on doit trier un tableau quelconque alors on utilisera un
algorithme qui est bon dans le cas gnral, mais si le tableau est presque dj tri alors on fera appel un
algorithme qui est plus efficace pour ce cas.
Il faut examiner la complexit des algorithmes, la nature des donnes, et ventuellement la machine cible.

Exemple 1 : Calcul de complexit


1. fonction Algo(n)
2.
crer une matrice Mn,n
3.
pour i de 1 n
4.
pour j de 1 n
5.
Mi,j i*j
7.
retourner Mn,n
La complexit sera toujours la mme : il ny a pas regarder un cot dans le meilleur des cas, etc.,
puisquil ny a visiblement aucune condition relative la nature des donnes.
Lopration que lon considre comme lmentaire pour le temps de calcul est laffectation Mi,j i*j .
Avec la boucle j, on ralise n fois cette opration ; comme la boucle j est elle-mme excute n fois, alors
on a un total de n oprations lmentaires. Concernant la mmoire, on dclare un tableau de n*n
lments, do un cot de n en espace. Au final, lalgorithme a une complexit de n en temps et espace.

Exemple 2 : Recherche dans un tableau particulier


Quand on veut tester la prsence dun lment dans un tableau, pour le cas gnral on est oblig de
regarder tous les lments. Intressons nous maintenant un tableau tel que tout lment a une diffrence
de +- 1 avec llment suivant. Par exemple on a 3 2 1 1 1 1 2 3 4 5 6 7 8 9 et on cherche 5.
Comme 3 a une diffrence de +-1 avec le suivant, alors son suivant peut-tre 2 ou 1 ; on sait dj quil ne
nous intresse pas. Par contre, deux cases aprs 3, on peut avoir un 5 : on y va, 3 2 1 1 1 1 2 3 4 5 6 7 8 9.
Cest un 1, donc llment quon cherche, sil est prsent, ne peut-tre que 4 cases plus loin etc. etc.
Algo(T, i) = Algo(T, n)
Dans un tableau aussi particulier, on verra
i 1 ^ T[i] x
Algo(T, i |x T[i]|)
par la suite quon pourra utiliser la
i 1 ^ T[i] = x
vrai
technique de la dichotomie.
i<1
faux
Introduction lAlgorithmique, mai 2006

15

Philippe Giabbanelli

VII. Comment faire abstraction de la machine ?


La plupart du temps, il nest pas ncessaire de compter prcisment le nombre doprations car on entre
dans des considrations plus matrielles . Dans nos exemples prcdents, on a nglig la gestion de la
boucle. Pourtant, crire pour i de 1 n ncessite de rserver une case mmoire pour la variable i, et de
faire n incrmentations. Selon les langages, la variable i peut-tre place dans le registre pour un accs
rapide, et lincrmentation est une opration optimise ; autrement dit, le cot est assez variable
On cherche donc se dbarrasser le plus possible des caractristiques des machines : on veut un ordre de
grandeur sur la performance de lalgorithme. Les constantes ne nous intressent plus ; que ce soit 3n + 4
ou 20n, on est en n. Dans certains cas de la vie professionnelle, il est plus rentable dacheter assez de
machines pour utiliser rapidement lalgorithme qui cote 20n, plutt que de passer du temps vouloir
dvelopper un meilleur algorithme ; une quipe mobilise 6 mois cote davantage que 10 machines
Pour exprimer notre ordre dide, on va se donner quelques complexits de rfrence. De la meilleure
(algorithme le plus performant) la pire (algorithme difficilement exploitable), nous avons :
- Complexit constante :
f(n) = 1
- Complexit logarithmique : f(n) = log(n)
- Complexit linaire :
f(n) = n
f(n) = n.log(n)
- Complexit quadratique :
f(n) = n
- Complexit cubique :
f(n) = n3
- Complexit polynomiale : f(n) = np
- Complexit exponentielle : f(n) = 2n
On situe ensuite notre algorithme par rapport ces rfrences : est-il au pire en n ? Au mieux en n ?
Exactement en n ? Pour cela, on a introduit et dfini formellement les notations dites de Landau :
- Quand la complexit g(n) de lalgorithme est majore par f(n), on dit quil est en O(f(n)).
O(f(n)) = { g, n0, c>0, n>n0, g(n) c.f(n) }
Il existe un certain nombre partir duquel g(n) est toujours infrieur f(n)*constante.
- Quand la complexit g(n) de lalgorithme est minore par f(n), on dit quil est en (f(n)).
(f(n)) = { g, n0, c>0, n>n0, g(n) c.f(n) }
Il existe un certain nombre partir duquel g(n) est toujours suprieur f(n)*constante.
- Quand la complexit g(n) de lalgorithme est exactement en f(n), on dit quil est en (f(n)).
Sil est exactement en (f(n)), cela veut dire quil est born par deux multiples de f(n).
Autrement dit, il peut-tre minor et major par des multiples ; cest donc lintersection des
classes de complexit prcdentes : (f(n)) = O(f(n)) (f(n))

On parle dune complexit asymptotique : quand n devient trs grand, on peut dire que le
comportement de 3n + 4n + 2 est quivalent celui du terme en n. Autrement dit, pour un n trs grand,
le terme en n est le terme dominant. On peut alors dire que 3n + 4n + 2 est en (n). On a lhabitude de
noter 3n + 4n + 2 = (n), mais cest une sorte dabus. Il sagit davantage de lappartenance une classe
de fonctions, que lon devrait donc noter 3n + 4n + 2 (n) pour tre cohrent sur les objets manipuls.
Introduction lAlgorithmique, mai 2006

16

Philippe Giabbanelli

Exemple 1 : Choisir un algorithme


Nous avons lalgorithme F dont la complexit en temps est donn par la fonction f(n) = -4*n + 2*n + 3.
Nous avons lalgorithme G dont la complexit en temps est donn par la fonction g(n) = log(n) + 300*n.
On remarque que la fonction f est minore par n et majore par 3*n : elle est donc en (n).
De mme, la fonction g est minore par n et majore par 1000n, par exemple ; elle est donc en (n).
Pour une valeur de n trs grand, nous avons vu que la complexit n est pire que la complexit en n.
Donc, si nous avons une valeur de n trs grand, nous choisirons lalgorithme G, qui est linaire.
En revanche, pour des valeurs plus petites de n, il faut sintresser prcisment aux fonctions. Y a-t-il un
intervalle sur laquelle f est plus petite que g, ou lalgorithme G est-il toujours meilleur ? Comparons f et g.
On pose h(n) = f(n) g(n) = -4*n + 2*n2 + 3 - log(n) - 300*n. On rsout ceci, par exemple avec des
logiciels de calcul formel, et on obtient que h(n) est ngatif sur [0, 152] et positif partir de 153.
Autrement dit, sur [0, 152] il est plus coteux dutiliser lalgorithme G que lalgorithme F !
Si on est certain davoir des valeurs de n fortes, lalgorithme G est meilleur. Sinon, on procde une
tude, qui conclue que pour n dans [0, 152] alors on utilisera lalgorithme F, et sinon lalgorithme G.

Exemple 2 : Dmontrer rigoureusement lappartenance la classe O


On a dit que si g(n) est en O(f(n)), alors g(n) est majore par f(n) pour des valeurs assez fortes de n.
Ainsi, en regardant g(n) / f(n) en n = +, puisque f est nettement plus fort on doit trouver une constante.
Par exemple, on a g(n) = 3*n + 4n. Prouvons que cest bien en O(n). On peut utiliser un logiciel de
calcul formel ; en Maple : limit((3*n^2+4*n)/n^2), n=+infinity), et on a la constante 3. Sinon, on calcule :
3 $ n2 C 4 $ n
n2

w n /C N 3 $2n
n

= 3 , o a ~n+ b signifie que a est quivalent b lorsque n tend


vers + linfini. Les quivalences permettent de simplifier.

Exemple 3 : Evaluer vue la complexit dun algorithme


Si on sait suffisamment comment fonctionne un algorithme, on peut parfois en dduire facilement son
ordre de grandeur. Par exemple, lalgorithme du maximum parcours tous les lments et effectue
chaque fois un traitement lmentaire (si suprieur au maximum courant, alors je remplace). Comme il
y a exactement n lments parcourus, on en dduit que lalgorithme du maximum est en (n).
Lalgorithme pour dterminer si un lment est prsent ou non dans un ensemble sarrte ds quil a
trouv llment. Dans le pire des cas, llment ny est pas et on doit donc tout parcourir : on en dduit
que lalgorithme est en O(n). Dans le meilleur des cas, on rencontre directement llment, et on est donc
en (1). Notons que la complexit ne nous fourni quun ordre de grandeur, et lorsquon veut une analyse
plus fine, il faut tenter de compter le mieux possible les oprations fondamentales. Voyons une autre ide :
Cherche(, T, n)
= Cherche(x, T, 1) v Cherche(, T[1n], |T|)
T[n] = ^ n 1
Cherche(, T, n) = Vrai
T[n] = ^ n = 1
Cherche(, T, n) = Faux
T[n]
Cherche(, T, n) = Cherche(, T, n-1)
Lalgorithme de recherche de sarrte logiquement lorsquil a trouv . En temps normal, il faudrait
voir si n nest pas sortie du tableau. Pour viter ceci, on place la 1re case (la dernire visite) : on met
en sentinelle pour nous avertir quon sarrte ici. Puisque maintenant on est certain de sarrter, il est
inutile de tester les valeurs de n. On commence par regarder si est en 1, et sinon on fait le parcours.
Cet algorithme effectue donc deux fois moins de comparaison que lalgorithme de recherche classique,
grce la technique de la sentinelle. Pourtant, les deux auront exactement les mmes complexits
asymptotiques : O(n) et (1). Ces notations permettent denlever les constantes lies la machine et aux
langages, mais font aussi disparatre certaines optimisations intressantes pour lalgorithmique. Mfiance !
Introduction lAlgorithmique, mai 2006

17

Philippe Giabbanelli

VIII. Comment savoir si mon algorithme est le meilleur ?


Etant donn un problme, on veut connatre la complexit minimale ncessaire pour le rsoudre ;
autrement dit, la borne infrieure de complexit. Une fois cette borne connue, on pourra savoir si
lalgorithme dont nous disposons est passable, ou sil peut-tre vraiment amlior.
Par exemple, on trouve quun problme a besoin dau moins 2*n n oprations pour tre rsolu : si
notre algorithme est en (n), on est assez rassur ; sil est plutt en O(n3) ou plus, en revanche, on sait
quil doit pouvoir tre amlior de faon significative. Attention : mme si notre algorithme est dans la
classe de complexit de la borne infrieure (ici un O(n)), il peut rester quelques amliorations faire. Un
algorithme en 300n + 100 + 50 est certes en O(n), mais on aimerait pouvoir baisser ses constantes,
jusqu peut-tre arriver lalgorithme idal qui serait en 2*n n.
Cest en gnral assez difficile de connatre prcisment la borne infrieure dun problme. Pour avoir
une ide, on peut la calculer de faon asymptotique, cest--dire en lexprimant avec des classes de
complexit. Par exemple, pour trouver le maximum dun tableau, comme on est oblig de regarder tous
les lments, alors on est en (n). Comme lopration lmentaire est la comparaison, qui nest quune
constante, alors on sait que la borne infrieure pour le problme du maximum est (n).
De mme, si on veut trouver le deuxime plus grand lment dun tableau, on sait quon est en (n). En
effet, on doit regarder chaque lment donc dj (n), et on connat un algorithme linaire pour rsoudre
le problme : on cherche le maximum du tableau, on lenlve, on cherche nouveau le maximum, et cest
bien le deuxime plus grand lment. Par contre, si on veut exprimer prcisment la borne infrieure de
ce problme, cest--dire en donnant les constantes, alors il faudra tre beaucoup plus subtil

Exemple 1 : Prouver que lalgorithme du maximum est optimal


1. Max(T, n, i, max) = Max(T, |T|, 2, T[1]))
2. i n ^ T[i] > max Max(T, n, i, max) = Max(T, n, i+1, T[i])
3. i n ^ T[i] max Max(T, n, i, max) = Max(T, n, i+1, max)
4. i > n
Max(T, n, i, max) = max
On commence par calculer la complexit exacte de notre algorithme.
Lopration fondamentale est ici la comparaison : dterminons le nombre de comparaisons effectues. On
commence parcourir le tableau partir de la case 2 et on sarrte lorsquon dpasse la case n. Les lignes
2 et 3 sont donc examines n 2 + 1 = n 1 fois. Ces lignes contiennent chacune deux comparaisons, ce
qui nous fait un total de 2*(n 1) = 2n 2 comparaisons.
En pratique, on nglige trs souvent les comparaisons dindices, car cela porte sur de petits nombres et
lopration peut donc seffectuer rapidement. En revanche, les comparaisons entre contenus du tableau
sont nettement plus significatives ! Il serait donc courant de dclarer simplement n 1 comparaisons.
On calcule la borne infrieure.
Il y a n lments. On choisit comme maximum potentiel le premier lment. Tout lment doit avoir t
compar une fois au maximum, sinon on ne peut pas savoir sil est le maximum. Seul le maximum ne
sera pas compar. Ainsi, tout algorithme de recherche du maximum doit faire au moins n 1
comparaisons.
On compare la complexit de notre algorithme et la borne infrieure.
Or, notre algorithme effectue prcisment n 1 comparaisons. Il a atteint la borne infrieure, il est donc
optimal.

Introduction lAlgorithmique, mai 2006

18

Philippe Giabbanelli

Exemple 2 : Trouver la borne infrieure du tri par comparaison


Etant donn un tableau, comment trier ses lments en les comparant ? Regardons trois lments, a1a2a3 :
On commence par comparer les deux premiers
lments, a1 et a2 . Selon le rsultat, on va avoir
besoin de comparer dautres lments. On
arrive ainsi reprsenter le raisonnement par
un arbre, dit arbre de dcision .
Lexcution de lalgorithme de tri est donc un
simple chemin dans larbre. Lorsquon arrive
au bout, lalgorithme a dtermin lordre dans
lequel sont les lments, et effectue les
permutations : jchange a1 avec a2, ou On
voit que pour 3 lments, il existe 6 fins
possibles.
On veut savoir le nombre de fins possibles quil y a dans le cas gnral, autrement dit le nombre de
chemins ou encore de feuilles (les lments au bout) de larbre. Pour cela, on se donne n lments : le
nombre de combinaisons que peuvent former n lments est n! En effet, on a n possibilits de choisir le
premier lment, donc (n-1) de choisir le 2nd, etc., et le dernier est forc ; soit n*(n-1)*(n-2)**1 = n!.
Larbre de dcision a donc n! feuilles.
Quand on excute un algorithme de tri sur nos donnes, il va emprunter un des chemins possibles. Trouver
la plus grande longueur du chemin que peut prendre lalgorithme revient trouver la complexit ncessaire
pour rsoudre le pire des cas ; autrement dit, la complexit telle que tout cas puisse tre rsolu. Cette
complexit nest rien dautre que la borne infrieure du tri par comparaison !
Notons h la longueur maximale des chemins
dans larbre, aussi appele la hauteur. Pour une
hauteur de 1, on voit quon a au plus 2 feuilles.
Si on augmente la hauteur de 1, nos feuilles
peuvent avoir chacune au plus deux feuilles, ce
qui nous fait un maximum de 4 feuilles.
On montre par rcurrence que pour une hauteur
de h, le nombre maximum de feuilles est 2h.
Logiquement, le nombre de feuilles de notre arbre de dcision est infrieur au nombre maximal de
feuilles que peut avoir tout arbre. On a donc n! 2h.
Le reste est maintenant une histoire de mathmatiques de niveau terminale. Pour rcuprer la hauteur, qui
est actuellement en exposant du 2, on applique le logarithme base 2 :
n! 2h log2(n!) log2(2h) log2(n!) h
Or, log2(n!) nest pas trs parlant pour nous donner la complexit. On va donc transformer lexpression.
Pour cela, il faut connatre la formule de Stirling : n! (n/e)n * (2n). Pour mesurer la complexit, on
peut ngliger le terme constant. On obtient donc :
log2(n!) h log2[(n/e)n] h n.log2(n/e) h n.log2(n) n.log2(e) h n.log2(n) n h
car log2(ab) = b.log2(a)

car log2(a/b) = log2(a) log2(b)

car log2(ea) = a

On a obtenu n.log2(n) n h. Le plus long chemin que lalgorithme puisse prendre pour trier n lments
mesure n.log2(n) n. Ainsi, tout tableau peut ncessiter jusqu n.log2(n) n pour tre tri en comparant.
On vient donc de prouver que la borne infrieure dun tri par comparaison est n.log2(n) n.
Introduction lAlgorithmique, mai 2006

19

Philippe Giabbanelli

IX. Entranement
Les exercices suivants sont nots sous la forme a.b), o a dsigne la partie du chapitre concerne et b
un numro propre lexercice. Par exemple, 1.3) dsigne le 3me exercice sur la partie 1 Quest-ce
quun algorithme ? . On trouvera la correction la fin du livre, page XXX.
Quand on veut un algorithme, on fait un exemple, puis on abstrait et en dernier on lcrit formellement.
1.1)

On a dit quun algorithme doit fournir une rponse aprs un nombre fini dtape. Expliquez
pourquoi les deux algorithmes sur le nombre doccurrence sarrteront ncessairement.

1.2)

Lalgorithme dEuclide qui calcule le plus grand diviseur commun de deux nombres m et n,
entiers positifs, est donn de la faon suivante dans le livre de Donald Knuth :
E1. Divisons m par n et appelons n le reste.
E2. Si le reste est nul, alors lalgorithme termine et la rponse est n.
E3. m prend la valeur n, n prend la valeur r, et on retourne ltape E1.
Traduisez en algorithme abstrait. Que se passerait-il si on enlevait la contrainte m et n sont deux
entiers positifs ?

1.3)

On dispose dun tableau T dentiers strictement positifs, et on veut savoir quel est le plus grand
cart entre deux lments du tableau.

1.4)

On dispose dun tableau T, contenant n nombres rels. Votre algorithme donnera un rel r qui
nappartient pas au tableau. Par exemple, T = [-2, 12.5, -8.8, -5, 1.5], et une des rponses valides
est 4.5.

2.1)

En sinspirant du raisonnement pour le nombre de rgions


dtermines par n frontires rectilignes , donnez le nombre de
rgions dtermines par n cercles. Les cercles sont en position
gnrale : deux cercles ne sont pas tangents ni entirement
superposs.
On a R(1) = 2, R(2) = 4, R(3) = 8 et R(4) = 14.
Aidez vous en faisant les dessins pour les cas 3 et 4 cercles.

4.1)

On dispose dun tableau dont on ne connat pas la taille. On sait quil se termine ncessairement
par le caractre \0. Calculez sa taille des deux faons : rcursive enveloppe et rcursive terminale.
Ecrivez un quivalent de la version rcursive terminale en style impratif (tant que, pour).

6.1)

Donnez la complexit (cot en temps et en espace) des algorithmes 1.3), 1.4) et 4.1).

6.2)

On dispose dune matrice M, qui est un tableau deux dimensions, de largeur l et de hauteur h.
Votre premier algorithme donnera la somme des lments de la matrice.
Votre second algorithme donnera la somme de la diagonale de la matrice (appele la Trace de M).
Vous donnerez la complexit des deux algorithmes. Comme la manipulation des doubles boucles
nest pas trs pratique dans notre formalisation, vous pouvez commencer par un style impratif.

7.1)

Montrer que g(n) = [n*log(n) + (log n)4 + n]/(n+1) appartient la classe (log n).

8.1)

On dispose dun tableau T dentiers. Le tableau comprend tous les entiers de 0 n sauf un. Votre
algorithme donnera llment qui manque.
Par exemple, le tableau est 0 3 4 5 6 9 7 8 1, et llment 2 est manquant.
Expliquez pourquoi votre algorithme est optimal.

Introduction lAlgorithmique, mai 2006

20

Philippe Giabbanelli

Chapitre 2

Le problme des structures de donnes


I.

Prsentation formelle du problme

Le rle majeur des programmes consiste prendre des donnes et appliquer des algorithmes dessus. On
va effectuer trs souvent des oprations basiques dans ces donnes : chercher, ajouter, supprimer
Par exemple, dans un jeu en rseau, on dispose dun ensemble de joueurs. On voudra souvent chercher un
joueur, peut-tre plus rarement en ajouter ou en supprimer. Ainsi, il faut organiser les donnes de faon
ce que les oprations qui nous intressent soient effectues le plus rapidement possible. Dans ce cas, on
pourrait utiliser une structure, i.e. une organisation des donnes, nomme dictionnaire.
Supposons maintenant que nous avons un systme dexploitation, qui gre des tches avec des priorits.
On ajoute des tches, et on supprime celle avec la priorit maximale. La structure conseille est le tas.
Formellement, nous avons un univers U dans lequel vivent les donnes que nous manipulons. Ces
donnes elles-mmes sont un sous-ensemble S de cet univers. On souhaite appliquer un ensemble Op
doprations sur les donnes. Une structure de donne est donc formalise par <U, S, Op>.
Soit d une donne. Les oprations les plus courantes sont les suivantes :
- Member(d, S) : dS ? Ceci se lit Est-ce que d appartient S ? . Cest la recherche.
- Insert(d, S) : S S d. On ajoute la donne d au sous-ensemble S des donnes.
- Delete(d, S) : S S d. On supprime la donne d du sous-ensemble S des donnes.
- DeleteMax(d, S) : S S max(S). On supprime le maximum (exemple Systme dExploitation).
- Max(S), Min(S) : le maximum et le minimum.
- Suc(d, S), Pred(d, S) : le successeur (lment immdiatement plus grand que la donne) et le
prdcesseur (lment immdiatement infrieure la donne). Par exemple, dans 10 5 3 24 7 4, le
successeur de 7 est 10 et son prdcesseur est 5.
Le choix de la structure de donnes dpend des oprations demandes. Les structures fondamentales sont :
Nom de la structure
Points forts
Pile
Insertion, suppression du dernier lment entr.
File
Insertion, suppression du premier lment entr.
Dictionnaire
Insertion, suppression, recherche
File de priorit
Insertion, suppression de llment maximum
Tableaux ou listes chanes
Si nous ne sommes dans aucun des cas prcdents et quon veut
optimiser une seule opration.
Arbres
Si nous ne sommes dans aucun des cas prcdents et quon veut
optimiser plusieurs oprations.
On trouve rarement une structure de donnes qui soit parfaite pour tout ce quon veut faire. Cest donc
une affaire de compromis. Il faut par exemple choisir entre un programme qui tourne en moyenne le plus
rapidement possible, ou donner la priorit certaines oprations.
Enfin, il y a des questions intressantes se poser sur lunivers U et son sous-ensemble S de donnes :
- La taille. Si U ou S sont assez petits, on peut faire de nombreuses oprations en temps constant.
- La nature des donnes. Lunivers est-il ordonn ? S est-il un ensemble (i.e. un lment napparat
au maximum quune fois), ou un multi-ensemble (un lment peut apparatre plusieurs fois) ?
Exemple : Si un lment ne peut tre l quune fois, vrifier quil ny soit pas dj avant dajouter !
Introduction lAlgorithmique, mai 2006

21

Philippe Giabbanelli

II.

Un pointeur du point de vue physique

Les structures de donnes se construisent avec les tableaux et les pointeurs. Nous avons manipul des
tableaux, ou en avons au moins une ide intuitive. Nous allons donc prsenter les pointeurs, travers
lexemple des listes. Dans ces deux pages, nous tenons montrer au lecteur ce qui se passe physiquement ;
on dlaisse un instant lalgorithmique pour une pense plus Objet, avec des exemples de code en Java.
Une liste est un chanage : les donnes sont des maillons, et se connectent les unes aux autres.
donne

donne
donne
rien

Si partir dune donne on ne peut accder qu la donne suivante, alors cest un chanage simple.
Si on peut accder la suivante et la prcdente, alors on parle de chanage double, comme ci-dessous.
rien

donne

donne

donne

donne

donne

donne

rien

Les connections entre les donnes sont appeles des pointeurs : on pointe sur un autre objet.

Comment est faite une liste par chanage simple ?


La liste est forme de maillons. Pour dfinir la liste, il faut donc commencer par dire ce quest un maillon :
Un maillon est dfini par :
public class Maillon{
Une donne
public Object donnee ;
Un lien vers le maillon suivant
public Maillon suivant ;
public Maillon(Object o) {
donnee = o ;
suivant = null ;
}

Pour crer un maillon, on prend une donne


On stocke la donne
Par dfaut, il ny a pas de maillon suivant
}

Pour avoir une liste, il suffit davoir un lien sur le premier maillon, que lon nommera la tte . A partir
du premier, on pourra accder et manipuler tout le reste. Une liste est donc un pointeur sur le 1er maillon.
public class LinkedList{
public Maillon tete ;

Une liste par chanage simple est dfinie par :


Le premier maillon
Pour crer une liste, pas besoin de paramtres
On initialise le premier maillon rien

public LinkedList(){
tete = null ;
}

La notion de rien est trs utile. Au dpart, la liste est vide, elle contient donc rien. De mme, lorsquon
cre un maillon, au dpart il pointe sur rien. Les questions se formulent alors de faon trs logique :
La liste est-elle vide ?

Est-ce que la liste contient rien ?


On peut crire une premire version de lalgorithme pour tester si la liste est vide :
Vide?(L)
public boolean vide(LinkedList L){
L = Vide?(L) = vrai
if(L..tete == null) return true;
L Vide?(L) = faux
if(L..tete != null) return false ;
}
Il est cependant plus cours de renvoyer seulement le rsultat du test.
public boolean vide(LinkedList L){ return L . tete == null;}

Vide?(L) = L =
Introduction lAlgorithmique, mai 2006

22

Philippe Giabbanelli

Comment ajouter un lment ?


Pour ajouter un maillon, il suffit de dire que cest notre nouvelle tte, et dy rattacher la liste.
liste

tete

liste

maillon

tete

maillon

En gnral, on ajoute des donnes plutt que des maillons. Il faudra mettre la donne dans un maillon.
Ajouter(L, d)
public void ajouter(LinkedList L, Object o){
Soit M le maillon contenant d
Maillon M = new Maillon(o) ;
Le suivant de M est la tte de L
M . suivant = L..tete ;
La nouvelle tte de L est M
L . tete = M ;
}
Il est trs important de commencer par lier le maillon lancienne tte de liste. En effet, si on liait
directement la liste au maillon, alors on aurait perdu tout lien sur la tte, et donc on perdrait la liste !

Comment supprimer un lment ?


Pour supprimer un lment dune liste, il suffit que plus aucun maillon ne pointe dessus. Autrement dit,
on dcroche le maillon, on relie les autres, et on loublie. En Java, le Garbage Collector fera le mnage.
1
2
3
1
3
2
A supprimer
Ainsi, aprs 1 on a 3 comme prvu. On ne pourra plus accder au 2 en parcourant la liste, ce qui revient
donc bien le supprimer. Le principe est donc de trouver le nud dcrocher, et de relier son prcdent
avec son suivant. On voit facilement quil y a un cas particulier : la tte na pas de prcdent, ainsi sil
sagit de llment supprimer alors il faudra lavancer.
public void ajouter(LinkedList L, Object o){
if(L.tete.donnee == o){
L . tete = L . tete . suivant ; return ;}
Maillon M = L . tete ;
while(M . suivant != null){
if(M . suivant . donnee == o){
M . suivant = M . suivant . suivant ;
return ;
}
M = M . suivant ;
}
System . err . println( Element introuvable. ) ;
}

Supprimer(L, d)
Si cest la tte,
alors je lavance.
Sinon, je me positionne sur la tte
et javance tant que je ne suis pas au bout.
Si je trouve llment supprimer,
alors je le saute.

Si jai tout parcouru et rien trouv, alors


l'utilisateur voulait supprimer un lment
qui nexiste pas.

Parcours gnral dune liste


Dans des langages similaires Java (C, C++, ), le parcours dune liste se fait avec une boucle. On a
utilis ici une syntaxe, il peut y en avoir une autre. Les deux sont bien entendus strictement quivalentes :
Maillon m = L..tete ;
for(Maillon m = L..tete ; m.suivant != null ; m = m.suivant){
while(m.suivant != null){
<actions propres lalgorithme>
<actions propres lalgorithme>
}
m = m.suivant ;
}
Introduction lAlgorithmique, mai 2006

23

Philippe Giabbanelli

III. Un pointeur du point de vue algorithmique


Lalgorithmique se concentre sur la rflexion et la complexit des solutions trouves. On ne sintresse
pas explicitement tout ce qui relve de la manipulation de la mmoire, autrement dit des pointeurs : ce
sont des choses de trop bas niveau, trop proche de la machine. Pour concevoir un algorithme, on ne veut
pas sencombrer de ce genre de dtails, il nous faut un haut niveau dabstraction. On va donc simplement
noter par un . la liaison entre deux lments, et par la liste vide. Ainsi, on a lquivalence suivante :
physique

a.b.c.

rien

algorithmique

Il ny a rien de plus facile que dajouter un lment une liste L. Regardons lquivalence :
b

rien

b.c.

a.b.c.

rien

La notion de maillons est bonne pour une reprsentation objet, mais ne nous importe pas en algorithmique.
Il ne nous reste que des lments, un . de liaison et quand cest vide. Rajouter un lment la liste,
cest simplement le lier avec cette liste :
Ajouter(, L) = .L
A partir de qui reprsente la liste vide et de cette fonction nous permettant dajouter, on peut crer
nimporte quelle liste. La liste a. est obtenue avec ajouter(a, ). Ainsi, pour obtenir la liste a.b.c., on a :
Soit L Ajouter(a, Ajouter(b, Ajouter(c, )))
On a une base mathmatique solide : lalgorithmique procde par composition de fonctions. Notre faon
de faire nest pas purement thorique, elle peut tout fait se coder, pratiquement telle quelle. Pour cela,
au lieu dutiliser les langages du type C, C++, et Java, trs connus du grand public, on utilise des langages
de programmation fonctionnelle comme Lisp, Scheme et Haskell. Lannexe 1 la fin du livre, page
XXX, dfini une srie de macros Scheme permettant dcrire nos algorithmes dune faon trs proche.
Le Scheme est prfix, la diffrence des langages habituels : on crit (<op> a b) au lieu de (a <op> b).
Ainsi, si on veut crire (5 + 4) en Scheme, on fera (+ 5 4). Pour montrer au lecteur que notre formalisme
se code dans la pratique, on donnera lquivalent entre nos algorithmes et le code Scheme1. Commenons :
Ajouter(, L) = .L
(algorithme ajouter(a L) = (lier a L))
Scheme1

Soit L Ajouter(a, Ajouter(b, Ajouter(c, )))


(Soit L <- (ajouter 'a (ajouter 'b (ajouter 'c vide))))
Faisons un exemple dalgorithme : on veut crer la liste des entiers de n 1. La stratgie est donc de lier n
n 1, etc., jusqu arriver 0 o on fermera la liste avec . Ce qui scrit :
Construire(n)
(algorithme construire(n)
n = 0 Construire(n) =
((= n 0) -> vide)
n 0 Construire(n) = n.Construire(n-1)
((!= n 0) -> (lier n (construire (- n 1)))))
1

Ce quon crit nest pas exactement du langage Scheme : le but de ce livre nest pas dapprendre le langage Scheme ou un
quelconque autre langage. Simplement, ce quon crit se transforme en code du langage Scheme par une srie de macros qui
sont dfinies dans lannexe 1 la fin du livre, page XXX. Des macros sont des substitutions textuelles : notre code, vu
en tant que texte, sera transform sans que le lecteur ait quelque manipulation que ce soit effectuer ; cest transparent.
Par exemple, le lecteur marque (algorithm ajouter(a L) = (lier a L)), et cest transform (expans) en langage Scheme :
(algorithm ajouter(a L) = (lier a L))
(define (ajouter a L)
(cons a L)
Bien entendu, pour que tout ceci fonctionne, le lecteur devra prendre la peine de recopier les macros de notre annexe au
dbut de son programme. Il peut les mettre dans un fichier part, et demander les charger en dbut de programme avec
(require nom_du_fichier.ss ) si le fichier est dans le mme rpertoire. On recommande PLT Scheme pour commencer.

Introduction lAlgorithmique, mai 2006

24

Philippe Giabbanelli

Si on a du mal voir ce qui se passe, il faut toujours drouler la rcursivit, cest--dire suivre le
fonctionnement de lalgorithme sur un petit exemple. Demandons construire la liste des entiers de 4 1 :
Construire(4) = 4.Construire(3) = 4.3.Construire(2) = 4.3.2.Construire(1) = 4.3.2.1.Construire(0) = 4.3.2.1.
Passons maintenant la suppression dun lment dans un ensemble reprsent sous forme de liste ;
autrement dit, on veut supprimer llment d de la liste L, o un lment est prsent au plus une fois. On
aborde lalgorithmique des listes. Dans la plupart des cas, on peut adopter la dmarche rcursive suivante :
- Cas de base : jai une liste vide.
- Si ma liste nest pas vide, comment traiter llment courant et passer la suite ?
Cest une dmarche assez simple en cela quelle est naturelle. Regardons ce qui se passe pour Supprimer :
- Jai une liste vide. Je nai donc rien supprimer dedans, do Supprimer() = .
- Ma liste nest pas vide. Regardons llment courant.
Si cest llment que je veux supprimer, alors il suffit de loublier et de renvoyer la suite.
c = d Supprimer(c.L, d) = L
(o c est le 1er lment de la liste et L la suite)
Si ce nest pas llment que je veux supprimer, je continue de traiter la suite.
c d Supprimer(c.L, d) = c.Supprimer(L, d)
Faisons le bilan de notre algorithme qui supprime llment d de lensemble reprsent par la liste L :
1. Supprimer(L, d)
2.
L = Supprimer(L, d) =
3.
c = d Supprimer(c.L, d) = L
4.
c d Supprimer(c.L, d) = c.Supprimer(L, d)

Suppression de d dans un ensemble


liste vide : d ny est pas, cest valide.
si jai trouv d, jai fini le travail.
sinon, je continue de chercher

A nouveau, faisons un exemple qui nous permette de drouler cette rcurrence pour bien voir.
Supprimons llment 7 de la liste 3.4.7.9.3.6.. Notons que la liste reprsente bien un ensemble car
chaque lment napparat au plus quune fois ; les donnes de lexemple sont conformes aux contraintes.
Supprimer(3.4.7.9.3.6., 7) = 3.Supprimer(4.7.9.3.6., 7) = 3.4.Supprimer(7.9.3.6., 7) = 3.4.9.3.6.
Si on a un multi-ensemble au lieu dun ensemble, lalgorithme change. En effet, jusqu l on sest dit
on a un ensemble, llment ne peut-tre prsent au plus quune fois, donc quand je le trouve jai fini .
Or, dans le cas du multi-ensemble, llment peut-tre prsent plusieurs fois : pour dclarer quon a fini le
traitement, il faudra avoir trait toutes les donnes ; autrement dit, la ligne 3 doit poursuivre le traitement :
1. Supprimer(L, d)
2.
L = Supprimer(L, d) =
3.
c = d Supprimer(c.L, d) = Supprimer(L, d)
4.
c d Supprimer(c.L, d) = c.Supprimer(L, d)

Suppression de d en multi-ensemble
je continue le traitement, pas fini !

Il faut toujours penser ce que la structure doit reprsenter, en termes mathmatiques. La question la plus
fondamentale est de savoir si on a un ensemble ou un multi-ensemble. Aprs, on pourra peut-tre affiner
sur les valeurs que peuvent prendre les lments, lorganisation, les rapports entre eux On a vu par
exemple en page 15 quil y avait dj des optimisations possibles pour rechercher un lment dans un
tableau si deux donnes successives ont une diffrence de +- 1.
Cependant, on tire en gnral davantage parti des rapports entre les donnes en sappuyant sur un tableau
plutt que sur une liste. En effet, dans un tableau on a un accs en temps constant : si on veut aller une
case, on sadresse la zone concerne, et on y est ; tandis que dans une liste, on doit passer par tous les
lments prcdents, ce qui nous donne un accs en temps linaire.
Introduction lAlgorithmique, mai 2006

25

Philippe Giabbanelli

You might also like