Vous êtes sur la page 1sur 103

F.

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

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

Algorithmique et Structures de
Données Avancées
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 Introduction à l’Algorithmique, Preuve et Complexité d’un algorithme 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 preuve d’un algorithme . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2.1 Caractéristiques d’un algorithme . . . . . . . . . . . . . . . . . . . . . 7
1.2.2 Preuve d’algorithme par invariant de boucle . . . . . . . . . . . . . . . 8
1.2.3 Preuve d’algorithme par les assertions d’Hoare . . . . . . . . . . . . . . 9
1.3 Complexité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3.1 Définition de la complexité d’un algorithme . . . . . . . . . . . . . . . 11
1.3.2 Calculs de la complexité . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.3.3 Exemples de calculs de complexités . . . . . . . . . . . . . . . . . . . . 19

2 La récursivité 24
2.1 Définition de la récursivité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.2 Différentes configurations de la récursivité . . . . . . . . . . . . . . . . . . . . 25
2.2.1 Récursivité simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.2.2 Récursivité multiple . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.2.3 Récursivité mutuelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.2.4 Récursivité imbriquée . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.3 Intérêt, principe et difficultés de la récursivité . . . . . . . . . . . . . . . . . . 26
2.4 Importance de l’ordre des appels récursifs . . . . . . . . . . . . . . . . . . . . . 27
2.5 Exemple d’algorithme récursif : les tours de Hanoı̈ . . . . . . . . . . . . . . . . 28
2.6 Diviser pour régner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.7 Récursivité terminale et non terminale . . . . . . . . . . . . . . . . . . . . . . 32
2.8 Dérécursivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.8.1 Cas de la récursivité terminale . . . . . . . . . . . . . . . . . . . . . . . 33
2.8.2 Cas de la récursivité non terminale . . . . . . . . . . . . . . . . . . . . 34
2.8.3 Remarques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

3 Structures séquentielles : listes linéaires, piles, files 36


3.1 Structure de données : liste linéaire . . . . . . . . . . . . . . . . . . . . . . . . 36
3.1.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.1.2 Primitives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

1
Table des matières

3.1.3 Implémentation contiguë . . . . . . . . . . . . . . . . . . . . . . . . . 37


3.1.4 Implémentation chaı̂née . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.1.5 Listes chaı̂nées particulières . . . . . . . . . . . . . . . . . . . . . . . . 42
3.1.6 Comparaison des deux implémentations . . . . . . . . . . . . . . . . . 43
3.1.7 Exemple de manipulation des listes . . . . . . . . . . . . . . . . . . . . 43
3.2 Structure de données : pile . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.2.1 Primitives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.2.2 Implémentations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
3.2.3 Exemples de manipulation des piles . . . . . . . . . . . . . . . . . . . . 47
3.3 Structure de donnée : file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
3.3.1 Primitives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
3.3.2 Implémentations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
3.3.3 Usage des files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51

4 Structures Hiérarchiques : arbres, arbres binaires de recherche, TAS 53


4.1 Notion d’Arbres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
4.2 Arbre binaire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
4.2.1 Définitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
4.2.2 Primitives de consultation . . . . . . . . . . . . . . . . . . . . . . . . . 54
4.2.3 Primitives de construction/modifications . . . . . . . . . . . . . . . . . 54
4.2.4 Parcours . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4.2.5 Arbres binaires particuliers . . . . . . . . . . . . . . . . . . . . . . . . . 56
4.2.6 Représentation d’un arbre quelconque sous forme d’un arbre binaire . . 57
4.2.7 Implémentations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
4.3 Arbre général . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
4.3.1 Primitives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
4.3.2 Parcours . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
4.3.3 Implémentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
4.4 Arbre binaire de Recherche . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
4.4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
4.4.2 Recherche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
4.4.3 Ajout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
4.4.4 Suppression . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
4.4.5 Arbres binaires de recherche équilibrés . . . . . . . . . . . . . . . . . . 71
4.5 Structure de données Tas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
4.5.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
4.5.2 Implémentation d’un arbre binaire (quasi-)parfait . . . . . . . . . . . . 76
4.5.3 Tri pas tas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
4.5.4 Ajout d’un élément dans le tas . . . . . . . . . . . . . . . . . . . . . . . 76
4.5.5 Epluchage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
4.5.6 Exemple de tri par TAS . . . . . . . . . . . . . . . . . . . . . . . . . . 78

5 Les graphes 83
5.1 Introduction aux graphes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
5.1.1 Définitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
5.1.2 Graphes particuliers . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
5.2 Représentation d’un graphe . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85

2
Table des matières

5.2.1 Matrice d’adjacence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85


5.2.2 Matrice d’incidence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
5.2.3 Liste d’adjacence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
5.3 Parcours de graphes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
5.3.1 Parcours en largeur (Cas des graphes orienté) . . . . . . . . . . . . . . 88
5.3.2 Parcours en profondeur (Cas des graphes non orienté) . . . . . . . . . . 89
5.4 Algorithme de Dijkstra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91

3
Chapitre 1

Introduction à l’Algorithmique,
Preuve et Complexité d’un algorithme

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
1.1. Introduction à l’algorithmique

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
1.1. Introduction à l’algorithmique

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.

6
1.2. Qualités et preuve d’un algorithme

Un exemple d’algorithme récursif :


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 preuve d’un algorithme


1.2.1 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.

7
1.2. Qualités et preuve d’un algorithme

– 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é.
– 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.

1.2.2 Preuve d’algorithme par invariant de boucle


1.2.2.1 Comment ça marche
Une preuve d’algorithme par invariant de boucle utilise la démarche suivante. Nous prou-
vons tout d’abord que l’algorithme s’arrête en montrant qu’une condition d’exécution de
boucle finit par ne plus être réalisée. Nous exhibons alors un invariant de boucle, c’est-à-dire
une propriété P qui :
– si elle est valide avant l’exécution d’un tour de boucle, est aussi valide après l’exécution
du tour de boucle.
– nous vérifions alors que les conditions initiales rendent la propriété P vraie en entrée
du premier tour de boucle.
– nous en concluons que cette propriété est vraie en sortie du dernier tour de boucle.
Un bon choix de la propriété P prouvera qu’on a bien produit l’objet recherché. La difficulté
de cette méthode réside dans la détermination de l’invariant de boucle. Quand on l’a trouvé
il est en général simple de montrer que c’est bien un invariant de boucle.

8
1.2. Qualités et preuve d’un algorithme

1.2.2.2 Exemple de preuve d’algorithme : 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 ;
Analyse de l’algorithme TRI-INSERTION.
– Preuve de Terminaison : 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.
– Preuve de validité : On a un invariant de boucle pour la boucle pour j ← 2 à l faire
de la ligne 1 qui est “les j premiers éléments sont triés en l’ordre croissant”. Donc à la
fin de l’algorithme, c’est-à-dire quand j = l, le tableau est totalement trié.
– Analyse de la complexité dans le pire des cas en nombre de comparaisons :
sera traitée dans la section 1.3.3.4.

1.2.3 Preuve d’algorithme par les assertions d’Hoare


1.2.3.1 Définitions de base de la logique de Hoare
Il s’agit de prouver formellement la correction d’un algorithme. L’algorithme est carac-
térisé par sa précondition (condition éventuelle à respecter par les données de l’algorithme)
et sa postcondition (condition vraie à la fin de l’algorithme qui définit donc son objectif).
Pour réaliser la preuve, la méthode de Hoare définit des assertions logiques intermédiaires ;
on part de la post condition de l’algorithme et à chaque instruction on regarde quelle est
l’assertion qui doit être vraie avant l’appel de l’instruction pour que l’assertion après l’appel
de l’instruction soit vraie. Si on peut remonter de la sorte (‘backward substitution’) jusqu’à
la précondition de l’algorithme on prouve ainsi sa correction (si la précondition est vraie alors
la postcondition est vraie).
Prouver un algorithme revient donc à prouver :

{condition sur les données} Algorithme {condition exprimant les résultats attendus}

à partir des axiomes et des règles d’inférences de Hoare.

1.2.3.2 Triplets de Hoare


