Vous êtes sur la page 1sur 28

Chapitre 1

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.

1.1 Qu’est-ce qu’un algorithme?


Bien qu’il n’existe pas de définition universellement admise pour décrire cette notion, il y a un
consensus général au sujet de la signification de ce concept.
Un algorithme est une suite d’instructions non ambiguës pour résoudre un problème, c’est-à-
dire pour obtenir un résultat exigé pour n’importe quelle entrée permise dans un temps fini.
Cette définition peut être illustrée par un simple diagramme (figure 1.1).

Problème

Algorithme

Entrées Ordinateur Sorties

Figure 1.1 – Notion d’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 d’Euclide pour calculer GCD(m, n)


Etape 1. Si n = 0, retourner la valeur de m comme réponse et STOP sinon continuer
à l’Etape 2.
Etape 2. Diviser m par n et affecter la valeur du reste à r.
Etape 3. Affecter la valeur de n à m et la valeur de r à n. Aller à l’Etape 1.

Alternativement, nous pouvons exprimer le même algorithme en pseudocode.

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;
nr
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.

Algorithme de vérification des entiers consécutifs pour le calcul de GCD(m, n)


Etape 1. Affecter à p la valeur de min{m, n}
Etape 2. Diviser m par p. Si le reste de cette division est zéro, alors aller à l’Etape 3,
sinon aller à l’Etape 4.
Etape 3. Diviser n par p. Si le reste de cette division est zéro, retourner la valeur de p
comme réponse et STOP, sinon continuer à l’Etape 4.
Etape 4. Décrémenter la valeur de p. Aller à l’Etape 2.

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.

Procédure des classes intermédiaires pour le calcul de GCD(m, n)


Etape 1. Trouver la décomposition en facteurs premiers de m.
Etape 2. Trouver la décomposition en facteurs premiers de n.
Etape 3. Identifier tous les facteurs communs des décompositions en facteurs premiers
trouvées à l’Etape 1 et à l’Etape 2. Si p est un facteur commun apparaissant
pm fois et pn fois dans m et n, respectivement, il sera répété min{pm, pn} fois.
Etape 4. Calculer le produit de tous les facteurs communs et retourner ce produit
comme le plus grand diviseur commun des nombres m et n.

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é
jj+p
//Copier le reste des éléments de A dans L comme les nombres premiers.
i0
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 ?

1.2. Principes de base de la résolution algorithmique des problèmes

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

Décider sur : les moyens de


calcul, solution exacte ou
solution approximative,
structures de données,
technique de conception

Concevoir un algorithme

Prouver la correction

Analyser l’algorithme

Coder l’algorithme

FIGURE 1.2 – Processus de conception et d’analyse des algorithmes

1.2.1 Comprendre le problème


A partir d’une perspective pratique, la première chose que vous devez faire avant de concevoir un
algorithme est de comprendre complètement le problème à résoudre. Lire attentivement la
description du problème et poser des questions si vous avez des doutes concernant le problème,
exécuter quelques exemples à la main, penser aux cas spéciaux et poser encore des questions si
nécessaire.

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.

1.2.4 Choisir les structures de données appropriées

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.

1.2.5 Techniques de conception des algorithmes

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.

Deuxièmement, les algorithmes constituent la pierre angulaire de l’informatique. Chaque science


est intéressée par la classification de son sujet principal et l’informatique n’est pas une exception.
Les techniques de conception des algorithmes rendent possible la classification des algorithmes
suivant une idée sous-jacente de conception ; par conséquent elles peuvent servir comme un moyen
naturel à la fois pour catégoriser et étudier les algorithmes.

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.

1.2.7 Vérification de la correction d’un 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.

1.2.8 Analyse des algorithmes

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.

ALGORITHME MinDistance(A[0..n – 1])


//Input : un vecteur de nombres
//Output : La distance minimum entre deux de ses plus proches éléments
dmin  
for i  0 to n – 1 do
for j  0 to n – 1 do
if i  j and |A[i] – A[j]|  dmin
dmin  |A[i] – A[j]|
return dmin
Faites autant d’améliorations que vous pouvez dans cette solution algorithmique du problème. (Si
vous voulez aussi, vous pouvez aussi changer l’algorithme ; sinon améliorer l’implémentation
donnée).

1.3 Les principaux types de problèmes


