Académique Documents
Professionnel Documents
Culture Documents
de
l'apprenti
programmeur
Par bluestorm ,
Cygal
et lastsseldon
www.openclassrooms.com
Licence Creative Commons 7 2.0
Dernire mise jour le 10/08/2011
Sommaire
Sommaire ................................................................................................
........................................... 2
Partager ..................................................................................................
............................................ 2
Algorithmique pour l'apprenti
programmeur ....................................................................................... 4
But du
tutoriel ...........................................................................................................................
......................................... 4
Prrequis ........................................................................................................................
................................................... 4
Historique ......................................................................................................................
.................................................... 4
La notion de
complexit .....................................................................................................................
.............................. 11
Correction de
l'algorithme ......................................................................................................................................................
.................................... 11
Complexit .......................................................................................................................................................
.......................................................... 11
Mesure
'asymptotique' .................................................................................................................................................
.............................................. 12
Notation "grand
O" .....................................................................................................................................................................
............................... 12
Complexit en temps, complexit
mmoire ..........................................................................................................................................................
.... 13
Complexit dans le pire des
cas ...................................................................................................................................................................
............ 13
Un peu de
pratique .........................................................................................................................
................................. 15
Qu'est-ce qu'on attend de
vous ? ..............................................................................................................................................................
................ 15
Chercher le plus grand / petit
lment ...........................................................................................................................................................
........... 15
Trouver les lments
uniques ............................................................................................................................................................
....................... 16
Solution
propose ..........................................................................................................................................................
........................................... 16
Complexit .......................................................................................................................................................
......................................................... 17
Trouver les lments uniques : autre
solution ...........................................................................................................................................................
18
Listes ...............................................................................................................................................................
.......................................................... 21
Ajout / retrait, taille, accs un
lment ...........................................................................................................................................................
........ 22
Ajout /
Retrait ..............................................................................................................................................................
............................................... 22
Taille .................................................................................................................................................................
......................................................... 24
Accs un
lment ...........................................................................................................................................................
........................................ 24
Concatnation,
filtrage .............................................................................................................................................................
................................. 25
Concatnation ..................................................................................................................................................
......................................................... 25
Filtrage .............................................................................................................................................................
......................................................... 27
Synthse ..........................................................................................................................................................
......................................................... 28
Oprations .......................................................................................................................................................
.......................................................... 28
Conversions .....................................................................................................................................................
......................................................... 28
Attention aux langages de
moches ............................................................................................................................................................
.............. 29
Introduction au problme du
tri ...................................................................................................................................
.... 37
Formuler le problme du
tri .....................................................................................................................................................................
.................. 37
Question de la structure de
donne .............................................................................................................................................................
............. 37
Tri par
slection ..........................................................................................................................................................
............................................... 37
Complexit .......................................................................................................................................................
......................................................... 38
Implmentation du tri par
slection ..........................................................................................................................................................
................. 39
Pour une
liste ..................................................................................................................................................................
.......................................... 39
Pour un
tableau .............................................................................................................................................................
............................................ 40
Comparaison ....................................................................................................................................................
......................................................... 42
Tri par
insertion ...........................................................................................................................................................
.............................................. 42
Le retour du "diviser pour rgner" : Tri
fusion ............................................................................................................................................................
42
Algorithme .......................................................................................................................................................
.......................................................... 43
Implmentation avec des
listes ................................................................................................................................................................
................. 44
Implmentation avec des
tableaux ...........................................................................................................................................................
................. 48
Complexit .......................................................................................................................................................
......................................................... 49
Efficacit en
pratique ...........................................................................................................................................................
...................................... 50
Piles et
files ................................................................................................................................
..................................... 53
Concept ...........................................................................................................................................................
.......................................................... 53
2/70
Mise en
pratique ...........................................................................................................................................................
............................................. 54
Piles .................................................................................................................................................................
......................................................... 54
Files ..................................................................................................................................................................
......................................................... 55
Arbres ............................................................................................................................
.................................................. 58
Dfinition .........................................................................................................................................................
.......................................................... 59
Quelques algorithmes sur les
arbres ..............................................................................................................................................................
.......... 62
Taille .................................................................................................................................................................
......................................................... 62
Hauteur ............................................................................................................................................................
.......................................................... 64
Liste des
lments ..........................................................................................................................................................
.......................................... 64
Parcours en
profondeur .......................................................................................................................................................
...................................... 65
Parcours en
largeur .............................................................................................................................................................
...................................... 66
En mettant des
couches ............................................................................................................................................................
................................ 66
Avec une
file ....................................................................................................................................................................
.......................................... 67
Sommaire 3/70
But du tutoriel
Les deux notions cls de ce tutoriel sont les suivantes : la complexit, et les
structures de donnes. La complexit est une manire
d'estimer les performances d'un algorithme. Les structures de donnes sont la
manire dont vous organisez les informations dans
votre programme. En choisissant une structure de donnes adapte, vous serez
capables de coder des oprations trs
performantes (de faible complexit).
Chaque algorithme rsout un problme donn. Pour chaque problme, il existe un ou
plusieurs algorithmes intressants (mais on
en dcouvre de nouveaux tous les ans !). Nous vous prsenterons, dans ce tutoriel,
un petit panorama de problmes "courants",
dans le but de vous familiariser avec la complexit et les structures de donnes. Vous
apprendrez par exemple chercher un
lment qui vous intresse l'intrieur d'un ensemble d'lments, trier un
ensemble, ou mme trouver le plus court chemin
d'un "endroit" un autre.
Prrequis
Le but de ce tutoriel n'est pas de vous apprendre programmer. Pour le lire, vous
devez dj savoir programmer. L'apprentissage
de l'algorithmique n'utilise pas de concepts bas niveau (assembleur, etc.) ou de
bibliothques logicielles spcialises (SDL, Qt...),
mais vous devez tre l'aise avec les variables, conditions, boucles et fonctions. La
connaissance du concept de 'rcursivit' (si
vous vous sentez en manque, il y a dj un tuto ce sujet sur le SDZ) est aussi un
avantage.
Le langage que vous utilisez n'est pas trs important, car on tentera de formuler les
algorithmes d'une manire qui en est
indpendante. Nous donnerons aussi, pour les curieux, des exemples dans quelques
langages de programmation. Si vous n'y
voyez pas le vtre, trouvez-en un suffisamment proche, et faites un petit effort.
La complexit algorithmique est une mesure formelle de la complexit d'un
algorithme. Elle s'exprime donc en langage
mathmatique. Le calcul de certains algorithmes avancs est trs compliqu et
demande des connaissances mathmatiques
pousses. Cependant, notre tutoriel se concentre sur des choses simples, et devrait
tre largement accessible : une connaissance
des puissances et des racines (carres) devrait suffire tre l'aise. Un objet plus
avanc, la fonction logarithme, sera prsent
et expliqu avant son utilisation.
Historique
Ce tutoriel est en cours d'criture. Vous l'avez dj lu, et vous voulez savoir si
quelque chose a t rajout ?
Voici un historique des modifications, les plus rcentes en premier :
08 aot 2011 : correction d'une bvue repre par Arnolddu51, et clarification par
Cygal de la recherche de racine par
Sommaire 4/70
dichotomie, suite une question de bouboudu21
15 juin 2010 : rvision de l'implmentation C du tri par fusion sur les listes
13 juin 2010 : diverses reformulations suite aux commentaires des lecteurs (candide,
Equinoxe, programLyrique)
12 juin 2010 : implmentation en C du tri par slection sur les tableaux
juillet 2009 : correction de quelques typos, clarification de certains passages
26 avril 2009 : ajout d'exemples de code pour le chapitre sur les arbres
25 avril 2009 : ajout d'icnes pour les chapitres existants
22 avril 2009 (partie 3) ajout du deuxime chapitre : arbres; les exemples de code
sont venir
20 avril 2009 (partie 3) ajout d'un premier chapitre, assez simple, sur les piles et les
files
27 fvrier 2009 (partie 1) reformulation et clarification de certains paragraphes
22 fvrier 2009 : ajout de l'historique, prsentation d'un site d'exercices en fin de
deuxime partie
18 fvrier 2009 (partie 2) ajout d'exemples de code C pour les listes chanes
11 fvrier 2009 (partie 2) chapitre "Introduction au problme du tri"
janvier 2009 : zcorrection par ptipilou (rdaction arrte cause d'un bug du SdZ)
mi octobre 2008 (partie 2) chapitre "Notions de structures de donnes : tableaux et
listes chanes"
dbut septembre 2008 (partie 2) chapitre "Une classe d'algorithme non nafs : diviser
pour rgner", par lasts et Cygal
mi aot 2008 (partie 1) publication de la premire partie
Un exemple de problme qui nous concerne tous (oui, mme vous) est celui de la
cuisine : vous tes dans une cuisine, vous
trouvez du riz, comment le cuire ? Voici une marche suivre simple :
remplir une casserole d'eau ;
y ajouter une pince de sel ;
la mettre sur le feu ;
attendre l'bullition de l'eau ;
mettre le riz dans la casserole ;
le laisser cuire 10 15 minutes ;
goutter le riz.
J'ai dcrit une solution au problme "il faut faire cuire du riz", sous forme de concepts
simples. Vous remarquerez qu'il y a
pourtant beaucoup de choses implicites : j'ai prcis que vous tiez au dpart en
possession du riz, mais il faut aussi une
casserole, de l'eau, etc. On peut se trouver dans des situations spcifiques o tous
ces objets ne sont pas disponibles, et il
faudra alors utiliser un autre algorithme (ou commencer par construire une
casserole...).
Les instructions que j'ai utilises sont "prcises", mais on pourrait prciser moins de
choses, ou plus. Comment fait-on pour
remplir une casserole d'eau, plus prcisment ? Si le cuisinier qui la recette est
destine ne sait pas interprter la ligne "remplir
une casserole d'eau", il faudra l'expliquer en termes plus simples (en expliquant
comment utiliser le robinet, par exemple).
De mme, quand vous programmez, le degr de prcision que vous utilisez dpend
de nombreux paramtres : le langage que
vous utilisez, les bibliothques que vous avez disposition, etc.
Si on trouve des algorithmes dans la vie de tous les jours, pourquoi en parle-t-on
principalement en informatique ? La raison est
trs simple : les ordinateurs sont trs pratiques pour effectuer des tches rptitives.
Ils sont rapides, efficaces, et ne se lassent
pas.
On peut dcrire un algorithme permettant de calculer les dcimales de la racine
carre de deux, qui soit utilisable par un humain.
Vous pourrez ainsi calculer, l'aide d'une feuille et d'un crayon, les 10 premires
dcimales (1,4142135624). Mais s'il vous en faut
un million ? Un ordinateur deviendra alors beaucoup plus adapt.
De manire gnrale, on peut concevoir de nombreux algorithmes comme des
mthodes de traitement d'information : recherche,
comparaison, analyse, classement, extraction, les ordinateurs sont souvent trs utiles
pour tripoter la masse d'informations qui
nous entoure continuellement.
Vous aurez peut-tre pens au clbre moteur de recherche Google (qui a
initialement domin le march grce aux capacits de
son algorithme de recherche), mais ce genre d'activits n'est pas restreint au (vaste)
secteur d'Internet : quand vous jouez un
jeu de stratgie en temps rel, et que vous ordonnez une unit de se dplacer,
l'ordinateur a en sa possession plusieurs
informations (la structure de la carte, le point de dpart, le point d'arrive) et il doit
produire une nouvelle information : l'itinraire
que doit suivre l'unit.
Situation
Haskell est un gentil fermier, qui lve des grenouilles. Il a des relations trs
chaleureuses avec celles-ci, qui peuvent se
promener leur guise dans leur champ pendant toute l'anne, et ont droit une
petite cahute avec chauffage pendant l'hiver.
Mais ce qui fait vritablement la joie des grenouilles de Haskell, ce sont les
vacances : tous les ans, juste avant de s'occuper des
moissons, il les amne dans les marcages prs de sa ferme pour qu'elles puissent y
passer des vacances aquatiques.
Le dtail compliqu est le dpart en vacances. Haskell charge toutes ses grenouilles
dans une grande caisse, qu'il met sur son
camion, et part vers les nombreux marcages pour les y dposer. Le problme vient
du fait que les grenouilles, surtout pendant
les vacances, n'apprcient pas la foule : elles veulent aller dans un des marcages
les moins peupls.
Plus prcisment, la caisse est un carr en forme de quadrillage de N cases de large
par N cases de long (N est un nombre
inconnu de nous, mais fix). Chaque case de la caisse contient une grenouille. Il y
aussi N marcages vides, dans lesquels les
Comparaison
Dans les deux cas, les conditions de dpart sont respectes : les grenouilles sont
rparties de faon ce qu'aucun marcage ne
soit plus peupl que les autres, et sont donc satisfaites. Les deux mthodes de
Haskell le fermier rsolvent bien le problme
correctement.
La diffrence vient du fait que dans la premire mthode, chaque grenouille ou
presque lui demandait de changer de marcage. Il
faisait donc environ autant de voyages en camion qu'il y a de grenouilles. Dans le
deuxime cas, il dposait les grenouilles par
blocs d'une range, et donc faisait moins de voyages : seulement le nombre de
ranges de grenouilles.
La diffrence n'a pas l'air importante, mais cela dpend beaucoup du nombres de
ranges. Pour N ranges, comme la caisse est
carre, il y a N grenouilles par range, soit au total N*N, ou N 2 grenouilles. La
mthode habituelle demande environ N2 voyages
Haskell le fermier, alors que la deuxime mthode n'en demande que N.
La deuxime mthode est plus rapide, et surtout le temps gagn en l'appliquant
augmente plus il y a de grenouilles. S'il y a 6
La notion de complexit
Quand un programmeur a besoin de rsoudre un problme informatique, il crit
(gnralement) un programme pour cela. Son
programme contient une implmentation, c'est--dire si on veut une "transcription
dans un langage informatique" d'un
algorithme : l'algorithme, c'est juste une description des tapes effectuer pour
rsoudre le problme, a ne dpend pas du
langage ou de l'environnement du programmeur ; de mme, si on traduit une recette
de cuisine dans une autre langue, a reste la
"mme" recette.
Correction de l'algorithme
Que fait, ou que doit faire un programmeur qui implmente un algorithme ? Comme
Haskell le fermier, il doit commencer par
vrifier que son algorithme est correct, c'est--dire qu'il produit bien le rsultat
attendu, qu'il rsout bien le problme demand.
C'est trs important (si l'algorithme ne fait pas ce qu'on veut, on n'a pas besoin de
chercher l'optimiser), et c'est parfois l'tape la
plus complique.
Dans la pratique, la plupart des informaticiens "font confiance" leurs algorithmes :
avec un peu d'habitude et pour des
problmes abordables, un programmeur expriment peut se convaincre qu'un
algorithme fonctionne correctement, ou au
contraire trouver un problme s'il y en a ("et que se passe-t-il si tu as un nombre
impair de grenouilles ?"). L'approche plus
'srieuse' consiste crire une preuve que l'algorithme est correct. Il y a diffrents
niveaux de preuves, mais ils sont tous un peu
trop formels pour ce tutoriel, et nous n'aborderons sans doute pas (ou alors trs
brivement) cet aspect.
Bien sr, un algorithme correct ne veut pas dire un programme sans bug : une fois
qu'on a un algorithme correct, on l'implmente
(en crivant un programme qui l'effectue), et on est alors expos toutes les petites
erreurs, en partie spcifiques au langage de
programmation utilis, qui peuvent s'incruster pendant l'criture du programme. Par
exemple, l'algorithme ne dcrit pas en gnral
comment grer la mmoire du programme, et la vrification des erreurs de
segmentations et autres rjouissances de la sorte est
laisse aux soins du programmeur.
Complexit
Une fois que le programmeur est convaincu que son algorithme est correct, il va
essayer d'en valuer l'efficacit. Il veut savoir
par exemple, "est-ce que cet algorithme va vite ?".
On pourrait penser que la meilleure faon de savoir ce genre de choses est
d'implmenter l'algorithme et de le tester sur son
ordinateur. Curieusement, ce n'est gnralement pas le cas. Par exemple, si deux
programmeurs implmentent deux algorithmes
diffrents et mesurent leur rapidit chacun sur son ordinateur, celui qui a l'ordinateur
le plus puissant risque de penser qu'il a
l'algorithme le plus rapide, mme si ce n'est pas vrai. De plus, cela demande
d'implmenter l'algorithme avant d'avoir une ide de
sa rapidit, ce qui est gnant (puisque la phase d'implmentation, d'criture concrte
du code, n'est pas facile), et mme pas
toujours possible : si le problme que l'on veut rsoudre est li une centrale
nuclaire, et demande d'utiliser les capteurs de la
centrale pour grer des informations, on peut difficilement se permettre
d'implmenter pour tester en conditions relles tous les
algorithmes qui nous passent par la tte.
Les scientifiques de l'informatique ont cr pour cela un outil extrmement pratique
et puissant, que nous tudierons dans la
suite de ce tutoriel : la complexit algorithmique. Le terme de 'complexit' est un
peu trompeur parce qu'on ne parle pas d'une
difficult de comprhension, mais d'efficacit : "complexe" ne veut pas dire
"compliqu". Un algorithme de forte complexit a un
comportement asymptotique (le mot est expliqu dans la prochaine section) moins
efficace qu'un algorithme de faible complexit,
il est donc gnralement plus lent. Mais on peut avoir des algorithmes trs faible
complexit qui sont extrmement compliqus.
L'ide en deux mots de la complexit algorithmique, c'est :
Citation
si je donne mon programme une entre de taille N, quel est l'ordre de grandeur, en
fonction de N, du nombre d'oprations
qu'il va effectuer ?
Elle repose sur le fait que les programmes qui rsolvent un problme dpendent des
conditions du problme : si les conditions
changent, ils peuvent s'effectuer en plus ou moins de temps. La complexit permet
de quantifier (mettre une formule
mathmatique) la relation entre les conditions de dpart et le temps effectu par
l'algorithme.
Pour "compter les oprations", il faut dcider de ce qu'est une opration. cette
question, les sages scientifiques n'ont pas pu
rpondre dfinitivement, parce que le choix dpend du problme (et mme de
l'algorithme) considr. Il faut en fait choisir soimme
quelques petites oprations que l'algorithme effectue souvent, et que l'on veut
utiliser comme oprations de base pour
mesurer la complexit. Par exemple, pour faire une omelette, on peut considrer que
les trois oprations de base sont de casser un
oeuf, de battre un oeuf en omelette, et de faire cuire un oeuf battu en omelette. On
peut donc pour chaque recette compter le
nombre d'oeufs casss, cuits et battus, et avoir ainsi une ide de la complexit de la
recette (l'omelette tant un plat bien connu et
codifi, on peut s'attendre ce que toutes les recettes aient la mme complexit :
pour N oeufs, on effectue 3N oprations) :
l'ajout de sel, poivre ou herbes est trs rapide, et n'a pas besoin d'tre pris en
compte dans l'analyse de la complexit (donc
Mesure 'asymptotique'
peut-tre que B et C sont tous les deux quatre fois plus rapides que A, et que
globalement c'est donc l'algorithme en 2N
oprations qui est le plus rapide.
Plus gnralement, les facteurs multiplicatifs n'ont pas forcment d'influence sur
l'efficacit d'un algorithme, et ne sont donc pas
pris en compte dans la mesure de la complexit. Cela permet aussi de rpondre
notre question de tout l'heure : si deux
programmeurs ont deux ordinateurs, et que l'un est plus rapide que l'autre, il sera
par exemple 3 fois plus rapide en moyenne ; ce
facteur constant sera nglig, et les deux programmeurs peuvent donc comparer la
complexit de leurs algorithmes sans
problme.
On a donc nglig pas mal de choses, ce qui aboutit une notion plutt simple et
assez gnrale. Cette gnralit fait de la
complexit un outil trs pratique, mais elle a videmment des inconvnients : dans
certains cas trs particuliers, un algorithme
plus complexe mettra en ralit moins de temps s'effectuer (par exemple, les
facteurs constants peuvent jouer en ralit un rle
trs important : et si le rservoir de Haskell le fermier tait norme et mettait toute la
journe se remplir ?). Cependant, dans la
grande majorit des cas, la complexit est une indication fiable des performances de
l'algorithme. En particulier, le fait que ce soit
une mesure asymptotique veut dire que les carts entre deux complexits se font de
plus en plus importants quand la taille de
l'entre augmente. Un algorithme en N oprations longues sera peut-tre un peu plus
lent qu'un algorithme en N*N oprations
trs rapides quand N vaut 10 ou 20, mais pour N = 3000 ou N = 8 millions,
l'algorithme le moins complexe sera trs certainement le
plus rapide.
d'autres notations approchantes (par exemple, on peut faire une distinction entre
"environ N oprations ou (beaucoup) moins" et
"prcisment environ N oprations", mais utiliser O pour exprimer prcisment la
complexit d'un algorithme est une convention
commune aux scientifiques du domaine. Si vous dcidez de vous spcialiser dans
l'algorithmique (ou que vous avez la chance
d'tudier les comportements asymptotiques en analyse), il vous faudra sans doute
approfondir un peu plus les fondements
formels de cette notation, mais cela devrait largement suffire pour ce texte, et plus
gnralement pour une comprhension solide
de la complexit des algorithmes (qui dpend en pratique remarquablement peu des
subtilits mathmatiques de la notation O).
Plus les contraintes sur les programmes sont fortes, plus on a besoin d'informations
prcises. Dans certains domaines de
l'informatique on s'intressera d'autres caractristiques, parfois mesurables elles
aussi en terme de complexit, des algorithmes.
Par exemple, un programmeur pour calculatrice ou systme embarqu pourra
s'interroger sur la consommation lectrique de son
algorithme, afin d'conomiser la batterie. Dans le cas gnral, on s'intressera
cependant uniquement aux complexits en temps et
en mmoire, et mme principalement la complexit en temps.
Un peu de pratique
la complexit.
Voici un deuxime problme dont la solution prsente une complexit facile tudier.
On dispose (encore !) d'une liste d'lments,
qui contient des doublons (des lments prsents plusieurs fois) que l'on veut
liminer : on veut rcuprer une liste contenant
les mmes lments, mais o chaque lment ne serait prsent qu'une seule fois.
Pouvez-vous penser un algorithme permettant de faire cela ? Essayez de le
chercher, avant de lire la solution propose ici.
Solution propose
Complexit
Quelle est la complexit de cet algorithme ? Si vous avez bien compris le calcul de la
complexit des algorithmes prcdents,
celui-ci vous parat peut-tre simple, mais autant faire trop que pas assez.
Pour chaque lment de la liste de dpart, on effectue un parcours de la liste U, donc
autant d'oprations que U a d'lments. Le
problme c'est que la taille de U change pendant le parcours de la liste de dpart,
puisqu'on y ajoute des lments. Quand on
considre le premier lment, la liste U est vide (donc on n'effectue aucune
opration). Quand on considre le deuxime lment,
U a 1 lment, donc on effectue une opration de plus.
Il existe une autre solution, laquelle vous avez peut-tre (si vous tes un peu tordus
) pens en rflchissant l'algorithme :
il est possible de commencer par trier les lments de la liste. Ainsi, tous les
lments identiques se retrouvent cte cte, et il
devient trs simple d'liminer les doublons :
Citation
Il suffit de parcourir la liste en se souvenant du dernier lment
parcouru. Si l'lment actuel est le mme que l'lment prcdent,
alors c'est un doublon et on ne l'inclut pas dans la liste des
lments uniques.
L'algorithme n'est plus valable si les lments gaux ne sont pas juste ct les uns
des autres, donc il faut forcment trier la
liste avant. Quelle est sa complexit ? L'limination des doublons se fait en un seul
parcours de la liste, elle est donc linaire.
Mais comme on a d trier la liste avant, ce tri a aussi effectu des oprations qu'il
faut prendre en compte dans la complexit
totale de ce deuxime algorithme.
C'est un peu de la triche, parce que vous ne savez pas encore comment trier les
lments d'une liste (j'espre que vous saurez le
faire, et mme de plusieurs manires diffrentes, la fin de ce cours). Vous aurez
donc d utiliser une des fonctions de votre
langage de programmation ou d'une bibliothque externe fournie ct, ce qui ne
correspond pas forcment la dfinition d'un
algorithme, qui demande des descriptions "prcises, l'aide de concepts simples" :
on peut attendre d'un ordinateur qu'il sache
parcourir une liste ou comparer des lments, mais trier c'est plus difficile (un peu
comme ranger sa chambre, ou sa centrale
nuclaire). Quand vous connatrez de nombreux algorithmes, vous pourrez facilement
les utiliser pour mettre au point vos
propres solutions, mais actuellement vous devriez vous limiter ce que vous avez
dj conu (donc, pas de tri de tableau).
Dans tous les cas, cette mthode marche, et le fait que ce soit de la triche ne me
permet pas d'esquiver la question de la
complexit. Il se trouve que la complexit de cet algorithme dpend de la complexit
du tri : si par exemple le tri effectue environ
L2 oprations, c'est beaucoup plus que les L oprations que l'on fait ensuite, et la
complexit globale est bien en O(L2 ).
Cependant, il existe des tris plus sophistiqus (et plus compliqus) qui, tout en
faisant toujours plus de L oprations (et en ayant
dans le cas gnral une complexit plus grande que O(L)), font beaucoup moins de
L2 oprations. Nous le verrons le moment
venu, mais ces tris l produisent un algorithme plus efficace que le premier propos,
qui est plus "naf".
Enfin, il faut noter qu'il n'est pas ncessaire de trier la liste pour obtenir un
algorithme plus efficace. On peut aussi choisir
d'utiliser la place de la liste U une structure de donnes plus efficace : dans notre
cas, il faudrait qu'elle puisse rpondre
rapidement la question "l'lment machin a-t-il dj t rencontr ?". Si l'on peut y
rpondre sans parcourir toute la structure
(comme on fait pour U), on peut avoir un algorithme plus rapide. De telles structures
de donnes existent, et permettent d'obtenir
un algorithme aussi efficace qu'avec le tri (en plus, comme il est proche de
l'algorithme naf, il est plus naturel concevoir et plus
facile comprendre), mais nous ne les aborderons pas non plus tout de suite.
Vous avez maintenant dj trois algorithmes dans votre carquois.
La recherche d'un lment donn dans une liste, et la recherche du plus grand
lment d'une liste sont des algorithmes assez
proches, linaire en temps (en O(N)) et utilisation mmoire constante (en O(1)).
L'limination des lments en double dans une
liste est plus complique, puisque l'algorithme le plus simple a une complexit
quadratique (en O(N*N)) en temps et linaire en
mmoire.
J'espre que ces tudes plus concrtes (mais avec encore un peu trop de blabla) vous
ont convaincus que cette discipline
servait quand mme parfois quelque chose, et que vous commencez vous
habituer aux concepts de base : algorithme,
complexit en temps et en mmoire, structure de donnes.
Dfinition
Tableaux
Le tableau est sans doute la structure de donnes la plus courante, du moins dans les
langages drivs ou inspirs par le
langage C.
Le principe d'un tableau est trs simple : on stocke les lments dans des cases,
chaque case tant tiquete d'un numro
(gnralement appel indice). Pour accder un lment particulier d'un tableau, on
donne son indice.
Les indices sont des entiers conscutifs, et on considrera qu'ils commencent 0,
comme dans la plupart des langages de
programmation. Le premier lment est donc l'indice 0, le deuxime l'indice 1,
etc. (attention au dcalage). En particulier, si n
est la taille du tableau, le dernier lment se trouve l'indice n-1. Demander l'accs
un indice qui n'existe pas provoque une
erreur.
On considre que la taille d'un tableau est toujours connue (le programmeur a d la
connatre quand il a demand la cration du
tableau, et ensuite il suffit de la conserver).
Listes
La liste est une structure de donnes extrmement utilise. En particulier, elle a jou
un rle majeur dans le langage Lisp et reste
trs prsente dans les nombreux langages qui s'en sont inspirs.
Remarque : pour dcrire correctement une liste, je suis forc de m'carter
lgrement des pures considrations algorithmiques,
partie de la dfinition des listes, que l'on peut reformuler ainsi : soit une liste vide,
soit un cons d'une liste.
Implmentation : Dans les langages o les listes existent dj, il est extrmement
simple de dfinir cons. Par exemple en Caml :
Code : OCaml
let cons tete queue = tete::queue
Sinon, il faut utiliser le type que l'on a dfini soi-mme. En C, il faut en plus s'occuper
de l'allocation mmoire :
Code : C
List *cons(int valeur, List *liste)
{
List *elem = malloc(sizeof(List));
if (NULL == elem)
exit(EXIT_FAILURE);
elem->val = valeur;
elem->next = liste;
return elem;
}
Pour les tableaux, la question est plus dlicate. La taille d'un tableau tant fixe
l'avance, il n'est pas possible de rajouter des
lments (tout simplement parce qu'il n'y a pas forcment de place disponible dans
la mmoire, sur les bords du tableau, pour
pouvoir l'agrandir). La mthode sre pour ajouter un (ou plusieurs) lments est de
crer un tableau plus grand autre part, qui
contienne assez de place pour tous les anciens lments et le (ou les) nouveau(x), et
de recopier les anciens lments dans le
nouveau tableau, avant d'ajouter les nouveaux. Cette mthode demande la cration
d'un tableau de taille N+1, puis une recopie
de chaque lment du tableau, elle est donc en O(N) (o N est la taille du tableau
avant insertion), ou encore linaire. De mme, la
taille d'un tableau tant fixe l'avance, il n'est pas possible d'en retirer des cases.
Remarque : dans certains langages, il est possible d'essayer de redimensionner les
tableaux sur place dans certains cas, ou bien
d'liminer des lments qui sont en dbut ou en fin de tableau. Cela reste assez
hasardeux, et nous ne considrerons pas ces
Taille
Remarque : comme pour les tableaux, il serait possible de stocker la taille des listes
dans la structure elle-mme, au lieu de devoir
la calculer chaque fois : en plus d'avoir tte et queue, on ajouterait chaque cellule
un champ taille qui contiendrait la
taille de la liste. Le problme de cette mthode est que l'opration cons devient plus
coteuse : quand on cre une nouvelle
cellule pour l'lment rajouter, il faut y mettre le nouvel lment et la queue
comme auparavant, mais ensuite il faut accder la
premire cellule de la queue, pour y lire la taille N de l'ancienne liste, pour pouvoir
mettre N+1 dans le champ taille de la
nouvelle cellule. Cela ne fait que rajouter une tape (plus prcisment, deux lectures
de cellules, une addition et une initialisation
de champ en plus), donc l'opration reste en O(1), mais cela ralentit quand mme
sensiblement l'opration, ce qui est gnant
quand on utilise beaucoup cons. En pratique, la plupart des gens utilisent beaucoup
cons, et ont trs peu souvent besoin de
la taille de la liste ; cette "optimisation" n'est donc pas intressante, car elle
ralentirait le programme. Encore une fois, on retrouve
l'ide centrale, qui est qu'il faut choisir ses structures de donnes selon l'utilisation
qu'on veut en faire, pour que les oprations
les plus courantes soient les plus rapides possibles.
Accs un lment
Comment faire si l'on veut rcuprer par exemple le cinquime lment de notre
collection (liste ou tableau) ? Pour un tableau,
c'est simple : on demande l'lment d'indice 4 (attention au dcalage), et on l'obtient
immdiatement. Cette opration est en O(1).
Pour une liste, c'est plus difficile : quand on a une liste, on a accs directement la
premire cellule, donc on ne connat que sa
tte, et sa queue ; on ne peut donner rapidement que le premier lment. Mais en
fait, on peut aussi avoir accs au deuxime :
c'est la tte de la queue de la liste ! Et au troisime : la tte de la queue de la queue
de la liste. En fait, on cherche la tte de la
queue de la queue de la queue de la queue de la liste. Trop facile.
Voici un algorithme pour rcuprer l'lment d'indice n dans une liste :
si n = 0 (on demande le premier lment), renvoyer l'lment qui est dans le champ
tte ;
sinon, renvoyer l'lment qui est l'indice n-1 dans la liste qui est dans le champ
queue.
Vous pouvez remarquer qu'on considre directement notre liste comme une cellule :
si la liste est vide, on ne peut pas y rcuprer
d'lment, donc c'est une erreur.
Pour accder un lment, il faut parcourir toute la liste jusqu' la position voulue.
Pour accder l'lment d'indice k il faut
donc faire environ k oprations. Quelle est la complexit de l'opration ? Comme
expliqu dans la premire partie, il faut tre
pessimiste et considrer la complexit dans le pire des cas : dans le pire des cas, on
cherche le dernier lment de la liste, il faut
donc la parcourir toute entire. L'opration est donc linaire, en O(N).
Vous avez sans doute remarqu la grande diffrence entre le problme de l'accs au
premier lment, et l'accs "n'importe quel"
lment. Dans une liste, la premire opration est en O(1) ( ) et la deuxime en O(N)
( ).
Pour bien les diffrencier, les informaticiens ont un terme spcifique pour dire
"l'accs n'importe quel lment" : ils parlent
sont plus lentes pour l'accs arbitraire. Les tableaux ont la proprit d'avoir un accs
arbitraire en temps constant, ce qui est rare
et trs utile dans certains cas.
Remarque : vous ne connaissiez peut-tre pas le terme "accs arbitraire", mais vous
avez srement dj rencontr son
quivalent anglais, random access. Ou alors, vous ne vous tes jamais demand, en
tripotant la mmoire vive de votre
ordinateur, ce que signifiait RAM : Random Access Memory, mmoire accs
arbitraire.
Le problme de l'accs une liste ne se limite pas seulement la lecture de
l'lment une position donne : on pourrait aussi
vouloir rajouter ou enlever un lment cette position. Ces algorithmes sont proches
de celui de lecture, et ont eux aussi une
complexit linaire.
Petite anecdote pour illustrer l'importance de l'tude de la complexit : lorsque
nous ne travaillons pas sur ce tutoriel, il nous
arrive de jouer. Parmi ces jeux, l'un d'entre eux avait un temps de chargement de 90
secondes ds qu'il fallait gnrer une nouvelle
carte du monde. Un peu surpris, et tant donn que le code source du jeu tait
disponible, nous avons tudi la fonctionnalit
fautive. Le jeu passait 88 secondes accder de manire alatoire aux lments
d'une liste ! En transformant cette liste en simple
tableau, le chargement est devenu quasi-instantan. Les plus curieux peuvent aller
tudier le changement effectu qui a t
accept par l'auteur du jeu vido en question.
Concatnation, filtrage
Concatnation
Imaginons que l'on ait deux groupes d'lments, stocks dans deux listes (ou deux
tableaux) diffrents, et que l'on veuille les
runir. On veut construire une structure qui est en quelque sorte la "somme" des
deux structures de dpart. On appelle cette
opration la "concatnation" (cela vient du latin pour "enchaner ensemble").
Pour des tableaux, c'est assez facile : si le premier tableau est A, et le deuxime B, et
que l'on note L la taille de A et L' (lire "L
prime") la taille de B, on cre un tableau de taille L + L', o l'on recopie tous les
lments de A, puis tous les lments de B. Cela
demande L + L' copies (et la cration de L + L' cases) : l'opration est en O(L + L').
Remarque : j'ai ici donn la complexit en fonction de deux variables, L et L'. J'avais
auparavant dfini la complexit comme
dpendant d'une seule variable, mais c'est un cas particulier. La complexit d'un
algorithme peut dpendre de tous les paramtres
dont dpend l'algorithme, et pas seulement d'un seul. De plus, la complexit n'a de
sens que quand les variables que l'on utilise
pour l'exprimer sont bien dfinies : dire O(N3) ne suffit pas, il faut s'assurer que tout
le monde comprend ce que dsigne la
variable N (mme si en gnral, c'est vident et laiss implicite).
Pour une liste, la situation est un peu diffrente : comme on peut facilement ajouter
un lment en tte de liste, on peut aussi
ajouter une suite d'lments. Il suffit donc d'ajouter tous les lments de A en tte de
la liste B. Cela revient faire une copie de A
devant B. Vous pouvez dj deviner (l'algorithme sera prcis ensuite) que comme
on ajoute L (la taille de A) lments en tte de
B, la complexit sera en O(L).
Remarque : avec cet algorithme, on recopie (par le cons) chaque cellule de la liste A
: la liste B est laisse inchange, mais on a
cr L cellules. Vous avez peut-tre remarqu qu'une autre manire de faire serait
possible : on pourrait prendre directement la
dernire flche de A (celle qui pointe vers la liste vide), et la modifier pour la faire
pointer vers la premire cellule de B. Cette
mthode a l'avantage de ne pas demander de recopie des cellules de A, mais aussi
un inconvnient majeur : elle modifie la liste A.
Si vous aviez une variable qui dsignait A avant l'opration, elle dsigne maintenant
concat(A, B). La liste A, en quelque
sorte, a t "dtruite".
Ce comportement, que l'on appelle un effet de bord , peut donner lieu des bugs si
vous ne faites pas attention (par exemple si
vous croyez avoir encore accs A, alors qu'en fait vous tes en train de manipuler
concat(A, B)). Si l'on limine la
ngligence du programmeur (parce que vous tes srement persuads que vous,
vous ne faites pas ce genre d'erreurs - haha !), il
peut encore se poser des problmes dlicats dans le cas d'applications multi-thread
par exemple (un thread calcule le nombre
d'lments de votre liste, mais juste avant qu'il l'utilise pour faire quelque chose, un
autre thread modifie silencieusement la liste
en lui ajoutant plein d'lments la fin ; la taille calcule par votre premier thread
n'est plus valide : boum !).
Globalement, l'algorithme prsent, qui a la proprit de ne pas modifier les listes A
et B de dpart, est beaucoup plus sr et
pratique utiliser. Il en existe d'autres formulations, mais elles ont de toute manire
toutes la mme complexit.
Vous pouvez noter que la concatnation de deux listes ne dpend pas de la taille de
la deuxime liste, qui est conserve
l'identique, mais seulement de la premire. Pour les tableaux, la concatnation
dpend des deux. C'est une diffrence qui peut tre
trs importante si vous voulez concatner trs souvent de petites listes (ou de petits
tableaux) une grande liste (ou un grand
tableau). Dans le cas des tableaux, cette opration sera trs coteuse puisque vous
devrez recopier le grand tableau chaque
fois. En pratique, il est donc assez rare de concatner des tableaux, alors que
l'opration est plus courante pour les listes.
Filtrage
Voici une dernire opration qui se prsente rgulirement quand vous manipulez
des donnes : slectionner une partie d'entre
elles. Par exemple "parmi les personnes que je connais, je veux le nom de toutes
celles qui parlent allemand". En informatique, on
reprsentera la question "est-ce qu'il parle allemand ou non ?" par une fonction : elle
prend une personne en paramtre, et
renvoie true (vrai) si elle parle allemand, false (faux) sinon. J'appelle ce genre de
fonctions des "fonctions de choix" (on les
nomme parfois aussi prdicats). On veut effectuer une opration de filtrage : tant
donn une collection (liste ou tableau)
contenant des lments, et une fonction de choix sur ces lments, vous voulez
rcuprer seulement les lments pour lesquels
la fonction de choix renvoie true.
Si l'on utilise des tableaux (en particulier si l'on veut que les rsultats du filtrage
soient stocks dans un tableau), on est
confront un problme : on ne sait pas a priori quel sera le nombre d'lments
renvoyer. Vous ne savez pas a priori, sans
rflchir ou leur poser la question, combien de vos connaissances parlent allemand. Il
y a plusieurs possibilits. J'en citerai une
seule pour l'instant (je parlerai d'une deuxime ensuite, et de toute faon si vous
pensiez une autre, c'est trs bien).
La premire possibilit consiste partir d'un tableau de taille 0 (vide, quoi), en
l'agrandissant chaque fois que vous trouvez un
nouveau germaniste dans vos connaissances. Comme on l'a vu, agrandir un tableau
demande en gnral autant de recopies qu'il
a de cases. la premire personne trouve, vous ne ferez aucune recopie (crer un
tableau de taille 1 pour mettre la personne).
la deuxime, vous ferez une recopie (la premire personne trouve). la troisime,
vous ferez 2 recopies. Au final, si le tableau
filtr possde K lments, il aura t construit en faisant 0, puis 1, puis 2, ..., puis K-1
recopies, soit 0 + 1 + 2 + ... + (K-1) recopies
au total, c'est--dire environ K2 /2 recopies. Dans le pire des cas, K est gal N, la
taille du tableau de dpart (toutes vos
connaissances parlent allemand !), et vous avez environ N2 /2 oprations : cet
algorithme est en O(N2 ).
On peut obtenir un algorithme intressant pour les listes en appliquant exactement
cette mthode, mais en utilisant des listes la
place de tableaux : on commence avec une liste vide, et pour chaque lment
intressant (c'est--dire qui fait renvoyer true la
fonction de choix), on l'ajoute en tte de la liste (par un cons). Chaque cons est en
O(1), et au final on en fait au maximum N.
L'algorithme utilisant une liste est donc en O(N). Il est assez frappant de voir qu'en
utilisant exactement le mme algorithme, on
peut obtenir des complexits trs diffrentes simplement en changeant de structure
de donnes. Cela illustre le fait que le choix
des structures est important, et que le programmeur doit en tre conscient.
Synthse
Oprations
Conversions
Enfin, il faut savoir que les choix de structures de donnes, ce n'est pas pour toute la
vie. Les structures de donnes ne sont
qu'un moyen de stocker des informations, et, de mme qu'il vous arrive peut-tre de
temps en temps de rorganiser votre bureau
ou votre logement, il est possible de changer d'organisation, c'est--dire de passer
d'une structure de donnes une autre, en
conservant les informations stockes.
Exercice : crire une fonction convertissant une liste en tableau, et une fonction
convertissant un tableau en liste. Les deux
fonctions doivent tre en O(N).
Le passage d'une structure de donnes une autre peut tre une trs bonne ide si
votre programme passe par plusieurs phases
bien spares, qui utilisent des oprations trs diffrentes.
Par exemple, vous pouvez commencer votre programme en rcoltant de l'information
(beaucoup d'ajouts d'lments, de
concatnations, de filtrages pour liminer les mauvais lments, etc.), avec ensuite
une deuxime moiti du programme consacre
Une fois que vous avez lu ce chapitre, vous avez (peut-tre) tout compris des
diffrences entre l'accs arbitraire en O(1) et l'accs
linaire en O(N), et vous vous apprtez faire un massacre dans votre langage de
programmation prfr : "je vais pouvoir
indexer deux milliards de pages supplmentaires par jour !".
Malheureusement, il arrive que certains langages ne fonctionnent pas exactement
comme je l'ai dcrit ici. Au lieu d'avoir des
structures de donnes hautement spcialises, comme les listes et les tableaux, ils
prfrent des donnes qui se comportent "pas
trop mal" sur la plupart des oprations courantes (ou plutt que les concepteurs du
langage ont juges les plus courantes),
quitte tre des structures de donnes compliques et inconnues, et se comporter
parfois beaucoup moins bien que vous ne
l'espriez. Le problme c'est que ces structures surprises ont aussi l'habitude
agaante de prendre des noms innocents comme
"listes" ou "tableaux".
Par exemple, les "listes" de Python sont en fait des tableaux tranges, et les
"tableaux" de PHP ne sont pas du tout des tableaux
(vous auriez d vous en douter en remarquant qu'ils proposent des oprations
"ajouter au dbut", "ajouter la fin", "retirer au
dbut", etc.). D'autres langages encore, souvent ceux que l'on appelle des "langages
de scripts", ont des politiques aussi
cavalires quant aux structures de donnes.
Rsultat des courses, il faut toujours se mfier avant d'utiliser une structure de
donnes. Essayez de vous renseigner sur la
complexit algorithmique des oprations qui vous intresse, pour vrifier que c'est
bien celle laquelle vous vous attendez. Sur
certains algorithmes, passer de O(1) O(N) pour une opration peut faire trs mal !
Il existe une mthode pour reprer les "structures de donnes risques", c'est--dire
celles qui ne sont pas tout fait de vrais
tableaux ou de vraies listes, mais des structures de donnes hybrides dguises :
leur interface. On a vu par exemple qu'un
tableau supporte trs mal l'insertion d'lment : si la bibliothque des "tableaux" de
votre langage propose d'insrer et de
supprimer des lments comme si c'tait une opration naturelle, alors ce ne sont
sans doute pas de vrais tableaux.
Ces langages "de moches" (nom plus ou moins affectueux dsignant les langages qui
font passer la 'simplicit' de la
programmation avant toute chose, y compris la gestion de l'efficacit (algorithmique)
la plus lmentaire) permettent parfois
d'utiliser les "vraies" structures de donnes, dans une bibliothque spcialise. Par
exemple, les listes de Python proposent un
accs arbitraire, mais il vaut mieux utiliser (c'est possible) les "vrais" tableaux de la
bibliothque array.
C'est aussi quelque chose que vous devrez prendre en compte si vous crez un jour
votre propre bibliothque de structures de
donnes (eh oui, a arrive !). Si une opration est trop coteuse pour votre structure,
vous n'tes pas obligs de la proposer aux
utilisateurs (qui pourraient se croire encourags l'utiliser) : une fonction de
conversion vers une structure plus classique qui
supporte efficacement cette opration fera l'affaire. De manire gnrale, il est
important de bien prciser la complexit prvue des
oprations que vous offrez aux utilisateurs.
Le dictionnaire peut tre reprsent sous la forme d'un tableau tri par ordre
alphabtique. Pour trouver la dfinition d'un mot
dans le dictionnaire, on utilise l'algorithme de dichotomie :
on regarde le mot situ au milieu du dictionnaire. En fonction de sa position par
rapport au mot cherch, on sait dans
quelle moiti continuer la recherche : s'il est plus loin dans la liste alphabtique, il
faut chercher dans la premire moiti,
sinon dans la deuxime ;
on a nouveau un demi-dictionnaire dans lequel chercher : on peut encore appliquer
la mme mthode.
Essayez de l'implmenter dans votre langage prfr avant de regarder la solution.
Nous, on kiffe le PHP :
Code : PHP
<?php
function find($mot, $dictionnaire, $begin, $end) {
if ($begin > $end)
return false;
$new = floor(($begin + $end) / 2);
$cmp = strcmp($dictionnaire[$new], $mot);
if ($cmp == 0)
return true;
else if ($cmp > 0)
return find($mot, $dictionnaire, $begin, $new - 1);
else
return find($mot, $dictionnaire, $new + 1, $end);
}/
/ exemple :
$dictionnaire = array('chat', 'cheval', 'chien', 'grenouille');
echo find('chien', $dictionnaire, 0, sizeof($dictionnaire) - 1);
?>
Remarque : pour la recherche dichotomique, on a crucialement besoin de l'accs
arbitraire : tout repose sur la possibilit de
Calcul de la complexit
avanc, et nous n'essaierons pas de le faire ici. Il suffit de se dire que log(n)
correspond au "nombre de fois qu'on doit diviser
n par 2 pour obtenir un nombre infrieur ou gal 1".
Pour y parvenir, on choisit un intervalle [a;b] qui contient la solution (par exemple
l'intervalle [0; 2]).
On constate que f(0) est ngatif et que f(2) est positif. Comme notre fonction est
continue, l'quation f(x) = 0 possde
ncessairement une solution entre 0 et 2. En utilisant la dichotomie, on peut rduire
l'intervalle et donc amliorer la qualit de
l'approximation.
Comme pour la recherche d'un mot dans le dictionnaire, on slectionne une nouvelle
valeur m positionne au milieu de l'intervalle
[a;b]. Si f(m) est positif, on peut en dduire que la solution est comprise dans
l'intervalle [a;m]; sinon, elle se trouve dans
l'intervalle [m; b].
On a donc rduit l'intervalle initial de moiti, affinant ainsi la qualit de
l'approximation. On continue de la sorte jusqu' ce que la
taille de l'intervalle soit considre comme suffisamment petite.
Voici une mise en pratique, en PHP :
Code : PHP
<?php
function f($x) {
return pow($x, 2) - 2;
}
function find_zero($a, $b, $erreur) {
if (($b - $a) < $erreur)
return array($a, $b);
$m = ($a + $b) / 2;
if (f($a) * f($m) < 0)
return find_zero($a, $m, $erreur);
else
return find_zero($m, $b, $erreur);
}
// exemple :
echo '<pre>';
print_r(find_zero(0, 2, 0.0001));
echo '</pre>';
?>
Le principe de la dichotomie est toujours le mme : on divise notre problme en deux
parties, et on en limine une. La diffrence
principale avec les codes prcdents se trouve dans le test qui permet de dcider si
on reste dans l'intervalle [a; m] ou [m; b].
Dans le cas o f(a) et f(b) sont tous les deux positifs ou tous les deux ngatifs, le
produit des deux sera positif. Dans le cas o les
deux valeurs ont un signe oppos, le produit sera ngatif, et cela nous assure que le
zro se trouve dans cet intervalle.
Si vous n'tes toujours pas convaincu, voici un tableau explicitant les diffrents cas
possibles qui permet de justifier la condition
que nous avons utilise :
Signe de f($a) Signe de f($b) Signe de f($a)*f($b)
+++
--+
-++-Il existe d'autres mthodes pour rechercher les 'zros de fonctions' (c'est--dire les
points o les fonctions s'annulent), certaines
tant encore plus efficaces dans des cas spcifiques. Cependant, la recherche
dichotomique est une trs bonne base parce
qu'elle marche sur n'importe quelle fonction continue (si on connat deux points dont
les valeurs sont de signes opposs) et que
le gain de prcision est "garanti" : on sait prcisment quelle approximation on aura
au bout de N tapes (et c'est L/2N , o L est
la longueur de l'intervalle de recherche initial).
La dichotomie n'est pas le seul exemple d'algorithme qui dcoupe les donnes pour
les traiter sparment. La mthode gnrale
est appele "diviser pour rgner" (ou parfois en anglais "divide and conquer", en
rfrence aux souverains qui pensaient que
sparer leurs sujets en groupes bien distincts permettait de les gouverner plus
facilement - cela vitait, par exemple, qu'ils se
regroupent pour demander des augmentations de salaire, ou ce genre de choses
gnantes). Il est important de noter que ces
algorithmes ne font pas qu'liminer, comme la dichotomie, une partie des donnes,
mais qu'ils peuvent profiter de la subdivison
pour effectuer moins de calculs.
On peut mettre en application cette ide de faon trs astucieuse pour calculer les
puissances d'un nombre. Pour ceux qui ne
seraient pas au courant, x la puissance n, not xn, vaut x multipli par lui-mme n
fois : x * x * ... * x , avec n termes 'x', et x0 vaut
1 (c'est une convention naturelle et pratique). Cette opration est trs utile, par
exemple si on se demande "quelle est la quantit
de nombres diffrents qui ont au plus 3 chiffres ?" : la rponse est 10 3 ; de 0 999, il
y a 103 = 10 * 10 * 10 = 1000 nombres.
Avec cette dfinition, comment calculer x8 ? Il est vident qu'une bonne rponse est :
(x * x * x * x * x * x * x * x). Si l'on demande
l'ordinateur de calculer a, il va effectuer 7 multiplications (vous pouvez compter).
Plus gnralement, on peut calculer xn en
effectuant n-1 multiplications : c'est un algorithme linaire, en O(n).
Exercice : implmentez cette mthode simple de calcul de la puissance d'un
nombre.
Cependant, on peut faire mieux, en remarquant que x 8 = x4 * x4 : si l'on calcule x4
(avec la mthode simple, x4 = x * x * x * x, cela
fait 3 multiplications), il suffit de le multiplier ensuite par lui-mme pour obtenir x 8,
en faisant seulement une opration
supplmentaire, soit 4 au total. On peut faire encore mieux en utilisant le fait que x 4
= x2 * x2 : on peut calculer x4 en deux
multiplications, donc x8 en trois multiplications : c'est beaucoup mieux que les 7
multiplications initiales.
C'est un algorithme de "diviser pour rgner", parce qu'en dcoupant le problme
(calcul de x8) en deux sous-problmes (deux
calculs de x4), on s'est rendu compte qu'on avait normment simplifi la question :
il suffit de s'intresser un des sousproblmes
et le deuxime tombe avec, puisqu'on peut rutiliser le rsultat du premier calcul (en
faisant une multiplication
supplmentaire). Vous avez peut-tre dj reconnu la configuration qui commence
tre familire : pour passer de x4 x8, donc
en doublant la difficult, il suffit de rajouter une opration (une multiplication) ; il y a
du logarithme dans l'air !
Cependant, il reste un problme rsoudre : avec cette bonne ide, on peut calculer
facilement x2, x4, x8, x16, x32, etc., mais
comment faire pour les nombres qui sont moins "gentils" comme 7, 13 ou 51 ?
Plus gnralement, on sait comment rduire efficacement le problme du calcul de
xn quand n est pair : on a xn = xn/2 * xn/2.
Quand n est impair, la division par 2 donne un reste, cette mthode n'est donc pas
utilisable directement. Cependant, il suffit de
calculer xn-1 (ce qui est facile puisque si n est impair, alors n-1 est pair), et de
multiplier ensuite par x pour obtenir xn :
x0 = 1 ;
si n est pair, xn = xn/2 * xn/2 ;
Les algorithmes de la forme "diviser pour rgner" sont trs importants et nous en
rencontrerons plusieurs reprises dans les
prochains chapitres.
Comme on l'a vu, il est facile de rechercher un lment particulier dans un ensemble
tri, par exemple un dictionnaire. Mais dans
la "vraie vie", ou plutt dans la vie d'un programmeur, les informations ne sont pas
souvent tries. Il se produit mme un
phnomne assez agaant et trs gnral : quand on laisse quelque chose changer,
a devient vite le bazar (exemple : votre
chambre). Des scientifiques trs intelligents ont pass beaucoup de temps tudier
ce principe.
Il y a plusieurs approches pour se protger de ce danger. La premire est de faire trs
attention, tout le temps, ce que les choses
soient bien ranges. C'est ce que fait par exemple un bibliothcaire : quand on lui
rend un livre, il va le poser sur le bon rayon, au
bon endroit, et s'il fait bien cela chaque fois il est facile de trouver le livre qu'on
cherche dans une bibliothque. C'est aussi ce
que certains font avec leur chambre, ils passent leur temps rordonner leurs livres,
leurs cahiers, etc. D'autres prfrent une
mthode plus radicale : toutes les semaines, ou tous les mois, ou tous les dix ans, ils
font un grand mnage.
Pour l'instant, nous allons nous intresser au grand mnage : quand on a un
ensemble de donnes dans un ordre quelconque,
comment rcuprer les mmes donnes dans l'ordre ? C'est le problme du tri, et il a
de multiples solutions. Curieusement, les
mthodes utilises par l'ordinateur sont parfois trs diffrentes de celles qu'utilisent
les humains ; il y a plusieurs raisons, par
exemple le fait qu'ils trient souvent beaucoup plus de choses (vous imaginez une
chambre avec 5 millions de chaussettes sales ?)
mais surtout qu'ils ne font presque jamais d'erreurs et ne s'ennuient jamais.
Pour crire un algorithme, il faut se mettre bien d'accord sur le problme qu'il rsout.
Problme du tri : On possde une collection d'lments, que l'on sait comparer
entre eux. On veut obtenir ces lments dans
l'ordre, c'est--dire une collection contenant exactement les mmes lments, mais
dans laquelle un lment est toujours "plus
petit" que tous les lments suivants.
Vous noterez qu'on n'a pas besoin de prciser quel est le type des lments : on peut
vouloir trier des entiers, des mots ou des
chaussettes. On n'a pas non plus prcis de mthode de comparaison particulire : si
on veut trier une liste de personnes, on
peut la trier par nom, par adresse ou par numro de tlphone. Mme pour des
entiers, on peut vouloir les trier par ordre
croissant ou par ordre dcroissant, c'est--dire en les comparant de diffrentes
manires. Le tout est de convenir ce que veut dire
"plus petit" pour ce que l'on veut trier (paradoxalement, si l'on veut trier des entiers
en ordre dcroissant, on dira que 5 est "plus
petit" que 3, puisqu'on le veut avant dans la liste trie).
Dans la plupart des cas, on triera des entiers par ordre croissant. C'est le cas le plus
simple, et les tris exposs ici seront tous
Le tri par slection est sans doute le tri le plus simple imaginer.
On a une suite d'lments dans le dsordre, que l'on va appeler E (comme "entre"),
et on veut construire une suite de rsultats,
contenant les mmes lments dans l'ordre, que l'on va appeler S (comme "sortie").
Quel sera le premier lment de S ? C'est le plus petit lment de E. Il suffit donc de
parcourir E, d'en choisir le plus petit lment,
et de le mettre en premire position dans S. On peut, au passage, l'enlever de la
suite E, pour ne pas risquer de se tromper et de
l'ajouter plusieurs fois dans S.
Quel sera le deuxime lment de S ? C'est le deuxime plus petit lment de E.
Quand on a une suite quelconque, c'est plus
difficile de trouver le deuxime plus petit lment que le premier (mais ce n'est pas
trs difficile, vous pouvez essayer comme
Exercice) ; mais ici, on peut jouer sur le fait qu'on a enlev le plus petit lment,
c'est--dire qu'on a disposition la suite E prive
de son plus petit lment, que l'on peut noter E'. Le deuxime plus petit lment de
E, c'est clairement le premier plus petit lment
de E'. Il suffit donc de trouver le plus petit lment de E', le mettre en deuxime
position dans S. On peut continuer ainsi pour
obtenir le troisime lment, etc. , jusqu'au dernier lment de S.
Complexit
Code : OCaml
let rec tri_selection = function
| [] -> []
| tete::queue ->
let plus_petit, reste = retire_min tete [] queue in
plus_petit :: tri_selection reste
Code : C
List *tri_selection(List *liste)
{
if (NULL == liste)
return NULL;
else {
List *selection, *resultat;
selection = retire_min(liste->next, NULL, liste->val);
resultat = cons(selection->val, tri_selection(selection>next));
free_list(selection); /* on libre la liste intermdiaire
*/
return resultat;
}
}
Remarque : on pourrait modifier l'implmentation C de retire_min pour modifier la
liste qu'on lui donne au lieu d'en allouer
une nouvelle qu'il faut librer ensuite. Comme a ne changerait rien la complexit
de l'algorithme (on doit parcourir la liste dans
tous les cas, pour trouver le minimum), j'ai choisi de privilgier la simplicit.
De manire gnrale, les codes que je mets dans ce tutoriel n'ont pas pour but d'tre
les plus rapides possibles, mais d'tre les
plus clairs possible (en ayant la bonne complexit). Il y a de nombreuses autres
faons d'crire le mme algorithme, certaines
tant plus performantes ou moins lisibles. Si vous avez un code plus efficace mais
plus compliqu pour le mme algorithme, vous
pouvez le poster en commentaire, mais je ne changerai l'implmentation du tutoriel
que si vous m'en proposez une plus simple ou
aussi simple.
Pour un tableau
Quand on trie une liste, on renvoie une nouvelle liste, sans modifier la liste de dpart.
Pour trier un tableau, on procde souvent
(quand l'algorithme s'y prte) diffremment : au lieu d'crire les lments dans
l'ordre dans un nouveau tableau, on modifie le
tableau d'entre en rordonnant les lments l'intrieur.
Cette approche a un avantage et un inconvnient. L'avantage c'est qu'il n'y a pas
besoin de crer un deuxime tableau, ce qui
utilise donc moins de mmoire. On dit que c'est un tri en place (tout est fait sur
place, on n'a rien rajout). L'inconvnient c'est
que le tableau de dpart est modifi. Si pour une raison ou une autre vous aviez
envie de conserver aussi l'ordre initial des
lments (par exemple, si vous vouliez vous souvenir aussi de l'ordre dans lequel les
donnes sont arrives), il est perdu et vous
ne pourrez pas le retrouver, moins de l'avoir sauvegard dans un autre tableau
avant le tri.
On commence par une fonction qui change la position de deux lments dans un
tableau.
Code : PHP
<?php
function echange(&$tab, $i, $j)
{
if ($i != $j) {
$temporaire = $tab[$i];
$tab[$i] = $tab[$j];
$tab[$j] = $temporaire;
}
}?
>
Code : C
void echange(int tab[], int i, int j)
{
if (i != j) {
int temp = tab[i];
tab[i] = tab[j];
tab[j] = temp;
}
}
Au lieu de stocker la valeur du minimum, on stocke son indice (sa position dans le
tableau) pour pouvoir changer les cases
ensuite.
Code : PHP
<?php
function tri_selection(&$tab)
{
$taille = count($tab);
for ($i = 0; $i < $taille - 1; ++$i) {
$i_min = $i;
for ($j = $i+1; $j < $taille; ++$j)
if ($tab[$j] < $tab[$i_min])
$i_min = $j;
echange($tab,$i,$i_min);
}
}?
>
Code : C
void tri_selection(int tab[], int taille)
{
int i, j;
for (i = 0; i < taille - 1; ++i) {
int i_min = i;
for (j = i + 1; j < taille; ++j)
if (tab[j] < tab[i_min])
i_min = j;
echange(tab, i, i_min);
}
}
On parcourt le tableau avec un indice i qui va de 0 la fin du tableau. Pendant le
parcours, le tableau est divis en deux parties :
gauche de i (les indices 0 .. i-1) se trouvent les petits lments, tris, et droite les
autres lments dans le dsordre.
chaque tour de boucle, on calcule le plus petit lment de la partie non encore trie,
et on l'change avec l'lment plac en i.
Ainsi, la partie 0 .. i du tableau est trie, et on peut continuer partir de i+1 ; la fin
le tableau sera compltement tri. Pour
faire le parallle avec les listes, au lieu de retirer l'lment du tableau, on le met
dans une partie du tableau qu'on ne parcours plus
ensuite.
Remarque : la boucle sur i s'arrte en fait avant taille-1, et pas avant taille comme
d'habitude : quand il ne reste plus
Comparaison
Les deux implmentations de la mme ide illustrent bien les diffrences majeures
entre les listes et les tableaux. On utilise dans
un cas la possibilit d'ajouter et d'enlever facilement des lments une liste, et
dans l'autre l'accs arbitraire qui permet de
parcourir, comparer et changer seulement des cases bien prcises du tableau.
Il existe un tri trs proche du tri par slection, appel tri par insertion, qui a la mme
complexit (O(N2)) mais est en pratique plus
efficace (car il effectue moins de comparaisons). Vous pouvez vous rfrer deux
tutoriels le dcrivant :
une version avec des tableaux, lire en premier, implmente en C
une version avec des listes, implmente en OCaml
Vous avez maintenant vu le tri par slection, dont le fonctionnement est assez
naturel. Vous vous dites peut-tre que finalement,
ce tuto est assez inutile, puisqu'il ne fait que parler longuement de chose assez
videntes. Dcouvrir que pour trier une liste il
faut commencer par chercher le plus petit lment, merci, votre petite soeur de deux
ans et demi l'aurait devin (et en plus, elle est
mignonne, et elle mange de la pure de potiron, avantages dcisifs qui manquent
ce modeste tutoriel).
Nous allons maintenant voir un autre tri, le tri par fusion. Il est surprenant par deux
aspects, qui sont trs lis :
il n'est pas du tout naturel au dpart ;
il est beaucoup plus efficace que les tri quadratiques vus jusqu' prsent.
C'est en effet un tri qui a une complexit bien meilleure que les tris par slection ou
insertion. On ne le voit pas sur un petit
nombre d'lment, mais sur de trs gros volumes c'est dcisif. Nous verrons sa
complexit en dtail aprs avoir dcrit
l'algorithme.
Algorithme
l'ordre : on sait que les plus petits lments des deux listes sont au dbut, et le plus
petit lment de la liste globale est forcment
soit le plus petit lment de la premire liste, soit le plus petit lment de la
deuxime (c'est le plus petit des deux). Une fois qu'on
l'a dtermin, on le retire de la demi-liste dans laquelle il se trouve, et on
recommence regarder les lments du dbut. Une fois
qu'on a puis les deux demi-listes, on a bien effectu la fusion.
print_list(c);
En C :
Code : C
return resultat;
}
}
Complexit
L'tude de la complexit du tri par fusion est assez simple. On commence avec une
liste (ou un tableau) de N lments. On le
dcoupe, ce qui fait deux tableaux de N/2 lments. On les dcoupe, ce qui fait 4
tableaux de N/4 lments. On les dcoupe, ce
qui fait 8 tableaux ...
Quand est-ce que la phase de dcoupage s'arrte ? Quand on est arriv des
tableaux de taille 1. Et combien de fois faut-il
diviser N par 2 pour obtenir 1 ? On l'a dj vu, c'est la fonction logarithme ! En effet,
si on a un tableau de taille 1, on renvoie le
tableau en une seule opration (f(0) = 1), et si on double la taille du tableau il faut
faire une dcoupe de plus (f(2*N) = f(N) + 1).
C'est bien notre sympathique fonction du chapitre prcdent. On a donc log(N)
phases de "dcoupe" successives.
Quel est le travail effectu chaque tape ? C'est le travail de fusion : aprs le tri, il
faut fusionner les demi-listes. Notre
algorithme de fusion est linaire : on parcours les deux demi-listes une seule fois,
donc la fusion de deux tableaux de taille N/2 est
en O(N).
Vous allez srement me faire remarquer que plus on dcoupe, plus on a de fusions
faire : au bout de 4 tapes de dcoupe, on se
retrouve avec 16 tableaux fusionner ! Oui, mais ces tableaux sont petits, ils ont
chacun N/16 lment. Au total, on a donc 16 *
N/16 = N oprations lors des fusions de ces tableaux : chaque tape, on a O(N)
oprations de fusion.
On a donc log(N) tapes O(N) oprations chacune. Au total, cela nous fait donc O(N
* log(N)) oprations : la complexit du tri
fusion est en O(N * log(N)) (parfois not simplement O(N log N), la multiplication est
sous-entendue).
Efficacit en pratique
va mme un peu plus vite que le tri par fusion). La diffrence se fait sur de grandes
valeurs, mais surtout elle caractrise
l'volution des performances quand les demandes changent. Avec une complexit de
O(N2 ), si on double la taille de l'entre, le
tri par slection va environ 4 fois plus lentement (c'est assez bien vrifi sur nos
exemples). Avec une complexit de O(N *
log(N)), cela va seulement un peu plus de 2 fois plus lentement environ (vu les petits
temps de calcul, les mesures sont plus
sensibles aux variations, donc moins fiables).
En extrapolant ce comportement, on obtient sur de trs grandes donnes un foss
absolument gigantesque. Par exemple, dans ce
cas prcis, le tri fusion sur 10 millions d'lments devrait prendre environ une demi
heure, alors que pour un tri par slection il
vous faudra... un an et demi.
Ce genre de diffrences n'est pas un cas rare. On est pass d'un facteur N un
facteur log(N), ce qui est plutt courant quand on
passe d'un code "naf" (sans rflexion algorithmique) quelque chose d'un peu
mieux pens. Cela vous donne une ide des
gains que peut vous apporter une connaissance de l'algorithmique.
Le passage des tris quadratique aux tri par fusion tait impressionnant. Est-ce que
dans le prochain chapitre, je vais encore vous
dcoiffer avec quelque chose d'encore plus magique ? Un tri en O(log(N)) ? Un tri qui
renvoie la sortie avant qu'on lui ai donn
l'entre ?
La rponse est non. On dit que le tri fusion est "optimal" parmi les tris par
comparaison, c'est dire qui trient en comparant les
lment deux par deux. Si on ne connat rien des donnes que l'on trie, on ne peut
pas les trier avec une meilleure complexit. Si
l'on avait des informations supplmentaires, on pourrait peut-tre faire mieux (par
exemple si on sait que toutes les valeurs sont
gales, bah on ne s'embte pas, on renvoie la liste directement), mais pas dans le
cas gnral. Ce rsultat assez tonnant sera
montr dans la dernire partie de ce tutoriel (qui n'est pas encore crite : vous
devrez attendre).
a ne veut pas dire que le tri par fusion est le meilleur tri qui existe. Il existe d'autres
tris (de la mme complexit, voire parfois
moins bonne dans le pire des cas) qui sont plus rapides en pratique. Mais d'un point
de vue algorithmique, vous ne pourrez pas
faire beaucoup mieux.
Remarque : c'est le moment de mentionner un petit dtail intressant, qui sort du
cadre de l'algorithmique proprement dite. On a
dj expliqu que la mesure de la complexit tait de nature asymptotique, c'est
dire qu'elle n'tait pertinente que pour de
grandes valeurs, une constante multiplicative prs. Il se trouve que pour des
petites valeurs (disons jusqu' 20, 50 ou 100
lments par exemple), le tri par insertion, bien que quadratique, est trs efficace en
pratique.
On peut donc donner un petit coup de pouce au tri par fusion de la manire
suivante : au lieu de couper la liste en deux jusqu'
qu'elle n'ait plus qu'un seul lment, on la coupe jusqu' qu'elle ait un petit nombre
d'lments, et ensuite on applique un tri par
insertion. Comme on n'a chang l'algorithme que pour les "petites valeurs", le
comportement asymptotique est le mme et la
complexit ne change pas, mais cette variante est un peu plus rapide.
Ce genre de petits dtails, qui marchent trs bien en pratique, sont l pour nous
empcher d'oublier que l'approche algorithmique
n'est pas la rponse toutes les questions de l'informatique. C'est un outil parmi
d'autres, mme si son importance est capitale.
Je pense que vous avez maintenant acquis les bases de l'algorithmique. Si vous avez
bien compris tout ce qui a t dit jusque l,
vous devriez tre capable de vous faire vous-mme une ide sur la complexit des
algorithmes simples, et d'intgrer cette
rflexion dans votre manire de programmer.
Ne vous attendez pas cependant faire des merveilles ds maintenant. Le "sens de
la complexit" (la capacit valuer la
complexit de son travail, sans forcment rentrer dans des prcisions formelles
pointues) demande de la pratique, il faut que cela
devienne une habitude. Dans la prochaine partie, nous vous prsenterons d'autres
algorithmes courants, que vous rencontrerez
sans doute dans de nombreuses situations, et qui agrandiront donc la fois votre
trousse outils algorithmique, et votre
www.openclassrooms.com
Piles et files
On a vu que les listes reprsentaient des suites d'lments que l'on pouvait
facilement agrandir ou rtrcir selon ses besoins. Il
se trouve qu'en pratique, certaines manires d'ajouter ou d'enlever des lments
reviennent trs souvents, et on leur a donc
donn un nom pour les reprer plus facilement : les piles et les files.
Concept
Imaginez que l'on manipule une liste d'lments qui volue au cours du temps : on
peut en ajouter et en retirer. Supposons que
l'on ajoute toujours les lments au dbut de la liste. Pour retirer les lments, il y a
deux possibilits simples qu'il est intressant
d'tudier :
le cas o on retire toujours les lments au dbut de la liste
le cas o on retire toujours les lments la fin de la liste
Ces deux cas de figures se retrouvent trs souvent dans la vie de tous les jours. Dans
le premier cas, on dit qu'on utilise une
structure de pile, dans le deuxime cas une structure de file.
Par exemple, imaginez qu'un professeur a donn un devoir ses lves, faire en
temps limit. Un peu avant la fin, certains
lves commencent rendre leur copie en avance. Le professeur dcide de
commencer corriger les copies qu'il reoit, pour
gagner du temps. Il y a une pile de copies sur son bureau, et :
quand un lve a termin, il rend sa copie au professeur qui la pose sur la pile de
copie
quand il a termin de corriger une copie, il prend la premire copie sur la pile pour la
corriger
Cela correspond bien ce que j'ai appel une pile. On dcrit souvent ce
comportement par l'expression dernier entr, premier
sorti (ou LIFO, de l'anglais Last In, First Out) : la dernire copie rendue est la premire
tre corrige.
Au contraire, quand vous faites la queue devant la caisse d'un supermarch, cela
correspond bien une file d'attente : les clients
arrivent d'un ct de la queue (au "dbut de la queue"), et la caissire est l'autre
bout ( la "fin de la queue"). Quand elle a
termin de compter les achats d'un client, elle fait passer le client qui est la fin de
la queue. C'est le comportement premier entr,
premier sorti (ou FIFO, First In, First Out).
Question : Imaginez un boulanger qui fait cuire du pain, le stocke puis le vend ses
clients. Les clients aiment bien avoir du pain
le plus frais possible (qui sort tout juste du four), et le boulanger ne peut pas leur
vendre de pain trop sec (qui est sorti du four
depuis trop longtemps). Quels sont les avantages d'une pile ou d'une file dans ce
cas ? Quelle structure choisiriez-vous ?
Mise en pratique
Piles
Les piles sont trs simples, parce que ce sont essentiellement des listes. On a vu
qu'avec une liste, il tait facile d'ajouter et de
retirer des lments en tte de liste. Cela fait exactement une pile.
Voici un exemple de code C pour grer les piles, qui rutilise le type List dfinit pour
les listes. Pour ajouter un lment on
utilise push, et pop pour enlever un lment et rcuprer sa valeur.
Code : C
typedef List *Stack;
Stack *new_stack(void)
{
Stack *stack;
if ((stack = malloc(sizeof *stack)) == NULL)
return NULL;
*stack = NULL;
return stack;
}
void free_stack(Stack *stack)
{
free_list(*stack);
free(stack);
}
int stack_is_empty(Stack *stack)
{
return *stack == NULL;
}
int stack_push(Stack *stack, int elem)
{
List *pushed;
if ((pushed = cons(elem, *stack)) == NULL)
return -1;
*stack = pushed;
return 0;
}
int stack_pop(Stack *stack, int *elem)
{
List *tail;
if (*stack == NULL)
return -1;
tail = (*stack)->next;
*elem = (*stack)->val;
free(*stack);
*stack = tail;
return 0;
}
Files
Les files sont un peu plus dlicates : si on retire les lments en tte de liste (au
dbut de la liste), il faut ajouter les lments la
fin de la liste. C'est quelque chose que l'on ne fait pas d'habitude, car ce n'est pas
pratique : dans une liste, on connat le premier
lment, mais pour accder au dernier lment il faut parcourir toute la liste jusqu'
la fin, ce qui est lent (complexit linaire). On
va donc crer une structure supplmentaire, qui contient une liste, mais qui stocke
aussi la cellule correspondant son dernier
lment, pour pouvoir y accder (et rajouter de nouveaux lments derrire).
Arbres
Les structures de donnes que nous avons vu jusqu'ici (tableaux, listes, piles, files)
sont linaires, dans le sens o elles
stockents les lments les uns la suite des autres : on peut les reprsenter comme
des lments placs sur une ligne, ou des
oiseaux sur un fil lectrique.
Au bout d'un moment, les oiseaux se lassent de leur fil lectrique : chaque oiseau a
deux voisins, et c'est assez ennuyeux, pas
pratique pour discuter. Ils peuvent s'envoler vers des structures plus complexes,
commencer par les arbres.
Dfinition
Un arbre est une structure constitue de noeuds, qui peuvent avoir des enfants (qui
sont d'autres noeuds). Sur l'exemple, le
noeud B a pour enfant les noeuds D et E, et est lui-mme l'enfant du noeud A.
Deux types de noeuds ont un statut particulier : les noeuds qui n'ont aucun enfant,
qu'on appelle des feuilles, et un noeud qui
n'est l'enfant d'aucun autre noeud, qu'on appelle la racine. Il n'y a forcment qu'une
seule racine, sinon cela ferait plusieurs
arbres disjoints. Sur l'exemple, A est la racine et D, E, G, H, I sont les feuilles.
Bien sr, on ne s'intresse en gnral pas seulement la structure de l'arbre (quelle
est la racine, o sont les feuilles, combien tel
noeud a d'enfants, etc.), mais on veut en gnral y stocker des informations. On
considrera donc des arbres dont chaque noeud
contient une valeur (par exemple un entier, ou tout autre valeur reprsentable par
votre langage de programmation prfr).
Comme pour les listes, on peut dfinir les arbres rcursivement : "un arbre est
constitu d'une valeur et d'une liste d'arbre (ses
enfants)". Vous pouvez remarquer qu'avec cette description, le concept d'"arbre
vide" n'existe pas : chaque arbre contient au
moins une valeur. C'est un dtail, et vous pouvez choisir une autre reprsentation
permettant les arbres vides, de toute manire
ce ne sont pas les plus intressants pour stocker de l'information
Exercice : essayer de reprsenter l'arbre donn en exemple dans le langage de
votre choix. Vous aurez peut-tre besoin de dfinir
une structure ou un type pour les arbres.
Solutions :
Solution en C :
Secret (cliquez pour afficher)
Vous pouvez remarquer qu'on a choisi ici d'utiliser un tableau pour stocker les
enfants. Dans ce chapitre, nous manipulerons
les arbres comme des structures statiques, sans ajouter ou enlever d'enfants un
noeud particulier, donc a ne sera pas un
problme.
Solution en Java :
Secret (cliquez pour afficher)
Code : Java
class Tree<T> {
T val;
Tree<T>[] enfants;
Tree(T val) {
this.val = val;
this.enfants = new Tree[0];
}
Tree(T val, Tree<T>[] enfants) {
this.val = val;
this.enfants = enfants;
}
public static void main(String[] args) {
Tree d = new Tree('D');
Tree e = new Tree('E');
Tree g = new Tree('G');
Tree h = new Tree('H');
Tree i = new Tree('I');
Tree[] enfants_de_f = { g, h, i };
Tree f = new Tree('F', enfants_de_f);
Tree[] enfants_de_b = { d, e };
Tree b = new Tree('B', enfants_de_b);
Tree[] enfants_de_c = { f };
Tree c = new Tree('C', enfants_de_c);
Tree[] enfants_de_a = { b , c };
Tree a = new Tree('A', enfants_de_a);
System.out.println(a.taille());
}
}
Vous pouvez remarquer qu'on a choisi ici d'utiliser un tableau pour stocker les
enfants. Dans ce chapitre, nous manipulerons
les arbres comme des structures statiques, sans ajouter ou enlever d'enfants un
noeud particulier, donc a ne sera pas un
problme.
Remarque : on utilise trs souvent en informatique des arbres comportant des
restrictions supplmentaires : nombre d'enfants,
lien entre les enfants et les parents, etc. En particulier les arbres binaires, o chaque
noeud a deux enfants au plus, sont trs
courants. Pour faire la diffrence on parle parfois d'arbres n-aires pour les arbres
gnraux que je prsente ici, mais le nom n'est
pas trs bon et je prfre garder "arbre".
Taille
Hauteur
Parcours en profondeur
Parcours en largeur
La mthode de parcours en profondeur est simple, mais l'ordre dans lequel les
noeuds sont parcourus n'est pas forcment trs
intuitif. En faisait un peu attention (par exemple en essayant de suivre le parcours du
doigt), vous serez capable de vous y
habituer, mais on pourrait esprer faire quelque chose qui paraisse plus "naturel"
un humain. Et si, par exemple, on voulait
parcourir notre arbre dans l'ordre alphabtique : A, B, C, D, E.. ?
Si cet ordre vous parat "logique", c'est parce que j'ai nomm mes sommets par
"couche" : on commence par la racine (A), puis
on donne un nom tous les noeuds de "premire gnration" (les enfants de la
racine) B et C, puis tous les noeuds de
"deuxime gnration" (les enfants des enfants de la racine), etc.
Remarque : l'algorithme de parcours en largeur est un peu plus compliqu que les
algorithmes que j'ai prsent jusqu'ici. Si vous
avez des difficults comprendre ce qui suit, c'est normal. Essayez de bien faire les
exercices, de bien relire les parties qui vous
posent problme, et n'hsitez pas aller demander sur les forums d'aide si vous
pensez avoir un vrai problme de
Avec notre algorithme, on stocke les noeuds dans deux structures spares : les
noeuds appartenant la couche courante, que
l'on va bientt parcourir, et les noeuds de la couche des enfants, que l'on va
parcourir plus tard. Est-il possible de conserver ce
sens de parcours, en utilisant une seule structure au lieu de deux ?
On peut obtenir une rponse cette question en considrant les deux structures
comme une seule : on considre qu'un noeud
"entre" dans nos structures quand on l'ajoute la couche des enfants, et qu'il en
"sort" quand on le prend dans la couche
courante pour le parcourir. Pour que notre parcours soit correct, il faut que l'ordre des
couches soit respect : un noeud d'une
couche doit entrer avant les noeuds de la couche suivante, et sortir avant tous les
noeuds de la couche suivante. Cela
correspond donc une forme de premier entr, premier sorti : premire couche
entre, premire couche sortie.
a ne vous rappelle pas quelque chose ? Normalement, si : premier entr, premier
sorti, c'est le comportement des files. On peut
donc utiliser une file pour stocker les noeuds, car l'ordre de la file respectera l'ordre
des couches.
On obtient donc l'algorithme suivant :
au dpart, on commence avec une file vide, dans laquelle on ajoute la racine
tant que la file n'est pas vide, on enlve le premier noeud de la file, on le parcourt et
on ajoute tous ses enfants dans la
file.
Exercice : implmenter cet algorithme de parcours, et vrifier que la proprit de
parcours en largeur est bien respecte.
Vous avez srement remarqu que les files et les piles sont des structures trs
proches, qui proposent le mme genre
d'oprations (ajouter et enlever des lments). Il est donc naturel de se demander :
que se passe-t-il quand, dans l'algorithme de
parcours en largeur avec une file, on remplace la file par une pile ?
On passe alors d'un rgime premier entr, premier sorti un rgime dernier entr,
premier sorti. Imaginez qu'on vient de
parcourir un noeud : on a ajout tous ses enfants la pile, et maintenant on passe au
noeud suivant parcourir. Quel est le
noeud suivant qui sera pris ? C'est le dernier noeud entr sur la pile, donc un des
enfants du noeud prcdent.
On se retrouve avec un parcours o, quand on a parcouru un noeud, on parcourt
ensuite ses enfants. a ne vous rappelle rien ?
Si (ou alors vous n'avez pas t asssez attentif) : c'est le parcours en profondeur !
On peut donc implmenter le parcours en profondeur exactement comme le parcours
en largeur, en remplaant la file par une pile.
Cela montre que les deux parcours sont trs proches, et qu'il y a des liens trs forts
entre les algorithmes et les structures de
donnes.
Choix de l'implmentation
Analyse de complexit
Les deux parcours ont une complexit en temps linaire en la taille de l'arbre (son
nombre de noeuds) : en effet, on parcourt
chaque noeud une seule fois, et on effectue avant et aprs quelques oprations
(entre et sortie d'une structure) qui sont en
Je parle de O(H) et O(L) ici, mais a ne vous apporte pas grand chose : un arbre N
lments, c'est parlant, mais comment avoir
une ide des valeurs de H et L ? Si vous ne connaissez pas les arbres qu'on va vous
donner, c'est difficile de le savoir. Une
approximation pessimiste est de se dire que H et L sont toujours infrieurs N, le
nombre de noeuds de l'arbre. En effet, dans le
"pire cas" pour H, chaque noeud a un seul enfant, et sa hauteur est donc N (vous
pouvez remarquer que cela correspond
exactement une liste). Dans le "pire cas" pour H, la racine a N-1 enfants, donc H =
N-1 = O(N).
On peut donc dire que les complexits mmoires des parcours d'arbres sont en O(N).
Si j'ai tenu vous parler de H et L, c'est
parce que dans la plupart des cas on peut avoir des estimations plus prcises de
leurs valeurs. En particulier, on verra plus tard
que H est souvent nettement plus petit que N.
Utilisation en pratique
Dans quels cas utiliser plutt un parcours ou l'autre ? Le parcours en profondeur est
le plus simple implmenter (par la
rcursivit), donc si vous avez besoin de parcourir tout l'arbre c'est un bon choix; par
exemple, on peut coder la fonction "taille
d'un arbre" en utilisant un parcours en largeur (il suffit d'incrmenter un compteur
chaque noeud parcouru), mais c'est
nettement plus compliqu (et donc sans doute un peu plus lent) et a n'apporte rien.
De mme, si on veut trouver "tous les noeuds qui vrifient la proprit donne" (par
exemple "leur valeur est suprieure 100"
ou "ils reprsentent une salle du labyrinthe qui contient un trsor"), les deux
mthodes de parcours sont aussi bien, et il vaut
mieux utiliser le parcours en profondeur qui est plus simple. Si on cherche "un noeud
qui vrifie la proprit donne", les deux
parcours sont tous les deux aussi bien.
Il y a un cas cependant o le parcours en largeur est le bon choix : quand on a besoin
de la proprit de "distance la racine" du
parcours en largeur. Pour dvelopper mon exemple prcdent, imaginez que l'arbre
dcrit le plan d'un labyrinthe : l'entre est la
racine, et quand vous tes dans la salle correspondant un noeud, vous pouvez vous
rendre dans les salles enfant (ou remonter
dans la salle parent). Certains noeuds contiennent des trsors; vous voulez que votre
algorithme vous donne, pas la liste des
trsors, pas une seule salle qui contient un trsor, mais le trsor le plus proche de
l'entre (qui est aussi la sortie).
Alors il faut utiliser un parcours en largeur : il va visiter les cases les plus proches de
la racine en premier. Ds que vous aurez
trouv un trsor, vous savez que c'est le trsor le plus proche de l'entre, ou en tout
cas un des trsors les plus proches : il y a
peut-tre d'autres trsors dans la mme couche. Un parcours en profondeur ne
permet pas cela : le premier trsor qu'il trouve
peut tre trs profond dans l'arbre, trs loin dans la racine (imaginez sur notre
exemple qu'il y a un trsor en E et en C).
Pour rsumer, le parcours en profondeur est trs bien pour rpondre aux questions
du style "le nombre total de ...", "la liste des
...", "le plus long chemin qui ...", et le parcours en largeur pour les questions du style
"le plus court chemin qui ..", "le noeud le
plus proche qui ...", "la liste des .. en ordre de distance croissante".
Ce n'est pas fini !
Comme vous l'avez peut-tre devin, le tutoriel est encore en cours de rdaction.
Pour vous tenir en haleine, voici les deux principaux constituants de la troisime
partie (en cours d'criture) : arbres et graphes.
Vous y verrez un autre tri optimal, plus efficace, diffrentes mthodes de recherche
de sortie d'un labyrinthe, etc.
Post Scriptum (t 2011) : en raison de l'absence de modification visible depuis
maintenant assez longtemps, on nous demande
rgulirement si le tutoriel est abandonn. La rponse est non, mais la prparation
de nouveaux chapitres progresse lentement.
En attendant, nous continuons effectuer de petites mises jour pour apporter des
amliorations ou des clarifications, suite aux
retours des lecteurs, qui nous sont donc fort utiles.