Académique Documents
Professionnel Documents
Culture Documents
BEN-NAOUM
Maı̂tre de Conférences A
Algorithmique et Structures de
Données Avancées
AVANT-PROPOS
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é.
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
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
1
Table des matières
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
3
Chapitre 1
Introduction à l’Algorithmique,
Preuve et Complexité d’un algorithme
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.
5
1.1. Introduction à l’algorithmique
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
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.
8
1.2. Qualités et preuve d’un algorithme
{condition sur les données} Algorithme {condition exprimant les résultats attendus}
{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 :
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 :
10
1.3. Complexité
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.
12
1.3. Complexité
Et voici le grahe représentant la différence entre leurs croissances respectives lorsque n tend
vers l’infini :
13
1.3. Complexité
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).
15
1.3. Complexité
niter(n)
P
TA (n) = (TC (i, n) + TA1 (i, n)) + TC (niter(n) + 1, n).
i=1
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).
19
1.3. Complexité
20
1.3. Complexité
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.
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é
– 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é
25
2.3. Intérêt, principe et difficultés de la récursivité
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.
27
2.5. Exemple d’algorithme récursif : les tours de Hanoı̈
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
30
2.6. Diviser pour régner
r = ae + bf , s = ag + bh, t = ce + df et u = cg + dh.
31
2.7. Récursivité terminale et non terminale
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
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.
33
2.8. Dérécursivation
34
2.8. Dérécursivation
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
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).
37
3.1. Structure de données : liste linéaire
38
3.1. Structure de données : liste linéaire
39
3.1. Structure de données : liste linéaire
40
3.1. Structure de données : liste linéaire
41
3.1. Structure de données : liste linéaire
42
3.1. Structure de données : liste linéaire
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.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
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.
46
3.2. Structure de données : pile
47
3.3. Structure de donnée : file
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.
49
3.3. Structure de donnée : file
50
3.3. Structure de donnée : file
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
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.4 Parcours
But : passer en revue (pour un traitement quelconque) chaque sommet une et une seule fois.
54
4.2. Arbre binaire
55
4.2. Arbre binaire
56
4.2. Arbre binaire
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.
59
4.2. Arbre binaire
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.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.
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.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
65
4.3. Arbre général
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.
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
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
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 :
72
4.4. Arbre binaire de Recherche
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.
73
4.4. Arbre binaire de Recherche
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.
74
4.4. Arbre binaire de Recherche
75
4.5. Structure de données Tas
76
4.5. Structure de données Tas
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 :
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 ) .
78
4.5. Structure de données Tas
79
4.5. Structure de données Tas
80
4.5. Structure de données Tas
81
4.5. Structure de données Tas
Lire la racine 36 puis la supprimer du TAS ce qui donnera un TAS vide à la fin.
82
Chapitre 5
Les graphes
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 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
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 :
85
5.2. Représentation d’un graphe
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.
86
5.2. Représentation d’un graphe
87
5.3. Parcours de graphes
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
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
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
É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
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.
97
Bibliographie
M. A. Weiss, Data Structures and Algorithm Analysis in Java, Pearson, Third Edition, 2012.
98
5.4. Algorithme de Dijkstra
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
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