Dans l’océan illimité des problèmes que nous rencontrons dans le domaine du calcul, il y a peu de
domaines qui ont retenu de façon particulière l’attention des chercheurs. De façon générale, l’intérêt
a été guidé par l’importance pratique du problème ou par certaines caractéristiques spécifiques qui
rendent le problème un sujet de recherche intéressant ; heureusement, ces deux sources de
motivation se renforcent mutuellement dans la plus part des cas.
Dans cette section, nous présentons les principaux types de problèmes rencontrés en algorithmique :
 Problèmes de tri
 Problèmes de recherche
 Problèmes de traitement des chaînes
 Problèmes sur les graphes
 Problèmes combinatoires
 Problèmes géométriques
 Problèmes numériques
Ces problèmes sont utilisés dans les chapitres suivants pour illustrer différentes techniques de
conception des algorithmes et les méthodes d’analyse des algorithmes.

1.3.1 Problèmes de tri


Le problème de tri nous demande de réarranger les éléments d’une liste donnée par ordre croissant.
Bien entendu, pour que ce problème soit utile, la nature de éléments de la liste doit permettre un tel
ordre. (Les mathématiciens diront qu’il doit existe une relation d’ordre total). Comme une affaire
pratique, nous avons habituellement besoin de trier des listes de nombres, des caractères d’un

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.

1.3.2 Problèmes de recherche


Le problème de recherche aborde la recherche d’une valeur donnée, appelée clé de recherche, dans
un ensemble donné (ou un multi-ensemble qui permet à plusieurs éléments d’avoir la même valeur).
Il existe de nombreux algorithmes de recherche. Ces algorithmes s’étendent de la recherche
séquentielle à la recherche dichotomique qui est une technique de recherche particulièrement
efficace mais limitée et des algorithmes basés sur la représentation de l’ensemble sous-jacent sous
une forme différente plus appropriée pour la recherche. Ces derniers algorithmes sont d’une

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.

1.3.3 Problèmes de traitement des chaînes


Ces dernières années, la rapide prolifération des applications traitant des données non numériques a
intensifié l’intérêt des chercheurs et des praticiens de l’informatique dans les algorithmes de
traitement des chaînes. Une chaîne est une suite de caractères appartenant à un alphabet. Les
chaînes ayant un intérêt particulier sont les chaînes alphanumériques comprenant des lettres, des
nombres et des caractères spéciaux ; les chaînes de bits comprenant des zéros et des uns et des
chaînes génétiques qui peuvent être modélisées par des chaînes de caractères appartenant à
l’alphabet {A, C, G, T} des caractères génétiques. Il faudra souligner cependant souligner que les
algorithmes de traitement des chaînes ont été importants pour l’informatique pendant longtemps en
conjonction avec les langages de programmation et les problèmes de compilation.
Un problème particulier a attiré une attention particulière des chercheurs. Ils l’ont appelé la
reconnaissance des chaînes (string matching). Plusieurs algorithmes qui exploitent la nature
spéciale de ce type de recherche ont été inventés. Nous introduisons un algorithme très simple au
Chapitre 3 et discutons deux autres algorithmes basés sur l’idée remarquable de R. Boyer et J.
Moore au Chapitre 7.

1.3.4 Problèmes sur les graphes


Un des domaines les plus anciens et les plus intéressants en algorithmique est celui des problèmes
sur les graphes. Informellement, un graphe peut être considéré comme une collection de points
appelés sommets, certains d’entre eux reliés par des segments de droites appelés arcs. Les graphes
sont un sujet intéressant à étudier à la fois pour des raisons théoriques et pratiques. Les graphes
peuvent être utilisés pour modéliser une grande variété d’applications réelles, incluant les réseaux
de transport et de communication, la planification des projets et les jeux. Une application
intéressante récente est l’estimation du diamètre du Web, qui est le nombre maximum de liens que
l’on doit suivre pour atteindre une page Web à partir d’une autre en passant par le chemin le plus
direct entre elles.
Les algorithmes fondamentaux sur les graphes comprennent les algorithmes de parcours des
graphes, les algorithmes du plus court chemin, le tri topologique pour les graphes ayant des arcs
orientés. Heureusement, ces algorithmes peuvent être considérés comme illustrations pour les
techniques générales de conception, par conséquent nous allons les trouver dans les chapitres
correspondants de ce cours.
Certains problèmes sur les graphes sont très difficiles. Les exemples les plus connus sont le
problème du voyageur de commerce et le problème de coloriage des graphes. Le problème du
voyageur de commerce est le problème qui consiste à trouver le plus court chemin qui passent par n
villes en visitant chaque cité exactement une seule fois. En plus des applications évidentes
14
concernant la planification routière, on enregistre des applications modernes comme la fabrication
des chips VLSI, la cristallographique par rayon X et le génie génétique. Le problème du coloriage
des graphes est le problème qui consiste à affecter le plus petit nombre de couleurs aux sommets
d’un graphe tel que deux sommets adjacents n’est pas la même couleur. Ce problème intervient
dans plusieurs applications tel que la programmation des événements : si les événements sont
représentés par des sommets qui sont reliés par un arc si et seulement si les événements
correspondants ne peuvent pas être programmés au même moment, une solution au problème du
coloriage des graphes peut produire une programmation optimale.

