Vous êtes sur la page 1sur 88

F.

BEN-NAOUM
Maı̂tre de Conférences A

Université Djilali Liabès, département d’informatique

Algorithmique et Structures de
Données 3
AVANT-PROPOS

Cet ouvrage s’adresse aux étudiants de la deuxième année licence en informatique. Il


comporte les notions de base en algorithmique avancé que doit acquérir l’étudiant et qu’il
retrouvera tout au long de son cursus.

L’objectif est de développer la capacité à définir et à manipuler les structures de données


abstraites des plus simples (linéaires) aux plus complexes (arborescence et graphe).

L’une des notions importantes invoquées tout au long de cet ouvrage concerne le calcul de
la complexité des algorithmes. Le but essentiel a été donc de montrer l’impact du choix des
structures de données sur la complexité.

Un autre aspect est la sensibilisation à la notion de preuve et qualité des algorithmes.

Ce cours ayant été conçu avec un souci constant de pédagogie et la volonté de rendre les
concepts de l’algorithmique accessibles à chacun, je souhaite que tout étudiant en ayant fait
la consultation puisse y trouver les réponses à ses interrogations.

Farah BEN-NAOUM
Table des matières

Table des matières 1

1 Complexité Algorithmique 4
1.1 Introduction à l’algorithmique . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.1.1 Qu’est ce que l’algorithmique . . . . . . . . . . . . . . . . . . . . . . . 4
1.1.2 Le langage algorithmique . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.1.3 Récursivité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.2 Qualités et caractéristiques d’un algorithme . . . . . . . . . . . . . . . . . . . 7
1.3 Définition de la complexité algorithmique . . . . . . . . . . . . . . . . . . . . . 8
1.4 Calculs de la complexité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.4.1 Détails . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.4.2 Calculs élémentaires de complexité et rappels mathématiques . . . . . 11
1.4.3 Règles du calcul de complexité “en pire des cas” . . . . . . . . . . . . . 12
1.5 Exemples de calculs de complexités . . . . . . . . . . . . . . . . . . . . . . . . 16
1.5.1 Complexité liéaire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.5.2 Complexité constante . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.5.3 Complexité logarithmique . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.5.4 Complexité quadratique . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.5.5 Complexité au mieux, au pire et en moyenne . . . . . . . . . . . . . . . 18
1.5.6 Complexité exponentielle . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.5.7 Des problèmes sans solution . . . . . . . . . . . . . . . . . . . . . . . . 21
1.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22

2 Algorithmes de Tri 26
2.1 Présentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.2 Tri à Bulles (Bubble-Sort) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.2.1 Principe et Algorithme . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.2.2 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.2.3 Complexité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.3 Tri par sélection (Selection-sort) . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.3.1 Principe et algorithme . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.3.2 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.3.3 Complexité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.4 Tri par insertion (Insertion-sort) . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.4.1 Principe et algorithme . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.4.2 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.4.3 Complexité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32

1
2.5 Tri fusion (Merge-sort) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.5.1 Principe et Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.5.2 Algorithme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.5.3 Complexité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
2.6 Tri rapide (quick-sort) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2.6.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2.6.2 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
2.6.3 Complexité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
2.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38

3 Les Arbres 40
3.1 Notion d’Arbres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.2 Arbre binaire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.2.1 Définitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.2.2 Primitives de consultation . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.2.3 Primitives de construction/modifications . . . . . . . . . . . . . . . . . 41
3.2.4 Parcours . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.2.5 Arbres binaires particuliers . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.2.6 Représentation d’un arbre quelconque sous forme d’un arbre binaire . . 43
3.2.7 Implémentations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.3 Arbre général . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
3.3.1 Primitives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
3.3.2 Parcours . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
3.3.3 Implémentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
3.4 Arbre binaire de Recherche . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
3.4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
3.4.2 Recherche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.4.3 Ajout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.4.4 Suppression . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
3.4.5 Arbres binaires de recherche équilibrés . . . . . . . . . . . . . . . . . . 58
3.5 Structure de données Tas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
3.5.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
3.5.2 Implémentation d’un arbre binaire (quasi-)parfait . . . . . . . . . . . . 63
3.5.3 Tri pas tas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
3.5.4 Ajout d’un élément dans le tas . . . . . . . . . . . . . . . . . . . . . . . 63
3.5.5 Epluchage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
3.5.6 Exemple de tri par TAS . . . . . . . . . . . . . . . . . . . . . . . . . . 65

4 Les graphes 71
4.1 Introduction aux graphes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
4.1.1 Définitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
4.1.2 Graphes particuliers . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
4.2 Représentation d’un graphe . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
4.2.1 Matrice d’adjacence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
4.2.2 Matrice d’incidence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
4.2.3 Liste d’adjacence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
4.3 Parcours de graphes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76

2
4.3.1 Parcours en largeur (Cas des graphes orienté) . . . . . . . . . . . . . . 76
4.3.2 Parcours en profondeur (Cas des graphes non orienté) . . . . . . . . . . 77
4.4 Algorithme de Dijkstra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79

3
Chapitre 1

Complexité Algorithmique

1.1 Introduction à l’algorithmique


1.1.1 Qu’est ce que l’algorithmique
Un algorithme est une “spécification d’un schéma de calcul sous forme d’une suite finie
d’opérations élémentaires obéissant à un enchaı̂nement déterminé”, ou encore “la description
des étapes à suivre pour réaliser un travail”. Il s’agit donc de fournir la solution à un problème
dont :
– La première étape consiste à analyser le problème, c’est-à-dire en cerner les limites et
le mettre en forme dans un langage descriptif appelé algorithme.
– L’étape suivante consiste à traduire l’algorithme dans un langage de programmation
spécifique, il s’agit de la phase de programmation.
– Le programme est ensuite transformé en langage machine lors d’une étape appelée
compilation. La compilation est une phase réalisée par l’ordinateur lui-même grâce à
un autre programme appelé compilateur.
– La phase suivante s’appelle l’édition de liens, elle consiste à lier le programme avec tous
les éléments externes (généralement des librairies auxquelles il fait référence).

4
Un programme est donc la description d’un algorithme dans un langage accepté par
la machine. Un algorithme structuré à travers un langage algorithmique, à l’inverse d’un
programme, est indépendant du langage de programmation (et donc de la machine). Un
algorithme peut toutefois aboutir à plusieurs programmes.

1.1.2 Le langage algorithmique


Les composants du langage Algorithmique sont les suivants :

1.1.2.1 Les variables, les constantes et les types


Les variables : Un algorithme manipule des données qui sont stockées dans des variables.
Chaque variable possède un nom, donné par celui qui écrit l’algorithme. Elle possède égale-
ment un emplacement mémoire, ou adresse dans la mémoire de l’ordinateur, qui est décidé(e)
(calculé) automatiquement par le compilateur lorsque l’algorithme sera devenu un programme
que l’on compilera. Une variable est caractérisée par le triplet (nom, type, valeur).
Les constantes : entités dont la valeur ne change pas durant tout l’algorithme, elles sont
caractérisées par (nom, valeur).
Les types : caractérisés par (nom, ensemble de valeurs).
Type de données de base : entiers (12, 732), caractères (’a’, ’g’, ’3’), réels (12.3,-5), chaı̂nes
(de caractères), booléens (vrai ou faux).

1.1.2.2 Les instructions


L0 af f ectation : variable ← expression

Lecture d0 une donnée : Lire(une variable)

Af f ichage d0 une expression : Ecrire(liste d0 expressions, de variables ou de constantes)

Sélection si : si expression − logique alors


instruction(s) à ef f ectuer si l0 expression est vraie
finsi

Sélection si − sinon : si expression − logique alors


instruction(s) à ef f ectuer si l0 expression est vraie
sinon instruction(s) à ef f ectuer si l0 expression est f ausse
finsi

Itération répéter : repeter


instruction(s) à répéter
jusqu0 à expression − logique

Itération tant − que : Tant que expression − logique


instruction(s) à répéter
fintantque

5
Itération pour : Pour variable ← val init à val f in faire
instruction(s) à répeter
finpour

1.1.2.3 Ecriture d’un algorithme


On donnera un nom à tout algorithme, afin de pouvoir le réutiliser. Il est essentiel de préciser
(s’il y en a) : les variables en entrée (les données du problème), les variables en sorties (les
résultats), les variables à la fois en entrée et en sorties (les données modifiées par l’algo) et les
variables locales (utilisées juste dans cet algo). Préciser les variables veut aussi dire préciser
leurs types.
Pour les procédures et fonctions les variables autres que les variables locales sont les para-
mètres formels de l’algorithme. Lorsqu’un autre algo appelle cet algo, c’est avec des para-
mètres effectifs.
Exemple :
Algorithme Bonjour()
// simple ecriture d’un message de bienvenue
debut
ecrire(”Bonjour”)
fin
Algo Multiplication(entier x, entier y) : retourne un entier
//la multiplication des deux entiers donnes x et y en n’effectuant que des additions
entiers i, result
debut
result ← 0
pour i ← 1 à y faire
result ← result + x
finpour
retourner result
fin
Algo Principal() // algo qui appelle les deux précédents
locales : a, b : entiers
debut
Bonjour()
ecrire (”donnez deux entiers”)
lire(a, b)
ecrire (” leur multiplication donne : ”, Multiplication(a, b) )
fin

1.1.3 Récursivité
Un algorithme est dit récursif lorsqu’il s’appelle lui même. Attention, la récursivité peut être
cachée si par exemple un algorithme A appelle un algorithme B qui appelle l’algorithme A.
Un exemple d’algorithme récursif :

6
algo Somme(a, b : entiers) : retourne un entier
// a et b étant deux entiers positifs ou nuls, calcul de a + b suivant le principe :a + b =
a + (b − 1) + 1
debut
si b = 0 alors retourner a
sinon tmp ←Somme(a, b − 1) // appel récursif
retourner tmp + 1
finsi
fin
Regardons les appels sur l’exemple de Somme(2,3) :
appel de Somme(2,3) :
- on a les valeurs des variables a=2 et b=3
- provoque l’appel de Somme(2,2)
- la on a les valeurs des variables a=2 et b=2
- provoque l’appel de Somme(2,1)
- la on a les valeurs des variables a=2 et b=1
- provoque l’appel de Somme(2,0)
- la on a les valeurs des variables a=2 et b=0
- donc on retourne 2
- donc tmp reçoit 2
- donc l’appel de Somme(2,1) retourne 3
- donc tmp reçoit 3
- donc l’appel de Somme(2,2) retourne 4
- donc tmp reçoit 4
- donc retourne 5

Comment écrire un algorithme récursif :


– exprimer le problème de ”taille” n en fonction du même problème de taille(s) inférieur(s)
– mettre une condition d’arrêt : lorsque le problème est de taille suffisamment petite pour
être résolu sans appel récursif. Il faut penser à tous les cas d’arrêts (on peut constater
sur les exemples que c’est le cas à chaque fois).
Remarque : Un algorithme récursif est plus lent qu’un algorithme itératif car il y a la gestion
des appels de fonctions (empilement et dépilement du contexte).

1.2 Qualités et caractéristiques d’un algorithme


L’algorithme est un moyen pour le programmeur de présenter son approche du problème
à d’autres personnes. Un algorithme doit donc être :
– lisible : l’algorithme doit être compréhensible même par un non-informaticien.
– de haut niveau : l’algorithme doit pouvoir être traduit en n’importe quel langage de
programmation, il ne doit donc pas faire appel à des notions techniques relatives à un
programme particulier ou bien à un système d’exploitation donné.
– précis : chaque élément de l’algorithme ne doit pas porter à confusion, il est donc
important de lever toute ambiguı̈té.

7
– concis : un algorithme ne doit pas dépasser une page. Si c’est le cas, il faut décomposer
le problème en plusieurs sous-problèmes.
– structuré : un algorithme doit être composé de différentes parties facilement identi-
fiables.
D’où on peut extraire les qualités suivantes :

– Qualité d’écriture : un algorithme doit être structuré, indenté, modulaire, avec des
commentaires pertinents, etc. Il faut pouvoir comprendre la structure d’un coup d’œil
rapide, et pouvoir aussi revenir dessus 6 mois plus tard et le comprendre encore.
– Terminaison : le résultat doit être atteint en un nombre fini d’étapes. Cela peut être
réalisé grâce aux techniques de preuves de terminaison de la théorie des programmes.
– Validité : le résultat doit répondre au problème demandé. Attention, un jeu d’essais
ne prouve jamais qu’un programme soit correct. Il peut seulement prouver qu’il est
faux. Parmi les méthodes de preuve de validité d’un algorithme : invariant de boucle,
méthodes des assertions (Hoare, Floyd).
– Performance : étude du coût (complexité) en temps et en mémoire.
De ce fait, la seule écriture d’un pseudo-code ne suffit certes pas à valider un algorithme.
Nous distinguons les points fondamentaux à vérifier impérativement :
– Preuve de Terminaison : s’assurer que l’algorithme se termine en un temps fini.
– Preuve de validité : vérifier qu’il donne toujours le bon résultat.
– Analyse de la complexité : on ne peut concevoir l’écriture d’un algorithme sans
essayer de donner une idée de son coût algorithmique.

Exemple de preuve de Terminaison : Algorithme de Tri insertion Algo TRI-


INSERTION(T : tab[1..l]d’entiers) : retourne le tableau T
1 pour j ← 2 à l faire
2 tmp ← T [j] ;
3 i ← j − 1;
4 tant que T [i] > tmp et i > 0 faire
5 T [i + 1] ← T [i] ;
6 i ← i − 1;
7 T [i + 1] ← tmp ;
8 retourner T ;
finTRI-INSERTION ;
La boucle Tant que ligne 4 s’achève toujours (elle contient au plus l − 1 itérations). De
même, la boucle pour j ← 2 à l faire produit l=1 itérations. Donc, l’algorithme se termine
après un nombre fini d’étapes.

1.3 Définition de la complexité algorithmique


La théorie de la complexité algorithmique s’intéresse à l’estimation de l’efficacité des
algorithmes (en terme de mémoire et de temps d’exécution). La complexité en mémoire
(c’est à dire la place mémoire prise par l’algorithme) est un problème de moins en moins
primordial vu les capacités techniques actuelles. L’efficacité des algorithmes s’attache alors à

8
la question : entre différents algorithmes réalisant une même tâche, quel est le plus rapide et
dans quelles conditions ? on parle dans ce cas de complexité temporelle.
La complexité temporelle (ou par abus complexité) d’un algorithme est le nombre d’opé-
rations élémentaires (affectations, comparaisons, opérations arithmétiques) effectuées par un
algorithme. Ce nombre s’exprime en fonction de la taille n des données. On s’intéresse au coût
exact quand c’est possible, mais également au coût moyen (que se passe-t-il en moyenne sur
toutes les exécutions de l’algorithme sur des données de taille n), au cas le plus favorable, ou
bien au cas le pire. On dit que la complexité de l’algorithme est O(f (n)) où f est d’habitude
une combinaison de polynômes, logarithmes ou exponentielles. Ceci reprend la notation ma-
thématique classique, et signifie que le nombre d’opérations effectuées est borné par cf (n),
où c est une constante, lorsque n tend vers l’infini.
Considérer le comportement à l’infini de la complexité est justifié par le fait que les données
des algorithmes sont de grande taille et qu’on se préoccupe surtout de la croissance de cette
complexité en fonction de la taille des données. Une question systématique à se poser est :
que devient le temps de calcul si on multiplie la taille des données par 2 ? De cette façon, on
peut également comparer des algorithmes entre eux.
Les algorithmes usuels peuvent être classés en un certain nombre de grandes classes de
complexité.
– Les algorithmes sub-linéaires, dont la complexité est en général en O(log n). C’est le
cas de la recherche d’un élément dans un ensemble ordonné fini de cardinal n.
– Les algorithmes linéaires en complexité O(n) ou en O(n log n) sont considérés comme
rapides, comme l’évaluation de la valeur d’une expression composée de n symboles ou
les algorithmes optimaux de tri.
– Plus lents sont les algorithmes de complexité située entre O(n2 ) et O(n3 ), c’est le cas
de la multiplication des matrices et du parcours dans les graphes.
– Au delà, les algorithmes polynomiaux en O(nk ) pour k > 3 sont considérés comme
lents, sans parler des algorithmes exponentiels (dont la complexité est supérieure à tout
polynôme en n) que l’on s’accorde à dire impraticables dès que la taille des données est
supérieure à quelques dizaines d’unités.
La recherche de l’algorithme ayant la plus faible complexité, pour résoudre un problème
donné, fait partie du travail régulier de l’informaticien. Il ne faut toutefois pas tomber dans
certains excès, par exemple proposer un algorithme excessivement alambiqué, développant
mille astuces et ayant une complexité en O(n1,99 ), alors qu’il existe un algorithme simple et
clair de complexité O(n2 ). Surtout, si le gain de l’exposant de n s’accompagne d’une perte
2
importante dans la constante multiplicative : passer d’une complexité de l’ordre de n2 à une
complexité de 1010 n log n n’est pas vraiment une amélioration. Les critères de clarté et de
simplicité doivent être considérés comme aussi importants que celui de l’efficacité dans la
conception des algorithmes.