Un triplet de Hoare est de la forme :

{P (x)}S{Q(x)}

9
1.2. Qualités et preuve d’un algorithme

Où : S est un algorithme faisant référence à la variable x et P (x) et Q(x) sont des formules
portant sur les variables de l’algorithme S. Ces deux formules forment la spécification de
l’algorithme S.
Le sens intuitif de {P (x)}S{Q(x)} est le suivant : Si l’exécution de l’algorithme S com-
mence dans un état x qui vérifie P , alors à l’issue de l’exécution de S, l’état mémoire x vérifie
Q.
Exemple : Considérons par exemple un algorithme M ax qui doit donner à une variable z,
la plus grande valeur des variables x et y :

si x > y alors z ← x sinon z ← y

Cet algorithme doit être définit pour tout entier. Sa précondition sera : (x ∈ N, y ∈ N, z ∈ N),
et sa postcondition :(x > y ⇒ z = x) ∧ (x 6 y ⇒ z = y).
Le triplet de Hoare permettant d’énoncer la correction de M ax par rapport à sa spécification
sera :

{x ∈ N, y ∈ N, z ∈ N} si x > y alors z ← x sinon z ← y


{(x > y ⇒ z = x) ∧ (x 6 y ⇒ z = y)}

1.2.3.3 Exemple d’utilisation des règles d’inférence de la Logique de Hoare


Elles expriment comment raisonner sur chacune des instructions du langage. Il existe une
règle pour : la Séquence, l’affectation, la conditionnelle, le tant que, le renforcement de la
précondition, et dualement l’affaiblissement de la postcondition.
Reprenons l’exemple de l’algorithme M ax, sur lequel doit s’appliquer :
la règle d’inférence du conditionnel : Pour démontrer la validité de l’assertion :

{P } si cond alors A sinon B {Q},

il suffit de vérifier la validité des deux assertions :

{P ∧ cond} A {Q} et {P ∧ ¬cond} B {Q}.

la règle d’inférence de l’affectation : L’axiome de substitution arrière :


{Q[x/exp]} x ← exp {Q}
dit que si Q est vraie pour x après l’affectation alors Q est vraie pour exp avant l’affectation.
Il suffit de réécrire l’expression Q avec exp à la place de x.
Donc pour l’algorithme M ax, pour démontrer la validité de l’assertion :

{x ∈ N, y ∈ N, z ∈ N} si x > y alors z ← x sinon z ← y


{(x > y ⇒ z = x) ∧ (x 6 y ⇒ z = y)}

il suffit de démontrer les deux assertions :

{(x ∈ N, y ∈ N, z ∈ N)∧(x > y)} z ← x {(x > y ⇒ z = x) ∧ (x 6 y ⇒ z = y)}

{(x ∈ N, y ∈ N, z ∈ N)∧(x 6 y)} z ← y {(x > y ⇒ z = x) ∧ (x 6 y ⇒ z = y)}

10
1.3. Complexité

1- preuve de l’assertion {(x ∈ N, y ∈ N, z ∈ N)∧(x > y)} z ← x {(x > y ⇒ z = x) ∧ (x 6


y ⇒ z = y)} :
la postcondition {(x > y ⇒ z = x) ∧ (x 6 y ⇒ z = y)} devient après application de la règle
de l’affectation (remplacer toute occurance de z par x) :

(x > y ⇒ x = x) ∧ (x 6 y ⇒ x = y) ⇔ (x > y ⇒ vrai) ∧ (x 6 y ⇒ x = y)


⇔ (vrai) ∧ ((x > y) ∨ (x = y))
⇔x>y

qui coı̈ncide avec la précondition pour tout (x ∈ N, y ∈ N, z ∈ N).


2- preuve de l’assertion {(x ∈ N, y ∈ N, z ∈ N)∧(x 6 y)} z ← y {(x > y ⇒ z = x) ∧ (x 6
y ⇒ z = y)} :
la postcondition {(x > y ⇒ z = x) ∧ (x 6 y ⇒ z = y)} devient après application de la règle
de l’affectation (remplacer toute occurance de z par y) :

(x > y ⇒ y = x) ∧ (x 6 y ⇒ y = y) ⇔ (x > y ⇒ y = x) ∧ (x 6 y ⇒ vrai)


⇔ ((x 6 y) ∨ (y = x)) ∧ (vrai)
⇔x6y

qui coı̈ncide avec la précondition pour tout (x ∈ N, y ∈ N, z ∈ N).


Remarque : Les preuves des algorithmes sont peu utilisées dans la pratique car leur com-
plexité est grande et les erreurs y sont possibles au même titre que dans les programmes. Mais
la maı̂trise des assertions, précondition, postcondition et invariants de boucle, doit aider à
concevoir et à documenter plus rigoureusement les programmes.

1.3 Complexité
1.3.1 Définition de la complexité d’un algorithme
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 à
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

11
1.3. Complexité

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.

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

12
1.3. Complexité

Et voici le grahe représentant la différence entre leurs croissances respectives lorsque n tend
vers l’infini :

1.3.2 Calculs de la complexité


1.3.2.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é.
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

13
1.3. Complexité

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.3.2.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

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

14
1.3. Complexité

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.3.2.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 :
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 :

15
1.3. Complexité

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 :
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
Op(n) = 2n2/2 − 4 ( /2)(2/2+1) + 5n/2
n n

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 à

16
1.3. Complexité

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

17
1.3. Complexité

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 ∞
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)

18
1.3. Complexité

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.3.3 Exemples de calculs de complexités


1.3.3.1 Complexité liéaire
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.3.3.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).

19
1.3. Complexité

1.3.3.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
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.3.3.4 Complexité Quadratique


Completons l’analyse de l’algorithme TRI-INSERTION de la section 1.2.2.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 ).

20
1.3. Complexité

1.3.3.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
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.3.3.6 Complexité exponentielle


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 dis-
pose donc d’un algorithme qui donne à coup sûr la solution optimale mais qui est totalement

21
1.3. Complexité

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.
2) Le problème de la tour de Hanoı̈ (Intuition d’une explosion combinatoire) :

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, tout en respectant les règles suivantes :
– on ne peut déplacer plus d’un disque à la fois,
– on ne peut placer un disque que sur un autre disque plus grand que lui ou sur un
emplacement vide.
On suppose que cette dernière règle est également respectée dans la configuration de départ.

1.3.3.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.
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.

22
1.3. Complexité

1.3.3.8 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é).

23
Chapitre 2

La récursivité

2.1 Définition de la récursivité


De façon générale, une définition récursive est une définition dans laquelle intervient ce
que l’on veut définir.
Un algorithme récursif est un algorithme qui résout un problème en calculant des solutions
d’instances plus petites du même problème. L’approche récursive est un des concepts de base
en informatique. Les premiers langages de programmation qui ont autorisé l’emploi de la
récursivité sont LISP et Algol 60. Depuis, tous les langages de programmation généraux
réalisent une implémentation de la récursivité.
On oppose généralement les algorithmes récursifs aux algorithmes dits itératifs qui s’exé-
cutent sans appeler explicitement l’algorithme lui-même.
En d’autres termes, un algorithme est dit récursif lorsqu’il est défini en fonction de lui-
même. Le but est d’écrire des algorithmes qui résolvent des problèmes que l’on ne sait pas
résoudre soi-même par une méthode ittérative.
La récursivité se retrouve dans d’autres situations, où elle prend parfois d’autres noms :
– L’autosimilarité est le caractère d’un objet dans laquelle on peut trouver des similarités
en l’observant à différentes échelles.
– Les fractales ont cette propriété d’autosimilarité. Le tapis de Sierpinski, est une frac-
tale obtenue à partir d’un carré. Le tapis se fabrique en découpant le carré en neuf
carrés égaux avec une grille de trois par trois, et en supprimant la pièce centrale, et en
appliquant cette procédure récursivement aux huit carrés restants.

– Mise en abyme est un procédé consistant à représenter une œuvre dans une œuvre
similaire, par exemple en incrustant dans une image cette image elle-même. On retrouve
dans ce principe l’autosimilarité et le principe des fractales ou de la récursivité en
mathématiques.

