Vous êtes sur la page 1sur 81

Cours Algorithmique HAI101I

Licence L1 Année 2021-2022


Version 1.0
Université de Montpellier
Place Eugène Bataillon
34095 Montpellier Cedex 5

S YLVAIN DAUDÉ ET RODOLPHE G IROUDEAU


161, RUE A DA
34392 MONTPELLIER C EDEX 5
M AIL : RGIROU @ LIRMM . FR ,
SYLVAIN . DAUDE @ UMONTPELLIER . FR
Avertissement & planning
Ce manuscrit est le cours d’algorithmique du L1 de l’université de Montpellier.
Les notions abordées dans ce cours portent sur les algorithmes récursifs et ité-
ratifs, leurs forces et leurs faiblesses. La notion de complexité algorithmique sera
présentée par l’exemple ; nous verrons aussi des structures linéaires de base (piles,
files, listes) et leurs propriétés structurelles. Nous finissons par l’étude de plusieurs
algorithmes de tri.
Les livres sur lesquels le cours est basé sont les suivants :
— Introduction to algorithms [3]. Ce livre sera utile tout le long de votre for-
mation.
— Types de données et algorithmes [2]. Ce livre est ancien mais très intéressant
sur le calcul de complexité pour les algorithmes (tri, recherche, . . .).
— Algorithms and Data Structures [4].
D’autres livres sont tout aussi intéressants [1].
Tout le long de votre formation, vous serez amenés à rédiger des rapports scienti-
fiques comme ce manuscrit. Il est écrit en LATEX et les figures sont en TikZ.
Ce manuscrit n’a aucune vocation à être distribué ou déposé sur un site autre que
le site Moodle de la l’université de Montpellier.

Planning : voir l’application PROSE sur le site ENT.


Table des matières

1 Algorithmes itératifs versus récursifs 1


1.1 Préliminaires et notations . . . . . . . . . . . . . . . . . . . . . . 2
1.2 Problèmes algorithmiques . . . . . . . . . . . . . . . . . . . . . 5
1.3 Algorithmes itératifs . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3.1 Introduction et définition . . . . . . . . . . . . . . . . . . 5
1.3.2 Algorithme itératif pour le problème M ULTIPLICATION . 6
1.4 Algorithmes récursifs . . . . . . . . . . . . . . . . . . . . . . . . 8
1.4.1 Introduction et définition . . . . . . . . . . . . . . . . . . 8
1.4.2 Algorithme récursif pour le problème FACTORIELLE . . . 9
1.4.3 Limites de la récursivité . . . . . . . . . . . . . . . . . . 10
1.5 Algorithmes utilisant la méthode diviser-pour-régner . . . . . . . 12
1.5.1 Présentation de la méthode . . . . . . . . . . . . . . . . . 12
1.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

2 Complexité algorithmique 17
2.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.2 Premières définitions . . . . . . . . . . . . . . . . . . . . . . . . 20
2.2.1 Coût d’une fonction . . . . . . . . . . . . . . . . . . . . 20
2.2.2 Complexité des opérations élémentaires . . . . . . . . . . 20
2.2.3 Problème - instance . . . . . . . . . . . . . . . . . . . . 21
2.2.4 Algorithme et complexité . . . . . . . . . . . . . . . . . 22
2.3 Comment calculer la complexité d’un algorithme ? . . . . . . . . 25
2.3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.3.2 Calcul de complexité pour les algorithmes itératifs . . . . 27
2.3.3 Calcul de complexité pour les algorithmes récursifs . . . . 29
2.3.4 Résolution d’équation de récurrence type diviser-régner
& Master theorem . . . . . . . . . . . . . . . . . . . . . 33

i
TABLE DES M ATIÈRES

3 Listes, piles, files et listes chaînées 37


3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.2 Les listes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.2.1 Présentation . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.3 Piles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.3.2 Opérations de base . . . . . . . . . . . . . . . . . . . . . 41
3.4 Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3.5 Listes chaînées . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.5.1 Présentation . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.5.2 Recherche dans une liste chaînée . . . . . . . . . . . . . . 46
3.5.3 Insertion dans une liste chaînée . . . . . . . . . . . . . . 46
3.5.4 Suppression dans une liste chaînée . . . . . . . . . . . . . 46
3.6 Analyse amortie . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
3.6.1 Piles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
3.6.2 Incrémentation d’un compteur binaire . . . . . . . . . . . 50
3.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51

4 Algorithmes de tri 53
4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4.2 Algorithmes de complexité quadratique . . . . . . . . . . . . . . 56
4.2.1 Tri par sélection . . . . . . . . . . . . . . . . . . . . . . 56
4.2.2 Tri par insertion séquentielle . . . . . . . . . . . . . . . . 57
4.3 Algorithme de complexité O(n log n) . . . . . . . . . . . . . . . 59
4.4 Complexité optimale d’un algorithme de tri par comparaison . . . 60
4.5 Algorithmes de complexité linéaire . . . . . . . . . . . . . . . . . 61
4.5.1 Tri par dénombrement . . . . . . . . . . . . . . . . . . . 61
4.5.2 Tri par paquets . . . . . . . . . . . . . . . . . . . . . . . 65
4.5.3 Tri par base . . . . . . . . . . . . . . . . . . . . . . . . . 67
4.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69

ii
Table des figures

2.1 Opérations de base en arithmétique. . . . . . . . . . . . . . . . . 21


2.2 Illustration du master theorem. . . . . . . . . . . . . . . . . . . . 34
2.3 Représention des fonctions de bases. Attention les échelles ne sont
pas les mêmes. . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

3.1 Implémentation via un tableau d’une pile P . En a) la situation de


la pile initiale. En b) la situation de la pile avec l’ajout de deux
éléments 9 et 4 pour l’utilisation de la fonction E MPILER(P, 9) et
E MPILER(P, 4). En. c) la situation de la pile P après l’utilisation
de la fonction D ÉPILER(P ). . . . . . . . . . . . . . . . . . . . . 41
3.2 Implémentation via un tableau d’une file P . . . . . . . . . . . . . 43
3.3 a) Une liste doublement chaînée L représentant l’ensemble dy-
namique {7, 15}. Chaque élément de la liste est un objet avec
des champs contenant la clé et des pointeurs (représentés par
des flèches) sur les objets suivant et précédent. Le champ SUCC
de la queue et le champ PRÉD de la tête valent NIL, représenté
par un slash. b) Après l’exécution de I NSÉRER - LISTE(L, x), où
CLÉ [x] = 35, la liste chaînée contient à sa tête un nouvel ob-
jet ayant pour clé 35. Ce nouvel objet pointe sur l’ancienne tête
de clé 15. c) Le résultat de l’appel S UPPRIMER - LISTE(L, x) ulté-
rieur, où x pointe sur l’objet ayant pour clé 15. . . . . . . . . . . . 45

4.1 Arbre de décision pour le tri de trois éléments a, b, c. . . . . . . . 61


4.2 Fonctionnement de T RI -D ÉNOMBREMENT sur un tableau
A[1..8], où chaque élément de A est un entier positif pas plus
grand que k = 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . 63

iii
TABLE DES M ATIÈRES

iv
Liste des Algorithmes

1.1 Algorithme A LGO M UL(x, n) calculant la valeur de xn pour le


problème M ULTIPLICATION. . . . . . . . . . . . . . . . . . . . . 6
1.2 Calcul de factorielle n, notée n! : FACT(n). . . . . . . . . . . . . 9
1.3 La recherche d’un élément X dans un tableau trié T par la re-
cherche dichotomique : D ICHO(X, T, g, d, res). . . . . . . . . . 12
2.1 Algorithme de recherche d’un élément x appartenant à un tableau
T. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.2 Algorithme pour la recherche du plus grand élément dans un tableau. 24
2.3 Algorithme itératif linéaire . . . . . . . . . . . . . . . . . . . . . 28
2.4 Algorithme itératif quadratique . . . . . . . . . . . . . . . . . . . 28
2.5 Algorithme itératif cubique . . . . . . . . . . . . . . . . . . . . . 28
2.6 Procédure R EC 1. . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.7 Procédure R EC 2. . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.8 Procédure R EC 3. . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.9 Procédure R EC 4. . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.10 Calcul de factorielle n! FACT(n). . . . . . . . . . . . . . . . . . . 31
2.11 Calcul des nombres de Fibonacci F IBO. . . . . . . . . . . . . . . 32
2.12 Calcul de la fonction M YSTERE. . . . . . . . . . . . . . . . . . . 32
3.1 Procédure P ILE - VIDE. . . . . . . . . . . . . . . . . . . . . . . . 42
3.2 Procédure E MPILER(P, x) . . . . . . . . . . . . . . . . . . . . . 42
3.3 Procédure D ÉPILER(F ). . . . . . . . . . . . . . . . . . . . . . . 42
3.4 Procédure E NFILER(F, x). . . . . . . . . . . . . . . . . . . . . . 43
3.5 Procédure D ÉFILER(F ) . . . . . . . . . . . . . . . . . . . . . . . 43
3.6 Procédure R ECHERCHER - LISTE(L, k). . . . . . . . . . . . . . . . 47
3.7 Procédure I NSÉRER - LISTE(L, x). . . . . . . . . . . . . . . . . . 47
3.8 Procédure S UPPRIMER - LISTE(L, x). . . . . . . . . . . . . . . . . 47
3.9 Procédure D ÉPILER M UL(S, k). . . . . . . . . . . . . . . . . . . 48
3.10 Procédure I NCRÉMENTER(A). . . . . . . . . . . . . . . . . . . . 50

v
TABLE DES M ATIÈRES

4.1 Tri par sélection T RI_S ÉLECTION(T, n). . . . . . . . . . . . . . . 57


4.2 Tri par insertion séquentielle T RI_ I NSERTION(T, n) . . . . . . . 57
4.3 Tri par fusion T RI _F USION(T ) ; . . . . . . . . . . . . . . . . . . 59
4.4 T RI_D ÉNOMBREMENT (A, B, k). . . . . . . . . . . . . . . . . . 62
4.5 T RI_PAQUETS (A). . . . . . . . . . . . . . . . . . . . . . . . . . 65
4.6 T RI_BASE(A, c). . . . . . . . . . . . . . . . . . . . . . . . . . . 68

vi
Chapitre
Algorithmes itératifs ver-
1 sus récursifs

Sommaire
1.1 Préliminaires et notations . . . . . . . . . . . . . . . . . . . 2
1.2 Problèmes algorithmiques . . . . . . . . . . . . . . . . . . . 5
1.3 Algorithmes itératifs . . . . . . . . . . . . . . . . . . . . . . 5
1.3.1 Introduction et définition . . . . . . . . . . . . . . . . 5
1.3.2 Algorithme itératif pour le problème M ULTIPLICATION 6
1.4 Algorithmes récursifs . . . . . . . . . . . . . . . . . . . . . 8
1.4.1 Introduction et définition . . . . . . . . . . . . . . . . 8
1.4.2 Algorithme récursif pour le problème FACTORIELLE . 9
1.4.3 Limites de la récursivité . . . . . . . . . . . . . . . . 10
1.5 Algorithmes utilisant la méthode diviser-pour-régner . . . 12
1.5.1 Présentation de la méthode . . . . . . . . . . . . . . . 12
1.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

Résumé

Dans ce chapitre, nous étudierons la notion d’algorithme et présente-


rons les principes de leur écriture, dans les styles récursif et itératif.
Nous étudierons les notions de validité et de terminaison d’un algo-
rithme en se basant sur le principe d’un invariant. Pour finir, nous
présenterons le principe diviser pour régner.

1
A LGORITHMES ITÉRATIFS VERSUS RÉCURSIFS

1.1 Préliminaires et notations


Ce cours a pour but de vous familiariser avec la notion d’algorithme. On peut s’in-
terroger sur l’intérêt d’introduire la notion d’algorithme. L’être humain est géné-
ralement un être peu enclin aux efforts répétitifs. Supposons qu’après avoir résolu
un problème A (contenant énormément de calculs fastidieux), on soit confrontés
à un nouveau problème B similaire à A avec simplement des valeurs différentes.
On connaît la méthode pour arriver à une solution, on l’a déjà utilisée dans le pro-
blème A mais avec d’autres valeurs. On se demande s’il serait possible de trouver
une méthode automatique permettant de résoudre tous les problèmes similaires
au problème A avec simplement de nouvelles valeurs. C’est ainsi qu’apparaît la
notion d’algorithme.
D ÉFINITION 1.1.1 : De manière informelle, un algorithme est une procédure de
calcul bien définie, qui prend en entrée une valeur, ou un ensemble de valeurs,
et qui produit en sortie une valeur, ou un ensemble de valeurs. Un algorithme
est donc une séquence d’étapes de calcul permettant de passer de la valeur
d’entrée à la valeur de sortie.
On peut remarquer qu’avec cette définition nous ne savons rien de la procédure
qui permet d’obtenir les valeurs de sortie. D’ailleurs, pour un problème donné,
est-on certain qu’il existe un algorithme pour le résoudre ?
La réponse est non : il existe des problèmes mathématiquement bien posés, mais
trop complexes pour être résolus par un algorithme. Par l’exemple, l’arithmétique
est une théorie dont les énoncés sont complètement formalisés dans un langage
bien précis, mais il est démontré qu’il ne peut pas exister d’algorithme général
pour décider si une assertion arithmétique quelconque est vraie ou fausse.
Nous entrons dans la théorie de la calculabilité. Elle décrit et caractérise les pro-
blèmes qui peuvent être résolus par un algorithme (on parle de problème décidable
ou calculable) et répertorie ceux qui ne le peuvent pas. Ce sont les problèmes in-
décidables.
En 1900, David Hilbert prononça à Paris sa fameuse conférence intitulée « Ma-
thematische Probleme » devant le Congrès International des Mathématiciens. Cet
article contient 23 problèmes , plus précisément 23 groupes de problèmes appa-
rentés que le dix-neuvième siècle laissait au vingtième siècle le soin de résoudre.
Le problème numéro dix est consacré aux équations Diophantiennes 1 et constitue
un problème indécidable :
1. Une équation diophantienne est une équation polynomiale à une ou plusieurs inconnues et
à coefficients entiers, dont les solutions sont cherchées parmi les nombres entiers ou rationnels.

2
De la possibilité de résoudre une équation Diophantienne : on donne une équation
de Diophante à un nombre quelconque d’inconnus et à coefficients entiers rela-
tifs, on souhaite trouver une méthode pour laquelle, au moyen d’un nombre fini
d’opérations, on pourra distinguer si l’équation est résoluble par nombre entier
relatif.
Au XXIe siècle nous dirions plutôt « trouver un algorithme » pour signifier « trou-
ver une méthode ».
E XEMPLE
Voici un exemple d’équation Diophantienne : (x + 1)3 + (y + 1)3 = (z + 1)3 .
Une solution consiste à prendre x = z et y = −1.

Pour démontrer qu’il existe une infinité de fonctions non calculables, on peut par
exemple remarquer que l’ensemble des fonctions de N dans N n’est pas dénom-
brable (c’est à dire qu’il ne peut pas être mis en bijection avec N ou une partie de
N) alors que l’ensemble des fonctions calculables est dénombrable 2 .
Un exemple fondamental de problème non décidable est le problème de l’arrêt :
il est démontré qu’il ne peut pas exister d’algorithme général qui, pour tout pro-
gramme P et toute donnée D, répondrait oui ou non à la question “P termine-t-il
pour D ?”.
Dans ce cours, on étudie des problèmes pour lesquels il existe des algorithmes,
et on analyse la complexité de ces algorithmes , c’est-à-dire les ressources néces-
saires à leur exécution, en temps et en mémoire. On ne s’intéresse qu’aux algo-
rithmes de complexité raisonnable. En effet, il ne suffit pas de savoir qu’il existe
un algorithme pour être sûr qu’il est utilisable pratiquement. Un exemple bien
connu est le jeu d’échecs :
E XEMPLE
En théorie, à chaque coup joué, il y a un nombre fini de mouvements pos-
sibles. Par ailleurs, il existe un nombre maximal de coups joués à partir du-
quel on est certain que la partie est terminée. On peut donc concevoir un
programme qui calculerait toutes les conséquences de tous les coups pos-
sibles et qui serait meilleur que tout joueur humain. Mais la complexité du
problème est telle que qu’il n’est pas envisageable de mettre un tel algo-

2. Le premier résultat est une conséquence d’un théorème célèbre, le théorème de Cantor,
qui s’appuie sur le tout aussi célèbre argument diagonal. Le second se démontre simplement :
à chaque fonction calculable, on peut associer un algorithme. Comme il existe un nombre fini
d’algorithmes de longueur donnée, on obtient que l’ensemble des algorithmes est dénombrable,
donc que l’ensemble des fonctions calculables est dénombrable.