1.3.5 Problèmes combinatoires


A partir d’une perspective plus abstraite, le problème du voyageur de commerce et le problème de
coloriage des graphes sont des exemples de problèmes combinatoires. Ce sont des problèmes qui
demandent (explicitement ou implicitement) de trouver un objet combinatoire (une permutation,
une combinaison ou un sous-ensemble) qui satisfait certaines contraintes et a une certaine propriété
désirée (ex : maximise une valeur ou minimise un coût).
A proprement parler, les problèmes combinatoires sont les problèmes les plus difficiles en
informatique, à la fois sur les points de vue théorique et pratique. Leur difficulté tient des faits
suivants. D’abord, le nombre d’objets combinatoires typiquement croit extrêmement vite avec la
taille du problème, atteignant des magnitudes inimaginables même pour des instances de taille
modérée. Secundo, il n’y a pas d’algorithmes connus pour résoudre exactement plusieurs de tels
problèmes en un temps acceptable. Par ailleurs, plusieurs spécialistes en informatique pensent que
de tels algorithmes n’existent pas. Cette conjecture n’a été ni confirmée ni infirmée, et elle demeure
le plus important sujet non résolu en informatique théorique. Nous discutons ce sujet plus en détail
à la section 11.3.
Certains problèmes combinatoires peuvent être résolus des algorithmes efficaces, mais ils seront
considérés comme des exceptions à la règle. Le problème du plus court chemin mentionné
précédemment figure parmi ces exceptions.

1.3.6 Problèmes géométriques


Les algorithmes géométriques traitent des objets géométriques comme les poins, les droites et les
polygones. Les anciens grecs étaient très intéressés par le développement des procédures pour
résoudre une variété de problèmes géométriques incluant les problèmes de construction de simples
formes géométriques (triangle, cercles, etc. avec une règle non graduée et un compas. Ensuite pour
environ 2000 ans, le grand intérêt pour les algorithmes géométriques a disparu, pour être ressuscité
à l’heure des ordinateurs, non plus avec les règles et les compas, juste des bits, des octets et la
bonne vielle ingéniosité humaine. Bien entendu, les personnes d’aujourd’hui sont intéressées par les
algorithmes géométriques pour des applications tout à fait différentes, telles que le graphisme, la
robotique et la tomographie.
Nous étudierons uniquement des algorithmes pour deux problèmes classiques de la géométrique
numérique : le problème de la paire la plus proche et le problème de la houle convexe. Le problème
de la paire la plus proche (closest-pair problem) s’explique lui-même : étant donné n points du plan,
trouver la paire la plus proche d’entre eux. Le problème de la houle convexe (convex-hull problem)
demande de trouver le plus petit polygone convexe qui sera contiendra tous les points d’un
ensemble donné.

1.3.7 Problèmes numériques


Les problèmes numériques, un autre vaste domaine d’applications, sont les problèmes qui
impliquent des objets mathématiques de nature continue ; résoudre des équations et des systèmes
d’équations, calculer des intégrales finies, évaluer les fonctions et ainsi de suite. La majorité de tels

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é :

ALGORITHME ComparisonCountingSort(A[0..n – 1])


//Trie avec vecteur par comparaison des comptes
//Input : Un vecteur A[0..n – 1] de valeurs ordonnables
//Output : Un vecteur S[0..n – 1] formé des éléments de A classés par ordre croissant.
for i  0 to n – 1 do
Count[i]  0
for i  0 to n – 2 do
for j  i + 1 to n – 1 do
if A[i]  A[j]
Count[j]  Count[j] + 1
else Count[i]  Count[i] + 1
for i  0 to n – 1 do
S[Count[i]]  A[i]
return S

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 3. Concevoir un algorithme simple pour le problème de correspondance des chaînes.

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.

a) Formuler ce problème comme un problème de graphe.