24
2.2. Différentes configurations de la récursivité

2.2 Différentes configurations de la récursivité


2.2.1 Récursivité simple
Prenons la fonction puissance x → xn . Cette fonction peut être définie récursivement :
(
1 si n = 0
xn = n−1
x×x si n > 1

L’algorithme correspondant s’écrit :


P uissance(x, n)
Si n = 0 alors retourner 1
sinon retourner x × P uissance(x, n − 1)

2.2.2 Récursivité multiple


Une définition récursive peut contenir plus d’un appel récursif. Nous voulons calculer ici
les combinaisons C pn en se servant de la relation de Pascal :
(
1 si p = 0 ou p = n
C pn = p p−1
C n−1 + C n−1 sinon

L’algorithme correspondant s’écrit :


Combinaison(n, p)
Si p = 0 ou p = n alors retourner 1
sinon retourner Combinaison(n − 1, p) + Combinaison(n − 1, p − 1)

2.2.3 Récursivité mutuelle


Des définitions sont dites mutuellement récursives si elles dépendent les unes des autres.
Ça peut être le cas pour la définition de la parité :
( (
vrai si n = 0 f aux si n = 0
pair(n) = et impair(n) =
impair(n − 1) sinon pair(n − 1) sinon

Les algorithmes correspondants s’écrivent :


P air(n)
Si n = 0 alors retourner vrai
sinon retourner Impair(n − 1).
Impair(n)
Si n = 0 alors retourner f aux
sinon retourner P air(n − 1)

25
2.3. Intérêt, principe et difficultés de la récursivité

2.2.4 Récursivité imbriquée


La fonction d’Ackermann est définie comme suit :

 n+1
 si m = 0
A(m, n) = A(m − 1, 1) si m > 0 et n = 0

A(m − 1, A(m, n − 1)) sinon

L’algorithme correspondant s’écrit :


Ackermann(m, n)
si m = 0 alors retourner n + 1
sinon si n = 0 alors retourner Ackermann(m − 1, 1)
sinon retourner Ackermann(m − 1, Ackermann(m, n − 1)).

2.3 Intérêt, principe et difficultés de la récursivité


Le grand intérêt de la récursivité est qu’elle permet d’écrire des algorithmes concis et
élégants. Mais essentiellement, certains intérêts se démarquent :
1. décomposer une action répétitive en sous-actions identiques de petites tailles :
a) pour rechercher un élément dans un tableau trié, ou pour trier un tableau (trifu-
sion), on parle de diviser pour régner.
b) pour dessiner ou faire des structures fractales.
2. pour explorer un ensemble de possibilités (par exemple des coups dans un jeu), on
peut faire un appel récursif sur chaque possibilité. La conservation des environnements
locaux permet de revenir en arrière.
3. Sur des structures de données naturellement récursives, il est bien plus facile d’écrire
des algorithmes récursifs qu’itératifs. Certains algorithmes sont extrêmement difficiles
à écrire en itératif. Nous verrons dans les chapitres suivants des specimens de ces struc-
tures tels que les arbres et les graphes.
Le principe de la récursivité est le même que celui de la démonstration par récurrence en
mathématiques. On doit avoir :
– un certain nombre de cas dont la résolution est connue, ces cas simples formeront les
cas d’arrêt de la récursion ;
– un moyen de se ramener d’un cas compliqué à un cas plus simple.
Cependant la récursivité comporte certaines difficultés :
– sa définition peut être abstraite et incompréhensible.
– il faut être sûrs que l’on retombera toujours sur un cas connu, c’est-à-dire sur un cas
d’arrêt ;
– il nous faut nous assurer que la fonction est complètement définie, c’est-à-dire, qu’elle
est définie sur tout son domaine d’applications.
– Le problème essentiel qui reste ouvert est la non décidabilité de la terminaison d’un
algorithme récursif, c.-à-d. qu’il n y a vraiment aucun moyen de déterminer automati-
quement si un algorithme récursif quelconque va terminer.

26
2.4. Importance de l’ordre des appels récursifs

Pour répondre à ces difficultés il faut être sûrs de l’existence d’un ordre strict tel que la suite
des valeurs successives des arguments invoqués par la définition soit strictement monotone,
et finit toujours par atteindre une valeur pour laquelle la solution est explicitement définie.
Exemple : L’algorithme ci-dessous teste si a est un diviseur de b.
Diviseur(a, b)
Si a 6 0 alors Erreur
sinon si a > b alors retourner (a = b) (test d’égalité)
sinon Diviseur(a, b − a)
La suite des valeurs b, b − a, b − 2a, etc. est strictement décroissante, car a est strictement
positif, et on finit toujours pas aboutir à un couple d’arguments (a, b) tel que b−a est négatif,
cas défini explicitement.
Par contre la méthode suivante ne permet pas de traiter tous les cas :
Syracuse(n)
Si n = 0 ou n = 1 alors retourner 1
sinon si n mod 2 = 0 alors retourner Syracuse(n/2)
sinon retourner Syracuse((3 × n) + 1).
Ainsi personne n’a jusqu’à présent été capable de démontrer que la fonction Syracuse
présentée plus haut se termine pour toute valeur de n. Si c’était le cas, elle définirait effecti-
vement la fonction identiquement égale à 1.
La terminaison d’un algorithme récursif peut être un problème extrêmement difficile.
Cependant, pour prouver la terminaison d’un algorithme récursif, la méthode la plus usuelle
est la suivante : chacun des ensembles dans lesquels les paramètres prennent leurs valeurs
sont équipés d’un ordre. Cet ordre ne doit avoir que des chaı̂nes descendantes finies (on dit
qu’il est bien fondé) et être tel que les invocations internes de l’algorithme se font avec des
valeurs plus petites des paramètres, pour l’ordre en question.

2.4 Importance de l’ordre des appels récursifs


Fonction qui affiche les entiers par ordre décroissant, de n jusqu’à 1 :
Décroissant(n)
Si n = 0 alors ne rien faire
sinon début
afficher n
Décroissant(n − 1)
fin
Exécution pour n = 3 :
Appel de Décroissant(3)
Affichage de 3. Appel de Décroissant(2)
Affichage de 2. Appel de Décroissant(1)
Affichage de 1. Appel de Décroissant(0)
L’algorithme ne fait rien.
Résultat affichage d’abord de 3 puis de 2 puis 1 : l’affichage a lieu dans l’ordre décroissant.

27
2.5. Exemple d’algorithme récursif : les tours de Hanoı̈

Intervertissons maintenant l’ordre de l’affichage et de l’appel récursif :


Croissant(n)
Si n = 0 alors ne rien faire
sinon début
Croissant(n − 1)
afficher n
fin
Exécution pour n = 3 :
Appel de Croissant(3)
Appel de Croissant(2)
Appel de Croissant(1)
Appel de Croissant(0)
L’algorithme ne fait rien.
Affichage de 1.
Affichage de 2.
Affichage de 3.
Résultat affichage d’abord de 1 puis de 2 puis 3 : l’affichage a lieu dans l’ordre croissant.
Remarque : L’importance de l’ordre des appels récursifs sera largement utilisé par exemple
dans les différentes stratégies de parcours des arbres et graphes.

2.5 Exemple d’algorithme récursif : les tours de Hanoı̈


Le problème des tours de Hanoı̈ est une application classique de la récursivité. 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 :

28
2.5. Exemple d’algorithme récursif : les tours de Hanoı̈

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

29
2.6. Diviser pour régner

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.

2.6 Diviser pour régner


Principe
Nombres d’algorithmes ont une structure récursive : pour résoudre un problème donné, ils
s’appellent eux-mêmes récursivement une ou plusieurs fois sur des problèmes très similaires,
mais de tailles moindres, résolvent les sous-problèmes de manière récursive puis combinent
les résultats pour trouver une solution au problème initial. Le paradigme diviser pour régner
donne lieu à trois étapes à chaque niveau de récursivité :
Diviser : le problème en un certain nombre de sous-problèmes ;
Régner : sur les sous-problèmes en les résolvant récursivement ou, si la taille d’un sous-
problème est assez réduite, le résoudre directement ;
Combiner : les solutions des sous-problèmes en une solution complète du problème initial.

Exemple : multiplication de matrices


Nous nous intéressons ici à la multiplication de matrices carrés de taille n.
L’algorithme classique est le suivant :
M ultiplier − M atrices(A, B)
Soit n la taille des matrices carrés A et B
Soit C une matrice carré de taille n
Pour i ← 1 à n faire
Pour j ← 1 à n faire
ci,j ← 0
Pour k ← 1 à n faire
ci,j ← ci,j + ai,k .bk,j
FinPour
FinPour
FinPour
Retourner C
Fin
Cet algorithme effectue O(n3 ) multiplications et autant d’additions.

30
2.6. Diviser pour régner

Algorithme diviser pour régner


Dans la suite nous supposerons que n est une puissance exacte de 2. Décomposons les
matrices A, B et C en sous-matrices de taille n/2 × n/2. L’équation C = AB peut alors se
récrire :
    
r s a b e g
= .
t u c d f h

En développant cette équation, nous obtenons :

r = ae + bf , s = ag + bh, t = ce + df et u = cg + dh.

Chacune de ces quatre opérations correspond à deux multiplications de matrices carrés


de taille n/2 et une addition de telles matrices. À partir de ces équations on peut aisément
dériver un algorithme diviser pour régner dont la complexité est donnée par la récurrence :

T (n) = 8T (n/2) + O(n2 ).

l’addition des matrices carrés de taille n = 2 étant en O(n2 ).

Analyse des algorithmes diviser pour régner


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

31
2.7. Récursivité terminale et non terminale

2.7 Récursivité terminale et non terminale


Récursivité non terminale
Un algorithme récursif est dit non terminal si le résultat de l’appel récursif est utilisé pour
réaliser un traitement (en plus du retour d’une valeur).
Exemple de non terminalité : forme récursive non terminale de la factorielle, les calculs
se font à la remontée.
fonction f actorielleN T (n : entier) : entier
début
si (n = 1) alors retourne 1
sinon retourne n ∗ f actorielleN T (n − 1) ;
finsi
fin

Récursivité terminale
Un algorithme récursif est dit terminal si aucun traitement n’est effectué à la remontée
d’un appel récursif (sauf le retour d’une valeur).
Exemple de terminalité : forme récursive terminale de la factorielle, les calculs se font à
la descente.
// la fonction doit être appelée en mettant resultat à 1

32
2.8. Dérécursivation

fonction f actorielleT (n : entier, resultat : entier) : entier


début
si (n = 1) alors retourne resultat
sinon retourne f actorielleT (n − 1, n ∗ resultat) ;
finsi
fin

Intérêt de la récursivité terminale :


– Une fonction récursive terminale est en théorie plus efficace (mais souvent moins facile
à écrire) que son équivalent non terminale : il n’y a qu’une phase de descente et pas de
phase de remontée.
– En récursivité terminale, les appels récursifs n’ont pas besoin d’êtres empilés dans la pile
d’exécution car l’appel suivant remplace simplement l’appel précédent dans le contexte
d’exécution.
– Certains langages utilisent cette propriété pour exécuter les récursions terminales aussi
efficacement que les itérations (ce n’est pas le cas de Java).
– Il est possible de transformer de façon simple une fonction récursive terminale en une
fonction itérative : c’est la dérécursivation.

2.8 Dérécursivation
Dérécursiver, c’est transformer un algorithme récursif en un algorithme équivalent ne
contenant pas d’appels récursifs.

2.8.1 Cas de la récursivité terminale


Une fonction récursive terminale a pour forme générale :
fonction recursive(P ) : retour T
début
I0
si (C) alors I1
sinon début
I2
recursive(f (P ))
fin
fin
T est le type de retour

33
2.8. Dérécursivation

P est la liste des paramètres


C est la condition d’arrêt
I0 le bloc d’instructions exécuté dans tous les cas
I1 le bloc d’instructions exécuté si C est vraie
I2 et le bloc d’instructions exécuté si C est fausse
f la fonction de tranformation des paramètres
La fonction itérative correspondante est :
fonction iterative(P ) : retour T
début
I0
Tant que (non C) faire
I2
P ← f (P ) ;
I0 ;
finTantque
I1
fin
Exemple : dérécursivation de la factorielle terminale.
Fonction f actorielleRecurT erm(n : entier, a : entier) : entier
// Cette fonction doit être appelée avec a = 1
début
si (n <= 1) alors retourne a
sinon retourne f actorielleRecurT erm(n − 1, n ∗ a) ;
fin
fonction f actorielleIterT erm(n : entier, a : entier) : entier
début
Tant que (n > 1) faire
a ← n ∗ a;
n ← n − 1;
finTantque
retourne a ;
fin.

2.8.2 Cas de la récursivité non terminale


Une fonction récursive non terminale a pour forme générale :
fonction recursive(P ) : retour T
début
I0
si (C) alors I1
sinon début
I2
recursive(f (P )) ;
I3
fin
fin

34
2.8. Dérécursivation

T est le type de retour


P est la liste des paramètres
C est la condition d’arrêt
I0 le bloc d’instructions exécuté dans tous les cas
I1 le bloc d’instructions exécuté si C est vraie
I2 et I3 les blocs d’instructions exécutés si C est fausse
f la fonction de transformation des paramètres
La fonction itérative correspondante doit gérer la sauvegarde des contextes d’exécution (va-
leurs des paramètres de la fonction). La fonction itérative correspondante est donc moins
efficace qu’une fonction écrite directement en itératif.

2.8.3 Remarques
Les programmes itératifs sont souvent plus efficaces, mais les programmes récursifs sont
plus faciles à écrire. Les compilateurs savent, la plupart du temps, reconnaı̂tre les appels
récursifs terminaux, et ceux-ci n’engendrent pas de surcoût par rapport à la version itérative
du même programme. Il est toujours possible de dérécursiver un algorithme récursif.

35
Chapitre 3

Structures séquentielles : listes


linéaires, piles, files

3.1 Structure de données : liste linéaire


Une liste linéaire est la forme la plus courante d’organisation des données. On l’utilise
pour stocker des données qui doivent être traitées de manière séquentielle. La structure doit
également être évolutive, c’est à dire que l’on doit pouvoir ajouter et supprimer des éléments.

3.1.1 Définition
Une liste est une suite finie (éventuellement vide) d’éléments de même type repérés selon leur
rang dans la liste.
On remarque que l’ordre des éléments est fondamental. Mais attention, il ne s’agit pas d’un
ordre sur les valeurs des éléments mais d’un ordre sur les places dans la liste (rang). Chaque
élément est rangé à une certaine position. Il ne faut pas confondre le rang et la position.

3.1.2 Primitives
On définit le type abstrait de données par la définition des primitives qui permettent de le
manipuler :
– créer liste() : liste ; Création d’une liste vide,
– début( L : liste) : position ; Retourne la position du premier élément de la liste, IN-
CONNUE si la liste est vide,
– fin( L : liste) : position ; Retourne la position du dernier élément de la liste, INCONNUE
si la liste est vide,
– suivante( p : position, L : liste) : position ; Retourne la position de l’élément qui suit
celui en position p, INCONNUE si on sort de la liste,
– précédente( p : position, L : liste) : position ; Retourne la position de l’élément qui
précède celui en position p, INCONNUE si on sort de la liste,
– accès(p : position, L : liste) : élément ; Retourne l’élément en position p,
– longueur(L : liste) : entier ; Retourne le nombre d’éléments contenus dans la liste,

36
3.1. Structure de données : liste linéaire

– insérer(e : élément, r : rang, L : liste) ; Rajoute l’élément dans la liste (qui est donc
modifiée) au rang r,
– supprimer(r : rang, L : liste) ; Supprime l’élément de rang r dans la liste (qui est donc
modifiée),
– liste est vide(L : liste) : booléen ; Teste si la liste est vide, retourne VRAI ou FAUX,
– ieme(r : rang, L : liste) : élément ; Retourne l’élément de rang r,
– ajouter(e : élément, p : position, L : liste) ; Rajoute l’élément dans la liste (qui est
donc modifiée) après celui en position p,
– enlever(p : position, L : liste) ; Supprime l’élément de position p dans la liste (qui est
donc modifiée).

3.1.3 Implémentation contiguë


Les éléments sont rangés les uns à côté des autres, dans un tableau. La ième case du
tableau contient le ième élément de la liste. Le rang est égal à la position.
type position = entier
type liste = enregistrement {
t : tab[1..M AX] d’éléments
nbr : entier // nbr = N le nombre effectifs d’éléments dans la liste
}
Algo creer liste vide() : retourne une liste locales // Algorithme en O(1)
Var L : liste
debut
L.nbr ← 0
retourner L
Fin
Algo acces( p : position, L : liste) : retourne un élément // Algorithme en O(1)
debut
retourner L.t[p]
fin
Algo début(L : liste) : retourne une position // Algorithme en O(1)
debut
si L.nbr >= 1 alors retourner 1
sinon retourner IN CON N U E
finsi
fin
Algo suivante(p : position, L : liste) : retourne une position // Algorithme en O(1)
debut
si p < L.nbr alors retourner p + 1
sinon retourner IN CON N U E
finsi
fin

37
3.1. Structure de données : liste linéaire

Algo précédente(p : position, L : liste) : retourne une position // Algorithme en O(1)


debut
si p > 1 alors retourner p − 1
sinon retourner IN CON N U E
finsi
fin
Algo longueur (L : liste) // Algorithme en O(1)
retourner L.nbr
Algo fin(L : liste) : retourne une position // Algorithme en O(1)
retourner L.nbr
Algo est vide(L : liste) : retourne un booleen // Algorithme en O(1)
debut
si L.nbr = 0 alors retourner V RAI
sinon retourner F AU X
finsi
fin
Algo inserer(e : élément, r : rang, VAR L : liste ) // Algorithme en O(N )
Var i : rang
debut
pour i ← L.nbr à r (en décrémentant i) faire // décaler vers la droite pour faire un trou
L.t[i + 1] ← L.t[i]
finpour
L.t[r] ← e // mettre l’élément
L.nbr ← (L.nbr) + 1 // changer la taille car il y a un élément de plus
fin
Algo supprimer(r : rang, VAR L : liste ) // Algorithme en O(N )
Var i : rang
debut
pour i ← r à L.nbr − 1 faire // décaler vers la gauche pour écraser l’élément
L.t[i] ← L.t[i + 1]
finpour
L.nbr ← (L.nbr) − 1 // changer la taille car il y a un élément en moins
fin

3.1.4 Implémentation chaı̂née


Les éléments ne sont pas rangés les uns à côté des autres. On a besoin de connaı̂tre, pour
chaque élément, la position (l’adresse) de l’élément qui le suit. La position n’a donc plus rien
à voir avec le rang, mais c’est l’adresse d’une structure qui contient l’élément ainsi que la
position du suivant.

38
3.1. Structure de données : liste linéaire

type cellule = enregistrement {


valeur : élément
suivant : adresse d’une cellule
}
type liste = adresse d’une cellule
Une adresse s’appelle aussi un pointeur. L’adresse nulle sera notée V IDE, ainsi que le contenu
d’une cellule d’adresse D sera notée D ↑
Algo creer liste vide() : retourne une liste // Algorithme en O(1)
debut
retourner V IDE
fin
Algo début( L : liste) : retourne une position // Algorithme en O(1)
debut
retourner L // adresse de la 1ere cellule
fin
Algo est vide(L : liste) : retourne un booleen // Algorithme en O(1)
debut
retourner L ← V IDE
fin
Algo acces(p : position, L : liste) : retourne un élément // Algorithme en O(1)
debut
retourner p ↑ .valeur
fin
Algo longueur(L : liste) : retourne un entier // Algorithme en O(N )
Var p : position ; l : entier ;
debut
l ← 0; p ← L;
tant que p <> V IDE faire
p ← p ↑ .suivant
l ←l+1
fintantque
retourner l
fin

39
3.1. Structure de données : liste linéaire

Algo suivante(p : position, L : liste) : retourne une position // Algorithme en O(1)


debut
retourner p ↑ .suivant
fin
Algo précédente( p : position, L : liste) : retourne une position // Algorithme en O(N )
(N =nombre d’éléments)
Var q : position ; trouv : booleen ;
debut
si L = V IDE alors erreur ”la liste est vide”
sinon
si p = L alors // Le précédant du premier n’existe pas
retourner V IDE
sinon // se positionner sur l’élément qui précéde celui pointé par p s’il existe
q ← L ; trouv ← F AU X ;
tant que q <> V IDE et non(trouv) faire
si q ↑ .suivant = p alors trouv ← V RAI
sinon q ← q ↑ .suivant
fintantque
si q = V IDE alors erreur ”l’élément pointé par p n’exite pas dans la liste”
sinon retourner q
finsi
finsi
finsi
fin
Algo inserer(e : élément, r : rang, VAR L : liste) // Algorithme en O(N )
Var p : position ; nouv : adresse d’une cellule ; i : entier(rang) ;
debut
nouv ← cree cellule(e) // allouer de la memoire pour une cellule et y mettre e
//dans le champs valeur
si r = 1 alors // cas particulier : insertion en tête
nouv ↑ .suiv ← L
L ← nouv // ATTENTION : L est modifié
sinon // se positionner sur le r − 1ème élément s’il existe
p ← L; i ← 1;
tant que p <> V IDE et i < r − 1 faire
p ← p ↑ .suivant
i←i+1
fintantque
si p = V IDE alors erreur ”la liste ne comporte pas assez d’éléments”
sinon // rattacher l’élément nouv à la liste après l’élément de position p
nouv ↑ .suivant ← p ↑ .suivant
p ↑ .suivant ← nouv
finsi
finsi
fin

40
3.1. Structure de données : liste linéaire

Algo supprimer(r : rang, VAR L : liste) // Algorithme en O(N )


Var p : position
i : entier(rang)
debut
si L = V IDE alors erreur ”la liste est vide”
sinon
si r = 1 alors // cas particulier : suppression en tête
L ← L ↑ .suiv // ATTENTION : L est modifié
sinon // se positionner sur le r − 1ème élément s’il existe
p←L
i←1
tant que p <> V IDE et i < r − 1 faire
p ← p ↑ .suivant
i←i+1
fintantque
si p = V IDE alors erreur ”la liste ne comporte pas assez d’éléments”
sinon si p ↑ .suivant = V IDE alors erreur ”la liste ne comporte pas assez d’éléments”
sinon p ↑ .suivant ← (p ↑ .suivant) ↑ .suivant
finsi
finsi
finsi
fin
Algo enlever(p : position, VAR L : liste) // Algorithme en O(N )
Var q : position
trouv : booleen
debut
si L = V IDE alors erreur ”la liste est vide”
sinon
si p = L alors // cas particulier : suppression en tête
L ← L ↑ .suiv // L est modifié
sinon // se positionner sur l’élément qui précéde celui pointé par p s’il existe
q←L
trouv = F AU X
tant que q <> V IDE et non(trouv) faire
si q ↑ .suivant = p alors trouv = V RAI
sinon q ← q ↑ .suivant
fintantque
si q = V IDE alors erreur ”la liste ne comporte pas assez d’éléments”
sinon si q ↑ .suivant = V IDE alors erreur ”la liste ne comporte pas assez d’éléments”
sinon q ↑ .suivant ← (q ↑ .suivant) ↑ .suivant
finsi
finsi
finsi
fin

41
3.1. Structure de données : liste linéaire

Algo ajouter(e : élément, p : position, VAR L : liste) // Algorithme en O(1)


Var nouv : adresse d’une cellule
debut
nouv ← cree cellule(e) // allouer de la memoire pour une cellule et y mettre e dans
// le champs valeur
si L = V IDE alors // cas particulier : insertion en tête
nouv ↑ .suiv ← V IDE
L ← nouv // L est modifié
sinon // rattacher l’élément nouv à la liste après l’élément de position p
nouv ↑ .suivant ← p ↑ .suivant
p ↑ .suivant ← nouv
finsi
fin

3.1.5 Listes chaı̂nées particulières


3.1.5.1 Listes chaı̂nées circulaires
Le cycle est la propriété que présente une liste chaı̂née sous forme de boucle (ou chaı̂ne
fermée). La notion de début ou de fin de chaı̂ne disparaı̂t. Une liste cyclique (ou circulaire) est
créée lorsque le dernier élément possède une référence vers le premier élément. L’utilisation
de ce type de liste requiert des précautions pour éviter des parcours infinis, par exemple, lors
d’une recherche vaine d’élément.

3.1.5.2 Listes doublement chaı̂nées


Certaines actions peuvent demander de parcourir la liste à l’envers. Si c’est une action
souvent demandée, il peut être avantageux de rajouter un pointeur vers l’élément précédent
dans la structure de données.

42
3.1. Structure de données : liste linéaire

type cellule = enregistrement {


valeur : élément
suivant : adresse d’une cellule
precedent : adresse d’une cellule }
type liste = adresse d’une cellule
Il faut bien sûr penser à le mettre à jour comme il faut dans toutes les primitives. Dans ce
cas l’ajout/suppression connaissant la position de l’élément ne nécessite plus de parcours de
la liste, et devient donc en O(1).
Et on peut bien sûr faire des listes circulaires doublement chaı̂nées, dans ce cas le premier
élément possède aussi une référence vers le dernier.

3.1.6 Comparaison des deux implémentations


Opération contiguë doublement chaı̂née
Création O(1) O(1)
Ajout/Suppression en queue connaisant la position O(1) O(1)
Ajout/Suppression en tête connaisant la position O(N ) O(1)
Ajout/Suppression générale connaisant la position O(N ) O(1)
Ajout/Suppression en queue connaisant le rang O(1) O(N )
Ajout/Suppression en tête connaisant le rang O(N ) O(1)
Ajout/Suppression générale connaisant le rang O(N ) O(N )
Accès à l’élément de rang r O(1) O(N )
Accès à l’élément de position p O(1) O(1)
Concatenation O(N ) O(1)
Représentation contiguë :
– Accès facile au k ème élément, mais insertion et suppression longues à cause des décalages.
– Problèmes de mémoire à allouer, réallouer, etc, lorsque la taille varie.
Représentation doublement chaı̂née :
– Pas de longueur fixée (mais plus de mémoire prise (place pour les pointeurs)).
– Insertion et suppression rapides, mais pas d’accès direct au k ème élément.
Conclusion : Le choix de l’implémentation dépend de ce qu’on a à faire, et des primitives
que l’on utilise le plus souvent.

3.1.7 Exemple de manipulation des listes


Question : Etant une liste d’éléments triés par ordre croissant, insérer un nouvel élément e
de telle sorte à ce que la liste reste triée.
Objectif : Créer une liste d’éléments triés, le tri se fait lors de l’insertion un à un.

43
3.2. Structure de données : pile

Principe :
– si la liste est vide faire l’insertion de e au rang 1.
– sinon chercher le rang r tel que e soit supérieur à l’élément de rang r − 1 et inférieur à
celui du rang r. Dans ce cas e sera inséré au rang r.
Fonction insertion-Tri(e : élément, L : liste) : liste
Var r : entier
debut
si est vide(L) alors inserer(e, 1, L) // cas particulier : insertion en tête
sinon // chercher le rang r où insérer e
r←1
Tant que ieme(r, l) < e faire
r ←r+1
fin tant que
inserer(e, r, L)
retourner L
finsi
fin

3.2 Structure de données : pile


Une pile est une liste linéaire particulière : on ne peut accéder (= consulter, ajouter,
supprimer) qu’au dernier élément, que l’on appelle le sommet de la pile. Dans une pile, il est
impossible d’accéder à un élément au milieu.
Les piles sont aussi appelées structures LIFO pour Last In First Out, cad dernier-entré-
premier-sorti. C’est une structure très très utilisée en informatique. Par exemple, les appels
de fonctions d’un programme utilisent une pile pour sauvegarder toutes les informations
(variables, adresse de l’instruction de retour, etc. ).

3.2.1 Primitives
– créer pile() : pile ; création d’une pile vide
– créer pile(taille max : entier ) : pile ; creation d’une pile vide d’au maximum taille max
éléments
– empiler(élément, pile) : pile ; met l’élément en sommet de pile, erreur si la taille est
limitée et la pile pleine
– depiler(pile) : pile ; enlève de la pile l’élément en sommet de pile, erreur si la pile est
vide

44
3.2. Structure de données : pile

– dépiler(pile) : élément ; enlève l’élément en sommet de pile et le retourne, erreur si la


pile est vide
– sommet(pile) : élément ; retourne l’élément en sommet de pile, erreur si la pile est
vide
– pile est vide(pile) : booléen ; teste si la pile est vide
– pile est pleine(pile) : booléen ; teste si la pile est pleine (seulement dans le cas d’une
taille limitée)
Remarque : Quelque soit l’implémentation ces primitives sont de O(1).

3.2.2 Implémentations
3.2.2.1 Implémentation d’une pile par un tableau
type pile = enregistrement {
t : tab [1..max] d’éléments
s : entier // indice du sommet
}
Le sommet de pile correspond au dernier élément effectif du tableau (d’indice s). La gestion
de la pile se fait en ajoutant et supprimant en fin de tableau.
Algorithme creerPile() : retourne une pile
Var P : pile
debut
P.s ← 0 // alloc du tableau si nécessaire
retourner P
fin
Algorithme empiler (e : élément, Var p : pile)
debut
si p.s = max alors erreur “la pile est pleine”
sinon
p.s ← p.s + 1
p.t[p.s] ← e
finsi
fin
Algorithme depiler (Var p : pile)
debut
si p.s = 0 alors erreur “la pile est vide”
sinon p.s ← p.s − 1
finsi
fin
Algorithme sommet (p : pile) : élément
Var e : élément
debut
si p.s = 0 alors erreur “la pile est vide”
sinon retourner p.t[p.s]
finsi
fin

45
3.2. Structure de données : pile

Le problème de cette implémentation est le problème des tableaux, c’est à dire qu’on est
limité par la taille. L’avantage des tableaux sur les listes chaı̂nées c’est l’accès direct à un
élément, or ça ne nous intéresse pas dans les piles. Donc c’est mieux d’implémenter par une
liste chaı̂née.

3.2.2.2 Implémentation d’une pile par une liste chaı̂née


Par exemple une liste chaı̂née avec lien vers le sommet :

type cellule = enregistrement {


valeur : élément
suivant : adresse d’une cellule
}
type pile = adresse d’une cellule // le sommet
Algo empiler (e : élément, Var p :pile)
Var nouv : adresse d’une cellule
debut
nouv ← cree cellule() // allouer de la memoire et init les champs de la nouvelle cellule
(nouv ↑).valeur ← e
(nouv ↑).suivant ← p // faire les liens : ajout en tete
p ← nouv
fin
Algo depiler (Var p :pile)
debut
si p = V IDE alors erreur”la pile est vide”
sinon p ← p ↑ .suivant // suppression en tete
finsi
fin
Algo sommet (p :pile) : élément
debut
si p = V IDE alors erreur”la pile est vide”
sinon retourner p ↑ .valeur
finsi
fin

46
3.2. Structure de données : pile

3.2.3 Exemples de manipulation des piles


3.2.3.1 Equilibrage des symboles (Dire si une expression parenthèsée est
bien-formée ou non)
Les compilateurs vérifient les erreurs syntaxiques dans les programmes, mais souvent un
symbole manquant (comme un crochet) peut provoquer la production par le compilateur
d’une centaine de lignes de diagnostic sans identifier l’erreur réelle.
Un outil utile dans cette situation est un programme qui vérifie si tout est équilibré. Ainsi, à
chaque accolade droite correspond son homologue gauche. La séquence [()] est bien-formée,
mais [(]) ne l’est pas.
Pour plus de simplicité, nous allons juste vérifier l’équilibrage des parenthèses, crochets et
accolades et d’ignorer tout autre caractère qui apparaı̂t dans la chaı̂ne en entrée.
L’algorithme utilise simplement une pile et se présente comme suit :
1. Créer une pile vide
2. Parcourir la chaine en entrée caractère par caractère :
a) Si le caractère est un symbole d’ouverture ’(’ ou ’[’ ou ’{’ alors l’empiler
b) Si le caractère est symbole de fermeture ’)’ ou ’]’ ou ’}’ alors
i. Si la pile est vide alors signaler une erreur
ii. Sinon vérifier que le symbole en sommet de pile est le symbole ouvrant associé
au caractère fermant rencontré dans la chaı̂ne, dans ce cas le dépiler, dans le
cas contraire signaler une erreur.
3. En fin de parcours la chaine n’est équilibrée que si la pile est vide et qu’aucune erreur
n’a été signalé.
Voici avec plus de détails la fonction correspondante :
Fonction equilibré(C : chaine[max]) : booleen ;
Var P :pile ; i :entier ; pb :booleen ;
debut
i←1
pb ← f aux
P ←creerpile()
Tant que i 6 longueur(C) et non(pb) faire
si C[i] =0 (0 ou C[i] =0 [0 ou C[i] =0 {0 alors empiler(C[i], P )
sinon si (C[i] =0 )0 ou C[i] =0 ]0 ou C[i] =0 }0 ) et pile est vide(P ) alors pb ← vrai
sinon si (C[i] =0 )0 et sommet(P )=’(’) ou (C[i] =0 ]0 et sommet(P )=’[’) ou
(C[i] =0 }0 et sommet(P )=’{’) alors depiler(P )
sinon pb ← vrai ;
i←i+1
Fin tant que
Retourner non(pb) et pile est vide(P )
Fin.