3
A LGORITHMES ITÉRATIFS VERSUS RÉCURSIFS

rithme en pratique. Il faudrait considérer de l’ordre de 1019 coups possibles


pour décider de chaque déplacement. Rappelons que 1019 millisecondes
est de l’ordre de 300 millions d’années.

Dans le cas d’une complexité “hors du possible”, comme pour le jeu d’échecs, on
envisage alors des méthodes approchées, appelées des heuristiques. Une heuris-
tique est une méthode qui produit la plupart du temps un résultat acceptable, pas
nécessairement optimal, dans un temps raisonnable.
On donne maintenant deux autres définitions sur la notion d’algorithme :
D ÉFINITION 1.1.2 :
Un algorithme décrit un traitement sur un certain nombre, fini, de données
(éventuellement aucune).

D ÉFINITION 1.1.3 :
Un algorithme est la composition d’un ensemble fini d’étapes, chaque étape
étant formée d’un nombre fini d’opérations dont chacune est :
— définie de façon rigoureuse et non ambiguë ;
— effective, c’est-à-dire pouvant être effectivement réalisée par une machine.
Cela correspond à une action qui peut être réalisée avec un papier et un
crayon en un temps fini ; par exemple la division entière est une opération
effective, mais pas la division avec un nombre infini de décimales.

S PÉCIFICATION 1.1.1 :
1. Quelle que soit la donnée sur laquelle il travaille, un algorithme doit toujours
se terminer après un nombre fini d’opérations et fournir un résultat.
2. Un algorithme appliqué plusieurs fois sur une même donnée doit toujours
retourner la même valeur et donner lieu à la même suite d’opérations.

Dans toute la suite du cours nous étudierons uniquement des algorithmes dits
déterministes, ceux qui respectent la condition 2 de la spécification 1.1.1. Ceux
qui ne respectent pas cette condition sont dits probabilistes.

4
1.2 Problèmes algorithmiques
La notion de problème est centrale dans le domaine de l’informatique fondamen-
tale. Il s’agit d’une question dépendant d’un certain nombre de paramètres à la-
quelle il faudra répondre. On distingue deux principaux types de problèmes :
1. les problèmes de décision pour lesquels il faudra répondre OUI ou NON.
2. les problèmes d’optimisation pour lesquels il faudra trouver une « meilleure
» solution, en fonction d’un ou plusieurs paramètres. Ces paramètres
peuvent prendre différentes valeurs, ce qui détermine un certain nombre
d’entrées possibles pour le problème, qui sont alors appelées instances. Ré-
soudre une instance consistera à répondre à la question du problème pour
cette instance.
Pour illustrer, considérons le problème M ULTIPLICATION.
M ULTIPLICATION
E NTRÉE : Soit x ∈ R et n ∈ N.
TÂCHE : Calculer xn .
Maintenant que le problème mathématique à résoudre est correctement posé nous
décrivons plusieurs types d’algorithmes.

1.3 Algorithmes itératifs


1.3.1 Introduction et définition
D ÉFINITION 1.3.1 : En mathématiques, une itération désigne l’action de répé-
ter un processus. Par exemple, appliquer une fonction à plusieurs reprises.
En informatique, une itération est la répétition d’un bloc d’instructions dans un
programme informatique. Elle peut être utilisée de manière générale comme
un synonyme de répétition, ou décrire une forme spécifique de la répétition.
Par extension, le mot peut également se référer à toute répétition utilisant une
structure de répétition explicite.
Si la programmation récursive peut être considérée comme un exemple d’itéra-
tion, au sens où un même bloc d’instructions est exécuté de manière répétée,
l’usage est de limiter l’utilisation du mot itération au style de programmation im-
pératif, dans lequel les itérations sont effectuées au moyen de structures de
contrôle particulière, les boucles.

5
A LGORITHMES ITÉRATIFS VERSUS RÉCURSIFS

1.3.2 Algorithme itératif pour le problème M ULTIPLICATION

Nous souhaitons proposer un algorithme itératif que nous souhaitons efficace 3


et valide 4 . Considérons le problème M ULTIPLICATION défini ci-avant qui nous
servira d’exemple.
Pour résoudre ce problème, nous considérons l’algorithme 1.1.

Algorithme 1.1 Algorithme A LGO M UL(x, n) calculant la valeur de xn pour le


problème M ULTIPLICATION.
y := 1
for i = 1 à n do
y := x ∗ y ;
end for
R ETOURNER y ;

A partir de l’écriture de l’algorithme 1.1, il est nécessaire de montrer les trois


principes suivants :
— la terminaison de l’algorithme (voir section 1.3.2.1),
— la correction ou validité de l’algorithme (voir section 1.3.2.2),
— la complexité (voir section 1.3.2.3).

1.3.2.1 Terminaison de l’algorithme

Pour montrer qu’un algorithme termine, il est nécessaire de montrer que le nombre
d’étapes dans l’algorithme est borné.
L EMME 1.3.1 : L’algorithme 1.1 termine.
Preuve
Il est clair que la variable i définie dans la boucle for parcourra tous les indices
entre 1 et n, et ainsi l’algorithme termine.


3. un algorithme est efficace s’il utilise peu de calculs ou de mémoire. Il est dit optimal si on
ne peut pas trouver d’algorithme plus efficace pour le même problème.
4. un algorithme est valide s’il se termine avec le bon résultat.

6
1.3.2.2 Correction de l’algorithme

Pour montrer que l’algorithme est correct, il est nécessaire de prouver un invariant
I NVi 5 . L’invariant dépend de l’algorithme.
L EMME 1.3.2 : L’algorithme 1.1 est correct.
Preuve
Considérons l’invariant défini de la manière suivante I NVi :

après i tours de boucle, y contient xi

La preuve se fera par récurrence sur i.


— Pour i = 0, I NV0 est vrai car, avant la boucle, y vaut 1 (x0 ).
— Supposons que I NVi−1 est vrai, c’est-à-dire qu’après (i − 1) tours, y est égal
à xi−1 . Ainsi après le ieme tour, la valeur y prend la valeur x ∗ y = x ∗ xi =
xi+1 . En conclusion, I NVi est vérifié.


Point méthode : les invariants de boucles Pour prouver une propriété d’une
boucle dans un programme, le principe est de sélectionner un invariant de boucle,
ou assertion par récurrence, qui est une assertion S vraie chaque fois que nous
atteignons un endroit particulier dans la boucle. L’assertion S est alors prouvée
par récurrence sur un paramètre qui mesure le nombre de fois que nous avons
parcouru la boucle. Par exemple, le paramètre peut être le nombre de fois que nous
avons atteint un test d’une boucle WHILE, la valeur d’un indice de boucle dans une
boucle FOR ou une expression mettant en jeu les variables du programme qui sont
supposées augmenter de 1 à chaque parcours de boucle.
Pour les boucles WHILE, il est logique de chercher un invariant de boucle à éva-
luer juste avant le test de la condition. On prouve généralement cet invariant par
récurrence, où l’indice de la récurrence correspond au nombre de fois où la boucle
est parcourue. Ensuite, lorsque la condition devient fausse, nous pouvons utiliser
l’invariant et le fait que la condition de la boucle soit devenue fausse pour déduire
la valeur des expressions calculées.
Cependant à la différence des boucles FOR, il est possible d’entrer dans une boucle
infinie. Ainsi, une preuve de fonctionnement pour une boucle WHILE doit inclure
5. un invariant est une expression booléenne (assertion) toujours vraie à un endroit précis de
l’itération.

7
A LGORITHMES ITÉRATIFS VERSUS RÉCURSIFS

une preuve qu’elle terminera certainement. Habituellement, nous prouvons la ter-


minaison en identifiant une expression E, mettant en jeu les variables du pro-
gramme, telle que :
1. la valeur de E décroisse d’au moins un à chaque fois que nous parcourons
la boucle, et
2. la condition de boucle soit fausse si E est inférieure ou égale à une constante
donnée, comme zéro.

1.3.2.3 Complexité de l’algorithme

La complexité d’un algorithme est égal aux nombres de fois où l’on exécute l’opé-
ration fondamentale pour le problème, multiplication et addition pour le problème
M ULTIPLICATION, comparaison et affectation pour le tri, . . .
La complexité en temps est le nombre d’opérations élémentaires de l’algorithme
en fonction de la valeur des données. Souvent, on se contente d’en donner un ordre
de grandeur (n, n2 , ...)
L EMME 1.3.3 : La complexité en temps est de l’ordre n.
Preuve
Nous avons un nombre d’itérations qui de l’ordre de n, la longueur de la boucle,
et chaque itération effectue un nombre constant d’opérations.


1.4 Algorithmes récursifs


1.4.1 Introduction et définition

Cette section présente la notion de récursivité, notion très utilisée en program-


mation, et qui permet l’expression d’algorithmes concis, faciles à écrire et à com-
prendre. La récursivité peut toujours être remplacée par son équivalent sous forme
d’itérations, mais au prix d’algorithmes plus complexes surtout lorsque les struc-
tures de données à traiter sont elles-mêmes de nature récursive.
D ÉFINITION 1.4.1 : La récursivité est une méthode de description d’algo-
rithmes qui permet à une procédure de s’appeler elle-même (directement ou
indirectement). Une notion est récursive si elle se contient elle-même en partie,
ou si elle est partiellement définie à partir d’elle-même.

8
L’expression d’algorithmes sous forme récursive permet des descriptions concises
et naturelles. Le principe est d’utiliser, pour décrire l’algorithme sur une donnée
D, l’algorithme lui-même appliqué à une ou plusieurs décompositions de D, jus-
qu’à ce que le traitement puisse s’effectuer sans nouvelle décomposition. Dans
une procédure récursive, il y a deux notion à retenir :
1. la procédure s’appelle elle-même : on recommence avec de nouvelles don-
nées,
2. il y a un test de fin : dans ce cas, il n’y a pas d’appel récursif. Il est souvent
préférable d’indiquer le test de fin des appels récursifs en début de procé-
dure.

1.4.2 Algorithme récursif pour le problème FACTORIELLE

Algorithme 1.2 Calcul de factorielle n, notée n! : FACT(n).


if n = 0 then
R ETOURNER 1 ;
else
R ETOURNER n∗FACT(n − 1) ;
end if

A partir de l’écriture de l’algorithme 1.1, il est nécessaire de montrer :


— La terminaison de l’algorithme (voir section 1.4.2.1),
— La correction ou validité de l’algorithme (voir section 1.4.2.2),
— la complexité (voir section 1.4.2.3).

1.4.2.1 Terminaison de l’algorithme

Pour montrer que les algorithmes terminent il est nécessaire de montrer que le
nombre d’étapes dans l’algorithme est borné.
L EMME 1.4.1 : L’algorithme 1.2 termine.
Preuve
Il est clair que l’algorithme se termine car les appels récursifs utilisent une variable
entière naturelle qui décroît d’une unité à chaque fois, et l’algorithme possède un
cas de base lorsque la variable vaut 0.


9
A LGORITHMES ITÉRATIFS VERSUS RÉCURSIFS

1.4.2.2 Correction de l’algorithme

Pour montrer que l’algorithme est correct, il est nécessaire de prouver un invariant
I NVi . L’invariant dépend de l’algorithme.
L EMME 1.4.2 : L’algorithme 1.2 est correct.
Preuve
Considérons l’invariant défini de la manière suivante :

I NVi : l’algorithme appelé avec la valeur i retourne i!


La preuve se fera par récurrence sur i.
— Pour i = 0, I NV0 est vrai car l’algorithme retourne la valeur 1.
— Supposons que I NVi−1 est vrai, c’est-à-dire que l’algorithme appelé avec
(i − 1) renvoie (i − 1)!. Regardons le ieme appel récursif : nous avons i ∗
(i − 1)! = i!.


1.4.2.3 Complexité de l’algorithme

L EMME 1.4.3 : La complexité en temps pour l’algorithme 1.2 est de l’ordre n.


Preuve
Nous avons un nombre d’itérations qui de l’ordre de n, qui correspond au nombre
d’appels récursifs, et chaque appel effectue un nombre d’opérations majoré par
une constante.


1.4.3 Limites de la récursivité

Dans la théorie de la récursivité, la fonction d’Ackermann (aussi appelée fonction


d’Ackermann-Péter) est un exemple simple de fonction récursive non récursive
primitive (pas calculable avec une simple boucle pour), trouvée en 1926 par Wil-
helm Ackermann. Elle est souvent présentée sous la forme qu’en a proposée la
mathématicienne Rózsa Péter :

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

10
Nous savons que pour montrer la terminaison d’un processus récurrent ou récursif,
il faut trouver un ordre sur les valeurs successives manipulées par la fonction
récursive. On considère l’ordre classique, l’ordre lexicographique, sur (n, p) les
deux arguments entier de ACK.
On considère l’ordre lexicographique sur IN × IN est défini comme suit :

x < x0 ou,

0 0
(x, y) < (x , y ) ssi
x = x0 et y < y 0
Il suffit donc de vérifier qu’à chaque appel de la fonction ACK, les arguments
successifs (n, p) sont de plus en petits, au sens de l’ordre lexicographique que
nous venons de définir. Regardons les trois suivants :
— ACK(0, p) = p + 1 : il n’y a pas d’appel récursif, la terminaison est acquise.
— ACK(n + 1, 0) = ACK(n, 1) : (n, 1) < (n + 1, 0) décroît à l’appel suivant.
— ACK(n + 1, p + 1) = ACK(ACK(n + 1, p)) : (n + 1, p) < (n + 1, p + 1) et
(n, ACK(n + 1, p)) < (n + 1, p + 1) décroissent aux appels suivants.
Les boucles simples peuvent de façon générale s’exprimer comme des appels ré-
cursifs, parfois de façon plus naturelle, ce qui n’est certainement pas le cas ici.
Mais surtout, les fonctions récursives permettent d’écrire des fonctions qu’on ne
pourrait pas coder autrement.
La fonction ACK n’est pas implémentable par boucles, quel que soit le langage de
programmation. Dans un certain nombre de cas, les définitions récursives peuvent
se dérécursiver, c’est-à-dire être transformées en boucles simples. Dans certains
cas, cela est fait directement par le compilateur, par exemple dans le cas de la
récursion terminale.
Il existe d’autres fonctions qui montrent les limites des fonctions récursives.

11
A LGORITHMES ITÉRATIFS VERSUS RÉCURSIFS

1.5 Algorithmes utilisant la méthode diviser-pour-


régner
1.5.1 Présentation de la méthode
D ÉFINITION 1.5.1 : En informatique, diviser pour régner est une technique
algorithmique consistant à :
1. Diviser : découper un problème initial en sous-problèmes ;
2. Régner : résoudre les sous-problèmes (récursivement ou directement s’ils
sont assez petits) ;
3. Combiner : calculer une solution au problème initial à partir des solutions
des sous-problèmes.

Algorithme 1.3 La recherche d’un élément X dans un tableau trié T par la re-
cherche dichotomique : D ICHO(X, T, g, d, res).
var m : 1 . . . n
if g ≤ d then
m = (g + d)div2 ;
if X = T [m] then
RES := m ;
else if X < T [m] then
D ICHO(X, T, g, m − 1, res) ;
else
D ICHO(X, T, m + 1, d, res) ;
end if
else
RES := 0 ;
end if

Cette procédure récursive recherche par dichotomie l’élément X dans le tableau


T dont les éléments sont triés en ordre croissant ; le résultat de la procédure est
contenu dans le paramètre modifiable res : c’est 0 si X n’appartient pas au tableau,
et c’est i ∈ {1, . . . , n} si X se trouve à l’indice i du tableau.
Pour réaliser une recherche sur une liste de n éléments, on appelle cette procédure
avec les paramètres g = 1 et d = n. On remarque que la liste des éléments est
représentée par un tableau ; ceci est nécessaire car la recherche par dichotomie