9
En résumé voici la nomenclature des classes de complexité :

Notation Type de complexité


O(1) complexité constante (indépendante de la taille de la donnée)
O(log(n)) complexité logarithmique
O(n) complexité linéaire
O(n log(n)) complexité quasi-linéaire
O(n2 ) complexité quadratique
O(n3 ) complexité cubique
O(np ) complexité polynomiale
O(n log(n)) complexité quasi-polynomiale
O(2n ) complexité exponentielle
O(n!) complexité factorielle
Et voici le grahe représentant la différence entre leurs croissances respectives lorsque n tend
vers l’infini :

1.4 Calculs de la complexité


1.4.1 Détails
Pour qu’une analyse ne dépende pas de la vitesse d’exécution de la machine ni de la
qualité du code produit par le compilateur, il faut utiliser comme unité de comparaison des
opérations élémentaires en fonction de la taille des données en entrée. Voici quelques exemples
d’opérations élémentaires :
– accès à une cellule mémoire ;
– comparaison de valeurs ;
– opérations arithmétiques (sur valeurs à codage de taille fixe) ;
– opérations sur des pointeurs.
Il faut souvent préciser quelles sont les opérations élémentaires pertinentes pour le problème
étudié.

10
On définit alors la taille de la donnée sur laquelle s’applique chaque problème par un entier lié
au nombre d’éléments de la donnée. Par exemple, le nombre d’éléments dans un algorithme
de tri, le nombre de sommets et d’arcs dans un graphe.
On évalue le nombre d’opérations élémentaires en fonction de la taille de la donnée : si n est
la taille, on calcule une fonction t(n).
Les critères d’analyse : le nombre d’opérations élémentaires peut varier substantiellement
pour deux données de même taille. On retiendra trois critères pour un algorithme A :
– La complexité au sens du meilleur cas :
InfA (n) est le temps d’exécution du meilleur cas et le minimum des coûts sur toutes
les données de taille n : InfA (n) = inf {coutA (d)/d de taille n}.
– analyse au sens du plus mauvais cas :
SupA (n) est le temps d’exécution du plus mauvais cas et le maximum sur toutes les
données de taille n : SupA (n) = sup{coutA (d)/d de taille n}.
– analyse au sens de la moyenne :
comme le “plus mauvais cas” peut en pratique n’apparaı̂tre que très rarement, on
étudie M oyA (n), l’espérance sur l’ensemble des temps d’exécution, où chaque entrée
d a une certaine probabilité p de se présenter. L’analyse mathématique de la com-
plexité moyenne est souvent délicate. De plus, la signification de la distribution des
probabilités parPrapport à l’exécution réelle (sur un problème réel) est à considérer :
M oyA (n) = p(d) ∗ cout(d) .
d de taille n

On étudie systématiquement la complexité asymptotique, notée grâce aux notations de Lan-


dau :
– idée 1 : évaluer l’algorithme sur des données de grande taille. Par exemple, lorsque n
est “grand”, 3n3 + 2n2 est essentiellement 3n3 ;
– idée 2 : on élimine les constantes multiplicatrices, car deux ordinateurs de puissances
différentes diffèrent en temps d’exécution par une constante multiplicatrice. De 3 × n3 ,
on ne retient que n3 .
L’algorithme est dit en O(n3 ).
L’idée de base est donc qu’un algorithme en O(na ) est “meilleur” qu’un algorithme en O(nb )
si a < b.
Les limites de cette théorie :
– le coefficient multiplicateur est oublié : est-ce qu’en pratique 100 × n2 est “meilleur” que
5 × n3 ?
– l’occupation mémoire, les problèmes d’entrées/sorties sont occultés ;
– dans les algorithmes numériques, la précision et la stabilité sont primordiaux.
Point fort : c’est une notion indispensable pour le développement d’algorithmes efficaces.

1.4.2 Calculs élémentaires de complexité et rappels


mathématiques
- Sommations :
n
P
– i = n(n + 1)/2
i=0
n
a.ri = a(rn+1 − 1)/(r − 1)
P

i=0

11
- Logarithmes : Pour b > 1, x > 0, y est le logarithme en base b de x si et seulement si by
= x (noté logb x = y).
Pour b et c > 1 :
– logb ba = a
– logb (x.y) = logb (x) + logb (y)
– logb (xa ) = a.logb (x)
– logc (x) = logb (x)/logb (c)
– ∀n entier strictement positif, ∃k tel que 2k 6 n < 2k+1 .
- Propriétés sur les fonctions :
– Si f et g sont deux fonctions positives réelles, on écrit f = O(g) si et seulement si le
rapport f /g est borné à l’infini Autrement dit, f ne croı̂t pas plus vite que g.
– Autres notations : f = Θ(g) si f = O(g) et g = O(f ).
– Les règles de calcul simples sur les O sont les suivantes (n’oublions pas que nous tra-
vaillons sur des fonctions de coût, qui sont à valeur positive) : si f = O(g) et f 0 = O(g 0 ),
alors f + f 0 = O(g + g 0 ), f × f 0 = O(g × g 0 ).
n
– On montre également facilement que si f = O(nk ) et h = f (i), alors h = O(nk+1 )
P
i=1
(approximer la somme par une intégrale).

1.4.3 Règles du calcul de complexité “en pire des cas”


Nous essayons ici de fixer des règles pour aider à l’évaluation de la complexité en temps ou
en nombre d’opérations d’un algorithme. Notons TA (n) le temps ou le nombre d’opérations,
“en pire des cas” correspondant à la suite d’actions A, ou au calcul de l’expression A. Une
suite d’actions est considérée ici comme une action non élémentaire.
Règle-1 : Enchaı̂nement
Soient deux suites d’actions A1 et A2 , et A1 + A2 , la suite “A1 suivi de A2 ”. Alors :

TA1 +A2 (n) = TA1 (n) + TA2 (n) .

Règle-2 : Conditionnelle
Soit une action A de la forme “Si C Alors A1 Sinon A2 FinSi” Alors :

TA (n) = TC (n) + M ax(TA1 (n), TA2 (n)).

En effet, dans le pire des cas, c’est toujours la plus coûteuse des deux actions qui s’exécute.
Exemple :

12
Pour i ← 1 à N faire
Res ← X + Y + Z + Res
Si T [i] + K < B alors
{Action1}
Pour j ← 1 à N faire
Res ← Res + T [j]
FinPour
Sinon
{Action2}
Res ← Res + T [i]
FinSi
FinPour
Soit Op(n) le nombre d’additions “en pire des cas”, avec N = n. Notons Opc(i, n) le nombre
d’additions dans la structure “Si........Sinon...FinSi” à la ième itération de la boucle externe,
Op1 (i, n) le nombre d’additions dans Action1 et Op2 (i, n) le nombre d’additions dans Ac-
tion2 : n
P
Op(n) = (3 + Opc(i, n))
i=1
Opc(i, n) = 1 + M ax(Op1 (i, n), Op2 (i, n)) = 1 + M ax(n, 1) = 1 + n
n
Op(n) = (4 + n) = n2 + 4n
P
i=1
Op(n) ∼ n2 donc Op(n) est en O(n2 ).

Règle-3 : Itération (TantQue)
Soit une action A de la forme “TantQue C Faire A1 FinTantQue”. En notant niter(n) le
nombre d’itérations, on a :
niter(n)
P
TA (n) = (TC (i, n) + TA1 (i, n)) + TC (niter(n) + 1, n).
i=1

Il est intéressant de noter que :


– La condition C est exécutée dans le “TantQue” une fois de plus que le “corps” A1 de la
boucle.
– TC n’est pas toujours une constante, mais peut dépendre de n et du rang i de l’itération
(notée ici TC (i, n)), qui est souvent lié à une variable.
Exemple : Nous nous intéressons ici au nombre d’opérations (+, −, ∗), Op(n), avec N = n.
Nous supposons ici que T ruc(l, n) < n et que T ruc(l, n) nécessite Tt (n) = (n − l) opérations.
On suppose aussi que T ab est de dimension N max 6 N + 1.
Res ← 0
L←2
TantQue L ≤ T ruc(L, N ) faire
Res ← Res + 2 ∗ T ab[L + 1] + T ruc(L, N )
L←L+2
FinTantQue
Remarquons d’abord que L augmente de 2 à chaque itération et que dans le pire des cas la
condition testée est L < N , pour toute valeur de L (puisque T ruc(L, N ) < N ). Nous pouvons
alors écrire Op(n) de la manière suivante :

13
n, parP
pas de 2
Op(n) = (n − l) + 4 + (n − l) + 1
l=2
à la dernière ittération le temps de la condition est (n − l) pour (l = n) ce qui fait un temps
nul.
Pour se ramener à des pas de 1 on pose 2k = l, ce qui permet d’écrire :
n/2
P
Op(n) = ((n − 2k) + 4 + (n − 2k) + 1)
k=1
n/2
Op(n) = 2n2/2 − 4
P
k + 5n/2
k=1
2n2/2 − 4 ( /2)(2/2+1) +
n n
Op(n) = 5n/2
Op(n) = n2 − (n2 +2n)/2 + 5n/2
Op(n) ∼ n2/2 donc Op(n) est en O(n2 ).

Remarquons que, sans faire tous les calculs, on pouvait anticiper que Op(n) serait en O(n2 )
en remarquant que le terme de plus haut degré serait de degré 2.
Règle-4 : Itération (Pour)
En ce qui concerne la structure “Pour ..... Faire ...... FinPour” on procèdera de la manière
suivante : on considère la boucle TantQue équivalente :
Pour I ← ideb à if in faire
Action1
FinPour
est considéré équivalent à
I ← ideb
TantQue I 6 if in Faire
Action1
I ←I +1
FinTantQue
C’est à dire que l’on compte en plus des (if in−ideb+1) itérations, (if in−ideb+2) affectations,
additions, comparaisons. Remarquons qu’une pratique courante consiste à négliger (lorsque
cela ne change pas la complexité) ces opérations implicites dans le “Pour.....”, comme nous
l’avons fait ci-dessus.
Exemple :
Pour I ← 1 à N Faire
Res ← Res + I
FinPour
le nombre d’additions est ici N si on néglige ces opérations implicites, et 2N + 1 si on les
compte (ici if in − ideb + 1 = N ).
Règle-5 Fonctions et Procédures non récursives
On évalue d’abord les fonctions et procédures qui ne contiennent pas d’appels à d’autres
fonctions et procédures, puis celles qui contiennent des appels aux précédentes, etc....
Exemple :