47
3.3. Structure de donnée : file

3.2.3.2 Pile d’exécution


Pendant l’exécution d’un algorithme, on doit pouvoir déterminer à tout moment quelle
est la prochaine instruction à exécuter. En général, c’est celle qui suit dans le texte de l’algo-
rithme. Dans le cas de l’appel d’une procédure ou fonction, il faut se rappeler de reprendre
l’exécution à l’instruction qui suit l’appel de la procédure ou fonction. Pour cela on gère une
pile des points de retour. À chaque appel, on empile le point de retour, à chaque retour, on
dépile et reprend l’exécution à ce point.
La pile d’exécution est une structure de données de type pile qui sert à enregistrer des
informations au sujet des fonctions actives dans un programme. Une pile d’exécution est
utilisée pour emmagasiner plusieurs valeurs, mais sa principale utilisation est de garder la
trace de l’endroit où chaque fonction active doit retourner à la fin de son exécution (les fonc-
tions actives sont celles qui ont été appelées, mais n’ont pas encore terminé leur exécution).
Si, par exemple, un programme DessineCarré appelle une fonction DessineLigne à quatre
endroits différents, la fonction DessineLigne doit avoir un moyen de savoir où poursuivre
l’exécution à la fin de chacune de ses exécutions. Cela est fait par chacun des appels à la fonc-
tion DessineLigne qui place l’adresse de l’instruction suivant l’appel (l’adresse de retour) sur
la pile d’exécution avant de transférer le contrôle de l’exécution à la fonction DessineLigne.
Étant donné que la pile d’exécution est une pile, l’appelant empile l’adresse de retour
sur la pile, et la fonction appelée, quand elle se termine, récupère l’adresse de retour au
sommet de la pile d’exécution (et y transfère le contrôle). Si une fonction appelée appelle
une autre fonction, elle empilera son adresse de retour sur la pile d’exécution. Les adresses
de retour s’accumulent donc sur la pile d’exécution et sont récupérées une à une lors de la fin
de l’exécution des fonctions. Si l’accumulation des adresses de retour consomme tout l’espace
alloué à la pile d’exécution, un message d’erreur appelé un dépassement de pile se produit.
En plus d’emmagasiner des adresses de retour, la pile d’exécution emmagasine aussi
d’autres valeurs associées comme les variables locales de la fonction, les paramètres de la
fonction, etc. Dans les langages de programmation de haut niveau, les spécificités de la pile
d’exécution sont cachées au programmeur. Le programmeur a uniquement accès aux appels
de fonctions et aux paramètres associés, et non au contenu de la pile elle-même.