12
demande un accès direct aux éléments de la liste. Une représentation à l’aide de
listes chaînées est donc à proscrire puisque, dans ce cas, l’accès à un élément
implique le parcours de tous les éléments qui le précèdent dans la liste.
T HÉORÈME 1.5. 1 : L’algorithme 1.3 de complexité O(log2 n) est valide.
Preuve
Pour prouver que l’algorithme 1.3 est correct, il faut montrer les points suivants :
1. la procédure termine toujours, soit avec RES > 0, soit avec RES= 0,
2. s’il existe un entier i (1 ≤ i ≤ n) tel que X = T [i] alors, après exécution
de l’algorithme, on a RES > 0 et T [RES] = X,
3. réciproquement, si RES > 0 alors il existe un entier i (1 ≤ i ≤ n) tel que
X = T [i].
Le dernier point est évident car l’affectation à RES d’une valeur m différente de 0
se fait seulement lorsque le test X = T [m] est vrai.
Pour prouver que la procédure termine, on va montrer que les appels récursifs
sont toujours en nombre fini, c’est-à-dire que la suite (gi , di ) des bornes des dif-
férents sous-tableaux avec lesquels on appelle récursivement la procédure D ICHO
est finie.
Supposons les (gi , di ) construits pour 0 ≤ i ≤ k, avec g0 = 1 et d0 = n ; et soit
mk = (gk + dk )div2. Plusieurs cas sont à considérer :
— si gk > dk , ou si X = T [mk ] alors la procédure termine et il n’y a plus
d’appel récursif ;
— si gk ≤ dk et X < T [mk ], il y a un appel récursif et gk+1 = gk et dk+1 =
mk − 1 ; on a alors dk+1 − gk+1 = mk − 1 − gk < dk − gk ;
— Si gk ≤ dk et X > T [mk ], il y a un appel récursif et gk+1 = mk + 1 et
dk+1 = dk ; on a encore dk+1 − gk+1 = dk − mk − 1 < dk − gk .
Ainsi la suite des écarts (di − gi ) est entière, positive et strictement décroissante.
Il existe donc un entier p tel que :
— soit gp > dp , et dans ce cas la procédure termine avec RES= 0,
— soit X = T [mp ] avec mp = (gp + dp )/2, et dans ce cas la procédure termine
avec RES= mp > 0.
Prouvons le deuxième point.
On suppose qu’il existe un entier i (1 ≤ i ≤ n) tel que X = T [i]. Considérons
la suite (gk , dk )0≤k≤p précédemment définie. L’entier p est l’indice d’arrêt : pour

13
A LGORITHMES ITÉRATIFS VERSUS RÉCURSIFS

k < p on a gk ≤ dk et d’autre part on a gp > dp ou X = T [mp ]. Montrons par


récurrence la propriété (P ) suivante :
Pour tout k, 0 ≤ k ≤ p, il existe un entier ik , gk ≤ ik ≤ dk , tel que X = T [ik ].
— (P ) est vraie pour k = 0, c’est notre hypothèse de départ.
— Supposons (P ) vraie pour k < p.
Par hypothèse de récurrence gk ≤ dk ; soit mk = (gk + dk )/2. Le cas
X = T [mk ] est impossible par définition de p, car k < p, il reste donc
deux cas :
— soit X < T [mk ], et alors ik < mk puisque la liste est triée ; comme
gk+1 = gk et dk+1 = mk − 1, on a gk+1 ≤ ik ≤ dk+1 , et X = T [ik ] ;
— soit X > T [mk ], et alors ik > mk puisque la liste est triée ; comme
gk+1 = mk + 1 et dk+1 = dk , on a gk+1 ≤ ik ≤ dk+1 , et X = T [ik ] ;
La récurrence est établie. En conséquence, il existe ip tel que gp ≤ ip ≤ dp .
On n’a donc pas gp > dp . Si la suite est terminée, c’est parce que X =
T [mp ] et R ES= mp > 0.
Pour simplifier supposons que n = 2k . Le nombre de comparaisons est égal à k,
c’est- à-dire log2 (n).
Ce résultat est à comparer à celui de la recherche séquentielle (de l’ordre de n
comparaisons dans le pire des cas). Améliorer la recherche séquentielle en uti-
lisant le fait que les éléments sont triés ne permet de gagner qu’un facteur 2 en
moyenne.

Peut-on améliorer la recherche dichotomique dans le pire des cas ? A priori non
car il y a n+1 conclusions possibles. Il est donc nécessaire d’avoir n+1 exécutions
possibles.
De façon générale, si un algorithme basé sur les comparaisons nécessite p exé-
cutions différentes nécessaires alors le nombre de comparaisons au pire est supé-
rieur à log2 (p). Une façon de se convaincre de ce résultat est de construire l’arbre
de décision de l’algorithme. Il s’agit d’un arbre binaire qui représente toutes les
exécutions possibles. Les feuilles de l’arbre indiquent les résultats des différentes
exécutions (remarque : deux exécutions différentes peuvent donner le même résul-
tat). Les noeuds internes correspond à la comparaison de deux éléments effectuées
par l’algorithme. Si la comparaison est VRAI (resp. FAUX) alors le sous arbre
gauche (resp. droit) représente la suite de l’exécution.

14
1.6 Conclusion
En conclusion, nous pouvons tenter de proposer un cadre pour la conception et
l’étude d’un algorithme itératif ou récursif :
1. Ecrire un algorithme,
2. Définir les structures de données utilisées pour l’implémentation. Selon les
structures la complexité d’un algorithme peuvent sensiblement varier.
3. Analyser l’algorithme :
(a) Terminaison
Montrer qu’une quantité entière naturelle décroît strictement durant
l’exécution de l’algorithme.
(b) Complexité en temps
Proposer une borne supérieure du nombre d’opérations élémentaires
dans le pire cas (resp. en moyenne)
(c) Correction de l’algorithme
Montrer un invariant de l’algorithme c’est-à-dire montrer I NVi est va-
lable après i itérations ou i appels récursifs.

15
A LGORITHMES ITÉRATIFS VERSUS RÉCURSIFS

16
Chapitre
Complexité algorithmique
2
Sommaire
2.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.2 Premières définitions . . . . . . . . . . . . . . . . . . . . . 20
2.2.1 Coût d’une fonction . . . . . . . . . . . . . . . . . . 20
2.2.2 Complexité des opérations élémentaires . . . . . . . . 20
2.2.3 Problème - instance . . . . . . . . . . . . . . . . . . 21
2.2.4 Algorithme et complexité . . . . . . . . . . . . . . . 22
2.3 Comment calculer la complexité d’un algorithme ? . . . . . 25
2.3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . 25
2.3.2 Calcul de complexité pour les algorithmes itératifs . . 27
2.3.3 Calcul de complexité pour les algorithmes récursifs . . 29
2.3.4 Résolution d’équation de récurrence type diviser-
régner & Master theorem . . . . . . . . . . . . . . . . 33

Résumé

Nous allons voir la notion de complexité d’un algorithme et ses consé-


quences sur l’implémentation des algorithmes. Pour cela, nous in-
troduisons la notation de Landau, puis nous calculons la complexité
asymptotique via la résolution d’équations de récurrence et l’utilisa-
tion des sommes.

17
C OMPLEXITÉ ALGORITHMIQUE

2.1 Introduction
Les problèmes qui nous intéressent ici sont ceux dont nous connaissons une repré-
sentation mathématique. Ces modèles mathématiques sont facilement implémen-
tés en informatique et peuvent ainsi être « résolus » par des algorithmes. Un mo-
dèle mathématique pour un problème décrit formellement les propriétés qu’une
solution pour ce problème doit vérifier. La résolution d’un problème est le
calcul d’une telle solution que l’on appellera solution réalisable ; une
telle solution n’est pas nécessairement optimale, c’est à dire la meilleure parmi
les solutions vérifiant les propriétés requises. Pour aborder formellement la ques-
tion de la résolution informatique d’un problème, nous avons besoin d’un certain
nombre d’éléments préliminaires. Ils sont présentés dans la suite.
Un problème peut être résolu par plusieurs algorithmes, encore plus de pro-
grammes (chaque algorithme pouvant être implémenté dans plusieurs langages
et de différentes façons).
Comment comparer les diverses solutions ? Quels sont les critères importants pour
comparer et donc choisir entre plusieurs algorithmes ou plusieurs programmes ?
Diverses propriétés ou paramètres physiques peuvent caractériser l’efficacité d’un
programme (supposé juste) : place mémoire nécessaire, durée d’exécution, sim-
plicité du code, . . .
Dans ce cours (faute de temps et pour des raisons de simplification) nous allons
nous limiter aux deux premiers paramètres et nous intéresser plus aux aspects al-
gorithmiques que de programmation (dans la mesure du possible car il est évident
que la réflexion autour de la mise en œuvre d’un algorithme a un impact parfois
déterminant sur son efficacité).
Comment mesurer la place mémoire ? Par le nombre de bits utilisés. Comment
mesurer la durée d’exécution ? Par le nombre d’unités de temps de la durée d’exé-
cution. Mais alors le temps d’exécution dépend de la donnée, du compilateur, de
l’ordinateur, du langage utilisé . . . Bref la mesure devient difficilement utilisable.
Il faut simplifier, modéliser. En effet, des énoncés du type : « L’algorithme A im-
plémenté dans le langage P sur l’ordinateur O et exécuté sur la donnée D utilise
k secondes et j bits de mémoire » sont d’une portée très limitée.
Que se passe-t-il si l’on exécute sur la donnée D0 ? puis si l’on change d’ordina-
teur ? Nous allons donc chercher des informations plus générales : l’algorithme A
est toujours meilleur que l’algorithme B dès que D est grand.
En fait, à chaque problème, on peut en général associer une ou des opérations
élémentaires : c’est-à-dire une (ou des) opération(s) caractéristique(s) ou élémen-
taire(s) que l’on exécutera au moins autant de fois que les autres. Par exemple,

18
si on recherche un élément dans un tableau, une comparaison entre éléments sera
l’opération élémentaire, si on multiplie deux matrices les opérations élémentaires
seront les additions et les multiplications d’entiers, si on trie des éléments les opé-
rations élémentaires seront les comparaisons entre éléments et les déplacements
d’éléments etc . . . Axiome : le temps d’exécution de l’algorithme est proportion-
nel au nombre d’exécutions de l’opération fondamentale (ou élémentaire). On se
contentera donc à chaque fois de compter le nombre de fois où cette opération est
effectuée.

Importance de formaliser la notion de complexité

Les algorithmes prévus pour résoudre le même problème différent souvent énor-
mément par leur efficacité. Ces différences peuvent être beaucoup plus significa-
tives que la différence entre deux ordinateurs :
E XEMPLE
soit un algorithme de tri par insertion sur un ordinateur A, et un algo-
rithme de tri par fusion sur un ordinateur B. Les deux doivent trier un ta-
bleau d’un million de nombres. Supposons que l’ordinateur A exécute 100
millions d’instructions à la seconde, tandis que l’ordinateur B n’en exécute
qu’un million. Pour accroître la différence, supposons que le tri par inser-
tion pour l’ordinateur A est écrit en langage machine par le programmeur
le plus habile du monde, et que le code résultant ait besoin de seulement
2n2 instructions pour trier n nombres. De son côté, le tri par fusion est pro-
grammé sur un ordinateur B par un étudiant à l’aide d’un langage de haut
niveau et d’un compilateur inefficace, ce qui produit un code consommant
50n log n instructions de l’ordinateur B.
Ainsi nous obtenons pour le temps de calculs pour l’ordinateur A :

2.(106 )2 instructions
= 20000secondes ≡ 5, 56 heures
108 instructions/seconde
et pour l’ordinateur B :

50.(106 ) log 106 instructions


= 1000secondes ≡ 16, 67 minutes
106 instructions/seconde

Cet exemple montre que les algorithmes, comme les matériels informatiques,
s’apparentent à la technologie. L’efficacité totale d’un système dépend du choix
du bon algorithme autant que du choix du matériel le plus rapide. De même que

19
C OMPLEXITÉ ALGORITHMIQUE

des avancées rapides ont lieu dans toutes les technologies informatiques, elles ont
lieu également en algorithmique.

2.2 Premières définitions


2.2.1 Coût d’une fonction

Comment déterminer la complexité intrinsèque d’un algorithme ?


D ÉFINITION 2.2.1 : [Première tentative] On appelle complexité d’une fonction
le coût du programme le moins coûteux qui calcule la dite fonction.
Arrêtons-nous sur la notion de coût d’un programme. Cette définition est encore
trop générale et peu précise. Faut-il prendre en compte :
— le langage utilisé pour l’implémenter ?
— le coût de sa fabrication et de sa mise en œuvre ?
— le temps de calcul pour le résoudre ?
— la mémoire utilisée ?
Existe-t-il un lien entre consommation mémoire et temps de calculs ?
D ÉFINITION 2.2.2 : [Seconde tentative] On appelle complexité en temps (resp.
en mémoire) d’une fonction f une fonction cout qui mesure le nombre de pas
de calculs dans le pire des cas utilisé par le programme le moins coûteux pour
trouver f . La fonction cout prend pour arguments les éléments en entrée du
programme.

D ÉFINITION 2.2.3 : On appelle taille du problème à résoudre la caractéristique


des entrées qui est retenue pour mesurer le coût : nombre de chiffres, nombre
de bits, taille de la matrice, degré du polynôme, . . .
Pour illustrer ces observations, nous considérons les procédures de calculs des
opérations élémentaires.

2.2.2 Complexité des opérations élémentaires


E XEMPLE
Considérons les opérations élémentaires enseignées à l’école primaire avec
pour temps de calcul le nombre de chiffres à écrire.
— Pour l’addition de deux nombres à n et m chiffres, il faut calculer au pire
les max(n, m) + 1 chiffres du résultat.

20
multiplication

division
X4 X3 X2 X1 X0 X 4 X3 X 2 X1 X0 X4 X 3 X2 X 1 X0 Y1 Y0
addition

+ Y2 Y1 Y0 ∗ Y2 Y1 Y0 R4 R3 R2 R1 R0 Z2 Z1 Z0
R3 R2 R1 R0
Z5 Z4 Z3 Z2 Z1 Z0 R50 R40 R30 R20 R10 R00 R2 R1 R0
1 1 1 1 1 1
R5 R4 R3 R2 R1 R0 R1 R0
R52 R42 R32 R22 R12 R02 R0
Z8 Z7 Z6 Z5 Z4 Z3 Z2 Z1 Z0

F IGURE 2.1 – Opérations de base en arithmétique.

— Pour la multiplication, il faut (n+1)∗m chiffres des résultats intermédiaires


plus les n + m + 1 chiffres du résultat.
— Pour la division d’un nombre de n chiffres par un nombre de m chiffres. Il
est nécessaire de calculer le n − m chiffres du résultat, plus les n ∗ (n + 1)/2
des résultats intermédiaires.
Reprenons l’exemple précédent mais avec la consommation mémoire, qui
peut être assimilée à la surface d’un rectangle englobant tous les chiffres
de l’opération.
— Pour l’addition de deux nombres à n et m il faut calculer au pire les 3 ∗
(max(n, m) + 1) chiffres du résultat.
— Pour la multiplication, (n + 1) ∗ (n + m + 1) .
— Pour la division (n + max(m, n − m)) ∗ n.

2.2.3 Problème - instance

Dans la théorie algorithmique, un problème est une question générale pour la-
quelle on veut obtenir une réponse. Cette question possède généralement des pa-
ramètres ou des variables dont la valeur reste à fixer. Un problème est spécifié en
donnant la liste de ces paramètres ainsi que les propriétés que doit vérifier la ré-
ponse. Une instance d’un problème est obtenue en explicitant la valeur de chacun
des paramètres du problème instancié.
On remarque que le nombre de comparaisons dépend de la taille des données et
de leur organisation (l’élément recherché est-il absent ou présent dans l’une des

21
C OMPLEXITÉ ALGORITHMIQUE

Algorithme 2.1 Algorithme de recherche d’un élément x appartenant à un tableau


T.
i = 1;
while T [i] 6= x do
i = i + 1;
end while
R ETOURNER i ;

cases et s’il est présent dans laquelle ?). D’où la nécessité d’introduire la notion de
fonctions qui prennent en paramètre la taille des données et donnent comme valeur
le nombre d’opérations effectuées : le nombre moyen ou le nombre maximum. Le
calcul du nombre moyen de comparaisons est souvent un problème très difficile
qui nécessite de connaître la probabilité d’avoir la donnée d.
Dans la suite nous limiterons par exemple ce problème en faisant l’hypothèse que
toutes les données de même taille ont la même probabilité (distribution uniforme).
Ainsi à chaque algorithme A, on essayera d’associer une fonction fA : IN → IN
dans qui associe le nombre d’exécutions de l’opération élémentaire en fonction de
la taille de la donnée (l’unité de taille pourra être le bit, l’entier, le flottant, . . .).
E XEMPLE
Sur l’exemple de l’algorithme 2.1, il est facile de voir que le nombre de
comparaisons d’éléments est au mieux 1 (l’élément x à la première case),
au pire n (l’élément x à la dernière case), en moyenne (n + 1)/2 (i compa-
raisons si l’élément x est dans la case i, avec une probabilité 1/n).
Pour traiter le cas où x ∈
/ T : on ajoute le test i < n.

