Académique Documents
Professionnel Documents
Culture Documents
Introduction
Pourquoi avons-nous besoin d’étudier les algorithmes ? Si nous voulons devenir des professionnels
de l’informatique, il y a deux raisons à la fois d’ordre théorique et pratique d’étudier les
algorithmes. D’un point de vue pratique, vous devez connaître un ensemble standard d’algorithmes
relevant de différents domaines de calcul. En plus, vous devez être capable de concevoir de
nouveaux algorithmes et analyser leur efficacité. D’un point de vue théorique, l’étude des
algorithmes a été reconnue comme la pierre angulaire de l’informatique. David Harel, dans son
magnifique ouvrage intitulé opportunément Algorithmics : the Spirit of Computing, déclare :
L’algorithmique est plus qu’une branche de l’informatique. C’est l’âme de l’informatique et
en toute vérité, elle a une grande importance pour la majeure partie de la science, des
affaires et de la technologie (Algorithmics is more than a branch of computer science. It is
the core of computer science, and, in all fairness, can be said to be relevant to most of
science, business, and technology).
Même si vous n’êtes pas étudiant dans un programme associé à l’informatique, il y a des raisons
valables d’étudier les algorithmes. Pour bien le comprendre, les programmes informatiques
n’existeraient pas sans les algorithmes. Avec les applications informatiques qui deviennent
désormais indispensables dans tous les aspects de notre vie professionnelle et personnelle, étudier
les algorithmes devient une nécessité pour davantage de personnes.
Une autre raison d’étudier les algorithmes est leur utilité dans le développement des capacités
d’analyse. En somme, les algorithmes peuvent être considérés comme des types particuliers de
solutions aux problèmes, pas des réponses mais des procédures définies avec précision pour obtenir
des réponses. Par conséquent, certaines techniques spécifiques de conception des algorithmes
peuvent être interprétées comme des stratégies de résolution de problèmes qui peuvent être utiles
indépendamment des considérations informatiques. Bien entendu, la précision inhérente imposée
par la réflexion algorithmique limite le type de problèmes qui peuvent être résolus avec un
algorithme. Vous ne trouverez pas par exemple un algorithme pour vivre une vie heureuse ou pour
devenir riche et fameux. D’un autre côté, cette précision requise a un avantage éducatif important.
Donald Knuth, un des informaticiens de premier plan dans l’histoire des algorithmes, le déclare de
la manière suivante :
A person-well-trained in computer science knows how to deal with algorithms: how to
construct them, manipulate them, analyse them. This knowledge is preparation for much
more than writing good computer programs; it is a general-purpose mental tool that will be
a definite aid to the understanding of other subjects, whether they be chemistry, linguistics,
or music, etc. The reason for this may be understood in the following way: it has often been
said that a person does not really understand something until after teaching it to some
someone else. Actually, a person does not really understand something until after teaching it
to a computer, i.e., expressing it as an algorithm…An attempt to formalize things as
algorithms leads to a much deeper understanding than if we simply try to comprehend
things in the traditional way. [Knu96].
Nous considérons la notion d’algorithme à la Section 1.1. Comme exemples, nous utilisons trois
algorithmes pour le même problème : le calcul du plus grand diviseur commun de deux nombres. Il
existe plusieurs raisons pour ce choix. D’abord, il traite d’un problème familier pour tout le monde
depuis les études élémentaires. Deuxièmement, il souligne le point important que le même
problème peut souvent être résolu par plusieurs algorithmes. Typiquement, ces algorithmes
1
diffèrent dans leur idée, le niveau de sophistication et l’efficacité. Troisièmement, un de ces
algorithmes mérite d’être présenté en premier, à la fois à cause de son ancienneté, sa puissance et
son importance durables. Enfin, la procédure élémentaire pour le calcul du plus grand diviseur
commun nous permet de souligner un besoin critique que chaque algorithme doit satisfaire.
La Section 1.2 aborde le problème de la résolution algorithmique des problèmes. Ici nous discutons
plusieurs questions importantes reliées à la conception et à l’analyse des algorithmes. Les différents
aspects de la résolution algorithmique des problèmes allant de l’analyse du problème et des moyens
de description d’un algorithme à l’établissement de sa correction et à l’analyse de son efficacité. La
section ne comprend pas de recette magique pour concevoir un algorithme pour un problème
arbitraire. C’est un fait bien établi qu’une telle recette n’existe pas. Aussi, le matériel de la Section
1.2 devrait vous être utile pour organiser votre travail de conception et d’analyse des algorithmes.
La Section 1.3 est consacrée à quelques types de problèmes qui se sont montrés particulièrement
importants pour l’étude des algorithmes et leurs applications. En effet, il existe des ouvrages
organisés autour de tels types de problèmes. Mon opinion—partagée par beaucoup d’autres—est
qu’une organisation basée sur des techniques de conception des algorithmes est supérieure. Dans
tous les cas, il est très important de connaître les principaux types de problèmes. Non seulement ils
constituent les types de problèmes les plus couramment rencontrés dans les applications réelles,
mais également ils sont utilisés à travers ce cours pour démontrer des techniques particulières de
conception des algorithmes.
La Section 1.4 contient une review des structures de données fondamentales. Elle est là pour servir
de référence plutôt qu’une discussion délibérée de ce sujet. Si vous avez besoin d’un exposé plus
détaillé, il existe une grande variété de bons ouvrages sur le sujet, la plupart d’entre eux étant
conçus en fonction d’un langage de programmation particulier.
Problème
Algorithme
La référence aux instructions dans la définition implique qu’il existe quelque chose ou quelqu’un
capable de comprendre et de suivre les instructions données. Nous appelons cela un « calculateur »,
en gardant à l’esprit qu’avant l’invention du calculateur numérique, le vocable « calculateur »
désignait un homme impliqué dans l’exécution de calculs numériques. Aujourd’hui bien entendu,
les ordinateurs sont des équipements électroniques qui sont devenus indispensables dans la plus part
des choses que nous faisons. Notons cependant que même si la majorité des algorithmes sont effet
2
conçus pour une éventuelle implémentation informatique, la notion d’algorithme ne repose pas
essentiellement sur une telle assomption.
Comme exemples pour illustrer la notion d’algorithme, nous considérons dans ce paragraphe trois
méthodes différentes pour résoudre le même problème : le calcul du plus grand diviseur commun de
deux entiers naturels. Ces exemples vont nous aider à illustrer plusieurs points importants :
La non ambiguïté exigée pour chaque étape d’un algorithme ne peut jamais être compromise.
La gamme des entrées pour lesquelles un algorithme marche doit être spécifiée attentivement.
Le même algorithme peut être représenté de différentes façons.
Plusieurs algorithmes pour résoudre le même problème peuvent exister.
Des algorithmes pour le même problème peuvent être basés sur des idées différentes et
peuvent résoudre le problème avec des vitesses radicalement différentes.
Rappelons que le plus grand diviseur commun de deux entiers positifs non tous nuls m et n, dénoté
gcd( m, n) , est défini comme le plus grand entier naturel qui divise à la fois m et n. Euclide of
Alexandria a proposé un algorithme pour résoudre ce problème dans l’un des volumes de ses
Elements, plus fameux de sa présentation systématique de la géométrie. En termes modernes,
l’Algorithme d’Euclide est basé sur l’application répétitive de la relation :
gcd( m, n) gcd( n, m mod n)
(où m mod n est le reste de la division euclidienne de m par n) jusqu’à ce que m mod n soit égal à
zéro. Comme gcd( m, 0) m (pourquoi ?), la dernière valeur de m est aussi le plus grand diviseur
commun de m et n.
Voici une description plus structurée de cet algorithme.
ALGORITHME Euclide(m, n)
//Calcul de GCD(m, n) par l’algorithme d’Euclide
// Input : Deux entiers positifs non tous nuls m et n.
//Output : Le plus grand diviseur commun de m et n.
while n 0 do
r m mod n;
m n;
nr
return m
Comment savoir que l’algorithme d’Euclide finit éventuellement par s’arrêter ? Ceci résulte de
l’observation que le deuxième nombre de la paire devient plus petit à chaque itération et ne peut pas
devenir négatif. En effet, la nouvelle valeur de n à l’itération suivante est m mod n, qui est toujours
plus petit que n. Donc, la valeur du deuxième nombre de la paire devient éventuellement zéro et
l’algorithme s’arrête.
3
Comme avec beaucoup d’autres problèmes, il y a plusieurs algorithmes pour calculer le plus grand
diviseur commun. Examinons deux autres méthodes pour ce problème. La première est simplement
basée sur la définition du plus grand diviseur commun de m et n comme le plus grand entier qui
divise les deux nombres. Clairement, un tel diviseur commun ne peut pas être plus grand que le plus
petit de ces nombres, que nous dénotons par p = min{m, n}. Ainsi on commence par vérifier si p
divise les deux nombres, si oui p est la réponse, sinon, on décrémente p et on essaie encore.
Remarquons que contrairement à l’algorithme d’Euclide cet algorithme, dans la forme présentée ici,
ne marche pas correctement lorsque l’un des deux nombres est nul. Cet exemple illustre pourquoi il
est si important de spécifier explicitement et attentivement le domaine des entrées permises d’un
algorithme.
La troisième procédure pour le calcul du plus grand diviseur commun sera familière aux élèves des
classes intermédiaires.
Présentons maintenant un algorithme simple pour générer les nombres premiers consécutifs
inférieurs à un entier donné n. L’algorithme commence par initialiser la liste des nombres premiers
candidats par les entiers consécutifs de 2 à n. Ensuite, à la première itération de l’algorithme, on
élimine de la liste tous les multiples de 2. Ensuite on passe au deuxième élément de la liste qui est 3,
et on élimine tous ses multiples. Le prochain nombre restant dans la liste et qui est utilisé à la
troisième itération est 5. Comme pour 2 et 3, tous les multiples de 5 sont supprimés de la liste.
L’algorithme continue de cette façon jusqu’à ce qu’aucun nombre ne puisse plus être supprimé de la
liste. Les nombres restant de la liste sont les nombres premiers recherchés.
Comme exemple, considérions l’application de cet algorithme pour trouver la liste des nombres
premiers inférieurs ou égaux à n = 24.
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
2 3 5 7 9 11 13 15 17 19 21 23
2 3 5 7 11 13 17 19 23
2 3 5 7 11 13 17 19 23
4
Pour cet exemple, aucun autre passage ne sera plus nécessaire après la suppression des multiples de
5 car tout passage supplémentaire tentera d’éliminer des nombres déjà éliminés lors des itérations
précédentes. Les nombres restants dans la liste sont les nombres premiers consécutifs inférieurs ou
égaux à 24.
En général, quelle est la plus grande valeur de p dont les multiples peuvent encore restant dans la
liste ? Avant de répondre à cette question, notons d’abord que si p est un nombre dont les multiples
sont éliminés au passage courant, alors les premier multiple qui sera considéré est p 2 parce que
tous ses multiples inférieurs ont déjà été éliminés lors des passages précédents. Cette observation
aide à éviter d’éliminer le même nombre plusieurs fois. Clairement, p 2 ne sera pas plus grand que
n, et par conséquent p ne peut pas dépasser la partie entière de n , dénotée n . Nous supposons
dans le pseudocode suivant qu’une fonction est disponible pour calculer ; alternativement, nous
pouvons vérifier l’inégalité p. p n comme condition de condition de continuation de la boucle.
ALGORITHME Sieve(n)
//Implémente le Sieve de Eratosthenes
//Input : Un entier n 2
//Output : Un vecteur L contenant tous les nombres premiers inférieurs ou égaux à n
for p 2 to n do A[ p] p
for p 2 to n do
if A[p] 0
j p*p
while j n do
A[j] 0 //marque un élément comme éliminé
jj+p
//Copier le reste des éléments de A dans L comme les nombres premiers.
i0
for p 2 to n do
if A[p] 0
L[i] A[p]; i i + 1
return L
Ainsi nous pouvons maintenant introduire le crible de Eratosthenes dans une procédure de classe
intermédiaire pour obtenir un algorithme légitime pour le calcul du plus grand diviseur commun de
deux entiers positifs.
Exercices 1.1
Problème 1. Effectuer des recherches sur al-Khorezmi (ou al-Khwarizmi), l’homme de qui provient
le mot « algorithme ». En particulier, vous pourrez apprendre ce que les origines des mots
« algorithme » et « algèbre » ont en commun.
Problème 2. Concevoir un algorithme pour calculer
n pour un entier positif n. En plus des
affectations et des comparaisons, votre algorithme ne peut utiliser que les quatre opérations
arithmétiques de base.
Problème 3. Démontrer l’égalité gcd( m, n) gcd( n, m mod n) pour toute paire d’entiers positifs m
et n.
Problème 4. Que fait l’algorithme d’Euclide pour une paire de nombres dont le premier est plus
petit que le second ? Quel le maximum de fois que ceci peut se produire pendant l’exécution de
l’algorithme sur de telles entrées ?
5
Problème 5. a) Quel est le plus petit nombre de divisions effectuées par l’algorithme d’Euclide
pour toutes les entrées 1 m, n 10
b) Quel est le plus grand nombre de divisions effectuées par l’algorithme d’Euclide pour toutes les
entrées 1 m, n 10 ?
Commençons par rappeler un point important fait dans l’introduction de ce chapitre : Nous pouvons
considérer les algorithmes comme des solutions procédurales aux problèmes.
Ces solutions ne sont pas des réponses mais des instructions spécifiques pour obtenir les réponses.
C’est cette emphase sur des procédures constructives précisément définies qui distingue
l’informatique des autres disciplines. En particulier, ceci la distingue des mathématiques théoriques
dont les praticiens sont typiquement satisfaits juste par la démonstration de l’existence d’une
solution à un problème et possiblement par l’étude des propriétés de la solution.
Nous listons et discutons brièvement une suite d’étapes que l’on peut suivre pour concevoir et
analyser un algorithme (Figure 1.2).
Comprendre le problème
Concevoir un algorithme
Prouver la correction
Analyser l’algorithme
Coder l’algorithme
6
Il existe quelques types de problèmes que l’on rencontre assez souvent dans les applications
informatiques. Nous allons les passer en review dans la section suivante. Si le problème que vous
voulez résoudre se trouve parmi eux, il vous sera possible d’utiliser l’un des algorithmes connus
pour le résoudre. Bien entendu, il est bon de comprendre comment un tel algorithme fonctionne et
connaître ses forces et faiblesses, spécialement si vous avez à choisir entre plusieurs algorithmes
existants. Mais souvent, vous ne trouverez pas un algorithme directement utilisable pour résoudre
vous problème. En ce moment là, vous devriez concevoir le votre en vous appuyant si possible sur
les algorithmes existants et sur les nombreuses techniques de conception des algorithmes que nous
étudierons dans la suite du cours. La suite des étapes indiquée dans cette section pourra vous aider
dans cette tâche excitante mais pas toujours facile.
Une entrée à un algorithme spécifie une instance du problème que l’algorithme résout. Il est très
important de spécifier exactement la gamme des instances que l’algorithme doit traiter. Vous
manquez de le faire, votre algorithme pourra marcher correctement pour une majorité d’entrées
mais échoue sur certaines valeurs limites. Rappeler vous qu’un algorithme correct n’est pas celui
qui marche très souvent, mais celui qui marche correctement pour les entrées légitimes.
Vous ne devrez pas sauter cette première étape du processus de résolution algorithmique des
problèmes car si vous le faites, vous risquez de refaire un travail inutile.
1.2.2 Vérifier les capacités des moyens informatiques
Une fois que vous avez complètement compris le problème, vous avez besoin de vérifier les
capacités du dispositif informatique cible de votre algorithme. La grande majorité des algorithmes
actuellement utilisés sont encore destinés à être programmés sur des machines très semblables à la
machine de von Neumann une architecture de machine proposée par le célèbre mathématicien
Hongrois-Américain John von Neumann. L’essence de cette architecture est captée par ce l’on
appelle la mémoire à accès aléatoire (RAM). Sa principale hypothèse est que les instructions sont
exécutées une après l’autre, une opération à la fois. Par conséquent, les algorithmes conçus pour
être exécutés sur de telles machines sont appelés des algorithmes séquentiels.
La principale hypothèse du modèle de la RAM ne tient pas pour les nouveaux ordinateurs qui
peuvent exécuter des opérations concomitamment i.e., en parallèle. Les algorithmes qui reposent sur
cette capacité sont appelés des algorithmes parallèles. L’étude des techniques de conception et
d’analyse des algorithmes dans le cadre du modèle RAM restera encore longtemps la pierre
angulaire de l’algorithmique.
Pourriez-vous avoir des regrets au sujet de la vitesse et de la quantité de mémoire d’un ordinateur
dont vous disposez ? Si vous concevez un algorithme comme un exercice scientifique, la réponse
est un non qualifié. Comme vous le verrez à la Section 2.1, la plupart des scientifiques
informatiques préfèrent étudier les algorithmes indépendamment de la spécification des paramètres
d’un ordinateur particulier. Si vous concevez un algorithme comme un outil pratique, la réponse
peut dépendre du problème que vous voulez résoudre. Même les ordinateurs que l’on considère
comme lents aujourd’hui sont presque inimaginablement rapides. Par conséquent, dans la plupart
des situations, vous n’avez pas à avoir des regrets qu’un ordinateur est si lent pour la tâche. Il existe
cependant des problèmes importants très complexes par nature, devant traiter de grands volumes de
données ou traitant des applications où le temps est crucial. Dans de telles situations, il est impératif
de faire attention à la vitesse et à la mémoire disponible sur un système informatique particulier.
1.2.3 Choisir entre une solution approximative ou exacte
La prochaine décision est de choisir entre résoudre le problème exactement ou le résoudre
approximativement. Dans le premier cas, un algorithme est appelé un algorithme exact, dans le
dernier cas, un algorithme est appelé un algorithme approximatif. Pourquoi pourra-on opter pour un
7
algorithme approximatif ? Premièrement, il existe des problèmes importants pour lesquels la plupart
des instances ne peuvent pas être résolus exactement; des exemples comprennent le calcul des
racines carrées, la résolution des équations non linéaires et l’évaluation des intégrales définies.
Deuxièmement, les algorithmes disponibles pour résoudre exactement certains problèmes peuvent
être exagérément lents à cause de la complexité intrinsèque du problème. Ceci arrive, en particulier,
pour plusieurs problèmes impliquant un grand nombre de choix ; vous trouverez des exemples de
tels problèmes difficiles aux Chapitres 3 et 8. Troisièmement, un algorithme d’approximation peut
être une partie d’un algorithme plus sophistiqué qui résout exactement un problème.
Certains algorithmes ne demandent pas une ingéniosité quelconque pour représenter leurs entrées.
D’autres par contre sont préconçus sur des structures de données ingénieuses. En outre, certaines
techniques de conception des algorithmes que nous étudierons par la suite dépendent intimement de
la structuration ou de la restructuration des données spécifiant l’instance du problème. Il y a
plusieurs années, un ouvrage remarquable proclamait l’importance fondamentale des algorithmes et
des structures de données pour la programmation informatique à travers son titre [Wir76] :
Algorithmes + Structures de données = Programmes
Dans le nouveau monde de la programmation orienté objets, les structures de données restent
crucialement importantes à la fois pour la conception et l’analyse des algorithmes. Nous passons en
review les structures de données de base à la Section 1.4.
Maintenant que tous les composants de la résolution algorithmique des problèmes sont en place,
comment pouvez-vous concevoir un algorithme pour résoudre un problème donné ? Ceci est la
question principale à laquelle ce cours cherche à répondre en vous enseignant plusieurs techniques
générales de conception. Qu’est-ce qu’une technique de conception ?
Une technique de conception des algorithmes (ou stratégie ou paradigme) est une approche
générale de résolution algorithmique des problèmes qui est applicable à une variété de
problèmes provenant de différents domaines de l’informatique.
Consultez le sommaire de ce cours et vous verrez que la majorité des chapitres sont consacrés aux
techniques individuelles de conception. Elles distillent quelques idées clés qui ont montré leur
utilité dans la conception des algorithmes. Etudier ces techniques est d’une importance capitale pour
les raisons suivantes :
D’abord, elles donnent une ligne directrice pour la conception des algorithmes pour de nouveaux
problèmes, c’est-à-dire des problèmes pour lesquels il n’existe pas d’algorithme satisfaisant. Par
conséquent—pour utiliser le langage d’un proverbe fameux—apprendre de telles techniques est
utile pour apprendre à pêcher au lieu de se donner du poisson pêché par quelqu’un d’autre. Il n’est
pas bien entendu vrai que chacune de ces techniques générales sera nécessairement applicable à
chacun des problèmes que vous pourrez rencontrer. Mais prises ensemble, elles constituent une
collection puissante d’outils que vous trouverez certainement utile dans vos études et votre travail.
8
1.2.6 Méthodes de description des algorithmes
Une fois que vous avez conçu un algorithme, vous avez besoin de le décrire d’une certaine façon.
Dans la Section 1.1, pour vous donner un exemple, nous avons décrit l’algorithme d’Euclide
littéralement (dans une forme libre et aussi dans une forme étape par étape) et en pseudocode. Ce
sont là les deux options qui sont les plus utilisées pour la spécification des algorithmes.
Employer un langage naturel a un appel évident ; cependant, l’ambiguïté inhérente de n’importe
quel langage naturel rend la description succincte et claire des algorithmes étonnamment difficile.
Néanmoins, être capable de la faire est une compétence importante que vous devriez développer
dans votre processus d’apprentissage des algorithmes.
Un pseudocode est un mélange d’un langage naturel et des constructions d’un langage de
programmation. Un pseudocode est habituellement plus précis qu’un langage naturel et son usage
produit souvent des descriptions plus succinctes des algorithmes. Ce qui est surprenant ici est que
les informaticiens ne se sont jamais accordés sur une forme unique de pseudocode, laissant à chaque
auteur d’ouvrage la latitude de définir son propre dialecte. Heureusement, ces dialectes sont assez
proches les uns des autres que toute personne familière à un langage de programmation moderne
sera capable de les comprendre tous.
Le dialecte que nous avons adopté dans ce cours a été choisi pour causer le moins de problème
possible aux lecteurs. Pour des raisons de simplicité, nous omettons les déclarations des variables et
utilisons des indentations pour montrer la portée des instructions telles que for, if et while. Nous
utilisons la flèche pour l’opération d’affectation et deux slash // pour les commentaires.
Dans les premiers jours de l’informatique, le support dominant pour la spécification des algorithmes
était les organigrammes, une méthode d’expression des algorithmes par une collection de figures
géométriques connectées contenant les descriptions des étapes de l’algorithme. Cette technique de
représentation a montré s’est avéré incommode pour tout sauf des algorithmes très simples ; de nos
jours, on la trouve seulement dans de vieux livres d’algorithmique.
L’état de l’art de l’informatique n’a pas encore atteint un point où la description d’un algorithme, en
langage naturel ou en pseudocode, peut être entrée directement dans ordinateur. En effet, cette
description a besoin d’être convertie en un programme informatique écrit dans un langage de
programmation donné. Nous pouvons considérer un tel programme informatique comme une autre
manière de spécification des algorithmes, bien qu’il soit préférable de le considérer comme
l’implémentation de l’algorithme.
Une fois qu’un algorithme a été spécifié, vous devez montrer sa correction. C’est-à-dire que vous
vous devez montrer que l’algorithme produit le résultat désiré pour toutes les entrées légitimes en
un temps fini. Par exemple, la correction de l’algorithme d’Euclide pour le calcul du plus grand
diviseurs communs de deux nombres entiers repose sur la correction de l’égalité
gcd( m, n) gcd( n, m mod n) (qui à son tour doit être prouvée ; voir Problème 6 dans les exercices),
la simple observation que le deuxième nombre devient de plus en plus petit à chaque itération de
l’algorithme, et le fait que l’algorithme s’arrête lorsque le second membre est zéro.
Pour certains algorithmes, une preuve de correction est assez facile ; pour d’autres, elle peut être
tout à fait complexe. Une technique simple pour montrer la correction d’un algorithme consiste à
utiliser une induction mathématique parce que les itérations d’un algorithme offrent une suite
naturelle d’étapes nécessaires pour de telles preuves. Il peut être intéressant de mentionner que bien
que suivre la performance d’un algorithme pour quelques entrées spécifiques puisse être une
activité très valable, il ne peut pas prouver la correction de l’algorithme d’une manière concluante.
9
Mais dans le but de qu’un algorithme est incorrect, vous avez besoin juste d’une instance de son
input pour lequel l’algorithme échoue. Si l’algorithme est trouvé incorrect, vous devez soit le
reconcevoir avec les mêmes décisions concernant les structures de données, la technique de
conception ou dans le cas extrême reconsidérer une ou plusieurs de ces décisions.
La notion de correction pour des algorithmes d’approximation est moins évidente que les
algorithmes exacts. Pour algorithme d’approximation, on préfèrera souvent être capable de montrer
que l’erreur produite par l’algorithme ne dépasse pas une limite prédéfinie.
Nous voulons habituellement que nos algorithmes possèdent plusieurs qualités. Après la correction,
la qualité de loin la plus importante est l’efficacité. En effet, on distingue deux types d’efficacité des
algorithmes : l’efficacité temporelle et l’efficacité mémoire. L’efficacité temporelle indique avec
quelle vitesse l’algorithme s’exécute. L’efficacité mémoire indique combien de mémoire
l’algorithme demande. Un cadre général et des techniques spécifiques pour l’analyse de l’efficacité
des algorithmes est donné au Chapitre 2.
Une autre caractéristique désirable est la simplicité. Contrairement à l’efficacité, qui peut être
définie précisément et étudiée avec une rigueur mathématique, la simplicité, comme la beauté, se
trouve à un degré considérable dans les yeux du propriétaire. Par exemple, plusieurs personnes
accepteront que l’algorithme d’Euclide est plus simple que la procédure élémentaire de calcul du
plus grand diviseur commun, mais il n’est pas clair si l’algorithme d’Euclide est plus simple que
l’algorithme de test des entiers consécutifs. En outre, la simplicité est une caractéristique importante
des algorithmes qu’il faut essayer d’obtenir. Pourquoi ? Parce que plus simples sont les algorithmes
plus facile il est de les comprendre et plus facile il est de les programmer. Par conséquent, les
programmes résultants contiennent généralement peu de bogues. Il y a également l’aspect
esthétique indéniable de la simplicité. Malheureusement, il n’est pas toujours facile de savoir dans
quel cas un compromis judicieux doit être effectué.
Une autre caractéristique désirable d’un algorithme est la généralité. Il y a en effet, deux aspects
ici : la généralité du problème que l’algorithme résout et la gamme des entrées qu’il accepte. Dans
le premier cas, notons q’il est parfois facile de concevoir un algorithme pour un problème posé dans
des termes plus généraux. Considérons par exemple le problème de la détermination si deux entiers
sont premiers entre eaux. Il est plus facile de concevoir un algorithme pour un problème plus
général du calcul du plus grand diviseur commun de deux entiers et résoudre le problème posé en
vérifiant que le GCD est égale à un ou non. Il y a cependant des situations où concevoir un
algorithme plus général est inutile ou difficile ou même impossible. Par exemple, il est inutile de
trier une liste de n nombres pour trouver sa médiane qui est son n / 2 ième plus petit élément. Pour
donner un autre exemple, la formule standard des racines d’une équation quadratique ne peut pas
être généralisée pour traiter des polynômes de degré arbitraire.
Comme pour la gamme des entrées, votre principale préoccupation est de concevoir un algorithme
qui traite une gamme d’entrées qui est naturelle pour le problème considéré. Par exemple, exclure
les entiers égaux à 1 comme entrées possibles de l’algorithme du plus grand diviseur commun sera
naturellement peu naturel. D’un autre côté, bien que la formule classique des racines d’une équation
quadratique tient pour les coefficients complexes, nous ne devons normalement pas l’implémenter à
ce degré de généralité à moins que cette capacité soit explicitement demandée.
Si vous n’êtes pas satisfait par l’efficacité de l’algorithme, la simplicité ou la généralité, vous devez
revenir en arrière et reconcevoir l’algorithme. En fait, même si votre évaluation est positive, il est
toujours inutile de chercher d’autres solutions algorithmiques. Rappelons les trois algorithmes
différents de la section précédente pour le calcul du plus grand diviseur commun ; généralement
vous n’allez espérer avoir le meilleur algorithme à la première tentative. Dans le meilleur des cas,
10
vous allez essayer d’affiner un algorithme que vous avez déjà. Par exemple, nous avons effectué
plusieurs améliorations de notre implémentation du Sieve de Erastosthenes comparé à la version
initiale donnée à la Section 1.1. Vous ferrez mieux vous garder à l’esprit l’observation suivante de
Antoine de Saint-Exupéry, l’écrivain français, pilote et concepteur d’avions : « Un concepteur sait
qu’il est arrivé à la perfection non quand il n’y a plus quelque chose à ajouter, mais quand il n’y a
plus quelque chose à enlever. »
1.2.9 Codage des algorithmes
La plupart des algorithmes sont destinés à être ultimement implémenté comme programmes
informatiques. Programmer un algorithme présente à la fois un péril et une opportunité. Le péril se
trouve dans la possibilité de rendre le passage d’un algorithme à un programme soit incorrect soit
très inefficace. Certains informaticiens croient fermement que même si la correction d’un
algorithme est établis avec toute la rigueur mathématique, le programme ne pas être considéré
comme correct. Ils ont développé des techniques spéciales pour faire de telles preuves, mais la
puissance de ces techniques de vérification formelle est limitée jusque là à de très petits
programmes. Comme autre considération pratique, la validité des programmes est encore établie au
moyen de tests. Tester les programmes informatiques est un art plutôt qu’une science, mais cela ne
signifie pas qu’il n’y a rien apprendre de cela.
Notons aussi que tout au long de notre cours, nous supposons que les entrées des algorithmes
appartiennent aux ensembles spécifiés et ne nécessitent par conséquent pas de vérification. Quand
vous implémenterez des algorithmes comme des programmes devant être utilisés dans des
applications réelles, vous devrez prévoir de telles vérifications.
Exercices 1.2
Problème 1. Puzzle du vieux monde. Un berger se trouve sur une rive avec un loup, une chèvre et
une tête de chou. Il doit faire traverser chacun des trois protagonistes de l'autre côté de la rivière au
moyen d’une barque. La barque étant petite pour les transporter tous, à chaque traversée de la
rivière, il ne peut emporter qu’un seul des trois protagonistes. On ne peut laisser la chèvre et le chou
(resp. le loup et la chèvre) seuls sur une rive. Comment doit faire le berger pour faire traverser les
trois protagonistes sous les contraintes indiquées. (Note: Le berger est un végétarien mais n'aime
pas le chou et par conséquent ne peut pas manger ni la chèvre ni le chou pour l'aider à résoudre le
problème. Et il va de soi que le loup est une espèce protégée).
Problème 2. Puzzle du nouveau monde. Il y a quatre personnes qui veulent traverser un pont; elles
commencent toutes du même côté. Vous avez 17 minutes pour les faire toutes traverser de l'autre
côté du pont. C'est la nuit et elles ont une lampe-torche. Un maximum de deux personnes peuvent
traverser le pont en même temps. Chaque partie qui traverse, que ce soit une ou deux personnes,
doit avoir la lampe-torche avec elle. La lampe-torche doit être amenée dans les deux sens; elle ne
peut pas être jetée, par exemple. La personne 1 prend 1 minute pour traverser le pont, la personne 2
prend 2 minutes, la personne 3 prend 5 minutes et la personne 4 prend 10 minutes. Une paire doit
marcher ensemble au rythme de la personne la plus lente. Par exemple, si la personne 1 et la
personne 4 doivent d'abord traverser, 10 minutes se seront écoulées quand ils atteindront l'autre côté
du pont. Si la personne 4 ramène la lampe-torche, un total de 20 minutes seront passées et vous
aurez échoué la mission.
Problème 3. Lesquelles des formules suivantes peuvent être considérées comme un algorithme
pour le calcul de la surface d’un triangle dont les longueurs des côtés sont des nombres positifs a, b
et c ?
a) S p( p a)( p b)( p c) , où p (a b c) / 2
1
b) S bc sin A , où A est l’angle entre les côtés b et c
2
11
1
c) S aha , où ha est la hauteur de base a.
2
Problème 4. Décrire l’algorithme classique pour trouver la représentation binaire d’un nombre
entier positif :
a) en Français
b) en pseudo code
Problème 5. Donnez un exemple de problème autre le calcul du plus grand diviseur commun pour
lequel vous connaissez plusieurs algorithmes. Lequel est le plus simple ? Le plus efficace ?
Problème 6. Considérons l’algorithme suivant pour trouver la distance est les éléments plus
proches dans un vecteur de nombres.
12
alphabet, des chaînes de caractères et des enregistrements plus importants semblables à ceux utilisés
par les facultés au sujet de leurs étudiants, les librairies au sujet de leurs ouvrages, et les entreprise
au sujet de leurs employés. Dans le cas des enregistrements, nous devons choisir un élément
d’information pour guider le tri. Par exemple, nous pouvons choisir de trier les enregistrements des
étudiants par ordre alphabétique des noms, par les matricules ou par les moyennes des notes des
étudiants. Un tel élément d’information choisi spécialement est appelé une clé.
Pourquoi pouvons-nous avoir besoin d’une liste triée ? Eh bien, le tri rend plusieurs questions
concernant les listes faciles à répondre. Le plus important de ces questions est la recherche ; c’est
pour cela que les dictionnaires, les répertoires téléphoniques, les listes des classes et ainsi de suite
sont triés. Vous verrez d’autres exemples de l’utilité des listes triées à la Section 6.1. Dans le même
esprit, le tri est utilisé comme une étape auxiliaire dans plusieurs algorithmes importants dans
d’autres domaines, par exemple les algorithmes géométriques.
Pour le moment, les informaticiens ont découvert des dizaines d’algorithmes de tri différents. En
effet, inventer un nouvel algorithme de tri a été comparé l’invention de la roue proverbiale. Je suis
cependant heureux de révéler que la recherche de meilleurs algorithmes de tri se poursuit. Cette
persévérance est admirable au vu des faits suivants. D’une part, il y a très peu de bons algorithmes
de tri qui trient un vecteur arbitraire de taille n avec environ n log 2 n comparaisons. D’autre part,
aucun algorithme qui trie par des comparaisons clés (par opposition à la comparaison à de petits
éléments de clé) ne peut par faire substantiellement mieux que cela.
Il y a des raisons pour cette embarrassante richesse algorithmique dans le domaine du tri. Bien que
certains algorithmes soient en effet meilleurs que d’autres, il n’y a aucun algorithme qui sera la
meilleure solution dans toutes les solutions. Certains algorithmes sont simples mais relativement
lents tandis que d’autres sont plus rapides mais plus complexes. Certains marchent mieux sur des
entrées ordonnées aléatoirement tandis que d’autres font mieux sur des listes presque triées.
Certains sont appropriés pour des listes résidant dans la mémoire rapide tandis que d’autres peuvent
être adaptés pour trier de gros fichiers stockés sur disque et ainsi de suite.
Deux propriétés des algorithmes de tri méritent une mention particulière. Un algorithme de tri est
dit stable s’il préserve l’ordre relatif de deux éléments égaux de la liste d’entrée dans la liste triée.
En d’autres termes, si une liste contient deux éléments égaux dans les positions i et j avec i j, alors
dans la liste triée ils doivent se trouver respectivement dans des positions i’ et j’ tels que i’ j’.
Cette propriété peut être désirable si, par exemple, nous avons une liste d’étudiants triés par ordre
alphabétique et nous voulons les trier selon le GPA de l’étudiant : un algorithme stable produira une
liste dans laquelle les étudiants ayant le même GPA seront encore triés alphabétiquement.
Généralement parlant, les algorithmes qui peuvent échanger des clés éloignés ne sont pas stables
mais sont habituellement plus rapides.
La deuxième propriété importante d’un algorithme de tri est la quantité de mémoire additionnelle
que l’algorithme nécessite. Un algorithme de tri est dit interne s’il ne nécessité pas de mémoire
additionnelle en plus de celle occupée par la liste d’entrée, à l’exception possiblement de quelques
unités de mémoire. Il existe d’importants algorithmes de tri qui sont internes et d’autres qui ne le
sont pas.
13
importance particulière pour les applications réelles parce qu’ils sont indispensables pour le
stockage et la recherche des informations dans des bases de données.
Pour la recherche aussi, il n’y a pas un seul algorithme qui s’adapte mieux à toutes les situations.
Certains algorithmes marchent plus vite que d’autres mais nécessitent plus de mémoire, certains
sont plus rapides mais applicables seulement à des vecteurs triés, etc. Contrairement aux
algorithmes de tri, il n’y a pas de problème de stabilité, mais différents sujets émergent.
Spécifiquement, dans les applications où les données sous-jacentes peuvent changer fréquemment
par rapport au nombre de recherches, la recherche doit être considérée en conjonction avec deux
autres opérations : insertion et suppression d’un élément dans l’ensemble de données. Dans de telles
situations, les structures de données et les algorithmes devront être choisies pour réduire la balance
entre les exigences de chaque opération. Aussi, l’organisation de grands ensembles de données pour
une recherche efficace pose de challenges particuliers avec des implications importantes pour des
applications réelles.
15
problèmes mathématiques ne peuvent être résolus que approximativement. Une autre principale
difficulté tient du fait que de tels problèmes requièrent typiquement la manipulation de nombres
réels, qui ne peuvent être représentés dans la machine que approximativement. Par ailleurs, un
grand nombre d’opérations arithmétiques effectuées sur des nombres représentés
approximativement peuvent conduire à une accumulation des erreurs d’arrondi à un point où elles
peuvent drastiquement une sortie produite par un algorithme apparemment juste.
Plusieurs algorithmes sophistiqués ont été développés pendant des années dans ce domaine, et ils
continuent de jouer un rôle critique dans plusieurs applications scientifiques et d’ingénierie. Mais au
cours des 25 dernières années, l’industrie de l’informatique a déplacé son intérêt dans le domaine
des applications de gestion. Ces nouvelles applications nécessitent principalement des algorithmes
pour le stockage, la recherche et la transmission des informations à travers des réseaux, et leur
présentation aux utilisateurs. Comme conséquence de ce changement révolutionnaire, l’analyse
numérique a perdu sa position dominante d’antan à la fois dans l’industrie et les programmes
informatiques. Il est toutefois toujours important pour tout débutant en informatique d’avoir au
moins une idée rudimentaire sur les algorithmes scientifiques.
Exercices 1.3
Problème 1. Considérons l’algorithme de tri qui trie un vecteur en comptant, pour chacun de ses
éléments, le nombre d’éléments qui lui sont plus petits et ensuite utilise cette information pour
placer l’élément dans sa position finale dans le vecteur trié :
a) Appliquer cet algorithme pour trier la liste 60, 35, 81, 98, 14, 47.
b) Cet algorithme est-il stable ?
c) Est-il interne ?
Problème 2. Nommez les algorithmes de recherche que vous connaissez déjà. Donnez une
description succincte de chacun des ces algorithmes en Français. (Si vous ne connaissez pas de tels
algorithmes, profitez pour en concevoir un)/
Problème 4. Les ponts de Königsberg. Le puzzle des ponts de Königsberg est universellement
accepté comme problème qui a donné naissance à la théorie des graphes. Il a été résolu par le grand
mathématicien d’origine Suisse Léonard Euler (1707-1783). Le problème demandait si on pourrait,
16
en une seule promenade, traverser chacun des sept ponts de la cité de Königsberg exactement une
fois et revenir au point de départ. Ci-dessous un croquis du fleuve avec ses îles et ses sept ponts.
Trouvez un circuit Hamiltonien, un chemin qui visite tous les sommets du graphe exactement une
fois avant de revenir au sommet de départ, pour ce graphe.
Problème 6. On considère la carte suivante :
b
a
c d
e f
a) Expliquer comment vous pouvez utiliser la problème de coloriage des graphes pour colorier
la carte tel que deux régions voisines ne soient pas coloriées par la même couleur.
b) Utiliser la réponse de la question (a) pour colorier la carte avec le plus petit nombre de
couleurs.
Problème 7. Concevoir un algorithme pour le problème suivant : étant donné un ensemble de n
points dans un plan cartésien, déterminer si tous ces points sont situés sur la même circonférence.
1.4 Les structures de données fondamentales
Comme la majorité des algorithmes d’intérêt opèrent sur des données, des moyens particuliers
d’organiser les données jouent un rôle critique dans la conception et l’analyse des algorithmes. Une
17
structure de données peut être définie comme une façon particulière d’organiser des items de
données apparentés. La nature des items de données est dictée par le problème sous la main ; ils
peuvent aller des types de données élémentaires aux structures de données. Il y a très peu de
structures de données qui se sont montrées particulièrement intéressantes pour les algorithmes
informatiques. Comme vous êtes doute familiers à toutes ou presque de ces structures de données,
juste une révision rapide est donnée ici.
Les structures de données linéaires les plus importantes sont les vecteurs et les listes chaînées. Un
vecteur est une suite de n éléments de même type de données et qui sont stockés des cases
consécutives de la mémoire de l’ordinateur et rendu accessibles en spécifiant la valeur de leur
indice dans le vecteur (Figure 1.3).
Dans la majorité des cas, l’indice est un entier compris entre 0 et n – 1 ou entre 1 et n. Certains
langages de programmation autorisent un indice pouvant varier entre deux e
Le vecteur et la liste chaînée deux principaux choix pour représenter une structure de données plus
abstraite appelée liste linéaire ou liste simple. Une liste est une suite finie d’éléments de données,
i.e., une ensemble d’éléments de données disposés dans un certain ordre. Les opérations de base
effectuées sur cette structure de données sont la recherche, l’insertion et la suppression d’un
élément.
Deux types spéciaux de listes, les piles et les queues, sont particulièrement importants.
DEFINITION. Une pile est une liste chaînée dans laquelle toutes les insertions et les suppressions
se font toujours à la fin de la liste. Cette fin est communément appelé sommet de la pile de telle
sorte qu’une pile est toujours représentée verticalement. Lorsque les éléments sont ajoutés (empilés)
dans une pile et supprimés de la pile (dépilés), cette structure opère selon le modèle « dernier-
arrivé-premier-sorti » ou « dernier-entré-premier-sorti » ou « list-in-first-out » (LIFO). Les piles ont
une multitude d’applications ; en particulier, elles sont indispensables pour implémenter des
algorithmes récursifs.
DEFINITION.
Une queue est une liste dans laquelle toutes les insertions se font la fin de la liste et toutes les
suppressions en tête de liste. Par conséquent, une queue opère suivant le modèle du « premier-
arrivé-premier-sorti » ou « premier-entré-premier-sorti » ou « first-in-first-out » (FIFO). Les queues
ont également des applications importantes incluant plusieurs algorithmes pour les problèmes des
graphes.
19
plus grand élément et l’ajout d’un nouvel élément. Bien entendu, une queue de priorité doit être
implémentée de telle sorte que ces deux dernières opérations produisent une autre queue de priorité.
Une implémentation directe de cette structure peut être basée sur un vecteur ou sur un vecteur
ordonné, mais aucune de ces options ne produit la solution le plus efficace. Une meilleure
implémentation d’une queue de priorité est basée sur une structure de données ingénieuse appelée le
tas (heap).
1.4.2 Les graphes
DEFINITION. Un graphe un couple G = (V, E) où V est un ensemble fini non vides d’objets
appelés sommets ou nœuds et E un ensemble de paires non ordonnées de sommets appelés arcs. Ces
paires de sommets ne sont pas ordonnées, c’est-à-dire que la paire de sommets (u, v) est identique à
la paire (v, u), on dit les sommets u et v sont adjacents et qu’ils sont reliés par un arc non orienté (u,
v). Les sommets u et v sont appelés les extrémités de l’arc (u, v) et on dit que u et v sont incidents à
cet arc ; on dit également que l’arc (u, v) est incident à ses extrémités.
Si une paire de sommets (u, u) n’est équivalente à la paire (v, u), on dit que l’arc (u, v) est orienté à
partir du sommet u appelé queue au sommet v appelé tête. On dit aussi que l’arc (u, v) sortit du
sommet u et entre dans le sommet v. Un graphe dont tous les arcs sont orientés est appelé un graphe
orienté. Les graphes orientés sont aussi appelés des digraphes.
Il est commode d’étiqueter les sommets d’un graphe ou d’un digraphe avec des lettres, des entiers
ou si l’application le recommande, des chaînes de caractères (figure 1.6). Le graphe de la figure
1.6a a six sommets et sept arcs :
V a, b, c, d , e, f , E (a, c), (a, d ), (b, c), (b, f ), (c, e), (d , e), (e, f ).
V a, b, c, d , e, f , E (a, c), (b, c), (b, f ), (c, e), (d , a), (d , e), (e, c), (e, f ).
a c b a c b
d e f d e f
Notre définition d’un graphe n’interdit pas des boucles, ou arcs reliant des sommets à eux-mêmes.
Sauf précision contraire, nous allons considérer des graphes sans boucles. Comme notre définition
interdit des arcs multiples entre les mêmes sommets d’un grande non orienté, nous avons l’inégalité
suivante pour le nombre d’arcs possibles E dans un graphe non orienté ayant V sommets et ne
contenant pas de boucles :
0 E V V 1/ 2.
Un graphe dans lequel chaque sommet est relié à chaque autre sommet par un arc est dit complet.
Une notation standard pour dénoter un graphe complet de V sommets est K V . Un graphe dans
lequel quelques arcs seulement manquent est dit dense. Un arc ayant très peu d’arcs par rapport au
nombre des ses sommets est dit éparse. Le fait que nous travaillons avec un graphe dense ou éparse
20
peut influencer la façon de le représenter, et par conséquent le temps d’exécution de l’algorithme en
conception ou utilisé.
Représentation des graphes. Les graphes pour les algorithmes informatiques peuvent être
représentés de deux principales façons : la matrice d’adjacence et les listes d’adjacence. La matrice
d’adjacence d’un graphe de n sommets est une matrice binaire A d’ordre n ayant une ligne et une
colonne pour sommet du graphe, dans laquelle A[i, j ] 1 s’il existe un arc entre les sommets i et j,
et A[i, j ] 0 sinon. Par exemple la matrice d’adjacence du graphe de la figure 1.6a est donnée à la
Figure 1.7a. On remarquera la matrice d’adjacence d’un graphe non orienté est toujours symétrique,
c’est-à-dire que A[i, j ] A[ j, i], 0 i, j n 1 (why ?)
Les listes d’adjacence d’un graphe ou d’un digraphe constituent une collection de listes chaînées,
une pour chaque sommet, contenant tous les sommets adjacents au sommet de la liste (i.e. tous les
sommets reliés à lui par un arc). Habituellement, de telles listes commencent par une sentinelle
identifiant un sommet pour lequel la liste est compilée. Par exemple, la Figure 1.7b représente le
graphe de la Figure 1.6a à l’aide de ses listes d’adjacence. En d’autres termes, les listes d’adjacence
indiquent les colonnes de la matrice d’adjacence qui, pour un sommet donné, contient des uns.
a b c d e f a c d
a 0 0 1 1 0 0
0 b c f
b 0 1 0 0 1
c a b e
c 1 1 0 0 1 0
d a e
d 1 0 0 0 1 0
e 0 0 1 1 0 1 e c d f
f 0 1 0 0 1 0 f b e
(a) (b)
FIGURE 1.7 – (a) Matrice d’adjacence et (b) listes d’adjacence du graphe de la figure 1.6a
Si le graphe est creux, la représentation sous forme de listes d’adjacence pourra utiliser moins
d’espace que la représentation sous forme de matrice d’adjacence en dépit de la mémoire
supplémentaire utilisée par les pointeurs des listes chaînées ; la situation est exactement l’opposé
pour les graphes denses. En général, le choix de la représentation la plus commode dépend de la
nature du problème, de l’algorithme utilisé pour le résoudre, et possiblement du type du graphe
d’entrée (dense ou creux).
Graphes pondérés. Un graphe (ou digraphe) est un graphe (ou digraphe) dans lequel chaque arc est
affecté d’un poids numériques. Ces nombres sont appelés des poids ou des coûts. Un intérêt pour de
tels graphes est motivé par de nombreuses applications réelles, tels que trouver le plus court chemin
entre deux points dans un réseau de transport ou de télécommunication ou le problème du voyageur
de commerce mentionné précédemment.
a b c d
a 5 b a 5 1 a b,5 c,1
5 7 0
1 4 b b a,5 c,7 d,4
7 c 1 7 2 c a,1 b,7 d,2
c d d 4 2
d b,4 c,2
2
(a) (b) (c)
21
FIGURE 1.8 – (a) Graphe pondéré. (b) Sa matrice d’adjacence. (c) Ses listes d’adjacence.
Les deux principales représentations des graphes peuvent être facilement adaptées pour
correspondre aux graphes pondérés. Si un graphe pondéré est représenté sous forme de matrice
d’adjacence, alors l’élément A[i, j ] contiendra simplement le poids de l’arc reliant le sommet i au
sommet j si un tel arc existe et un symbole spécial, par exemple , sinon. Une telle matrice est
appelée une matrice pondérée ou une matrice de coûts. Les listes d’adjacence d’un graphe pondéré
doivent incluse dans leurs nœuds non seulement les noms des nœuds adjacents mais aussi le poids
de l’arc correspondant.
Chemins et cycles. Parmi les propriétés intéressantes des graphes, deux sont importantes pour un
grand nombre d’application : la connectivité et la cyclicité. Les deux sont basées sur la notion de
chemin. Un chemin d’un sommet u à un sommet v d’un graphe G peut être défini comme la suite de
tous les sommets adjacents (reliés par un arc) commençant par u et se terminant par v. si tous les
sommets d’un chemin sont distincts, le chemin est dit simple. La longueur d’un chemin est le
nombre total de sommets contenu dans la suite définissant le chemin moins un, qui est identique au
nombre d’arcs contenus dans le chemin.
Un chemin orienté est une suite de sommets dans laquelle chaque paire de sommets est reliée par
un arc orienté reliant le sommet listé en premier au sommet liste en deuxième.
Un graphe est dit connexe si pour chaque paire de sommets u et il existe un chemin allant de u à v.
Informellement, cette propriété signifie que si nous faisons un modèle de graphe connexe en
connectant des boules représentant les sommes du graphe avec des chaînes représentant les arcs, on
aura une pièce. Si un graphe n’est pas connexe, un tel modèle consistera en plusieurs pièces
connectées appelées composantes connexes du graphe. Formellement, une composante connexe est
un sous-graphe maximal (non extensible via une inclusion d’un sommet extra) d’un graphe. Par
exemple, les graphes des Figure 1.6a et 1.8b sont connexes, tandis le graphe de la Figure 1.9 a deux
composantes connexes ayant les sommets {a, b, c, d, e} et {f, g, h, i}, respectivement.
a f
b c e g h
a i
Des graphes ayant plusieurs composantes connexes n’arrivent pas dans des applications réelles. Un
graphe représentant un système d’autoroute reliant plusieurs pays de l(union européenne sera un
exemple (why ?).
Il est important de savoir pour plusieurs applications si un graphe en considération comprend ou
non des cycles. Un cycle est un chemin de longueur positive commençant et se terminant par un
même sommet et ne traversant un même sommet plusieurs fois. Un graphe qui ne contient pas de
cycle est dit acyclique. Nous étudions les graphes acycliques dans la section suivante.
22
1.4.3 Les arbres
Un arbre (ou plus précisément un arbre libre) est un graphe connexe acyclique (Figure 1.10a). Un
graphe qui n’a pas de cycle mais qui n’est pas nécessairement connexe est appelé une forêt (Figure
1.10b).
Les arbres ont plusieurs propriétés importantes que les graphes n’ont pas. En particulier, le nombre
d’arcs dans un arbre est toujours inférieur au nombre de ses sommets :
E V 1.
Comme le montre la graphe de la figure 1.9, cette propriété est nécessaire mais pas suffisante pour
qu’un graphe soi un arbre. Cependant, pour des graphes connexes elle est suffisante et donc offre un
moyen commode de déterminer si un graphe connexe a un cycle.
a b a b h
c d c d e i
f g f g j
(a) (b)
Figure 1.10 – (a) Un arbre. (b) Une forêt
Arbres avec racine. Une autre propriété importante des arbres est le fait que pour deux nouds dans
un arbre, il existe toujours exactement un chemin simple de l’un de ces nœuds à l’autre. Cette
propriété rend possible le choix d’un sommet arbitraire dans un arbre et le considérer comme la
racine. Un arbre avec racine est toujours représenté en plaçant sa racine au dessus (niveau 0 de
l’arbre), les sommets adjacents à la racine en dessous (niveau 1), les sommets deux arcs plus loin de
la racine en dessous de cela (niveau 2), et ainsi de suite. La figure 1.11 présente une telle
transformation d’un arbre libre en un arbre avec racine.
Les arbres avec racine jouent un rôle important en informatique, un rôle plus important que celui
des arbres libres. En effet, pour être bref, ils sont souvent considérés comme des arbres simples. Les
applications évidentes de arbres sont pour la description des hiérarchies, à partir des fichiers de
dictionnaires aux organigrammes des entreprises. Il y a beaucoup d’applications évidentes ; telles
que l’implémentation des dictionnaires, le stockage efficace de grands ensembles de données et le
codage des données. Les arbres sont aussi utiles pour l’analyse des algorithmes récursifs. Pour finir
cette liste largement incomplète des applications des arbres, nous pourrions mentionner les arbres
état-espace (state-space trees) qui soustendent deux techniques importantes de conception des
algorithmes : le backtracking et le branch-and-bound.
Pour chaque arc v d’un arbre T, tous les sommets sur le chemin simple allant de la racine à ce
sommet sont appelés des ancêtres de v. Le sommet lui-même est souvent considéré comme son
propre ancêtre ; l’ensemble des ancêtres qui excluent le sommet lui-même sont considérés comme
23
des ancêtres propres. Si (u, v) est le dernier arc du chemin simple allant de la racine à un sommet v
(et u v ), u est appelé le parent de v et v est un fils de u ; les sommets qui ont le même parent sont
appelés des cousins (siblings). Un somment sans fils est appelé une feuille ; un sommet ayant au
moins un fils est appelé parental. Tous les sommets pour lesquels un sommet v est un ancêtre sont
appelés des descendants de v ; les descendants propres excluent le sommet v lui-même. Tous les
descendants d’un sommet v ayant tous les arcs les reliant à un sous-arbre de T prennent leur racine à
ce sommet. Ainsi, pour l’arbre de la Figure 1.11b, la racine de l’arbre est a ; les sommets d, g, f, h et
i sont des feuilles, tandis que les sommets a, b, e et c sont parentaux, les sommets du sous-arbre de
racine b sont {b, c, g, h, i}
La profondeur d’an sommet v est la longueur du chemin simple allant de la racine à v. La hauteur
d’un arbre est la longueur du chemin simple le plus long allant de la racine à une feuille. Ainsi, si
nous comptons les niveaux d’un arbre en commençant avec 0 pour le niveau de la racine, la
profondeur d’un sommet est simplement son niveau dans l’arbre, et la hauteur de l’arbre est le
niveau maximum de ses sommets.
i d a
c b a e b d e
h g c g f
f
h i
(a) (b)
FIGURE 1.11 – (a) Arbre libre. (b) Sa transformation en un arbre avec racine.
Arbres ordonnés. Un arbre ordonné est un arbre avec racine dans lequel tous les fils de chaque
nœud sont ordonnés. Il est commode de supposer que dans le diagramme d’un arbre, tous les fils
sont ordonnés de la gauche vers la droite. Un arbre binaire peut être défini comme un arbre
ordonné dans lequel chaque sommet n’a pas plus de deux fils et chaque fils est désigné soit comme
un fils gauche soit comme un fils droit de son parent. Le sous-arbre ayant sa racine au fils gauche
(droite) d’un sommet est appelé le sous-arbre gauche (droit) de ce sommet. Un exemple d’arbre
binaire est donné à la figure 1.12a.
Dans la figure 1.12b, des numéros sont attribués aux sommets de l’arbre binaire de la Figure 1.12a.
Noter que un numéro affecté à chaque sommet parental est plus grand que tous les numéros dans
son sous-arbre gauche et plus petit que tous les numéros dans son sous-arbre droit. De tels arbres
sont appelés des arbres binaires de recherche. Les arbres binaires et les arbres binaires de
recherche ont une grande variété d’applications en informatique. En particulier, les arbres binaires
de recherche peuvent être généralisés à des types plus généraux d’arbres de recherche appelés arbre
de recherche multivoies, qui sont indispensables pour le stockage efficace de très grands fichiers
sur disque.
Comme nous le verrons plus tard, l’efficacité des algorithmes les plus efficaces pour les arbres
binaires de recherche et leurs extensions dépend de la hauteur de l’arbre. Par conséquent, les
24
inégalités suivantes pour la hauteur h d’un arbre binaire ayant n sommets sont spécialement
importantes pour l’analyse de tels algorithmes :
log 2 n h n 1.
Un arbre binaire est habituellement implémenté pour des objectifs de calcul par une collection de
nœuds correspondant aux sommets de l’arbre. Chaque nœud contient des informations associées au
sommet et deux pointeurs sur des nœuds représentant le fils gauche et le fils droit du sommet,
respectivement. La figure 1.23 illustre une telle implémentation pour l’arbre binaire de recherche de
la figure 1.12b.
5 12
1 7 10
Une représentation informatique d’un arbre ordonné arbitraire peut être effectuée en donnant
simplement à chaque sommet parental un nombre de pointeurs égal au nombre de ses fils. Cette
représentation peut être inappropriée si le nombre de fils varie grandement parmi les nœuds. Nous
pouvons éviter cet inconvénient en utilisant des nœuds ayant juste deux pointeurs, comme pour les
arbres binaires. Ici cependant, le pointeur gauche pointera sur le premier fils du sommet, tandis que
le pointeur droit pointera sur son cousin suivant. Par conséquent, cette représentation est appelée la
représentation premier fils-premier cousin (first child-next sibling representaion). Donc, tous les
cousins d’un sommet (à travers les pointeurs droits des nœuds) sont chaînées dans une seule liste
chaînée, le premier élément de la liste étant pointé par le pointeur gauche de leur parent. La figure
1.14a illustre cette représentation pour l’arbre de la figure 1.11b.
5 12 nil
nil 4 nil
25
FIGURE 1.13 – Implémentation standard de l’arbre binaire de recherche de la Figure 1.12b
Il n’est pas difficile de voir que cette représentation transforme effectivement un arbre ordonné en
un arbre binaire dit être associé à l’arbre ordonné. On obtient cette représentation en tournant les
pointeurs de 45° dans le sens des aiguilles d’une montre (Figure 1.14b).
a nil
b nil d e nil
(a)
c d
h g e
i f
(b)
FIGURE 1.14 – (a) Représentation premier fils-prochain cousin du graphe de la Figure 1.11b. (b)
Sa représentation sous forme d’arbre binaire.
Exercices 1.4
Problème 1. Décrire comment on peut implémenter chacune des opérations suivantes sur un
vecteur tel que le temps que ça prend ne dépende pas de la taille n du vecteur.
a) Supprimer le ième élément d’un vecteur ( 1 i n )
b) Supprimer le ième élément dans un vecteur trié (le vecteur restant doit bien entendu rester
trié)
27
Problème 2. Si vous résolvez le problème de recherche dans une liste de n nombres, comment
pouvez-vous prendre avantage du fait que la liste est connu être trié ? Donnez des réponses séparées
pour :
a) les listes représentées sous forme de vecteur.
b) les listes représentées sous forme de liste chaînée.
Problème 3. a) Montrer le contenu de la pile après chacune des opérations de la séquence suivante
en commençant par la pile vide :
push(a), push(b), pop, push(c), push(d), pop
b) Montrer le contenu de la queue après chacune des opérations de la séquence suivante en
commençant par la queue vide :
enqueue(a), enqueue(b), dequeue, enqueue(c), enqueue(d), dequeue
Problème 4. a) Soit A la matrice d’adjacence d’un graphe non orienté. Expliquez quelle propriété
de la matrice indique que :
i. Le graphe est complet
ii. Le graphe a une boucle, c’est-à-dire un arc reliant un sommet à lui-même.
iii. Le graphe a un sommet isolé, c’est-à-dire un sommet n’ayant pas d’arc incident.
b) Répondre aux mêmes questions pour la représentation sous forme de listes d’adjacence.
Problème 5. Donnez une description complète d’un algorithme qui transforme un arbre libre en un
arbre dont la racine se trouve en un sommet donné de l’arbre libre.
Problème 6. Indiquez comment le type de données abstrait queue de priorité peut être implanté
comme
a) un vecteur (non trié)
b) un vecteur trié
c) un arbre binaire de recherche.
Problème 8. Vérification des anagrammes. Concevoir un algorithme pour vérifier si deux mots
donnés sont des anagrammes, c’est-à-dire un des mots peut être obtenu en permutant les lettres de
l’autre.
28