3.3 Structure de donnée : file


Une file est une liste linéaire particulière : on ne peut ajouter qu’en queue, consulter qu’en
tête, et supprimer qu’en tête. Les files sont aussi appelées structures FIFO pour First In First
Out, cad premier-entré-premier-sorti.

48
3.3. Structure de donnée : file

3.3.1 Primitives
– créer file() : file ; création d’une file vide
– créer file(taille max : entier ) : file ; création d’une file vide d’au maximum taille max
éléments
– enfiler(élément, file) : file ; met l’élément à la fin de la file, erreur si la taille est limitée
et la pile pleine
– défiler(file) : file ; enlève le premier élément de la file, erreur si la file est vide
– consulter(file) : élément ; retourne le premier élément de la file , erreur si la file est
vide
– file est vide(file) : booléen ; teste si la file est vide
– file est pleine(file) : booléen ; teste si la file est pleine (seulement dans le cas d’une
taille limitée)
Remarque : Même remarque que pour les piles, quelque soit l’implémentation ces primitives
sont de O(1).

3.3.2 Implémentations
3.3.2.1 Implémentation d’un file par un tableau
L’idée conceptuellement simple mais un peu délicate à programmer, est de gérer deux
indices : f in qui marque la position qui précède celle où ajouter le prochain élément et deb
qui marque la position d’où proviendra le prochain élément enlevé. L’indice deb marque le
début de la file et f in sa fin.