Exploitation de la fonction :
En fait, la taille des données n’est évidemment pas égale à la durée d’exécution du
programme (la mesure qui intéresse l’utilisateur) mais elle a de bonnes chances
d’en fournir une mesure. Par exemple, elle pourra être proportionnelle à n : si pour
n = 100, l’algorithme s’exécute en 1ms, il mettra environ 10 ms pour n = 1000.

2.2.4 Algorithme et complexité

Un algorithme est une suite d’opérations élémentaires (affectations de variables,


tests, branchements, . . .) qui, quand on lui fournit une instance d’un problème
en entrée, s’arrête après exécution de la dernière opération en nous renvoyant

22
la solution. Le déroulement et le résultat de l’algorithme dépendent de la valeur
de tous les paramètres de l’instance du problème. A chaque type de problème
correspond un type de solution ; par exemple :
— une liste de nombre, un graphe, un chemin dans un graphe, . . . ;
— un ensemble de variables, un tableau de nombres, . . .
— oui \ non ;
— une valeur ;
— l’affirmation de l’absence de solution.
Le temps d’exécution d’un algorithme est compté en nombre d’instructions né-
cessaires à son déroulement. L’utilisation du nombre d’instructions comme unité
de temps est justifiée par le fait qu’un même programme utilisera le même nombre
d’instructions sur deux machines différentes mais prendra plus ou moins de temps
selon leurs rapidités respectives. Il est généralement considéré qu’une instruction
correspond à une multiplication, une affectation, un marquage, . . . En informa-
tique, toute opération plus compliquée peut se ramener à l’une des précédentes.
Ce qu’on appelle la complexité d’un algorithme correspond à peu près à une me-
sure du temps qu’il prendra pour résoudre un problème d’une taille donnée, en
fonction de la taille de la donnée. C’est en réalité une fonction qui associe à la
taille d’une instance d’un problème donné, une approximation du nombre d’ins-
tructions nécessaires à sa résolution.
Nous supposons, pour des raisons de simplicité, que la complexité d’un algo-
rithme A s’écrit de la façon suivante : O(f (n)) et se lit « la complexité de l’algo-
rithme A est en ordre de f (n) » où f est une fonction à valeurs dans IN et n la
taille de l’instance du problème.
D ÉFINITION 2.2.4 :
Nous définissons les notations O et θ de la manière suivante :
Soient f et g, deux fonctions de IN dans IN.
— f = O(g) si ∃c ∈ IR∗+ , ∃n0 ∈ IN tels que ∀n > n0 , f (n) ≤ cg(n). On dit que
f est dominée par g.
— f = Ω(g) si ∃d ∈ IR∗+ , ∃n0 ∈ IN tels que ∀n > n0 , f (n) ≥ dg(n).
— f = θ(g) si ∃c, d ∈ IR∗,+ , ∃n0 ∈ IN tel que ∀n > n0 , dg(n) ≥ f (n) ≥ cg(n).
Dans ce cas la croissance de f est du même ordre que celle de g.

Dans la pratique, les notations O() et θ() sont utilisées de la même façon et signi-
fient plutôt θ(). Pour des raisons de conformité avec le reste de la littérature nous
allons employer O() même lorsque les deux fonctions sont de même ordre.

23
C OMPLEXITÉ ALGORITHMIQUE

Les résultats, dans le cadre de l’étude de la complexité d’un problème, ne sont pas
censés changer si on définit la taille d’une instance d’une manière ou d’une autre,
comme le nombre de ses variables ou de ses variables ou de ses contraintes, ou le
nombre de sommets (arêtes, arcs) dans le graphe qui représente l’instance, . . .. En
pratique, on peut toujours se ramener au nombre de bits nécessaires à coder toutes
les informations qui caractérisent de manière univoque le problème. La taille de
l’instance d’un problème peut-être considérée comme étant l’un des ordres de
grandeur suivants :
E XEMPLE
Si on reprend l’exemple de la recherche, le nombre de comparaisons d’élé-
ments en moyenne (n + 1)/2 donc en θ(n). On peut donc en déduire que la
durée de l’exécution de l’algorithme sera proportionnelle à n. Ainsi si en
moyenne on met 1ms pour chercher un élément dans un tableau de taille
100, on mettra environ 1s pour chercher un élément dans un tableau de
taille 100000. f (n) = 2n2 + 3n et g(n) = n2 + 7n. On a f = θ(g) en prenant
c = 1, d = 3, n0 = 5.

E XEMPLE

Algorithme 2.2 Algorithme pour la recherche du plus grand élément dans un


tableau.
m = T [1] ;
for i de 2 à n do
if t[i] > m then
m = T [i] ;
end if
end for
R ETOURNER m ;

L’algorithme 2.2 nécessite (n−1) comparaisons d’éléments : une comparai-


son à chaque passage dans la boucle. Ce résultat est a priori optimal car il
y a (n − 1) éléments qui ne sont pas optimaux et pour le savoir il faut com-
parer chacun avec un élément plus grand. Si l’on désire calculer le nombre
d’affectations, le problème est plus délicat car le résultat ne dépend pas
que du nombre d’éléments.

Le tableau 2.1 permet d’avoir une idée de la durée d’exécution d’un algorithme

24
Taille de l’instance (n)
Fonction 20 30 40 50 60
n 2 ∗ 10−5 sec. 3 ∗ 10 sec. 4 ∗ 10−5 sec.
−5
5 ∗ 10−5 sec. 6 ∗ 10−5 sec.
n2 0, 0004 sec. 0, 0009 sec. 0, 0016 sec. 0, 0025 sec. 0, 0036 sec.
n3 0, 008 sec. 0, 27 sec. 0, 064 sec. 0, 125 sec. 0, 216 sec.
n5 3, 2 sec. 24, 3 sec. 1, 7 min. 5, 2 min. 13 min.
2n 1 sec. 17, 9 min. 12, 7 jours 35, 7 jours 366 siècles
3n 58 min. 6, 5 années 3855 s. 2 × 108 s. 1, 3 × 1013 s.
TABLE 2.1 – Comparaison des temps d’exécution pour un panel d’algorithmes de
différentes complexités, pour une machine effectuant un million d’opérations par
seconde.

en fonction de l’ordre de grandeur de son exécution. Si la complexité est infé-


rieure à log(n) alors il n’y a aucune contrainte sur la taille des données, si la com-
plexité est inférieure à n, seules des données de taille très grande peuvent poser
des problèmes (données codées sur quelques dizaines de bits), si la complexité est
inférieure à nk avec k > 1 seules les données de taille moyenne peuvent être trai-
tées, si la complexité est exponentielle seules les données de petite taille (quelques
dizaines) peuvent être traitées.

2.3 Comment calculer la complexité d’un algo-


rithme ?
2.3.1 Introduction

Souvent la complexité dépendra de sa taille mais aussi de la donnée en elle-même,


en particulier la façon dont sont réparties les différentes valeurs qui la constituent.
Imaginons par exemple que l’on effectue une recherche séquentielle d’un élément
dans une liste non triée. Le principe de l’algorithme est simple, on parcourt un par
un les éléments jusqu’à trouver, ou pas, celui recherché. Ce parcours peut s’arrêter
dès le début si le premier élément est "le bon". Mais on peut également être amené
à parcourir la liste en entier si l’élément cherché est en dernière position, ou même
n’y figure pas. Le nombre d’opération élémentaires effectuées dépend donc non
seulement de la taille de la liste, mais également de la répartition de ses valeurs.
Cette remarque nous conduit à préciser un peu notre définition de la complexité

25
C OMPLEXITÉ ALGORITHMIQUE

en temps. En toute rigueur, on devra en effet distinguer trois formes de complexité


en temps :
— la complexité dans le meilleur des cas : c’est la situation la plus favorable,
qui correspond par exemple à la recherche d’un élément situé à la première
position d’une liste, ou encore au tri d’une liste déjà triée.
— la complexité dans le pire des cas : c’est la situation la plus défavorable,
qui correspond par exemple à la recherche d’un élément dans une liste alors
qu’il n’y figure pas, ou encore au tri par ordre croissant d’une liste triée par
ordre décroissant.
— la complexité en moyenne : on suppose là que les données sont réparties
selon une certaine loi de probabilités.
E XEMPLE
Pour l’analyse de l’algorithme 2.2, on suppose que les éléments sont tous
disjoints et que toutes les positions pour les éléments dans le tableau sont
équiprobables. Ainsi, si le tableau contient les éléments 2, 3 et 5 alors il y a
6 tableaux possibles :
1. 2|3|5 avec 3 affectations,
2. 2|5|3 avec 2 affectations,
3. 3|2|5 avec 2 affectations,
4. 3|5|2 avec 2 affectations,
5. 5|2|3 avec 1 affectation,
6. 5|3|2 avec 1 affectation,
Seul l’ordre des éléments est important ainsi on peut limiter l’analyse au
cas où le tableau ne contient que les entiers de 1 à n (on peut donc se limiter
dans l’analyse aux permutations des entiers de 1 à n). Et chacune apparaît
avec la même probabilité : 1/6.
Dans le cas général, il y a n! possibilités (le nombre de permutations), cha-
cune ayant la probabilité 1/n! d’apparaître.
On remarque donc que le nombre minimum d’affectations est égal à 1
(avec la probabilité 1/n, le plus grand élément est dans la case 1), le nombre
maximum est égal à n (avec la probabilité 1/n!, les éléments sont triés
par ordre croissant) et le nombre moyen est égal Hn (à l’itération i, il y a
une probabilité 1/i d’avoir une affectation) c’est à dire approximativement
log(n).

26
On calculera le plus souvent la complexité dans le pire des cas, car elle est la plus
pertinente. Il vaut mieux en effet toujours envisager le pire. La moyenne est aussi
pertinente mais elle nécessite de connaître la distribution des données (problème
indécidable sans information a priori).
E XEMPLE
Prenons par exemple la fonction suivante f (1) = 1, f (n) = f (n − 1)/2 si
n est pair, f (n) = 3f (n − 1) + 1 sinon. On conjecture que cette fonction
est définie pour tout n et que sa valeur est 1 pour tout n. Donc à l’heure
actuelle personne ne connaît la complexité du calcul de cette fonction (en
utilisant le programme récursif induit par la définition). On ne sait même
pas si elle peut former une boucle infinie ou non.

D ÉFINITION 2.3.1 : Soit P (X) : le nombre de fois que l’on exécute l’opération
élémentaire sur la structure X. Dans la suite nous allons voir quelques règles
(malheureusement non complètes) qui permettront souvent de calculer P (X) :
— X = X1; X2 (X1 suivi de X2) alors P (X) = P (X1) + P (X2).
— X = si C alors X1 sinon X2 alors
— P (X) ≤ P (C) + max(P (X1), P (X2)).
— si p(C) est la probabilité que C soit vrai alors moy(P (X)) = p(C) ×
moy(P (X1)) + (1 − p(C)) × moy(P (X2)).
— P
Si X est une boucle pour i allant de a à b faire Xi alors P (X) =
b
i=a P (Xi).

Lors d’un appel à une fonction avec comme paramètre une donnée d, on compte
le nombre d’exécutions de l’opération élémentaire effectuées pendant l’appel. Si
l’appel est récursif alors nous sommes confrontés à la résolution d’une équation
de récurrence.

2.3.2 Calcul de complexité pour les algorithmes itératifs

Pour le calcul de complexité pour les algorithmes itératifs il est nécessaire de


compter le nombre de boucles et les intervalles de parcours pour les indices des
boucles.
Nous utiliserons les formules suivantes :
Pn
— i=1 a = an = O(n),
Pn n(n+1)
— i=1 i = 2
= O(n2 ),

27
C OMPLEXITÉ ALGORITHMIQUE

Pn n(n+1)(2n+1)
— i=1 i2 = 6
= O(n3 ).
E XEMPLE
1. Considérons l’algorithme 2.3

Algorithme 2.3 Algorithme itératif linéaire


for i allant de 1 à n do
Afficher (*) ;
end for

Pn
Le nombre d’étoiles affiché est i=1 1 = n,
2. Considérons l’algorithme 2.4

Algorithme 2.4 Algorithme itératif quadratique


for i allant de 1 à n do
for j allant de 1 à n do
Afficher (*) ;
end for
end for

Pn Pn
Le nombre d’étoiles affiché est i=1 ( j=1 1) = n2 .
3. Considérons l’algorithme 2.4

Algorithme 2.5 Algorithme itératif cubique


for i allant de 1 à n do
for j allant de 1 à n do
for k allant de 1 à n do
Afficher (*) ;
end for
end for
end for

Pn Pn Pn
Le nombre d’étoiles affiché est i=1 ( j=1 k=1 1) = n3 .

28
2.3.3 Calcul de complexité pour les algorithmes récursifs

Nous allons établir quelques résultats de complexité concernant les appels récur-
sifs. Dans ce qui suit, on note tn ou encore T (n) le nombre d’appels récursifs
effectués par un appel de l’algorithme avec la donnée n.

2.3.3.1 Résolution d’équation de récurrence d’ordre un

Dans cette section, nous proposons de donner les solutions de certaines équations
de récurrence classiques en informatique.
T HÉORÈME 2.3. 1 : Nous avons les résultats suivants (si le nombre d’instruc-
tions lors des appels récursifs est constant) :
1. 1 appel récursif au rang (n − 1) : l’algorithme admet une complexité globale
de θ(n),
2. 1 appel récursif au rang (n/2) : l’algorithme admet une complexité globale
de θ(log n),
3. 2 appels récursifs au rang (n − 1) : l’algorithme admet une complexité glo-
bale de θ(2n ),
4. 2 appels récursifs au rang (n/2) : l’algorithme admet une complexité globale
de θ(n).

Preuve
Pour simplifier, on se contente de montrer ce résultat sur un algorithme représen-
tatif de chaque catégorie.

Algorithme 2.7 Procédure R EC 2.


Algorithme 2.6 Procédure R EC 1.
if n < 2 then
if n < 2 then
R ETOURNER 0 ;
R ETOURNER 0 ;
else
else
R ETOURNER (R EC 2(ndiv2) +
R ETOURNER (R EC 1(n − 1)) ;
2),
end if
end if

29
C OMPLEXITÉ ALGORITHMIQUE

Algorithme 2.8 Procédure R EC 3. Algorithme 2.9 Procédure R EC 4.


if n < 2 then if n < 2 then
R ETOURNER 0 ; R ETOURNER 1 ;
else else
R ETOURNER (R EC 3(n − R ETOURNER (R EC 4(ndiv2) +
1)∗R EC 3(n − 1) + 2)) ; 2)∗R EC 4(ndiv2) + 3) ;
end if end if
1. Etude de l’algorithme R EC 1 :

t0 = t1 = 0
tn = 1 + tn1
tn−1 = 1 + tn−2
..
.
t2 = 1 + t1
Xn
tn = 1 + t1
i=2
tn = n − 1

L’algo est en θ(n).


2. Nous avons tn = 1 + tndiv2 et t0 = t1 = 0. Pour simplifier, on suppose
n = 2k . Nous effectuons un changement de variable en posant n = 2k et
vk = tn . Alors nous avons v0 = t1 = 0 et

vk = 1 + vk−1

Ainsi nous avons vk = k et tn = k = log2 n.


3. Comme précédemment nous calculons une relation de récurrence :

30
tn = 2 + 2tn−1
= 2 + 4 + 4tn−2
= 2 + 4 + 8 + 8tn−3
= ...
n−1
X
= 2i + t1 = 2n − 2
i=1

Il est important de noter que nous pouvons linéariser l’algorithme, en mé-


morisant rec3(n − 1) dans une variable aux et en retournant (aux ∗ (aux +
2)). On obtient ainsi une complexité en O(n).
4. Pour simplifier, on pose n = 2k et vk = tn , alors nous avons tn = 2tn/2 + 2
donc vk = 2vk−1 + 2. Alors vk = 2k+1 − 2 = 2n − 2 = tn .

E XEMPLE

Algorithme 2.10 Calcul de factorielle n! FACT(n).


if n = 0 then
R ETOURNER 1 ;
else
R ETOURNER n∗FACT(n − 1) ;
end if

Soit tn le nombre de multiplications pour calculer FACT(n) (c’est évidem-


ment l’opération fondamentale pour le calcul de n!). On a t0 = 0 et pour
n > 0, tn = 1 + tn−1 . La résolution est immédiate et la solution est tn = n.

2.3.3.2 Equation de récurrence d’ordre deux

Soit tn le nombre d’additions pour le calcul de Fibonacci de n.


