Explorer les Livres électroniques
Catégories
Explorer les Livres audio
Catégories
Explorer les Magazines
Catégories
Explorer les Documents
Catégories
et Algorithmes
NOTES DE COURS
partie 1
Michel Lemaître
ONERA, Centre de Toulouse
Le matériel associé au cours, y compris la dernière version de ces notes ainsi que les programmes en Java, est disponible
à partir de l’adresse http://www.cert.fr/dcsd/cd/MEMBRES/lemaitre/Enseignement
Toutes les remarques concernant ces notes seront appréciées. Elles doivent être adressées à Michel.Lemaitre@cert.fr.
Table des matières
1 Introduction 6
1.1 De quoi parle ce cours ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.2 Quels sont les buts du cours ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3 Quel est le contenu du cours ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2 Récursivité 10
2.1 Qu’est-ce que la récursivité ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2 Critères d’un algorithme récursif correct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.3 Récursion et itération . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.4 Récursion et efficacité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.5 Récursions célèbres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.6 Programme Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
5 Tableau 29
5.1 Implémentations de la collection, de l’ensemble et du dictionnaire par un tableau . . . . . . . . . . . . . 29
5.2 Implémentations de la pile et de la file par un tableau . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
6 Liste chaînée 31
6.1 La liste chaînée la plus simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
6.2 Autres listes chaînées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
6.3 Intérêts et inconvénients de la liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3
4 TABLE DES MATIÈRES
6.4 Implémentations de la pile, de la file et de la file de priorité par une liste chaînée . . . . . . . . . . . . . . 32
6.5 Programme Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
7 Table de hachage 36
7.1 Présentation générale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
7.2 Fonctionnement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
7.3 Fonctions de hachage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
7.4 Analyse du temps de l’opération RECHERCHE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
11 B-Arbre 54
11.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
11.2 Propriété essentielle du B-arbre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
11.3 Recherche dans un B-arbre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
11.4 Insertion dans un B-arbre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
11.5 Suppression dans un B-arbre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
12 Tas 58
12.1 Définition du tas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
12.2 Restauration de la contrainte d’ordre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
12.3 Construction d’un tas à partir d’un tableau quelconque . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
12.4 Maximum et extraction du maximum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
12.5 Insertion dans un tas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
12.6 Le tri du tas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
12.7 Programme Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
TABLE DES MATIÈRES 5
13 Tris 67
13.1 Le problème . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
13.2 Borne inférieure de complexité en temps
des tris par comparaison . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
13.3 Tris simples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
13.4 Tri par tas (heapsort ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
13.5 Tri par fusion (mergesort ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
13.6 Tri rapide (quicksort ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
13.7 Tri par dénombrement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
13.8 Résumé des propriétés des tris . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
13.9 Programme Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
15 Références 87
15.1 Livres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
15.2 Toile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
Chapitre 1
Introduction
Problème
En algorithmique, nous nommerons problème une question générale paramétrée. La description d’un problème doit com-
porter :
– les données du problème : une structure faisant apparaître des paramètres (des variables libres)
– une caractérisation de la solution cherchée, sous forme d’une contrainte ou d’une propriété à satisfaire.
Cela va s’éclaircir sur un exemple : la résolution d’une équation du second degré.
– données : l’équation du second degré ax2 + bx + c = 0, avec les paramètres a, b, c
– solution : une valeur de x satisfaisant l’équation.
La caractérisation de la solution cherchée peut revêtir des formes différentes, pour la même structure. Des formes clas-
siques sont :
– existe-t-il x tel que . . .. La solution est «oui» ou «non» (problème d’existence).
– trouver un x, ou tous les x satisfaisant telle contrainte ...
– trouver x, ou tous les x minimisant telle fonction (problème d’optimisation).
Autre exemple, le problème du tri :
– données : un sous-ensemble de n objets a1 , a2 , . . . , an à trier, tirés d’un ensemble muni d’une relation d’ordre total notée
; paramètres : le sous-ensemble à trier.
6
1.1. DE QUOI PARLE CE COURS ? 7
– solution cherchée : trouver une permutation σ : {1, . . . , n} → {1, . . . , n} telle que ∀i, j : i < j =⇒ aσ(i) aσ( j) .
Encore un exemple : des problèmes de plus court chemin dans un graphe (décrit de façon plus informelle) :
– données : une carte C avec des villes et des segments de routes de longueurs données reliant certaines de ces villes ;
deux villes particulières a et b ; paramètres : C, a, b et k un nombre réel.
– plusieurs types de solutions peuvent être recherchées, donnant lieu à des problèmes différents ; ce peut être :
– existe-t-il dans C un chemin de a vers b de longueur inférieure à k ?
– trouver un chemin dans C de a vers b de longueur inférieure à k.
– trouver un chemin dans C de a vers b de longueur minimale.
On distinguera soigneusement «problème» et «instance d’un problème». Une instance d’un problème est obtenue en
remplaçant les paramètres du problème par des constantes. Exemple d’instance du problème du tri : trier la séquence de
nombres (2, 8, −178, 87).
Algorithme
Définition de l’Encyclopedia Universalis : « un algorithme est la spécification d’un schéma de calcul, sous forme d’une
suite d’opérations élémentaires obéissant à un enchaînement déterminé ».
Remarquer que l’on ne parle pas ici d’ordinateur.
Autre définition : un algorithme est une procédure générale permettant de résoudre un problème : appliqué à n’importe
quelle instance du problème, il produit sûrement une solution en un nombre de pas fini.
Dire qu’un algorithme résout un problème, c’est dire que le problème est une spécification pour l’algorithme.
Exemples d’algorithmes :
– les opérations arithmétiques manuelles.
– l’algorithme d’Euclide : pour calculer le pgcd de deux nombres entiers, on calcule la suite des restes des divisions
entières successives ; le dernier reste non nul est le pgcd.
– l’algorithme de Dijkstra, pour résoudre un problème de plus court chemin.
– la recherche d’un mot dans un dictionnaire.
– une recette de cuisine.
Un algorithme s’exprime de façon plus ou moins formelle : langage naturel, ou pseudo-code plus ou moins précis, ou
langage de programmation de haut niveau. On peut se contenter des grandes lignes, en négligeant les problèmes d’implé-
mentation fine, de façon à privilégier la lisibilité et la compréhension. En général, la description d’un algorithme tient en
quelques lignes, et dépasse rarement une page. On néglige à ce stade les problèmes de génie logiciel tels que protection,
visibilité, modularité, traitement des erreurs . . . : ces aspects importants mais hors du champ de l’algorithmique sont traités
en programmation.
Structures de Données
L’essentiel des traitements informatiques consiste à manipuler des données. Il faut nécessairement organiser et structurer
les données d’une manière appropriée aux traitements envisagés.
Ce problème d’organisation des données est bien antérieur à l’informatique. Par exemple, on numérote les pages d’un
livre ; on construit des index des termes importants ; dans un dictionnaire, les mots sont rangés dans l’ordre alphabétique,
etc ... L’informatique ne fait que pousser à l’extrême cette démarche organisatrice.
Nous distinguerons soigneusement structure de données abstraite et structure de données concrète. La distinction est
de même nature qu’entre problème et algorithme. Une structure de données abstraite spécifie un ensemble de services
algorithmiques associés à la structure, permettant la création, la consultation et la modification des données. Une structure
de donnée concrète décrit et regroupe les algorithmes promis par la structure abstraite correspondante.
Donnons quelques exemples de structures de données abstraites. La plus simple est la collection1 : elle regroupe simple-
ment une collection d’objets. Une structure plus élaborée est celle de dictionnaire, qui regroupe un ensemble d’objets
associés chacun à une clé, et permet de retrouver facilement un objet à partir de sa clé. Le tableau et la liste chaînée sont
deux exemples de structures de données concrètes.
Programme
Pour exécuter un algorithme sur un ordinateur, on le traduit — on dit : on le réalise ou on l’implémente — en un
programme, exprimé dans un langage de programmation. On pourrait dire aussi que l’algorithme spécifie le programme
1 On dit également : multi-ensemble.
8 CHAPITRE 1. INTRODUCTION
qui l’implémente.
Alors qu’un algorithme peut rester plus ou moins formel, un programme est on ne peut plus formel (c’est à dire qu’il est
soumis à des règles syntaxiques et sémantiques rigoureuses).
Pour être pleinement et facilement utilisables, les algorithmes, et spécialement ceux des structures de données, doivent
être réalisés sous forme de bibliothèque logicielle. Lorsque l’on réalise une bibliothèque, on doit obligatoirement prendre
en compte tous les aspects de génie logiciel qui facilitent la réutilisation, par exemple en s’appuyant sur les concepts de
la programmation orientée objet.
Le tableau qui suit résume les relations entre les différentes notions que nous venons d’introduire. La flêche descendante
indique la relation «spécifie». La flêche montante indique la relation «résout» ou «implémente».
problème structure de données abstraite
↓ ↑ ↓ ↑
algorithme structure de données concrète
↓ ↑ ↓ ↑
programme bibliothèque logicielle
Il existe des bibliothèques toutes faites et très bien faites de structures de données et d’algorithmes. Citons au moins deux
bibliothèques : le collections framework de la plate-forme Java4 , et la bibliothèque LEDA pour C++5 . Pour bien les utiliser,
une formation de base en algorithmique et structures de données est indispensable, car il faut connaître les propriétés des
différentes structures pour choisir celles qui sont les mieux adaptées à son problème et les utiliser convenablement, mais
surtout pour ne pas réinventer la poudre. C’est l’objet de ce cours.
Ce cours est un cours d’algorithmique, et non un cours de programmation. Cependant nous exprimerons la plupart des
algorithmes étudiés sous forme de petits programmes Java indépendants. On aura ainsi le loisir de les exécuter et d’appré-
hender concrètement leur comportement. Le choix de Java est motivé par sa simplicité6 , sa portabilité, et par le fait qu’il
est de plus en plus connu et apprécié. De plus, il existe, associée au langage, une excellente bibliothèque implémentant
à peu près toutes les structures de données que nous allons étudier, nommée collections framework. Elle est présentée au
chapitre 14.
Un graphe est une structure mathématique extrêmement utile en informatique pour la représentation et la résolution de
nombreux problèmes, et pour laquelle il existe toute une algorithmique spécifique. L’étude de cette algorithmique fera
l’objet de la seconde partie du cours.
Le cours de Programmation Fonctionnelle présente un langage et une philosophie de programmation de haut niveau,
facilitant l’écriture, la lisibilité et la maintenance des programmes, ainsi plus proches des algorithmes qu’ils implémentent.
7 Techniquement : temps logarithmique, linéaire ou polynomial. Ces termes seront expliqués au chapitre 3.
Chapitre 2
Récursivité
La récursivité est un paradigme de calcul semblable au raisonnement par induction. Bien utilisée, la récursion permet
souvent une expression simple et élégante des algorithmes.
n! = 1, si n = 0
n! = n × (n − 1)! , si n > 0
10
2.3. RÉCURSION ET ITÉRATION 11
J
Exercice 2.1 Écrire un algorithme récursif calculant le pgcd de deux nombres par soustractions successives.
J
Exercice 2.2 Écrire l’algorithme d’Euclide.
4 f o r ( i n t i = 1; i <= n ; i += 1) {
5 resultat *= i;
6 }
7 r e t u r n resultat ;
8 }
Théoriquement, la récursivité n’est pas indispensable : on peut toujours transformer un algorithme récursif en un algo-
rithme itératif (c’est-à-dire avec des boucles) équivalent, éxécutant la même séquence d’opérations. Cependant, il est
beaucoup de cas pour lesquels l’expression récursive est simple et naturelle. Certains algorithmes récursifs n’ont pas
d’équivalent itératif évident, comme le tri par fusion (voir page 10), ou l’algorithme résolvant le problème des tours de
Hanoi (voir page 12).
Dans le cas général, la traduction itérative nécessite une pile au moins, pour simuler la gestion du passage des arguments
et des retours.
5 s t a t i c i n t fib_eff_aux ( i n t n , i n t u , i n t v ) {
6 r e t u r n ( ( n == 1)
7 ? v
8 : fib_eff_aux (n - 1, v , u + v ) );
9 }
On montre facilement que le nombre d’additions effectuées par ce dernier algorithme est n − 1 (fonction linéaire).
La fonction fib_eff_aux est dite récursive terminale : le résultat, dans le cas général (c’est-à-dire hors du cas d’arrêt)
est directement le résultat de l’appel récursif. La récursivité terminale confère aux algorithmes une propriété intéressante :
on peut toujours les convertir en algorithme itératif (avec boucle), sans besoin de pile auxiliaire.
12 CHAPITRE 2. RÉCURSIVITÉ
6 w h i l e ( n > 1) {
7 n -= 1;
8 vieux_u = u;
9 u = v;
10 v += vieux_u ;
11 }
12 r e t u r n v;
13 }
J
Exercice 2.4 (difficile) : inventer un algorithme calculant f ib(n) en un nombre A(n) d’additions croissant de façon
logarithmique.
On trouvera dans le programme qui commence page 13 une expression en Java de cet algorithme.
La fonction d’Ackermann
Ack(0, n) = n + 1
Ack(m, 0) = Ack(m − 1, 1) pour m > 0
Ack(m, n) = Ack(m − 1, Ack(m, n − 1)) pour m, n > 0
J
Exercice 2.5 Que valent Ack(1, n), Ack(2, n), Ack(3, n), Ack(4, n), Ack(5, 1) ?
La fonction 91 de Mc Carthy
f (n) = n − 10, pour n > 100
f (n) = f ( f (n + 11)), sinon
J
Exercice 2.6 Que vaut f (96) ? Donner une forme non récursive de cette fonction.
2.6. PROGRAMME JAVA 13
La fonction de Morris
g(0, n) = 1
g(m, n) = g(m − 1, g(m, n)), pour m > 0
J
Exercice 2.7 Que vaut g(1, 0) ?
9 /**
10 * Cette classe rassemble des exemples de fonctions récursives.
11 *
12 * @version 4 Août 2000
13 * @author Michel Lemaître
14 */
15 c l a s s TestRecursivite {
16 s t a t i c PrintWriter sortie ;
17
40 s t a t i c i n t fact ( i n t n ) {
41 r e t u r n ( ( n == 0) ? 1 : n * fact (n - 1) );
42 }
43
44 s t a t i c i n t fact_iter ( i n t n ) {
45 i n t resultat = 1;
46
47 f o r ( i n t i = 1; i <= n ; i += 1) {
48 resultat *= i;
49 }
50 r e t u r n resultat ;
51 }
52
14 CHAPITRE 2. RÉCURSIVITÉ
53 s t a t i c i n t fib ( i n t n ) {
54 r e t u r n ( (( n == 1) || ( n == 2))
55 ? 1
56 : fib (n - 1) + fib (n - 2) );
57 }
58
59 s t a t i c i n t fib_eff ( i n t n ) {
60 r e t u r n fib_eff_aux (n , 0, 1);
61 }
62
63 s t a t i c i n t fib_eff_aux ( i n t n , i n t u , i n t v ) {
64 r e t u r n ( ( n == 1)
65 ? v
66 : fib_eff_aux (n - 1, v , u + v ) );
67 }
68
69 static i n t fib_iter ( i n t n ) {
70 int u = 0;
71 int v = 1;
72 int vieux_u ;
73
74 w h i l e ( n > 1) {
75 n -= 1;
76 vieux_u = u;
77 u = v;
78 v += vieux_u ;
79 }
80 r e t u r n v;
81 }
82
93 s t a t i c i n t ackermann ( i n t m , i n t n ) {
94 r e t u r n ( ( m == 0)
95 ? n + 1
96 : ( ( n == 0)
97 ? ackermann (m - 1, 1)
98 : ackermann (m - 1, ackermann (m , n - 1)) ) );
99 }
100 }
12 Déplacement de B à A
13 Déplacement de B à C
14 Déplacement de A à C
15
16 ackermann(3,0) = 5
17 ackermann(3,1) = 13
18 ackermann(3,2) = 29
19 ackermann(3,3) = 61
20 ackermann(3,4) = 125
21 ackermann(3,5) = 253
22 ackermann(3,6) = 509
23 ackermann(3,7) = 1021
24 ackermann(3,8) = 2045
25 ackermann(3,9) = 4093
26 ackermann(3,10) = 8189
Chapitre 3
3.1 Motivation
Il est important d’analyser les algorithmes afin d’évaluer les ressources — temps de calcul et espace mémoire — qu’ils
consomment. On parle d’analyse de complexité d’un algorithme. Analyser un algorithme, c’est donc prévoir les res-
sources nécessaires à l’exécution des programmes qui l’implémenteront.
L’analyse d’un algorithme fait partie intégrante de son exposé. Elle oblige à une compréhension fine de l’algorithme. En
outre, l’analyse permet de comparer entre eux différents algorithmes résolvant le même problème.
Analyse détaillée
Nous allons le montrer sur un exemple, en réalisant une analyse très fine de la complexité en temps d’un algorithme
16
3.2. ANALYSE DE COMPLEXITÉ EN TEMPS 17
de tri par insertion. Voici cet algorithme, figure 3.1. Il trie sur place le tableau d’entiers t[i], pour i de 1 à n. En face
de chaque ligne, nous indiquons d’une part un temps pour son exécution (première colonne), et le nombre de fois que
cette ligne sera exécutée, au cours de l’exécution complète de l’algorithme, pour trier un tableau de n éléments (dernière
colonne). Le temps d’exécution de la ligne i est noté ci . Chaque ligne correspond à une suite d’opérations suffisamment
élémentaires pour que l’on puisse considérer que les ci sont des constantes. Puisque nous analysons un algorithme et non
un programme, nous ne connaissons pas les valeurs précises des ci , mais nous considérons que ce sont des constantes,
pour tout programme implémentant l’algorithme.
Le nombre de fois que la boucle intérieure 6-8 est exécutée dépend de la configuration exacte de l’instance (le contenu
effectif du tableau à trier). Pour en tenir compte et faciliter notre analyse fine, nous introduisons les paramètres t j , j =
2, . . . n : t j est le nombre de fois que le test de la ligne 6 est effectué pour cette valeur de j.
Il ne nous reste plus qu’à faire la somme des produits des quantités de chaque ligne pour obtenir le temps total d’exécution
T:
n n
T = c1 + c2 + c3 n + (c4 + c5 + c9 + c10 )(n − 1) + c6 ( ∑ t j ) + (c7 + c8 )( ∑ (t j − 1))
j=2 j=2
On montre que, lorsque les nombres sont déjà triés, on n’entre jamais dans le corps de la boucle 6-8, et donc t j = 1 pour
tout j. T devient alors
T = a + bn
où a et b sont de nouvelles constantes. Le temps de calcul est dans ce cas linéaire en fonction de la taille du tableau à
trier. On résume ceci par la notation T (n) ∈ Θ(n). Cette notation est définie précisément au paragraphe suivant.
Examinons maintenant la situation inverse, celle où le tableau est au départ trié dans l’ordre inverse. Pour cet algorithme
de tri, c’est la plus mauvaise configuration, car ∀ j = 2, . . . , n : t j = j. On montre facilement que T est alors de la forme
T (n) = c + dn + en2
où c, d, e sont de nouvelles constantes, dont peu importent les valeurs précises. Dans ce cas, le temps de calcul est une
fonction quadratique de la taille du tableau à trier. On note T (n) ∈ Θ(n2 ).
Analyse rapide
En pratique, on effectue rarement une analyse aussi fine des algorithmes. On se contente souvent d’analyser le comporte-
ment de l’algorithme dans le pire cas possible. On obtient ainsi une borne supérieure du temps nécessaire. Avec un peu
d’habitude, on calcule directement sur des taux de croissance. Ainsi dans notre exemple et dans le pire cas, l’algorithme
parcourt deux boucles imbriquées englobant un bloc en temps constant (lignes 7 et 8), chaque boucle étant effectuée au
plus n fois. On a donc toujours T (n) ≤ kn2 , pour une certaine constante k, ce que l’on résume par la notation T (n) ∈ O(n2 ).
Cette notation est également définie précisément au paragraphe suivant.
Autres techniques
La technique générale d’analyse des algorithmes récursifs est exposée page 20. Cependant, pour analyser un algorithme
récursif terminal, on peut par la pensée analyser l’algorithme itératif correspondant.
Voici une technique intéressante, souvent utilisée, lorsque l’algorithme (récursif ou non) visite une structure (typiquement
un tableau, un arbre ou un graphe) : on analyse le temps maximum passé sur chaque élément de la structure (typiquement
sur chaque élément du tableau, ou chaque nœud de l’arbre ou sommet du graphe) ; ensuite on somme ces temps sur les
éléments visités.
Autres analyses
On peut s’intéresser au cas moyen au lieu du cas pire, mais cela nécessite des hypothèses sur la distribution des données,
et l’analyse est généralement bien plus difficile.
Une autre analyse de complexité est importante : la complexité en occupation mémoire. Elle est en général beaucoup plus
facile à faire. Nous en verrons des exemples dans la suite du cours.
18 CHAPITRE 3. ANALYSE DES ALGORITHMES
Résumé
En résumé, on caractérise la complexité en temps d’un algorithme par le taux de croissance du temps d’exécution en
fonction de la taille de l’instance.
c.f(n)
T(n) T(n)
c.g(n)
n n
T (n) = O( f (n)) T (n) = Ω(g(n))
c.h(n)
T(n)
d.h(n)
n
T (n) = Θ(h(n))
Propriétés
f (n) ∈ O(g(n)) ⇐⇒ g(n) ∈ Ω( f (n))
f (n) ∈ Θ(g(n)) ⇐⇒ g(n) ∈ Θ( f (n))
Règle de style : on ôte les termes d’ordre inférieur et les constantes : ne pas écrire T (n) ∈ Θ(2n2 ) ou T (n) ∈ Θ(n2 + n)
mais T (n) ∈ Θ(n2 ).
Discussion et remarques
Il y a une analogie avec la comparaison de nombres :
– f (n) ∈ O(g(n)) ≈ a ≤ b
– f (n) ∈ Ω(g(n)) ≈ a ≥ b
– f (n) ∈ Θ(g(n)) ≈ a = b.
Cependant attention : les fonctions ne sont pas toujours comparables par leurs taux de croissance ; par exemple f (n) = n
et g(n) = n1+sin n .
n3 croit plus vite que n2 , donc n2 ∈ O(n3 ) et n3 ∈ Ω(n2 ).
Soit T (n) ∈ 2n2 . Alors T (n) ∈ O(n4 ), T (n) ∈ O(n3 ), T (n) ∈ O(n2 ), T (n) ∈ Θ(n2 ) sont vrais, mais l’information intéres-
sante est bien sûr la dernière.
La notation Θ signifie que le taux de croissance donné est le meilleur (le plus étroit) possible. On s’efforcera de donner le
taux de croissance en notation Θ.
Un algorithme en Θ(n2 ) est meilleur de façon asymptotique qu’un autre en Θ(n3 ), mais pour les petites tailles le second
peut être meilleur. Exemple A1 : 100n2 ; A2 : 5n3 . A2 est meilleur tant que n < 20.
Voici des taux de croissance typiques rencontrés en analyse d’algorithmes, ordonnés en ordre croissant :
1 : constant
log n : logarithmique
n : linéaire
n log n
n2 : quadratique
n3 : cubique
nc avec c > 1 : polynomial
2n , cn avec c > 1 : exponentiel
Le temps d’execution d’un algorithme est en Θ(g(n)) si et seulement si son temps d’exécution dans le pire des cas est en
O(g(n)) et dans le meilleur des cas est en Ω(g(n)).
On constate que la croissance des fonctions exponentielles 2n et 3n devient rapidement déraisonnable. Les algorithmes
ayant de tels taux de croissance sont confrontés au phénomène dit d’«explosion combinatoire» : ils deviennent rapidement
inutilisables au fur et à mesure que l’on augmente la taille des instances à résoudre.
On s’interroge maintenant sur l’effet des améliorations technologiques : et si on disposait d’ordinateurs plus puissants ?
Soit N la taille de la plus grosse instance traitable en une heure. On se demande quelle taille on pourra traiter lorsque les
ordinateurs seront 100 et 1000 fois plus rapides.
Exemple pour T (n) = n2 : aujourd’hui T (N) = kN 2 , demain T 0 (N) = kN 2 /100. On cherche donc N 0 tel que T (N) = T 0 (N 0 )
soit kN 2 = kN 02 /100 soit N 0 = 10N.
Dans les cas linéaire et polynomial, on profite pleinement de la nouvelle puissance. Dans le cas exponentiel, le surcroît de
puissance apporte peu.
trifusion(S,n) =
si n=1 retour S
sinon partager S en deux sous-séquences S1 et S2 ;
retour fusion(trifusion(S1,n/2),
trifusion(S2,n/2))
Pour effectuer cette analyse, nous supposerons que le partage et la fusion sont en Θ(n). Le temps de calcul T (n) du tri des
n nombres s’exprime par un système de deux équations :
T (n) = c1 , si n = 1
T (n) = 2T (n/2) + c2 n , si n > 1
Voici la solution générale de la récurrence T (n) = γT (n/2) + Θ(nβ ), dans laquelle n/2 peut être remplacé par les parties
entières supérieures ou inférieures de n/2 :
– si γ < 2β alors Θ(nβ )
– si γ = 2β alors Θ(nβ log n)
– si γ > 2β alors Θ(nlog2 γ )
J
Exercice 3.1 Résoudre le système précédent pour le tri par fusion.
Toutefois, il est bon de conserver du recul par rapport à la complexité théorique d’un algorithme. On doit souvent faire le
choix entre un algorithme A lent mais simple à comprendre et à programmer, et un autre algorithme B plus rapide mais
compliqué et plus difficile à programmer. D’autres phénomènes peuvent jouer : lorsque la taille des données est petite,
l’algorithme A peut suffire ; de même si l’algorithme est peu employé.
On doit se méfier aussi des effets des constantes de proportionalité cachées dans les taux de croissance, ou du fait que le
pire cas peut être statistiquement improbable. Ainsi, on connait l’exemple de l’algorithme du simplexe en programmation
linéaire : il est exponentiel en théorie, mais polynomial en pratique. Dans le même ordre d’idée, l’algorithme du tri rapide
(quicksort, voir page 69), est en Θ(n2 ) dans le pire cas, mais ce pire cas est extrêmement improbable. L’algorithme est en
moyenne et en pratique en O(n log n).
Enfin, d’autres critères de jugement entrent en ligne de compte : la consommation mémoire centrale ou mémoire secon-
daire, la clarté, la simplicité de l’algorithme.
3.6 Exercices
Exercice 3.2 L’affirmation suivante : «le temps d’exécution de l’algorithme A est au moins en O(n2 )» a-t-elle un
J
sens ?
Exercice 3.4 Montrer rigoureusement que f (n) = 3n n’est pas O(nc ), ∀c.
J
Exercice 3.5 Montrer que n! croit plus vite que 2n (très facile).
J
J
Exercice 3.7 Analyser le tri par bulle (fonction tri_bulle dans le programme Java qui commence en page 72, lignes
85 à 95).
J
Exercice 3.8 Analyser le tri par sélection (fonction tri_selection dans le programme Java qui commence en page
72, lignes 97 à 110).
J
Exercice 3.9 Analyser le tri par dénombrement (fonction tri_denombrement dans le programme Java qui commence
en page 72, lignes 138 à 157). Faire l’analyse en fonction de n et k.
22 CHAPITRE 3. ANALYSE DES ALGORITHMES
J
Exercice 3.10 Quel travail accomplit la fonction récursive suivante, et quel est son intérêt ?
static double toto(double x, int n) throws Exception {
if (n < 0) {
throw new Exception("x est négatif !");
} else if (n == 0) {
return 1.0;
} else if (n % 2 == 1) { // teste si n est impair
return x * toto(x, n - 1);
} else {
return toto(x * x, n /2 );
}
}
Analysez sa complexité en temps (pire cas), en fonction de n. Aide : considérer la décomposition binaire de n.
J
Exercice 3.11 Étant donnés un texte et un motif sous forme de séquences de caractères, déterminer le nombre de fois
où le motif apparait dans le texte.
Exemples :
– le motif "elle" apparait 2 fois dans le texte "quelle belle journee",
– le motif "abcabdabc" apparait 1 fois dans le texte "abcabcabdabc",
– le motif "aa" apparait 4 fois dans le texte "aaaaa".
Voici un algorithme (en Pascal) qui résoud le problème :
1 function p0(motif,texte:string):integer ;
2 var lt,lm,i,j,n :integer ;
3 var c :bool ;
4 begin
5 lt := length_string(texte); (* longueur du texte *)
6 lm := length_string(motif); (* longueur du motif *)
7 n := 0; (* nombre d’occurences trouvees *)
8 i := 1; (* indice sur les caracteres du texte *)
9 while i <= lt-lm+1 do
10 begin
11 j := 1; (* indice sur les caracteres du motif *)
12 c := true;
13 while (j <= lm) do
14 begin
15 c := c and ( texte[i+j-1] = motif[j] ) ;
16 j := j+1
17 end ;
18 if c then n := n+1;
19 i := i+1
20 end;
21 return(n)
22 end.
8
9 /* ici on a 0<=n<p*p */
10
11 b = 0;
12 h = p;
13 d = p;
14
15 while (d>1) {
16 /* d = h-b b*b<=n<h*h */
17 e = d/2;
18 m = b+e;
19 if (n<(m*m)) h = m; else b = m ;
20 d = e;
21 }
22
23 return b ;
24 }
25
26 main()
27 {
28 int n;
29
30 while (1) {
31 printf("Donnez n\n ");
32 scanf("%d", &n);
33 if (n < 0)
34 printf("Illegal number\n");
35 else printf("Racine carree de %d = %d\n",n,rac(n)) ;
36 }
37 }
J
Exercice 3.14 La multiplication de deux nombres complexes utilise 4 multiplications et 2 additions ou soustractions
sur les réels. Montrer que 3 multiplications suffisent.
J
Exercice 3.15 Le thème de cet exercice est l’analyse de complexité de deux algorithmes de multiplication. On prendra
pour taille d’une multiplication le maximum du nombre de chiffres des deux nombres à multiplier, soit n.
1. Montrer par un argument rapide que la complexité en temps dans le pire cas de l’algorithme de multiplication
“ordinaire” (la multiplication à la main, apprise à l’école), est Θ(n2 ).
2. On supposera dans la suite que n est une puissance de 2. Les nombres à multiplier sont donnés sous forme d’une
suite de n bits. Cette suite est coupée en deux parties de n/2 bits, de telle sorte que, x et y étant les deux nombres à
multiplier, on a : x = a2n/2 + b et y = c2n/2 + d, a, b, c, d étant des nombres de n/2 bits.
(a) Vérifier la formule : xy = ac2n + [(a − b)(d − c) + ac + bd]2n/2 + bd.
(b) En déduire un nouvel algorithme de multiplication, que l’on énoncera informellement.
(c) Evaluer la complexité en temps dans le pire cas de cet algorithme. Est-il meilleur que le premier ?
Aides : on tiendra pour acquis que l’addition ou la soustraction de nombres de n bits est Θ(n) ; d’autre part, la multipli-
cation par 2n se fait simplement par n décalages, ce qui est aussi Θ(n).
J
Exercice 3.16 Multiplication de deux complexes.
Chapitre 4
Au niveau individuel
Au niveau individuel, les données sont organisées en enregistrements (figure 4.1). Dans chaque enregistrement, on trouve
– un ensemble d’informations organisées en champs
– une clé associée aux informations, et qui sert à les retrouver.
clé
champ1
champ2
. informations
.
.
F IG . 4.1 – Un enregistrement.
La clé est le plus souvent présente, mais elle peut être absente. La clé est souvent un des champs d’information.
Voici des exemples d’enregistrements :
– dans un dictionnaire de langue, les mots sont les clés, les définitions sont les informations, le couple clé-définition
constitue l’enregistrement.
– dans un annuaire téléphonique, la clé est le nom, les champs sont l’adresse et le numéro de téléphone.
– dans un agenda, la clé est la date.
– dans un dossier de sécurité sociale, la clé est le numéro de sécurité sociale, identifiant unique pour chaque individu.
En général, chaque clé est unique (c’est le cas dans nos exemples sauf pour l’annuaire téléphonique à cause des homony-
mies), et il existe un ordre total sur les clés (numérique, alphabétique ...).
Au niveau collectif
Au niveau collectif, la collection1 des enregistrements constituant les données est organisée de façon à faciliter leur
manipulation. Des opérations, telles que : retrouver un enregistrement par sa clé, modifier, ajouter ou supprimer des
1 J’emploie à dessein le terme «collection», et non «ensemble», pour signifier un groupe d’éléments, dans lequel il peut y avoir éventuellement des
éléments dupliqués. Dans un ensemble au contraire, chaque élément est unique. Un autre mot possible pour «collection» est «multi-ensemble».
24
4.2. STRUCTURE ABSTRAITE ET STRUCTURE CONCRÈTE 25
enregistrements, vont s’appliquer sur la collection et la faire évoluer au cours du temps. L’organisation de la collection,
conçue en fonction des opérations que l’on prévoit de lui faire subir pendant sa vie, est nommée structure de données.
Structure abstraite
Une structure de donnée abstraite2 est définie par l’ensemble des opérations prévues sur la structure. Chaque opération
est décrite par sa signature, c’est-à-dire l’énoncé des types des objets qu’elle reçoit et qu’elle renvoie. On décrit les pro-
priétés que doivent respecter les effets des opérations entre elles. En somme, une structure de données abstraite est une
spécification. Éventuellement, cette spécification peut être décrite formellement dans un langage à caractère mathéma-
tique.
Structure concrète
Une structure de données concrète correspond à l’implémentation d’une structure de donnée abstraite. Une structure de
données concrète est composée d’un algorithme pour chaque opération, plus éventuellement des données spécifiques à la
structure pour sa gestion.
Une même structure de données abstraite peut donner lieu à plusieurs structures de données concrètes, avec des perfor-
mances différentes.
Le reste de ce chapitre est consacré à la présentation des structures de données abstraites dont on a le plus souvent besoin.
4.4 Dictionnaire
Le dictionnaire3 est une structure de donnée abstraite fondamentale. Dans un dictionnaire, les enregistrements ont chacun
une clé unique. Mathématiquement, le dictionnaire correspond à la notion de fonction, du domaine des clés vers celui des
enregistrements.
Cette structure est définie par les quatre opérations suivantes, qui s’appliquent implicitement à l’ensemble courant des
enregistrements du dictionnaire :
– EST _ VIDE() → Booléen : teste si le dictionnaire est vide.
– INSÈRE(Clé , Enregistrement ) : insère dans le dictionnaire un enregistrement de clé donnée.
2 On pourrait dire égalemement : type de données abstrait. En anglais : abstract data type, ADT.
3 Quelquefois nommé liste d’associations.
26 CHAPITRE 4. GÉNÉRALITÉS SUR LES STRUCTURES DE DONNÉES
Le programme Java des pages 4.8 et suivantes donne l’exemple de la création et de l’exploitation d’un dictionnaire,
utilisant la bibliothèque collections framework.
4.5 Pile
La pile est une structure abstraite permettant de gérer une collection d’enregistrements dépourvus de clés, offrant les
opérations suivantes :
– EST _ VIDE() → Booléen : teste si la pile est vide.
– EMPILE(Enregistrement ) : insère un enregistrement au sommet de la pile.
– DÉPILE() → Enregistrement {null} : supprime de la pile le dernier enregistrement empilé, celui qui occupe le som-
S
met, et renvoie cet enregistrement. Il va donc y avoir un nouveau sommet, sauf si la pile est vide après la suppression.
– SOMMET() → Enregistrement : donne accès au sommet de la pile (le dernier enregistrement empilé), sans l’ôter de
celle-ci.
En anglais, une pile se dit stack, et les trois dernières opérations se nomment respectivement push, pop, top.
La pile applique le principe « dernier entré, premier sorti » (Last In, First Out, LIFO).
La pile, bien que très simple, a beaucoup d’usages, et spécialement (mais pas uniquement) dans le domaine de la compi-
lation des langages informatiques. Voici quelques usages classiques de la pile :
– vérification du bon équilibrage d’une expression parenthésée,
– calcul des expressions postfixées,
– conversion d’une expression en notation infixe en une notation postfixée,
– mémorisation des appels de procédures imbiquées au cours de l’exécution d’un programme, et en particulier les appels
des procédures récursives,
– parcours de structures arborescentes (voir page 40), et parcours de graphe (voir la seconde partie du cours),
– tout simplement, la pile peut servir à implémenter un ensemble d’enregistrements sans clés, jouissant des seules opéra-
tions «ajouter un élément», «ôter et renvoyer un élément», «ensemble vide ?», avec une grande efficacité. Ce peut être
une simple collection d’enregistrements que l’on doit mémoriser au fur et à mesure de leur création, et traiter chacun
une seule fois.
En fait, l’idée de pile est très liée à celle de récursion. L’usage d’une procédure récursive évite d’avoir à gérer explicitement
une pile.
J
Exercice 4.1 Proposer trois algorithmes correspondant aux trois premières applications énoncées ci-dessus (difficulté
croissante).
4.6 File
La file est une structure abstraite analogue à la pile, sauf qu’elle applique le principe « premier entré, premier sorti » (First
in, First out, FIFO). Cela donne un comportement analogue à une file d’attente devant un guichet.
En anglais, file se dit queue.
Les usages de la file sont nombreux. La plupart correspondent à l’idée simple de file d’attente. Exemples :
– gestion des travaux d’impression d’une imprimante.
– ...
– certains parcours de graphe (voir la seconde partie du cours).
4 Plus précisément, un pointeur sur l’enregistrement.
4.7. FILE DE PRIORITÉ 27
On se sert de files de priorité pour gérer des tâches auquelles sont associées des priorités. L’ordonnancement des tâches
d’un ordinateur en est un exemple.
Une application majeure des file de priorité est la simulation évènementielle. Dans un simulateur temporisé (par exemple
un simulateur de réseau de transports, de théatre de combats . . .), l’évènement courant est traité ; ce traitement génère de
nouveaux évènements dans le futur ; ces nouveaux évènements, auquels on associe comme clé l’instant auquel ils auront
lieu, sont rangés dans une file de priorité. Le prochain évènement courant est extrait de la file par EXTRAIRE _ MINIMUM.
La file de priorité est utilisée également dans un algorithme de recherche du plus court chemin (algorithme de Dijkstra).
La performance de l’algorithme dépend directement des performances des opérations de la file de priorité.
Les chapitres suivants sont consacrés à l’étude des principales structures de données concrètes, qui servent à implémenter
les structures abstraites que nous venons de voir.
10 /**
11 * Exemples d’utilisation d’un dictionnaire.
12 *
13 * @version 24 Août 2000
14 * @author Michel Lemaître
15 */
16 c l a s s TestDictionnaire {
17 p u b l i c s t a t i c void main ( String [] args ) {
18
28 m. put ( a1 . nom , a1 );
28 CHAPITRE 4. GÉNÉRALITÉS SUR LES STRUCTURES DE DONNÉES
29 m. put ( a2 . nom , a2 );
30 m. put ( a3 . nom , a3 );
31
37 c l a s s Appareil {
38 String nom ;
39 double poids ;
40 double envergure ;
41 i n t equipage ;
42 double vitesse ;
43
52 p u b l i c String toString () {
53 return
54 "< nom =" + nom +
55 "; poids =" + poids +
56 "; envergure =" + envergure +
57 "; equipage =" + equipage +
58 "; vitesse =" + vitesse + ">" ;
59 }
60 }
61
62 /*==================
63
64 >java TestDictionnaire
65 <nom=Boeing747; poids=174400.0; envergure=59.6; equipage=3; vitesse=940.0>
66 null
67
68 ================== */
Chapitre 5
Tableau
Le tableau, que tout le monde connait, et qui est disponible dans presque tous les langages de programmation, est la
structure de données concrète la plus simple. Le tableau est également à la base de toutes les structures concrètes, de
façon plus ou moins implicite. Sa propriété essentielle est la suivante : à partir d’un indice, on accède en temps constant
à l’élément du tableau correspondant. On résume cela en disant que le tableau est une structure à accès direct.
L’inconvénient du tableau, c’est sa rigidité : on est obligé de prévoir sa taille au moment de sa création. Le faire grossir
ou rétrécir dynamiquement coûte cher.
1 2 clé
tableau ....... ......
F IG . 5.1 – Implémentation d’un dictionnaire avec un tableau, lorsque le domaine des clés est entier positif et petit. La clé
sert à indicer directement l’enregistrement dans le tableau. Pour les clés non utilisées, le tableau contient la valeur null.
29
30 CHAPITRE 5. TABLEAU
Liste chaînée
La liste chaînée est une structure de donnée concrète très souple, qui peut servir à implémenter — plus ou moins efficace-
ment — la plupart des structures de données abstraites.
Dans une liste chaînée, l’ordre des enregistrements n’est pas déterminé par un indice comme dans un tableau, mais par
une suite de pointeurs1 (voir la figure 6.1). La liste chaînée n’est pas une structure à accès direct : pour retrouver un
enregistrement dans la liste (opération RECHERCHE), on doit suivre la séquence des pointeurs depuis le début (tête) de la
liste. La liste est une structure à accès séquentiel.
Nous allons illustrer dans ce chapitre l’utilisation de la liste chaînée pour implémenter un dictionnaire2 (définition page
25). Les structures abstraites collection et ensemble s’implémentent avec une liste chaînée essentiellement de la même
façon.
Element
info
clé suivant
tête
5 12 8
F IG . 6.1 – Liste simplement chaînée de trois enregistrements de clés respectives 5, 12, et 8. Le pointeur null est représenté
par un "/".
Un programme Java de liste simplement chaînée est proposé page 33 et suivantes. La classe Liste contient un seul
champ : le champ tête de type Element. C’est par la tête de la liste qu’on pourra accéder à tous ses enregistrements.
Dans notre implémentation, nos enregistrements sont représentés par les deux champs :
– clé, ici de type int,
– info de type Element.
Le dernier champ suivant de la classe Element, de type Element lui-même, est un pointeur vers la suite de la liste. Ce
pointeur peut être la valeur spéciale null, signalant ainsi la fin de la liste.
Pour construire une liste vide, on alloue simplement un nouvel objet de type Liste en inititialisant sa tête à null (ligne
75).
L’opération EST _ VIDE est simplement un test reconnaissant une tête égale au pointeur null (ligne 92).
1 Un pointeur est simplement une adresse interne.
2 Bien que ce ne soit pas l’implémentation la plus efficace. La table de hachage (chapitre 7), ou l’arbre «rouge et noir» (chapitre 10), sont de
meilleures structures concrètes pour le dictionnaire.
31
32 CHAPITRE 6. LISTE CHAÎNÉE
L’opération RECHERCHE (lignes 95 à 105) est implémentée par une boucle while dont on sort si la fin de liste est atteinte,
ou bien si on tient l’enregistrement cherché.
L’opération INSÈRE (lignes 107 à 113) insère un nouvel élément (dont seul le champ info est supposé initialisé) en début
de liste.
L’implémentation de l’opération SUPPRIME proposée ici (lignes 115 à 133) est plus subtile qu’il n’y parait au premier
abord. Remarquer tout d’abord que la méthode supprime reçoit un argument de type Element. Cet argument peut
avoir été retrouvé auparavant par l’opération RECHERCHE. La méthode supprime a recours à une fonction auxiliaire
supprime_aux(e, t), qui supprime récursivement l’élément e de la sous-liste t, et rend la sous-liste t modifiée. On
renvoie null s’il n’y a pas eu de suppression (l’élément n’était pas dans la liste).
J
Exercice 6.1 Donner une version non récursive de SUPPRIME.
Element
clé
précédent info
tête suivant
5 12 8
F IG . 6.2 – Liste doublement chaînée de trois enregistrements de clés respectives 5, 12, et 8. Le pointeur null est représenté
par un "/". Par rapport à la liste simplement chaînée, un champ précédent a été ajouté dans Element.
J
Exercice 6.5 Proposer une implémentation de la file avec une liste chaînée, assurant toutes les opérations sur la file
en O(1). Comparer avec l’implémentation en tableau (exercice 5.3 page 30), et discuter les avantages et les inconvénients
des deux implémentations.
J
Exercice 6.6 Proposer deux implémentations de la file de priorité par une liste chaînée. Analyser la complexité en
temps des opérations, et discuter des avantages et inconvénients des deux implémentations.
9 /**
10 * Définition et test de la liste simplement chaînée, non triée, non circulaire.
11 *
12 * @version 3 Septembre 2000
13 * @author Michel Lemaître
14 */
15 c l a s s TestListe {
16 s t a t i c PrintWriter sortie ;
17
28 l1 . insère (12, e1 );
29 l1 . insère (18, e2 );
30 l1 . insère (24, e3 );
31 sortie . println ( l1 );
32
36 l1 . supprime ( e2 );
37 sortie . println ( l1 );
38
39 l1 . supprime ( e3 );
40 sortie . println ( l1 );
41
42 l1 . supprime ( e1 );
43 sortie . println ( l1 );
44
45 l1 . supprime ( e4 );
46 sortie . println ( l1 );
47
48 sortie . close () ;
49 }
50 }
51
52
34 CHAPITRE 6. LISTE CHAÎNÉE
53 c l a s s Element {
54 i n t clé ;
55 Object info ;
56 Element suivant ;
57
63 /** Imprimeur */
64 p u b l i c String toString () {
65 r e t u r n "( clé =" + clé + ", info =" + info + ")\ n";
66 }
67 }
68
69
70 c l a s s Liste {
71 Element tête ;
72
78 /** Imprimeur */
79 p u b l i c String toString () {
80 String s = " Liste : (\ n";
81 Element e = tête ;
82
83 w h i l e ( e != n u l l ) {
84 s = s + e. toString ();
85 e = e. suivant ;
86 }
87 r e t u r n s + ")\ n";
88 }
89
Le fichier sortie_TestListe
1 Liste : (
2 (clé=24, info=toto)
3 (clé=18, info=titi)
4 (clé=12, info=tata)
5 )
6
7 (clé=18, info=titi)
8
9 null
10 Liste : (
11 (clé=24, info=toto)
12 (clé=12, info=tata)
13 )
14
15 Liste : (
16 (clé=12, info=tata)
17 )
18
19 Liste : (
20 )
21
22 Liste : (
23 )
24
Chapitre 7
Table de hachage
7.2 Fonctionnement
1
2 k3
15 k2 k1
F IG . 7.1 – Table de hachage contenant trois enregistrements de clés k1 , k2 et k3 , avec h(k1 ) = h(k2 ) = 15 (collision), et
h(k3 ) = 2. Le pointeur null est représenté par un "/".
Le fonctionnement de la table de hachage est très simple. Voir la figure 7.1. On utilise un tableau support t de longueur
m. C’est un tableau de listes chaînées : chaque élément du tableau est une tête de liste. Soit x un enregistrement de clé k à
insérer dans la table. Il sera inséré dans la liste d’indice t[h(k)], où h est une fonction de U dans 1, . . . , m. Cette fonction
1 La technique, inventée en 1953, est due à H. P. Luhn.
36
7.3. FONCTIONS DE HACHAGE 37
h est appelée fonction de hachage. Elle doit être déterministe2 et calculable en temps O(1).
Les listes ne sont pas triées sur les clés. L’insertion peut se faire en tête de liste, donc en temps O(1), à condition d’être
sûr de l’unicité de la clé insérée. Sinon, il faut passer par une recherche préalable. Pour rechercher un enregistrement de
clé donnée k, il suffit de le chercher dans la liste t[h(k)], ce qui prend un temps proportionnel à la longueur de la liste.
Pour une suppression, on est ramené au problème de supprimer un enregistrement dans une liste.
On déduit de ce fonctionnement que les performances des deux opérations de recherche et de suppression sont étroitement
liées à la longueur des listes : il faut qu’elles soient les plus courtes possibles. On dit qu’il y a collision lorsque deux clés
distinctes tombent dans la même liste. C’est le cas figure 7.1 pour les clés k1 et k2 . À la limite, toutes les clés pourraient
tomber dans la même liste. Pour éviter ce phénomène catastrophique, on s’arrange pour que la fonction h répartisse bien
les clés dans les listes, c’est-à-dire de façon uniforme.
On pourrait croire qu’il est possible de concevoir une fonction h telle que les listes seraient toutes de longueur 1 au
maximum (et que donc on pourrait se passer des listes). Mais le phénomène de collision est inévitable, du fait que |U| n.
1
pour tout i ∈ (1, . . . , m) : ∑ P(k) =
m
{k | h(k)=i}
où P(k) est la probabilité que la clé k soit utilisée (on rappelle que |U| n, et que m est la longueur du tableau support).
La plupart du temps les probabilités P(k) sont inconnues, et donc on ne peut pas construire de fonction de hachage parfaite.
On ne peut que vérifier empiriquement sa qualité, par observation de répartition des longueurs des listes pendant la vie
du dictionnaire. Cependant, beaucoup d’études ont été faites sur de bonnes fonctions de hachage. Nous allons en décrire
deux classiques.
Tout d’abord, on s’arrange pour transformer les clés non entières en clés entières. Par exemple si les clés sont de chaînes
de caractères codés en ASCII, la clé X1 sera transformée en l’entier 88 × 128 + 31, sachant que le code ASCII de X est
88 et celui de 1 est 31.
Méthode de la division
On prend h(k) = k mod m + 1 (reste de la division entière plus 1). On rappelle que m est la longueur du tableau support.
On évite de prendre pour m une puissance de 2, car alors h(k) serait constituée des bits droits de k, ignorant les gauches :
ça ne mélange pas assez. De même on évite de prendre une puissance de 10 (penser par exemple aux numéros de sécurité
sociale pris comme clé). Une bonne chose est de prendre pour m un entier premier proche de la longueur visée au départ.
Par exemple, on prévoit un dictionnaire de n = 2000 enregistrements, et on accepte des listes de longueur 3 en moyenne.
On prendra m = 701, premier et proche de 2000/3, et loin d’une puissance de 2.
Méthode de la multiplication
On prend h(k) = bm(kA − bkAc)c + 1, où A est un nombre réel bien choisi tel que 0 < A < 1. bxc désigne la partie entière
du nombre réel x. La quantité kA − bkAc est simplement la partie fractionnaire de kA.
L’avantage de cette fonction est qu’elle n’impose pas de contrainte sur m.
Hachage universel
J
Exercice 7.1 Un pirate informatique fantaisiste ou hostile, connaissant votre fonction de hachage, pourrait tenter
de planter votre application (un compilateur par exemple) en s’arrangeant pour créer des enregistrements dont les clés
(des noms de variables) tombent toutes dans la même liste. Comment parer cette grave menace ? (le principe est facile à
trouver, l’application plus difficile.)
2 Cela veut dire que c’est une véritable fonction : appliquée au même paramètre, elle renvoie toujours le même résultat.
38 CHAPITRE 7. TABLE DE HACHAGE
Les chapitres suivants décriront des structures de données concrètes de type arborescence1 . Ce chapitre regroupe les
notions et le vocabulaire sur les arborescences utiles pour la suite du cours.
b c d
e f g
h i j k l m n
o p q
F IG . 8.1 – Une arborescence. Elle est composée d’une racine, le nœud a, et de trois sous-arborescences de racines b, c et
d. Les arcs sont implicitement orientés du haut vers le bas. Les fils du nœud g sont les nœuds j, k l, m et n. Le père du
nœud j est le nœud g. Les feuilles de cette arborescence sont les nœuds h, i, f, c, j, o, p, q, l, m, n. Les ancêtres de k sont
k, g, d et a. Les descendants de b sont b, e, f, h et i. Les hauteurs respectives des nœuds b, c, d sont 2, 0, 3. La hauteur de
l’arborescence est 4.
Les ri sont les fils de r, et r est le père de chaque ri . La racine de l’arborescence est le seul nœud sans père. Les nœuds qui
n’ont pas de fils sont appelés feuilles de l’arborescence. Les nœuds qui ont au moins un fils sont appelés nœuds internes.
Tous les arcs sont orientés de la racine vers les feuilles. Une séquence de nœuds partant d’un nœud a en suivant des arcs
(selon leur orientation) jusqu’à un nœud b s’appelle un chemin de a à b. Tous les nœuds du chemin de a à b sont des
ancêtres de b et des descendants de a. On peut donc dire que les nœuds d’une sous-arborescence de racine x sont les
descendants de x (y compris x lui-même). La longueur d’un chemin a1 , a2 , · · · , a p est le nombre d’arcs sur le chemin,
c’est-à-dire p − 1. Il existe exactement un chemin de la racine jusqu’à tout nœud. La hauteur d’un nœud est la longueur
1 On dit également arbre enraciné (en anglais : rooted tree ).
2 Dans la seconde partie du cours, on définira un arbre comme un graphe non orienté, connexe et acyclique, et une arborescence comme un arbre
dans lequel un sommet – la racine – est distingué, ce qui oriente implicitement toutes les arêtes. La définition donnée ici de l’arborescence est différente,
mais équivalente.
39
40 CHAPITRE 8. GÉNÉRALITÉS SUR LES ARBORESCENCES
du plus long chemin partant de ce nœud et aboutissant à une feuille. La hauteur de toute feuille est donc 0. La hauteur
de l’arborescence est la hauteur de sa racine. L’arité d’un nœud est le nombre de ses fils. Une arborescence binaire, ou
simplement arbre binaire, est une arborescence dans lequel tout nœud possède au plus deux fils.
Un arbre binaire complet est un arbre binaire dont tous les nœuds internes ont deux fils, et dont tous les chemins de la
racine aux feuilles sont de longueur égales.
F IG . 8.2 – À gauche, un arbre binaire complet donc parfaitement équilibré. À droite, un arbre binaire possédant le même
nombre de nœuds, mais complètement déséquilibré.
J
Exercice 8.1 Quelles sont les hauteurs maximale et minimale d’une arborescence binaire de n nœuds ?
L’arbre binaire de recherche, en abrégé ABR, est une structure de donnée concrète arborescente servant à implémenter
les structures abstraites avec clés, lorsqu’il existe une relation d’ordre total sur le domaine des clés. L’ABR peut donc
implémenter les opérations du dictionnaire (définition page 25), ainsi que d’autres opérations exploitant l’ordre sur les
clés : MAXIMUM, MINIMUM, SUCCESSEUR et PRÉDÉCESSEUR. Elle peut servir également à implémenter la file de priorité
(définition page 27 ; voir l’exercice 9.14).
Toutes les opérations ont, dans le pire des cas, un temps d’exécution proportionnel à la hauteur de l’arborescence.
En informatique, on emploie souvent par facilité le terme «arbre» à la place du terme plus correct «arborescence». Nous
ne dérogerons pas à cette habitude.
8 /** Constructeur.
9 * On met juste l’information dans le noeud,
10 * Le reste sera garni lors de l’insertion. */
11 Noeud ( Object o ) {
12 info = o;
13 }
14
15 /** Imprimeur */
16 p u b l i c String toString () {
17 r e t u r n ( clé + "=" + info + " " );
18 }
19 }
Dans cet exemple, les clés employées sont des entiers, mais dans le cas général le domaine des clés est quelconque, pourvu
qu’il existe une relation d’ordre total sur les clés.
1 Le pointeur vers le nœud père est facultatif, mais est utile pour des implémentations efficaces.
41
42 CHAPITRE 9. ARBRE BINAIRE DE RECHERCHE (ABR)
L’ABR possède de plus une propriété fondamentale, exploitant un ordre total sur les clés. Soit x un nœud quelconque
d’un ABR. Pour tout nœud y du sous-arbre gauche de x, la clé de y est inférieure ou égale à la clé de x, et pour tout nœud
z du sous-arbre droit de x, la clé de z est supérieure ou égale à la clé de x.
La figure 9.1 représente deux exemples d’ABR. Elle montre que plusieurs ABR différents peuvent posséder la même
collection de clés. Les deux arbres (a) et (b) sont équilibrés différemment, et leurs hauteurs respectives sont différentes.
6 2
3 7 3
2 5 8 7
(a) 6 8
5 (b)
F IG . 9.1 – Deux arbres binaires de recherche ayant la même collection de clés. Les clés sont des entiers. Dans les figures
d’arbres binaires de recherche, nous représentons seulement la clé associée à chaque nœud, et non l’information associée.
De même, les pointeurs issus de chaque nœud sont implicitement représentés par les segments de droite entre nœuds. Les
pointeurs null ne sont pas représentés. Comme d’habitude, la racine de l’ABR est en haut.
L’énoncé ci-dessus de la propriété fondamentale de l’ABR utilise des inégalités non-strictes. Pour un dictionnaire, on
pourrait imposer des inégalités strictes, vu que dans ce cas les clés sont uniques. Nous conservons cependant cette défi-
nition plus large, car elle permet de décrire également l’ABR comme support de la file de priorité (pour laquelle les clés
sont ordonnées mais non nécessairement uniques).
15
6 18
3 7 17 20
2 4 13
F IG . 9.2 – Recherches dans un arbre binaire de recherche. La clé minimum est 2. Le successeur de la racine est le nœud
de clé 17, minimum de son sous-arbre droit. Le successeur du nœud de clé 4, qui n’a pas de sous-arbre droit, est le nœud
de clé 6.
4 w h i l e ( x. gauche != n u l l ) {
5 x = x. gauche ;
6 }
7 r e t u r n x;
8 }
Cet algorithme est itératif. À partir du nœud z, on suit les fils gauches jusqu’à tomber sur un sous-arbre vide. Le nœud de
clé minimum est le dernier visité. Ce peut être z lui-même.
J
Exercice 9.3 Écrire minimum sous forme récursive.
J
Exercice 9.4 Écrire maximum.
4 w h i l e (( x != n u l l ) && ( c != x. clé )) {
5 x = ( c < x. clé ) ? x. gauche : x. droit ;
6 }
7 r e t u r n x;
8 }
C’est encore une forme itérative. Depuis z, on descend un chemin dans l’arbre, à gauche ou à droite selon le résultat de la
comparaison entre la clé cherchée et celle du nœud visité. La descente s’arrête
– soit sur un pointeur null, ce qui signifie que la clé n’est pas dans le sous-arbre de racine z, on renvoie null,
– soit sur le nœud cherché : il est renvoyé.
Bien sûr, pour rechercher une clé dans l’ABR tout entier, on appelle la procédure précédente sur le nœud racine.
J
Exercice 9.5 Écrire recherche sous forme récursive.
6 Noeud x = z;
7
8 w h i l e (( y != n u l l ) && ( x == y. droit )) {
9 x = y;
10 y = y. père ;
11 }
12 r e t u r n y;
13 }
14 }
Si le sous-arbre droit de z n’est pas vide (test ligne 2), alors le successeur de z est simplement le nœud de clé minimum
parmi les nœuds du sous-arbre droit (ligne 3). Par exemple, dans la figure 9.2, le successeur de la racine est le nœud de
clé 17.
Si le sous-arbre droit de z est vide (bloc qui débute ligne 5), alors le successeur de z, s’il existe, est le premier ancêtre de
z dont le fils gauche est aussi un ancêtre de z. Par exemple, dans la figure 9.2, le successeur du nœud de clé 4 est le nœud
de clé 6. Pour le trouver, on remonte dans l’arbre vers la racine à partir de z jusqu’au «premier virage à droite» (test ligne
8). Plus précisément on sort de la boucle des lignes 8-11
– soit par (y == null), auquel cas la racine est atteinte, signifiant que z était le maximum : on renvoie null,
– soit par (x != y.droit), auquel cas y est le successeur cherché.
J
Exercice 9.6 Démontrer rigoureusement la correction de l’algorithme précédent (deux cas).
Les notions de successeur et de prédécessur se généralisent facilement au cas où les clés ne sont pas toutes distinctes.
L’algorithme ci-dessus fonctionne aussi dans ce cas.
J
Exercice 9.7 Vérifier que toutes les opérations de recherche ci-dessus s’exécutent en temps O(h), où h est la hauteur
de l’arbre.
5 z. clé = c;
6 z. droit = n u l l ;
7 z. gauche = n u l l ;
8 w h i l e ( x != n u l l ) {
9 y = x;
10 x = ( z. clé < x. clé ) ? x. gauche : x. droit ;
11 }
12 z. père = y;
13 i f ( y == n u l l ) {
14 racine = z;
15 } e l s e i f ( z. clé < y. clé ) {
16 y. gauche = z;
17 } else {
18 y. droit = z;
19 }
20 }
L’idée est simple : comme pour une recherche, on descend dans l’arbre en suivant les fils gauches ou droits selon les
résultats des comparaisons entre c et les clés rencontrées sur le chemin (boucle des lignes 8-11). Dans cette descente, x
parcourt le chemin, et y est toujours le père de x. On s’arrête lorsque x vaut null. C’est ce null que l’on doit remplacer,
dans y, par le nouveau nœud z à insérer. L’accrochage effectif a lieu lignes 12-19. Il faut distinguer le cas d’un arbre vide
au départ (lignes 13-14) du cas général (lignes 15-19).
9.5. SUPPRESSION DANS UN ABR 45
J
Exercice 9.8 Vérifier que l’insertion ci-dessus s’exécute en temps O(h), où h est la hauteur de l’arbre.
J
Exercice 9.9 Écrire une version récursive de l’insertion.
15 15
5 16 5 16
3 12 p 20 3 12 20
z
y
10 13 18 23 10 18 23
6 x = null 6
7
(a) 7
15 p 15
z
5 16 y 5
3 12 20 x 3 12 20
10 13 18 23 10 13 18 23
6 6
(b)
7 7
15 15 15
z
5 16 y 6 5 16 6 16
3 12 20 3 12 20 3 12 20
p 10 13 18 23 10 13 18 23 10 13 18 23
y
6 7 7
x 7 (c)
et la recherche se font en O(n) pour la liste. L’ABR est donc très avantageux s’il reste équilibré. Malheureusement, on ne
peut pas toujours garantir qu’un ABR restera équilibré et ne va pas dégénérer pendant sa vie. C’est la raison pour laquelle
on a mis au point des structures arborescentes qui restent équilibrées, et dont les principales sont l’arbre rouge et noir et
le B-arbre, qui font l’objet des deux chapitres suivants.
J
Exercice 9.14 Discuter des possibilités d’utilisation de l’arbre binaire de recherche pour implémenter la file de prio-
rité. Argumenter les avantages et les inconvénients.
J
Exercice 9.15 Discuter des possibilités d’utilisation de l’arbre binaire de recherche pour construire un algorithme de
tri. Analyser la complexité en temps et en espace de l’algorithme.
J
Exercice 9.16 Dans le programme des pages suivantes, que se passe-t-il si on ajoute, ligne 71, la suppression du nœud
de clé 6, sous la forme :
a.supprime(n6) ;
Est-ce une erreur ou non ? Quelles seraient les solutions pour éviter ce comportement ?
J
Exercice 9.17 Modifier la méthode de parcours de l’ABR afin qu’on puisse lui passer comme paramètre la fonction
de traitement à effectuer sur chaque nœud. (C’est plutôt un exercice de programmation Java que d’algorithmique.)
10
11 /**
12 * Définition et test de l’arbre binaire de recherche.
13 *
14 * @version 12 Septembre 2000
15 * @author Michel Lemaître
16 */
17 c l a s s TestABR {
18 s t a t i c PrintWriter sortie ;
19
41 a. insère (3, n3 );
42 a. insère (12, n12 );
43 a. insère (13, n13 );
44 a. insère (10, n10 );
45 a. insère (6, n6 );
46 a. insère (7, n7 );
47 a. insère (16, n16 );
48 a. insère (20, n20 );
49 a. insère (18, n18 );
50 a. insère (23, n23 );
51
58 a. parcours_infixe ( sortie );
59
60 a. supprime ( n13 );
61 a. parcours_infixe ( sortie );
62
63 a. supprime ( n16 );
64 a. parcours_infixe ( sortie );
65
66 a. supprime ( n5 );
67 a. parcours_infixe ( sortie );
68
69 a. supprime ( n15 );
70 a. parcours_infixe ( sortie );
71
76
77 c l a s s Noeud {
78 i n t clé ;
79 Object info ;
80 Noeud gauche ;
81 Noeud droit ;
82 Noeud père ;
83
84 /** Constructeur.
85 * On met juste l’information dans le noeud,
86 * Le reste sera garni lors de l’insertion. */
87 Noeud ( Object o ) {
88 info = o;
89 }
90
91 /** Imprimeur */
92 p u b l i c String toString () {
93 r e t u r n ( clé + "=" + info + " " );
94 }
95 }
96
97
98 c l a s s ArbreBR {
99 Noeud racine ;
100
105
128 w h i l e ( x. gauche != n u l l ) {
129 x = x. gauche ;
130 }
131 r e t u r n x;
132 }
133
169
170 z. clé = c;
171 z. droit = n u l l ;
172 z. gauche = n u l l ;
173 w h i l e ( x != n u l l ) {
174 y = x;
175 x = ( z. clé < x. clé ) ? x. gauche : x. droit ;
176 }
177 z. père = y;
178 i f ( y == n u l l ) {
179 racine = z;
180 } e l s e i f ( z. clé < y. clé ) {
181 y. gauche = z;
182 } else {
183 y. droit = z;
184 }
185 }
186
Le fichier sortie_TestABR
1 3=trois
2 6=six
3 20=vgt
4 null
5 { 3=trois 5=cinq 6=six 7=sept 10=dix 12=dze 13=trz 15=qz 16=sz 18=dh 20=vgt 23=vt }
6 { 3=trois 5=cinq 6=six 7=sept 10=dix 12=dze 15=qz 16=sz 18=dh 20=vgt 23=vt }
7 { 3=trois 5=cinq 6=six 7=sept 10=dix 12=dze 15=qz 18=dh 20=vgt 23=vt }
8 { 3=trois 6=six 7=sept 10=dix 12=dze 15=qz 18=dh 20=vgt 23=vt }
9 { 3=trois 6=six 7=sept 10=dix 12=dze 18=dh 20=vgt 23=vt }
Chapitre 10
L’arbre rouge et noir, en abrégé ARN, est une variante d’arbre binaire de recherche (ABR, vu au chapitre précédent),
avec équilibrage automatique. C’est donc un ABR avec en plus une information booléenne supplémentaire par nœud : la
couleur, rouge ou noir. En contrôlant cette information de couleur dans chaque nœud, on garantit qu’aucun chemin ne
peut être deux fois plus long que n’importe quel autre, de sorte que l’arbre reste équilibré. Même si l’équilibrage n’est pas
parfait, on montre que toutes les opérations de recherche, d’insertion et de suppression sont en O(log n).
Nous ne décrirons pas précisément dans ce cours les algorithmes de l’ARN. Nous donnerons seulement sa définition et
nous citerons sa propriété essentielle d’équilibrage. Puis nous décrirons seulement le principe des opérations d’insertion
et de suppression. Elles utilisent essentiellement l’opération de rotation qui permet de maintenir l’équilibrage de l’ARN.
1. C’est un ABR. Par commodité, on remplace les pointeurs null vers des sous-arbres vides par des feuilles sans clés
(voir la figure 10.1).
2. Chaque nœud est soit rouge, soit noir.
3. Chaque feuille (voir le point 1) est noire.
4. Si un nœud est rouge, ses deux fils doivent être noirs.
5. Tous les chemins issus d’un nœud x donné et aboutissant à une feuille, contiennent le même nombre de nœuds noirs.
26
17 41
30 47
14 21
10 16 19 23 28 38
7 12 15 20 35 39
F IG . 10.1 – Un arbre rouge et noir. Les nœuds rouges sont cerclés d’un trait fin, les noirs d’un trait épais. Les feuilles
(toujours noires et sans clés) sont représentées par un petit disque noir.
51
52 CHAPITRE 10. ARBRE ROUGE ET NOIR (ARN)
J
Exercice 10.2 Montrer qu’au moins la moitié des nœuds d’un chemin d’un ARN reliant la racine à une feuille, racine
non comprise, doivent être noirs.
Voici maintemant la propriété essentielle de l’ARN : un ARN comportant n nœuds internes (n est donc le nombre d’enre-
gistrements stockés dans la structure) a une hauteur au plus égale à 2 log2 (n + 1).
J
Exercice 10.3 Démontrer la propriété essentielle de l’ARN. Aide : on appelle hauteur noire du nœud x le nombre de
nœuds noirs présents dans un chemin quelconque issu de x vers une feuille, noté hn(x). Montrer d’abord que hn(x) est
bien définie. Montrer ensuite que le sous-arbre de racine x contient au moins 2hn(x) − 1 nœuds internes (par induction sur
la hauteur de x). Utiliser enfin le résultat de l’exercice 10.2 pour établir une relation entre la hauteur de l’arbre et n, le
nombre de nœuds internes de l’ARN.
On déduit immédiatement de la propriété essentielle de l’ARN que les opérations RECHERCHE, MINIMUM, MAXIMUM,
PRÉDÉCESSEUR et SUCCESSEUR , utilisant les mêmes algorithmes que ceux de l’ABR, sont toutes en O(log n). Par contre,
les opérations INSÈRE et SUPPRIME ne peuvent pas être implémentées comme pour l’ABR puisque ce sont des opérations
qui modifient la structure de l’arbre. Il faut des algorithmes spécifiques pour ces opérations, qui préservent la définition
de l’ARN, ce que n’assurent pas les algorithmes correspondants de l’ABR. Ces nouveaux algorithmes sont basés sur la
notion de rotation.
10.3 Rotations
Une rotation est une opération locale visant à un rééquilibrage de l’arbre, tout en préservant l’ordre sur les clés. Elle
s’effectue simplement par des modifications de pointeurs (voir la figure 10.2).
y rotation droite x
x c a y
rotation gauche
a b b c
F IG . 10.2 – Rotations dans un arbre binaire de recherche. Les rotations ne changent pas la propriété fondamentale sur
l’ordre des clés : on a toujours a ≤ x ≤ b ≤ y ≤ c avant et après rotation, où a, b et c représentent n’importe quelle clé des
sous-arbres correspondants.
7 7
4 11 4 18
3 6 9 18 3 6 11 19
2 14 19 2 9 14 22
(a)
12 17 22 12 17 20
(b)
20
F IG . 10.3 – Rééquilibrage d’un arbre par une rotation. Une rotation gauche sur le nœud de clé 11 de l’arbre (a) conduit à
un arbre (b) mieux équilibré : la hauteur de l’arbre est passée de 5 à 4.
haut, restaurent l’ARN. Comme ces réajustements n’affectent qu’un chemin de l’arbre, l’insertion est en temps O(log n),
comme souhaité.
La suppression est nettement plus complexe. Elle est aussi en O(log n).
10.5 Exercices
J
Exercice 10.4 Supposons que la racine d’un ARN soit rouge. Si on change cette couleur en noir, l’arbre reste-t-il un
ARN ?
J
Exercice 10.5 Montrer qu’un ABR quelconque de n nœuds peut être transformé en n’importe quel autre ABR de
mêmes clés en O(n) rotations.
Chapitre 11
B-Arbre
Le B-arbre est un arbre de recherche équilibré. Il généralise l’arbre binaire de recherche (ABR) en ce sens qu’il possède
un facteur de branchement (nombre de fils possibles d’un nœud) qui peut être très grand : en pratique plusieurs milliers.
C’est une structure de donnée destinée à implémenter en mémoire secondaire un dictionnaire avec ordre sur les clés. Par
mémoire secondaire, nous entendons un support tel qu’un disque dur, différent de la mémoire centrale de l’ordinateur.
Chaque nœud du B-arbre est contenu typiquement dans une page de disque, c’est-à-dire une quantité d’information qui
transite en une seule opération d’écriture ou de lecture entre la mémoire secondaire et la mémoire principale. C’est une
structure de donnée destinée à la gestion efficace d’énormes masses de données. La figure 11.1 donne une exemple de
B-arbre.
D H Q T X
BC FG JKL NP RS VW YZ
F IG . 11.1 – Un B-arbre. On n’a représenté que les clés. Dans ce B-arbre les clés sont des lettres.
11.1 Définition
La définition tient en 5 points.
54
11.2. PROPRIÉTÉ ESSENTIELLE DU B-ARBRE 55
Le point 3 de la définition implique que l’on admet des clés non uniques. Cette définition large permet de traiter l’implé-
mentation de variantes du dictionnaire dans lesquelles ce serait le cas.
Le point 5 implique que tout nœud interne possède au moins t fils, et au plus 2t fils.
Le B-arbre le plus simple est tel que t = 2. Tout nœud interne possède alors 2, 3 ou 4 fils.
J
Exercice 11.1 Combien d’enregistrements contient un B-arbre de hauteur 2 et stockant 1000 enregistrements par
nœud ? Combien faut-il d’accès disque pour accéder à n’importe quel enregistrement de ce B-arbre ?
Pour analyser les performances des opérations sur B-arbre, on ne compte plus le temps en nombre d’instructions machine,
mais en nombre d’accès disque (c’est à dire le nombre d’accès à un nœud) car le temps d’accès disque devient largement
prépondérant devant le temps CPU. Le nœud racine réside généralement en mémoire centrale et n’a pas besoin d’être relu
sans cesse.
P Q R S T U V P Q R T U V
1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
avant après
F IG . 11.2 – B-arbre : découpage d’un nœud complet. Ce B-arbre possède un degré minimum t = 4. On peut donc
entreposer de 3 à 7 clés par nœud. La clé médiane S remonte dans l’arbre.
A D F H L N P A D F L N P
1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
avant après
F IG . 11.3 – Découpage de la racine complète d’un B-arbre (t = 4). Une nouvelle racine est créée. La hauteur de l’arbre
augmente de 1.
(a) G M P X
A C D E J K N O R S T U V Y Z
(b) G M P X
A B C D E J K N O R S T U V Y Z
(c) G M P T X
A B C D E J K N O Q R S U V Y Z
P
(d)
G M T X
A B C D E J K L N O Q R S U V Y Z
(e) P
C G M T X
A B D E F J K L N O Q R S U V Y Z
F IG . 11.4 – Insertions successives dans un B-arbre. La flèche indique la nouvelle clé insérée. Ce B-arbre possède un degré
minimum t = 3. On peut donc entreposer de 2 à 5 clés par nœud.
Chapitre 12
Tas
Le tas est une bonne structure pour l’implémentation de la file de priorité (définie page 27), c’est-à-dire une collection de
liaisons clé-enregistrement. On n’exige pas que les clés soient uniques. On ne pourra pas, comme dans un dictionnaire,
retrouver n’importe quel enregistrement par sa clé, mais on aura seulement accès à un enregistrement de plus grande clé.
De même pour la suppression d’un enregistrement : seul un enregistrement de clé maximale pourra être supprimé. On
parle alors d’opération d’extraction. Par contre, il n’y a pas de restrictions sur l’insertion.
L’accès à un enregistrement de clé maximale est en temps constant, tandis que les opérations d’insertion et d’extraction
se font en O(log n), où n est la taille du tas.
La structure de tas est supportée par un tableau. Les enregistrements1 sont placés dans le tableau, à des indices déterminés
en fonction de la valeur des clés. Les relations entre ces indices sont telles que le tas est implicitement un arbre enraciné.
Dans la suite de ce chapitre, pour la commodité de l’exposé, nous allons faire comme si nos enregistrements étaient réduits
à leur clés, et ces clés seront des entiers. Cela va simplifier les dessins et les algorithmes, sans rien changer aux principes.
Si on doit implémenter un vrai tas, il convient de restaurer la gestion des liaisons clé-enregistrement, ce qui est très simple.
F IG . 12.1 – Un tas. À gauche, la vue du tas sous forme d’arborescence, à droite le tableau qui le supporte.
Les tas est un arbre binaire tabulé, presque complet, et respectant une contrainte d’ordre particulière. Détaillons
chacun de ces points.
– arbre binaire tabulé : le tas est supporté par un tableau t. Chaque indice2 du tableau correspond à l’indice d’un nœud
dans un arbre binaire. Les liens père-fils possibles de cette arborescence sont implicitement représentés par les trois
fonctions suivantes, où i est un indice dans le tableau. père(i) donne l’indice du père de i, gauche(i) et droit(i)
donnent les indices des fils gauche et droit de i :
– père(i) = i / 2 (division entière)
– gauche(i) = 2 i
– droit(i) = 2 i + 1
Voir la figure 12.1. Par exemple, le père du nœud d’indice 5 est le nœud d’indice 2, le fils gauche du nœud d’indice 3
est le nœud d’indice 6, le fils droit du nœud d’indice 3 est le nœud d’indice 7. Par construction du système d’indices, la
1 Plus exactement des pointeurs vers les enregistrements.
2 Attention, mes indices commencent à 1 !
58
12.2. RESTAURATION DE LA CONTRAINTE D’ORDRE 59
Exercice 12.1 Montrer que la hauteur d’un tas est Θ(log n) où n est la taille du tas.
J
F IG . 12.2 – Effets de la procédure entasser(2), qui restaure la contrainte d’ordre à partir du nœud d’indice 2. (a) : un
tas dont le nœud d’indice 2 et de clé 4 marqué d’une flèche, enfreint la contrainte d’ordre avec ses deux fils. La procédure
entasser(2), appelée sur ce nœud, va en échanger la clé avec le nœud d’indice 4. (b) : ce nœud, marqué d’un flèche,
enfreint également la contrainte avec son fils droit ; il est donc échangé avec le nœud d’indice 9. (c) : l’entassement s’est
propagé jusqu’à une feuille, la contrainte d’ordre est complètement restaurée.
La procédure entasser(i) ci-dessous a un rôle central dans la manipulation des tas. Elle sert à restaurer la contrainte
d’ordre dans la sous-arborescence enracinée en i, lorsque t[i] est inférieur à l’un de ses fils. On l’emploie sous les
hypothèses suivantes : les sous-arborescences enracinées en gauche(i) et droit(i) respectent la contrainte d’ordre. Par
exemple, dans la figure 12.2, le nœud d’indice 2 et de clé 4 enfreint la contrainte d’ordre avec son fils gauche, mais les
sous-arborescences enracinées en ses deux fils (indices 4 et 5) respectent la contrainte.
1 void entasser ( i n t i ) {
2 i n t max = i;
3 i n t g = gauche (i );
4 i n t d = droit (i );
5
1
4
2 3
1 2 3 4 5 6 7 8 9 10
1 3
4 1 3 2 16 9 10 14 8 7
4 5 6 7
2 16 9 10
8 9 10
14 8 7
F IG . 12.3 – Début de la construction d’un tas. La boucle d’appels à entasser va commencer au nœud d’indice 5, marqué
d’une flèche.
La procédure construire ci-dessous restaure complètement la contrainte d’ordre du tas dans le tableau t contenant des
clés quelconques, sans faire d’hypothèse préalable.
1 void construire () {
2 taille = longueur ;
3 f o r ( i n t i = taille / 2; i >= 1; i -= 1) {
4 entasser (i );
5 }
6 }
L’idée est simplement d’appeler successivement la procédure entasser sur chacun des nœuds, en commençant par les
indices les plus élévés. De cette façon les hypothèses de fonctionnement pour entasser sont vérifiées à chaque appel.
On peut améliorer cette idée en remarquant que les feuilles n’ont pas besoin d’être entassées. La boucle for peut donc
commencer à taille / 2.
J
Exercice 12.3 Montrer que les nœuds d’indices taille/2 + 1 (division entière) à taille d’un tas sont des feuilles.
J
Exercice 12.4 Montrer que construire est en temps O(n log n), où n est la taille du tas.
Plus difficile : montrer que construire est en temps O(n).
tas, et on entasse la racine. Ainsi, on restaure les propriétés du tas, et on est sûr que les deux sous-arborescences de la
racine vérifient l’hypothèse de l’appel à entasser ! Malin, non ?
1 i n t extraire_max () {
2 i f ( taille < 1) { // extraction d’un tas vide :
3 r e t u r n Integer . MIN_VALUE ; // renvoie une valeur conventionnelle
4 } else {
5 i n t max = t [1];
6 t [1] = t[ taille ];
7 taille -= 1;
8 entasser (1);
9 r e t u r n max ;
10 }
11 }
J
Exercice 12.5 Montrer que extraire_max est en temps O(log n), où n est la taille du tas.
11 }
Le tableau à trier (on suppose ici que c’est justement le tableau t support du tas, complètement rempli) est soumis à la
procédure construire, ce qui établit la contrainte d’ordre. t[1] étant le maximum, il doit prendre la dernière place. On
l’y met donc, par échange avec t[longueur]. On diminue la taille du tas de 1, ce qui «exclut» du tas la clé maximale
que l’on vient de bien placer, puis on rétablit la contrainte d’ordre à partir de la racine, de façon analogue à la procédure
extraire_max. Et ainsi de suite.
J
Exercice 12.8 Montrer que le tri du tas est en temps O(n log n), où n est la longueur du tableau à trier.
Exercice 12.9 Montrer que le tri du tas est en temps Ω(n log n), où n est la longueur du tableau à trier.
J
4 /*
5 * TestTas.java
6 *
7 * SupAéro -- Cours Structures de Données et Algorithmes
8 */
9
12 /**
13 * Structure de tas et tri du tas
14 *
15 * @version 21 Août 2000
16 * @author Michel Lemaître
17 */
18 c l a s s TestTas {
19 s t a t i c PrintWriter sortie ;
20
46 i n t n = 20;
47 i n t [] tab = new i n t [n + 1]; // mes tableaux commencent à 1
12.7. PROGRAMME JAVA 63
48
57 verif ( tab );
58 sortie . close ();
59 }
60
61 s t a t i c void remplir ( i n t [] t , i n t n , i n t k ) {
62 f o r ( i n t i = 1; i <= n ; i += 1) {
63 t[i ] = aleatoire (k );
64 }
65 }
66
72 s t a t i c void imprime ( i n t [] t ) {
73 sortie . println ();
74 f o r ( i n t i = 1; i <= t. length - 1; i += 1) {
75 sortie . println ("t [" + i + "] = " + t[i ]);
76 }
77 sortie . println ();
78 }
79
80 s t a t i c void verif ( i n t [] t ) {
81 f o r ( i n t i = 1; i <= t. length - 2; i += 1) {
82 i f ( t[i ] > t[i +1]) {
83 sortie . println (" Erreur quelquepart " );
84 System . exit (1);
85 }
86 }
87 }
88 }
89
90
91 c l a s s Tas {
92 i n t [] t ; // le tableau support du tas
93 i n t longueur ; // la longueur du tableau support
94 i n t taille ; // la taille du tas
95
112 s t a t i c i n t pere ( i n t i ) {
113 r e t u r n i / 2;
114 }
115
116 s t a t i c i n t gauche ( i n t i ) {
117 r e t u r n 2 * i;
118 }
119
120 s t a t i c i n t droit ( i n t i ) {
121 r e t u r n 2 * i + 1;
122 }
123
144 i n t maximum () {
145 r e t u r n t [1];
146 }
147
148 i n t extraire_max () {
149 i f ( taille < 1) { // extraction d’un tas vide :
150 r e t u r n Integer . MIN_VALUE ; // valeur conventionnelle
151 } else {
152 i n t max = t [1];
153 t [1] = t[ taille ];
154 taille -= 1;
155 entasser (1);
156 r e t u r n max ;
157 }
158 }
159
Le fichier sortie_TestTas
1 insertion : 8
2 insertion : 3
3 insertion : 16
4 le tas contient : 16 3 8
5 extraction : 16
6 le tas contient : 8 3
7 insertion : 5
8 insertion : 8
9 insertion : 9
10 le tas contient : 9 8 5 3 8
11 extraction max : 9
12 le tas contient : 8 8 5 3
13 extraction max : 8
14 le tas contient : 8 3 5
15 extraction max : 8
16 le tas contient : 5 3
17 extraction max : 5
18 le tas contient : 3
19 extraction max : 3
20 le tas est vide.
21 extraction max : -2147483648
22 le tas est vide.
23
24 t[1] = 8
25 t[2] = 10
26 t[3] = 10
27 t[4] = 1
28 t[5] = 9
66 CHAPITRE 12. TAS
29 t[6] = 10
30 t[7] = 1
31 t[8] = 7
32 t[9] = 7
33 t[10] = 3
34 t[11] = 7
35 t[12] = 9
36 t[13] = 8
37 t[14] = 5
38 t[15] = 6
39 t[16] = 1
40 t[17] = 8
41 t[18] = 1
42 t[19] = 10
43 t[20] = 5
44
45
46 t[1] = 1
47 t[2] = 1
48 t[3] = 1
49 t[4] = 1
50 t[5] = 3
51 t[6] = 5
52 t[7] = 5
53 t[8] = 6
54 t[9] = 7
55 t[10] = 7
56 t[11] = 7
57 t[12] = 8
58 t[13] = 8
59 t[14] = 8
60 t[15] = 9
61 t[16] = 9
62 t[17] = 10
63 t[18] = 10
64 t[19] = 10
65 t[20] = 10
Chapitre 13
Tris
13.1 Le problème
Dans toute sa généralité, le problème du tri se pose ainsi :
– données : un sous-ensemble de n objets a1 , a2 , . . . , an , tirés d’un ensemble muni d’une relation d’ordre total notée ;
paramètres : le sous-ensemble à trier.
– solution cherchée : trouver une permutation σ : {1, . . . , n} → {1, . . . , n} telle que ∀i, j : aσ(i) aσ( j) .
Nous donnerons plusieurs algorithmes de tri. Nous étudierons et comparerons leurs performances, dans le cas particulier
suivant :
– le sous-ensemble à trier est rangé dans un tableau,
– les éléments du tableau sont des nombres entiers (on trie des nombres entiers).
Le sous-ensemble à trier pourrait être rangé dans une autre structure, une liste chaînée par exemple. Cependant, on préfère
en général trier des objets rangés au préalable dans un tableau, pour profiter de l’accès direct aux éléments, et obtenir ainsi
de bonnes performances.
Quand au choix de trier des entiers, il s’agit ici d’une facilité pour l’écriture des algorithmes. Le tri de nombres n’est pas
spécialement utile en soi. Il est bien plus utile de trier des enregistrements, en comparant leurs clés. Toutefois, que ce
soient des entiers ou des enregistrements, le principe de l’algorithme demeure inchangé (voir l’exercice 13.10).
On dira qu’un algorithme trie sur place lorsqu’il ne consomme pas de mémoire supplémentaire, sauf une place mémoire
de taille constante (ne dépendant pas de la taille du tableau à trier).
Dans tout ce chapitre, le paramètre n dénotera le nombre d’entiers à trier.
67
68 CHAPITRE 13. TRIS
a<b<c
a<c<b
b<a<c
b<c<a
c<a<b
c<b<a
a<b
OUI NON
a<b<c b<a<c
a<c<b b<c<a
c<a<b c<b<a
a<c b<c
OUI NON OUI NON
permutations possibles. Après la première comparaison a < b (test qui apparait dans l’ovale), il acquiert une information
nouvelle, qui lui permet d’écarter 3 permutations. À chaque nouvelle comparaison, il écarte au mieux la moitié de celles
qui restent en lisse. Et ainsi de suite jusqu’à «reconnaître» la permutation d’entrée.
Avec c comparaisons successives, on peut distinguer au plus 2c situations (feuilles de l’arbre de décision). Or nous avons
n! situations à distinguer. Il faut donc que c soit tel que n! ≤ 2c .
Pour résoudre cette équation en c, nous remarquons que n! est le produit d’au moins n/2 facteurs valant chacun au moins
n/2. Il s’en suit donc :
(n/2)(n/2) ≤ n! ≤ 2c
en passant aux logarithmes :
(n/2) log2 (n/2) ≤ c
soit
(n/2) log2 n − n/2 ≤ c
c’est-à-dire c ∈ Ω(n log n).
En résumé, nous avons établi que tout algorithme de tri qui n’utilise que des comparaisons entre éléments pour trier n
éléments nécessite Ω(n log n) comparaisons.
6 while ( true ) {
7 do j -= 1; w h i l e ( tab [j ] > x ); // sort dès que tab[j] <= x
8 do i += 1; w h i l e ( tab [i ] < x ); // sort dès que tab[i] >= x
9 i f ( i < j ) { // échange tab[i] et tab[j]
10 i n t temp = tab [i ];
11 tab [i ] = tab [j ];
12 tab [j ] = temp ;
13 } else {
14 r e t u r n j;
15 }
1 Il existe une version utilisant moins de place, mais elle est plutôt complexe (voir [Sedgewick]).
70 CHAPITRE 13. TRIS
16 }
17 }
On commence, ligne 2, par choisir un pivot, c’est-à-dire un élément du sous-tableau dont la valeur servira de frontière à la
partition. Dans cette version, on choisit comme pivot le premier élément du sous-tableau, tab[p]. On positionne ensuite
(lignes 3 et 4) deux indices i et j de part et d’autre de la région à trier. Au cours du partitionnement, ces deux indices vont
progresser l’un vers l’autre jusqu’à se rejoindre. Ceci est réalisé par la boucle while lignes 6 à 16, dont on sortira lorsque
i ≥ j (par le return). Dans le corps de cette boucle, on commence par décrémenter j jusqu’à obtenir tab[j] ≤ x, puis on
incrémente i jusqu’à obtenir tab[i] ≥ x. Les nombres à l’extérieur de i et j sont correctement partitionnés par la valeur
du pivot. Si i < j, on échange tab[i] et tab[j] (lignes 10-12), ce qui permet de progresser dans le partionnement. Si i
≥ j, sous-tableau est entièrement partitionné, et j délimite la frontière : tous les nombres de tab[p..j] sont inférieurs
ou égaux au pivot, et tous les éléments de tab[j+1..r] sont supérieurs ou égaux au pivot. L’indice j est donc l’indice
renvoyé par la procédure. La figure 13.2 détaille le fonctionnement de l’algorithme de partitionnement sur un exemple.
pivot x = 5
p r p r
5 3 2 6 4 1 3 7 5 3 2 6 4 1 3 7
i j i j
(a) (a’)
p r p r
3 3 2 6 4 1 5 7 3 3 2 6 4 1 5 7
i j i j
(b) (b’)
p r p q r
3 3 2 1 4 6 5 7 3 3 2 1 4 6 5 7
i j j i
(c) (c’)
F IG . 13.2 – Fonctionnement de la procédure partitionner. Les nombres imprimés en gras sont correctement position-
nés. En (a) : le sous-tableau de départ, avec la position des indices p, r, i et j. En (a’) : état du sous-tableau après la
première séquence de déplacements des indices i et j ; on a tab[j] ≤ x ≤ tab[i]. En (b) : état du sous-tableau juste
après le premier échange. En (b’) : après la seconde séquence de déplacement des indices i et j. En (c) : après le second
échange. En (c’) : après la troisième et dernière séquence de déplacement des indices i et j. La double barre verticale
indique le partitionnement final. Tous les nombres à gauche de la barre sont inférieurs ou égaux à 5 et tous ceux de droite
sont supérieurs ou égaux à 5.
L’apparente simplicité de l’algorithme de partitionnement présenté cache quelques subtilités, sujets des exercices qui
suivent.
J
Exercice 13.1 Montrer que dans l’algorithme de partitionnement ci-dessus, i et j ne sortent jamais des limites du
sous-tableau.
J
Exercice 13.2 Que se passe-t-il dans l’algorithme de partitionnement ci-dessus si on prend tab[r] comme pivot (à
la place de tab[p]) et que tab[r] est le plus grand élément du sous-tableau ?
Exercice 13.3 Dans l’algorithme de partitionnement, il semble que les tests des lignes 7 et 8 pourraient être tab[j] ≥
J
x et tab[i] ≤ x au lieu d’une inégalité stricte, ce qui permettrait de pousser plus loin chaque séquence de déplacement,
et donc d’échanger moins. Donner deux raisons pour lesquelles on préfère conserver des inégalités strictes et donc
effectuer apparemment plus d’échanges que nécessaire (la première raison est facile à trouver ; la seconde est beaucoup
plus subtile et a rapport avec l’analyse de complexité du tri dans le cas ou les nombres à trier sont presque tous égaux).
L’algorithme de base du tri rapide a été inventé en 1960 par Hoare. La version présentée ici est celle de [Cormen]. Il existe
d’autres variantes du tri rapide en peu plus efficaces et surtout résistant mieux à des entrées «pathologiques», notamment
des nombres déjà ou presque triés. Voir [Sedgewick] et [Weiss].
Exercice 13.4 Montrer que le temps de partitionnement est Θ(n), où n est la longueur du sous-tableau à partitionner.
J
13.7. TRI PAR DÉNOMBREMENT 71
La forme récursive de l’algorithme nous permet d’écrire le temps T (n) nécessaire pour trier n nombres sous la récurrence :
où α est la proportion de nombres dans la partition de gauche du premier partitionnement, et Θ(n) représente le temps du
partitionnement lui-même. On ne peut résoudre directement cette équation car le facteur α est un résultat propre à chaque
partitionnement. Cependant, nous pouvons analyser les pire et meilleur cas.
Le pire cas est atteint lorsque la partition est toujours complètement déséquilibrée, c’est-à-dire (1, n − 1).
J
Exercice 13.5 Réécrire la récurrence ci-dessus dans le cas d’une partition complètement déséquilibrée. Montrer que
si cela arrive systématiquement, alors T (n) = Θ(n2 ).
Examinons ce qui se passe lorsque chaque partition est systématiquement parfaitement équilibrée, c’est-à-dire α = 1/2.
La récurrence ci-dessus devient :
4 f o r ( i = 1; i <= k ; i += 1) {
5 c[i ] = 0;
72 CHAPITRE 13. TRIS
6 }
7 f o r ( j = 1; j <= n ; j += 1) {
8 c[t[j ]] = c[t[j ]] + 1;
9 }
10 f o r ( i = 2; i <= k ; i += 1) {
11 c[i ] += c[i -1];
12 }
13 f o r ( j = n ; j >= 1; j -= 1) {
14 b[c[t[j ]]] = t[j ];
15 c[t[j ]] -= 1;
16 }
17 f o r ( j = 1; j <= n ; j += 1) {
18 t[j ] = b[j ];
19 }
20 }
t est le tableau à trier. b est le tableau dans lequel on trouvera le résultat, donc de même dimension que t. c est un autre
tableau auxiliaire, qui doit pouvoir être indexé de 1 à k.
La boucle des lignes 4 à 6 initialise les éléments du tableau c à 0. La boucle des lignes 7 à 9 «compte» chaque clé utilisée :
à la fin de cette boucle, c[i] contient le nombre d’enregistrements dont la clé est égale à i. Après la boucle des lignes
10 à 11, c[i] contient le nombre d’enregistrements dont la clé est inférieure ou égale à i. La boucle des lignes 13 à
16 remplit le tableau de sortie b en y plaçant tour à tour chaque élément du tableau t. Ligne 14, c[t[j]] est la bonne
position de t[j] dans b. La ligne suivante (15) traite le cas des clés égales : le prochain élément du tableau de départ qui
a aussi cette clé t[j] sera placé juste avant dans b. La dernière boucle est une facilité : elle recopie b dans t.
Exercice 13.8 Montrer que le temps d’exécution du tri par dénombrement est Θ(n + k).
J
J
Exercice 13.9 Objection d’un élève : la détermination du maximum et du minimum d’un tableau de nombres demande
un temps O(n). Il suffit donc de réserver un tableau assez grand et on pourrait trier n’importe quelle séquence de nombres
en O(n) avec un tri par dénombrement, ce qui rend obsolètes la plupart des tris. Réfutez cette objection.
1 /*
2 * TestTris.java
3 *
4 * SupAéro -- Cours Structures de Données et Algorithmes
5 */
6
7 import java . io .* ;
8
9 /**
10 * Cette classe rassemble et compare des algorithmes de tri :
11 * tri par insertion, tri bulle, tri par sélection,
12 * tri rapide (quicksort), tri par dénombrement.
13 *
14 * @version 16 Août 2000
15 * @author Michel Lemaître
16 */
17 c l a s s TestTris {
18 s t a t i c i n t nmax = 5000000; // nombre max de nombres à trier
19 s t a t i c i n t k = 1000000; // on trie des nombres de l’intervalle [1 .. k]
20
39 i f ( n < 100000) {
40 remplir (t , n ); // remplit le tableau à trier de nbres aléatoires
41 init (" insertion " );
42 tri_insertion (t , n );
43 verif (t , n );
44 }
45
46 i f ( n < 50000) {
47 remplir (t , n );
48 init (" bulle ");
49 tri_bulle (t , n );
50 verif (t , n );
51 }
52
53 i f ( n < 100000) {
54 remplir (t , n );
55 init (" selection " );
56 tri_selection (t , n );
57 verif (t , n );
58 }
59
60 remplir (t , n );
61 init (" rapide ");
62 tri_rapide (t , 1, n );
63 verif (t , n );
64
74 CHAPITRE 13. TRIS
65 remplir (t , n );
66 init (" denombrem " );
67 tri_denombrement (t , b , c , n );
68 verif (t , n );
69 }
70 sortie . close ();
71 }
72
73 s t a t i c void tri_insertion ( i n t [] t , i n t n ) {
74 i n t i;
75
76 f o r ( i n t j = 2; j <= n ; j += 1) {
77 i n t clé = t[j ];
78 f o r ( i = j - 1; ( i > 0) && ( t[i ] > clé ); i -= 1) {
79 t[i + 1] = t[i ];
80 }
81 t[i + 1] = clé ;
82 }
83 }
84
85 s t a t i c void tri_bulle ( i n t [] t , i n t n ) {
86 f o r ( i n t i = 1; i <= n - 1; i += 1) {
87 f o r ( i n t j = n ; j >= i + 1; j -= 1) {
88 i f ( t[j - 1] > t[j ]) {
89 i n t temp = t[j - 1];
90 t[j - 1] = t[j ];
91 t[j ] = temp ;
92 }
93 }
94 }
95 }
96
97 s t a t i c void tri_selection ( i n t [] t , i n t n ) {
98 f o r ( i n t i = 1; i <= n - 1; i += 1) {
99 i n t min = t[i ];
100 i n t p = i;
101 f o r ( i n t j = i + 1; j <= n ; j += 1) {
102 i f ( t[j ] < min ) {
103 min = t[j ];
104 p = j;
105 }
106 }
107 t[p ] = t[i ];
108 t[i ] = min ;
109 }
110 }
111
141 f o r ( i = 1; i <= k ; i += 1) {
142 c[i ] = 0;
143 }
144 f o r ( j = 1; j <= n ; j += 1) {
145 c[t[j ]] = c[t[j ]] + 1;
146 }
147 f o r ( i = 2; i <= k ; i += 1) {
148 c[i ] += c[i -1];
149 }
150 f o r ( j = n ; j >= 1; j -= 1) {
151 b[c[t[j ]]] = t[j ];
152 c[t[j ]] -= 1;
153 }
154 f o r ( j = 1; j <= n ; j += 1) {
155 t[j ] = b[j ];
156 }
157 }
158
193 }
194 }
195 }
196 }
Le fichier sortie_TestTris
(exécuté sur PC Pentium II 235 MH, Java 2 SDK SE 1.3)
1
2 n=1024
3 insertion : 0.0s
4 bulle : 0.11s
5 selection : 0.06s
6 rapide : 0.0s
7 denombrem : 0.16s
8
9 n=2048
10 insertion : 0.11s
11 bulle : 0.28s
12 selection : 0.05s
13 rapide : 0.0s
14 denombrem : 0.17s
15
16 n=4096
17 insertion : 0.33s
18 bulle : 0.88s
19 selection : 0.28s
20 rapide : 0.0s
21 denombrem : 0.16s
22
23 n=8192
24 insertion : 1.21s
25 bulle : 3.52s
26 selection : 0.93s
27 rapide : 0.0s
28 denombrem : 0.16s
29
30 n=16384
31 insertion : 4.67s
32 bulle : 14.01s
33 selection : 3.52s
34 rapide : 0.0s
35 denombrem : 0.16s
36
37 n=32768
38 insertion : 18.73s
39 bulle : 56.18s
40 selection : 14.01s
41 rapide : 0.11s
42 denombrem : 0.22s
43
44 n=65536
45 insertion : 74.75s
46 selection : 56.19s
47 rapide : 0.17s
48 denombrem : 0.22s
49
50 n=131072
51 rapide : 0.27s
52 denombrem : 0.32s
53
13.9. PROGRAMME JAVA 77
54 n=262144
55 rapide : 0.61s
56 denombrem : 0.44s
57
58 n=524288
59 rapide : 1.15s
60 denombrem : 0.71s
61
62 n=1048576
63 rapide : 2.36s
64 denombrem : 1.37s
65
66 n=2097152
67 rapide : 4.99s
68 denombrem : 2.53s
69
70 n=4194304
71 rapide : 10.33s
72 denombrem : 4.94s
Chapitre 14
La bibliothèque nommée collections framework de Java, regroupe un ensemble de structures de données abstraites et
concrètes. Nous donnons dans ce chapitre l’essentiel de ce qu’il faut connaître de cette bibliothèque (Java 2 SDK,
Standard Edition Version 1.3). La documentation complète est accessible publiquement sur le site de Sun à l’adresse
http://java.sun.com/j2se/1.3/docs. Nous encourageons fortement l’utilisation de cette bibliothèque : elle est très
bien conçue, homogène, gratuite, portable, parfaitement intégrée à Java, et robuste1 . Pour utiliser la bibliothèque, il est
très agréable d’avoir un navigateur ouvert sur la documentation en ligne de Sun, remarquable et facile à utiliser. Malgré
tout, ce chapitre peut vous aider au début pour aller à l’essentiel, ou comme aide-mémoire.
Cette bibliothèque définit deux interfaces principales : la Collection et le Map. Au sens de Java, une interface corres-
pond d’assez près à ce que nous avons appelé «structure de données abstraite» (voir page 25).
– une Collection est simplement une collection d’objets (avec duplications possibles).
– un Map représente un ensemble d’associations entre objets (un ensemble de liens clé-valeur), une fonction au sens
mathématique.
Bien sûr, cette bibliothèque procure un floppée de structures de données concrètes implémentant ces interfaces.
Toutes les méthodes de ce chapitre font partie du package java.util2 . Tout programme qui les utilise doit donc com-
mencer par l’incantation «import java.util.*».
Remarque : les clés et les valeurs stockés dans un Map, ainsi que les objets d’une Collection ne peuvent être directement
des types primitifs (int, double, boolean, char). Il faut utiliser des wrappers : classes Integer, Double, Boolean,
Character.
La Collection est une interface qui représente une collection d’objets, de la façon la plus générale possible.
Les principales méthodes offertes par la Collection sont :
– boolean isEmpty() : teste si la Collection est vide.
– boolean contains(Object o) : teste si la Collection contient l’objet spécifié.
– int size() : renvoie le nombre d’éléments de la Collection.
– boolean add(Object o) : ajoute l’objet spécifié à la Collection.
– boolean remove(Object o) : ôte de la Collection l’objet spécifié, s’il y est.
– void clear() : ôte tous les objets de la Collection.
– Iterator iterator() : renvoie un itérateur, objet particulier qui permettra d’itérer sur les éléments de la Collection
(voir page 82).
Les opérations add et remove, qui modifient la Collection, renvoient true si la Collection a été modifiée, et false
sinon.
1 Conçue par une armée de professionnels, utilisée et testée par des milliers de programmeurs . . .
2 Sauf Comparable, qui fait partie du package java.lang, mais qui est importé automatiquement.
78
14.1. STRUCTURES DE DONNÉES ABSTRAITES 79
Les principales implémentations de Collection sont celles de Set et de List, c’est-à-dire HashSet, TreeSet, ArrayList
et LinkedList.
Set
Cet interface, qui étend l’interface Collection, représente un groupe non ordonné d’objets, et ne contenant pas d’objets
dupliqués. L’interface Set offre les mêmes méthodes que son super-interface Collection. Il empêche seulement add de
provoquer des duplications.
SortedSet
SortedSet est une sous-interface de Set garantissant un ordre sur les éléments du Set. Les éléments doivent donc être
des objets comparables. L’itérateur renvoyé par la méthode iterator énumèrera les éléments du Set dans l’ordre prévu.
En plus des méthodes de sa super-interface, le SortedSet procure les méthodes suivantes :
– Object first() : renvoie le plus petit élément du SortedSet.
– Object last() : renvoie le plus grand élément du SortedSet.
L’interface SortedSet est implémenté par la classe TreeSet.
List
Cette interface, qui étend l’interface Collection, représente un groupe ordonné3 d’objets (une séquence), pouvant conte-
nir des duplications d’objets. Le terme List ne doit pas s’entendre au sens concret de liste chaînée, mais au sens abstrait
de séquence ordonnée d’éléments. Chaque élément d’une List possède un index, ou position (entier positif ou nul).
L’index du premier élément d’une liste est 0. Les éléments peuvent être manipulés par leur index.
L’interface List offre les mêmes méthodes que son super-interface Collection, mais offre de plus des méthodes de
manipulation avec index. Parmi ces méthodes supplémentaires, les plus utiles sont :
– Object get(int index) : renvoie l’objet d’index spécifié. Une erreur se produit si l’index est négatif ou supérieur ou
égal à la taille (nombre d’éléments) de la List.
– Object set(int index, Object o) : met l’objet dans la List à l’index spécifié. L’ancien contenu est écrasé. Une
erreur se produit si l’index est négatif ou supérieur ou égal à la taille (nombre d’éléments) de la List.
– void add(int index, Object o) : une version de add qui ajoute l’objet à la List, à l’index spécifié. Les objets
qui suivent voient leurs index décalés. Une erreur se produit si l’index est négatif ou supérieur à la taille (nombre
d’éléments) de la List (on a le droit d’ajouter juste après le dernier élément de la liste).
La version de add() sans argument index ajoute l’objet à la fin de la liste.
– boolean remove(int index) : une version de remove qui ôte à la List l’objet dont l’index est spécifié. Une erreur
se produit si l’index est négatif ou supérieur ou égal à la taille (nombre d’éléments) de la List.
La List offre un itérateur spécifique ListIterator, plus puissant que celui de la Collection, ainsi que la possibilité
de manipuler des sous-listes (se reporter à la documentation complète).
Le Map est une interface qui représente un ensemble d’associations (des paires) entre des objets clés et des objets valeurs.
Le Map correspond à ce que nous avons appelé dictionnaire (voir la définition page 25), mais offre des services bien plus
complets et sophistiqués.
L’ensemble des clés d’un Map doit être un véritable ensemble : chaque clé d’un Map doit être unique. La collection des
valeurs n’est pas soumise à cette restriction.
Les principales opérations4 offertes par le Map sont :
3 Le groupe d’objet est ordonné, mais les objets ne sont pas nécessairement comparables entre eux.
4 En Java on parle de méthodes.
80 CHAPITRE 14. LA BIBLIOTHÈQUE COLLECTIONS FRAMEWORK DE JAVA
SortedMap
SortedMap est une sous-interface de Map garantissant un ordre sur les clés. Les clés doivent donc être des objets compa-
rables. Les collections renvoyées par les méthodes keySet, values et entrySet seront itérées dans l’ordre des clés. En
plus des méthodes de sa super-interface, le SortedMap procure les méthodes suivantes :
– Object firstKey() : renvoie la plus petite clé du SortedMap.
– Object lastKey() : renvoie la plus grande clé du SortedMap.
L’interface SortedMap est implémenté par TreeMap.
La classe HashSet implémente l’interface Set par une table de hachage (voir page 36), et donc les méthodes add,
remove et contains sont très efficaces (temps quasiment constant). En plus des méthodes de l’interface qu’il implé-
mente, HashSet procure les contructeurs suivants :
– HashSet() : le constructeur par défaut.
– HashSet(int capacité) : un constructeur permettant de spécifier le nombre approximatif d’éléments prévus, lors-
qu’on le connait à l’avance.
– HashSet(Collection c) : construit un HashSet à partir d’une Collection.
TreeSet
La classe TreeSet implémente l’interface SortedSet par un arbre rouge et noir (voir page 51), et donc les méthodes
add, remove et contains sont relativement efficaces (temps logarithmique). Les éléments du TreeSet doivent être com-
parables : soit ils implémentent naturellement l’interface Comparable (voir page 82), soit on a fourni au constructeur de
TreeSet un Comparator (voir page 82). En plus des méthodes de l’interface qu’il implémente, un TreeSet procure les
constructeurs suivants :
– TreeSet() : le constructeur par défaut.
– TreeSet(Comparator c) : le constructeur qui permet de spécifier un Comparator définissant l’ordre sur les éléments.
– TreeSet(Collection c) : construit un TreeSet à partir d’une Collection.
14.3. AUTRES INTERFACES ET CLASSES UTILES 81
ArrayList
La classe ArrayList implémente l’interface List par un tableau (ce tableau est reconstruit dynamiquement si la List
grandit ou rapetisse beaucoup) et donc les méthodes get et set sont très efficaces (en temps constant). Une ArrayList
s’utilise en fait comme un tableau à dimension automatiquement ajustable.
En plus des méthodes de l’interface qu’il implémente, une ArrayList procure les constructeurs suivants :
– ArrayList() : le constructeur par défaut.
– ArrayList(int capacité) : un constructeur permettant de spécifier le nombre approximatif d’éléments prévus, lors-
qu’on le connait à l’avance.
– ArrayList(Collection c) : construit une ArrayList à partir d’une Collection.
LinkedList
La classe LinkedList implémente l’interface List par une liste doublement chaînée. Les méthodes get et set sont donc
relativement peu efficaces (en temps linéaire). Par contre add et remove sont en temps constant. En plus des méthodes de
l’interface List, une LinkedList procure les constructeurs et méthodes spécifiques suivants :
– LinkedList() : le constructeur par défaut.
– LinkedList (Collection c) : construit une LinkedList à partir d’une Collection.
– Object getFirst() : renvoie le premier objet de la LinkedList.
– Object getLast() : renvoie le dernier objet de la LinkedList.
– void addFirst(Object o) : ajoute l’objet spécifié en début de la LinkedList.
– void addLast(Object o) : ajoute l’objet spécifié en fin de la LinkedList.
– void removeFirst() : ôte le premier élément de la LinkedList.
– void removeLast() : ôte le dernier élément de la LinkedList.
La classe HashMap implémente l’interface Map par une table de hachage (voir page 36), et donc les méthodes get et put
sont très efficaces (temps quasiment constant). En plus des méthodes de l’interface qu’il implémente, HashMap procure
les contructeurs suivants :
– HashMap() : le constructeur par défaut.
– HashMap(int capacité) : un constructeur permettant de spécifier le nombre approximatif d’associations prévues,
lorsqu’on le connait à l’avance.
– HashMap(Map m) : construit un HashMap à partir d’un autre Map.
TreeMap
La classe TreeMap implémente l’interface SortedMap par un arbre rouge et noir (voir page 51), et donc les méthodes get,
put, remove et containsKey sont relativement efficaces (temps logarithmique). Les clés doivent être comparables : soit
les clés implémentent naturellement l’interface Comparable (voir page 82), soit on a fourni au constructeur de TreeMap un
Comparator (voir page 82). En plus des méthodes de l’interface qu’il implémente, un TreeMap procure les constructeurs
suivants :
– TreeMap() : le constructeur par défaut.
– TreeMap(Comparator c) : le constructeur qui permet de spécifier un Comparator définissant l’ordre sur les clés.
– TreeMap(Map m) : construit un TreeMap à partir d’un autre Map.
La méthode List asList(Object[] a) jette un pont entre le tableau et la Collection : elle renvoie une «vue» List
du tableau d’objets spécifié.
Iterator
Cette interface définit des méthodes pour énumérer les objets d’une Collection (voir les exemples, page 83). Lors de
la création d’un itérateur (par la méthode iterator() de la Collection à énumérer), cet itérateur est positionné sur le
premier élément de la Collection. L’interface Iterator offre trois méthodes :
– boolean hasNext() : renvoie true s’il reste encore des éléments à énumérer, false s’il ont tous été énumérés.
– Object next() : positionne l’itérateur sur le prochain élément.
– void remove() : ôte de la Collection énumérée le dernier objet renvoyé par next.
Voir un exemple d’utilisation de l’interface Iterator dans le programme Java qui commence page 83, lignes 49 à 54.
Comparable
Cette interface5 contient une seule méthode : int compareTo(Objet o), pour comparer deux objets selon leur ordre
«naturel». La méthode renvoie un entier négatif si l’objet this est plus petit que celui passé en argument, un entier positif
si l’objet this est plus grand que celui passé en argument, et 0 si les deux objets sont égaux. Les termes «plus petit», «plus
grands» et «égaux» s’entendent selon l’ordre naturel des objets. La plupart des objets standard implémentent l’interface
Comparable. C’est le cas en particulier de tous les wrappers.
Si vous voulez définir un ordre sur des objets qui n’implémentent pas l’interface Comparable, ou définir un ordre différent
de l’ordre naturel, vous devrez définir une classe implémentant l’interface Comparator.
Comparator
Cette interface offre deux méthodes :
– int compare(Object o1, Object o2) : renvoie un entier négatif si o1 vient avant o2 dans l’ordre voulu, positif si
o1 vient après o2, et 0 si les deux objets sont équivalents relativement à l’ordre voulu.
– boolean equals(Object o) : o1.equals(o2) teste si o1 et o2 sont égaux relativement à l’ordre voulu.
Pour utiliser l’interface Comparator, par exemple pour trier, ou pour construire un SortedSet ou un SortedMap spéci-
fique, il faut construire une classe implémentant l’interface (avec au moins une implémentation de la méthode compare),
et passer à qui de droit un objet de cette classe construit par new. Souvent, la classe en question est définie localement
comme une classe interne (voir un exemple dans le programme Java qui commence page 83, lignes 127 à 155). Cela parait
compliqué au début, mais en fin de compte c’est assez simple.
Collections
Attention : Collections avec un ‘s’ !
Cette classe définit un ensemble de méthodes statiques utiles en relation avec Collection et Map. Parmi les méthodes les
plus utiles, on trouve :
– static void sort(List l) : trie sur place la liste passée en argument, selon l’ordre naturel des éléments, c’est-à-
dire celui donné par la méthode CompareTo de l’interface Comparable sensé être implémenté par les éléments de la
liste.
– static void sort(List l, Comparator c) : trie sur place la liste passée en argument, selon l’ordre spécifié par le
Comparator.
– static int binarySearch(List l, Object o) : recherche dans la List triée l’objet spécifié, et retourne l’index
de l’objet dans la liste s’il y est, et un nombre négatif sinon. La recherche est par dichotomie (temps logarithmique)
sur une ArrayList, et séquentielle (temps linéaire) sur une LinkedList. C’est la version utilisant l’ordre naturel des
objets en question.
– static int binarySearch(List l, Object o, Comparator c) : version avec Comparator.
On trouve aussi de quoi effectuer des copies de List, remplir des List, renverser une List, trouver dans une List non
ordonnée un minimum et un maximum, mélanger aléatoirement les éléments d’une liste, . . . (se reporter à la documenta-
tion).
5 Comparable appartient au package de base java.lang, importé automatiquement.
14.4. EXERCICES 83
14.4 Exercices
J
Exercice 14.1 Quelle classe du collections framework choisiriez-vous pour implémenter une pile ? une file ?
J
Exercice 14.2 Pourquoi Map fournit la méthode containsKey alors que get a priori suffirait ?
J
Exercice 14.3 Quels sont les raisons pour lesquelles on exige que les clés d’un Map forment un ensemble (clés
uniques) ? Est-ce réellement une restriction ?
J
Exercice 14.4 Quelles structures de données abstraites pourtant courantes ne sont pas implémentées directement par
le collections framework ? Pourquoi ?
Le programme Java qui suit regroupe un certain nombre d’exemples d’utilisation de méthodes du collections framework.
Le fichier TestCollectionsFramework.java
1 /*
2 * TestCollectionsFramework.java
3 *
4 * SupAéro -- Cours Structures de Données et Algorithmes
5 */
6
10 /**
11 * Petits exemples d’utilisation du collections framework
12 *
13 * @version 6 Septembre 2000
14 * @author Michel Lemaître
15 */
16 c l a s s TestCollectionsFramework {
17 s t a t i c PrintWriter sortie ;
18
22 // ==================================
23 // Quelques objets qui nous serviront.
24 Object o1 = new Integer (36);
25 Object o2 = " aaaaa ";
26 Object o3 = new Double ( Math . PI );
27
28
29 // ==================================
30 // Création d’une Collection (les duplications sont permises)
31 // implémentée en liste doublement chaînée.
32 Collection c = new LinkedList ();
33
84 CHAPITRE 14. LA BIBLIOTHÈQUE COLLECTIONS FRAMEWORK DE JAVA
55
56 // ==================================
57 // Création d’un SortedMap, c’est-à-dire un dictionnaire
58 // dont les clés (obligatoirement des Objets) sont ordonnées,
59 // implémentée par un arbre <<rouge et noir>> (seule implémentation disponible)
60 SortedMap m = new TreeMap ();
61
82
83 // ==================================
84 // Création d’une List (une séquence)
85 // implémentée par un tableau.
86 List l = new ArrayList (20); // crée toujours une List vide
87 sortie . println (" l1 : " + l );
88 // Insérons quelques objets.
89 l. add ( o1 );
90 l. add ( o2 );
91 l. add ( o3 );
92 l. add ( n u l l );
93 l. add ( n u l l );
94 l. add ( o2 );
95 sortie . println (" l2 : " + l );
96
107
108 // ==================================
109 // Utilisation des méthodes de Collections
110
126 // Tri d’une List de String selon un ordre particulier : leur longueur.
127 // Il faut utiliser l’interface Comparator.
128 Collections . sort ( ll , new CompStringLongueur ());
129 sortie . println (" ll3 : " + ll );
130
145 /**
146 * Une classe implémentant Comparator et définissant
147 * une méthode compare de comparaison de String
148 * selon leur longueur.
149 */
150 c l a s s CompStringLongueur implements Comparator {
151 p u b l i c i n t compare ( Object o1 , Object o2 ) {
152 r e t u r n (( String ) o1 ). length () - (( String ) o2 ). length ();
153 }
154 }
Le fichier sortie_TestCollectionsFramework
1 c1: [36, aaaaa, 3.141592653589793]
2 c2: [36, aaaaa, 3.141592653589793, 36, 36]
86 CHAPITRE 14. LA BIBLIOTHÈQUE COLLECTIONS FRAMEWORK DE JAVA
Références
15.1 Livres
Les ouvrages qui traitent d’algorithmique et de structures de données sont légions. Voici quelques bonnes références. Les
algorithmes et les exemples présentés dans ces notes proviennent essentiellement du [Cormen].
87
88 CHAPITRE 15. RÉFÉRENCES
15.2 Toile
Le collections framework de Java : http://java.sun.com/j2se/1.3/docs