14
Algorithme T ruc
Var C : char
N, R0, R1, I : entier
Procédure A (Var R :entier)
Var I : entier
debut
Pour I ← 1 à N faire
R←R∗I
FinPour
Fin
Procédure B (Var R : entier)
Var I, J : entier
debut
J ←1
Pour I ← 1 à N faire
A(J)
R←R∗J
FinPour
Fin
debut
lire(C) ; lire(N ) ;
R0 ← 1 ; R1 ← 1 ;
si C =0 #0 alors
Pour I ← 1 à N faire
B(N )
FinPour
FinSi
A(N )
Fin.
Nous calculons ici le nombre de multiplications Op(n) pour N = n.
Nous observons d’abord que l’algorithme, dans le pire des cas (ici C =0 #0 ).
Pn
Op(n) = OpB(n) + OpA(n)
i=1
où OpA(n) et OpB(n) représentent le nombre de multiplications correspondant à l’exécution
de A et B. De plus on a :
Pn
OpB(n) = 1 + OpA(n)
i=1
et finalement n
P
OpA(n) = 1 = n
i=1
le calcul se déroule
n
alors ainsi :
OpB(n) = (1 + n) = n + n2
P
i=1
n
(n + n2 ) + n = n3 + n2 + n ∼ n3 donc Op(n) est en O(n3 ).
P
Op(n) =
i=1 ∞

15
Remarque importante : lorsqu’on fait un appel, il faut en toute rigueur compter l’appel
lui-même comme une opération particulière, mais aussi compter les opérations correspondant
au passage de l’argument.
Plus précisément : lors d’un passage par valeur quelles sont les opérations mises en jeu ? Pour
chaque argument passé il faut évaluer l’argument (par exemple 1’addition pour factorielle
(n + 1), et affecter cette valeur à une nouvelle variable (locale à la fonction). On néglige
souvent cette dernière opération, cependant si l’argument passé est un tableau de taille N ,
alors l’affectation correspond à N affectations élémentaires, et ce coût n’est plus négligeable.
C’est en particulier une des raisons pour lesquelles on évite souvent de passer par valeur un
tableau même si sa valeur ne doit pas être modifiée par la procédure (ou fonction). En effet un
passage par adresse ne correspond pas à N affectations élémentaires puisque seule l’adresse
en mémoire du tableau est fournie à la procédure lors de l’appel.
Règle-6 Cas des procédures et fonctions récursives
Dans ce cas on obtient, lorsqu’on calcule un temps d’exécution ou un nombre d’opérations,
des équations de récurrence.
Exemple : Considérons le cas de n!, pour lequel la fonction F act est définie en utilisant les
propriétés suivantes :
F act(0) = 1
F act(n) = n ∗ f act(n − 1)
Si nous nous intéressons au temps d’exécution, nous obtenons l’équation suivante (où t0 et
t1 sont des constantes) :
Op(0) = t0
Op(n) = Op(n − 1) + t1
Nous résolvons cette équation par substitutions successives. Une méthode pour présenter les
substitutions consiste à écrire les équations de manière à ce qu’en sommant celles-ci on ait à
gauche Op(n) et à droite une expression non récurrente (les termes en gras se simplifient) :
{1} Op(n) = Op(n − 1) + t1
{2} Op(n − 1) = Op(n − 2) + t1
{3} Op(n − 2) = Op(n − 3) + t1
..
.
{k} Op(n − k + 1) = Op(n − k) + t1
..
.
{n-1} Op(2) = Op(1) + t1
{n} Op(1) = t0 + t1
Op(n) = t0 + n.t1 est en O(n).

1.5 Exemples de calculs de complexités


1.5.1 Complexité liéaire

16
Algo Somme(entier N ) : retourne un entier
Var accu, nb : entier
debut // somme des N premiers entiers
accu ← 0 ; nb ← 1
tant que nb <= N faire
accu ← accu + nb
nb ← nb + 1
fintantque
retourner accu
fin
Ce petit algorithme demande 2 affectations, N +1 comparaisons, N sommes et affectations, N
sommes et affectations, ce qui fait au total 3N + 3 opérations élémentaires. Ce qui nous inté-
resse c’est un ordre de grandeur (valeur asymptotique), donc la complexité de cet algorithme
est de l’ordre de N , ou encore linéaire en N .

1.5.2 Complexité constante


Algo Somme(entier N ) : retourne un entier
Var accu : entier
debut // somme des N premiers entiers
accu ← N ∗ (N + 1)/2
retourner accu
fin
Même problème résolu en temps constant (indépendant des données), de O(1).

1.5.3 Complexité logarithmique


Prenons l’exemple de la recherche dichotomique dans un tableau trié : Si t est un tableau
d’entiers triés de taille n, on peut écrire une fonction qui cherche si un entier donné se trouve
dans le tableau. Comme le tableau est trié, on peut procéder par dichotomie : cherchant à
savoir si x est dans t[g..d[, on calcule m = (g + d)/2 et on compare x à t[m]. Si x = t[m], on a
gagné, sinon on réessaie avec t[g..m[ si t[m] > x et dans t[m + 1..d[ sinon. Voici l’algorithme
correspondant :
Algo rechercheDichotomique( x,n : entier, t : tab[1..n] d’entiers) : retourne un entier
Var m, g, d : entier ; trouv : booleen ;
debut
g ← 1 ; d ← n ; trouv ← f aux ;
TantQue (g <= d) et (non trouv) faire
m ← (g + d)/2
si (t[m] = x) alors trouv ← vrai
retourner m
sinon si (t[m] < x) alors g ← m + 1
sinon d ← m − 1
FinTantQue
si (non trouv) alors retourner −1
fin

17
Le nombre maximal de comparaisons à effectuer pour un tableau de taille n est :

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

Pour résoudre cette récurrence, on écrit n = 2t , ce qui conduit à

T (2t ) = T (2t−1 ) + 1 = · · · = T (1) + t

d’où un coût en O(t) = O(log n).

1.5.4 Complexité quadratique


Completons l’analyse de l’algorithme TRI-INSERTION de la section 1.2.
Algo TRI-INSERTION(T : tab[1..l]d’entiers) : retourne le tableau T
1 pour j ← 2 à l faire
2 tmp ← T [j] ;
3 i ← j − 1;
4 tant que T [i] > tmp et i > 0 faire
5 T [i + 1] ← T [i] ;
6 i ← i − 1;
7 T [i + 1] ← tmp ;
8 retourner T ;
finTRI-INSERTION ;
Analyse de la complexité dans le pire des cas en nombre de comparaisons : Dans le pire des
cas, la boucle Tant que fait j =1 comparaisons à l’étape j de la boucle de la ligne 1. Ce qui fait
en sommant, 1 + 2 + ... + (l=1) comparaisons au total, soit l(l=1)/2 comparaisons. Or, ce cas
se produit quand on cherche à trier toute séquence strictement décroissante. Le tri-insertion
est donc un algorithme quadratique dans le pire des cas en nombre de comparaisons, de O(l2 ).

1.5.5 Complexité au mieux, au pire et en moyenne


Prenons l’exemple de Recherche du plus grand élément d’un tableau :
Algorithme Maximum (t : tab[1..n] d’entiers, n : entier) : retourne un entier
// Recherche l’élément le plus grand d’un tableau de taille n non nulle
Var i, max : entier
Début
max ← t[1]
Pour i ← 2 à n faire
Si (t[i] > max) Alors
max ← t[i]
Fin si
Fin Pour
Retourner max
Fin
Pour mesurer la complexité d’un algorithme, il faut tout d’abord désigner une ou plusieurs
opérations élémentaires utilisées par l’algorithme. Dans le cas de la recherche du maximum

18
d’un tableau, on prendra en considération l’affectation d’une valeur à la variable contenant
le maximum (max ← t[1] et max ← t[i]).
Mesurer la complexité de l’algorithme revient alors à compter le nombre de fois où ces
opérations sont effectuées par l’algorithme. Naturellement, nous le voyons avec l’algorithme
Maximum et le nombre d’affectations qu’il effectue, la complexité peut varier avec les données
fournies en entrée. Nous allons donc distinguer trois notions de complexité : au mieux, au
pire et en moyenne.
– Le cas le plus favorable pour notre algorithme Maximum est le cas où le maximum du
tableau se trouve en première position : la variable max prend cette valeur au début et
n’en change plus ensuite. La complexité au mieux vaut donc 1.
– Au pire, le tableau est trié en ordre croissant et la variable max doit changer de valeur
avec chaque case. La complexité au pire vaut donc n.
– La complexité en moyenne est plus difficile à calculer. Il ne s’agit pas comme on pourrait
le penser de la moyenne des complexités au mieux et au pire. Nous pouvons observer
que si nous choisissons un tableau au hasard, il est beaucoup plus probable d’avoir un
tableau dont le maximum est en première place (cas le mieux) qu’un tableau complè-
tement trié (cas le pire). Par conséquent, la complexité en moyenne est plus proche de
1 que de n et, en effet, il est possible de montrer que cette complexité en moyenne vaut
log(n).
En résumé, nous avons pour la complexité de l’algorithme Maximum en nombre d’affectations
sur un tableau de taille n :

au mieux en moyenne au pire


1 log(n) n

1.5.6 Complexité exponentielle


1.5.6.1 Le jeu d’échecs :
On peut concevoir un algorithme qui, à chaque fois, étudie tous les coups possibles (il y en a
un nombre fini donc c’est tout à fait possible) et retient le meilleur. Mais il y a de l’ordre de
1019 coups possibles et 1019 millisecondes c’est 300 millions d’années. On dispose donc d’un
algorithme qui donne à coup sûr la solution optimale mais qui est totalement irréalisable
dans la pratique. La complexité est hors du possible (problème exponentiel). Dans ce genre
de problèmes, on écrira une heuristique, c’est à dire une méthode (un algorithme) qui produit
la plupart du temps un résultat acceptable mais pas nécessairement optimal (on ne pourra
pas prouver s’il est optimal ou non) en un temps raisonnable.

1.5.6.2 Le problème de la tour de Hanoı̈ (Intuition d’une explosion


combinatoire) :

19
Le problème des tours de Hanoı̈ est un jeu de réflexion consistant à déplacer des disques
de diamètres différents d’une tour de départ à une tour d’arrivée en passant par une tour
intermédiaire, et ceci en un minimum de coups. Une tour est constituée d’un ou plusieurs
disques de tailles variables. On dispose de 3 tours. Initialement, la première comprend n
disques et les deux autres sont vides. Le but est de placer les n disques sur la 3ème tour, avec
les règles suivantes :
– on ne peut déplacer qu’un disque à la fois d’une tour à une autre ;
– on ne peut poser un disque que sur un disque plus grand ou sur un emplacement vide.

(exemple cité dans http ://www.lisyc.univ-brest.fr/pages perso/dmasse/cours/S2recursivite.pdf)

Principe de résolution Pour déplacer le disque le plus grand de la 1ère à la 3ème tour, il
faut que tous les autres disques soient sur la deuxième tour :

Donc :
1. on déplace les n=1 disques plus petits de la 1ère à la 2ème tour
2. on déplace le plus grand disque de la 1ère à la 3ème tour
3. puis on déplacer les n=1 disques de la 2ème à la 3ème tour.
Les étapes 1 et 3 se font par appel récursif, en changeant le numéro des tours.

Algorithme Fonction hanoi (nb disques, depart, destination, intermediaire : entiers)


début
si nb disques = 1 alors déplacer le disque 1 de depart vers destination
sinon début
hanoi (nb disques − 1, depart, intermediaire, destination)
déplacer le disque nb disques de depart vers destination
hanoi(nb disques − 1, intermediaire, destination, depart)
fin
fin
Cette fonction termine si nb disques est supérieur ou égal à 1.
Exemple : Avec hanoi(3, 1, 3, 2), la fonction affiche :
Déplacer le disque 1 de la tour 1 à la tour 3.

20
Déplacer le disque 2 de la tour 1 à la tour 2.
Déplacer le disque 1 de la tour 3 à la tour 2.
Déplacer le disque 3 de la tour 1 à la tour 3.
Déplacer le disque 1 de la tour 2 à la tour 1.
Déplacer le disque 2 de la tour 2 à la tour 3.
Déplacer le disque 1 de la tour 1 à la tour 3.

Complexité Combien d’étapes sont nécessaires pour résoudre le problème avec n disques ?
Si on note un ce nombre d’étapes, on a :
(
1 si n = 1 (cas de base)
un =
un−1 + 1 + un−1 si n > 1

Par récurrence, on montre : un = 2n =1


Cet algorithme a donc une complexité exponentielle. Pour n = 64, un ordinateur faisant
1500000 affichages à la seconde prendrait 380000 ans pour produire la solution qui occuperait
900 milliards de Go.

1.5.7 Des problèmes sans solution


Commençons par donner un numéro à tous les algorithmes. Supposons qu’il existe un
algorithme dont la spécification serait la suivante :
Algorithme T estArrêt (a,n : entier)
debut si l’algorithme de numéro a s’arrête sur l’entrée n alors Affiche 1
sinon Affiche 0
Fin
Nous allons montrer en raisonnant par l’absurde qu’un tel algorithme ne peut pas exister.
Pour cela, considérons l’algorithme suivant qui n’est qu’une simple utilisation de l’algorithme
T estArrêt.

21
Algorithme Bizarre (e : entier)
Var i : entier
Début
Répéter
i ← T estArrêt(e, e)
Jusqu’à i = 0
Afficher(1)
Fin
Nous constatons que cet algorithme Bizarre boucle à l’infini si l’algorithme de numéro e
s’arrête ; par contre, Bizarre s’arrête si l’algorithme de numéro e boucle. Voyons où cela nous
mène si nous fournissons à l’algorithme Bizarre son propre numéro : Bizarre boucle si Bizarre
s’arrête, et Bizarre s’arrête si Bizarre boucle.
Nous devons conclure que l’algorithme T estArrêt n’existe pas.

1.6 Conclusion
Nous avons vu qu’il existe des algorithmes pour différents problèmes. Nous avons vu des
problèmes plus difficiles :
– soit nous avons des algorithmes mais qui ne permettent pas de traitement du problème
dans un temps raisonnable (complexité exponentielle) comme par exemple le problème
du jeux d’échecs ;
– soit qu’il n’existe aucun algorithme pour certains problèmes (indécidabilité).

22
TD Chapitre 1 : Complexité Algorithmique
Exercice 1 :
En partant du nombre d’opérations T (n), déterminer la complexité et classer les algo-
rithmes suivant par ordre croissant des complexité du meilleur au plus mauvais :
A1 : T (n) = 3n + 2
A2 : T (n) = 6
A3 : T (n) = 4n2 + n + 2
A4 :
Pour i ← 1 à n faire A3 ;
A1 ;
A5 : A1 ; A2 ; A3 ;
A6 : Pour i ← 1 à 5 faire A1 ;

Exercice 2 :
Calculez la complexité au pire des cas des algorithmes suivants sachant que les Actions
ont une complexité constante :
a) Pour i ← 1 à n faire Act ;
b) Pour i ← 0 à n faire Act ;
c) Pour i ← 0 à n − 1 faire
Act1 ;
Pour j ← 0 à n − 1 faire Act2 ;
d) Pour i ← 0 à n − 1 faire
Act1 ;
Pour j ← 0 à n − 1 faire
Act2 ;
Pour k ← 0 à n − 1 faire Act3 ;
e) Pour i ← 0 à n − 1 faire
Act1 ;
Pour j ← 1 à i faire Act2 ;
f) Tant que n > 1 faire
Act ;
n ← n/2 ;
g) Pour i ← 0 à n faire
si i mod 2=0 alors Act1
sinon Pour j ← 0 à n faire Act2 ;
h) Pour i ← 0 à n faire
Act1 ; j ← 3 ;
Repeter
Act2 ;
j ← j + 3;
jusqu’à j > i ;
i) Pour i ← 0 à n faire
Act1 ; j ← 1 ;
Repeter

23
Act2 ;
j ← j ∗ 3;
jusqu’à j > n ;
j) k ← 1 ;
Repeter
j ← 1;
Tant que j ≤ n faire j ← j ∗ 10 ;
k ← k + 2;
jusqu’à k ≤ n ;

Exercice 3 :
Faire l’algorithme ittératif et ensuite récursif qui fait la multiplication à l’aide de l’addition.
Calculez la complexité pour chaque cas.

Exercice 4 :
Même questions pour le problème de la division Euclidienne par la soustraction.

Exercice 5 :
Ecrire une fonction récursive qui inverse l’ordre des éléments dans un tableau d’entiers.
Calculez sa complexité.

Exercice 6 :
Ecrire une fonction récursive qui recherche par dichotomie un élément dans un tableau
d’entiers. La fonction renvoie l’indice de l’élément s’il existe et -1 sinon. Calculez sa com-
plexité.