On a t0 = t1 = 0 et pour n > 1, tn = tn−1 + tn−2 .
On reconnaît une équation linéaire homogène d’ordre 2 associée au polynôme
X 2 − X − 1. Sa solution est égales à α × x1 + β × x2 si le polynôme a deux

31
C OMPLEXITÉ ALGORITHMIQUE

Algorithme 2.11 Calcul des nombres de Fibonacci F IBO.


if n < 2 then
R ETOURNER n ;
else
R ETOURNER F IBO(n − 1)+F IBO(n − 2) ;
end if

racines distinctes, sinon à (α × n + β)x1 . Les valeurs initiales de u permettent de


calculer α et β.
La résolution est moins triviale que pour la formule√précédente mais elle est fai-
sable : tn = θ(ρn ) où ρ est le nombre d’or ((1 + 5)/2). En pratique dans de
nombreux cas on ne sait pas résoudre l’équation de récurrence et l’on se contente
d’encadrer ou de majorer tn .
E XEMPLE

Algorithme 2.12 Calcul de la fonction M YSTERE.


if n < 2 then
R ETOURNER 1 ;
else
R ETOURNER M YSTERE(n − 1)+M YSTERE(n/2) + 1 ;
end if

Soit tn le nombre d’additions pour calculer M YSTERE(n) (c’est évidem-


ment l’opération fondamentale pour le calcul de mystère de n). On a
t0 = t1 = 0 et pour n > 1, tn = 2 + tn−1 + tn/2 .
Le résultat sera compris entre les 2 résultats précédents (on peut évidem-
ment améliorer cet encadrement).

32
2.3.4 Résolution d’équation de récurrence type diviser-régner
& Master theorem

Nous allons utiliser le Théorème dit le Master Theorem qui permet de calculer
rapidement la complexité des algorithmes.
T HÉORÈME 2.3. 2 : S’il existe trois entiers a ≥ 0, b > 1, d ≥ 0 et n0 > 0 tels
que pour tout n ≥ n0 , T (n) ≤ aT (d nb e) + O(nd ), alors


 O(nd ) si bd > a
T (n) = O(n log n) si bd = a
d

 O(n log
 a
log b ) si bd < a

Dans ce théorème, on peut considérer que nd représente le nombre d’opérations


élémentaires par appel, a représente le nombre d’appels récursifs contenus dans
le corps de l’algorithme, n/b représente le rang de ces appels.
Preuve
Nous allons illustrer le Théorème 2.3.2 par la figure 2.2.
Ainsi nous trouvons

O(nd ) si bd > a

l l
X n X a 
= ai ( i )d = nd ( d )i = O(nd log n) si bd = a
b b log a
si bd < a

i=0 i=0 O(n log b )

Le tableau 2.2 suivant récapitule les complexités de références :
La complexité en temps n’est pas la seule mesure de performance d’un algorithme.
Une autre mesure, moins fréquemment utilisée (mais pas moins intéressante ma-
thématiquement), est la complexité en espace. Elle spécifie l’espace mémoire uti-
lisé par l’algorithme pour résoudre un problème. Parfois on peut avoir des méta-
algorithmes qui permettent d’obtenir une complexité temporelle en fonction d’une
complexité spatiale : plus on a d’espace plus cela va vite.

33
C OMPLEXITÉ ALGORITHMIQUE

1 × nd

a +

a × (n/b)d

a2 × (n/b2 )d

..
.
+

al−1 × (n/bl−1 )d

... +

al × (n/bl )d
l = logb n

F IGURE 2.2 – Illustration du master theorem.

O Type de complexité Exemple


O(1) constant Affectation
O(log n) logarithmique Recherche dichotomique
O(n) linéaire Parcours dans un tableau
O(n log n) quasi-linéaire Tri fusion
O(n2 ) quadratique Tri de base
O(n3 ) cubique Multiplication matricielle
O(2n ) exponentielle Enumération des sous-ensembles {1, . . . , n}
O(n!) factorielle Enumération des permutations {1, . . . , n}
TABLE 2.2 – Tableau synthétique des complexités de référence et exemple.

34
f (n) 2n n3
n2

n log n
n


n

ln(n)
n

F IGURE 2.3 – Représention des fonctions de bases. Attention les échelles ne sont pas les
mêmes.

35
C OMPLEXITÉ ALGORITHMIQUE

36
Chapitre
Listes, piles, files et listes
3 chaînées

Sommaire
3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.2 Les listes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.2.1 Présentation . . . . . . . . . . . . . . . . . . . . . . . 39
3.3 Piles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . 40
3.3.2 Opérations de base . . . . . . . . . . . . . . . . . . . 41
3.4 Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3.5 Listes chaînées . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.5.1 Présentation . . . . . . . . . . . . . . . . . . . . . . . 44
3.5.2 Recherche dans une liste chaînée . . . . . . . . . . . . 46
3.5.3 Insertion dans une liste chaînée . . . . . . . . . . . . 46
3.5.4 Suppression dans une liste chaînée . . . . . . . . . . . 46
3.6 Analyse amortie . . . . . . . . . . . . . . . . . . . . . . . . 47
3.6.1 Piles . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
3.6.2 Incrémentation d’un compteur binaire . . . . . . . . . 50
3.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . 51

Résumé

Dans ce chapitre nous étudierons deux structures linéaires de données


fondamentales. La notion de spécification des structures de données
est essentielle pour le conception d’algorithmes sûrs. Il est fondamen-
tal que l’utilisation des fonctions de base soit bornée et limitée.

37
S TRUCTURES DE DONNÉES CLASSIQUES

3.1 Introduction
La notion d’ensemble est aussi fondamentale pour l’informatique que pour les
mathématiques. Alors que les ensembles mathématiques sont stables, ceux ma-
nipulés par les algorithmes peuvent croître, diminuer, ou subir d’autres modifi-
cations au cours du temps. On dit de ces ensembles qu’ils sont dynamiques. Les
cinq prochains chapitres présentent quelques techniques élémentaires permettant
de représenter des ensembles dynamiques finis et de les manipuler sur un ordi-
nateur. Les types d’opération à effectuer sur les ensembles peuvent varier d’un
algorithme à l’autre. Par exemple, de nombreux algorithmes se contentent d’in-
sérer, de supprimer ou de tester l’appartenance. Un ensemble dynamique qui re-
connaît ces opérations est appelé dictionnaire. D’autres algorithmes nécessitent
des opérations plus complexes. Par exemple, les files de priorités min, présentées
au chapitre 6 dans le contexte de la structure de données tas, permettent de faire
des opérations d’insertion et d’extraction du plus petit élément d’un ensemble. La
meilleure façon d’implémenter un ensemble dynamique dépend des opérations
qu’il devra reconnaître.
Les piles et les files sont des ensembles dynamiques pour lesquels l’élément à
supprimer via l’opération S UPPRIMER est défini par la nature intrinsèque de l’en-
semble. Dans une pile, l’élément supprimé est le dernier inséré : la pile met en
œuvre le principe dernier entré, premier sorti, ou LIFO (Last-In, First-Out). De
même, dans une file, l’élément supprimé est toujours le plus ancien ; la file met en
œuvre le principe premier entré, premier sorti, ou FIFO (First-In, First-Out).
E XEMPLE
Noter l’analogie avec une pile d’assiette et la file d’attente.

Il existe plusieurs manières efficaces d’implémenter des piles et des files dans un
ordinateur. Dans cette section, nous allons montrer comment les implémenter à
l’aide d’un tableau simple.
Nous allons définir des types abstraits et les fonctions de base servant à les mani-
puler. Il est important que noter les types abstraits sont définis à partir de construc-
teurs.

38
3.2 Les listes
3.2.1 Présentation

Le premier type abstrait que nous allons étudier est le type liste des éléments.
D ÉFINITION 3.2.1 : Une liste est une structure de données permettant de re-
grouper des données. Une liste L est composée de 2 parties : sa tête (souvent
noté T ÊTE), qui correspond au dernier élément ajouté à la liste, et sa queue
(souvent noté Q UEUE) qui correspond au reste de la liste.
Avec cette définition, il est évident que la structure de données qui sera utilisée
aura une influence sur la complexité des opérations de base.
Regardons les caractéristiques souhaitées pour une liste d’éléments.
S PÉCIFICATION 3.2.1 :
— créer une liste vide (L =V IDE () on a créé une liste L vide)
— tester si une liste est vide (EST V IDE(L) renvoie vrai si la liste L est vide)
— ajouter un élément en tête de liste (AJOUTE E N T ÊTE (x, L) avec L une liste
et x l’élément à ajouter)
— supprimer la tête x d’une liste L (Q UEUE(L)) et renvoyer cette tête x
(T ÊTE(L)
— Compter le nombre d’éléments présents dans une liste (COMPTE(L) ren-
voie le nombre d’éléments présents dans la liste L).
La fonction AJOUTE E N T ETE permet d’obtenir une nouvelle liste à partir d’une
liste et d’un élément (L1 = A JOUTE E N T ETE(x,L)). Il est possible « d’enchaî-
ner » les AJOUTE E N T ETE et d’obtenir ce genre de structure : AJOUTE E N -
T ETE(x,AJOUTE E N T ETE(y,AJOUTE E N T ETE(z, L)))

39
S TRUCTURES DE DONNÉES CLASSIQUES

3.3 Piles
3.3.1 Introduction

Dans cette partie nous allons préciser les fonctionnalités souhaitées pour manipu-
ler les piles.
S PÉCIFICATION 3.3.1 :
— Sorte Pile
— utilise Booléen, Elément
— Opérations

— P ILE - VIDE :→ pile


— E MPILER :P ile × Elément → P ile
— D ÉPILER :P ile → P ile
— SOMMET : P ile → Elément
— EST- VIDE : P ile → Booléen

— Les opérations D ÉPILER et SOMMET ne sont définies que si la pile n’est pas
vide :

— D ÉPILER(p) est défini ssi EST V IDE(p) =FAUX


— SOMMET(p) est défini ssi EST V IDE(p) =FAUX

— Ces opérations vérifient les axiomes suivants, où p est de sorte Pile, et e


de sorte Elément ;

— D ÉPILER(E MPILER(p, e)) = p


— SOMMET(E MPILER(p, e)) = e
— EST- VIDE(P ILE - VIDE)=VRAI
— EST- VIDE(E MPILER(p, e)) =FAUX

Cette spécification est suffisamment complète car on sait réécrire en utilisant le


deuxième axiome toute expression définie, sans variable, comportant une opé-
ration D ÉPILER, en une expression équivalente ne comportant que les opérations
P ILE - VIDE et E MPILER. Or, on peut déduire par les autres axiomes le résultat des
observateurs SOMMET et EST- VIDE sur toute expression sans variable, définie, ne
comportant que P ILE - VIDE et E MPILER.

40
1 2 3 4 5 6 7 1 2 3 4 5 6 7

P 15 7 2 8 P 15 7 2 8 9 4

a) SOMMET[P ] = 4 b) SOMMET[P ] = 6

1 2 3 4 5 6 7

P 15 7 2 8 9 4

c) SOMMET[P ] = 5

F IGURE 3.1 – Implémentation via un tableau d’une pile P . En a) la situation de la pile


initiale. En b) la situation de la pile avec l’ajout de deux éléments 9 et 4 pour l’utilisation
de la fonction E MPILER(P, 9) et E MPILER(P, 4). En. c) la situation de la pile P après
l’utilisation de la fonction D ÉPILER(P ).

3.3.2 Opérations de base

L’opération I NSÉRER dans une pile est souvent appelée E MPILER et l’opération
S UPPRIMER, qui ne prend pas d’élément pour argument, est souvent appelée D É -
PILER . Ces noms font allusion aux piles rencontrées dans la vie de tous les jours,
comme les piles d’assiettes automatiques en usage dans les cafétérias. L’ordre
dans lequel les assiettes sont dépilées est l’inverse de celui dans lequel elles ont
été empilées, puisque seule l’assiette supérieure est accessible.
Comme on le voit à la figure 3.1, il est possible d’implémenter une pile d’au
plus n éléments avec un tableau P [1 . . . n]. Le tableau possède un attribut SOM -
MET [P ] qui indexe l’élément le plus récemment inséré. La pile est constituée des
éléments P [1 . . .SOMMET[P ]], où P [1] est l’élément situé à la base de la pile et
P [SOMMET[P ]] est l’élément situé au sommet.
Quand SOMMET[P ] = 0, la pile ne contient aucun élément ; elle est vide. On peut
tester si la pile est vide à l’aide de l’opération de requête P ILE - VIDE. Si l’on tente
de D ÉPILER une pile vide, on dit qu’elle déborde négativement, ce qui est en
général une erreur. Si SOMMET[P ] dépasse n, on dit que la pile déborde. Dans

41
S TRUCTURES DE DONNÉES CLASSIQUES

notre pseudo code, on ne se préoccupera pas d’un débordement éventuel de la


pile.

Algorithme 3.1 Procédure P ILE -


VIDE . Algorithme 3.2 Procédure E MPI -
if SOMMET[P ] = 0 then LER (P, x)
R ETOURNER VRAI
SOMMET [P ] = SOMMET [P ] + 1 ;
else
P [SOMMET[P ]] := x ;
R ETOURNER FAUX
end if

Algorithme 3.3 Procédure D ÉPILER(F ).


if P ILE - VIDE(P ) then
R ETOURNER ERREUR ;
else
SOMMET [P ] = SOMMET [P ] − 1 ;
end if
R ETOURNER P [SOMMET[P ] + 1] ;

3.4 Files
Nous présentons une nouveau type abstrait, les files. La spécification de la notion
de file est donnée ci-dessous.
S PÉCIFICATION 3.4.1 : On appelle E NFILER l’opération I NSÉRER sur une file
et on appelle D ÉFILER l’opération S UPPRIMER . La propriété FIFO d’une file la
fait agir comme une file à un guichet d’inscription.
La file comporte une tête et une queue. Lorsqu’un élément est enfilé, il prend
place à la queue de la file.
La figure 3.2 montre une manière d’implémenter une file d’au plus n − 1 éléments
à l’aide d’un tableau F [1 . . . n]. La file comporte un attribut TÊTE[F] qui indexe,
ou pointe vers, sa tête. L’attribut QUEUE[F ] indexe le prochain emplacement où
sera inséré un élément nouveau. Les éléments de la file se trouvent aux empla-
cements TÊTE[F ], TÊTE[F ] + 1, . . ., QUEUE[F ] − 1, après quoi l’on « boucle » :
l’emplacement 1 suit immédiatement l’emplacement n dans un ordre circulaire.
Quand TÊTE[F ] =QUEUE[F ], la file est vide. Au départ, on a TÊTE[F ] =
QUEUE[F ] = 1. Quand la file est vide, tenter de D ÉFILER un élément provoque
un débordement négatif de la file.

42
1 2 3 4 5 6 7 8 9 10 11 12
a) F 15 7 2 8 4

TÊTE [F ] =7 QUEUE [F ] = 12
1 2 3 4 5 6 7 8 9 10 11 12
b) F 11 6 15 7 2 8 4 3

QUEUE[F ] =3 TÊTE [F ] =7

1 2 3 4 5 6 7 8 9 10 11 12
c) F 11 6 15 7 2 8 4 3

QUEUE[F ] =3 TÊTE [F ] =8

F IGURE 3.2 – Implémentation via un tableau d’une file P .

Quand TÊTE[F ] =QUEUE[F ] + 1, la file est pleine ; tenter d’enfiler un élément


provoque alors un débordement.
Dans nos procédures E NFILER et D ÉFILER, le test d’erreur pour les débordements
a été omis.

Algorithme 3.5 Procédure D ÉFI -


Algorithme 3.4 Procédure E NFI -
LER (F )
LER (F, x).
x = F [TÊTE[F ]] ;
F [QUEUE[F ]] = x ;
if TÊTE[F ]] =LONGUEUR[F ]
if QUEUE[P ] =LONGUEUR[F ] ;
then
then
TÊTE [F ]] = 1
QUEUE[F ] = 1 ;
else
else
TÊTE [F ]] = TÊTE [F ]] + 1 ;
QUEUE[F ] = QUEUE[F ] + 1 ;
end if
end if
R ETOURNER x ;

43
S TRUCTURES DE DONNÉES CLASSIQUES

E XEMPLE
Figure 3.2 les emplacements de la file paraissent uniquement aux positions
en gris clair.
— a) La file contient cinq éléments, aux emplacements F [7..11].
— b) La configuration de la file après les appels E NFILER(F, 3), E NFILER(F, 11)
et E NFILER(F, 6).
— c) La configuration de la file après l’appel D ÉFILER(F ) qui retourne la va-
leur de clé 15 précédemment en tête de file. La nouvelle tête a la clé 7.