b) Ce problème a-t-il une solution? Si vous le croyez, dessiner une telle promenade; si vous ne
croyez pas, expliquer pourquoi et indiquer le plus petit nombre de nouveaux ponts qui
seraient exigés pour rendre une telle promenade possible.
Problème 5. Le jeu Ecossais. Un siècle après la découverte d'Euler, un autre puzzle célèbre—celui-
ci a été inventé par un mathématicien de renommé irlandais Sir William Hamilton (1805-1865)—a
été publié sous le nom du jeu Ecossais. Le jeu a été joué sur un tableau en bois circulaire sur lequel
le graphique suivant a été découpé:

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.

1.4.1 Les structures de données linéaires

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

Item[0] Item[1] … Item[n-1]

FIGURE 1.3 – Un vecteur de n éléments.


On peut accéder à chaque et à tout élément d’un vecteur en un temps constant quel que soit l’endroit
om l’élément en question est localisé. Cette propriété distingue positivement les vecteurs des listes
chaînées. IL est aussi admis que chaque élément d’un tableau occupe la même quantité de mémoire.
Les vecteurs sont utilisés ne variété d’autres structures de données. Parmi elles on trouvé la chaîne,
une suite caractères terminée par un caractère spécial indiquant le fin de la chaîne. Les chaînes
composées de zéro et de un sont appelées des chaînes binaires. Les chaînes sont indispensables
pour le traitement des données textuelles, la définition des langages de compilation e la compilation
des programmes écrits dans ces langages, et l’étude de modèles de calcul abstraits. Les opérations
que effectuons habituellement sur les chaînes diffèrent de celles que nous effectuons sur d’autres
vecteurs (ex : vecteurs de nombres). Elles comprennent le calcul de la longueur d’une chaîne, la
comparaison des chaînes pour déterminer qui précède l’autre dans l’ordre lexicographique et la
concaténation de deux chaînes.
Une liste chaînée est une suite de zéro ou plusieurs éléments appelés nœuds chacun contenant deux
types d’information : des données et une plusieurs liens appelés pointeurs sur d’autres nœuds de la
liste chaînée. (Un pointeur spécial appelé nil est utilisé pour indiquer l’absence de nœud
successeur.) Dans une liste chaînée simple, chaque nœud à l’exception du dernier contient un seul
pointeur sur l’élément suivant de la liste (Figure 1.4).

Item 0 Item 1 … Item n-1

FIGURE 1.4 – Une liste chaînée simple de n éléments.


Pour accéder à un nœud particulier d’une liste chaînée, on commence par la première cellule de la
liste et on traverse la chaîne des pointeurs jusqu’à ce le nœud recherché soit atteint. Ainsi, le temps
nécessaire pour accéder à un élément dans une liste simple, contrairement aux vecteurs, dépend de
la position de l’élément dans la liste. Du côté positif, les listes chaînées de nécessitent pas une
réservation préalable de la mémoire, les insertions et les suppressions peuvent être effectuées assez
efficacement dans une liste chaînée en repositionnant quelques pointeurs appropriés.
Nous pouvons exploiter la flexibilité de la structure des listes chaînées de différentes manières. Par
exemple, il est souvent commode de débuter une liste chaînée par un nœud spécial appelé sentinelle
18
ou en-tête (header). Ce nœud contient souvent des informations sur la liste telles que sa longueur
courante ; il peut aussi contenir, en plus d’un pointeur sur le premier élément, un pointeur sur le
dernier élément de la liste.
Une extension est la structure appelée la liste doublement chaînée, dans laquelle chaque nœud, à
l’exception du premier et du dernier, contient des pointeurs à la fois sur son successeur et son
prédécesseur (Figure 1.5).

Item 0 Item 1 … Item n-1

Figure 1.5 – Une liste doublement chaînée de n éléments.

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.

Vérifier que la pile est vide


Empiler un élément
Dépiler un élément

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.

Vérifier que la queue est vide


Insérer un élément dans la queue
Supprimer un élément dans la queue
Plusieurs applications importantes nécessitent la sélection d’un élément ayant la plus grande priorité
parmi un ensemble dynamiquement changeant de candidats. Une structure de données qui peut
combler les besoins de telles applications est appelée une queue de priorité. Une queue de priorité
est une collection d’éléments appartement à un ensemble totalement ordonné. Les principales
opérations sur une queue de priorité sont la recherche de son plus grand élément, la suppression du

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 ).