type file = enregistrement {


t : tab [1..M AX] d’éléments
deb : entier // l’indice de l’élément en tête de file
f in : entier // l’indice de l’élément en queue de file
nbr : entier // nombre effectif d’élément dans la file
}
Initialement les indices deb et f in ainsi que nbr sont mis à 0.
Algo enfiler(e : élément, Var F : file)
debut
si F.f in < M AX alors
F.f in ← F.f in + 1
F.t[F.f in] ← e
F.nbr ← F.nbr + 1
si F.deb = 0 alors F.deb ← 1 // cas du premier élément inséré
sinon erreur “file pleine”
finsi
fin

49
3.3. Structure de donnée : file

Algo defiler(Var F : file)


debut
si F.debut <= F.f in alors
F.debut ← F.debut + 1
F.nbr ← F.nbr − 1
si F.nbr = 0 alors //supression du dernier élément
F.debut ← 0 //remise à zero des indices
F.f in ← 0
finsi
sinon erreur ”file vide”
finsi
fin
La file est pleine dès que f in touche la fin du tableau. Mais toutes les places libres au
début du tableau son inutilisées. Pour résoudre ce problème on utilise le tableau circulaire.

3.3.2.2 Implémentation d’une file par un tableau circulaire


Dans le schéma ci-dessus, les cases valides sont grisées, de sorte que la file ci-dessus contient
les entiers 2, 7, 0, 9 (dans l’ordre, 2 est le premier entré et 9 le dernier entré). Au cours de
la vie de la file, les deux indices sont croissants. Plus précisément, on incrémente f in après
ajout et on incrémente deb après suppression. Lorsqu’un indice atteint la fin du tableau, il
fait tout simplement le tour du tableau et repart à zéro. Il en résulte que l’on peut avoir
f in < deb. Par exemple, voici une autre file contenant cette fois 2, 0, 4, 6, 2, 7.