3.5 Listes chaînées


3.5.1 Présentation
S PÉCIFICATION 3.5.1 :
Une liste chaînée est une structure de données dans laquelle les objets sont
arrangés linéairement. Toutefois, contrairement au tableau, pour lequel l’ordre
linéaire est déterminé par les indices, l’ordre d’une liste chaînée est déterminé
par un pointeur 1 dans chaque objet. Les listes chaînées fournissent une re-
présentation simple et souple pour les ensembles dynamiques, supportant (pas
toujours très efficacement) toutes les opérations de bases.
E XEMPLE
Comme le montre la figure 3.3, chaque élément d’une liste doublement
chaînée L est un objet comportant un champ clé et deux autres champs
pointeurs : SUCC et PRÉD.
L’objet peut aussi contenir d’autres données satellites. Étant donné un élé-
ment x de la liste, SUCC[x] pointe sur son successeur dans la liste chaînée
et PRÉD[x] pointe sur son prédécesseur. Si PRÉD[x] =NIL, l’élément x n’a
pas de prédécesseur et est donc le premier élément, aussi appelé tête de
liste.
Si SUCC[x] =NIL, l’élément x n’a pas de successeur et est donc le dernier
élément, aussi appelé QUEUE de liste 2 . Un attribut TÊTE[L] pointe sur le
premier élément de la liste. Si TÊTE[L] =NIL, la liste est vide.

2. ne pas confondre avec la fonction Q UEUE de la section 3.2 qui supprime le premier élément
d’une liste

44
PRÉD CLÉ SUCC

a) TÊTE[L] / 15 7 /

b) TÊTE[L] / 35 15 7 /

c) TÊTE[L] / 35 7 /

F IGURE 3.3 – a) Une liste doublement chaînée L représentant l’ensemble dynamique


{7, 15}. Chaque élément de la liste est un objet avec des champs contenant la clé et des
pointeurs (représentés par des flèches) sur les objets suivant et précédent. Le champ SUCC
de la queue et le champ PRÉD de la tête valent NIL, représenté par un slash. b) Après
l’exécution de I NSÉRER - LISTE(L, x), où CLÉ[x] = 35, la liste chaînée contient à sa tête
un nouvel objet ayant pour clé 35. Ce nouvel objet pointe sur l’ancienne tête de clé 15.
c) Le résultat de l’appel S UPPRIMER - LISTE(L, x) ultérieur, où x pointe sur l’objet ayant
pour clé 15.

E XEMPLE
— a) Une liste doublement chaînée L représentant l’ensemble dynamique
{7, 15}. Chaque élément de la liste est un objet avec des champs contenant
la clé et des pointeurs (repré- sentés par des flèches) sur les objets suivant
et précédent. Le champ SUCC de la queue et le champ PRÉD de la tête valent
NIL , représenté par un slash. L’attribut TÊTE [L] pointe sur la TÊTE .
— b) Après l’exécution de I NSÉRER - LISTE(L, x), où CLÉ[x] = 35, la liste chaî-
née contient à sa tête un nouvel objet ayant pour clé 35. Ce nouvel objet
pointe sur l’ancienne tête de clé 15.
— c) Le résultat de l’appel S UPPRIMER - LISTE(L, x) ultérieur, où x pointe sur
l’objet ayant pour clé 15.

Une liste peut prendre différentes formes. Elle peut être chaînée, ou doublement
chaînée, triée ou non, circulaire ou non. Si une liste chaînée est simple, on omet

45
S TRUCTURES DE DONNÉES CLASSIQUES

le pointeur PRÉD de chaque élément. Si une liste est triée, l’ordre linéaire de la
liste correspond à l’ordre linéaire des clés stockées dans les éléments de la liste ;
l’élément minimum est la TÊTE de la liste et l’élément maximum est la QUEUE.
Si la liste est non-triée, les éléments peuvent apparaître dans n’importe quel ordre.
Dans une liste circulaire, le pointeur PRÉD de la TÊTE de liste pointe sur la QUEUE
et le pointeur SUCC de la QUEUE de liste pointe sur la TÊTE. La liste peut donc être
vue comme un anneau d’éléments. Dans le reste de cette section, on suppose que
les listes sur lesquelles nous travaillons sont non triées et doublement chaînées.

3.5.2 Recherche dans une liste chaînée

La procédure R ECHERCHER - LISTE(L, k) trouve le premier élément de clé k dans


la liste L par une simple recherche linéaire et retourne un pointeur sur cet élément.
Si aucun objet de clé k n’apparaît dans la liste, la procédure retourne NIL. Si l’on
prend la liste chaînée de la figure 3.3 b), l’appel R ECHERCHER - LISTE(L, 7) re-
tourne un pointeur sur le troisième élément et l’appel R ECHERCHER - LISTE(L, 4)
retourne NIL.
Pour parcourir une liste de n objets, la procédure R ECHERCHER - LISTEs’exécute
en θ(n) dans le cas le plus défavorable, puisqu’on peut être obligé de parcourir la
liste entière.

3.5.3 Insertion dans une liste chaînée

Étant donné un élément x dont le champ clé a déjà été initialisé, la procédure
I NSÉRER - LISTE « greffe » x à l’avant de la liste chaînée, comme on le voit sur
la figure 3.3 b).
Le temps d’exécution de I NSÉRER - LISTE sur une liste de n éléments est O(1).

3.5.4 Suppression dans une liste chaînée

La procédure S UPPRIMER - LISTE élimine un élément x d’une liste chaînée L. Il


faut lui fournir un pointeur sur x et elle se charge alors de « détacher » x de la liste
en mettant les pointeurs à jour. Si l’on souhaite supprimer un élément ayant une
clé donnée, on doit commencer par appeler R ECHERCHER - LISTE pour récupérer
un pointeur sur l’ élément.
La figure 3.3 c) montre comment un élément est supprimé dans une liste chaî-
née. S UPPRIMER - LISTE s’exécute dans un temps en O(1) ; mais si l’on souhaite

46
supprimer un élément à partir de sa clé, il faut un temps θ(n) dans le cas le plus
défavorable, car on doit commencer par appeler R ECHERCHER - LISTE.

Algorithme 3.6 Procédure R ECHERCHER - LISTE(L, k).


x = TÊTE[L]] ;
while x 6= NIL et CL É[x] 6= k do
x = SUCC[x] ;
end while
R ETOURNER x ;

Algorithme 3.7 Procédure I NSÉRER - LISTE(L, x).


SUCC [x] = TÊTE [L]] ;
if TÊTE[L] 6= NIL ; then
PRÉD [ TÊTE [L]] := x ;
end if
TÊTE [L] = x ;
PRÉD [x] = NIL ;

Algorithme 3.8 Procédure S UPPRIMER - LISTE(L, x).


if PRÉD[x] 6= NIL ; then
SUCC [ PRÉD [x]] := SUCC [x] ;
else
TÊTE [L]= SUCC [x]
end if
if SUCC[x] 6= NIL then
PRÉD [ SUCC [x]] := PRÉD [x]
end if

3.6 Analyse amortie


Dans une analyse amortie, le temps requis pour effectuer une suite d’opérations
sur une structure de données est une moyenne sur l’ensemble des opérations ef-
fectuées. L’analyse amortie permet de montrer que le coût moyen d’une opération
est faible si l’on établit sa moyenne sur une suite d’opérations, même si l’opéra-
tion prise individuellement est coûteuse. L’analyse amortie diffère de l’analyse du

47
S TRUCTURES DE DONNÉES CLASSIQUES

cas moyen au sens où on ne fait pas appel aux probabilités : une analyse amor-
tie garantit les performances moyennes de chaque opération dans le cas le plus
défavorable.
D ÉFINITION 3.6.1 : Dans la méthode de l’agrégat on montre que, pour tout
n, une suite de n opérations prend le temps total T (n) dans le cas le plus dé-
favorable. Dans le cas le plus défavorable le coût moyen, ou coût amorti, par
opération est donc T (n)/n. Notez que ce coût amorti s’applique à chaque opé-
ration, même quand il existe plusieurs types d’opérations dans la séquence.
Les deux autres méthodes que nous étudierons pourront affecter des coûts amortis
différents aux différents types d’opérations.

3.6.1 Piles

Dans notre premier exemple d’analyse via méthode de l’agrégat, on analyse des
piles qui ont été étendues avec une nouvelle opération. La section 3.3 présentait
les deux opérations fondamentales de pile qui prenaient chacune un temps O(1) :
— E MPILER(S, x) empile l’objet x sur la pile S.
— D ÉPILER(S) dépile le sommet de la pile S et retourne l’objet dépilé.
Comme chacune de ces opérations s’exécute en O(1), considérons qu’elles ont
toutes les deux un coût égal à 1. Le coût total d’une suite de n opérations E MPI -
LER et D ÉPILER vaut donc n, et le temps d’exécution réel pour n opérations est
donc θ(n). Ajoutons l’opération D ÉPILER M UL(S, k), qui retire les k premiers ob-
jets du sommet de la pile S, ou dépile la pile toute entière si elle contient moins de
k objets. Dans le pseudo code suivant, l’opération P ILE - VIDE retourne VRAI s’il
n’y a plus aucun objet sur la pile, et FAUX sinon.

Algorithme 3.9 Procédure D ÉPILER M UL(S, k).


while !P ILE - VIDE(S) et k 6= 0 do
D ÉPILER(S)
k = k − 1;
end while

Quel est le temps d’exécution de D ÉPILER M UL(S, k) sur une pile de s objets ?
Le temps d’exécution réel est linéaire par rapport au nombre d’opérations D É -
PILER effectivement exécutées, et il suffit donc d’analyser D ÉPILER M UL en
fonction des coûts abstraits de 1 attribués à E MPILER et D ÉPILER. Le nombre

48
d’itérations de la boucle tant que est le nombre min(s, k) d’objets retirés de la
pile. Pour chaque itération de la boucle, on fait un appel à D ÉPILER(S). Le coût
total de D ÉPILER M UL est donc min(s, k), et le temps d’exécution réel est une
fonction linéaire de ce coût.
L EMME 3.6.1 : Dans le pire des cas, n opérations de E MPILER, D ÉPILER et
D ÉPILER M UL admettent une complexité de O(n2 ).
Preuve
Analysons une suite de n opérations E MPILER, D ÉPILER, et D ÉPILER M UL sur
une pile initialement vide. Le coût dans le cas le plus défavorable d’une opéra-
tion D ÉPILER M UL dans la séquence est O(n), puisque la taille de la pile est au
plus égale à n. Le coût dans le cas le plus défavorable d’une opération de pile
quelconque est donc O(n), et une suite de n opérations coûte O(n2 ), puisqu’on
pourrait avoir O(n) opérations D ÉPILER M UL coûtant chacune O(n). Bien que
cette analyse soit correcte, le résultat O(n2 ), obtenu en considérant le coût le plus
défavorable de chaque opération individuelle n’est pas assez fin.

L EMME 3.6.2 : Dans le cas d’une analyse amortie (ou de coût moyen) n opé-
rations de E MPILER, D ÉPILER et D ÉPILER M UL admettent un complexité de O(1)
par opération.
Preuve
Grâce à la méthode d’analyse par agrégat, on peut obtenir un meilleure majorant,
qui prend en compte globalement la suite des n opérations. En fait, bien qu’une
seule opération D ÉPILER M UL soit potentiellement coûteuse, une suite de n opé-
rations E MPILER, D ÉPILER et D ÉPILER M UL sur une pile initialement vide peut
coûter au plus O(n).
Pourquoi ? Chaque objet peut être dépilé au plus une fois pour chaque empilement
de ce même objet. Donc, le nombre de fois que D ÉPILERpeut être appelée sur
une pile non vide, y compris les appels effectués à l’intérieur de D ÉPILER M UL,
vaut au plus le nombre d’opérations E MPILER, qui lui-même vaut au plus n. Pour
une valeur quelconque de n, n’importe quelle suite de n opérations E MPILER,
D ÉPILER et D ÉPILER M UL prendra donc au total un temps O(n).
Le coût moyen d’une opération est O(n)/n = O(1). Dans l’analyse de l’agrégat,
chaque opération se voit affecter du même coût amorti, égal au coût moyen. Dans
cet exemple, chacune des trois opérations de pile a donc un coût amorti O(1).
Insistons encore sur le fait que, bien que nous ayons simplement montré que le
coût moyen, et donc le temps d’exécution, d’une opération de pile était O(1), nous
n’avons fait intervenir aucun raisonnement probabiliste. Nous avons en fait établi

49
S TRUCTURES DE DONNÉES CLASSIQUES

une borne O(n) dans le cas le plus défavorable pour une suite de n opérations. La
division de ce coût total par n a donné le coût moyen, c’est-à-dire le coût amorti
par opération. 

3.6.2 Incrémentation d’un compteur binaire

Un autre exemple de la méthode de l’agrégat est illustré par le problème consistant


à implémenter un compteur binaire sur k bits qui compte positivement à partir de
0.
S PÉCIFICATION 3.6.1 : On utilise pour représenter ce compteur un tableau
A[0 . . . k − 1] de bits, où longueur[A] = k.
Un nombre binaire x qui est stocké dans le compteur a son bit d’ordre infé-
rieurPdans A[0] et son bit d’ordre supérieur dans A[k − 1], de manière que
x = k−1 i
i=0 A[i]2 .
Au départ, x = 0, et donc A[i] = 0, pour i = 0, 1, . . . , k − 1. Pour ajouter 1
(modulo 2k ) à la valeur du compteur, on utilise l’algorithme 3.6.1.

Algorithme 3.10 Procédure I NCRÉMENTER(A).


i = 0;
while i < LONGUEUR[A] et A[i] = 1 ; do
A[i] = 0 ; i = i + 1 ;
end while
if i < LONGUEUR[A] then
A[i] = 1 ;
end if

T HÉORÈME 3.6. 1 : Le coût moyen de chaque opération, et donc le coût amorti


par opération, est O(n)/n = O(1).
Preuve
Le tableau 3.1 montre ce qui arrive à un compteur binaire quand il est incrémenté
16 fois, en commençant à la valeur 0 et en finissant par la valeur 16. Au début de
chaque itération de la boucle tant que des lignes 2 − 4, on souhaite ajouter un 1 à
la position i. Si A[i] = 1, alors l’addition d’un 1 fait basculer à 0 le bit i et génère
une retenue de 1, à ajouter à la position i + 1 lors de la prochaine itération de la
boucle. Sinon, la boucle se termine ; ensuite, si i < k, on sait que A[i] = 0, et
donc l’ajout d’un 1 à la position i, qui fait passer le 0 à 1, est pris en charge par

50
l’affectation A[i] = 1. Le coût de chaque opération I NCRÉMENTER est linéaire
par rapport au nombre de bits basculés.
Comme avec l’exemple de la pile, une analyse rapide fournit une borne correcte
mais pas assez fine. Une exécution individuelle de I NCRÉMENTERprend un temps
θ(k) dans le cas le plus défavorable, celui où le tableau A ne contient que des 1.
Donc, une séquence de n opérations I NCRÉMENTERsur un compteur initialement
nul prend un temps O(nk) dans le cas le plus défavorable.
On peut affiner notre analyse pour établir un coût de O(n), dans le pire des cas,
pour une suite de n opérations I NCRÉMENTER en observant que tous les bits ne
basculent pas chaque fois que I NCRÉMENTER est appelée. Comme le montre le
tableau 3.1, A[0] bascule à chaque appel de I NCRÉMENTER. Le deuxième bit de
poids le plus fort, A[1], ne bascule qu’une fois sur deux : une suite de n opérations
I NCRÉMENTER sur un compteur initialisé à zéro fait basculer A[1] bn/2c fois.
De même, le bit A[2] ne bascule qu’une fois sur quatre, c’est-à-dire bn/4c dans
une séquence de n opérations I NCRÉMENTER.
En général, pour i = 0, 1, . . . , k − 1, le bit A[i] bascule bn/2i c fois dans une
séquence de n opérations I NCRÉMENTER sur un compteur initialisé à zéro. Pour
i = k, le bit A[i] ne bascule jamais. Le nombre total de basculements dans la
séquence est donc
k−1 ∞
X X 1
bn/2i c < n i
= 2n
i=0 i=0
2

Sachant que ∞ k 1
P
k=0 x = 1−x Le temps d’exécution, dans le cas le plus défavo-
rable, d’une suite de n opérations I NCRÉMENTER sur un compteur initialisé à
zéro est donc O(n). Le coût moyen de chaque opération, et donc le coût amorti
par opération, est O(n)/n = O(1). 

