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
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
iii
TABLE DES M ATIÈRES
iv
Liste des Algorithmes
v
TABLE DES M ATIÈRES
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é
1
A LGORITHMES ITÉRATIFS VERSUS RÉCURSIFS
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
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.
5
A LGORITHMES ITÉRATIFS VERSUS RÉCURSIFS
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 :
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
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.
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.
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
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 :
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
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
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
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é
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.
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 :
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.
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
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
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.
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
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.
25
C OMPLEXITÉ ALGORITHMIQUE
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.
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
Pn
Le nombre d’étoiles affiché est i=1 1 = n,
2. Considérons l’algorithme 2.4
Pn Pn
Le nombre d’étoiles affiché est i=1 ( j=1 1) = n2 .
3. Considérons l’algorithme 2.4
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.
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.
29
C OMPLEXITÉ ALGORITHMIQUE
t0 = t1 = 0
tn = 1 + tn1
tn−1 = 1 + tn−2
..
.
t2 = 1 + t1
Xn
tn = 1 + t1
i=2
tn = n − 1
vk = 1 + vk−1
30
tn = 2 + 2tn−1
= 2 + 4 + 4tn−2
= 2 + 4 + 8 + 8tn−3
= ...
n−1
X
= 2i + t1 = 2n − 2
i=1
31
C OMPLEXITÉ ALGORITHMIQUE
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
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
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é
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
— Les opérations D ÉPILER et SOMMET ne sont définies que si la pile n’est pas
vide :
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
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
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
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.
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 /
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.
É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).
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.
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.
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.
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é
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.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.
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
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.
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 :
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).
60
a b c
a≤b a>b
a b c b a c
a b c a c b b a c b c a
a c b c a b b c a c b a
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.
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)
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.
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
66
E[n2i ] = V ar[ni ] + E 2 [ni ]
= 1 − 1/n + 12
= 2 − 1/n
= θ(1)
67
A LGORITHMES DE TRI
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.
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
71