On parcourt le tableau circulaire en incrémentant un indice modulo M AX, où M AX est la


taille du tableau.
Algo enfiler(e : élément, Var F : file)
debut
si F.nbr = M AX alors erreur “file pleine”
sinon
si F.f in < M AX alors
F.f in ← F.f in + 1
sinon
F.f in ← 1
finsi
F.t[F.f in] ← e
F.nbr ← F.nbr + 1
si F.deb = 0 alors F.deb ← 1 //cas d’insertion du premier élément
finsi
fin
Ou encore : F.f in ← ((F.f in + 1)modulo(M AX + 1)) − 1 .

50
3.3. Structure de donnée : file

Algo defiler(Var F : file)


debut
si F.nbr = 0 alors erreur “file vide”
sinon
F.nbr = F.nbr − 1
si F.f in = F.deb alors // reste un seule élément dans la file
F.deb ← 0 //remettre les indices à zero
F.f in ← 0
sinon
si F.deb < M AX alors
F.deb ← F.deb + 1
sinon
F.deb ← 1
finsi
finsi
finsi
fin

3.3.2.3 Implémentation d’une file par une liste chaı̂née


L’implémentation est conceptuellement simple. Les éléments de la file sont dans une liste,
on enlève au début de la liste et on ajoute à la fin de la liste. Pour garantir des opérations
en temps constant (O(1)), on utilise une référence sur la dernière cellule de liste.