3.7 Conclusion
Dans ce chapitre, nous avons proposé des structures de données linéaires fonda-
mentales en informatique. Dans la suite de votre cursus vous verrez des structures
arborescentes.

51
S TRUCTURES DE DONNÉES CLASSIQUES

V. Compteur A[7] A[6] A[5] A[4] A[3] A[2] A[1] A[0] Coût total
0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 1 1
2 0 0 0 0 0 0 1 0 3
3 0 0 0 0 0 0 1 1 4
4 0 0 0 0 0 1 0 0 7
5 0 0 0 0 0 1 0 1 8
6 0 0 0 0 0 1 1 0 10
7 0 0 0 0 0 1 1 1 11
8 0 0 0 0 1 0 0 0 15
9 0 0 0 0 1 0 0 1 16
10 0 0 0 0 1 0 1 0 18
11 0 0 0 0 0 0 1 1 19
12 0 0 0 0 1 1 0 0 22
13 0 0 0 0 1 1 0 1 23
14 0 0 0 0 1 1 1 0 25
15 0 0 0 0 1 1 1 1 26
16 0 0 0 0 1 0 0 0 31
TABLE 3.1 – Le comportement d’un compteur binaire sur 8 bits quand sa valeur
passe de 0 à 16 après une séquence de 16 opérations I NCRÉMENTER. Les bits qui
sont basculés pour atteindre la prochaine valeur sont en gras. Les coûts d’exécu-
tion des bits basculés sont donnés à droite. Le coût total ne vaut jamais plus de
deux fois le nombre total d’opérations I NCRÉMENTER.

52
Chapitre
Algorithmes de tri
4
Sommaire
4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4.2 Algorithmes de complexité quadratique . . . . . . . . . . . 56
4.2.1 Tri par sélection . . . . . . . . . . . . . . . . . . . . 56
4.2.2 Tri par insertion séquentielle . . . . . . . . . . . . . . 57
4.3 Algorithme de complexité O(n log n) . . . . . . . . . . . . . 59
4.4 Complexité optimale d’un algorithme de tri par comparaison 60
4.5 Algorithmes de complexité linéaire . . . . . . . . . . . . . . 61
4.5.1 Tri par dénombrement . . . . . . . . . . . . . . . . . 61
4.5.2 Tri par paquets . . . . . . . . . . . . . . . . . . . . . 65
4.5.3 Tri par base . . . . . . . . . . . . . . . . . . . . . . . 67
4.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . 69

Résumé

Ce chapitre est dédié au problème de tri. Dans ce chapitre, nous al-


lons parcourir divers algorithmes de tri et mesurer la complexité de
ceux-ci.

53
A LGORITHMES DE TRI

4.1 Introduction
Le tri d’un ensemble d’objets consiste à les ordonner en fonction de clés et d’une
relation d’ordre définie sur ces clés. Le tri est une opération classique et très fré-
quente. De nombreux algorithmes et méthodes utilisent des tris. Par exemple pour
l’algorithme de Kruskal qui calcule un arbre couvrant de poids minimum dans
un graphe, une approche classique consiste, dans un premier temps, à trier les
arêtes du graphe en fonction de leurs poids. Autre exemple, pour le problème
des éléphants, trouver la plus longue séquence d’éléphants pris dans un ensemble
donné, telle que les poids des éléphants dans la séquence soient croissants et que
leurs Q.I. soient décroissants, une approche classique consiste à considérer une
première suite contenant tous les éléphants ordonnés par poids croissants, une
deuxième suite avec les éléphants ordonnés par Q.I. décroissants, puis à calcu-
ler la plus longue sous-séquence commune à ces deux suites. Trier un ensemble
d’objets est aussi un problème simple, facile à décrire, et qui se prête à l’utilisation
de méthodes diverses et variées. Ceci explique l’intérêt qui lui est porté et le fait
qu’il est souvent présenté comme exemple pour les calculs de complexité. Dans
le cas général on s’intéresse à des tris en place, c’est-à-dire des tris qui n’utilisent
pas d’espace mémoire supplémentaire pour stocker les objets, et par comparai-
son, c’est-à-dire que le tri s’effectue en comparant les objets entre eux. Un tri qui
n’est pas par comparaison nécessite que les clés soient peu nombreuses, connues
à l’avance faciles à indexer. Un tri est stable s’il préserve l’ordre d’apparition des
objets en cas d’égalité des clés. Cette propriété est utile par exemple lorsqu’on trie
successivement sur plusieurs clés différentes. Si l’on veut ordonner les étudiants
par rapport à leur nom puis à leur moyenne générale, on veut que les étudiants qui
ont la même moyenne apparaissent dans l’ordre lexicographique de leurs noms.
Dans ce cours nous distinguerons les tris en O(n2 ) (tri à bulle, tri par insertion, tri
par sélection), les tris en O(n × logn) (tri par fusion, tri par tas et tri rapide, bien
que ce dernier n’ait pas cette complexité dans le pire des cas) et les autres (tris
spéciaux instables ou pas toujours applicables). Il convient aussi de distinguer le
coût théorique et l’efficacité en pratique : certains tris de même complexité ont des
performances très différentes dans la pratique. Le tri le plus utilisé et globalement
le plus rapide est le tri rapide (un bon nom : quicksort) ; nous l’étudierons en TD.
En général les objets à trier sont stockés dans des tableaux indexés, mais ce n’est
pas toujours le cas. Lorsque les objets sont stockés dans des listes chaînées, on
peut soit les recopier dans un tableau temporaire, soit utiliser un tri adapté comme
le tri par fusion (à condition de pouvoir couper une liste en deux).
Définissons formellement le problème T RI.

54
T RI
E NTRÉE : Un ensemble de n éléments (a1 , a2 , . . . , an ).
Q UESTION :Trouver une permutation σ de la suite de donnée en en-
trée de façon que a01 ≤ a02 ≤ . . . ≤ a0n .
Terminons par quelques définitions.
D ÉFINITION 4.1.1 : Un tri est dit stable s’il préserve l’ordonnancement initial
des éléments que l’ordre considère comme égaux. Pour définir cette notion, il
est nécessaire que la collection à trier soit ordonnancée (ce qui est souvent le
cas pour beaucoup de structures de données, par exemple pour les listes ou les
tableaux).

D ÉFINITION 4.1.2 : Un tri est dit en place s’il n’utilise qu’un nombre très li-
mité de variables et qu’il modifie directement la structure qu’il est en train de
trier. Ceci nécessite l’utilisation d’une structure de donnée adaptée (un tableau
par exemple). Cette propriété peut être très importante si on ne dispose pas de
beaucoup de mémoire. Toutefois, on ne déplace pas, en général, les données
elles-mêmes, on modifie seulement des références (ou pointeurs) vers ces der-
nières.
E XEMPLE
Définissons la relation d’ordre  définie sur les couples d’entiers par
(a, b)  (c, d) ssi a ≤ c, qui permet de trier deux couples selon leur pre-
mière valeur.
Soit L = [(4, 1); (3, 2); (3, 3); (5, 4)] une liste de couples d’entiers que l’on
souhaite trier selon la relation  préalablement définie.
Puisque (3, 2) et (3, 3) sont égaux pour la relation  appeler un algorithme
de tri avec L en entrée peut mener à deux solutions possibles :
— L1 = [(3, 2); (3, 3); (4, 1); (5, 4)]
— L2 = [(3, 3); (3, 2); (4, 1); (5, 4)]
L1 et L2 sont toutes deux triées selon la relation L1 =
[(3, 2); (3, 3); (4, 1); (5, 4)] mais seule L1 conserve l’ordre relatif. Dans
L2 (3, 3) apparaît avant (3, 2), ce qui indique un tri instable.

Parmi les algorithmes listés plus bas, les tris stables sont : le tri à bulles, le tri
par insertion et le tri fusion. Les autres algorithmes nécessitent O(n) mémoire
supplémentaire pour stocker l’ordre initial des éléments. Notons que cela dépend
de l’implémentation.

55
A LGORITHMES DE TRI

D ÉFINITION 4.1.3 : Un tri interne s’effectue entièrement en mémoire centrale


tandis qu’un tri externe utilise des fichiers sur une mémoire de masse pour trier
des volumes trop importants pour pouvoir tenir en mémoire centrale. Certains
types de tris, comme le tri fusion ou les tris par distribution, s’adaptent facilement
à l’utilisation de mémoire externe. D’autres algorithmes, à l’inverse, accèdent
aux données et ne se prêtent pas à cet usage : cela nécessiterait d’effectuer
constamment des lectures/écritures entre les mémoires principale et externe.

D ÉFINITION 4.1.4 : Certains algorithmes permettent d’exploiter les capacités


multitâches de la machine. Notons également que certains algorithmes, notam-
ment ceux qui fonctionnent par insertion, peuvent être lancés sans connaître
l’intégralité des données à trier ; on peut alors trier et produire les données à
trier en parallèle.

D ÉFINITION 4.1.5 : Les tris par comparaison sont basés sur le principe que
deux éléments quelconques sont comparables.
Dans la suite nous étudierons des algorithmes basé sur le principe de la comparai-
son des éléments sauf mention contraire.

4.2 Algorithmes de complexité quadratique


Dans cette partie nous allons étudier les premiers tris de complexité O(n2 ).

4.2.1 Tri par sélection

Le tri par sélection consiste simplement à sélectionner l’élément le plus petit de


la suite à trier, à l’enlever, et à répéter itérativement le processus tant qu’il reste
des éléments dans la suite. Au fur et à mesure les éléments enlevés sont stockés
dans une pile. Lorsque la suite à trier est stockée dans un tableau on s’arrange
pour représenter la pile dans le même tableau que la suite : la pile est représenté
au début du tableau, et à chaque fois qu’un élément est enlevé de la suite, il est
remplacé par le premier élément du tableau qui n’est pas dans la pile, dont il prend
la place. Lorsque le processus s’arrête, la pile contient tous les éléments de la suite
triés dans l’ordre croissant.
T HÉORÈME 4.2. 1 : L’algorithme 4.1 réalise le tri du tableau T par ordre crois-
sant et la complexité est O(n2 ).
Preuve
La preuve sera faite en travaux dirigés.

56
Algorithme 4.1 Tri par sélection T RI_S ÉLECTION(T, n).
for i = 0 à n − 1 do
M IN:= i
for j := i + 1 à n do
if T [j] ≤ T [M IN] then
M IN:= j ;
end if
end for
if M IN 6= i then
E CHANGER(T [i], T [MIN]) ;
end if
end for

4.2.2 Tri par insertion séquentielle

Le tri par insertion d’un tableau T [1 . . . n] de n éléments consiste à réaliser un tri


préliminaire du tableau T [1 . . . n − 1], puis à insérer le dernier élément du tableau
à sa place dans le tableau, en décalant d’une position vers la droite les éléments
plus grands que lui.

Algorithme 4.2 Tri par insertion séquentielle T RI_ I NSERTION(T, n)


if n > 0 then
T RI_ I NSERTION(T, n − 1)
k := n
while (k > 1) et (T [k − 1] > T [k]) do
E CHANGER (T [k − 1], T [k])
k := k − 1
end while
end if

57
A LGORITHMES DE TRI

T HÉORÈME 4.2. 2 : L’algorithme 4.2 réalise le tri du tableau T par ordre crois-
sant et la complexité est de O(n2 ).
Preuve
On démontre tout d’abord la terminaison par récurrence sur n. De façon évidente,
l’appel de la procédure T RI_I NSERTION avec un paramètre 0 se termine immé-
diatement. Si on suppose que l’appel de T RI_I NSERTION avec un paramètre n se
termine, l’appel de T RI_I NSERTION avec le paramètre n + 1 appelle la procédure
T RI_I NSERTION avec le paramètre n, qui par hypothèse se termine. Elle parcourt
alors tous les éléments du tableau en partant du dernier (correspondant à l’indice
k = n + 1) jusqu’à tomber sur un indice k tel que k = 1 ou T [k − 1] ≤ T [k]. Or
à chaque itération de cette boucle, l’indice k est décrémenté, ce qui prouve que
l’appel de la procédure pour le paramètre n + 1 se termine. Donc, quelle que soit
la valeur du paramètre n, l’appel de la fonction T RI_I NSERTION se termine.
On montre également par récurrence sur n la validité de l’algorithme « l’appel
de la fonction T RI_I NSERTION avec le paramètre n réalise le tri des n premières
valeurs du tableau ».
— Si n = 1, le tableau ne contient qu’une seule valeur et est donc trié, et l’ap-
pel de la procédure T RI_I NSERTION ne réalise aucune modification dans le
tableau.
— On suppose que l’appel de la procédure T RI_I NSERTION avec un paramètre
n réalise le tri des n premières valeurs du tableau. On considère alors l’exé-
cution de la procédure T RI_I NSERTION avec le paramètre n + 1. Celle-ci
commence par un appel récursif avec le paramètre n, qui par hypothèse de
récurrence trie les n premières valeurs du tableau par ordre croissant.
La boucle tant que compare alors deux à deux tous les éléments du tableau
T , en commençant par la paire (T [n], T [n − 1]), et procède à l’échange
des deux éléments considérés, jusqu’à ce que la paire (T [k], T [k − 1]) soit
telle que T [k − 1] ≤ T [k]. A l’issue de cette boucle, le dernier élément
du tableau (T [n + 1]) a donc été « remonté » à sa place, et le sous-tableau
(T [1 . . . n + 1]) se retrouve donc trié par ordre croissant. Dès l’instant où la
procédure se termine (ce qui a été montré), elle effectue donc bien le tri des
valeurs du tableau T par ordre croissant.
Le tri effectue n − 1 insertions, qui correspondent aux appels de la fonction de 2 à
n. A la ième boucle,P dans le pire des cas, l’algorithme effectue i − 1 échanges. Le
coût du tri est donc ni=2 (i − 1) = O(n2 ). Remarquons que dans le meilleur des
cas le tri par insertion requiert seulement O(n) traitements. C’est le cas lorsque
l’élément à insérer reste à sa place, donc quand la suite est déjà triée. Lorsque la

58
suite est stockée dans une liste chaînée, on insère en tête de liste donc le meilleur
cas correspond à une liste triée à l’envers.


4.3 Algorithme de complexité O(n log n)


Le T RI _F USION (« merge sort » en anglais) implémente une approche de type
diviser pour régner très simple : la suite à trier est tout d’abord scindée en deux
suites de longueurs égales à un élément près. Ces deux suites sont ensuite triées
séparément avant d’être fusionnées. L’efficacité du tri par fusion vient de l’effica-
cité de la fusion : le principe consiste à parcourir simultanément les deux suites
triées dans l’ordre croissant de leur éléments, en extrayant chaque fois l’élément
le plus petit. Le tri par fusion est bien adapté aux listes chaînées : pour scinder
la liste il suffit de la parcourir en liant les éléments de rangs pairs d’un coté et
les éléments de rangs impairs de l’autre. La fusion de deux listes chaînées se fait
facilement. Si la suite à trier est stockée dans un tableau, il est nécessaire de faire
appel à un tableau annexe lors de la fusion, sous peine d’avoir une complexité en
O(n2 ).

Algorithme 4.3 Tri par fusion T RI _F USION(T ) ;


if |T | ≥ 1 then
Scinder la suite T en deux sous-tableaux T1 et T2 de longueurs égales (en
temps constant ou temps linéaire) ;
T1 := T RI _F USION(T1 ) ;
T2 :=T RI _F USION(T2 ) ;
T :=FUSION(T1 , T2 ) ;
end if

T HÉORÈME 4.3. 1 : L’algorithme 4.3 réalise le tri du tableau T par ordre crois-
sant et la complexité est de O(n log n).
Preuve
La preuve de la validité sera faite en TD.
Dans le cas général, on peut évaluer à O(n) le coût de la scission du tableau T et à
O(n) le coût de la fusion des sous-tableaux T1 et T2 . En supposant pour simplifier
n pair, l’équation récursive du tri par fusion est donc :

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

59
A LGORITHMES DE TRI

On en déduit que le tri par fusion est en O(n log n). On le vérifie en cumulant
les nombres de comparaisons effectués à chaque niveau de l’arbre qui représente
l’exécution de la fonction (voir figure ci-dessous) : chaque noeud correspond à
un appel de la fonction, ses fils correspondent aux deux appels récursifs, et son
étiquette indique la longueur de la suite. La hauteur de l’arbre est donc log2 n et à
chaque niveau le cumul des traitements locaux (scission et fusion) est O(n) et on
déduit un coût total de O(n) × log2 n = O(n log n).