Le digraphe de la figure 1.6b a six sommets et huit arcs :

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

FIGURE 1.9 – Un graphe non connexe

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

FIGURE 1.12 – (a) Arbre binaire. (b) Arbre binaire de recherche

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 1 nil 7 nil nil 10 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

c nil g nil nil f nil

nil h nil i 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.

1.4.4 Ensembles et dictionnaires


La notion d’ensemble joue un rôle central en mathématiques. Un ensemble peut être décrit comme
une collection non ordonnée (possiblement vide) d’objets distincts appelés éléments de l’ensemble.
Un ensemble est défini soit en extension soit en compréhension. Les principales opérations sur les
ensembles sont la vérification de l’appartenance d’un élément, le calcul de la réunion de deux
ensembles, et le calcul de l’intersection de deux ensembles.
26
Les ensembles peuvent être implémentés dans les applications informatiques de deux façons. La
première considère uniquement les ensembles qui des sous-ensembles d’un certain grand ensemble
U, appelé ensemble universel. Si l’ensemble U a n éléments, alors un sous-ensemble S de U peut
être représenté par une chaîne de bits de taille n, appelé vecteur binaire, dans lequel le ième
élément est 1 si et seulement si le ième élément de U est inclus dans S. Cette façon de représenter
les ensembles permet d’implémenter très rapidement les opérations ensemblistes classiques mais au
détriment de l’utilisation potentielle de grande quantité de mémoire.
La deuxième façon la plus commune de représenter un ensemble est d’utiliser la structure de liste
pour indiquer les éléments de l’ensemble. Notons cependant les principales différences entre les
ensembles et les listes. D’abord un ensemble ne peut pas contenir des éléments identiques : une liste
peut le faire. Cette exigence de l’unicité est parfois contournée par l’introduction d’un ensemble
multiple (multiset) ou sac, une collection non ordonnée d’éléments qui ne sont pas nécessairement
distincts. Deuxièmement, un ensemble est une collection non ordonnée d’objets, par conséquent
changer l’ordre de ces éléments ne modifie pas l’ensemble. Une liste, définie comme une collection
ordonnée d’éléments est exactement le contraire. Ceci est une importante distinction théorique, mais
heureusement elle n’est pas importante pour plusieurs applications. Il est aussi intéressant de
mentionner que si un ensemble est représenté par une liste ; dépendant e l’application sous la main,
il peut être intéressant de maintenir la liste triée.
En informatique, les opérations que nous avons d’effectuer pour un ensemble ou un sac très souvent
sont la recherche d’un élément donné ; l’ajout d’un nouvel élément et la suppression d’un élément
de la collection. Une structure de données qui implémente ces trois opérations est appelée un
dictionnaire. Par conséquent, une implémentation efficace d’un dictionnaire doit un compromis
entre l’efficacité de la recherche et l’efficacité des deux autres opérations. Il y a très peu de façons
de représenter un dictionnaire. Elles vont d’une utilisation non sophistiquée des vecteurs aux
techniques plus sophistiquées telles que le hachage et les arbres de recherche balancés ; qui nous
étudions dans ce cours.
De nombreuses applications requièrent une partition dynamique d’un ensemble n éléments en une
collection se sous ensembles disjoints. Après avoir été initialisé comme une collection de n sous
ensemble de un élément, la collection est sujette à une suite de réunion mixte et d’opération de
recherche. Ce problème est appelé le problème de la réunion des ensembles (set union problem).
Vous avez du remarquer que dans notre review des structures de données de base, nous avons
presque toujours mentionné des opérations spécifiques qui sont typiquement effectuées pour la
strcuture en question. Cette relation étroite entre les données et les opérations a longtemps été
reconnue par les informaticiens. Elle les a amenés en particulier vers la notion de type de données
abstraites : un ensemble d’objets abstraits représentant des éléments de données avec un ensemble
d’opérations qui peuvent s’appliquer à ces objets. Bien que les types de données abstraits puissent
être implémentés dans des langages procéduraux comme le Pascal, il est plus commode de le faire
dans des langages orientés objets tels que C++ ou Java, qui supportent les types de données abstraits
au moyen des classes.

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 7. Comment pourrez-vous implémenter un dictionnaire de petite taille raisonnable n si


vous savez que tous ses éléments sont distincts ? Spécifier une implémentation de chacune des
opérations du dictionnaire.

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

Vous aimerez peut-être aussi