type file= enregistrement {


debut : adresse d’une cellule //la premiere
f in : adresse d’une cellule // la derniere
}
La primitive d’enfilement est analogue à l’insertion en fin de liste, et la primitive de défilement
à la suppression en tête de liste.

3.3.3 Usage des files


Il existe de nombreux algorithmes qui utilisent des files d’attente pour améliorer la com-
plexité d’un problème. Plusieurs d’entre eux se trouvent dans la théorie des graphes, nous
allons en discuter dans les chapitres 4 et 5 (parcours en largeur d’un arbre général et d’un
graphe orienté). Pour l’instant, nous allons donner quelques exemples simples d’utilisation de
la file d’attente.

51
3.3. Structure de donnée : file

Lorsque les travaux sont soumis à une imprimante, ils sont classés par ordre d’arrivée.
Ainsi, essentiellement, les travaux envoyés à une imprimante de ligne sont placés sur une file
d’attente.
Un autre exemple concerne les réseaux informatiques. Il existe de nombreuses configu-
rations de réseaux d’ordinateurs personnels, dans lequel le disque est fixé à une machine,
connue sous le nom de serveur de fichiers. Les utilisateurs sur d’autres machines ont accès à
des fichiers sur la base du premier arrivé- premier servi, de sorte que la structure de données
soit une file d’attente.
D’autres exemples comportent ce qui suit :
– Les appels vers les grandes entreprises sont généralement placés sur une file d’attente
lorsque tous les opérateurs sont occupés.
– Dans les grandes universités, où les ressources sont limitées, les étudiants doivent signer
une liste d’attente si tous les terminaux sont occupés. L’étudiant qui a occupé un
terminal le plus longtemps est forcé en premier à le laisser, et l’étudiant qui a le plus
attendu est le prochain utilisateur à être autorisé à accéder au terminal.

52
Chapitre 4

Structures Hiérarchiques : arbres,


arbres binaires de recherche, TAS

4.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é.

4.2 Arbre binaire


4.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.

53
4.2. Arbre binaire

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.

4.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.

4.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)
.
– ..

4.2.4 Parcours
But : passer en revue (pour un traitement quelconque) chaque sommet une et une seule fois.

54
4.2. Arbre binaire

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

4.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

4.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

55
4.2. Arbre binaire

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) ;
Parcours infixe(fils droit(racine)) ;
finsi
fin

4.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

4.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.

56
4.2. Arbre binaire

4.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.
Exemple :

4.2.7 Implémentations
4.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

57
4.2. Arbre binaire

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

58
4.2. Arbre binaire

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.

4.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
}

59
4.2. Arbre binaire

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

60
4.2. Arbre binaire

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

61
4.3. Arbre général

4.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. . .

4.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)
.
– ..

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

4.3.2.1 Parcours en profondeur


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

62
4.3. Arbre général

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

63
4.3. Arbre général

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

4.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

64
4.3. Arbre général

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

65
4.3. Arbre général

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
}

66
4.4. Arbre binaire de Recherche

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.

4.4 Arbre binaire de Recherche


4.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 :

67
4.4. Arbre binaire de Recherche

4.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 :

4.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.

68
4.4. Arbre binaire de Recherche

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 :

69
4.4. Arbre binaire de Recherche

4.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 :

70
4.4. Arbre binaire de Recherche

4.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

71
4.4. Arbre binaire de Recherche

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.

72
4.4. Arbre binaire de Recherche

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 :

73
4.4. Arbre binaire de Recherche

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.

74
4.4. Arbre binaire de Recherche

L’arbre devient après équilibrage :

Exemple 3 : Le point critique est le 6.

Devient après équilibrage :

75
4.5. Structure de données Tas

4.5 Structure de données Tas


4.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é.

4.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

4.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.

4.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 :

76
4.5. Structure de données Tas

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

4.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

77
4.5. Structure de données Tas

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

4.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.

4.5.6.1 Construction du TAS


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

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

78
4.5. Structure de données Tas

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 :

79
4.5. Structure de données Tas

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

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

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

4.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 :

80
4.5. Structure de données Tas

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 :

81
4.5. Structure de données Tas

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.

82
Chapitre 5

Les graphes

5.1 Introduction aux graphes


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

5.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.

83
5.1. Introduction aux graphes

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.

84
5.2. Représentation d’un graphe

5.2 Représentation d’un graphe


5.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 :

85
5.2. Représentation d’un graphe

5.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 :

86
5.2. Représentation d’un graphe

5.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 :

87
5.3. Parcours de graphes

5.3 Parcours de graphes


5.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

88
5.3. Parcours de graphes

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.

5.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.

89
5.3. Parcours de graphes

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.

90
5.4. Algorithme de Dijkstra

5.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 .

91
5.4. Algorithme de Dijkstra

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

92
5.4. Algorithme de Dijkstra

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

93
5.4. Algorithme de Dijkstra

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

94
5.4. Algorithme de Dijkstra

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

95
5.4. Algorithme de Dijkstra

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

96
5.4. Algorithme de Dijkstra

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.

97
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.

98
5.4. Algorithme de Dijkstra

Exercices chapitre 3 : Structures linéaires listes, piles


et files
Exercice 1 :
Etant donné une matrice creuse dont plus que la moitié de ses éléments sont nuls.

Pour minimiser la taille de la représentation de cette matrice on choisi de la caractériser par


un tableau de listes linéaires, de telle façon à ce que la ième liste linéaire contient les éléments
non nuls de la ligne i de la matrice et chacun d’eux est doté du numéro de la colonne où il se
trouve.

Questions :
1. Définir la structure qui sera utilisée pour les cellules des listes linéaires.
2. Ecrire l’algorithme qui transforme une matrice M de taille nxm en tableau de listes
linéaire comme définit précédemment.
3. Ecrire la fonction qui affiche un élément M[i][j] à partir de T.

Exercice 2 :
Faire la représentation d’un polynôme à l’aide d’une liste linéaire.
En utilisant cette représentation écrire la fonction qui calcule la somme de deux polynômes
d’ordres n et m.

Exercice 3 :
Ecrire l’algorithme qui inverse le contenu d’une liste linéaire. Faire pour cela une fonction
itérative et la transformer ensuite en fonction récursive.

Exercice 4 :
Ecrire la fonction qui supprime toutes les occurrences d’une valeur x dans une liste linéaire.

99
5.4. Algorithme de Dijkstra

Exercice 5 :
Etant données deux listes linéaires dont les valeurs des éléments sont triés. Ecrire la fonction
qui fait leur fusion pour obtenir une liste triée également.

Exercice 6 :
Etant données deux listes linéaires :
1. Ecrire la fonction qui fait leur intersection.
2. Ecrire la fonction qui fait leur union.

Exercice 7 :
Ecrire la fonction qui dit si une chaı̂ne de caractères x est palindrome ou non.

Exercice 8 :
1. Ecrire la fonction récursive non terminale qui fait la somme des valeurs des éléments
d’une pile d’entiers.
2. Calculez sa complexité.
3. Transformez-la en fonction récursive terminale.
4. La dérécursiver.

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.

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

100
5.4. Algorithme de Dijkstra

TD Chapitre04 (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 correspon-
dant.

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.

101

Vous aimerez peut-être aussi