4.4 Complexité optimale d’un algorithme de tri par


comparaison
L’arbre de décision d’un tri par comparaison représente le comportement du tri
dans toutes les configurations possibles, si les valeurs sont toutes différentes. Les
configurations correspondent à toutes les permutations des objets à trier, en sup-
posant qu’ils soient tous comparables et de clés différentes. S’il y a n objets à
trier, il y a donc n! configurations possibles. On retrouve toutes ces configurations
sur les feuilles de l’arbre, puisque chaque permutation des valeurs correspond à
un comportement de tri différent.
Chaque noeud de l’arbre correspond à une comparaison entre deux éléments et a
deux fils, correspondant aux deux ordres possibles entre ces deux éléments.
T HÉORÈME 4.4. 1 :
Tout arbre de décision qui trie n éléments a pour hauteur Ω(n log n).
Preuve
Prenons un arbre de décision de hauteur h qui trie n éléments. Puisqu’il existe
n! permutations de n éléments, chaque permutation représentant un ordre de tri
distinct, l’arbre doit avoir au moins n! feuilles. Sachant qu’un arbre binaire de
hauteur h ne comporte pas plus de 2h feuilles, on a n! ≤ 2h , ce qui implique ,
en prenant les logarithmes, h ≥ log2 (n!). En utilisant la formule de Stirling n! >
( ne )n avec e = 2, 71828 . . ., on a h ≥ log( ne )n = n log n − n log e = P
Ω(n log n).
De
Pnmanière alternative, un autre calcul nous conduit à log(n!) = ni=1 log i ≥
n n
i=dn/2e log i ≥ 2 log 2 . 

60
a b c

a≤b a>b

a b c b a c

b≤c b>c a≤c a>c

a b c a c b b a c b c a

a≤c a>c b≤c b>c

a c b c a b b c a c b a

F IGURE 4.1 – Arbre de décision pour le tri de trois éléments a, b, c.

4.5 Algorithmes de complexité linéaire


4.5.1 Tri par dénombrement

Le tri par dénombrement (counting sort) est un tri sans comparaisons qui est
stable, c’est- à -dire qu’il respecte l’ordre d’apparition des éléments dont les clés
sont égales. Un tri sans comparaison suppose que l’on sait indexer les éléments en
fonction de leur clé. Par exemple, si les clés des éléments à trier sont des valeurs
entières comprises entre 0 et 2, on pourra parcourir les éléments et les répartir
en fonction de leur clé sans les comparer. Le tri par dénombrement utilise cette
propriété pour tout d’abord recenser les éléments pour chaque valeur possible des
clés. Ce comptage préliminaire permet de connaître, pour chaque clé c, la position
finale du premier élément de clé c qui apparaît dans la suite à trier. Sur l’exemple
ci-dessous, on a recensé dans le tableau T , 3 éléments avec la clé 0, 4 éléments
avec la clé 1 et 3 éléments avec la clé 2. On en déduit que le premier élément avec
la clé 0 devra être placé à la position 0, le premier élément avec la clé 1 devra être
placé à la position 3, et le premier élément avec la clé 2 devra être placé à la posi-
tion 7. Il suffit ensuite de parcourir une deuxième fois les éléments à trier et de les
placer au fur et à mesure dans un tableau annexe, en n’oubliant pas, chaque fois
qu’un élément de clé c est placé, d’incrémenter la position de l’objet suivant de

61
A LGORITHMES DE TRI

clé c. De cette façon les éléments qui ont la même clé apparaissent nécessairement
dans l’ordre de leur apparition dans le tableau initial.
Le T RI_D ÉNOMBREMENT suppose que chacun des n éléments d’entrée est un
entier de l’intervalle 1 à k donné. Lorsque k = O(n), le tri s’exécute en O(n).
Le principe du T RI_D ÉNOMBREMENT est de déterminer, pour chaque élément x
de l’entrée, le nombre d’éléments inférieurs à x. Cette information peut servir à
placer l’élément x directement à sa position dans le tableau de sortie. Par exemple,
s’il existe 17 éléments inférieurs à x, alors x e trouvera en sortie à la potion 18. Ce
schéma doit prendre en compte la situation dans laquelle plusieurs éléments ont
la même valeur, puisqu’on ne veut pas tous les placer à la même position.
Dans le code du T RI_D ÉNOMBREMENT, on suppose que l’entrée est un tableau
A[1 . . . n] et donc que longueur[A] = n. Nous avons besoin de deux autres ta-
bleaux : le tableau B[1 . . . n] contient la sortie triée et le tableau C[1 . . . k] sert
d’espace de stockage temporaire.

Algorithme 4.4 T RI_D ÉNOMBREMENT (A, B, k).


for i = 1 à k do
C[i] := 0 ; {Initialisation}
end for
for i = 1 à n do
C[A[i]] := C[A[i]] + 1 ; {C[i] contient à présent le nombre d’éléments égaux
à i}
end for
for i = 2 à k do
C[i] := C[i − 1] + C[i] ;
end for{C[i] contient à présent le nombre d’éléments inférieurs ou égaux à i}
for i = n à 1 do
B[C[A[i]]] := A[i];
C[A[i]] := C[A[i]] − 1 ;
end for{le F OR est par pas de -1 afin de permettre la stabilité du tri}
R ETOURNER B ;

Le tri est illustré par la figure 4.2.


E XEMPLE
Le tableau A et le tableau auxiliaire C après la ligne 4 (Figure 4.2 a)). Le
tableau C après la ligne 7 (Figure 4.2 b)). Le tableau en sortie B et le tableau
auxiliaire C après une, deux et trois itérations de la boucle des lignes 9–11
(Figure 4.2 c) − −e)). Le tableau résultant final B (Figure 4.2 f )).

62
1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
A 2 5 3 0 2 3 0 3 B 3

0 1 2 3 4 5 0 1 2 3 4 5 0 1 2 3 4 5
C 2 0 2 3 0 1 C 2 2 4 7 7 8 C 2 2 4 6 7

a) b) c)

1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
B B 0 3 3

0 1 2 3 4 5 0 1 2 3 4 5
C 1 2 4 6 7 8 C 1 2 4 5 7 8

d) e)
1 2 3 4 5 6 7 8
B 0 0 2 2 3 3 3 5

f)

F IGURE 4.2 – Fonctionnement de T RI -D ÉNOMBREMENT sur un tableau A[1..8], où


chaque élément de A est un entier positif pas plus grand que k = 5

63
A LGORITHMES DE TRI

T HÉORÈME 4.5. 1 : L’algorithme 4.4 réalise le tri par ordre croissant en temps
linéaire.
Preuve
La complexité en temps linéaire est évidente. La validité de l’algorithme est laissé
en exercice, vous devez pour chaque fonction trouver un invariant.

Le T RI_D ÉNOMBREMENT possède une propriété intéressante, à savoir la stabi-
lité : les nombres égaux apparaissent dans le tableau de sortie avec l’ordre qu’ils
avaient dans le tableau d’entrée. Autrement dit, une égalité éventuelle entre deux
nombres est arbitrée par la règle selon laquelle quand un nombre apparaît en pre-
mier dans le tableau d’entrée, il apparaît aussi en premier dans le tableau de
sortie. En principe, la stabilité n’est importante que si l’élément trié est accom-
pagné de données satellites. Mais la stabilité présente aussi un autre intérêt : le
T RI_D ÉNOMBREMENT sert souvent de sous-routine au T RI_BASE. Comme vous
le verrez à la section suivante, la stabilité du T RI_D ÉNOMBREMENT est un élé-
ment clé pour le bon fonctionnement du T RI_BASE.

64
4.5.2 Tri par paquets

Le tri par paquets s’exécute en temps linaire quand l’entrée suit une distribution
uniforme. A l’instar du tri par dénombrement, le tri par paquets est rapide car il fait
des hypothèses sur l’entrée. Là où le tri par dénombrement suppose que l’entrée
se compose d’entiers appartenant à un petit intervalle, le tri par paquets suppose
que l’entrée a été générée par un processus aléatoire qui distribue les éléments de
manière uniforme sur l’intervalle [0, 1[. L’idée sous-jacente au tri par paquets est
la suivante : on divise l’intervalle [0, 1[ en n sous-intervalles de même taille, ou
paquets, puis on distribue les n nombres de l’entrée dans les différents paquets.
Comme les entrées sont distribués de manière uniforme sur [0, 1[, on n’escompte
pas qu’un paquet contienne beaucoup de nombres. Pour produire le résultat, on se
contente de trier les nombres de chaque paquet, puis de parcourir tous les paquets,
dans l’ordre, en énumérant les éléments de chacun. Notre code duT RI_PAQUETS
suppose que l’entrée est un tableau A à n éléments et que chaque élément A[i] du
tableau satisfait à 0 ≤ A[i] < 1. Le code exige un tableau auxiliaire B[0 . . . n − 1]
de listes chaînes (paquets) et suppose qu’il existe un mécanisme pour la gestion
de ce genre de listes.

Algorithme 4.5 T RI_PAQUETS (A).


n :=LONGUEUR(A)
for i = 1 à n do
Insérer A[i] dans liste B[bnA[i]c]
end for
for i = 1 à n − 1 do
Trier la liste B[i] à l’aide du T RI_I NSERTION
end for
Concaténer les listes B[0], B[1], . . . , B[n − 1] dans l’ordre.
R ETOURNER B ;

T HÉORÈME 4.5. 2 : L’algorithme 4.5 réalise le tri par ordre croissant en temps
linéaire.
Preuve
Pour vérifier que cet algorithme est correct, considérons deux éléments A[i] et
A[j]. On peut supposer, sans nuire à la généralité, que A[i] ≤ A[j]. Comme
bnA[i]c ≤ bnA[j]c, l’élément A[i] est placé soit dans le même paquet que A[j],
soit dans un paquet d’indice inférieur. Si A[i] et A[j] sont placés dans le même pa-

65
A LGORITHMES DE TRI

quet, alors la boucle pour des lignes 4–5 les place dans le bon ordre. Si A[i] et A[j]
sont placés dans des paquets différents, alors c’est la ligne 6 qui les range dans le
bon ordre. Par conséquent, le T RI_PAQUETS fonctionne correctement. Pour ana-
lyser le temps d’exécution, observons que toutes les lignes sauf la ligne 5 prennent
un temps O(n) dans le cas le plus défavorable. Reste à faire le bilan du temps total
consommé par les n appels au tri par insertion en ligne 5. Pour analyser le coût des
appels au tri par insertion, notons ni la variable aléatoire qui désigne le nombre
d’éléments placés dans le paquet B[i]. Comme le tri par insertion tourne en temps
quadratique, le temps d’exécution du T RI_PAQUETS est
n−1
X
T (n) = θ(n) + O(n2i )
i=0

Si l’on prend les espérances des deux côtés et que l’on utilise la linéarité de l’es-
pérance, on a

n−1
X
E[T (n)] = E[θ(n) + O(n2i )]
i=0
n−1
X
= θ(n) + E[O(n2i )] d’après la linéarité de l’espérance
i=0
n−1
X
= θ(n) + O(E[n2i ]) à prouver
i=0

Nous affirmons que

E[n2i ] = 2 − 1/n, ∀i ∈ {0, . . . , n − 1}

En effet, il faut déterminer la distribution de chaque variable aléatoire ni . Nous


avons n éléments et n paquets. La probabilité pour qu’un élément donné se re-
trouve dans le paquet B[i] est 1/n, puisque chaque paquet représente 1/n de l’in-
tervalle [0, 1[ : on a n ballons (éléments) et n paniers (paquets), et chaque ballon
est lancé indépendamment avec une probabilité p = 1/n de tomber dans un panier
particulier. La probabilité pour que ni = k suit la loi binomiale b(n, k, p), qui a
pour moyenne E[ni ] = np = 1 et pour variance V ar[ni ] = np(1 − p) = 1 − 1/n.
On a :

66
E[n2i ] = V ar[ni ] + E 2 [ni ]
= 1 − 1/n + 12
= 2 − 1/n
= θ(1)

On obtient donc un algorithme en O(n).




4.5.3 Tri par base

Le T RI_D ÉNOMBREMENT est difficilement applicable lorsque les valeurs que


peuvent prendre les clés sont très nombreuses. Le principe du T RI_BASE (radix
sort) consiste, dans ce type de cas, à fractionner les clés (usuellement, de gauche à
droite) puis à effectuer un T RI_D ÉNOMBREMENT successivement sur chacun des
fragments des clés. Si on considère les fragments dans le bon ordre (en commen-
çant par les fragments « de poids faible », usuellement les plus à droite), après
la dernière passe, l’ordre des éléments respecte l’ordre lexicographique et donc la
suite est triée. Considérons l’exemple du tableau 4.1 dans lequel les clés sont des
nombres entiers à au plus trois chiffres. Le fractionnement consiste simplement à
prendre chacun des chiffres de l’écriture décimale des clés. La colonne de gauche
contient la suite des valeurs à trier, la colonne suivante contient ces mêmes valeurs
après les avoir trié par rapport au chiffre des unités,. . . Dans la dernière colonne
les valeurs sont effectivement triées. Du fait que le T RI_D ÉNOMBREMENT est
stable, si des valeurs ont le même chiffre des centaines, alors elles apparaitront
dans l’ordre croissant de leurs chiffres des dizaines, et si certaines ont le même
chiffre des dizaines alors elles apparaitront dans l’ordre croissant des chiffres des
unités.
La procédure suivante suppose que chaque élément du tableau à n éléments A
possède c chiffres, le chiffre 1 étant le chiffre d’ordre inférieur et le chiffre c étant
le chiffre d’ordre supérieur.

67
A LGORITHMES DE TRI

Algorithme 4.6 T RI_BASE(A, c).


for i = 1 à c do
Employer un tri stable pour trier le tableau A selon le chiffre i.
end for

536 592 427 167


893 462 536 197
427 893 853 427
167 853 462 462
853 536 167 536
592 427 592 592
197 167 893 853
462 197 197 893
TABLE 4.1 – Illustration du tri T RI_BASE avec k = 10, d = 3 et n = 8.

68
Nom Pire cas Cas moyen Stabilité #T Pire cas #T moy
n(n−1) n(n−1)
Tri sélection 2 2
Non 3(n − 1) 3(n − 1)
n(n−1) n(n−1) 3n(n−1) 3n(n−1)
Tri bulles 2 2
Non 2 4
n(n+3) n(n+1) n(n+3) n(n+7)
Insertion séq. 4
− 1 2
− 1 Oui 2
− 2 4
− 2
Insertion dicho. θ(n log n) θ(n log n) Oui ??? ???
Tri fusion θ(n log n) θ(n log n) Oui ??? ???

TABLE 4.2 – Synthèse des résultats pour les tris simples. #T indique le nombre
de transferts d’éléments.

T HÉORÈME 4.5. 3 : Étant donnés n nombres de c chiffres dans lesquels


chaque chiffre peut prendre k valeurs possibles, T RI_B ASE trie correctement
ces nombres en un temps θ(c(n + k)).
Preuve
On établit la validité du T RI_BASE en raisonnant par récurrence sur la colonne en
cours de tri.
Supposons que l’on ait n valeurs dont les clés sont fractionnées en c fragments
avec k valeurs possibles pour chaque fragment. Le coût du T RI_BASE est alors
O(c×n+c×k) puisque l’on va effectuer c tris par dénombrements sur n éléments
avec des clés qui auront k valeurs possibles.
Si k = O(n) on peut dire que le T RI_BASE est linéaire. Dans la pratique, sur des
entiers codés sur 4 octets que l’on fragmente en 4, le T RI_BASE est aussi rapide
que le Tri rapide. 

4.6 Conclusion
Dans la suite de votre cursus vous découvrirez des algorithmes de tris plus ou
moins sophistiqués basés sur des stratégies variées. Le tableau 4.2 donne les com-
plexités et le nombre de transferts.

69
A LGORITHMES DE TRI

70
Bibliographie

[1] B. Baynat, P. Chrétienne, C. Hanen S. Kedad-Sidhoum, A. Munier, and C. Pi-


couleau. Exercices et problèmes d’algorithmique. Dunod, 2010.
[2] C.Froidevaux, M.-C. Gaudel, and M. Soria. Types de données et algorithmes.
Edisciences, 1993.
[3] T. H. Cormen, C. E. Leiserson, R. L. Rivest, and C. Stein. Introduction to
Algorithms, 3rd Edition. MIT Press, 2009.
[4] K. Mehlhorn and P. Sanders. Algorithms and Data Structures : The Basic
Toolbox. Springer, 2008.

71

Vous aimerez peut-être aussi