Exercice 7 :
1. Proposez un algorithme permettant de calculer la valeur d’un polynôme P (x) de degrè
n pour une valeur x0 donnée. On pourra utiliser la fonction puissance(x, i) qui calcule
la valeur xi . Quel est la complexité de cet algorithme ?
2. La méthode de Horner consiste à écrire un polynôme P (x) = a0 + a1 x + a2 x2 + ... + an xn
de la façon suivante : P (x) = a0 + x(a1 + x(a2 + ... + x(an )..). Réecrire l’algorithme
d’évaluation d’un polynôme en utilisant la représentation de Horner. Calculez sa com-
plexité.
3. Proposez une nouvelle représentation d’un polynôme en utilisant les listes linéaires,
cela en ne sauvegardant que les termes dont les coefficients sont non nuls. Pour ce cas
écrire également la fonction d’évaluation du polynôme en comparant sa complexité par
rapport à celles des questions 1 et 2.

24
Exercice 8 :
Ecrire la fonction qui dit si une chaı̂ne de caractères x est palindrome ou non, calculez sa
complexité.

Exercice 9 :
On se donne une pile P1 contenant des entiers positifs.
1. Ecrire un algorithme pour déplacer les entiers de P1 dans une pile P2 de façon à avoir
dans P2 tous les nombres pairs en dessous des nombres impairs.
2. Ecrire un algorithme pour copier dans P2 les nombres pairs contenus dans P1 . Le contenu
de P1 après exécution de l’algorithme doit être identique à celui avant exécution. Les
nombres pairs dans P2 doivent être dans l’ordre où ils apparaissent dans P1 .
3. Calculez la complexité pour chaque cas.

Exercice 10 :
Gestion d’une piste d’atterrissage Un avion est caractérisé par un enregistrement contenant :
– un indicatif (6 caractères)
– sa destination (30 caractères)
– son autonomie résiduelle de carburant, comptée en heures de vol (entier)
– un booléen indiquant s’il y a un problème urgent (le feu par exemple).
Le problème consiste à :
1. définir les structures de données nécessaires à la gestion d’une piste d’atterrissage,
2. définir et écrire une fonction calculant la priorité d’un avion pour l’utilisation de la
piste,
3. définir et écrire les fonctions nécessaires à la gestion complète de la piste (on envisagera
la suppression d’un avion de la file d’attente lorsque celui si à un problème grave).
4. Calculez la complexité pour chaque fonction.

25
Chapitre 2

Algorithmes de Tri

2.1 Présentation
Il est facile de rechercher un élément particulier dans un ensemble trié, par exemple un
dictionnaire. Mais dans la ”vraie vie”, ou plutôt dans la vie d’un programmeur, les infor-
mations ne sont pas souvent triées. Il se produit même un phénomène assez agaçant et très
général : quand on laisse quelque chose changer, ça devient vite le bazar. Des scientifiques
très intelligents ont passé beaucoup de temps à étudier ce principe.
Il y a plusieurs approches pour se protéger de ce danger. La première est de faire très
attention, tout le temps, à ce que les choses soient bien rangées. C’est ce que fait par exemple
un bibliothécaire : quand on lui rend un livre, il va le poser sur le bon rayon, au bon endroit,
et s’il fait bien cela à chaque fois il est facile de trouver le livre qu’on cherche dans une
bibliothèque. D’autres préfèrent une méthode plus radicale : toutes les semaines, ou tous les
mois, ou tous les dix ans, ils font un grand rangement.
Pour l’instant, nous allons nous intéresser au grand rangement : quand on a un ensemble
de données dans un ordre quelconque, comment récupérer les mêmes données dans l’ordre ?
C’est le problème du tri, et il a de multiples solutions.
Ordonner, ranger, trier un nombre considérable de données sont des opérations fasti-
dieuses pour un être humain, alors qu’un ordinateur est bien plus efficace dans ces tâches
répétitives. Il existe de nombreuses manières de programmer un ordinateur pour trier des
données. Ainsi, les plus grands informaticiens se sont penchés sur la question afin d’élaborer
des algorithmes de tri les plus efficaces possibles. De nos jours, cette activité est croissante à
cause de l’explosion des données disponibles via Internet. Un des principaux défis des années
à venir est la manipulation des données issues du “BIG DATA” généré par l’interconnectivité
des objets de notre quotidien.
Problème du tri : On possède une collection d’éléments, que l’on sait comparer entre
eux. On veut obtenir ces éléments dans l’ordre, c’est-à-dire une collection contenant exacte-
ment les mêmes éléments, mais dans laquelle un élément est toujours ”plus petit” que tous
les éléments suivants.
Vous noterez qu’on n’a pas besoin de préciser quel est le type des éléments : on peut
vouloir trier des entiers, des mots ou autre. On n’a pas non plus précisé de méthode de
comparaison particulière : si on veut trier une liste de personnes, on peut la trier par nom,
par adresse ou par numéro de téléphone. Même pour des entiers, on peut vouloir les trier par
ordre croissant ou par ordre décroissant, c’est-à-dire en les comparant de différentes manières.
Le tout est de convenir ce que veut dire ”plus petit” pour ce que l’on veut trier.

26
Dans la plupart des cas, on triera des entiers par ordre croissant. C’est le cas le plus
simple, et les tris exposés ici seront tous généralisables aux autres situations. La collection à
trier est souvent donnée sous forme de tableau, afin de permettre l’accès direct aux différents
éléments de la collection, ou sous forme de liste, ce qui peut se révéler être plus adapté à
certains algorithmes. Dans la suite nous discuterons des algorithmes de tri sur les tableaux.
Pour résumer, un algorithme de tri est, en informatique ou en mathématiques, un algo-
rithme qui permet d’organiser une collection d’objets selon une relation d’ordre déterminée.
Les objets à trier sont des éléments d’un ensemble muni d’un ordre total. Les algorithmes de
tri sont utilisés dans de très nombreuses situations. Ils sont en particulier utiles à de nom-
breux algorithmes plus complexes dont certains algorithmes de recherche, comme la recherche
dichotomique.

2.2 Tri à Bulles (Bubble-Sort)


Le tri à bulles ou tri par propagation est un algorithme de tri. Il consiste à comparer
répétitivement les éléments consécutifs d’un tableau, et à les permuter lorsqu’ils sont mal
triés. Il doit son nom au fait qu’il déplace rapidement les plus grands éléments en fin de
tableau, comme des bulles d’air qui remonteraient rapidement à la surface d’un liquide.
Le tri à bulles est souvent enseigné en tant qu’exemple algorithmique, car son principe
est simple. Mais c’est le plus lent des algorithmes de tri communément enseignés, et il n’est
donc guère utilisé en pratique.

2.2.1 Principe et Algorithme


L’algorithme parcourt le tableau et compare les éléments consécutifs. Lorsque deux élé-
ments consécutifs ne sont pas dans l’ordre, ils sont échangés.
Après un premier parcours complet du tableau, le plus grand élément est forcément en fin
de tableau, à sa position définitive. En effet, aussitôt que le plus grand élément est rencontré
durant le parcours, il est mal trié par rapport à tous les éléments suivants, donc échangé à
chaque fois jusqu’à la fin du parcours.
Après le premier parcours, le plus grand élément étant à sa position définitive, il n’a plus
à être traité. Le reste du tableau est en revanche encore en désordre. Il faut donc le parcourir
à nouveau, en s’arrêtant à l’avant-dernier élément. Après ce deuxième parcours, les deux plus
grands éléments sont à leur position définitive. Il faut donc répéter les parcours du tableau,
jusqu’à ce que les deux plus petits éléments soient placés à leur position définitive.
Algorithme TriBulles (n : entier ; Var t : tableau d’entiers)
Var i, j : entier ;
Début
Pour i ← 1 à n − 1 faire
Pour j ← 1 à n − i faire
Si t[j] > t[j + 1] Alors
Echange(t[j], t[j + 1]) ;
Fin Si
Fin Pour
Fin Pour
Fin

27
Une optimisation courante de ce tri consiste à l’interrompre dès qu’un parcours des élé-
ments possiblement encore en désordre (boucle interne) est effectué sans échange. En effet,
cela signifie que tout le tableau est trié. Cette optimisation nécessite une variable supplémen-
taire.
Algorithme TriBullesOptimisé (n : entier ; Var t : tableau d’entiers)
Var i, j : entier ;
Début
Pour i ← 1 à n − 1 faire
tableau trié ← vrai ;
Pour j ← 1 à n − i faire
Si t[j] > t[j + 1] Alors
Echange(t[j], t[j + 1]) ;
tableau trié ← f aux ;
Fin Si
Fin Pour
Si tableau trié alors fin TriBullesOptimisé ;
Fin Pour
Fin

2.2.2 Exemple
soit la liste (5, 4, 2, 3, 7, 1), appliquons le tri à bulles sur cette liste d’entiers. Visualisons
les différents états de la liste pour chaque itération externe contôlée par l’indice i :
i = 6/ pour j de 2 jusqu’à 6 faire

i = 5/ pour j de 2 jusqu’à 5 faire

i = 4/ pour j de 2 jusqu’à 4 faire

i = 3/ pour j de 2 jusqu’à 3 faire

i = 2/ pour j de 2 jusqu’à 2 faire

28
i = 1/ pour j de 2 jusquà 1 faire (boucle vide)

2.2.3 Complexité
1. Pour le tri non optimisé, la complexité en temps est de O(n2 ), avec n la taille du tableau.
2. Pour le tri optimisé, le nombre d’itérations de la boucle externe est compris entre 1 et
n. En effet, on peut démontrer qu’après la ième étape, les i derniers éléments du tableau
sont à leur place. À chaque itération, il y a exactement n − 1 comparaisons et au plus
n − 1 échanges.
– Le pire cas (n itérations) est atteint lorsque le plus petit élément est à la fin du
tableau. La complexité est alors O(n2 ).
– En moyenne, la complexité est aussi O(n2 ). En effet, le nombre d’échanges de paires
d’éléments successifs est égal au nombre d’inversions de la permutation, c’est-à-dire
de couples (i, j) tels que i < j et T (i) > T (j). Ce nombre est indépendant de la
manière d’organiser les échanges. Lorsque l’ordre initial des éléments du tableau est
aléatoire, il est en moyenne égal à n(n − 1)/4.
– Le meilleur cas (une seule itération) est atteint quand le tableau est déjà trié. Dans
ce cas, la complexité est linéaire O(n).

2.3 Tri par sélection (Selection-sort)


2.3.1 Principe et algorithme
Le tri par sélection est l’un des tris les plus instinctifs. Le principe est que pour classer n
valeurs, il faut :
– Trouver le plus petit élément et le mettre au début de la liste
– Trouver le 2e plus petit et le mettre en seconde position
– Trouver le 3e plus petit élément et le mettre à la 3e place,
– Trouver le ie plus petit élément et le mettre à la ie place,
ainsi de suite...
L’algorithme se termine quand i = n, c’est à dire quand il n’y a plus qu’une valeur à
sélectionner ; celle ci est alors la plus grande valeur du tableau.
Une variante consiste à procéder de façon symétrique, en plaçant d’abord le plus grand
élément à la fin, puis le second plus grand élément en avant-dernière position, etc.
Le tri par sélection peut aussi être utilisé sur des listes. Le principe est identique, mais
au lieu de déplacer les éléments par échanges, on réalise des suppressions et insertions dans
la liste.
En pseudo-code, l’algorithme s’écrit ainsi :

29
Procédure Tri selection(n : entier ; Var t : tableau d’entiers)
Var i, j, min :entiers
Debut
pour i ← 0 à n − 2
min ← i
pour j ← i + 1 à n − 1
si t[j] < t[min] alors
min ← j
fin pour
si min 6= i alors échanger t[i] et t[min]
fin pour
fin procédure

2.3.2 Exemple

2.3.3 Complexité
Dans le pire cas ou en moyenne, la complexité du tri par sélection est en O(n2 ). Cet
algorithme est simple, mais considéré comme inefficace, car il s’exécute en temps quadratique
en le nombre d’éléments à trier, et non en temps pseudo linéaire.

2.4 Tri par insertion (Insertion-sort)


2.4.1 Principe et algorithme
Son principe est de parcourir la liste non triée (a1 , a2 , ..., an ) en la décomposant en deux
parties une partie déjà triée et une partie non triée. La méthode est identique à celle que
l’on utilise pour ranger des cartes que l’on tient dans sa main : on insère dans le paquet de
cartes déjà rangées une nouvelle carte au bon endroit. L’opération de base consiste à prendre
l’élément frontière dans la partie non triée, puis à l’insérer à sa place dans la partie triée
(place que l’on recherchera séquentiellement), puis à déplacer la frontière d’une position vers

30
la droite. Ces insertions s’effectuent tant qu’il reste un élément à ranger dans la partie non
triée.. L’insertion de l’élément frontière est effectuée par décalages successifs d’une cellule.
La liste (a1 , a2 , ..., an ) est décomposée en deux parties : une partie triée (a1 , a2 , ..., ak ) et
une partie non-triée (ak+1 , ak+2 , ..., an ) ; l’élément ak+1 est appelé élément frontière (c’est le
premier élément non trié).

En faisant varier j de k jusqu’à 2 , afin de balayer toute la partie (a1 , a2 , ..., ak ) déjà
rangée, on décale d’une place les éléments plus grands que l’élément frontière :
tant que aj−1 > ak+1 faire
décaler aj−1 en aj ;
passer au j précédent ;
fintant
La boucle s’arrête lorsque aj−1 < ak+1 ,ce qui veut dire que l’on vient de trouver au rang j − 1
un élément aj−1 plus petit que l’élément frontière ak+1 , donc ak+1 doit être placé au rang j.
Procédure Tri insertion(entier n, Var tableau T )
Début
pour i de 1 à n − 1
# mémoriser T [i] dans x
x ← T [i]
# décaler vers la droite les éléments de T [0]..T [i − 1] qui sont plus grands que x
# en partant de T [i − 1]
j←i
tant que j > 0 et T [j − 1] > x
T [j] ← T [j − 1]
j ←j−1
# placer x dans le ”trou” laissé par le décalage
T [j] ← x
FinProcedure ;

2.4.2 Exemple
Voici les étapes de l’exécution du tri par insertion sur le tableau [6, 5, 3, 1, 8, 7, 2, 4]. Le
tableau est représenté au début et à la fin de chaque itération.
i = 1 : [6, 5, 3, 1, 8, 7, 2, 4] 7−→ [5, 6, 3, 1, 8, 7, 2, 4]
i = 2 : [5, 6, 3, 1, 8, 7, 2, 4] 7−→ [3, 5, 6, 1, 8, 7, 2, 4]
i = 3 : [3, 5, 6, 1, 8, 7, 2, 4] 7−→ [1, 3, 5, 6, 8, 7, 2, 4]
i = 4 : [1, 3, 5, 6, 8, 7, 2, 4] 7−→ [1, 3, 5, 6, 8, 7, 2, 4]
i = 5 : [1, 3, 5, 6, 8, 7, 2, 4] 7−→ [1, 3, 5, 6, 7, 8, 2, 4]
i = 6 : [1, 3, 5, 6, 7, 8, 2, 4] 7−→ [1, 2, 3, 5, 6, 7, 8, 4]
i = 7 : [1, 2, 3, 5, 6, 7, 8, 4] 7−→ [1, 2, 3, 4, 5, 6, 7, 8]

31
2.4.3 Complexité
La complexité du tri par insertion est O(n2 ) dans le pire cas et en moyenne, et linéaire dans
le meilleur cas. Plus précisément :
1. Dans le pire cas, atteint lorsque le tableau est trié à l’envers, l’algorithme effectue de
l’ordre de n2 /2 affectations et comparaisons ;
2. Si les éléments sont distincts et que toutes leurs permutations sont équiprobables (ie
avec une distribution uniforme), la complexité en moyenne de l’algorithme est de l’ordre
de n2 /4 affectations et comparaisons ;
3. Si le tableau est déjà trié, dans le meilleur des cas, il y a n − 1 comparaisons et au plus
n affectations.

2.5 Tri fusion (Merge-sort)


2.5.1 Principe et Exemple
Le tri fusion est construit suivant la stratégie ”diviser pour régner”, en anglais ”divide and
conquer”. Le principe de base de la stratégie ”diviser pour régner” est que pour résoudre un
gros problème, il est souvent plus facile de le diviser en petits problèmes élémentaires. Une
fois chaque petit problème résolu, il n’y a plus qu’à combiner les différentes solutions pour
résoudre le problème global. La méthode ”diviser pour régner” est tout à fait applicable au
problème de tri : plutôt que de trier le tableau complet, il est préférable de trier deux sous
tableaux de taille égale, puis de fusionner les résultats.
L’algorithme proposé ici est récursif. En effet, les deux sous tableaux seront eux même
triés à l’aide de l’algorithme de tri fusion. Un tableau ne comportant qu’un seul élément sera
considéré comme trié : c’est la condition sine qua non sans laquelle l’algorithme n’aurais pas
de conditions d’arrêt. Etapes de l’algorithme :
– Division de l’ensemble de valeurs en deux parties
– Tri de chacun des deux ensembles
– Fusion des deux ensembles

32
On reconnai bien l’approche de type ”diviser pour régner” : on découpe le problème (un
tableau => deux demi-tableaux), on traite chaque sous-problème séparément, puis on ras-
semble les résultats de manière intelligente.
Évidemment, tout le travail se situe dans la phase de fusion : on a deux demi-listes triées,
et on veut obtenir une liste triée. On pourrait se dire qu’il suffit de mettre les deux listes
bout à bout, par exemple si on a les deux listes triées [1; 2; 3] et [4; 5; 6], on les colle en
[1; 2; 3; 4; 5; 6]. Malheureusement, ça ne marche pas, prenez par exemple [1; 3; 6] et [2; 4; 5]. Il
y a bien quelque chose à faire, et ce quelque chose a intérêt à être efficace : si cette opération
cruciale du tri est trop lente, on peut annuler cette méthode.
L’idée qui permet d’avoir une fusion efficace repose sur le fait que les deux listes sont
triées. Il suffit en fait de les parcourir dans l’ordre : on sait que les plus petits éléments des
deux listes sont au début, et le plus petit élément de la liste globale est forcément soit le
plus petit élément de la première liste, soit le plus petit élément de la deuxième (c’est le plus
petit des deux). Une fois qu’on l’a déterminé, on le retire de la demi-liste dans laquelle il se
trouve, et on recommence à regarder les éléments du début. Une fois qu’on a épuisé les deux
demi-listes, on a bien effectué la fusion.

33
2.5.2 Algorithme
Etant donné un tableau (ou une liste) de T [1, ..., n] :
– si n = 1, retourner le tableau T
– sinon :
– Trier le sous-tableau T [1... n2 ]
– Trier le sous-tableau T [ n2 + 1...n]
– Fusionner ces deux sous-tableaux. . .
En pseudo-code cela donne :
entrée : un tableau T
sortie : une permutation triée de T
Fonction triFusion(T [1, . . . , n])
si n ≤ 1 alors Retourner T
sinon Retourner fusion(triFusion(T [1, . . . , n2 ]), triFusion(T [ n2 + 1, . . . , n]))
Pour la fonction de fusion : le plus petit élément de la liste à construire est soit le plus petit
élément de la première liste, soit le plus petit élément de la deuxième liste. Ainsi, on peut
construire la liste élément par élément en retirant tantôt le premier élément de la première
liste, tantôt le premier élément de la deuxième liste (en fait, le plus petit des deux, à supposer
qu’aucune des deux listes ne soit vide, sinon la réponse est immédiate). Ce procédé est appelé
fusion et est au cœur de l’algorithme de tri-fusion.

34
entrée : deux tableaux triés A et B
sortie : un tableau trié qui contient exactement les éléments des tableaux A et B
Fonction fusion(A[1, . . . , k], B[1, . . . , l])
si A est le tableau vide alors Retourner B
si B est le tableau vide alors Retourner A
si A[1] ≤ B[1] alors Retourner A[1] :: fusion(A[2, . . . , k], B)
sinon Retourner B[1] :: fusion(A, B[2, . . . , l])
L’algorithme se termine car la taille du tableau à trier diminue strictement au fil des
appels. La fusion de A et B est en O(k + l) où k est la taille de A et l est la taille de B.

2.5.3 Complexité
Lorsqu’un algorithme contient un appel récursif à lui-même, son temps d’exécution peut
souvent être décrit par une équation de récurrence qui décrit le temps d’exécution global pour
un problème de taille n en fonction du temps d’exécution pour des entrées de taille moindre.
La récurrence définissant le temps d’exécution d’un algorithme diviser pour régner se
décompose suivant les trois étapes du paradigme de base :
1. Si la taille du problème est suffisamment réduite, n 6 c pour une certaine constante c,
la résolution est directe et consomme un temps constant O(1).
2. Sinon, on divise le problème en a sous-problèmes chacun de taille 1/b de la taille du
problème initial. Le temps d’exécution total se décompose alors en trois parties :
a) D(n) : le temps nécessaire à la division du problème en sous-problèmes.
b) aT (n/b) : le temps de résolution des a sous-problèmes.
c) C(n) : le temps nécessaire pour construire la solution finale à partir des solutions
aux sous-problèmes.
La relation de récurrence prend alors la forme :
(
O(1) si n 6 c
T (n) = .
aT (n/b) + D(n) + C(n) sinon

Pour l’algorithme de tri-fusion :


1. D(n) = 0
2. aT (n/b) = 2T (n/2)
3. C(n) = n.
(
O(1) si n 6 1
et donc T (n) = .
2T (n/2) + n sinon
Finalement, le développement de cette formule récursive nous amène à une complexité au
pire des cas en O(n.log2 n).

35
2.6 Tri rapide (quick-sort)
2.6.1 Principe
L’algorithme de tri rapide, ”quick sort” en anglais, est un algorithme de type dichotomique.
Son principe consiste à séparer l’ensemble des éléments en deux parties. La différence par
rapport au tri fusion, vu précédemment, est que la séparation des différentes valeurs ne
s’effectue pas n’importe comment. Pour effectuer la séparation, une valeur pivot est choisie.
Les valeurs sont réparties en deux ensembles suivant qu’elles sont plus grandes ou plus petites
que le pivot. Ensuite, les deux ensembles sont triés séparément, suivant la même méthode.
L’algorithme, tout comme le tri fusion, est récursif, mais cette fois, il n’est pas nécessaire de
fusionner les deux ensembles. Le résultat du tri est égal au tri de l’ensemble dont les valeurs
sont inférieures au pivot concaténé à l’ensemble des valeurs supérieures au pivot, ce dernier
étant pris en sandwich entre les deux ensembles.
Le choix du pivot est le problème central de cet algorithme. En effet, l’idéal serait de
pouvoir répartir les deux ensembles en deux parties de taille à peu prés égales. Cependant, la
recherche du pivot qui permettrait une partition parfaite de l’ensemble en deux parties égales
aurait un coût trop important. C’est pour cela que le pivot est choisit de façon aléatoire parmi
les valeurs de l’ensemble. Dans la pratique, le pivot est le premier ou le dernier élément de
l’ensemble à fractionner. En moyenne, les deux ensembles seront donc de taille sensiblement
égale.
Le principe du tri rapide est le suivant, étant donné un tableau de T [1, ..., n] :
– si n = 1, retourner le tableau T .
– sinon :
– Choisir un élément du tableau, élément que l’on nomme ensuite pivot.
– Placer le pivot à sa position finale dans le tableau : les éléments plus petits que lui
sont à sa gauche, les plus grands à sa droite.
– Trier, toujours à l’aide de cet algorithme, les sous-tableaux à gauche et à droite du
tableau.
(plus de fusion !)
Pour que cette méthode soit la plus efficace possible, il faut que le pivot coupe le tableau
en deux sous-tableaux de tailles comparables.
Ainsi, si l’on choisit à chaque fois le plus petit élément du tableau comme pivot, on se
retrouve dans le cas de l’algorithme de tri par extraction : la taille du tableau de diminue
que d’un à chaque fois alors que le but est de diviser cette taille par deux.
Cependant, bien choisir le pivot peut être coûteux en termes de complexité. Aussi on
suppose que le tableau arrive dans un ordre aléatoire et on se contente de prendre le premier
élément comme pivot.
Algorithme tri rapide :

36
2.6.2 Exemple
Par exemple, pour trier [101, 115, 30, 63, 47, 20], on va avoir les itérations suivantes :
[101(i), 115, 30(p), 63, 47, 20(j)]
[20, 115(i), 30, 63, 47(j), 101]
[20, 47, 30(i), 63(j), 115, 101]
[20, 47(j), 30, 63(i), 115, 101]
Et on relance le processus sur les deux sous tableaux [20, 47] et [63, 115, 101]
[63(g), 115(p), 101(d)]
[63, 115(i), 101(j)]
[63, 101, 115]

2.6.3 Complexité
En moyenne et en meilleurs des cas, la complexité est en O(n · log(n)) en se rapprochant
du principe du tri fusion (pivot toujours au centre du tableau et le coupe en deux parties).
Cependant, si dans le meilleur des cas, le tri rapide est plus rapide que le tri fusion puisque
l’étape ”fusion” n’est pas nécessaire, il n’en va pas toujours de même. En effet, le choix du
pivot est totalement aléatoire, or le hasard ne fait pas toujours bien les choses. Ainsi, dans le
pire des cas, c’est à dire si l’ensemble des éléments à trier est préalablement classé de manière
décroissante, le choix de la première valeur de la liste comme pivot produira deux ensembles
de taille 0 et (n − 1). Ce qui est loin d’être deux ensembles de taille égale. L’application de
l’algorithme de tri rapide à ce type de liste reviens, en fait, à effectuer le tri par sélection,

37
qui est en O(n2 ). Comme quoi, dans certains cas particuliers, le tri rapide n’est pas si rapide
que cela. Avec le tri fusion, il n’y a aucun facteur aléatoire et la complexité en O(n.log(n))
est garantie. Dans la majorité des cas, le tri rapide sera donc préféré au tri fusion, sauf pour
la réalisation d’applications ou la rapidité d’exécution doit être garantie, comme dans le cas
d’applications en temps réel.

2.7 Conclusion
Nous avons présenté dans ce chapitre certain algorithmes de Tri de différentes complexités.
Il en existe d’autres dont certains représentent une combinaison des méthodes déjà vue dans
un souci d’amélioration de la complexité. Cependant ce problème peut être résolu en modifiant
la représentation des données à trier, c’est ce que nous allons voir dans le chapitre suivant
(les arbres).

38
TD Chapitre 2 : Algorithmes de Tri
Méthodes de Tri sur les listes chaı̂nées
– Le principe du tri par insertion peut être adapté à des listes chaı̂nées. Dans ce cas, le
déplacement de chaque élément peut se faire en temps constant (une suppression et un
ajout dans la liste). Par contre, le nombre de comparaisons nécessaires pour trouver
l’emplacement où insérer reste de l’ordre de n2 /4.
– Le tri fusion se décrit naturellement sur des listes et c’est sur de telles structures qu’il
est à la fois le plus simple et le plus rapide.

39
Chapitre 3

Les Arbres

3.1 Notion d’Arbres


Une structure en arborescence est une série d’éléments placés hiérarchiquement à fin qu’un
élément puisse avoir un ou plusieurs éléments en dessus. On appelle aussi arbre un ensemble
d’éléments (appelé nœuds) dont on distingue :
– La racine dont sont issus tous les autres nœuds.
– Les autres nœuds appelés fils ou descendants de la racine.
La structure d’arbre est l’une des plus importantes et des plus spécifiques de l’informatique.
Par exemple : organisation des fichiers dans les systèmes d’exploitation, représentation des
programmes traités par un ordinateur, d’une table des matières, d’un questionnaire, d’un
arbre généalogique, ect. C’est une structure de données qui permet d’écrire des algorithmes
très performants.
Nous verrons tout d’abord les arbres binaires, qui sont un cas particulier des arbres géné-
raux. Une propriété intrinsèque de cette structure est la récursivité.

3.2 Arbre binaire


3.2.1 Définitions
Soit un ensemble de sommets (ou encore noeuds) auxquels sont associés des ”valeurs” (les
éléments à stocker : entier, chaı̂ne, structure, ...). Un arbre binaire est défini récursivement
de la manière suivante : un arbre binaire est composé
– soit d’un seul sommet appelé racine,
– soit d’un sommet racine à la gauche duquel est accroché un sous-arbre binaire gauche
– soit d’un sommet racine à la droite duquel est accroché un sous-arbre binaire droit
– soit d’un sommet racine auquel sont accrochés un sous-arbre binaire droit et un sous-
arbre binaire gauche.

40
Définition 1.
– fils gauche de x = le sommet (s’il existe) accroché à la gauche de x.
– fils droit de x = le sommet (s’il existe) accroché à la droite de x.
– fils de x = le ou les deux sommets accrochés sous x.
– père de x = le sommet p tel que x est fils de p.
– frère de x = un sommet (s’il existe) qui a le même père.
– sommet interne = un sommet qui a au moins un fils (gauche ou droit ou les deux).
– feuille = un sommet qui n’a pas de fils.
– branche = un chemin de fils en fils de la racine vers une feuille.
– branche gauche = la branche de fils gauche en fils gauche.
– branche droite = la branche de fils droit en fils droit.
– hauteur d’un sommet x = la longueur (en nombre d’arcs) du plus long chemin de x
à une feuille.
– hauteur d’un arbre = la hauteur de la racine.
– profondeur d’un sommet x = la longueur (en nombre d’arcs) du chemin de la racine
au sommet.
Remarque : la racine de l’arbre n’a pas de père et c’est le seul sommet comme ça.

3.2.2 Primitives de consultation


– racine( B : arbre) :sommet ; retourne la racine de l’arbre.
– fils gauche(x : sommet, B : arbre) : sommet ; retourne le sommet fils gauche de x ou
VIDE.
– fils droit(x : sommet, B : arbre) : sommet ; retourne le sommet fils droit de x ou VIDE.
– pere(x : sommet, B : arbre) : sommet ; retourne le sommet père de x ou VIDE.
– val(x : sommet, B :arbre) : élément ; retourne l’élément stocké au sommet x.
– est vide(B : arbre) : booleen ; retourne vrai ou faux.

3.2.3 Primitives de construction/modifications


– créer arbre vide() : arbre ; création d’un arbre initialisé comme il faut.
– créer sommet(e : élément, B : arbre) : sommet ; retourne le sommet.
– faire FG(père : sommet , f g : sommet, B : arbre)
– faire FD(père : sommet, f d : sommet, B : arbre)
– faire racine(rac : sommet, B : arbre)
– supprimer feuille(f : sommet, B : arbre)
.
– ..

3.2.4 Parcours
But : passer en revue (pour un traitement quelconque) chaque sommet une et une seule fois.
Théorème 2. Si les primitives sont réalisés en temps constant, la complexité du parcours
est en O(N ) (où N est le nombre de sommets).
Il Existe 3 types de parcours :
1. parcours dans l’ordre préfixe

41
2. parcours dans l’ordre infixe ou symétrique
3. parcours dans l’ordre postfixe
Prenons l’exemple de l’arbre binaire AB sur lequel nous allons établir l’ordre de parcours des
sommets selon la stratégie adoptée.

3.2.4.1 Parcours préfixe


Consiste à effectuer récursivement le parcours en suivant cet ordre :
1. consulter la valeur de la racine
2. parcourir le sous-arbre gauche
3. parcourir le sous-arbre droit
L’ordre de parcours pour les sommets de l’arbre AB sera : a b d e h c f i j g k.
Procedure Parcours prefixe(racine : sommet)
debut
si racine <> V IDE alors
Traiter(racine) ; \\ effectuer une opération spécifique au noeud racine
Parcours prefixe(fils gauche(racine)) ;
Parcours prefixe(fils droit(racine)) ;
finsi
fin

3.2.4.2 Parcours infixe ou symétrique


Consiste à effectuer récursivement le parcours en suivant cet ordre :
1. parcourir le sous-arbre gauche
2. consulter la valeur de la racine
3. parcourir le sous-arbre droit
L’ordre de parcours pour les sommets de l’arbre AB sera : d b h e a f j i c k g.
Procedure Parcours infixe(racine : sommet)
debut
si racine <> V IDE alors
Parcours infixe(fils gauche(racine)) ;
Traiter(racine) ;

42
Parcours infixe(fils droit(racine)) ;
finsi
fin

3.2.4.3 Parcours postfixe


Consiste à effectuer récursivement le parcours en suivant cet ordre :
1. parcourir le sous-arbre gauche
2. parcourir le sous-arbre droit
3. consulter la valeur de la racine
L’ordre de parcours pour les sommets de l’arbre AB sera : d h e b j i f k g c a.
Procedure Parcours postfixe(racine : sommet)
debut
si racine <> V IDE alors
Parcours postfixe(fils gauche(racine)) ;
Parcours postfixe(fils droit(racine)) ;
Traiter(racine) ;
finsi
fin

3.2.5 Arbres binaires particuliers


Définition .3 On appelle arbre binaire complet un arbre binaire tel que chaque sommet
possède 0 ou 2 fils.
Théorème .4 Un arbre binaire complet possède 2P + 1 sommets (nombre impair) dont P
sommets internes et P + 1 feuilles.
Définition .5 On appelle arbre binaire parfait un arbre binaire (complet) tel que chaque
sommet soit père de deux sous arbres de même hauteur.
Théorème .6 Un arbre binaire parfait possède 2h+1 − 1 sommets, où h est la hauteur de
l’arbre.
Définition .7 On appelle arbre binaire quasi-parfait un arbre binaire parfait éventuellement
grignoté d’un étage en bas à droite.
Définition .8 Un arbre binaire est plein s’il est complet et toutes ses feuilles sont au même
niveau.

3.2.6 Représentation d’un arbre quelconque sous forme d’un


arbre binaire
L’intérêt de l’étude des arbres binaires tient du fait que tout arbre ordonné A peut être
représenté par un arbre binaire AB. Les règles de passage sont les suivantes :
– Il y a une correspondance biunivoque entre les nœuds de A et de AB.
– Le premier fils d’un nœud A est le successeur gauche du nœud correspondant dans AB.
– Les autres fils d’un nœud A sont les successeurs droits de ce nœud dans AB. Ils forment
une chaine. Les liens directs des fils avec le père sont supprimés.

43
Exemple :

3.2.7 Implémentations
3.2.7.1 Arbre binaire sous forme de tableaux FG et FD
type sommet = entier
type arbre = enregistrement {
racine : sommet
nbSommets : entier
F G : tableau [1..MAX] de sommets
F D : tableau [1..MAX] de sommets
V AL : tableau [1..MAX] d’éléments
}
definir/alias V IDE = 0

44
Par exemple :

Exemples de primitives :
Algorithme fils gauche(x : sommet, B : arbre) : retourne un sommet
debut
retourner B.F G[x]
fin
Algorithme pere(x : sommet, B : arbre) : retourne un sommet
Var p : entier
debut
p←1
tant que (p <= B.nbSommets) et (B.F G[p] <> x) et (B.F D[p] <> x) faire
p←p+1
fintantque
si (p <= B.nbSommets) retourner p
sinon retourner V IDE
finsi
fin
Quelques primitives de construction :
Algorithme fait racine(r : sommet, VAR B : arbre)
// l’arbre est modifie
debut
B.racine ← r
fin
Algorithme creer arbre vide() : retourne un arbre
Var B : arbre
i : entier
debut
B.nbSommets ← 0
B.racine ← V IDE
retourner B ;
fin
Algorithme creer sommet(VAR B : arbre, e : élément) : retourne un sommet
// ajout du sommet sans liens
// retourne le sommet cree pour ensuite pouvoir faire les liens

45
debut
B.nbSommets ← B.nbSommets + 1
B.V AL[B.nbSommets] ← e
B.F G[B.nbSommets] ← V IDE
B.F D[B.nbSommets] ← V IDE
return B.nbSommets ;
fin
Algorithme fait FG(p : sommet, f g : sommet, VAR B : arbre)
debut
B.F G[p] ← f g
fin
Si on effectue des suppressions de sommets, il va falloir faire des décalages pour éviter les trous
dans le tableau. Pas simple car il ne faut pas seulement décaler, il faut mettre a jour les bons
numéros. On peut aussi accepter d’avoir des trous si on n’utilise pas les primitives ci-dessus
pour pere et racine. Il faut cependant modifier creer sommet pour trouver la première place
libre dans le tableau (qui n’est pas forcement nbSommets). Sinon on va consommer beaucoup
de place mémoire qu’on n’utilisera pas. En outre, pour indiquer qu’une ”place est vide”, il
faut un élément spécial élément vide, ce qui n’est pas toujours possible suivant la nature des
éléments.
Algorithme supprime feuille(f : sommet, VAR B : arbre)
// f doit être une feuille
Var p : sommet
debut
// supprimer le lien du pere
p ← pere(f, B)
si (B.F G[p] = f ) alors
B.F G[p] ← V IDE
sinon B.F D[p] ← V IDE
finsi
// ”supprimer” la feuille = ne plus la compter dans l’arbre
// et noter la place comme libre
B.V AL[f ] ← ELEM EN T V IDE
B.nbSommets ← B.nbSommets − 1
fin

La primitive père pouvant être plus simplement la consultation du champ tableau père.
Si c’est une primitive, il faut impérativement utiliser la variable p au lieu d’appeler plusieurs
fois la primitive.

3.2.7.2 Arbre binaire sous forme de listes chaı̂nées


type noeud = enregistrement {
F G : adresse d’un noeud
F D : adresse d’un noeud
val : élément
}

46
type sommet = adresse de noeud
type arbre = sommet
alias VIDE=adresse NULL
Un arbre est donc donné par l’adresse de sa racine.

Algorithme racine(B : arbre) : retourne un sommet


retourner B ;
Algorithme fils gauche(x : sommet, B : arbre) : retourne un sommet
retourner x ↑ .F G ;
Algorithme valeur(x : sommet, B : arbre) : retourne un élément
retourner x ↑ .val ;
Algorithme est vide(B : arbre) : retourne un booleen
retourner (B = V IDE) ;
Algorithme est feuille(f : sommet, B : arbre) : retourne un booleen
retourner ( f ↑ .F G = V IDE et f ↑ .F D = V IDE) ;
Algorithme pere(x : sommet, B : arbre) : retourne un sommet
// recherche récursive du pere de x dans le sous arbre (de racine) B
Var tmp, p : sommet
debut
si (est V ide(B)) retourner V IDE
sinon
p ← B // racine de B
si (p = x) alors retourner V IDE //cas particulier pere de la racine
sinon si (p ↑ .F G = x) ou (p ↑ .F D = x) alors retourner p
sinon tmp ← pere(x, p ↑ .F G) //recherche récursive dans le sous-arbre gauche
si (tmp = V IDE) // s’il n’est pas à gauche, c’est qu’il est à droite
tmp ← pere(x, p ↑ .F D)
finsi
retourner tmp
finsi
finsi
finsi
fin

47
Plutôt long comme primitive, donc si c’est souvent utilisé, il est plus intelligent de rajouter
un pointeur vers le père dans la structure
type noeud = enregistrement {
F G : adresse d’un noeud
F D : adresse d’un noeud
P ERE : adresse d’un noeud
val : élément }
type sommet = adresse de noeud
type arbre = sommet
Quelques primitives de constructions/modifications :
Algorithme fait racine(r : sommet,VAR B : arbre)
B←r
Algorithme creer arbre vide() : retourne un arbre
retourner V IDE
Algorithme creer sommet(e : élément, B : arbre) : retourne un sommet
// avec cette implementation B en fait n’est pas modifie
Var nouv : sommet
debut
nouv =allocation memoire pour un sommet
nouv ↑ .F G ← V IDE
nouv ↑ .F D ← V IDE
// nouv ↑ .pere ← V IDE
nouv ↑ .val ← creer element(e) // alloc memoire pour l’élément si besoin
retourner nouv
fin
Algorithme fait FG(p : sommet, f g : sommet, B : arbre)
// avec cette implementation B en fait n’est pas modifie
debut
p ↑ .F G ← f g
// f g ↑ .pere ← p
fin
Algorithme supprime feuille(f : sommet,VAR B : arbre) // l’arbre peut devenir vide
donc être modifié
Var p : sommet
debut
si (f = B) alors // la feuille est la racine de l’arbre
liberer la memoire occupee par f
B ← V IDE
sinon
p ← pere(f ) // p ← f ↑ .pere en cas d’existence de pointeur sur le père
si p ↑ .F G = f alors p ↑ .F G ← V IDE // feuille à gauche
sinon p ↑ .F D ← V IDE // feuille a droite
finsi
liberer la memoire occupee par f
finsi
fin

48
3.3 Arbre général
Dans un arbre général chaque sommet peut avoir un nombre quelconque de fils. On ne
distingue pas gauche, ou droit, ou centre. Il y a tout de même un ordre sur les fils. Le 3ème
ou le 7ème fils, ce n’est pas la même chose.
Définition 9. Un arbre est dit ordonné si l’ordre relatif des sous-arbres est important, est
il est dit non-ordonné ou orienté dans le cas contraire.
Des arbres qui diffèrent que par le seul ordre de leurs sous-arbres correspondent au même
arbre orienté, mais sont différents si on les considère comme des arbres ordonnés.
Les structures d’arbres orientés sont très utiles dans tous les problèmes de classification :
on part d’un ensemble que l’on décompose en sous-ensembles, eux-mêmes décomposables en
sous-ensembles plus fin, etc. . .

3.3.1 Primitives
– racine(A : arbre) : sommet ; retourne la racine
– premier fils(x : sommet, A : arbre) : sommet ; retourne le fils le plus à gauche, ou
VIDE
– frère(x : sommet, A : arbre) : sommet ; le premier frère à sa droite
– père(x : sommet, A : arbre) : sommet
– ieme fils(x : sommet, i : entier, A : arbre) : sommet
– val(x : sommet, A : arbre) : élément
– arbre est vide(A : arbre) : booleen
– existe fils(x : sommet, A : arbre) : booleen
– existe frère(x : sommet, A : arbre) : booleen
– creer vide() :arbre
– creer sommet(e : élément, A : arbre)
– fait racine(x : sommet, A : arbre)
– inserer fils(pere : sommet,nouv : sommet, place : entier, A : arbre)
– supprimer feuille(x : sommet, A : arbre)
.
– ..

3.3.2 Parcours
But : passer en revue chaque sommet de l’arbre.

3.3.2.1 Parcours en profondeur


Parcours à main gauche. On veut les sommets dans cet ordre :

49
L’algorithme de parcours en profondeur peut faire du récursif, comme avec les arbres
binaires :
Algorithme Parcours(A : arbre)
Parcours rec(racine(A),A) ;
Algorithme Parcours rec(r : sommet, A : arbre) // parcours du sous arbre de racine r
Var f : sommet
debut
si r <> V IDE alors
f ←premier fils(r, A)
tant que f <> V IDE faire
Parcours rec(f, A)
f ←frere(f, A)
fintantque
finsi
fin
Mais on va voir plutôt une méthode itérative : l’algorithme de Trémaux :
Algo Parcours(A : arbre)
Var x : sommet
debut
x ← racine(A)
repeter
Si existe f ils(x) Alors // aller à ce fils
x ← premier f ils(x)
Sinon // x est une feuille alors remonter au 1er pere qui a un fils non encore visité
TantQue x <> racine(A) et non existe f rere(x) Faire
x ← pere(x)
Fintantque
Si existe f rere(x) Alors x ← f rere(x)
FinSi
FinSi
Jusqu’a x = racine(A)
fin
Théorème 10. Si les primitives sont réalisées en temps constant, le parcours de Trémaux
est en O(N ).

50
3.3.2.2 Parcours en largeur
Parcours hiérarchique, ou militaire. On veut les sommets dans cet ordre :

Algorithme Parcours (A : arbre)


Var F : file ; x : sommet ;
debut
CreerFile(F )
Enf iler(racine(A), F )
TantQue non f ile vide(F ) faire
x ← SommetF ile(F )
T raiter(x)
Def iler(F )
Si existe f ils(x) Alors
Enf iler(x, F )
TantQue existe f rere(x) faire
x ← f rere(x)
Enf iler(x, F )
Fintantque
FinSi
Fintantque
Fin
Théorème 11. Si les primitives sont réalisées en temps constant, le parcours en largeur est
en O(N ) .

3.3.3 Implémentation
Pour chaque sommet, il faut la liste des ses fils dans l’ordre. Donc un pointeur vers le
premier de ses fils : P F . Comme un sommet est aussi le fils de quelqu’un (sauf la racine), il
fait partie d’une liste de fils. Donc il doit avoir l’info suivant dans cette liste de fils. C’est à
dire son frère : F R.
On peut utiliser des tableaux :
type sommet = entier

51
type arbre = enregistrement {
N b : entier // nb de sommets de l’arbre
P F : tableau[1..M AX] de sommets // lien vers le premier fils de gauche
F R : tableau[1..M AX] de sommets // lien vers le frere droit
V AL : tableau[1..M AX] d’éléments
}
alias V IDE = 0
Ou bien des pointeurs (c’est mieux pour les ajouts et suppressions de sommets) :
type noeud = enregistrement {
P F : adresse d’un noeud
F R : adresse d’un noeud
V AL : élément
}
type sommet = adresse de noeud
type arbre = sommet
alias VIDE=adresse NULL
Quelques primitives :
Algorithme frere(s : sommet, A : arbre) : retourne un sommet
// implementation tableaux
retourner A.F R[s]
Algorithme frere(s : sommet, A : arbre) : retourne un sommet
// implementation pointeurs
retourner s ↑ .F R
Algorithme ieme fils(p :sommet, i : entier, A : arbre) : retourne un sommet
// implementation tableaux
Var j : entier ; x : sommet
debut
x ← A.P F [s] ; j ← 1 ;
tant que x <> V IDE et j <> i faire
x ← A.F R[x]
j ←j+1
fintantque
retourner x
fin
Algorithme ieme fils(p :sommet, i : entier, A : arbre) : retourne un sommet
// implementation pointeurs
Var j : entier ; x : sommet
debut
x ← s ↑ .P F ; j ← 1 ;
tant que x <> V IDE et j <> i faire
x ← x ↑ .F R
j ←j+1
fintantque
retourner x
fin

52
Algorithme existe fils(x : sommet, A : arbre) : retourne un booleen
// implementation tableaux
retourner A.P F [x] <> 0
Algorithme existe fils(x : sommet, A : arbre) : retourne un booleen
// implementation pointeurs
retourner x ↑ .P F <> N U LL
Maintenant étudions la primitive racine :
Algorithme racine(A : arbre) : retourne un sommet
// implementation pointeurs
retourner A
Algorithme racine(A : arbre) : retourne un sommet
// implementation tableau
// la racine est le seul sommet qui n’apparait pas dans les tableaux
Var i : entier ; V U : tableau [1..A.N b] de booleen
debut
// pour l’instant on n’a rencontre aucun sommet :
pour i ← 1 a A.N b faire
V U [i] ← f aux
finpour
pour i ← 1 a A.N b faire // parcours de tout P F et F R
si A.P F [i] <> 0 alors V U [i] ← vrai
si A.F R[i] <> 0 alors V U [i] ← vrai
finpour
// quel est le seul pas vu ?
i←1
tant que V U [i] = vrai faire
i←i+1
fintant que
retourner i
fin
Avec les tableau ce n’est plus du temps constant, c’est linéaire en N .
Et c’est pareil pour la primitive pere, avec des tableaux ou avec des pointeurs. Que faire pour
avoir du temps constant ?
Solution 1 : rajouter l’info dans la structure de données :
// TABLEAU
type sommet = entier
type arbre = enregistrement {
N b : entier // nb de sommets de l’arbre
P F : tableau[1..M AX] de sommets // lien vers le premier fils de gauche
F R : tableau[1..M AX] de sommets // lien vers le frere droit
V AL : tableau[1..M AX] d’éléments
rac : sommet // indice du sommet racine
P ERE : tableau[1..M AX] de sommets // lien vers le père
}

53
alias V IDE = 0
// POINTEURS
type noeud = enregistrement {
P F : adresse d’un noeud
F R : adresse d’un noeud
V AL : élément
P ERE : sommet
}
type sommet = adresse de noeud
type arbre = sommet
alias VIDE=adresse NULL
L’adjonction de la racine ne pose pas de problèmes. Mais pour le père, ça coute tout de
même de la place mémoire. Et puis il faut faire attention à rajouter dans les primitives la
mise à jour de cette information.
Solution 2 : construire l’information uniquement quand on en a besoin.

3.4 Arbre binaire de Recherche


3.4.1 Introduction
Dans les méthodes classiques de recherche, en représentant l’ensemble des éléments par
un tableau (liste linéaire implémentée de manière contiguë), on peut faire de la recherche en
O(logN ) par une méthode dichotomique. C’est une bonne complexité, mais cette structure
de données est mauvaise si on veut faire des ajouts ou des suppressions, à cause des décalages
(complexité O(N )). Il faut trouver une structure de données offrant une recherche aussi bien,
cad en O(logN ) aussi. Cela existe, c’est l’arbre binaire de recherche.
Définition 12 . Un arbre binaire de recherche est un arbre binaire tel que la valeur stockée
dans chaque sommet est supérieure à celles stockées dans son sous-arbre gauche et inferieure
à celles de son sous-arbre droit.
Remarque : les éléments stockés dans l’arbre peuvent être compliqués. Quand on parle de
valeur de l’élément il faut alors comprendre valeur de la clé de l’élément.
Exemple d’un arbre binaire de recherche AB :

54
3.4.2 Recherche
Recherche d’un élément dans un arbre binaire de recherche :
Algorithme RechercheAux(e : élément, r : sommet, B : arbre) : retourne un booleen
/// vérifie si e appartient au sous-arbre de racine r (qui peut être vide)
debut
Si r = V IDE Alors // l’élément n’est pas dans l’arbre
retourner F AU X
Sinon
Si val(r, B) = e Alors // on l’a trouve
retourner V RAI
Sinon
Si e < val(r, B) Alors
// s’il existe, c’est dans le sous-arbre gauche
retourner RechercheAux(e,fils gauche(r, B), B)
Sinon
// s’il existe, c’est dans le sous-arbre droit
retourner RechercheAux(e,fils droit(r, B), B)
Finsi
Finsi
Finsi
fin
Algorithme Recherche(e : élément, B : arbre) : retourne un booleen
debut
retourner RechercheAux(e,racine(B), B) ;
fin
Exemple : Recherche de l’élément 33 dans l’arbre binaire de recherche AB :

3.4.3 Ajout
Ajout d’un élément dans un arbre binaire de Recherche : On suppose que l’élément n’est
pas déjà dans l’arbre.

55
Algorithme Ajout(e : élément,VAR B : arbre)
// ajoute l’élément e dans l’arbre, l’arbre peut etre vide, et sera modifié
Var r, y : sommet
debut
si est vide(B) Alors // l’arbre est vide
y ← creer sommet(e, B)
f ait racine(y, B)
sinon
r = racine(B)
si e < val(r, B) alors
Ajout aux(e, f ils gauche(r, B), r, gauche, B)
sinon Ajout aux(e, f ils droit(r, B), r, droit, B)
finsi
finsi
fin
Algorithme Ajout aux(e : élément, r : sommet, pere : sommet, lien : entier,VAR B : arbre)
// Ajout dans le sous arbre de racine r qui est fils de pere
// lien égal droit ou gauche suivant que r est fils gauche ou droit de son pere
Var nouv : sommet
debut
si r = V IDE alors // c’est ici qu’il faut l’ajouter
nouv ← creer sommet(e, B)
si lien = droit alors f aire F D(pere, nouv, B)
sinon f aire F G(pere, nouv, B)
finsi
sinon
si e < val(r, B) alors
Ajout aux(e, f ils gauche(r, B), r, gauche, B)
sinon Ajout aux(e, f ils droit(r, B), r, droit, B)
finsi
finsi
fin
Ici, on ajoute le sommet en tant que feuille. On pourrait aussi ajouter en tant que racine :
– couper l’arbre en deux sous-arbres (binaires de recherche) G et D, G contenant tous les
éléments de valeur plus petite que celle à ajouter, et D contenant tous les éléments de
valeur plus grande que celle à ajouter,
– créer un nouveau sommet de valeur voulue en lui mettant G comme sous-arbre gauche
et D comme sous-arbre droit.
Exemple : Ajout de l’élément 32 dans l’arbre binaire de recherche AB :

56
3.4.4 Suppression
La suppression commence par la recherche de l’élément. Puis :
– si c’est une feuille, on la supprime sans problèmes
– si c’est un sommet qui n’a qu’un fils, on le remplace par ce fils
– si c’est un sommet qui a deux fils, on a deux solutions :
- le remplacer par le sommet de plus grande valeur dans le sous-arbre gauche
- le remplacer par le sommet de plus petite valeur dans le sous-arbre droit
puis supprimer (récursivement) ce sommet.
Exemple : Suppression de l’élément 37 dans l’arbre binaire de recherche AB :

57
3.4.5 Arbres binaires de recherche équilibrés
La complexité de tous ces algorithmes est majorée par la hauteur de l’arbre. On a donc
intérêt à avoir des arbres les moins hauts possible. Ainsi, pour améliorer ces algorithmes,
on pourrait donc faire un rééquilibrage de temps à autres, c’est à dire : récupérer tous les
éléments par un parcours symétrique, puis reconstruire un arbre quasi-parfait à partir de ces
éléments (tout en gardant son critère d’arbre binaire de recherche).

Mais il y a encore mieux : on peut chercher à avoir tout le temps un arbre de moindre
hauteur, c’est à dire un arbre tel que les hauteurs des sous-arbres de chaque sommet diffèrent
d’au plus 1 (i.e. un arbre parfait grignoté d’un étage à gauche, à droite, au milieu, ...), et

58
donc rééquilibrer comme il faut après chaque ajout ou suppression. On pourrait appeler un
tel arbre : arbre AVL, du nom de leurs inventeurs Adelson, Velskii et Landis.

Arbres A.V.L.

Définition .13. Un arbre binaire est dit H-équilibré si en tout sommet de l’arbre les hauteurs
des sous-arbres gauche et droit diffèrent au plus de 1.
Théorème .14. La hauteur d’un arbre H-équilibré est en O(logN ).
Définition .15. Un arbre AVL est un arbre binaire de recherche H-équilibré.
Donc dans un AVL :
– la recherche d’un élément est en O(logN ) puisque la hauteur est en O(logN )
– l’ajout et la suppression sont en O(logN ).
Dans un arbre AVL pour chaque nœud la longueur du chemin le plus long dans le sous arbre
de gauche diffère de celle du sous-arbre de droite par au plus une unité.
Exemple :

Le problème de maintenance de la propriété AVL se pose au moment de l’insertion ou de


l’effacement d’un élément.
Supposons qu’un nouvel élément soit inséré dans les feuilles de l’arbre. Cette insertion peut
entrainer un déséquilibre de l’arbre, qui cessera alors d’être un arbre AVL.
Exemple : Ici l’arbre n’est plus AVL, puisque le sous-arbre gauche du nœud 20 a pour
longueur 2, alors que le sous-arbre de droite a pour longueur 0.

59
Dans d’autres cas, la propriété AVL peut être conservée. Ainsi :

On dira donc qu’un nœud est équilibré si les chemins les plus longs, dans chaque sous
arbre partant d’un nœud, sont égaux. Si le chemin du sous arbre est plus long d’une unité que
celui de droite, on dira qu’il est lourd à gauche. Inversement, si le nœud n’est ni équilibré
ni lourd à gauche, il sera lourd à droite. Dans un arbre AVL, chaque nœud se trouve dans
l’un de ces états.

Ajout d’un élément dans un arbre AVL


Cela provoque un changement d’état d’un ou de plusieurs nœuds. Les trois cas possibles sont
les suivants :
1. Un nœud équilibré devient lourd à droite ou lourd à gauche.
2. Un nœud lourd (à gauche ou à droite) devient équilibré.
3. Un nœud lourd (à gauche ou à droite) devient totalement déséquilibré par l’insertion
d’un nouvel élément dans le sous-arbre déjà lourd. On dit que le nœud correspondant
devient critique.
Les deux premiers cas ne changent pas la propriété AVL de l’arbre. Dans le 3ème cas il convient
de réorganiser l’arbre pour lui permettre de conserver sa propriété AVL. Les trois possibilités
peuvent être représentées de la manière suivante :
1. L’arbre associé au point critique ne dispose, après l’insertion, que de trois éléments.
Dans ce cas, pour rééquilibrer l’arbre, il suffit de placer N a la racine du sous arbre.
On a toujours B < N < A.

2. Le second cas est plus complexe, et peut être représenté par :

60
Un élément est ajouté à gauche de B, ce qui déséquilibre le sous-arbre gauche de A.
pour parvenir à une structure équilibrée, il suffit d’effectuer une rotation vers la droite
qui place B à la racine. Le même cas peut se présenter à droite.
3. Le 3ème cas est représenté par :

Si un élément est ajouté à l’un des sous-arbres de C le nœud A devient lourd à gauche.
Dans ce cas, pour rééquilibrer A il suffit d’effectuer une rotation double, d’abord (B, C)
à gauche, puis (C, A) à droite.

Exemple 1 : Le nœud critique est 20. On réequilible en appliquant la régle 1 :

Exemple 2 : Le nœud critique est 7.

61
L’arbre devient après équilibrage :

Exemple 3 : Le point critique est le 6.

Devient après équilibrage :

62
3.5 Structure de données Tas
3.5.1 Définition
Un tas est un arbre binaire quasi-parfait tel que la valeur de chaque sommet est inférieure
à la valeur de (son ou) ses fil(s). Cette structure de données permet en particulier de faire un
tri super géant en termes de complexité.

3.5.2 Implémentation d’un arbre binaire (quasi-)parfait


Pour un arbre binaire-quasi parfait, on n’a pas besoin des trois tableaux F G , F D et
V AL, un seul suffit, puisque l’on sait qu’il y aura un seul élément au premier niveau, 2 au
deuxième, 4 au troisième, ..., 2i au ième , ...
type TAS = enregistrement {
tab : tableau[1..M AX] d’éléments
N b : entier //nb reel d’éléments (de sommets)
}
Le sommet tas.tab[i] :
– a pour fils gauche : tas.tab[2 ∗ i]
– a pour fils droit : tas.tab[2 ∗ i + 1]
– a pour père : tas.tab[i div 2]
– est une feuille ssi 2 ∗ i > tas.N b

3.5.3 Tri pas tas


Le tri par tas comporte les étapes suivantes :
– Construire un tas contenant les éléments à trier
– soit en ajoutant les éléments un par un
– soit en construisant directement le tas
– éplucher le tas c’est à dire :
– écrire la racine (qui est le plus petit élément)
– la supprimer et réorganiser pour que ça reste un tas
– recommencer jusqu’à ce que le tas soit vide.

3.5.4 Ajout d’un élément dans le tas


On ajoute le sommet en tant que première feuille possible (à la fin du tableau) puis on
l’échange avec son père jusqu’à ce que ça forme de nouveau un tas.
Exemple :

63
Algo ajout(Var tas : TAS, e : élément)
debut
tas.N b ← tas.N b + 1
tas.tab[tas.N b] ← e
Monter(tas, tas.N b)
fin
Algorithme Monter(Var tas : TAS, pos : entier)
// monter dans le tas l’élément en position pos
Var pere : entier
debut
si pos > 1 alors
pere ← pos div 2
si tas.tab[pere] > tas.tab[pos] alors
permuter(tas.tab, pos, pere)
Monter(tas, pere)
finsi
finsi
fin

3.5.5 Epluchage
”Eplucher ” c’est retourner le plus petit élément du tas, le supprimer et faire en sorte que
ce qui reste soit toujours un tas, cela est réalisé comme suit :
– on retourne la racine (le plus petit élement).
– on met la dernière feuille à la place de la racine (on la supprime).
– on fait descendre cette ”racine” à sa bonne place en l’échangeant avec le plus petit de
ses deux fils.
Exemple :

Algorithme Oter-min(Var tas : TAS) : retourne un élément


Var garde : élément
debut
si tas.N b > 0 alors
garde ← tas.T [1]
tas.tab[1] ← tas.tab[tas.N b]
tas.N b ← tas.N b − 1
Descendre(tas, 1)
retourner garde
sinon erreur ”tas vide”
finsi fin

64
Algorithme Descendre(Var tas : TAS, pos : entier) // descendre dans le tas l’élément
en position pos
Var f ils : entier
debut
si pos <= (tas.N b div 2) alors // ce n’est pas une feuille
f ils ← plus-petit-fils(tas, pos)
si tas.tab[f ils] < tas.tab[pos] alors
permuter(tas.tab, pos, f ils)
Descendre(tas, f ils)
finsi
finsi
fin
Algorithme plus-petit-fils(tas : TAS, pere : entier) : retourne un entier
// retourne la position dans le tas du plus petit fils du sommet pere
// pere a au moins un fils gauche (n’est pas une feuille) mais peut etre pas de fils droit
Var f ils : entier
debut
f ils ← 2 ∗ pere // le fils gauche
si f ils + 1 <= tas.N b alors // il y a un fils droit
si tas.tab[f ils + 1] < tas.tab[f ils] alors
f ils ← f ils + 1
finsi
finsi
retourner f ils
fin
La complexité est majorée par la hauteur de l’arbre binaire. Comme l’arbre est quasi parfait,
on obtient O(logN ) .

3.5.6 Exemple de tri par TAS


Etant donnés la suite d’éléments entiers à trier, arrivant dans cet ordre : 30, 21, 4, 10, 36,
17, 20, 8, 11, 5. Le tri se fait en construisant tout d’abord le TAS en insérant les éléments un
à un, et ensuite en faisant son épluchage.

3.5.6.1 Construction du TAS


Insérer l’élément de valeur 30 :

Insérer l’élément de valeur 21 :

65
Insérer l’élément de valeur 4 :

Insérer l’élément de valeur 10 :

Insérer l’élément de valeur 36 :

Insérer l’élément de valeur 17 :

Insérer l’élément de valeur 20 :

66
Insérer l’élément de valeur 8 :

Insérer l’élément de valeur 11 :

Insérer l’élément de valeur 5 :

3.5.6.2 Epluchage du TAS


Le résultat du tri par ordre croissant correspond à la séquence des valeurs lus lors de l’étape
d’épluchage du TAS.
Lire la racine 4 puis la supprimer du TAS :

67
Lire la racine 5 puis la supprimer du TAS :

Lire la racine 8 puis la supprimer du TAS :

Lire la racine 10 puis la supprimer du TAS :

Lire la racine 11 puis la supprimer du TAS :

Lire la racine 17 puis la supprimer du TAS :

68
Lire la racine 20 puis la supprimer du TAS :

Lire la racine 21 puis la supprimer du TAS :

Lire la racine 30 puis la supprimer du TAS :

Lire la racine 36 puis la supprimer du TAS ce qui donnera un TAS vide à la fin.

69
TD Chapitre 3 : Les arbres
Exercice 1 :
Ecrire la fonction qui cherche un élément de valeur v dans un arbre binaire A.

Exercice 2 :
Ecrire la fonction récursive qui dit si un arbre binaire A est complet ou non.

Exercice 3 :
Ecrire la fonction récursive qui calcule la hauteur d’un sommet x dans un arbre binaire A.

Exercice 4 :
En utilisant les fonctions de l’exercice 2 et 3, écrire la fonction qui dit si un arbre binaire A
est parfait ou non.

Exercice 5 :
Ecrire la fonction qui dit si un arbre binaire est A.V.L ou non. Indication : utiliser la fonction
‘hauteur’ de l’exercice3.

Exercice 6 :
Ecrire la fonction récursive qui fait la somme des éléments (de type entier) d’un arbre général,
et cela en proposant deux solutions qui utilisent des stratégies de parcours différentes.

Exercice 7 :
Ecrire la fonction qui étant donné un arbre général A construit l’arbre binaire AB corres-
pondant.

Exercice 8 :
Ecrire la fonction qui transforme un arbre A d’éléments entiers quelconques, en un arbre
binaire de recherche B. Indication pour la solution : parcourir l’arbre A en utilisant une des
primitives de parcours d’un arbre quelconque et insérer chaque élément lu dans l’arbre B en
utilisant la primitive d’ajout dans un arbre binaire de recherche.

Exercice 9 :
Ecrire la fonction qui dit si un arbre binaire est arbre binaire de recherche ou non.

70
Chapitre 4

Les graphes

4.1 Introduction aux graphes


4.1.1 Définitions
Intuitivement, un graphe est un ensemble de points, dont certaines paires sont directement
reliées par un lien. Ces liens peuvent être orientés, c’est-à-dire qu’un lien entre deux points
u et v relie soit u vers v, soit v vers u : dans ce cas, le graphe est dit orienté. Sinon, les liens
sont symétriques, et le graphe est non-orienté.
Dans la littérature récente de la théorie des graphes, les points sont appelés les sommets ou
les nœuds. Les liens sont appelés arêtes dans les graphes non-orienté et arcs dans un graphe
orienté.
L’ensemble des sommets est le plus souvent noté V , tandis que E désigne l’ensemble des
arêtes. Dans le cas général, un graphe peut avoir des arêtes multiples, c’est-à-dire que plusieurs
arêtes différentes relient la même paire de points. De plus, un lien peut être une boucle, c’est-
à-dire ne relier qu’un point à lui-même.
Tous sommets de V reliés par une arête sont dit sommets adjacents.
Un chemin (appelé aussi une chaı̂ne) entre deux sommets v1 et v2 est la suite d’arcs successifs
reliants v1 à v2 .
Un graphe G = (V, E) est dit d’ordre n si il comporte n sommets (ou n noeuds).

4.1.2 Graphes particuliers


Graphe simple
Un graphe est simple s’il n’a ni liens multiples ni boucles, il peut alors être défini simple-
ment par un couple G = (V, E), où E est un ensemble de paires d’éléments de V . Dans le
cas d’un graphe simple orienté, E est un ensemble de couples d’éléments de V . Notons qu’un
graphe sans arête multiple peut être représenté par une relation binaire, qui est symétrique
si le graphe est non-orienté.

Graphe connexe
Un graphe est dit connexe si pour chaque couple de sommets il existe un chemin qui les
relie.

71
Graphe fortement connexe
Contrairement à la notion de connexité, celle de forte connexité n’est disponible que sur
les réseaux orientés.
On dit qu’un graphe G est fortement connexe si et seulement si, pour tout couple de sommets
(i, j) ∈ V × V , il existe un chemin permettant de joindre i à j et un chemin permettant de
joindre j à i dans G.

Graphe complet
Soit G un graphe simple non orienté. Le graphe G est complet si tout couple de sommets
distincts est lié par une arête, c’est-à-dire si tous les sommets sont adjacents.
Théorème : Pour tout entier naturel non nul n, on note Kn le graphe complet d’ordre n.
Le nombre d’arêtes du graphe complet Kn est égal à n(n − 1)/2.

Graphe biparti
Un graphe biparti est un graphe dont l’ensemble de sommets peut être partitionné en
deux sous-ensembles U et V tels que chaque arête ait une extrémité dans U et l’autre dans
V.

Graphe pondéré
– Un graphe étiqueté est un graphe où chaque arête est affectée soit d’une chaı̂ne de
caractères, soit d’un nombre.
– Un graphe pondéré est un graphe étiqueté où chaque arête est affectée d’un nombre
réel positif, appelé poids de cette arête.
– Le poids d’une chaı̂ne est la somme des poids des arêtes qui la composent.
– Une plus courte chaı̂ne entre deux sommets est, parmi les chaı̂nes qui les relient, une
chaı̂ne de poids minimum.

Graphe probabiliste
– Un graphe probabiliste est un graphe orienté pondéré dans lequel la somme des poids
des arêtes issues de chaque sommet est égale à 1.
– La matrice de transition associée à un graphe probabiliste d’ordre n est la matrice
carrée M = (ai,j ) d’ordre n telle que, pour tous entiers i et j vérifiant 1 6 i 6 n et
1 6 j 6 n, ai,j est égal au poids de l’arête orientée d’origine le sommet i et d’extrémité
le sommet j si cette arête existe, et est égal à 0 sinon. Cette matrice décrit le passage
d’un état au suivant.
– Un état probabiliste est une loi de probabilité sur l’ensemble des états possibles. Cette
loi est représentée par une matrice ligne.

72
4.2 Représentation d’un graphe
4.2.1 Matrice d’adjacence
Une matrice d’adjacence pour un graphe fini G à n sommets est une matrice de dimension
n × n dont l’élément non-diagonal aij est le nombre d’arêtes liant le sommet i au sommet j.
L’élément diagonal aii est le nombre de boucles au sommet i.
Définition : Supposez que G = (V, E) est un graphe simple, où |V | = n. Supposez aussi
que les sommets de G sont, arbitrairement v1 , v2 , ..., vn . La matrice d’adjacence A de G se
rapportant à cet ensemble de sommets est la matrice n × n booléenne A avec
(
1 si (vi , vj ) ∈ E
ai,j =
0 sinon

Une matrice d’adjacence d’un graphe est fondée sur la relation d’ordre établie pour les som-
mets.
Donc, il existe (au plus) n! matrices d’adjacences différentes pour un graphe comportant n
sommets puisqu’il y a n! possibilités d’ordonner ces sommets.
Remarque : La matrice associée à un graphe non orienté est symétrique, c’est-à-dire que,
pour tous entiers i et j tels que 1 6 i 6 n et 1 6 j 6 n, on a : aj,i = ai,j .
Exemples :
1) La matrice d’adjacence du graphe étiqueté non orienté suivant :

2) Le graphe orienté représenté ci-dessous admet pour matrice :

73
4.2.2 Matrice d’incidence
Matrice d’incidence sommets-arcs
La matrice d’incidence sommets-arcs d’un graphe G = (V, E) est une matrice A = (ai,u ),
(i, u) ∈ [1, N ]∗[1, M ], à coefficients entiers appartenant à l’ensemble 0, +1, −1 telle que chaque
colonne correspond à un arc de G, et chaque ligne à un sommet de G ; si u = (i, j) ∈ E,
la colonne u a tous les termes nuls sauf ai,u = 1 et aj,u = −1. D’une façon évidente, si on
considère une ligne i quelconque (correspondant au sommet i) alors :
– w+ (i) = {u| ai,u = 1}
– w− (i) = {u| aj,u = −1}
Exemple :

L’arc u1 relie le sommet 1 au sommet 2 donc a1,1 = 1, a2,1 = −1 et tous les autres ai,1 = 0.
De même pour les autres arcs.

Matrice d’incidence sommets-arêtes


Soit G = (V, E) où E est un ensemble d’arêtes, la matrice d’incidence sommets-arêtes est
une matrice à coefficients 0 ou 1, où chaque ligne i correspond à un sommet i de G, et chaque
colonne à une arête u = (i, j) de G. Si u = (j, j) alors la colonne ”u” a tous ses éléments nuls
sauf ai,u = 1, et aj,u = 1.

Propriétés de la matrice d’incidence :


– chaque arête est représentée par deux bits (matrice symétrique) ;
– on suppose qu’il existe une arête entre chaque sommet et lui même ;
– bien adapté aux graphes denses (ayant beaucoup d’arêtes).
Exemple :

74
4.2.3 Liste d’adjacence
Un graphe étiqueté et sa représentation par liste d’adjacence :

De même, l’espace qu’une structure consomme dépend du type de graphe considéré : un


raccourci abusif consiste à dire qu’une liste d’adjacences consomme moins d’espace qu’une
matrice car celle-ci sera creuse, mais cela prend par exemple plus d’espace pour stocker un
graphe aléatoire avec les listes qu’avec une matrice ; dans le cas général, une matrice utilise
un espace O(n2 ) et les listes utilisent O(m.log n) donc si le graphe est dense alors m peut-
être suffisamment grand pour qu’une matrice consomme moins d’espace, et si le graphe est
peu dense alors les listes consommeront moins d’espace. Des modifications simples d’une
structure de données peuvent permettre d’avoir un gain appréciable : par exemple, dans
une représentation partiellement complémentée d’une liste, un bit spécial indique si la liste
est celle des voisins présents ou manquants ; cette technique permet d’avoir des algorithmes
linéaires sur le complément d’un graphe.
Propriétés de la structure d’adjacence :
– chaque arête est représentée deux fois (AF et FA par exemple) ;
– en changeant l’ordre des arêtes dans les listes, on modifie le parcours de la structure
pour aller d’un nœud à un autre ;
– difficile de supprimer un sommet (en plus de supprimer la liste d’adjacence de F par
exemple, il faut supprimer F de toutes les listes où il apparaı̂t) ;
– bien adaptée aux graphes peu denses (n’ayant pas beaucoup d’arêtes) car sinon les listes
s’allongent.
Exemple de structure d’adjacence par liste chainée :

75
4.3 Parcours de graphes
4.3.1 Parcours en largeur (Cas des graphes orienté)
L’algorithme de parcours en largeur (ou BFS, pour Breadth First Search) permet le par-
cours d’un graphe de manière itérative, en utilisant une file. Il peut par exemple servir à
déterminer la connexité d’un graphe.

Principe
Cet algorithme diffère de l’algorithme de parcours en profondeur par le fait que, à partir
d’un sommet S, il liste d’abord les voisins de S pour ensuite les explorer un par un. Ce mode
de fonctionnement utilise donc une file dans laquelle il prend le premier sommet et place en
dernier ses voisins non encore explorés.
Lorsque l’algorithme est appliqué à un graphe quelconque, les sommets déjà visités doivent
être marqués afin d’éviter qu’un même sommet soit exploré plusieurs fois. Dans le cas parti-
culier d’un arbre, ce n’est pas nécessaire.
Étapes de l’algorithme :
1. Mettre le nœud de départ dans la file.
2. Retirer le nœud du début de la file pour l’examiner.
3. Mettre tous les voisins non examinés dans la file (à la fin).
4. Si la file n’est pas vide reprendre à l’étape 2.

Pseudo code

76
Algo BFS(G : graphe, s : sommet) :
debut
f ← CreerF ile();
M arquer(s) ;
Enf iler(f, s) ;
tant que non F ileV ide(f ) faire
x ← Sommet(f ) ; Déf iler(f ) ;
T raiter(x) ;
tant que ExisteF ils(x) faire
z ← F ilsSuivant(x) ;
si N on M arquer(z) alors
M arquer(z) ;
Enf iler(f, z) ;
finsi
fintantque
fintantque
fin.
Exemple : Sur le graphe suivant, cet algorithme va alors fonctionner ainsi :

Il explore dans l’ordre les sommets A, B, C, E, D, F, G, contrairement à l’algorithme de


parcours en profondeur qui cherche dans cet ordre : A, B, D, F, C, G, E.

4.3.2 Parcours en profondeur (Cas des graphes non orienté)


L’algorithme de parcours en profondeur (ou DFS, pour Depth First Search) est un algo-
rithme de parcours de graphe qui se décrit naturellement de manière récursive. Son application
la plus simple consiste à déterminer s’il existe un chemin d’un sommet à un autre.
Pour les graphes non orientés, il correspond à la méthode intuitive qu’on utilise pour
trouver la sortie d’un labyrinthe sans tourner en rond.

Principe
C’est un algorithme de recherche qui progresse à partir d’un sommet S en s’appelant
récursivement pour chaque sommet voisin de S.
Le nom d’algorithme en profondeur est dû au fait que, contrairement à l’algorithme de
parcours en largeur, il explore en fait “à fond” les chemins un par un : pour chaque sommet,
il prend le premier sommet voisin jusqu’à ce qu’un sommet n’ait plus de voisins (ou que tous
ses voisins soient marqués), et revient alors au sommet père.

77
Si G n’est pas un arbre, l’algorithme pourrait tourner indéfiniment, c’est pour cela que
l’on doit en outre marquer chaque sommet déjà parcouru, et ne parcourir que les sommets
non encore marqués.
Enfin, on notera qu’il est tout à fait possible de l’implémenter itérativement à l’aide d’une
pile LIFO contenant les sommets à explorer : on dépile un sommet et on empile ses voisins
non encore explorés.

Implémentation récursive
Initialement, aucun sommet n’est marqué.
V oisins(s) : renvoie la liste des sommets adjacents à s.
M arquer(s) : marque un sommet s comme exploré, de manière à ne pas le considérer plusieurs
fois.
Algo DFS (G : graphe, s : sommet)
debut
si N on M arquer(s) alors
T raiter(s) ;
M arquer(s) ;
finsi
pour chaque élément s f ils de V oisins(s) faire
si N on M arquer(s f ils) alors
DFS(G, s f ils) ;
finsi
finpour
fin
Exemple : Voyons concrètement le fonctionnement de cet algorithme sur le graphe suivant :

L’algorithme DFS commence au sommet A, nous conviendrons que les sommets à gauche
sur ce graphe seront choisis avant ceux de droite. Si l’algorithme utilise effectivement un
marquage des sommets pour éviter de tourner indéfiniment en boucle, on aura alors l’ordre
de visite suivant : A, B, D, F, E, C, G.
Supposons maintenant que nous n’utilisions pas la méthode de marquage, on aurait alors la
visite des sommets suivants dans l’ordre : A, B, D, F, E, A, B, D, F, E, etc indéfiniment,
puisque l’algorithme ne peut sortir de la boucle A, B, D, F, E et n’atteindra donc jamais C
ou G.

78
4.4 Algorithme de Dijkstra
En théorie des graphes, l’algorithme de Dijkstra sert à résoudre le problème du plus court
chemin. Il s’applique à un graphe connexe dont le poids lié aux arêtes est positif ou nul.
En théorie de la complexité on démontre que cet algorithme est polynomial.

Applications
– L’algorithme de Dijkstra trouve son utilité dans le calcul des itinéraires routiers. Le
poids des arcs pouvant être la distance (pour le trajet le plus court), le temps estimé
(pour le trajet le plus rapide), le plus économique (avec la consommation de carburant
et le prix des péages).
– Une application des plus courantes de l’algorithme de Dijkstra est le protocole open
shortest path first qui permet un routage internet très efficace des informations en
cherchant le parcours le plus efficace.
– Les routeurs IS-IS utilisent également l’algorithme.

Notations
Le graphe est noté G = (V, E) où :
– l’ensemble V est l’ensemble des sommets du graphe G ;
– l’ensemble E est l’ensemble des arêtes de G tel que : si (s1 , s2 ) est dans E, alors il existe
une arête depuis le nœud s1 vers le nœud s2 ;
– on définit la procédure P oids(s1 , s2 ) définie sur E qui renvoie le poids positif de l’arête
reliant s1 à s2 (et un poids infini pour les paires de sommets qui ne sont pas connectées
par une arête).

Principes
Le poids du chemin entre deux sommets est la somme des poids des arêtes qui le composent.
Pour une paire donnée de sommets sdeb (le sommet du départ) sf in (sommet d’arrivée) appar-
tenant à V , l’algorithme trouve le chemin depuis sdeb vers sf in de moindre poids (autrement
dit le chemin le plus léger ou encore le plus court).
L’algorithme fonctionne en construisant un sous-graphe P de manière à ce que la distance
entre un sommet s de P depuis sdeb soit connue et soit un minimum dans G. Initialement P
contient simplement le nœud sdeb isolé, et la distance de sdeb à lui-même vaut zéro. Des arcs
sont ajoutés à P à chaque étape :
1. en identifiant toutes les arêtes ai = (si1 , si2 ) dans P × G tel que si1 est dans P et si2
est dans G ;
2. en choisissant l’arête aj = (sj1 , sj2 ) dans P × G qui donne la distance minimum depuis
sdeb à sj2 en passant tous les chemins créés menant à ce nœud.
L’algorithme se termine soit quand P devient un arbre couvrant de G, soit quand tous les
nœuds d’intérêt sont dans P .

79
Principe sur un exemple
Il s’agit de construire progressivement, à partir des données initiales, un sous-graphe dans
lequel sont classés les différents sommets par ordre croissant de leur distance minimale au
sommet de départ. La distance correspond à la somme des poids des arêtes empruntées.
La première étape consiste à mettre de côté le sommet de départ et à repérer la distance
du sommet de départ aux autres sommets du graphe. Cette distance est infinie si aucun arc
ne relie le sommet au sommet de départ, elle est de n s’il existe un arc reliant ce sommet au
sommet de départ et que le poids le plus faible (s’il existe plusieurs arcs) est de n.
La seconde étape consiste à repérer le sommet qui possède alors la plus courte distance au
sommet de départ et à le mettre de côté. Pour tous les sommets restants, on compare alors la
distance trouvée précédemment à celle que l’on obtiendrait via le sommet que l’on vient de
mettre de côté et on ne conserve que la plus petite des valeurs. Puis on continue ainsi jusqu’à
épuisement des sommets ou jusqu’à sélection du sommet d’arrivée.
Distance entre la ville A et la ville J
L’exemple suivant montre les étapes successives dans la résolution du chemin le plus court
dans un graphe (Exemple cité dans https ://fr.wikipedia.org/wiki/Algorithme de Dijkstra).
Les nœuds symbolisent des villes identifiées par une lettre et les arêtes indiquent la distance
entre ces villes. On cherche à déterminer le plus court trajet pour aller de la ville A à la ville
J.
Étape 1 : à partir de la ville A, 3 villes sont accessibles, B, C, et E qui se voient donc
affectées des poids respectifs de 85, 217, 173, tandis que les autres villes sont affectées d’une
distance infinie.

Étape 2 : la distance la plus courte est celle menant à la ville B. Le passage par la ville B
ouvre la voie à la ville F (85+80 = 165).

80
Étape 3 : La distance la plus courte suivante est celle menant à la ville F. Le passage par
la ville F ouvre une voie vers la ville I (415).

Étape 4 : La distance la plus courte suivante est alors celle menant à la ville E. Le passage
par la ville E ouvre une voie vers la ville J (675).

81
Étape 5 : la distance la plus courte suivante mène alors à la ville C. Le passage par la ville
C ouvre une voie vers la ville G (403) et la ville H (320).

Étape 6 : la distance la plus courte suivante mène à ville H(320). Le passage par la ville H
ouvre une voie vers la ville D et un raccourci vers la ville J (487< 675).

82
Étape 7 : la distance la plus courte suivante mène à la ville G et ne change aucune autre
distance.

Étape 8 : la distance la plus courte suivante mène à la ville I. Le passage par la ville I ouvre
un chemin vers la ville J qui n’est pas intéressant (415+ 84 > 487).

83
Étape 9 : la distance la plus courte suivante mène à la ville J (487).

On connait ainsi le chemin le plus court menant de A à J, il passe par C et H et mesure 487
km.
Présentation sous forme de tableau
L’illustration par une série de graphes peut se révéler un peu longue. Il est d’autre part
un peu plus difficile de repérer le chemin le plus court à l’issue du dessin. Ainsi l’algorithme
de Dijkstra est souvent réalisé à l’aide d’un tableau dans lequel chaque étape correspond à
une ligne. À partir de la matrice des arcs orientés reliant les diverses villes :

84
On construit un tableau dans lequel les distances d’un sommet au sommet de départ sont
regroupées dans une même colonne. Les sommets sélectionnés sont soulignés. Les distances
des voies ouvertes par la sélection d’un nouveau sommet sont barrées si elles sont supérieures
à des distances déjà calculées. Quand un sommet est sélectionné, c’est que l’on découvert sa
distance minimale au sommet, il est alors inutile de chercher d’autres distances de ce sommet
au point de départ.

La construction de ce tableau donne non seulement la distance minimale de la ville A à la


ville J mais aussi le chemin à suivre (J - H - C - A) ainsi que toutes les distances minimales
de la ville A aux autres villes rangées par ordre croissant.

85
Bibliographie

M. A. Weiss, Data Structures and Algorithm Analysis in Java, Pearson, Third Edition, 2012.

T. H. Cormen, C. E. Leiserson, R. L. Rivest et C. Stein, Introduction à l’algorithmique, ISBN :


2-10-003922-9, 2ème édition, Dunod, 2002.

D. Beauquier, J. Berstel, P. Chrétienne, et al., Eléments d’algorithmique, volume 8, Masson,


1992.

G. Brassard, P. Bratley, Fundamentals of algorithmics, ISBN : 0-13-335068-1, Prentice-Hall,


Inc. Upper Saddle River, NJ, USA, 1996.

R. Sedgewick, P. Flajolet, Introduction a l’analyse des algorithmes, ISBN : 2841809579, In-


ternational Thomson Publishing, 1998.

S. Kannan, M. Naor, et S. Rudich, Implicit Representation of Graphs, SIAM J. on Discrete


Math., volume 5, pages 596-603, 1992.

A. Brygoo, T. Durand, M. Pelletier, C. Queinnec, et al., Programmation récursive (en


Scheme) Cours et exercices corrigés, Collection : Sciences Sup, Dunod, 2004.

86

Vous aimerez peut-être aussi