Vous êtes sur la page 1sur 440

Structures de données

et méthodes formelles
Springer
Paris
Berlin
Heidelberg
New York
Hong Kong
Londres
Milan
Tokyo
Marc Guyomard

Structures de données
et méthodes formelles
Marc Guyomard
Enssat Irisa
Université de Rennes 1
Technopole Anticipa
6, rue de Kerampont
BP 80518
22305 Lannion Cedex

ISBN : 978-2-8178-0199-5 Springer Paris Berlin Heidelberg New York

© Springer-Verlag France, 2011


Imprimé en France

Springer-Verlag est membre du groupe Springer Science + Business Media

Cet ouvrage est soumis au copyright. Tous droits réservés, notamment la reproduction et la repré-
sentation, la traduction, la réimpression, l’exposé, la reproduction des illustrations et des tableaux,
la transmission par voie d’enregistrement sonore ou visuel, la reproduction par microfilm ou tout
autre moyen ainsi que la conservation des banques de données. La loi française sur le copyright du
9 septembre 1965 dans la version en vigueur n’autorise une reproduction intégrale ou partielle que
dans certains cas, et en principe moyennant le paiement des droits. Toute représentation, reproduc-
tion, contrefaçon ou conservation dans une banque de données par quelque procédé que ce soit est
sanctionnée par la loi pénale sur le copyright.
L’utilisation dans cet ouvrage de désignations, dénominations commerciales, marques de fabrique, etc.
même sans spécification ne signifie pas que ces termes soient libres de la législation sur les marques de
fabrique et la protection des marques et qu’ils puissent être utilisés par chacun.
La maison d’édition décline toute responsabilité quant à l’exactitude des indications de dosage et
des modes d’emploi. Dans chaque cas il incombe à l’usager de vérifier les informations données par
comparaison à la littérature existante.

Maquette de couverture : Jean-François Montmarché


Illustrations de couverture : (Fotolia.com)
programming layout © Mike Kiev #11376759
circuit imprimé © Sylvie Thenard #8582813
Connected shape algorithm © 1xpert #29471089
COLLECTION Télécom
Directeur de la Collection : Pierre-Noël Favennec

COMITÉ SCIENTIFIQUE
Président : Claude Guéguen
Michel Allovon (Orange Labs)
Chantal Ammi (Télécom Ecole de management)
Annie Blandin (Télécom Bretagne)
Jean-Pierre Cocquerez (UTC Compiègne, GDR ISIS)
Frédérique de Fornel (ICB Dijon, GDR Ondes)
Georges Fiche (APAST Lannion)
Alain Hillion (Télécom Bretagne)
René Joly (Télécom ParisTech)
Henri Maître (Télécom ParisTech)
Chantal Morley (Télécom SudParis)
Gérard Pogorel (Télécom ParisTech)
Gérard Poulain (APAST Lannion)
Serge Proulx (UQAM Montreal)
Nicolas Puech (Télécom ParisTech)
Guy Pujolle (UPMC Paris)
Pierre Rolin (Télécom SudParis)
Basel Solaiman (Télécom Bretagne)
Sami Tabbane (SupCom Tunis)
Joe Wiart (Orange Labs)
À Éveline,
David, Astrid et Christophe
Orlane, Sirina, Émilie, Vic

. . . I have no objection
with people feeling more comfortable
with the word “English”
replacing the word “mathematics”.
I just wonder whether such people
are not assigning
themselves a more difficult task. . .

. . . Je n’ai rien à objecter


aux personnes qui se sentent plus à l’aise
quand le mot « français »
remplace le mot « mathématiques ».
Je me demande simplement
dans quelle mesure ces personnes
ne se compliquent pas la tâche. . .

J.-R. Abrial, The B-Book, 1996.


Préface
Le cours de mathématiques pour l’informatique, de même que le cours de
logique des propositions et des prédicats du premier ordre font encore partie de
la formation de base des informaticiens. Ainsi les départements d’informatique,
dès la création des iut, ont-ils bénéficié d’enseignements de mathématiques pour
l’informatique et d’enseignements de logique alors même que la formation dure
deux ans et que la plupart des étudiants, il y a 40 ans, ne poursuivaient pas
leurs études. Mais, trop souvent, l’utilisation des mathématiques demeure fort
limitée pour ne pas dire absente dans le processus qui mène de la spécification
du problème à la construction du logiciel. Marc Guyomard présente, dans cet
ouvrage, une approche de la construction des algorithmes relatifs aux structures
de données, une approche par la preuve. C’est le livre d’un sage ! Sapiens ni-
hil affirmat quod non probet. L’auteur délivre à la fois un cours original sur les
structures de données et une méthode formelle de construction de programmes.
Ce livre est au cœur de l’informatique. Il suffit d’y lire les applications des diffé-
rentes structures de données, pour constater que l’informatique du XXIe siècle
fait un grand usage des structures manipulées dans ce livre. Structures de don-
nées et méthodes formelles traite des fondements sans lesquels, malgré la puis-
sance actuelle des machines, nous ne disposerions pas des logiciels performants
qui sont à la disposition de tous aujourd’hui. Et ceux qui ont utilisé différents
systèmes d’exploitation bien connus fournis avec les micro-ordinateurs, peuvent
les donner en exemples de la « loi de Reiser » – appelée sur la Toile « Loi de
Wirth » alors que N. Wirth ne fait que citer M. Reiser : « Le logiciel ralentit
plus vite que le matériel n’accélère. » L’Église catholique en était-elle peut-être
consciente en choisissant comme saint patron des informaticiens Isidore de Sé-
ville qui, d’après ce que l’on peut lire, répété de nombreuses fois sur la Toile –
ce qui n’est pas un gage de vérité 1 même si c’est un critère pour améliorer son
« page rank » –, aurait inventé les « tries » (l’une des structures étudiées dans
ce livre), qui sont appliqués dans son ouvrage célèbre, les Etymologies. Leibniz
disait : Sedemus et calculemus, asseyons-nous et calculons. C’est ce que propose
Marc Guyomard. Son point de départ est une spécification écrite en utilisant la
mathématique élémentaire sur les ensembles et les relations ainsi que la logique
des prédicats (le quoi), spécification à partir de laquelle il dérive – il calcule – la
représentation des algorithmes (le comment). Il utilise la notation de la méthode
B mise au point par Jean-Raymond Abrial, méthode qui a maintenant fait la
preuve de son intérêt pédagogique et opérationnel. La notation est en grande
1. En fait, comme le note Donald Knuth dans le tome 3 de son monumental The Art of
Computer Programming cité parmi les douze meilleures monographies du siècle par l’American
Scientist – les « Étymologies » sont ordonnées selon un ordre lexicographique imparfait, portant
uniquement sur la première lettre. De là aux tries, il y a un long chemin !. . .
x Structures de données et méthodes formelles

partie celle qu’utilisent couramment les mathématiciens et les logiciens, com-


plétée par des symboles fort pratiques. Nous pensons à ce propos qu’il est sans
doute plus profitable d’enseigner une partie des programmes de mathématiques
dans le cadre de l’enseignement de spécification/programmation que d’en faire
des enseignements cloisonnés. L’avantage qui en résulte est de rapprocher l’outil
(les mathématiques) de sa principale application (la spécification), compétence
au cœur du métier d’informaticien. Ainsi on peut mettre l’accent sur une activité
essentielle, celle d’abstraction. La méthode B opère par raffinage, afin de passer
de l’abstrait (la spécification) au concret (l’algorithme rédigé dans un langage
exécutable sur un ordinateur) et à satisfaire en cours de processus, des obliga-
tions de preuves, preuves qui peuvent être assistées par des logiciels. Comme il
a réutilisé les spécifications ensemblistes et la logique, Marc Guyomard réutilise
le modèle fonctionnel en programmation, modèle très souvent enseigné à juste
raison, et il l’applique lors de la dérivation de programmes au lieu d’appliquer
le cadre algorithmique. Voilà sur quoi repose l’originalité de la démarche pré-
sentée et appliquée par Marc Guyomard dans ce livre. Il a renouvelé le fameux
« structure de données + algorithmes = programmes » de N. Wirth en énon-
çant « spécifications + fonction d’abstraction + calcul = programme ». Il lui
restait à choisir quelles structures de données présenter et dans quel ordre. Il
a choisi une perspective historique, perspective trop peu souvent utilisée dans
les enseignements d’informatique où l’on a parfois tendance à considérer qu’une
bibliographie de plus de 5 ans d’âge n’a pas sa place dans une publication. Nous
avions été séduit par les remarquables polycopiés de Marc Guyomard, en parti-
culier un cours sur la compilation et un autre sur la construction de la boucle.
C’est avec plaisir que nous avons appris que Marc Guyomard allait diffuser son
cours sur les structures de données qu’il dispense aux étudiants de l’Enssat de
Lannion. Je suis sûr que les enseignants, les étudiants, les praticiens apprécieront
ce livre qui leur fournit à la fois, une présentation raisonnée de très nombreuses
structures de données, bien plus que ce que l’on trouve habituellement dans les
livres comparables, et une méthode originale de développement de programmes
fondé sur la preuve. Toutes les notions et les notations nécessaires pour suivre
l’exposé sont présentées, lorsque le besoin s’en fait sentir, Structures de données
et méthodes formelles est un livre que les programmeurs doivent avoir à portée
de la main.

Henri Habrias
Professeur émérite à l’Université de Nantes
Table des matières

Préface ix

Avant-propos 1

I Les bases 13

1 Mathématiques pour la spécification et les structures


de données 15
1.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.2 Raisonnement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.3 Calcul propositionnel . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.4 Calcul des prédicats . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.4.1 Quantification universelle . . . . . . . . . . . . . . . . . . 22
1.4.2 Deux nouveaux types d’expressions . . . . . . . . . . . . . 25
1.5 Égalité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
1.6 Théorie des ensembles . . . . . . . . . . . . . . . . . . . . . . . . 27
1.6.1 Opérations ensemblistes traditionnelles . . . . . . . . . . . 29
1.6.2 Choix dans un ensemble . . . . . . . . . . . . . . . . . . . 30
1.6.3 Relations binaires . . . . . . . . . . . . . . . . . . . . . . 31
1.6.4 Fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.7 Ensembles particuliers . . . . . . . . . . . . . . . . . . . . . . . . 36
1.8 Exemple de modélisation ensembliste . . . . . . . . . . . . . . . . 40
1.9 Construction de structures inductives . . . . . . . . . . . . . . . . 43
1.10 Démonstration par récurrence . . . . . . . . . . . . . . . . . . . . 43
1.11 Opérations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
1.11.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 47
1.11.2 Expression préconditionnée . . . . . . . . . . . . . . . . . 48
1.11.3 Définition des opérations . . . . . . . . . . . . . . . . . . 48
1.11.4 Évaluation des opérations . . . . . . . . . . . . . . . . . . 50
1.11.5 Propagation des préconditions . . . . . . . . . . . . . . . 52
1.12 Conclusion et remarques bibliographiques . . . . . . . . . . . . . 56
xii Structures de données et méthodes formelles

2 Spécifications + Fonction d’abstraction + Calcul = Programme 59


2.1 Cadre général de la démarche . . . . . . . . . . . . . . . . . . . . 59
2.2 Formalisme pour les types – Exemple . . . . . . . . . . . . . . . . 61
2.3 Calcul des opérations – Exemple . . . . . . . . . . . . . . . . . . 64
2.3.1 Calcul d’une représentation de l’opération plus_n . . . . 66
2.3.2 Calcul d’une représentation de l’opération inf _n . . . . . 68
2.4 Induction, support et renforcement . . . . . . . . . . . . . . . . . 71
2.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75

3 Étude de quelques structures outils 77


3.1 Listes finies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
3.1.1 Présentation informelle . . . . . . . . . . . . . . . . . . . 78
3.1.2 Ébauche de construction . . . . . . . . . . . . . . . . . . . 79
3.1.3 Opérations sur les listes . . . . . . . . . . . . . . . . . . . 82
3.1.4 Notation linéaire . . . . . . . . . . . . . . . . . . . . . . . 82
3.2 Arbres finis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
3.3 Arbres non ordonnés . . . . . . . . . . . . . . . . . . . . . . . . . 85
3.3.1 Introduction : les arbres non ordonnés non étiquetés . . . 85
3.3.2 Arbres non ordonnés étiquetés . . . . . . . . . . . . . . . 85
3.4 Arbres planaires . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
3.4.1 Introduction : les arbres planaires non étiquetés . . . . . . 87
3.4.2 Arbres planaires étiquetés . . . . . . . . . . . . . . . . . . 87
3.5 Arbres n-aires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
3.5.1 Introduction : les arbres n-aires non étiquetés . . . . . . . 88
3.5.2 Les arbres binaires étiquetés . . . . . . . . . . . . . . . . . 89
3.6 Arbres binaires partiellement étiquetés : les arbres externes . . . 96
3.6.1 Présentation informelle . . . . . . . . . . . . . . . . . . . 96
3.6.2 Ébauche de construction . . . . . . . . . . . . . . . . . . . 97
3.6.3 Opérations sur les arbres externes . . . . . . . . . . . . . 98
3.6.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . 98
3.7 Sacs finis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
3.7.1 Présentation informelle . . . . . . . . . . . . . . . . . . . 100
3.7.2 Ébauche de construction . . . . . . . . . . . . . . . . . . . 101
3.7.3 Opérations sur les sacs . . . . . . . . . . . . . . . . . . . . 102
3.7.4 Propriétés des sacs . . . . . . . . . . . . . . . . . . . . . . 103
3.8 Conclusion et remarques bibliographiques . . . . . . . . . . . . . 104

4 Analyse d’algorithmes 107


4.1 Notations asymptotiques . . . . . . . . . . . . . . . . . . . . . . . 108
4.2 Analyse classique de la complexité
de fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
4.2.1 Passage du texte d’une fonction à une équation
récurrente . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
4.2.2 Complexité la meilleure, la pire, moyenne . . . . . . . . . 116
4.2.3 Résolution d’équations récurrentes . . . . . . . . . . . . . 117
4.2.4 Contourner ou simplifier certaines étapes
intermédiaires . . . . . . . . . . . . . . . . . . . . . . . . . 120
Table des matières xiii

4.2.5 Conclusion et remarques bibliographiques . . . . . . . . . 122


4.3 Analyse amortie de la complexité –
la méthode du potentiel . . . . . . . . . . . . . . . . . . . . . . . 123
4.3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 123
4.3.2 Fondements théoriques de la méthode du potentiel . . . . 125
4.3.3 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
4.3.4 Conclusion et remarques bibliographiques . . . . . . . . . 128

5 Exemples 129
5.1 Premier exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
5.1.1 Spécification du type abstrait . . . . . . . . . . . . . . . 129
5.1.2 Définition du support concret . . . . . . . . . . . . . . . . 130
5.1.3 Définition de la fonction d’abstraction . . . . . . . . . . . 130
5.1.4 Spécification formelle des opérations . . . . . . . . . . . . 131
5.1.5 Calcul de la représentation des opérations . . . . . . . . . 131
5.1.6 Complexités de l’opération suc_l . . . . . . . . . . . . . . 135
5.1.7 Conclusion et remarques bibliographiques . . . . . . . . . 138
5.2 Second exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
5.2.1 Spécification du type abstrait ensnat . . . . . . . . . . . 139
5.2.2 Définition du support concret . . . . . . . . . . . . . . . . 139
5.2.3 Définition de la fonction d’abstraction . . . . . . . . . . . 140
5.2.4 Spécification des opérations concrètes . . . . . . . . . . . 140
5.2.5 Calcul d’une représentation de l’opération eAjout_l . . . 140
5.2.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . 143

II Structures de données fondamentales :


spécification et mises en œuvre 145
6 Ensembles de clés scalaires 147
6.1 Présentation informelle . . . . . . . . . . . . . . . . . . . . . . . . 147
6.2 Spécification du type abstrait ensabst . . . . . . . . . . . . . . . 148
6.3 Méthodes arborescentes : les arbres binaires de recherche . . . . . 150
6.3.1 Définition du support concret . . . . . . . . . . . . . . . . 150
6.3.2 Définition de la fonction d’abstraction . . . . . . . . . . . 151
6.3.3 Spécification des opérations concrètes . . . . . . . . . . . 151
6.3.4 Calcul de la représentation des opérations concrètes . . . 151
6.3.5 Conclusion et remarques bibliographiques . . . . . . . . . 168
6.4 Méthodes de hachage . . . . . . . . . . . . . . . . . . . . . . . . . 169
6.4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 169
6.4.2 Définition du support concret . . . . . . . . . . . . . . . . 171
6.4.3 Définition de la fonction d’abstraction . . . . . . . . . . . 172
6.4.4 Spécification des opérations concrètes . . . . . . . . . . . 172
6.4.5 Calcul de la représentation des opérations concrètes . . . 172
6.4.6 Fonction de hachage . . . . . . . . . . . . . . . . . . . . . 175
6.4.7 Autres méthodes de hachage . . . . . . . . . . . . . . . . 175
6.4.8 Conclusion et remarques bibliographiques . . . . . . . . . 177
6.5 Méthodes arborescentes : les arbres externes de recherche . . . . 178
xiv Structures de données et méthodes formelles

6.5.1 Définition du support concret . . . . . . . . . . . . . . . . 178


6.5.2 Définition de la fonction d’abstraction . . . . . . . . . . . 179
6.5.3 Spécification des opérations . . . . . . . . . . . . . . . . . 179
6.5.4 Calcul de la représentation des opérations concrètes . . . 179
6.5.5 Renforcement du support par décomposition de la fonction
max . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
6.5.6 Conclusion et remarques bibliographiques . . . . . . . . . 190
6.6 Méthodes équilibrées : les Avl . . . . . . . . . . . . . . . . . . . 191
6.6.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 191
6.6.2 Définition et propriétés du support concret . . . . . . . . 192
6.6.3 Définition de la fonction d’abstraction . . . . . . . . . . . 196
6.6.4 Spécification des opérations concrètes . . . . . . . . . . . 196
6.6.5 Rotations . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
6.6.6 Calcul de la représentation des opérations concrètes . . . 202
6.6.7 Renforcement du support par décomposition du rayon . . 213
6.6.8 Conclusion et remarques bibliographiques . . . . . . . . . 214
6.7 Méthodes équilibrées : les B-arbres . . . . . . . . . . . . . . . . . 216
6.7.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 216
6.7.2 Définition du support concret . . . . . . . . . . . . . . . . 218
6.7.3 Définition de la fonction d’abstraction . . . . . . . . . . . 227
6.7.4 Spécification des opérations concrètes . . . . . . . . . . . 228
6.7.5 Calcul de la représentation des opérations concrètes . . . 228
6.7.6 Conclusion et remarques bibliographiques . . . . . . . . . 243
6.8 Structures autoadaptatives et analyse amortie :
les arbres déployés . . . . . . . . . . . . . . . . . . . . . . . . . . 245
6.8.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 245
6.8.2 Principe de l’insertion dans un arbre déployé . . . . . . . 245
6.8.3 Support concret et fonction d’abstraction . . . . . . . . . 246
6.8.4 Calcul de l’opération eAjout_ad . . . . . . . . . . . . . . 247
6.8.5 Analyse amortie de l’opération eAjout_ad . . . . . . . . 251
6.8.6 Conclusion et remarques bibliographiques . . . . . . . . . 257
6.9 Méthodes aléatoires : les treaps randomisés . . . . . . . . . . . . 258
6.9.1 Définition du support concret . . . . . . . . . . . . . . . . 258
6.9.2 Définition de la fonction d’abstraction . . . . . . . . . . . 259
6.9.3 Spécification des opérations concrètes . . . . . . . . . . . 260
6.9.4 Calcul de la représentation des opérations concrètes . . . 260
6.9.5 Renforcer ou non le support ? . . . . . . . . . . . . . . . . 268
6.9.6 Treaps randomisés . . . . . . . . . . . . . . . . . . . . . . 268
6.9.7 Conclusion et remarques bibliographiques . . . . . . . . . 270

7 Ensembles de clés structurées 273


7.1 Ensembles de chaînes, représentation par tries . . . . . . . . . . . 273
7.1.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 273
7.1.2 Définition des supports concrets . . . . . . . . . . . . . . 274
7.1.3 Définition des fonctions d’abstraction . . . . . . . . . . . 276
7.1.4 Spécification des opérations concrètes . . . . . . . . . . . 277
7.1.5 Calcul de la représentation des opérations concrètes . . . 277
Table des matières xv

7.1.6 Variantes des tries . . . . . . . . . . . . . . . . . . . . . . 286


7.1.7 Conclusion et remarques bibliographiques . . . . . . . . . 291
7.2 Ensembles de couples, représentation par kd-arbres . . . . . . . . 293
7.2.1 Ensembles de couples, spécification abstraite . . . . . . . 293
7.2.2 Ensembles de couples, introduction aux kd-arbres . . . . . 295
7.2.3 Définition du support concret . . . . . . . . . . . . . . . . 296
7.2.4 Définition de la fonction d’abstraction . . . . . . . . . . . 298
7.2.5 Spécification des opérations concrètes . . . . . . . . . . . 298
7.2.6 Calcul d’une représentation des opérations concrètes . . . 299
7.2.7 Conclusion et remarques bibliographiques . . . . . . . . . 309

8 Files simples 313


8.1 Présentation informelle des files simples . . . . . . . . . . . . . . 313
8.2 Spécification du type abstrait fileabst . . . . . . . . . . . . . . 314
8.3 Mise en œuvre « chaînée » . . . . . . . . . . . . . . . . . . . . . . 315
8.3.1 Définition du support concret . . . . . . . . . . . . . . . . 318
8.3.2 Définition de la fonction d’abstraction . . . . . . . . . . . 318
8.3.3 Spécification formelle des opérations . . . . . . . . . . . . 318
8.3.4 Calcul de la représentation des opérations concrètes . . . 318
8.3.5 Complexité de la mise en œuvre chaînée . . . . . . . . . . 322
8.3.6 Conclusion et remarques bibliographiques . . . . . . . . . 324

9 Files de priorité 327


9.1 Présentation informelle . . . . . . . . . . . . . . . . . . . . . . . . 327
9.2 Spécification du type abstrait fpabst . . . . . . . . . . . . . . . 328
9.3 Méthodes équilibrées : les tas . . . . . . . . . . . . . . . . . . . . 329
9.3.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . 329
9.3.2 Conclusion et remarques bibliographiques . . . . . . . . . 331
9.4 Méthodes équilibrées : les files binomiales . . . . . . . . . . . . . 331
9.4.1 Définition des supports concrets . . . . . . . . . . . . . . 333
9.4.2 Définition des fonctions d’abstraction . . . . . . . . . . . 335
9.4.3 Spécification des opérations concrètes . . . . . . . . . . . 336
9.4.4 Calcul de la représentation des opérations concrètes . . . 339
9.4.5 Renforcement du support par décomposition du rayon . . 358
9.4.6 Conclusion et remarques bibliographiques . . . . . . . . . 360
9.5 Méthodes autoadaptatives : les minimiers obliques (skew heaps) . 361
9.5.1 Définition du support concret . . . . . . . . . . . . . . . . 362
9.5.2 Définition de la fonction d’abstraction . . . . . . . . . . . 363
9.5.3 Spécification des opérations concrètes . . . . . . . . . . . 363
9.5.4 Calcul d’une représentation des opérations concrètes . . . 363
9.5.5 Analyse amortie de l’opération f us . . . . . . . . . . . . . 368
9.5.6 Conclusion et remarques bibliographiques . . . . . . . . . 373

10 Tableaux flexibles 377


10.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
10.2 Flexibilité faible à droite : présentation informelle . . . . . . . . . 378
10.3 Flexibilité faible : spécification du type abstrait . . . . . . . . . . 378
10.4 Flexibilité faible : mise en œuvre par arbres de Braun . . . . . . 378
xvi Structures de données et méthodes formelles

10.4.1 Flexibilité faible : définition du support concret . . . . . . 381


10.4.2 Flexibilité faible : définition de la fonction d’abstraction . 382
10.4.3 Flexibilité faible : spécification des opérations concrètes . 384
10.4.4 Flexibilité faible : calcul des opérations concrètes . . . . . 385
10.4.5 Décomposer la fonction w ? . . . . . . . . . . . . . . . . . 391
10.4.6 Conclusion et remarques bibliographiques . . . . . . . . . 391
10.5 Flexibilité forte : présentation informelle . . . . . . . . . . . . . . 394
10.6 Flexibilité forte : spécification du type abstrait tsabst . . . . . . 395
10.7 Flexibilité forte : mise en œuvre par minimiers . . . . . . . . . . 395
10.7.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395
10.7.2 Conclusion et remarques bibliographiques . . . . . . . . . 398

Annexes – Répertoire de propriétés 401


A Propriétés générales des ensembles 403

B Propriétés des relations binaires 405

C Propriétés des fonctions 411

D Propriétés des entiers 413

E Opérateurs et priorités 415

Bibliographie 417

Index 425
Avant-propos

Un de plus ! Un ouvrage de plus sur les structures de données ! Mais pour-


quoi diable ? Tout a été dit et écrit dans ce domaine. En témoignent les centaines
de livres publiés depuis l’avènement de l’informatique. C’est peut-être la réflexion
que se fait le lecteur en ce moment. Pourtant il lui suffit de feuilleter l’ouvrage
qu’il tient entre ses mains pour constater que le présent livre se démarque de la
plupart de ses homologues : il est émaillé de nombreuses formules, de démons-
trations, de programmes qui ne sont rédigés ni en C ni en Java ni en Perl. C’est
que l’informatique logicielle est engagée depuis quelques décennies déjà dans un
processus irréversible de rationalisation. Elle a d’ores et déjà acquis le statut de
science. C’est particulièrement visible à travers les avancées régulières de ce qu’il
est convenu d’appeler les « méthodes formelles pour le génie logiciel ». Cette
approche de l’informatique a déjà fait de nombreux adeptes dans l’industrie,
notamment pour la réalisation de systèmes critiques (transports, énergie, méde-
cine, télécommunications, etc.). En ce qui concerne l’enseignement, en général les
formations du supérieur incluent dans leur offre pédagogique un module sur ce
thème. Nous sommes donc engagés sur la bonne voie, mais nous pouvons main-
tenant aller plus loin. Les méthodes formelles peuvent et doivent sortir de leur
Ghetto. L’esprit et la lettre des méthodes formelles sont aujourd’hui à même
d’être diffusés dans la quasi-totalité des domaines de l’informatique logicielle.
C’est ce que nous tentons de démontrer ici pour ce qui concerne les structures
de données.

Trois engagements
Jouons carte sur table. Cet ouvrage sur les structures de données est fondé
sur un certain nombre de partis pris et n’est pas neutre didactiquement parlant.
Mais nous sommes convaincu que celui qui décide d’adhérer aux principes qui
sont défendus ici sera rapidement récompensé et trouvera autant de plaisir que
l’auteur à les pratiquer puis à les transmettre.

De la spécification à la représentation
Le premier principe vers lequel nous nous sommes engagé est celui d’une
séparation entre la notion de spécification et celle de représentation (ou mise
en œuvre) des structures de données et des algorithmes qui les accompagnent.
Certes ceci n’est pas nouveau. Beaucoup d’auteurs manifestent un intérêt pour
2 Structures de données et méthodes formelles

distinguer le « quoi » du « comment ». Il suffit pour s’en convaincre de consulter


la plupart des ouvrages portant sur le même sujet pour y découvrir un premier
chapitre, ou une annexe, consacré à la présentation de la logique des prédicats et
à la théorie des ensembles, outils de base à l’expression du « quoi ». Et puis. . . Et
puis en général cela s’arrête là. Comme si ces auteurs pressentaient l’importance
de ces notions et de leurs liens intimes avec le domaine des structures de données
mais ne souhaitaient pas, ou ne parvenaient pas à les exploiter dans la pratique.
Notre approche est beaucoup moins timide puisque nous ne proposons rien de
moins que de calculer (ou dériver) la représentation des algorithmes à partir
d’une spécification qui est essentiellement ensembliste.

Le modèle fonctionnel pour les structures de données


Le second engagement que nous prenons dépend d’une certaine façon du
premier. Il s’agit de l’utilisation du modèle fonctionnel de programmes et de
données. La démarche calculatoire est connue et appliquée depuis longtemps en
programmation impérative (cf. [30, 31, 45, 24]). Le mariage du modèle fonction-
nel avec l’approche calculatoire fait émerger une synergie appréciable tant pour
la simplicité et le naturel des calculs réalisés que pour la qualité des programmes
produits.

La perspective chronologique
« Qui ignore l’histoire est condamné à la revivre » affirme un philosophe
du XIXe siècle. Cela reste vrai même pour la petite histoire des structures de
données. Celle-ci peut être observée à travers le prisme d’une course-poursuite
entre recherche d’efficacité et recherche de simplicité. La période des pionniers va
des origines de la programmation (aux alentours des années 1950) jusqu’au tour-
nant des années 1960. Elle voit la naissance des structures de données « naïves »,
(pour lesquelles d’ailleurs l’histoire n’a que rarement daigné retenir l’identité des
découvreurs). En ce qui concerne le cas particulièrement caractéristique de la re-
présentation des ensembles, on voit apparaître la structure fondamentale qu’est
l’arbre binaire de recherche, mais aussi les tables de hachage. La seconde période
va des années 1960 jusqu’à la fin des années 1970. La pensée dominante est in-
carnée par la devise « des structures de données de plus en plus complexes pour
une efficacité de plus en plus grande ». C’est l’époque des structures explicite-
ment équilibrées. L’obtention d’une efficacité systématique est garantie par une
réorganisation opérée lors des mises à jour et permise par une information ad hoc
intégrée à la structure et portant sur son état d’équilibre. C’est le règne des Avl,
des B-arbres, et de bien d’autres structures semblables dont l’intérêt perdure. Les
deux périodes suivantes, bien que différentes sur un plan technique, constituent
un retour vers la simplicité des origines, retour qui s’accompagne cependant d’un
souci réaffirmé envers de bonnes performances. En quelque sorte on recherche
le meilleur des deux premières périodes. À la fin des années 1970, sans doute
lassés des gains en efficacité qui s’amenuisent, et qui en outre se payent par une
difficulté inversement proportionnelle à mettre au point des programmes opé-
rationnels, des chercheurs entament une double révolution : celle des structures
Avant-propos 3

autoadaptatives et celle de l’analyse amortie de la complexité. Elle se fonde sur le


constat suivant : restructurer systématiquement et aveuglément une structure de
données peut parfois permettre d’obtenir une bonne efficacité moyenne, à condi-
tion que celle-ci porte sur une séquence d’appels à une opération. C’est le début
de la troisième période, qui voit par exemple l’avènement des arbres déployés
(splays trees) ou encore, pour les files de priorité, des minimiers obliques (skew
heaps). La quatrième période débute en 1989, par la publication de la première
structure de données aléatoires 2 [105]. Certes la notion d’algorithme aléatoire
est, à cette date, loin d’être une idée nouvelle. Il semble cependant que personne
n’ait précédemment songé à administrer une dose d’aléatoire aux structures de
données afin de leur garantir un bon équilibre avec une forte probabilité. Il en
résulte des algorithmes très simples et le plus souvent très efficaces.
Le troisième engagement que nous nous efforçons de respecter est celui de
l’histoire de la discipline. Les quatre périodes évoquées ci-dessus ont encore à
nous apprendre. D’une certaine façon elles se poursuivent toutes les quatre et se
superposent en partie. Nous avons évité soigneusement d’exposer un catalogue
de structures de données. La perspective historique nous permet d’étudier des
échantillons variés autant qu’intéressants de structures de données typiques.
Ces trois engagements transparaissent clairement à travers la lecture de l’ou-
vrage et sa structuration. La distinction spécification/représentation est appli-
quée à chaque structure de données étudiée. Chacun des cinq types de données
étudiés est spécifié formellement. C’est la spécification abstraite. Chacune des
structures de données qui les mettent en œuvre est spécifiée (c’est la spécification
concrète). Le passage de la spécification concrète des opérations à leur représen-
tation s’effectue en utilisant un modèle fonctionnel de calcul. Enfin, pour ce qui
concerne la perspective historique, elle transparaît à travers le choix progressif
des différentes représentations concrètes étudiées.

Motivations
Le corps de cet ouvrage constitue depuis plusieurs années la substance d’un
cours sur les structures de données, cours dispensé en première année du cycle
d’études lsi (Logiciel et Systèmes Informatiques) à l’Enssat (École Nationale
Supérieure de Sciences Appliquées et de Technologie, Université de Rennes 1 et
Université européenne de Bretagne), école d’ingénieurs localisée à Lannion. Cet
enseignement a marqué une rupture avec l’approche plus traditionnelle pratiquée
précédemment. De l’avis des nombreux intervenants, il a aussi constitué une
avancée à travers plusieurs aspects : fondements solides, démarche rigoureuse et
progressive, choix motivés, séparation claire entre ce qui relève de la stratégie et
ce qui revient au calcul, etc. (cf. [49]). Aucun des enseignants impliqués dans ce
renouveau ne souhaite revenir à la situation précédente.
Il nous a semblé intéressant de faire profiter la communauté de notre expé-
rience et, sans prétention excessive, de contribuer à faire progresser l’enseigne-
ment de cette discipline.

2. Si l’on exclut les techniques de hachage qui, par certains côtés, peuvent se classer dans
cette catégorie.
4 Structures de données et méthodes formelles

Spécification, raffinement et calcul


Étudier les structures de données et les algorithmes qui s’y rattachent fait
depuis toujours partie de la formation de base de l’informaticien. Les approches
classiques rencontrées dans les ouvrages s’y consacrant se limitent souvent à
répertorier les représentations usuelles, en réservant un chapitre aux listes, un
second aux arbres, un troisième aux piles, etc.
Pourtant cette forme de présentation ne fait qu’entretenir la confusion qui
peut exister entre « type abstrait » (ou spécification de structures de données
abstraites) et « type concret » (ou mise en œuvre, ou encore raffinement). Un
type de structure de données se construit à partir d’un type (a priori) plus
basique : une file peut se construire à partir d’une liste (ou d’un tableau), une
file de priorité peut se construire à partir d’un arbre binaire, etc. Le type ultime
à partir duquel s’implantent tous les autres est la (les) mémoire(s) adressable(s).
Ce principe est le plus souvent et, à juste titre, ignoré de l’utilisateur. Il
l’est aussi parfois, hélas, du développeur. Pourtant nul ne devrait aujourd’hui
sous-estimer le bénéfice qu’il est possible de retirer de cette connaissance. En
exploitant la relation qui lie un type concret au type abstrait qui lui sert de
référence, il est possible de guider la recherche d’une représentation pour les
opérations accompagnant la structure de données proprement dite et d’obtenir
ainsi des programmes qui, par construction, sont conformes à leur spécification.
C’est l’approche défendue dans cet ouvrage.

Le lecteur déjà familiarisé avec les langages les plus récents peut constater que
ceux-ci offrent, à travers de volumineuses bibliothèques, un accès à une grande
variété de structures de données dont l’utilisation, à travers des « api » 3 , est
immédiate. Pourquoi alors introduire l’étude des structures de données dans les
cursus informatiques ? Trois raisons au moins peuvent être avancées :
– En informatique comme ailleurs, la meilleure façon de comprendre les fonc-
tionnalités d’un outil est probablement d’en façonner un exemplaire. Les
capacités de l’outil mais aussi ses limites deviennent alors évidentes.
– Malgré le soin que peut prendre le concepteur de la bibliothèque d’un lan-
gage à choisir ses structures de données fondamentales, il lui est impossible
de les répertorier toutes. Une nouvelle structure de données se définit par-
fois comme la composition de structures de données existantes (tableau
flexible de files, files de priorité de tableaux flexibles, etc.). Là, le travail de
conception est aisé, celui d’une mise en œuvre efficace ne l’est pas toujours
(cf. les ensembles de chaînes du chapitre 7). Mais le plus souvent une nou-
velle structure de données est complètement originale ou étend par son jeu
d’opérations une structure de données préexistante. Il est alors nécessaire
de faire un travail de définition abstraite et de mise en œuvre concrète
semblable à celui qui est effectué tout au long de cet ouvrage.
– Les structures de données génériques disponibles en bibliothèque sont
certes fonctionnellement bien identifiées. Par contre, en l’absence de normes
concernant les performances, l’utilisateur doit parfois déchanter. Il lui reste
3. api : Application Programming Interface. Sigle consacré pour dénommer l’ensemble des
procédures et des fonctions disponibles pour consulter ou modifier l’état d’une structure de
données, d’une application, d’un système d’exploitation, etc.
Avant-propos 5

alors à faire œuvre de concepteur et de développeur. Les connaissances pré-


sentées dans cet ouvrage se révèlent alors bien utiles.

Calcul de programmes
Alain dit à sa fille de 8 ans : « Dans deux ans je serai exactement quatre fois
plus âgé que toi. Quel est mon âge ? » Voilà un exercice typique de calcul posé en
classe de CM2 et pour la résolution duquel le maître laisse en général libre court
à l’imagination de l’élève. Qui d’entre nous n’a pas été tout à la fois émerveillé
et fasciné d’apprendre, quelques années plus tard, qu’il existe une démarche
qui, après avoir « mis en équation » l’énoncé et moyennant la connaissance de
quelques règles de calcul, permet de répondre sans effort à la question posée ?
C’est en quelque sorte à la prolongation de cette aventure scientifique que cet
ouvrage nous invite, appliquée à l’informatique et plus précisément aux struc-
tures de données. . . Les articles [64, 41, 42] constituent de ce point de vue un
bon complément au contenu du présent ouvrage.

Programmation et structures fonctionnelles


Revenons à présent sur l’un des choix qui conditionnent la méthodologie
utilisée ici : la programmation fonctionnelle et, par ricochet, les structures de
données fonctionnelles. Le principe de la programmation fonctionnelle consiste
à considérer qu’exécuter un programme (réaliser un calcul) s’identifie à évaluer
des fonctions (au sens mathématique de ce terme). Les notions classiques d’état
et de changement d’état, qui se traduisent en programmation impérative par les
notions de variable et d’affectation n’ont donc pas cours.
Strictement parlant, les programmes fonctionnels ne « manipulent » pas de
(structures de) données. Ils peuvent les consulter pour en extraire des infor-
mations, ils peuvent également les prendre comme argument pour délivrer une
nouvelle version, mais ils ne modifient ni ne détruisent une structure de données
existante.
Pour quelles raisons avons-nous réalisé ce choix ? De nombreux arguments
pourraient être avancés en sa faveur ou à son encontre. Celui qui a été décisif est
la facilité avec laquelle il permet le calcul de programmes. Ce choix a néanmoins
d’autres conséquences que nous passons en revue. Tout d’abord la persistance.
La persistance est la propriété que possèdent les programmes fonctionnels de ne
pas détruire les versions préexistantes d’une donnée tout en rendant disponible
la nouvelle version. Illustrons ce propos à travers l’exemple de la « mise à jour »
d’une liste triée, par adjonction d’un élément. Considérons tout d’abord le cas
classique des structures éphémères (non persistantes). Soit l la liste triée suivante
dans laquelle nous souhaitons insérer la valeur 6 :

l • 2 • 4 • 7 • 9 /

Passons sur l’aspect algorithmique pour en arriver au résultat : la valeur 6 a


été insérée et la cellule qui désignait la valeur 7 a été modifiée, elle désigne
maintenant la cellule nouvellement créée. La liste désignée par l est toujours
triée.
6 Structures de données et méthodes formelles

6 •

l • 2 • 4 • 7 • 9 /

Regardons à présent ce qui se passe si l’opération est réalisée de manière fonc-


tionnelle. La liste de départ est identique au cas de la solution impérative. Par
contre la structure obtenue en résultat est la suivante :

2 • 4 • 6 •

l • 2 • 4 • 7 • 9 /

Bien que la liste initiale soit intacte, elle est devenue inaccessible puisque l dé-
signe à présent la nouvelle liste. Il suffirait cependant d’une pincée de program-
mation impérative pour préserver, dans une variable annexe lV ieux, l’ancienne
liste qui resterait alors consultable ou même modifiable. La structure est deve-
nue persistante. En l’absence d’un tel artifice, les anciennes cellules contenant
les valeurs 2 et 4 seraient inaccessibles. La disponibilité d’un ramasse-miettes
(garbage collector ) est alors appréciable pour restituer ces deux cellules au « pot
commun » de la mémoire libre (le « tas »).
Nous remarquons que pour obtenir la nouvelle liste il est nécessaire de dupli-
quer tout ou partie (selon la position d’insertion) de la liste initiale en créant de
nouvelles cellules, entraînant un surcoût en temps d’exécution. Dans l’exemple
ci-dessus ce surcoût peut sembler rédhibitoire : nous recopions la moitié de la
liste. Imaginons un instant qu’au lieu d’une liste nous disposions d’un arbre.
L’adjonction d’une valeur va certes entraîner là aussi une duplication de cer-
tains nœuds. Mais cette fois la proportion des nœuds dupliqués est (en général)
négligeable par rapport au nombre total de nœuds de l’arbre.
Imaginons à présent que la structure de données soit constituée d’un tableau
que nous cherchons à mettre à jour. Le principe de la programmation fonction-
nelle, qui interdit de modifier une structure existante, va conduire à dupliquer
la totalité du tableau. Cette fois le coût par rapport à une méthode qui autorise
les modifications in situ est incomparablement plus élevé. Cependant, puisqu’il
ne s’agit pas d’une impossibilité logique, nous admettons et utilisons l’approche
fonctionnelle, y compris pour les structures comprenant des tableaux. Néanmoins
il faut être conscient qu’une étape de raffinement ultime est alors nécessaire pour
atteindre l’efficacité souhaitée.
La persistance des structures de données est une notion intéressante en soi :
elle permet de mettre en œuvre des bases de données temporelles ou des sau-
vegardes en ligne, qui autorisent l’accès aux états passés d’un système. Elle est
également à l’origine d’applications en géométrie calculatoire et en calcul paral-
lèle. Pour notre part le seul cas où nous l’exploitons est celui de la mise à jour
des arbres Avl (cf. section 6.6), afin de connaître l’état d’équilibre passé pour
établir un nouvel équilibre sans être obligé de mémoriser celui-là dans chacun des
Avant-propos 7

nœuds de l’arbre. Dans l’ouvrage [71], le chapitre « Persistent data structures »


de H. Kaplan est consacré à l’étude des structures persistantes.
Le mode de passage de paramètres utilisé pour l’application d’une fonction
à ses arguments est décrit au chapitre 1. Il correspond à celui connu sous le nom
de passage par valeur (tous les arguments sont évalués une fois, lors de l’appel).
L’ouvrage [93] étudie l’influence des modes de passage sur les différentes formes
de complexité.

Plan et guide de lecture


Cet ouvrage s’adresse à toute personne désireuse de faire évoluer sa connais-
sance et sa pratique des structures de données ainsi que de leurs liens avec
les méthodes formelles. Il devrait donc intéresser les professionnels déjà expé-
rimentés mais qui souhaitent s’ouvrir à des approches plus rigoureuses, ou qui
souhaitent rafraîchir et restructurer leurs bagages. Bien évidemment il s’adresse
également aux enseignants et aux étudiants en informatique. Il peut servir d’ou-
vrage de référence à un cours de premier ou de second cycle universitaire sur les
structures de données. Compte tenu de son orientation, qui met l’accent sur une
démarche de conception au détriment parfois de la couverture du domaine, il
peut être complété, pour un enseignement de troisième cycle, par des ouvrages
spécialisés sur l’algorithmique des graphes, sur les techniques de hachage, sur la
complexité, etc. L’ouvrage de D. Knuth [75] par exemple et les ouvrages de la
série constituent de bons compléments.

Plan de l’ouvrage

Le livre est organisé en deux parties qui se déclinent en dix chapitres. La


première partie est consacrée aux bases nécessaires à l’étude de la seconde par-
tie. Cette dernière développe cinq structures de données parmi les plus fonda-
mentales. Le choix de ces cinq structures est d’une certaine façon arbitraire.
Pourquoi écarter de ce référentiel des structures importantes comme les graphes
ou les structures de partition ? Pourquoi classer, par exemple, les arbres dans
les structures outils ? Il n’y a pas de réponse définitive à ce type de question,
seul l’intérêt pratique prime et le lecteur confronté à des impératifs différents est
invité à construire son propre référentiel. Le même type de remarque peut être
formulé à propos des opérations accompagnant chaque type abstrait, où seuls les
« constructeurs » sont incontournables. La contrainte la plus forte est l’existence
d’une structure concrète de nature fonctionnelle pure ou semi-fonctionnelle (au-
torisant les tableaux).
Chaque section est accompagnée d’une conclusion dans laquelle un certain
nombre de commentaires de nature bibliographique et de remarques sont formu-
lés.
1. Le chapitre 1 regroupe les connaissances mathématiques nécessaires à la
spécification et à la mise en œuvre des structures de données, depuis la
théorie des ensembles jusqu’aux structures génériques nécessaires à des
réalisations efficaces. Ce chapitre emprunte beaucoup à la méthode B.
8 Structures de données et méthodes formelles

2. Le chapitre 2 présente la méthodologie de développement utilisée tout


au long de l’ouvrage. Son titre (Spécification abstraite + Spécification
concrète + Fonction d’abstraction + Calcul = Programme) résume parfai-
tement son contenu. La notion de fonction d’abstraction, qui décrit formel-
lement comment une structure concrète représente une structure abstraite,
est essentielle pour la suite.
3. Le chapitre 3 s’arrête sur trois structures de données particulières que nous
croisons régulièrement dans les différentes mises en œuvre de la partie II :
les listes finies, les arbres finis et les sacs finis.
4. Le chapitre 4 est entièrement consacré aux notions liées à l’analyse de
la complexité d’algorithmes. Il s’agit d’un chapitre court, de nombreux
ouvrages sont consacrés à ce thème, nous n’avons pas jugé utile de le déve-
lopper excessivement. De par sa relation privilégiée avec les structures de
données, la complexité amortie (et en particulier la méthode du potentiel)
a cependant été l’objet d’une attention particulière.
5. Le chapitre 5 achève la première partie en développant, selon la démarche
introduite au chapitre 2, deux exemples simples.
6. La partie II débute par le chapitre 6 qui est consacré à la représentation des
ensembles finis de scalaires. Ce chapitre est long. Le sujet le mérite. Sept
types de représentations sont étudiés, dans un ordre « chronologique »,
depuis la méthode des arbres binaires de recherche jusqu’aux méthodes
aléatoires.
7. Le chapitre 7 étend en quelque sorte le champ d’étude du chapitre précé-
dent pour s’intéresser aux ensembles composites, qu’il s’agisse de chaînes
ou d’ensembles de couples. Ce dernier cas résiste pour l’instant aux assauts
des informaticiens lancés dans la course à l’efficacité et constitue de ce fait
un thème ouvert et un défi intéressant. . .
8. Le chapitre 8 est dédié aux files simples. Ce chapitre est bref. À cela deux
raisons : les mises en œuvre naïves sont très efficaces et connues de tous,
en outre elles ne sont pas fonctionnellement pures (elles exigent la mani-
pulation de pointeurs ou font usage de tableaux). Nous avons cependant
mis l’accent sur une représentation particulière qui dans notre contexte
présente des caractéristiques intéressantes : la représentation (chaînée) par
double liste. D’une part c’est une solution très élégante et efficace ; d’autre
part elle constitue, par sa simplicité, une porte d’entrée abordable dans
l’univers de la complexité amortie.
9. Le chapitre 9 est consacré à une structure de données qui est l’objet de
beaucoup d’attentions de la part des spécialistes : les files de priorité.
Cela s’explique par le large éventail d’applications où elles interviennent.
Trois mises en œuvre sont présentées. Elles couvrent un large spectre des
méthodes et connaissances dispersées dans le reste de l’ouvrage.
10. Enfin le chapitre 10 traite des tableaux flexibles. Un sujet habituellement
plutôt discret. Serait-ce en raison de l’intégration de cette fonctionnalité
dans beaucoup de langages récents ou de l’efficacité relative des mises en
œuvre par réallocation ? Qu’importe, les réalisations proposées sont très
intéressantes pédagogiquement parlant, qu’il s’agisse des arbres de Braun,
aussi astucieux qu’efficaces, ou de l’implantation par des minimiers.
Avant-propos 9

Parcours
Le chapitre 5 donne au lecteur pressé un bon aperçu de l’ensemble de l’ou-
vrage. Selon le niveau de l’enseignement visé, plusieurs types de parcours peuvent
être proposés à l’utilisation de cet ouvrage. Nous préconisons cependant à chaque
fois de débuter ou de faire précéder l’enseignement en question par, grosso modo
la matière du chapitre 1, c’est-à-dire par une initiation aux mathématiques dis-
crètes, aux notations de la théorie des ensembles et aux techniques de preuve
qui l’accompagnent. Parmi celles-ci, insistons sur les démonstrations par récur-
rence/induction, qui doivent être maîtrisées.
Pour un cours d’initiation aux structures de données, sur une base de 30
heures environ, qui se limiterait aux structures élémentaires, nous conseillons les
chapitres 2, 5, 6 (restreint aux cinq premières sections de ce chapitre) et 8.
Pour un cours de second cycle, d’une durée comprise entre 60 et 100 heures,
l’ensemble des chapitres doit être abordé. Un choix peut être réalisé parmi les
structures étudiées dans le détail. Il n’est pas indispensable par exemple d’appro-
fondir toutes les structures équilibrées ni toutes les structures autoadaptatives.
Pour un cours avancé, l’accent peut être mis sur la méthodologie (chapitres 1
et 2, et section 4.3 du chapitre 4) et sur quelques structures de données parmi
les plus récentes (structures autoadaptatives, aléatoires, arbres de Braun, etc.).
À l’exception de ce dernier parcours, il est bien entendu indispensable que le
cours soit accompagné d’enseignements dirigés et pratiques. Les nombreux exer-
cices proposés à la fin de la plupart des sections constituent une source à laquelle
il est possible de s’approvisionner. Les enseignements pratiques doivent bien en-
tendu conduire à mettre en œuvre les structures de données, mais ils doivent
aussi être l’occasion de mettre en pratique un cycle complet de développement
depuis la spécification informelle jusqu’au produit final accompagné d’une docu-
mentation de type « rapport de développement » et « guide de l’utilisateur ».
Le langage de programmation utilisé à l’occasion des enseignements pratiques
peut être quelconque, depuis un langage algorithmique comme Pascal, Ada ou
C, jusqu’aux langages fonctionnels (voire logiques) tel que ML, CAML, Haskell
ou Lisp, en passant par les langages objet. Il est même tout à fait possible, d’une
séance à la suivante, de faire varier le niveau de langage. Ceci peut contribuer à
une meilleure compréhension des mécanismes de la programmation fonctionnelle.
Bien entendu l’effort de traduction manuel est proportionnel au degré de proxi-
mité avec le langage de description utilisé dans ce livre. Standard ML constitue
probablement l’un des optimums.
Ainsi que nous l’avons annoncé ci-dessus, l’organisation de la partie II permet
un parcours différent, orthogonal à une lecture séquentielle. Pour les personnes
intéressées, il est possible d’adopter une lecture par thèmes selon une progres-
sion chronologique : les structures naïves de la première période, les structures
équilibrées, les structures autoadaptatives et les structures aléatoires.

Avertissements
Il est important d’avertir les étudiants des contraintes inhérentes au modèle
fonctionnel : l’utilisation de tableaux traditionnels est coûteuse, la manipula-
tion directe de pointeurs est interdite (excluant certaines implantations clas-
10 Structures de données et méthodes formelles

siques comme les files avec pointeurs de tête et de queue, les listes circulaires, les
listes doublement chaînées, les arbres inverses, etc.), la notion d’état n’est pas
pertinente. Si la durée du module d’enseignement le permet, il est possible de
consacrer une fraction du temps à des exercices sur la traduction des structures
(persistantes) obtenues en structures éphémères classiques, à l’introduction de
la notion d’état par le remplacement des fonctions par des procédures et par la
suppression des appels récursifs terminaux (cf. [24] et [61] pour des exemples).
La pratique de la démarche prônée dans cet ouvrage va naturellement
conduire un étudiant standard à s’imaginer que la difficulté de la démarche réside
dans les calculs. L’enseignant devra sans doute déployer quelques efforts pour
le convaincre que les vraies difficultés se situent dans les choix stratégiques qui
régentent le calcul et surtout dans la découverte des bonnes structures concrètes.

Prérequis
Cet ouvrage ne s’adresse pas à l’informaticien débutant. Pour en tirer pro-
fit, il est nécessaire d’avoir préalablement acquis une expérience d’au moins 30
heures en programmation classique et de maîtriser les mathématiques dispensées
en première année d’un enseignement supérieur scientifique. Une expérience en
récursivité est souhaitable. Une connaissance de base portant sur les structures
de données les plus élémentaires et les pointeurs peut être appréciable.

Terminologie
Nous présentons ici une définition de quelques termes fréquemment utilisés
dans le reste de l’ouvrage.
Calcul de programmes : méthode formelle qui permet d’obtenir un pro-
gramme exécutable à partir de sa spécification en décomposant le déve-
loppement en étapes de calculs élémentaires. Type de méthode fondé sur
l’application de règles, de stratégies et d’heuristiques issues des propriétés
sémantiques des constructions utilisées. On parle également de dérivation
de programmes. Le calcul de programmes s’applique aussi bien à la pro-
grammation impérative qu’à la programmation fonctionnelle comme c’est
le cas ici.
Fonction d’abstraction : il s’agit d’un terme propre à la méthode exposée
dans cet ouvrage (cf. [56, 101, 28] cependant). Une fonction d’abstraction
explicite comment une structure de données concrète réalise une structure
de données abstraite. Par exemple, c’est une fonction d’abstraction qui per-
met d’obtenir un ensemble (structure abstraite) à partir d’une liste (struc-
ture concrète). Les fonctions d’abstraction n’ont pas à être implantées, leur
utilité se limite à la spécification concrète et au calcul des opérations.
Méthode formelle : terme employé en informatique et en génie logiciel en par-
ticulier. Ensemble de techniques basées sur les mathématiques discrètes qui
permettent de spécifier, de prouver et de développer des systèmes compre-
nant des composants logiciels.
Avant-propos 11

Opération : ce terme est utilisé pour qualifier les opérateurs s’appliquant aux
valeurs d’un type de données. Du point de vue informatique, les opéra-
tions se présentent soit sous la forme de procédures (ou de routines) soit
sous la forme de fonctions. Pour ce qui nous concerne, ce terme recouvre
uniquement des fonctions. Nous distinguons les opérations de la spécifica-
tion abstraite, celles de la spécification concrète et la mise en œuvre de ces
dernières (appelées représentations).
Programmation fonctionnelle : paradigme de programmation qui assimile
tout calcul à l’application d’une fonction à des arguments. Un des plus
populaires parmi les nombreux paradigmes existants (cf. [57] pour une
synthèse de quatre d’entre eux). Il est possible de simuler ce style de pro-
grammation en utilisant un langage impératif (c’est-à-dire avec variables),
il suffit d’appliquer rigoureusement la règle de codage qui stipule qu’« on
ne modifie jamais une structure existante. » Les structures de données ob-
tenues sont qualifiées de fonctionnelles.
Raffinement : appliqué au développement informatique, ensemble de mé-
thodes permettant de passer de la spécification d’un système à sa réali-
sation, en intégrant graduellement des détails de plus en plus fins.
Spécification : texte (formalisé ou non) qui explicite les propriétés d’un sys-
tème. Pour notre part, nous distinguons la spécification abstraite (cf. ci-
dessous la notion de « structure de données » dans sa première acception)
de la spécification concrète, qui établit les relations existant entre proprié-
tés abstraites et concrètes à travers la fonction d’abstraction.
Structure de données : selon C. Okasaki (cf. [93]), ce terme couvre au moins
quatre significations différentes que le contexte permet en général de dis-
tinguer :
1. Un type de données accompagné des opérations (de consultation ou
de mise à jour) qui lui sont associées. Les termes algèbre, type, spé-
cification abstraite, spécification algébrique, abstraction de données
peuvent en général lui être substitués.
2. Une réalisation concrète d’une spécification abstraite, que nous appe-
lons aussi parfois implantation, implémentation, représentation. C’est
l’entité obtenue à l’issue de la mise en œuvre d’une spécification abs-
traite.
3. Une instance d’une réalisation concrète, qui est créée à un instant t0
et qui évolue au cours des mises à jour jusqu’à sa disparition.
4. Dans le cadre de structures persistantes, les différentes versions qui se
succèdent au cours de l’évolution de la structure. C. Okasaki utilise
le terme d’identité persistante.
Ajoutons à cette liste la configuration d’une instance à un moment donné.
C’est dans ce sens qu’il est utilisé lorsque nous écrivons : « la structure de
données contient la valeur 6. »
Type de données : c’est l’ensemble (fini ou non) des valeurs que peuvent
prendre les entités placées sous cette dénomination. Le type de données
entiers naturels s’identifie à {0, 1, 2, . . .}. Un type de données est en géné-
ral indissociable des opérations qui permettent de manipuler les valeurs de
12 Structures de données et méthodes formelles

l’ensemble en question. Le terme est à rapprocher de structure de données


dans la première acception mentionnée ci-dessus.

Convention de style
Des soucis d’homogénéité de style conduisent certains auteurs à adopter sys-
tématiquement l’un des types de formulation suivant, à l’exclusion des deux
autres : « Je . . . », « Nous . . . », « On . . . ».
Notre choix est différent. « Nous », sous sa forme sylleptique, est utilisé
lorsque nous évoquons une décision, une argumentation personnelle de l’auteur,
comme dans « Nous allons étudier les rotations . . . » (comprendre « l’auteur a
pris la décision d’étudier les rotations à ce moment-là du discours »). « On » est
utilisé pour introduire des considérations d’ordre général comme dans « Il est
rare que l’on utilise des ensembles de files » (comprendre « quiconque dans la
communauté des informaticiens n’utilise que rarement des ensembles de file »).

Remerciements
Nous tenons à exprimer nos sincères remerciements à l’Enssat et à son person-
nel, ainsi qu’à Pierre-Noël Favennec et à Henri Habrias. Qu’il nous soit également
permis de citer les personnes suivantes en témoignage de leur influence, de leur
soutien ou de leur amitié : J.-R. Abrial, P. Alain, C. Attiogbé, J.-P. Banâtre,
P. Bosc, J. Courtin, A. Delhay, A. Hadjali, D. Herman, H. Jaudoin, P. Quin-
ton, D. Lolive, G. Mercier, L. Miclet, J.-F. Monin, T. Napoléon, J.-C. Pettier,
D. Rocacher, C. Roussel, M.D. Sadek, J. Seguin, J. Siroux, G. Smits, M. Tréhel,
L. Trilling, J. Wolf. Nous remercions également les étudiants du cycle lsi de
l’Enssat pour leur contribution involontaire.
Première partie

Les bases
Chapitre 1

Mathématiques pour la
spécification et les structures
de données

1.1 Introduction
Il est communément admis, dans toutes les disciplines de l’ingénieur,
qu’après avoir couché sur le papier, en français, les caractéristiques du système
que l’on souhaite réaliser, il est nécessaire de modéliser (on dit aussi spécifier)
formellement ces caractéristiques avant de passer à la réalisation effective. C’est
vrai dans l’automobile, l’aviation ou le génie civil. Les avantages sur une approche
purement empirique sont connus mais méritent d’être rappelés : le modèle formel
n’est pas ambigu, il peut servir de « contrat » entre les parties prenantes ; il per-
met de démontrer les propriétés recherchées avant de passer à l’étape suivante ;
il facilite l’utilisation d’outils (notamment informatiques) pour « donner vie »
au modèle, il autorise la détection de problèmes ou d’erreurs avant d’avoir trop
investi dans le développement. Pour les activités industrielles sus-citées, spéci-
fier formellement signifie utiliser l’outil mathématique (et notamment le calcul
infinitésimal).
De ce point de vue, la mise en œuvre de systèmes informatiques (ou compre-
nant des composants logiciels) ne devrait pas s’écarter du schéma traditionnel.
Pourtant, bien que les pionniers ne s’y soient pas trompés (cf. [85] sur l’utilisation
par A. Turing de la logique enrichie de l’arithmétique pour réaliser la démonstra-
tion de la correction d’un programme), la nature discrète et immatérielle de la
discipline informatique a pu en troubler quelques-uns. Certains ont même accré-
dité l’idée d’une « exception informatique » qui ferait de la programmation une
discipline expérimentale ! Spécifier s’est longtemps limité à produire un texte en
français. Une première brèche a été ouverte à la fin des années 1960 avec l’usage
de la logique des prédicats comme moyen de spécification. Celle-ci (enrichie de
l’arithmétique, de l’égalité, de la notion de tableau) a été utilisée dans les pre-
miers travaux influents sur la preuve de programmes ou sur la construction de
16 Structures de données et méthodes formelles

programmes corrects (cf. [55, 30, 31]), avec à la clé un effet pervers imputable à
la difficulté de transposer ces méthodes à des applications industrielles. Cet effet
pervers a conduit certains à jeter le bébé avec l’eau du bain. Au cours des années
1970 sont apparues des méthodes graphiques semi-formelles 1 telles que le mo-
dèle entité-relation, Merise, ssadm, etc. et dont la postérité est aujourd’hui bien
implantée. Pourtant, à bien y regarder, ces méthodes ne sont ni plus ni moins
que des avatars notationnels de la (nous devrions dire d’une) théorie des en-
sembles. Pourquoi alors ne pas utiliser directement cette théorie ? C’est le choix
réalisé par la méthode B [3] (ainsi que par son successeur, la méthode Event-B
[5]) pour ce qui concerne la modélisation des données et la preuve. En raison de
ses nombreuses qualités, nous avons décidé d’adopter la théorie des ensembles
déclinée dans le formalisme des méthodes B/Event-B en tant que support formel
de notre démarche. Avant de présenter le principe de la preuve et la démarche
ensembliste de B, comparons, sur un exemple, les trois formes de spécification
mentionnées ci-dessus : informelle, prédicative et ensembliste.

Considérons le « système » constitué d’un échiquier 8 × 8 sur lequel sont


placées huit reines qui ne se menacent pas mutuellement, comme dans le schéma
ci-dessous :

8
0Z0L0Z0Z
7
Z0Z0Z0L0
6
0ZQZ0Z0Z
5
Z0Z0Z0ZQ
4
0L0Z0Z0Z
3
Z0Z0L0Z0
2
QZ0Z0Z0Z
1
Z0Z0ZQZ0
a b c d e f g h

Ceci constitue la caractérisation du système en question. Elle est concise et


compréhensible par toute personne maîtrisant le français. . . et les règles du jeu
d’échec. Incidemment, ce schéma nous offre l’occasion de nous interroger sur
les avantages et inconvénients des schémas en tant qu’outils de spécification.
Un schéma est incontestablement plus parlant qu’un texte ou qu’une formule
mathématique. À l’inverse, un schéma n’est le plus souvent qu’un exemple pos-
sible du système à réaliser (c’est particulièrement clair ici puisqu’il existe bien
d’autres configurations acceptables), qui de ce fait peut induire en erreur. Un
schéma interdit également toute forme de raisonnement rigoureux. Pour ces dif-
férentes raisons, nous ne considérons un schéma que comme un complément à
une description rigoureuse (cf. encadré page 199 pour un développement de cette
opinion).

Utilisons à présent la logique des prédicats pour formaliser cette spécification.


Cela exige de préciser deux points.
1. Pour lesquelles la syntaxe est formalisée, la sémantique ne l’est pas.
1. Mathématiques pour la spécification et les structures de données 17

– La « structure de données » : un tableau de 8 × 8 booléens r tel que r(i, j)


vaut true si et seulement s’il existe une reine en position (i, j).
– L’absence de menace : s’il existe une reine en position (i, j), alors il n’y a
pas d’autres reines sur la ligne i ni sur la colonne j. Il n’y a pas non plus
d’autres reines sur les deux diagonales se croisant à la position (i, j).
En admettant que l’échiquier r soit représenté par la déclaration r ∈ (1 .. 8)×
(1 .. 8) → bool qui définit r comme un tableau de booléens de 8 lignes et de 8
colonnes (contrairement à la tradition échiquéenne, les colonnes sont ici identi-
fiées par des nombres et non par des lettres), la spécification se traduit par la
formule prédicative suivante :

∀i, j ·(1 ≤ i ∧ i ≤ 8 ∧ 1 ≤ j ∧ j ≤ 8 ∧ r(i, j) = true ⇒


¬∃k ·(1 ≤ k ∧ k ≤ 8 ∧ k = i ∧ r(k, j) = true) ∧
¬∃k ·(1 ⎛≤ k ∧ k ≤ 8 ∧ k = j⎞∧ r(i, k) = ⎛
true) ∧ ⎞
1 ≤ k ∧ k ≤ 8∧ 1 ≤ k ∧ k ≤ 8∧
⎜ 1 ≤ l ∧ l ≤ 8∧ ⎟ ⎜ 1 ≤ l ∧ l ≤ 8∧ ⎟
⎜ ⎟ ⎜ ⎟
⎜ k = i ∧ ⎟ ⎜ k = i ∧ ⎟

¬∃k, l · ⎜ ⎟ ⎜ ⎟
l
= j ∧ ⎟ ∧ ¬∃k, l · ⎜ l = j ∧ ⎟
⎜ ⎟ ⎜ ⎟
⎝ k+l =i+j∧ ⎠ ⎝ k−l =i−j∧ ⎠
r(k, l) = true r(k, l) = true
)

∀i ·(1 ≤ i ∧ i ≤ 8 ⇒ ∃j ·(1 ≤ j ∧ j ≤ 8 ∧ r(i, j) = true))

Il est pour l’instant inutile de chercher à comprendre cette formule dans le détail.
Notons cependant qu’elle se présente sous forme conjonctive (c’est-à-dire sous
la forme de deux formules reliées par une conjonction ∧). Le premier conjoint
spécifie l’absence de menace mutuelle. Ce conjoint n’implique pas qu’il existe huit
reines sur l’échiquier, d’où le second conjoint qui affirme que chaque ligne est
dotée d’au moins une reine. La complexité de cette formule est manifeste. Cela est
certes imputable au fait qu’elle capte une connaissance complexe mais aussi au
fait qu’elle s’exprime dans un langage de bas niveau. Cependant, contrairement
à la spécification informelle, nous sommes en principe capables par exemple de
déduire de cette formule qu’il existe une reine sur chaque colonne de l’échiquier.

Venons-en à présent à une spécification ensembliste dans le style de celle qui


est utilisée au sein de la méthode B. L’échiquier est représenté par une relation
q entre deux instances de l’intervalle 1 .. 8 telle que (i, j) ∈ q si et seulement
s’il existe une reine à l’intersection de la ligne i et de la colonne j. La formule
suivante représente une spécification ensembliste possible :

q ∈ 1 .. 8 
 1 .. 8 ∧
λi ·(i ∈ 1 .. 8 | i + q(i)) ∈ 1 .. 8  2 .. 16 ∧ (1.1.1)
λi ·(i ∈ 1 .. 8 | i − q(i)) ∈ 1 .. 8  −7 .. 7

Le premier conjoint précise que la relation q est en fait une fonction bijective
totale qui de ce fait capte l’absence de prise mutuelle sur les lignes et colonnes.
Les deux derniers conjoints modélisent, grâce à deux fonctions injectives totales,
l’absence de prise mutuelle sur les diagonales.
18 Structures de données et méthodes formelles

La confrontation entre spécification prédicative et spécification ensembliste


est édifiante, abondance de quantificateurs d’un côté, concision de l’autre.

Formaliser les caractéristiques d’un système ne signifie pas contraindre le


modèle de façon à minimiser le nombre de solutions. Au contraire, il est pré-
férable de laisser un certain degré de liberté pour les étapes ultérieures. Ainsi,
pour reprendre l’exemple de l’échiquier, il nous faut les moyens linguistiques
de préciser que nous recherchons un échiquier quelconque qui satisfasse la for-
mule 1.1.1. C’est l’objectif des expressions qui sont ci-dessous qualifiées de non
déterministes.
Dans la suite de ce chapitre, nous précisons les caractéristiques de la théorie
des ensembles sur laquelle nous nous appuyons (cf. [3, 5]). Les aspects preuve
sont basés sur le calcul des séquents, qui est présenté à la section 1.2. Les sec-
tions 1.3 à 1.7 sont celles qui concernent plus précisément la théorie des en-
sembles et le noyau autour duquel elle est construite. La section 1.9 s’intéresse
aux structures inductives, celles-ci sont au cœur des mises en œuvre de beaucoup
de structures de données. La section 1.10 traite du cas particulier de l’induction
dans l’ensemble N des entiers naturels. La section 1.11 détaille le formalisme des
opérations.
Dans ce chapitre, nos objectifs sont moins ambitieux que ceux manifestés
dans [5] et surtout dans [3]. Il ne s’agit pas pour nous de construire la théorie des
ensembles mais d’utiliser sa richesse et d’exploiter les résultats qui en découlent.
En conséquence nous nous limitons à rappeler les principes généraux, sauf bien
entendu dans les rares cas où nous étendons la théorie ou bien ses notations.
Concernant les preuves réalisées à partir du chapitre 4, nous veillons à utiliser
autant que faire se peut les propriétés de la théorie (cf. annexes, page 403 et
suivantes). Le style utilisé pour réaliser les preuves est souvent plus relâché que
dans les deux ouvrages mentionnés ci-dessus.

Exercices

Exercice 1.1.1 Soit un échiquier et une collection de n reines. n est aussi grand qu’on le
souhaite. Spécifier la situation dans laquelle un nombre minimum de reines contrôle toutes les
cases. Fournir une version purement prédicative et une version ensembliste.

Exercice 1.1.2 Un carré magique normal d’ordre n est un tableau de n lignes et de n colonnes
contenant toutes les valeurs de l’intervalle 1 .. n2 , tel que la somme des valeurs de chaque ligne,
de chaque colonne et de chacune des deux grandes diagonales est identique. Spécifier de manière
totalement prédicative puis ensembliste un tel objet.

1.2 Raisonnement
Arrêtons-nous un instant sur la méthodologie utilisée pour construire une
théorie mathématique comme la théorie des ensembles. La première décision à
prendre concerne le choix du langage dans lequel s’expriment les conjectures,
les théorèmes, etc. Une solution possible est de choisir les séquents. Un séquent
se présente sous la forme H
B. H est appelé hypothèses et B but. B est une
1. Mathématiques pour la spécification et les structures de données 19

construction que nous appelons prédicat tandis que H est une collection finie,
éventuellement vide, de prédicats. Intuitivement H
B représente l’idée que la
conjonction des hypothèses de H entraîne B. Prenons un exemple simple : un
séquent permet d’exprimer que le but (1 .. 8 ∩ 3 .. 10) = ∅ est réalisé sous les
hypothèses 5 ∈ 1 .. 8 et 5 ∈ 3 .. 10 :

5 ∈ 1 .. 8, 5 ∈ 3 .. 10
(1 .. 8 ∩ 3 .. 10) = ∅ (1.2.1)

La syntaxe d’un séquent respecte les règles présentées ci-dessous. La syntaxe de


la catégorie prédicat est pour l’instant indéfinie.

séquent : := listePréd
prédicat |

prédicat
listePréd : := prédicat |
prédicat, listePréd

La seconde décision concerne le choix des règles d’inférence. Une règle d’in-
férence est la brique de base du raisonnement formel. Elle se présente sous la
A
forme où A est une collection finie, éventuellement vide, de séquents ap-
C
pelée antécédent, et C un séquent appelé conséquent. Une règle d’inférence dont
l’antécédent est vide s’appelle axiome.
Un séquent s est prouvé s’il existe une règle d’inférence dont le conséquent
s’identifie à s et si tous les séquents de l’antécédent sont prouvés. Il est facile
d’en déduire une stratégie élémentaire de preuve. On recherche une règle dont le
conséquent s’identifie au séquent. Si la règle est un axiome, le séquent est prouvé,
sinon on effectue la preuve de tous les séquents de l’antécédent. Illustrons ce
dernier point en considérant les cinq règles ci-dessous :


5 ∈ 4 .. 6
A: B: C:

5 ∈ 1 .. 8
5 ∈ 4 .. 6
5 ∈ 3 .. 10


5 ∈ 1 .. 8 ,
5 ∈ 3 .. 10
D:
5 ∈ 1 .. 8, 5 ∈ 3 .. 10
5 ∈ (1 .. 8 ∩ 3 .. 10)

5 ∈ 1 .. 8, 5 ∈ 3 .. 10
5 ∈ (1 .. 8 ∩ 3 .. 10)
E:
5 ∈ 1 .. 8, 5 ∈ 3 .. 10
(1 .. 8 ∩ 3 .. 10) = ∅

L’ensemble de ces règles d’inférence constitue une théorie. Les règles A et B n’ont
pas d’antécédent, ce sont des axiomes. Un théorème de la théorie peut se dé-
montrer en appliquant le principe décrit ci-dessus jusqu’à atteindre des axiomes.
L’ensemble de la démonstration se présente sous la forme d’un arbre fini dont la
racine est le séquent à prouver. Chaque nœud de l’arbre de démonstration est
constitué d’un séquent et de la règle utilisée pour le démontrer. La démonstration
de la formule 1.2.1 dans la théorie ci-dessus se présente sous la forme :
20 Structures de données et méthodes formelles

5 ∈ 1 .. 8, 5 ∈ 3 .. 10
(1 .. 8 ∩ 3 .. 10) = ∅
E

5 ∈ 1 .. 8, 5 ∈ 3 .. 10
5 ∈ (1 .. 8 ∩ 3 .. 10)
D


5 ∈ 1 .. 8
5 ∈ 3 .. 10
A C


5 ∈ 4 .. 6
B

Il s’agit maintenant d’exploiter ces notions (séquent, règle d’inférence, dé-


monstration) pour construire par étapes une théorie. La première étape consiste
à définir une théorie minimale captant le raisonnement mathématique de base.
Ceci peut se faire en proposant les trois règles d’inférence suivantes (présentées
horizontalement) :

Antécédent Conséquent Ident.


H, P
P (1.2.2)
H
Q H, P
Q (1.2.3)
H
P , H, P
Q H
Q (1.2.4)

La première règle rend compte du fait que si dans un séquent le but fait partie
des hypothèses alors le séquent est un théorème. La règle 1.2.3 capte le fait que si
H
Q est un théorème, alors en ajoutant n’importe quelle hypothèse on obtient
toujours un théorème. Enfin, la troisième règle, appelée règle de coupure, nous
apprend que pour démontrer Q sous les hypothèses H, il suffit de démontrer Q
sous les hypothèses H, P et de montrer P sous les hypothèses H.
Une théorie donnée peut être étendue en enrichissant la syntaxe des prédi-
cats constituant les séquents. Deux cas de figure peuvent alors survenir : soit les
extensions constituent de simples facilités de notation, auquel cas il faut sim-
plement définir les nouvelles notations à partir des notations existantes ; soit
il s’agit de nouvelles constructions, indépendantes des précédentes. Il faut alors
compléter la théorie existante par des règles d’inférence destinées à donner un
sens à ces nouvelles notations.

Exercice

Exercice 1.2.1 (inspiré de [58]) Considérons la théorie suivante où les formules sont constituées
des symboles , |, + et =.
1. Mathématiques pour la spécification et les structures de données 21

Antécédent Conséquent
 x+ |= x |
x+y =z  x + y |= z |

Les lettres x, y et z représentent des séquences de longueur finie du symbole |.


1. Prouver la formule suivante : ||| + ||=|||||.
2. Développer un prouveur pour cette théorie.

1.3 Calcul propositionnel


Trois opérateurs et un nouveau symbole sont ajoutés à la théorie de base
pour constituer le langage propositionnel. Le symbole ⊥ représente une formule
qui n’est pas un théorème (une formule improuvable). Les opérateurs ∧, ⇒ et ¬
se dénomment conjonction, implication et négation. Syntaxiquement le langage
initial s’étend comme le montrent les règles grammaticales suivantes :

prédicat : := ⊥ |
prédicat ∧ prédicat |
prédicat ⇒ prédicat |
¬prédicat
(prédicat)

L’attribution de priorités 2 et l’usage de parenthèses permettent de lever les


ambiguïtés véhiculées par cette grammaire. Dans la suite nous utilisons sou-
vent les espaces autour d’un opérateur afin de souligner une plus faible priorité.
La théorie minimale présentée à la section 1.2 s’enrichit des règles d’inférence
suivantes, qui rendent compte du comportement de chacun des nouveaux opé-
rateurs :

Antécédent Conséquent Ident.


H, ⊥
P (1.3.1)
H
P , H
¬P H
⊥ (1.3.2)
H
P, H
Q H
P ∧Q (1.3.3)
H, P, Q
R H, P ∧ Q
R (1.3.4)
H, P, Q
R H, P, P ⇒ Q
R (1.3.5)
H, P
Q H
P ⇒Q (1.3.6)
H, ¬Q
P H, ¬P
Q (1.3.7)
H, P
⊥ H
¬P (1.3.8)

Une extension purement notationnelle consiste à introduire les opérateurs


propositionnels ∨ et ⇔, ainsi que la notation . La grammaire s’enrichit de la
manière suivante :
2. L’annexe E précise la priorité des différents opérateurs.
22 Structures de données et méthodes formelles

prédicat : := ... |
 |
prédicat ∨ prédicat |
prédicat ⇔ prédicat

La définition des nouveaux symboles est donnée par le tableau suivant :

Notation Définition
 ¬⊥
P ∨Q ¬P ⇒ Q
P ⇔Q (P ⇒ Q) ∧ (Q ⇒ P )

Le lecteur étant supposé familiarisé avec la logique propositionnelle, les pro-


priétés de celle-ci sont utilisées sans démonstration ni rappel dans la suite de
l’ouvrage.

1.4 Calcul des prédicats


1.4.1 Quantification universelle
Le langage des prédicats enrichit le langage propositionnel de trois no-
tions : la notion de variable, celle de prédicat quantifié universellement et celle
d’expression. Débutons par la notion d’expression. La syntaxe d’une expression
se limite pour le moment à ce que permet la grammaire ci-dessous, mais elle est
destinée à s’enrichir notablement à la section 1.6.

expression : := variable |
expression → expression |
(expression, expression) |
(expression)
variable : := identificateur

Ainsi, les formules suivantes : x, x → y, (x, y), x → (y → z) sont des expressions.


Dans la suite, le terme formule est un terme générique qui regroupe prédicat et
expression. x → y est appelé couple, (x, y) est une notation alternative pour
x → y. Alors que les prédicats sont destinés à être prouvés, les expressions
représentent un objet (un entier, un ensemble, etc.). La dernière règle décrivant
la catégorie syntaxique expression exprime le fait qu’à l’instar des prédicats, il
est toujours possible de parenthèser une expression.
La syntaxe des prédicats quantifiés universellement est fournie par :

prédicat : := ... |
∀ listeVar · prédicat
listeVar : := variable |
variable, listeVar
1. Mathématiques pour la spécification et les structures de données 23

En supposant que la formule suivante est un prédicat (il faut pour cela attendre
la section sur la « théorie » des entiers naturels) : (x ∈ N∧y ∈ N⇒x ≤ y∨x ≥ y),
les formules
– ∀x ·(x ∈ N ∧ y ∈ N ⇒ x ≤ y ∨ x ≥ y),
– ∀x, y ·(x ∈ N ∧ y ∈ N ⇒ x ≤ y ∨ x ≥ y),
– ∀z ·(x ∈ N ∧ y ∈ N ⇒ x ≤ y ∨ x ≥ y),
sont des prédicats quantifiés universellement. Notons que les variables de quan-
tification de chaque listeVar doivent être différentes deux à deux, par contre
elles ne sont pas obligatoirement présentes dans la formule ainsi que l’illustre la
dernière formule.
L’introduction de prédicats quantifiés enrichit notablement la puissance d’ex-
pression du langage propositionnel. Ceci se traduit par l’extension du jeu des
règles d’inférence disponibles. Deux notions préalables liées à celle de variable
sont nécessaires à la définition des nouvelles règles d’inférence : celle d’occurrence
de variable non libre dans un prédicat et celle de substitution d’une variable par
une expression dans un prédicat. Ces notions sont définies rigoureusement dans
[3]. Nous effectuons ici un rappel informel.
Soit x une variable et P un prédicat. Toutes les occurrences de x sont libres
dans toutes les sous-formules non quantifiées de P . Si P est une formule quan-
tifiée, toutes les occurrences de x sont libres si x n’apparaît pas dans la liste
de quantification, aucune occurrence n’est libre si x apparaît dans la liste de
quantification. Ainsi la formule :

y = 5 ∧ ∀y ·(y ∈ N ⇒ y ≥ 0) ∧ ∀z ·(z ∈ N ⇒ z < y)

comporte deux occurrences libres de y. La première se situe dans le premier


conjoint et la seconde dans le dernier. Les occurrences de y du second conjoint
ne sont pas libres car elles sont dominées par la quantification. Si x ne présente
aucune occurrence libre dans un prédicat P , x est dite non libre dans P et l’on
note x\P . Les notions d’occurrence libre ou non libre s’étendent naturellement
aux autres quantificateurs que nous rencontrerons, ainsi qu’aux expressions.

Substitution. Une substitution est destinée à transformer une formule donnée


(prédicat ou expression) en une autre formule. Elle se note [x := E]F , où x est
une variable, E une expression et F une formule. Le résultat de la substitution
est la formule F dans laquelle toute occurrence libre de x est remplacée par E.
Ainsi par exemple :

[x := y]x + 5
= Définition de la substitution
y+5

ou encore :

[x := y + 3](2 · x = z ∧ ∀x ·(x ∈ N ⇒ x > z))


⇔ Définition de la substitution
2 ·(y + 3) = z ∧ ∀x ·(x ∈ N ⇒ x > z)
24 Structures de données et méthodes formelles

Dans ce dernier cas, le respect de la priorité des opérateurs exige l’introduction


de parenthèses.

Les deux nouvelles règles d’inférence nécessaires au raisonnement sur les pré-
dicats quantifiés universellement sont :

Antécédent Conséquent Condition Ident.


H, ∀x · P, [x := E]P
Q H, ∀x · P
Q (1.4.1)
H
P H
∀x · P x\H (1.4.2)
Les prédicats quantifiés existentiellement, définis syntaxiquement par :

prédicat : := ... |
∃ listeVar · prédicat
constituent une extension des notations du langage des prédicats. Ils se défi-
nissent par :

Notation Définition
∃x · P ¬∀x · ¬P

Exemples. La section 1.1 nous a déjà donné l’occasion d’utiliser le langage


des prédicats pour caractériser un système (celui des huit reines sur un échi-
quier). Afin d’enrichir notre expérience, complétons cet exemple par deux autres
exemples ayant trait à un tableau 3 d’entiers naturels t défini sur l’intervalle 1..n
(n ≥ 0). Exprimons tout d’abord que dans t, toutes les occurrences de la valeur
7 sont avant toutes les occurrences de la valeur 23 :
⎛ ⎛ ⎞⎞
∀i ·(1 ≤ i ∧ i < l ⇒ t(i) = 23)
∃l · ⎝1 ≤ l ∧ l ≤ n + 1 ∧ ⎝ ∧ ⎠⎠
∀j ·(l ≤ j ∧ j ≤ n ⇒ t(j) = 7)
Soit en paraphrasant : il existe (au moins) une frontière l en deçà de laquelle
il n’y a pas d’occurrence de la valeur 23 et à partir de laquelle il n’y a pas
d’occurrence de la valeur 7.
Formalisons maintenant la proposition qui affirme que les valeurs 7 et 23
alternent dans t :
⎛⎛ ⎞ ⎞
1≤i∧i<j∧j ≤n
∀i, j · ⎝⎝ ∧ ⎠ ⇒ ∃k ·(i < k ∧ k < j ∧ t(k) = 23)⎠
t(i) = 7 ∧ t(j) = 7
∧ ⎛⎛ ⎞ ⎞
1≤i∧i<j∧j ≤n
∀i, j · ⎝⎝ ∧ ⎠ ⇒ ∃k ·(i < k ∧ k < j ∧ t(k) = 7)⎠
t(i) = 23 ∧ t(j) = 23
Soit encore : s’il existe (au moins) deux occurrences de la valeur 7, alors il y a
au moins un 23 entre les deux, et réciproquement.
3. La notion de tableau ne fait pas pour l’instant partie de notre langage. Elle est introduite
en tant que fonction à la section 1.6.3.
1. Mathématiques pour la spécification et les structures de données 25

1.4.2 Deux nouveaux types d’expressions


Nous devons à présent étudier deux extensions pour les expressions : les
expressions conditionnelles et les expressions let.

Les expressions conditionnelles


Elles se définissent de la manière suivante :

expression : := ... |
if listeExpGardées fi
listeExpGardées : := expressionGardée |
expressionGardée | listeExpGardées
expressionGardée : := prédicat → expression

Le lecteur est supposé familiarisé avec les structures de contrôle classiques et


en particulier avec l’alternative. Cependant l’expression conditionnelle (appelée
parfois plus simplement conditionnelle), que nous utilisons tout au long de cet
ouvrage, présente quelques caractéristiques qui méritent d’être précisées.
Dans le contexte des conditionnelles, le prédicat est appelé garde. La sé-
mantique intuitive d’une expression conditionnelle est la suivante : une garde
« vraie 4 » quelconque est sélectionnée, l’expression correspondante est évaluée
et sa valeur est délivrée. S’il y a plusieurs gardes vraies, l’une quelconque d’entre
elles est choisie arbitrairement 5 . Si aucune garde n’est vraie, le programme s’ar-
rête en erreur. Le choix d’une telle construction est justifié ici par le fait qu’elle
permet un développement indépendant des expressions gardées et donc un dé-
veloppement itératif des opérations. Elle permet également de tirer profit des
situations symétriques afin d’abréger les développements.
Le tableau ci-dessous définit la notion de substitution dans les expressions
gardées et dans les conditionnelles. Ainsi que le montre le fragment de grammaire
ci-dessus, les expressions gardées ne sont donc pas des expressions « autonomes »,
il s’agit d’une notion auxiliaire pour définir les conditionnelles.

Substitution Définition Ident.

⎛ → F)
[x := E](G ⎞ [x := E]G ⇒ [x := E](G → F ) = [x := E]F (1.4.3)
if G1 →
⎜ ⎟
⎜ F1 ⎟
⎜ ⎟
⎜ ⎟
[x := E] ⎜ | G2 → ⎟ [x := E](G1 → F1 ) ∧ [x := E](G2 → F2 ) (1.4.4)
⎜ ⎟
⎜ ⎟
⎝ F2 ⎠

4. « Garde démontrable » serait plus approprié. Nous admettons cet abus de langage dans
la suite.
5. Il ne s’agit pas d’un choix aléatoire mais d’un choix non déterministe. Il n’est par exemple
pas question d’utiliser ce type d’alternative pour effectuer un tirage aléatoire. Le mode de mise
en œuvre dans un compilateur est en général une version « déterminisée » de la spécification.
26 Structures de données et méthodes formelles

Les expressions let


Elles se définissent par :

expression : := ... |
let listeRemp in expression end
listeRemp : := listeVar := listeExp
listeExpr : := expression |
expression, listeExpr
Une expression let permet d’introduire un identificateur auxiliaire et de lui
attribuer une valeur. Les éléments de listeV ar et de listeExp se correspondent
en nombre et en type. Il convient de ne pas confondre les variables de la listeRemp
avec des variables telles qu’elles existent en programmation impérative dans la
mesure où, ici, aucune variable ne change de valeur. Il s’agit plutôt de constantes
dont la valeur est déterminée au cours de l’exécution. Cette construction a pour
objectif d’éviter d’évaluer plusieurs fois une même sous-expression apparaissant
dans l’expression entre in et end.
Le tableau ci-dessous fournit la définition d’une expression de type let.

Notation Définition
let x := E in F end [x := E]F

1.5 Égalité
Cette section étend à nouveau le langage des prédicats en introduisant l’éga-
lité entre deux expressions. Syntaxiquement ceci revient à ajouter la règle sui-
vante à la catégorie syntaxique prédicat :

prédicat : := ... |
expression = expression
Cette extension exige d’introduire les nouvelles règles d’inférence suivantes :

Antécédent Conséquent
[x := F ]H, E = F
[x := F ]P [x := E]H, E = F
[x := E]P
[x := E]H, E = F
[x := E]P [x := F ]H, E = F
[x := F ]P
Les cas particuliers (réflexivité, égalité de deux couples, d’expressions compre-
nant une conditionnelle) sont traités comme des notations par le biais des défi-
nitions suivantes :
Notation Définition
E=E 
(E → F ) = (G → H) E=G ∧ F =H
E = (P → F ) P ⇒ E=F
E = (if F | G fi) E=F ∧ E=G
1. Mathématiques pour la spécification et les structures de données 27

Montrons sur l’exemple 5 = if x = 5 → x + 3 | x > 12 → 2 · x fi comment


se traduit une égalité comportant une conditionnelle :

5 = if x = 5 → x + 3 | x > 12 → 2 · x fi
⇔ Définition de = pour les conditionnelles
5 = (x = 5 → x + 3) ∧ 5 = (x > 12 → 2 · x)
⇔ Définition de = pour les expressions gardées
x = 5 ⇒ 5 = x + 3 ∧ x > 12 ⇒ 5 = 2 · x

La démonstration de cette dernière formule repose sur l’utilisation des règles


d’inférence pour l’égalité.
L’opérateur = est une extension de notation qui se définit comme la négation
d’une formule avec égalité. Cette définition n’est pas formalisée ici, pas plus que
la négation des autres opérateurs relationnels traditionnels (comme ∈ / pour les
ensembles).

1.6 Théorie des ensembles


Le langage de la théorie des ensembles étend le langage des prédicats en
autorisant des prédicats conformes à la syntaxe suivante :

prédicat : := ... |
expression ∈ expression

où l’expression à droite du symbole ∈ (symbole d’appartenance) représente un


ensemble. Les trois constructions de base permettant d’obtenir des ensembles
sont décrites syntaxiquement par :

expression : := ... |
expression × expression |
P(expression) |
{listeVar · prédicat | expression}

Ces constructions représentent respectivement le produit cartésien, l’ensemble


des parties et la définition en compréhension. Avant d’aborder l’aspect axio-
matique de ces nouvelles constructions, fournissons quelques exemples intuitifs.
Bien qu’elles ne soient pas formellement définies pour l’instant, nous supposons
disponibles les notions d’ensembles définis en extension, l’égalité d’ensembles et
la notation ∅ d’ensemble vide. Si s = {1, 2, 3} et t = {a, b, c}, alors :

s × t = {1 → a, 2 → a, 3 → a, 1 → b, 2 → b, 3 → b}
P(s) = {∅, {1}, {2}, {3}, {1, 2}, {1, 3}, {2, 3}, {1, 2, 3}}
{x · x ∈ s | x + 1} = {2, 3, 4}

Pour l’opérateur de définition en compréhension, il existe une notation abrégée


dans le cas où l’expression est réduite à la variable x comme dans {x · P | x}.
Dans ce cas, on adopte la notation {x | P }. Les trois constructions ci-dessus sont
28 Structures de données et méthodes formelles

définies pas trois axiomes. Cependant, pour respecter l’esprit de la méthode B


et contrairement à ce que nous avons pratiqué jusqu’à présent, ces trois axiomes
sont exprimés comme des extensions notationnelles du langage des prédicats
« pur ». Il en résulte que la démonstration d’un prédicat « ensembliste » se
fait en traduisant ce dernier dans une formule du langage prédicatif pur, avant
d’effectuer la preuve en logique des prédicats.

Axiome Traduction Condition Ident.


E → F ∈ S×T E∈S ∧ F ∈T (1.6.1)
E ∈ P(S) ∀x ·(x ∈ E ⇒ x ∈ S) x\E, x\S (1.6.2)
E ∈ {x · P | F } ∃x · P ∧ E = F x\E (1.6.3)

Montrons sur un exemple comment peut se prouver un prédicat ensembliste.


En anticipant quelque peu, la notation classique s ⊆ t est définie par s ∈ P(t).
Cherchons alors à prouver le séquent
s ⊆ s. La définition de la notation ⊆
fournit la traduction


s ∈ P(s)

Nous pouvons alors appliquer la règle 1.6.2 ci-dessus afin d’obtenir :


∀x ·(x ∈ s ⇒ x ∈ s)

La règle d’inférence 1.4.2 du calcul des prédicats (page 24) nous permet de
supprimer le quantificateur. Nous avons alors :


x∈s⇒x∈s

La présence du symbole ∈ peut sembler gênante de prime abord : nous ne dispo-


sons d’aucun moyen pour l’éliminer ni pour le transformer. Qu’à cela ne tienne !
Il suffit de considérer toute formule de ce type comme une variable proposition-
nelle. Cette transformation n’altère pas son caractère démontrable ou non. Donc,
si nous posons P =  x ∈ s, notre démonstration se réduit à prouver le séquent

P ⇒ P . Cette preuve s’obtient par application de la règle 1.3.6, page 21, puis
de l’axiome 1.2.2, page 20.

Nous venons d’étudier les trois opérateurs de construction d’ensembles. Nous


n’en avons pas tout à fait terminé avec les bases de la théorie des ensembles. Il
nous reste à considérer deux axiomes : l’axiome de choix et l’axiome de l’égalité.
La catégorie syntaxique expression s’enrichit de la règle suivante :

expression : := ... |
choice(expression)

Et l’axiome de choix qui correspond à cette extension est défini par :

Axiome Condition
choice(S) ∈ S ∃x ·(x ∈ S) ∧ x\S
1. Mathématiques pour la spécification et les structures de données 29

Cet axiome précise que si un ensemble n’est pas vide, l’application de l’opération
choice sur cet ensemble délivre l’un des éléments de cet ensemble. Cet axiome
trouve son utilité notamment dans la définition d’expressions non déterministes
(cf. section 1.6.2). L’axiome de l’égalité se présente sous la forme :

Axiome Traduction
S=T S ∈ P(T ) ∧ T ∈ P(S)

1.6.1 Opérations ensemblistes traditionnelles


Le reste de la section 1.6 est constitué d’extensions à la théorie de base que
nous venons d’étudier. Nous allons donc principalement introduire et définir des
notations. La maîtrise de ces notations est fondamentale pour la compréhension
du reste de l’ouvrage et pour son utilisation pratique. Tout d’abord, nous in-
troduisons syntaxiquement l’opérateur relationnel ⊆, qui sert à construire des
prédicats ensemblistes. Sa syntaxe est décrite par :

prédicat : := ... |
expression ⊆ expression

Les deux expressions sont des expressions ensemblistes. Ainsi que nous l’avons
dit ci-dessus, cet opérateur se définit par :

Notation Définition
S⊆T S ∈ P(T )

Les opérations classiques d’union, d’intersection, de différence, de définition


en extension et l’ensemble vide se décrivent syntaxiquement par les règles sui-
vantes :

expression : := ... |
expression ∪ expression |
expression ∩ expression |
expression − expression |
{listeExpr} |

Ainsi {3, 4+5, 8} est un ensemble défini en extension. Ces notations se définissent
par :

Notation Définition Ident.


E∈S∪T E ∈S∨E ∈T (1.6.4)
E∈S∩T E ∈S∧E ∈T (1.6.5)
E ∈ {e1 , e2 . . . , en } E = e1 ∨ E = e2 ∨ . . . ∨ E = en (1.6.6)
E∈∅ ⊥ (1.6.7)
30 Structures de données et méthodes formelles

Les seules propriétés de ces notations exploitées ensuite sont celles de l’an-
nexe A, regroupées dans le tableau dénommé « Propriétés des ensembles ».
De même que ∀
est le quantificateur qui généralise l’opérateur binaire ∧,
de même (resp. ) est le quantificateur qui généralise l’opérateur binaire
ensembliste ∪ (resp. ∩). La syntaxe de ces deux nouveaux quantificateurs se
définit par :

expression : := ... |

listeVar ·(prédicat | expression) |

listeVar ·(prédicat | expression)



Ainsi par exemple i ·(i ∈ 1..3 | {i, i+1, i+2}) définit l’union des trois ensembles
{1, 2, 3}, {2, 3, 4}

et {3, 4, 5}. Cette expression est donc égale à {1, 2, 3, 4, 5}. De
la même façon i ·(i ∈ 1 .. 3 | {i, i + 1, i + 2}) est égal à l’intersection des trois
ensembles {1, 2, 3}, {2, 3, 4} et {3, 4, 5}, et vaut donc {3}. Ces deux notations se
définissent par les règles de traduction suivantes :

Notation Définition Condition



E ∈ i ·(P | F ) ∃i ·(P ∧ E ∈ F ) i\E

E ∈ i ·(P | F ) ∀i ·(P ⇒ E ∈ F ) i\E

1.6.2 Choix dans un ensemble


Dans les chapitres qui suivent, il nous arrive fréquemment d’avoir à formaliser
une affirmation telle que « a est un élément quelconque d’un ensemble E », E
étant défini en compréhension. L’opérateur de choix et la notion d’ensembles
définis en compréhension se marient au sein de la construction « choix dans un
ensemble » pour offrir cette possibilité. La syntaxe de cette construction est :

expression : := ... |
any listeVar where prédicat then expression end

Ainsi par exemple l’expression

any e where
e ⊆ 1 .. 100 ∧
(7 ∈ e ∨ 23 ∈ e)
then
e − {49}
end

dénote un sous-ensemble de valeurs comprises entre 1 et 100 contenant 7 et/ou


23 mais ne contenant pas 49. La règle de traduction pour cette expression est
donnée par :

Notation Définition
any E where P then F end choice({E · P | F })
1. Mathématiques pour la spécification et les structures de données 31

Il existe une notation abrégée pour ce type d’expression : E : (P | F ). Par


ailleurs, dans le cas où E = F , la syntaxe se simplifie en any E where P end
(dont la notation abrégée est E : P ). La traduction est donnée par :

Notation Définition
any E where P end any E where P then E end

1.6.3 Relations binaires


Cette section introduit les relations binaires. Notre langage exclut les rela-
tions d’arité supérieure à 2, cependant une relation peut avoir comme ensemble
source ou destination une autre relation. Les règles suivantes introduisent les no-
tions de relation entre deux ensembles (↔), de domaine (dom) et de codomaine
(ran) d’une relation :

expression : := ... |
expression ↔ expression |
dom(expression) |
ran(expression)

Ces notations se définissent comme le montre le tableau suivant :

Notation Définition
r ∈S↔T r ⊆S×T
E ∈ dom(r) ∃y ·(E → y ∈ r)
E ∈ ran(r) ∃x ·(x → E ∈ r)

Le schéma suivant illustre ces trois notions, pour une relation r entre les en-
sembles S et T :

S
dom(r) r T
ran(r)
×
×
×
×
×
×
×
×
×

Les notations pour la relation inverse (r−1 ), la restriction (U  r), la cores-


triction (r  V ), l’antirestriction (U 
− r), l’anticorestriction (r 
− V ) et l’image
d’un ensemble par une relation sont prises en compte syntaxiquement par les
règles suivantes :
32 Structures de données et méthodes formelles

expression : := ... |
expression−1 |
expression  expression |
expression  expression |
expression 
− expression |
expression 
− expression |
expression[expression]

Elles se définissent comme suit :

Notation Définition
E → F ∈ r−1 F → E ∈ r
E → F ∈ Sr E ∈ S ∧ E → F ∈r
E → F ∈ rT F ∈ T ∧ E → F ∈r
E → F ∈ S−r E∈ / S ∧ E → F ∈r
E → F ∈ r
−T F ∈/ T ∧ E → F ∈r
F ∈ r[U ] F ∈ ran(U  r)

Le diagramme sagittal ci-dessous illustre les notions de restriction et de cores-


triction :

S T S T
U U r rV V
× ×
× ×
× ×
× ×
× ×
× ×
× ×
× ×
× ×

Restriction de r par U Corestriction de r par V

L’antirestriction et l’anticorestriction sont les opérations duales de la restriction


et de la corestriction comme le montre le schéma suivant :

S T S T
U U
−r r
−V V
× ×
× ×
× ×
× ×
× ×
× ×
× ×
× ×
× ×

Antirestriction de r par U Anticorestriction de r par V


1. Mathématiques pour la spécification et les structures de données 33

L’image d’un ensemble U par une relation r, notée r[U ], est constituée de
tous les points de l’ensemble destination atteints par les couples issus de U . Un
exemple est fourni par le schéma suivant :

S T
U r r[U ]
×
×
×
×
×
×
×
×
×

Image de U par r

La dernière série d’opérateurs ensemblistes purs est constituée des deux opé-
rateurs de composition de relation que sont ; et ◦ et de l’opérateur de surcharge
−. Leur syntaxe est donnée par les règles suivantes :

expression : := ... |
expression ; expression |
expression ◦ expression |
expression − expression

l’opérateur ◦ est l’opérateur de composition classique, ; est une notation alter-


native pour ◦ (r ; t = t ◦ r), tandis que la surcharge de la relation r par la relation
t est constituée de l’ensemble des couples de t plus l’ensemble des couples de r
dont l’origine n’est pas l’origine d’un couple de t.

Notation Définition Condition


E → F ∈ (f ; g) ∃x ·(E → x ∈ f ∧ x → F ∈ g) x\E, x\F, x\f, x\g
E → F ∈ (g ◦ f ) F → E ∈ (f ; g)
E → F ∈ r − t E → F ∈ (dom(t) − r) ∪ t

Le schéma ci-dessous présente un exemple de composition entre deux rela-


tions r et t.

S T U S U
r t r; t
× × × ×
×
× × × ×
×
× × × ×
×
× × × ×
×
× × × ×

Composition des relations r et t


34 Structures de données et méthodes formelles

La surcharge de r par t est illustrée par le diagramme sagittal suivant :

S r T S r − t T
t
× ×
× ×
× ×
× ×
× ×
× ×
× ×
× ×
× ×

Les annexes A et B fournissent un catalogue de propriétés pour les opérateurs


étudiés dans la présente section. Ces propriétés sont utilisées régulièrement dans
le développement de structures de données.

1.6.4 Fonctions
Les fonctions sont des cas particuliers de relations (toute fonction est une
relation). Ainsi que nous l’avons vu dans l’exemple introductif de la section 1.1
(le problème des huit reines), il convient de distinguer différentes catégories de
fonctions selon que leur domaine s’identifie à l’ensemble origine, le codomaine
à l’ensemble destination, ou que la relation inverse est aussi une fonction. La
syntaxe de ces différents cas est conforme aux règles suivantes :

expression : := ... |
expression →
 expression |
expression → expression |
expression 
 expression |
expression  expression |
expression 
 expression |
expression  expression |
expression 
 expression

Le symbole de base est →. Trois signes diacritiques indépendants peuvent enri-


chir cette notation pour donner l’un des six autres symboles ci-dessus. La barre
verticale →  signale que la fonction est partielle (son domaine est inclus dans
l’ensemble origine), l’empennage  est la marque d’une fonction injective (la
relation inverse est une fonction), la coiffe  désigne des fonctions surjectives
(le codomaine est identique à l’ensemble de destination) 6 . Par exemple à la sec-
tion 1.1 nous avons écrit q ∈ 1 .. 8 
 1 .. 8 pour signifier que q est un élément
de l’ensemble des fonctions totales injectives et surjectives (bijectives donc) de
l’intervalle 1 .. 8 sur lui-même.
Afin d’éviter toute méprise, il convient d’insister sur le fait que si f ∈ E 
F
alors f ∈ E  F , que si f ∈ E  F alors f ∈ E → F , que si f ∈ E → F
6. La combinaison des trois diacritiques, qui symboliserait des bijections partielles, n’est
pas autorisée par les règles syntaxiques ci-dessus.
1. Mathématiques pour la spécification et les structures de données 35

alors f ∈ E →
 F , etc. Les sept types de fonctions définis ci-dessus sont de ce fait
interdépendants : la relation d’ordre partielle définit un treillis (cf. exercice 1.6.6).

Lambda abstractions

Les lambda abstractions constituent un moyen de définir des fonctions ano-


nymes. Le principe consiste à préciser comment un élément du codomaine est
en relation avec un élément du domaine, sans pour cela fournir la liste exhaus-
tive des couples. Ainsi, si nous souhaitons définir une fonction qui, à chaque
élément de l’intervalle 1 .. 10 associe son double, nous pouvons écrire la lambda
abstraction suivante :

λi ·(i ∈ 1 .. 10 | 2 · i)

La syntaxe d’une telle construction se décrit par la règle syntaxique suivante :

expression : := ... |
λlisteVar ·(prédicat | expression)

La définition est formalisée par la règle suivante :

Notation Définition Ident.


E → F ∈ λi ·(P | G) [i, j := E, F ](P ∧ j = G) (1.6.8)

qui fournit la condition d’appartenance d’un couple à une lambda abstraction.

Évaluation de fonctions

L’évaluation de fonctions se décrit syntaxiquement par la règle suivante :

expression : := ... |
expression(expression)

La définition d’une telle expression exige que la première expression soit une
fonction et que la seconde, son argument, fasse partie du domaine de la fonction.
On a alors :

Notation Définition Condition


F = f (E) E → F ∈ f E ∈ dom(f )

En reprenant la fonction définie par une lambda abstraction ci-dessus, nous avons
λi ·(i ∈ 1 .. 10 | 2 · i)(8) = 16.
36 Structures de données et méthodes formelles

Remarque (à propos de l’ensemble R des réels). Cet ensemble n’est uti-


lisé que lors des calculs de complexité. Les deux fonctions x (fonction plancher)
et x (fonction plaf ond) définies sur R et à valeur dans Z sont les seules fonc-
tions à argument réel utilisées dans la suite. Elles se définissent par :

Notation Définition Ident.


x max({n | n ∈ Z ∧ n ≤ x}) (1.6.9)
x max({n | n ∈ Z ∧ n ≥ x}) (1.6.10)

Certaines des propriétés des fonctions apparaissent dans les annexes A et B


consacrées aux relations, d’autres sont présentées dans les annexes C ou D.

Exercices

Exercice 1.6.1 Fournir l’extension de l’ensemble suivant, décrit en compréhension : {i | i ∈


1 .. 10 ∧ i + 3 < 12}.

Exercice 1.6.2 Soit E = {1, 2} et F = {a, b, c}. Fournir l’extension de : (i) E × F, (ii) E → F,
(iii) E  F, (iv) P(E), (v) P(E) × F, (vi) P(E × F ).

Exercice 1.6.3 Exprimer par une lambda abstraction la fonction suivante décrite par un
ensemble défini en extension : {1 → 4, 2 → 8, 3 → 12, 4 → 16}.

Exercice 1.6.4 Soit f la fonction suivante λi ·(i ∈ 1 .. 10 | i ∗ 3) 


− {5 → 8, 7 → 4}. Évaluer
f (6) puis f (5).

Exercice 1.6.5 Soit l’ensemble suivant défini en compréhension : {(i, j) | i ∈ 1 .. 10 ∧ j ∈


N ∧ j = i + 3}.
1. Fournir l’extension de cet ensemble.
2. Cet ensemble est-il une fonction ? Si oui définir cet ensemble par une lambda abstrac-
tion.

Exercice 1.6.6 La relation d’inclusion sur les différents types de fonctions possibles (cf. sec-
tion 1.6.4) induit une structure de treillis. Déterminer ce treillis.

1.7 Ensembles particuliers


Cette section introduit, de manière informelle, des ensembles particuliers dont
nous aurons rapidement l’usage (c’est déjà fait pour les entiers naturels). Il s’agit
principalement des entiers naturels N et relatifs Z, des booléens bool et des
ensembles de caractères char. Il convient de ne pas confondre les prédicats (qui
sont rappelons-le des entités que l’on démontre) et les booléens, qui ont le statut
d’expression (que l’on évalue donc).
1. Mathématiques pour la spécification et les structures de données 37

expression : := ... |
bool |
true |
false |
bool(prédicat)
char |
    
a | b | . . . | 9 |
Z |
N |
N1 |
∞ |
0| 1| . . .|
+expression |
−expression |
expression + expression |
expression ÷ expression |
expression mod expression |
expression · expression |
expressionexpression

Dans la suite nous considérons que N et Z sont des ensembles finis dont le
plus grand élément est représenté par ∞ (d’un point de vue programmation
cette valeur peut en général être identifiée au plus grand entier représentable
sur une machine). La plus petite valeur de l’ensemble Z est représentée par −∞
qui s’identifie au plus petit entier représentable. L’ensemble bool est constitué
des deux constantes true et false tandis que l’ensemble N1 est l’ensemble des
entiers naturels privé de la valeur 0. L’opérateur ÷ (resp. mod) délivre le quotient
(resp. le reste) de la division entière. La fonction bool convertit un prédicat en
une expression booléenne. Elle est définie par :

Notation Définition
bool(⊥) false
bool() true

Concernant les entiers naturels ou relatifs, les opérateurs relationnels clas-


siques (<, ≤, etc.) permettent de construire des prédicats. L’opérateur .. délivre
un intervalle d’entiers, l’opérateur card appliqué à un ensemble renvoie le nombre
d’éléments présents dans cet ensemble. L’opérateur max (resp. min) appliqué à
un ensemble d’entiers quelconque délivre le plus grand élément de cet ensemble.
Adhérant en cela aux conventions adoptées dans [67] et dans [31], nous admet-
tons que max(∅) = −∞ et que min(∅) = ∞. Les règles de grammaire décrivant
ces expressions sont présentées ci-dessous.
38 Structures de données et méthodes formelles

prédicat : := ... |
expression ≤ expression |
expression < expression |
expression ≥ expression |
expression > expression
expression : := ... |
expression .. expression |
card(expression) |
max(expression) |
min(expression)

Les opérateurs + (somme) et · (produit)


se généralisent comme traditionnel-
lement pour devenir les quantificateurs et , selon la syntaxe :

expression : := ... |

listeVar ·(prédicat | expression) |

listeVar ·(prédicat | expression)

Nous pouvons ainsi écrire la formule classique qui fournit la somme des n pre-
miers entiers i ·(i ∈ 1 .. n | i) = (n ·(n + 1)) ÷ 2. Nous pourrons parfois noter
n n
n ·(n + 1)
ce type de formule i=1 i = n ·(n+1) 2 ou encore i = .
i=1
2
Les opérateurs de translation  et de décalage  s’appliquent respective-
ment à un sous-ensemble de Z et à une relation dont l’ensemble origine est
contenu dans Z. Syntaxiquement ils se présentent par :

expression : := ... |
expression  expression |
expression  expression |

L’opérateur  permet de translater un sous-ensemble fini de relatifs d’un cer-


tain nombre de positions. Ainsi par exemple l’ensemble {−6, −1, 4, 8, 10}  −2
est égal à {−8, −3, 2, 6, 8}. L’opérateur  s’applique à une relation dont le do-
maine est un sous-ensemble fini de Z. Dans la suite nous l’utilisons le plus sou-
vent sur des tableaux (c’est-à-dire des fonctions totales définies sur un intervalle).
Ainsi, si f = {3 → 7, 4 → 4, 5 → 5, 6 → 13}, f  2 = {5 → 7, 6 → 4, 7 → 5, 8 →
13}, tandis que f  −2 = {1 → 7, 2 → 4, 3 → 5, 4 →  13}. Ces notations se
définissent de la manière suivante :

Notation Définition Condition Ident.


a  v ran(λi ·(i ∈ a | i + v)) a ∈ P(Z) ∧ v ∈ Z (1.7.1)
−1
f v (λi ·(i ∈ dom(f ) | i + v)) ;f f ∈Z↔s∧v ∈Z (1.7.2)
1. Mathématiques pour la spécification et les structures de données 39

Les tableaux de l’annexe D intitulés « Propriétés du décalage » et « Propriétés


de la translation » regroupent un certain nombre de propriétés portant sur les
opérateurs  et .

Tableaux
Parmi les ensembles particuliers intéressants pour l’étude des structures de
données se trouvent les tableaux. Un tableau (à une dimension) est une fonction
totale définie sur un intervalle fini (éventuellement vide) d’entiers relatifs et à
valeur dans un ensemble quelconque. tab(s) représente l’ensemble de tous les
tableaux qu’il est possible de définir à valeur dans s.

expression : := ... |
tab(expression)

Notation Définition Ident.



tab(s) (i, j) ·(i, j ∈ Z × Z | i .. j → s) (1.7.3)

Le cas où la fonction qui définit le tableau est une fonction injective est un cas
particulier que nous rencontrons parfois. Un tel tableau est qualifié de « tableau
sans doublon ».

Exercices

Exercice 1.7.1 Démontrer les propriétés répertoriées dans l’annexe D portant sur l’opérateur
de décalage de fonction .

Exercice 1.7.2 On considère l’opérateur de décalage de relation défini par la notation 1.7.2,
page ci-contre, avec la restriction v ∈ N. Fournir une définition alternative inductive. Montrer
l’équivalence de ces deux définitions.

Exercice 1.7.3 À quelle condition la surcharge d’un tableau t1 par un tableau t2 est-elle
encore un tableau ? Effectuer la démonstration.

Exercice 1.7.4 Soit t1 ∈ 1 .. 3 → N et t2 ∈ 1 .. 4 → N deux tableaux. Soit t3 l’expression


suivante :

t1 
− t2  card(t1)

1. Montrer que t3 est bien un tableau (cf. exercice 1.7.3).


2. Calculer la valeur de t3 si t1 = {1 → 6, 2 → 8, 3 → 4} et t2 = {1 → 10, 2 → 9, 3 →
11, 4 → 5}.

Exercice 1.7.5 Montrer que si n ∈ N, 1 .. n + 1 − {n + 1} = 1 .. n

Exercice 1.7.6 Soit t ∈ 1 .. 100 → N et v ∈ N.


1. Exprimer le prédicat « v est l’une des valeurs du tableau t ».
2. Exprimer le prédicat « v est dans la première moitié du tableau t ».
.

Exercice 1.7.7 Soit t un tableau défini par t ∈ 1 .. 100 → N. Simplifier quand c’est possible
les expressions ensemblistes suivantes :
40 Structures de données et méthodes formelles

– 1 .. 50  (t 
− {65 → 12, 30 → 17}),
– (1 .. 50  t) 
− {65 → 12, 30 → 17}.

Exercice 1.7.8 Soit n ∈ N1 et t le tableau défini par t ∈ 1 .. n → Z. Exprimer le prédicat qui


affirme que tous les éléments de t sont identiques.

Exercice 1.7.9
1. Soit t défini par t ∈ 1 .. 100 → N. Exprimer que t est trié par ordre croissant.
2. Soit s défini par s ∈ 1 .. 100  N. Exprimer que s est trié par ordre croissant.

Exercice 1.7.10 Soit n ∈ N1 , m ∈ N1 et t le tableau défini par t ∈ 1 .. n × 1 .. m → N. Spécifier


l’opération qui délivre true si et seulement s’il existe au moins une « ligne » de t ne contenant
que des zéros.

Exercice 1.7.11 Soit n ∈ N1 et soit t le tableau défini par t ∈ 1 .. n → N.


1. Spécifier que pour i et s donnés tels que i ∈ 1 .. n et s ∈ i .. n, le sous-tableau i .. s  t
est une succession d’entiers consécutifs.
2. Spécifier l’ensemble des longueurs des sous-tableaux de t ne contenant que des entiers
consécutifs.
3. Spécifier l’expression qui délivre la longueur du plus long sous-tableau de t contenant
des entiers consécutifs.

Exercice 1.7.12 Soit n ∈ N1 et soit t le tableau défini par t ∈ 1 .. n  N. Spécifier l’expression


qui délivre la somme des valeurs situées entre le plus grand et le plus petit élément de t.

Exercice 1.7.13 Soit n ∈ N et soit t le tableau défini par t ∈ 1 .. n → Z. Spécifier la formule


qui exprime la plus grande valeur que peut prendre la somme des éléments d’un sous-tableau
lorsque l’on considère tous les sous-tableaux de t.

Exercice 1.7.14 Soit n ∈ N1 et soit t le tableau défini par t ∈ 1 .. n → Z. Soit p ∈ 1 .. n


et v ∈ Z. Pour chacune des expressions ci-dessous, dire si le résultat est ou non un tableau.
Caractériser, par une phrase en français, la nature de ce résultat.
1. ((1 .. p  t) ∪ (p + 1 .. n  t)  1) 
− {p + 1 → v},
2. (1 .. p  t)  n − p ∪ (p + 1 .. n  t)  −p.

Exercice 1.7.15 Soit n ∈ N1 et soit t le tableau défini par t ∈ 1 .. n → Z.


1. On appelle décalage logique à droite de p positions (p ∈ 1 .. n) du tableau t l’opération
aboutissant au tableau débutant par p zéros suivis des éléments du tableau t situés entre
les positions 1 et n − p. Spécifier formellement, en utilisant l’opérateur de décalage ,
la notion de décalage logique à droite.
2. On appelle décalage circulaire à droite de p positions (p ∈ 1 .. n) du tableau t l’opération
aboutissant au tableau débutant par les p derniers éléments de t suivis des n−p premiers
éléments de t. Spécifier formellement, en utilisant l’opérateur de décalage , la notion
de décalage circulaire à droite.

1.8 Exemple de modélisation ensembliste


En guise de bilan pour le début du chapitre 1 nous étudions une modélisa-
tion de la notion de tutelle. Avant d’aborder le cœur de la modélisation, nous
devons au préalable disposer des notions d’individu vivant (indiv), d’âge (age),
de sexe (sexe), d’homme (homme), de femme (f emme), de mineur (mineur),
de majeur (majeur), de père (pere), de mère (mere) et de parents (parents).
Une formalisation possible de cette modélisation est donnée par :

indiv ∈ N ∧
1. Mathématiques pour la spécification et les structures de données 41

age ∈ indiv → 0 .. 130 ∧


sexe ∈ indiv → {masc, f em} ∧
homme = dom(sexe  {masc}) ∧
f emme = dom(sexe  {f em}) ∧
mineur = dom(age  0 .. 17) ∧
majeur = indiv − mineur ∧
pere ∈ indiv →
 homme ∧
mere ∈ indiv → f emme ∧
parents = pere ∪ mere

Si nous souhaitons modéliser le fait que les parents sont plus âgés que leurs
enfants, nous pouvons écrire :

age−1 ; parents ; age ⊆ {i, j | i ∈ 0 .. 130 ∧ j ∈ 0 .. 130 ∧ i < j}

Cette formule se paraphrase en disant que la relation qui lie l’âge d’un individu
à celui de ses parents est « croissante » (tout entier n’est en relation qu’avec des
entiers supérieurs) ; ci-dessus l’ensemble défini en compréhension est une relation
qui associe à tout élément de l’intervalle 0 .. 130 les entiers de cet intervalle qui
lui sont supérieurs.
Nous en arrivons aux notions relatives à la tutelle. Nous avons besoin des
concepts suivants :
– aP ourT uteur, relation qui désigne le ou les tuteurs d’un individu (les deux
parents peuvent être tuteurs de leurs enfants) ;
– dechu, certains individus majeurs sont déchus de leurs droits, ils ne peuvent
être tuteurs ;
– incapable, selon le vocabulaire juridique, un individu sous tutelle (mineur
ou majeur) est appelé un incapable ;
– tuteurP ot, (tuteur potentiel) il s’agit des individus qui peuvent légalement
être tuteurs. Il s’agit des majeurs qui ne sont ni déchus ni incapables.
Ces notions peuvent être formalisées par :

aP ourT uteur ∈ indiv ↔ majeur ∧


dechu ⊆ majeur ∧
incapable = dom(aP ourT uteur) ∧
tuteurP ot = majeur − (dechu ∪ incapable)

Les règles suivantes doivent être modélisées :


1. Tout individu mineur a au moins un tuteur et certains individus majeurs
ont un tuteur.
2. Un parent majeur, qui n’est ni déchu ni incapable, est tuteur de ses enfants
incapables (mineurs ou majeurs sous tutelle). Un individu peut donc avoir
deux tuteurs.
3. Si (au moins) l’un des parents est tuteur d’un individu, ce dernier ne peut
avoir de tuteur « étranger ».
4. Un incapable orphelin, ou dont aucun des deux parents ne peut être tuteur,
a comme unique tuteur un individu habilité à l’être.
5. Tout individu habilité à être tuteur peut être le tuteur d’un nombre quel-
conque de personnes.
42 Structures de données et méthodes formelles

Le point 1 se traduit par la formule :

mineur ⊆ incapable

La partie de la proposition qui affirme que certains individus majeurs ont un tu-
teur n’a pas de formulation explicite. Le fait que l’on n’affirme pas que mineur =
incapable laisse la possibilité à d’autres individus (les majeurs donc) d’être in-
capables. Il ne faut surtout pas traduire ce point par majeur ∩ incapable = ∅
qui signifierait qu’il existe toujours des majeurs incapables.
Le point 2 se formule par :

parents  tuteurP ot ⊆ aP ourT uteur

Autrement dit : un parent qui peut être tuteur est un tuteur de ses enfants.
Le point 3 peut se formuler par :

dom(aP ourT uteur ∩ parents) ∩ dom(aP ourT uteur − parents) = ∅

qui signifie qu’il n’y a pas d’incapable qui aurait comme tuteurs un parent et un
étranger.
Les points 4 et 5 se traduisent par :

aP ourT uteur − parents ∈ indiv →


 majeur

formule qui se paraphrase en affirmant qu’un tuteur qui n’est pas l’un des parents
est le seul tuteur pour l’individu considéré. La nature fonctionnelle de la relation
rend compte du point 4 tandis que son caractère non injectif traduit la règle 5.

Exercices

Exercice 1.8.1 Introduire la notion d’orphelin dans l’exemple développé à la section 1.8. For-
maliser l’affirmation qu’un orphelin mineur n’a qu’un seul tuteur. Démontrer cette affirmation
à partir de la formalisation de la section 1.8.

Exercice 1.8.2 Dans l’exemple développé à la section 1.8, introduire la règle suivante qui
concerne les mineurs non orphelins : si tous les parents d’une telle personne sont sous tutelle
alors son tuteur est celui de l’un de ses parents.

Exercice 1.8.3 On considère la description géographique suivante :


– il existe un ensemble de villes,
– il existe un ensemble de pays,
– une ville est située dans un et un seul pays,
– tout pays possède une (et une seule) capitale,
– les pays peuvent partager des frontières.
Modéliser cette description informelle. On s’assurera que l’on a bien spécifié que la capitale
d’un pays est bien une ville de ce pays.
1. Mathématiques pour la spécification et les structures de données 43

1.9 Construction de structures inductives


Les structures construites en s’appuyant sur la théorie des ensembles sont en
général définies par induction. L’un des outils mathématiques les mieux adaptés
est la théorie du point fixe (qui se conclut par les théorèmes de Knaster-Tarski
et le principe d’induction). Nous proposons de débuter cette section en passant
en revue les éléments fondamentaux de cette théorie. Nous serons alors à même
de l’appliquer à la construction de plusieurs structures intéressantes telles que
les listes, certains arbres, les sacs (cf. chapitre 3). Des variantes de ces structures
sont utilisées dans la partie II de l’ouvrage. Par contre, afin de ne pas alourdir
le texte, ces variantes sont posées sans autres précautions, laissant au lecteur
le soin de s’assurer de leur légitimité avant, s’il le souhaite, de les construire à
partir de la théorie des ensembles.
Soit T un ensemble non vide et soit f tel que f ∈ T → T . x ∈ T est un
point fixe de f si x = f (x) (dans la suite la fonction f est appelée fonction
« pivot »). Ainsi, −2 et 4 sont des points fixes de la fonction f ∈ Z → Z définie
par f = λx ·(x ∈ Z | (x2 ÷ 2) − 4).
Pour notre propos, il est plus intéressant de considérer des fonctions f de
P(T ) dans P(T ). Les arguments de f sont alors des ensembles et la condition
pour x d’être un point fixe peut se formuler par : x ⊆ f (x) ∧ f (x) ⊆ x. Soit φ
l’ensemble de tous les x tels que f (x) ⊆ x (φ = {x | x ∈ P(T ) ∧ f (x) ⊆ x}) et
soit fix(f ) l’intersection généralisée de (tous les sous-ensembles de) φ : fix(f ) =

z ·(z ∈ φ | z). On montre (c’est l’un des deux théorèmes de Knaster-Tarski)


que si f est monotone 7 alors fix(f ) est un point fixe de f :

f ∈ P(T ) → P(T ) ∧
∀(x, y) ·((x, y) ∈ P(T ) × P(T ) ∧ x ⊆ y ⇒ f (x) ⊆ f (y))

f (fix(f )) = fix(f )

On montre en outre que fix(f ) est le plus petit point fixe. On montre enfin (c’est
le principe général d’induction) que pour prouver une propriété P pour tous les
éléments de fix(f ), il suffit de démontrer que :

f ({x | x ∈ fix(f ) ∧ P }) ⊆ {x | x ∈ fix(f ) ∧ P }

En général, ce principe se simplifie selon la nature de la fonction f . Le chapitre 3


offre plusieurs exemples de construction de structures inductives. Un cas fréquent
important est celui des entiers naturels N. La section suivante lui est consacrée.

1.10 Démonstration par récurrence


Le terme français consacré pour l’induction sur N est « récurrence ». Bien
qu’il soit possible de construire N sur la théorie des ensembles selon les bases
posées à la section précédente et d’en déduire des principes de récurrence (cf. [3]),
nous supposons ici N donné (ou axiomatisé) et tenons pour acquis les principes
de récurrence suivants :
7. f est monotone si et seulement si ∀(x, y) ·((x, y) ∈ P(T ) × P(T ) ∧ x ⊆ y ⇒ f (x) ⊆ f (y)).
44 Structures de données et méthodes formelles

Théorème 1 (Principe de récurrence simple).


[n := 0]P ∧
∀n ·(n ∈ N ∧ P ⇒ [n := n + 1]P )

∀n ·(n ∈ N ⇒ P )

En pratique, démontrer P pour tout entier naturel n revient donc à décomposer


la démonstration initiale en deux démonstrations a priori plus simples (appelées
respectivement « étape de base » et « étape inductive ») :
1. démontrer [n := 0]P ,
2. démontrer [n := n + 1]P sous les hypothèses P et n ∈ N (dites hypothèses
de récurrence).
Il existe un principe de récurrence connu sous le nom de récurrence complète,
qui correspond au théorème suivant :

Théorème 2 (Principe de récurrence complète).


∀n ·(n ∈ N ∧ ∀m ·(m ∈ N ∧ m < n ⇒ [n := m]P ) ⇒ P )

∀n ·(n ∈ N ⇒ P )

À la différence du principe de récurrence simple (théorème 1), l’hypothèse P


porte ici sur toutes les valeurs de l’intervalle 0..n. Démontrer P pour tout n se fait
en démontrant P sous les hypothèses n ∈ N et ∀m ·(m ∈ N∧m < n⇒[n := m]P ).
Cette dernière hypothèse peut être instanciée pour toute valeur de m inférieure
à n.
Prenons comme exemple de démonstration par le principe de récurrence
simple la propriété suivante :


P (n) = j ·(j ∈ N ∧ j < n | 2j ) = 2n − 1

(soit informellement 20 + 21 + 22 + · · · + 2n−1 = 2n − 1). Nous avons à démontrer


d’une part [n := 0]P et d’autre part [n := n + 1]P sous les hypothèses n ∈ N et
P . La démonstration de la base est triviale :

[n := 0]( j ·(j ∈ N ∧ j < n | 2j ) = 2n − 1)
⇔ Substitution
j ·(j ∈ N ∧ j < 0 | 2j ) = 20 − 1
⇔ Propriété de et arithmétique
0=0
⇔ Définition de l’égalité


Quant à la partie inductive de la démonstration, elle s’effectue de la manière


suivante :

[n := n + 1]( j ·(j ∈ N ∧ j < n | 2j ) = 2n − 1)
⇔ Substitution
j ·(j ∈ N ∧ j < n + 1 | 2j ) = 2n+1 − 1
⇔ n ∈ N et propriété de
1. Mathématiques pour la spécification et les structures de données 45


j ·(j ∈ N ∧ j < n | 2j ) + 2n = 2n+1 − 1
⇔ Hypothèse d’induction P
2n − 1 + 2n = 2n+1 − 1
⇔ Arithmétique
2n+1 − 1 = 2n+1 − 1
⇔ Définition de l’égalité


Récurrence et renforcement
L’expérience montre que pour démontrer une proposition Q il est parfois
plus facile de démontrer une propriété plus forte P (P ⇒ Q), puis d’en déduire
Q, que de démontrer Q directement. Cette technique est particulièrement utile
lorsqu’elle est utilisée conjointement avec un raisonnement par induction (ou par
récurrence). L’objectif de cette section est d’en fournir une illustration. Considé-
rons l’exemple suivant de démonstration par récurrence (cet exemple est inspiré
de [97], page 112). Soit à prouver pour tout n ∈ N la propriété P (n) définie
par 8 :

n
n

P (n) = i3 = ( i)2
i=0 i=0

Ainsi que nous l’avons appris à travers le théorème 1, pour démontrer la propriété
P (n) pour tout n, il suffit d’une part de démontrer [n := 0]P (n) et d’autre part
de démontrer [n := n + 1]P (n) sous les hypothèses n ∈ N et P (n). La base est
facile à démontrer. Arrêtons-nous sur la démonstration de [n := n + 1]P (n) :

[n := n + 1]P (n)
⇔ Substitution

n+1
n+1
i3 = ( i)2
i=0 i=0
⇔ Propriété de et hypothèse n ∈ N

n
n
i3 + (n + 1)3 = ( i + (n + 1))2
i=0 i=0
⇔ Arithmétique (identité remarquable)

n
n
n
i3 + (n + 1)3 = ( i)2 + 2 ·(n + 1) · i + (n + 1)2
i=0 i=0 i=0
⇔ P (n) (hypothèse d’induction)

n
n
n
i3 + (n + 1)3 = i3 + 2 ·(n + 1) · i + (n + 1)2
i=0 i=0 i=0
⇔ Arithmétique

n
(n + 1)3 = 2 ·(n + 1) · i + (n + 1)2
i=0

8. Le problème de savoir comment cette formule est conjecturée est largement développé
dans [97]. C’est également le thème sous-jacent à la notion d’induction constructive présenté
dans [17]. Il n’est par contre pas abordé ici.
46 Structures de données et méthodes formelles

⇔ Arithmétique

n
2 ·(n + 1) · i = (n + 1)3 − (n + 1)2
i=0
⇔ Arithmétique

n
2 ·(n + 1) · i = (n + 1)2 ·(n + 1 − 1)
i=0
⇔ Arithmétique

n
2 ·(n + 1) · i = n ·(n + 1)2
i=0
⇔ Arithmétique

n
n ·(n + 1) 2
i =
i=0
2 ·(n + 1)
⇔ Arithmétique

n
n ·(n + 1)
i =
i=0
2

Appelons Q(n) cette dernière propriété. Le problème se réduit donc à démon-


trer Q(n) pour tout n ∈ N. Il existe plusieurs façons d’atteindre ce but. Nous
pourrions considérer que c’est un résultat connu. Nous pouvons aussi démontrer
Q(n) en tant que lemme. Nous pouvons également tenter de démontrer non plus
P (n) pour tout n mais P (n)∧Q(n) pour tout n. Cette dernière méthode consiste
à renforcer la proposition initiale par le conjoint Q(n). C’est cette méthode qui
nous intéresse ici. Nous avons donc pour tout n ∈ N :

n
n
n
n ·(n + 1)
P (n) ∧ Q(n) =
 i3 = ( i)2 ∧ i=
i=0 i=0 i=0
2

Bien entendu, si nous réussissons à démontrer P (n) ∧ Q(n) pour tout n nous
aurons comme corollaire le théorème initial. Tentons cette démonstration. La
base [n := 0](P (n)∧Q(n)) est facile à démontrer. Démontrons [n := n+1](P (n)∧
Q(n)) sous les hypothèses n ∈ N, P (n) et Q(n) (dans la suite, le calcul précédent
n’est pas redéveloppé) :

[n := n + 1](P (n) ∧ Q(n))


⇔ Substitution
[n := n + 1]P (n) ∧ [n := n + 1]Q(n)
⇔ Calcul précédent

n
n ·(n + 1)
i= ∧ [n := n + 1]Q(n)
i=0
2
⇔ Hypothèse Q(n) et calcul propositionnel
[n := n + 1]Q(n)
⇔ Substitution et arithmétique

n+1
(n + 1) ·(n + 2)
i=
2
i=0
⇔ Propriété de et hypothèse n ∈ N
1. Mathématiques pour la spécification et les structures de données 47


n
(n + 1) ·(n + 2)
i + (n + 1) =
i=0
2
⇔ Hypothèse Q(n)
n ·(n + 1) (n + 1) ·(n + 2)
+ (n + 1) =
2 2
⇔ Arithmétique (non développé)
(n + 1) ·(n + 2) (n + 1) ·(n + 2)
=
2 2
⇔ Définition de l’égalité

Nous avons trouvé plus commode, pour démontrer un prédicat P , de dé-
montrer une proposition plus forte que P . C’est ce que, dans [97], G. Polya
appelle « le paradoxe de l’inventeur ». Le conjoint complémentaire Q(n) agit en
quelque sorte comme un catalyseur : il facilite la démonstration de P (n) pour
tout n avant d’être éliminé. Nous rencontrons au quotidien des professionnels
qui appliquent une technique similaire, sans que celle-ci nous apparaisse comme
paradoxale. C’est par exemple le cas d’un entrepreneur en bâtiments, qui trouve
plus pratique de construire une maison et un échafaudage en sachant pertinem-
ment d’une part que l’échafaudage va faciliter les travaux et d’autre part qu’il
pourra aisément être démonté à la fin du chantier pour ne laisser voir que le ré-
sultat attendu. Plus généralement, c’est le concept d’outil qui est exploité ici : il
est souvent plus rapide de perdre du temps à se forger un outil que de s’attaquer
directement au problème.
À la section 2.4, nous revenons sur l’utilisation de cette technique appliquée
au renforcement de structures de données. Elle permet en particulier d’améliorer
les performances de certaines opérations.

1.11 Opérations
1.11.1 Introduction
Les opérations sont destinées à construire les structures de données ou à
observer leur état. Le processus de développement exige de distinguer :
– les opérations abstraites, qui interviennent dans la spécification abstraite
d’une structure de données ;
– les opérations concrètes, qui elles se définissent au sein d’une spécification
concrète ;
– une opération particulière, qui apparaît dans la spécification concrète et
qui fait le lien entre spécification concrète et spécification abstraite : la
fonction d’abstraction.
Un critère complémentaire du précédent consiste à distinguer :
– les opérations principales, accessibles depuis l’extérieur du type considéré ;
– les opérations auxiliaires, qui sont des opérations outils uniquement desti-
nées à faciliter l’expression des autres opérations.
Pour être complet, il convient de faire la distinction au sein des opérations prin-
cipales, entre les opérations internes (qui délivrent une valeur du type considéré)
et les autres, appelées opérations externes.
48 Structures de données et méthodes formelles

Dans la suite de cette section, nous étudions tout d’abord un type parti-
culier d’expression utilisé uniquement dans les opérations : les expressions pré-
conditionnées. Nous définissons ensuite la notion d’opération avant d’examiner
comment s’évalue une opération. Enfin nous étudions la propagation des précon-
ditions au sein d’une opération. L’intérêt de ce dernier point se manifeste lors
du calcul d’une opération : il est de déterminer les gardes des conditionnelles et
de décider de la terminaison du développement.

1.11.2 Expression préconditionnée


La notion expressionPréc d’expression préconditionnée est nouvelle, elle est
spécifique des opérations. Le concept est présenté et développé ci-dessous. Les
exemples des sections suivantes nous permettent de préciser informellement cer-
tains aspects sémantiques.

expressionPréc : := pre
prédicat
then
expression
end

Le tableau ci-dessous définit la substitution dans les expressions précondi-


tionnées.

Substitution Définition Ident.


⎛ ⎞ ⎛ ⎞
pre pre
⎜ ⎟ ⎜ ⎟
⎜ P ⎟ ⎜ P ⎟
⎜ ⎟ ⎜ ⎟
⎜ ⎟ ⎜ ⎟
[x := E] ⎜ then ⎟ [x := E]P ∧ [x := E] ⎜ then ⎟ = [x := E]F (1.11.1)
⎜ ⎟ ⎜ ⎟
⎜ ⎟ ⎜ F ⎟
⎝ F ⎠ ⎝ ⎠
end end

Quant au tableau suivant, il définit les règles d’inférence pour les expressions
préconditionnées.

Antécédent Conséquent Ident.


H  P ∧ [x := F ]Q H  [x := pre P then F end]Q (1.11.2)
H  [x := pre P then F end]P H  P ∧ [x := F ]Q (1.11.3)

1.11.3 Définition des opérations


Malgré leur apparente diversité, les opérations présentent un caractère d’ho-
mogénéité puisqu’il s’agit toujours de fonctions. Les principales différences avec
les fonctions étudiées à la section 1.6.4 portent sur la notation et sur le fait
qu’une opération peut être définie récursivement. Syntaxiquement une opéra-
tion se présente sous la forme :
1. Mathématiques pour la spécification et les structures de données 49

opération : := function identificateur(listeVar) ∈ expression =



expressionFonct
expressionFonct : := expression |
expressionPréc

Dans la première règle, la liste listeVar représente les paramètres formels de


l’opération et expression le type de la fonction (partielle, surjective, etc.). Le
passage de paramètres se fait « par valeur » ; ce point est précisé ci-dessous.

Exemples

function double(n) ∈ N → N =
 2·n

L’opération double est une fonction totale dont le domaine de définition est N,
l’ensemble de destination est également N. Cette fonction délivre le double de
l’argument n.

function applic(f, i) ∈ (1 .. 3 → N) × 1 .. 3 → N =
 f (i)

L’opération applic est une fonction qui a comme premier argument une fonction
totale f de l’intervalle 1..3 dans N, comme second argument un entier naturel i de
l’intervalle 1 .. 3 et qui délivre comme résultat la valeur résultant de l’application
de f en i. applic({1 → 5, 2 → 45, 3 → 12}, 3) est un exemple d’application de la
fonction applic qui s’évalue à 12.

Une expression préconditionnée (de la catégorie syntaxique expressionPréc)


est destinée à préciser les contraintes imposées aux paramètres de la fonction
ainsi que les relations qu’ils entretiennent. La précondition se comporte en
quelque sorte comme un contrat entre le concepteur du programme et son uti-
lisateur. Si la fonction est appliquée dans une situation où la précondition est
satisfaite, alors le concepteur de la fonction s’engage à ce qu’elle délivre bien le
résultat attendu. Dans le cas contraire, le contrat est rompu et le concepteur
n’est pas engagé par ce qui peut survenir, comme un résultat dénué de sens
ou encore un débordement de la mémoire. La précondition doit permettre de
démontrer que tout appel à la fonction ne se fait que si la précondition est sa-
tisfaite. L’exemple qui suit illustre la notion d’expression préconditionnée dans
une opération.

Exemple

function divis(dd, ds) ∈ N × N →


 N =

pre
ds = 0
then
dd ÷ ds
end
50 Structures de données et méthodes formelles

L’opération divis(dd, ds) est une fonction qui délivre le quotient de dd par ds à
condition que le diviseur ne soit pas nul. Toute application de cette fonction doit
s’assurer au préalable que la précondition est satisfaite. Par ailleurs, dès qu’une
opération est définie par une fonction partielle, elle doit être préconditionnée car
le domaine d’application doit être précisé. Pour l’exemple ci-dessus, il aurait été
possible de définir plus simplement cette fonction par :

function divis(dd, ds) ∈ N × N1 → N =


 dd ÷ ds

Il n’est cependant pas toujours possible de contraindre suffisamment l’ensemble


d’origine pour pouvoir se passer d’une précondition.

1.11.4 Évaluation des opérations


À la section précédente nous avons présenté informellement la notion d’éva-
luation des opérations en un point. La présente section est destinée d’une part
à formaliser cette notion et d’autre part à développer des exemples de diffi-
culté croissante. La formule 1.11.4 ci-dessous dote l’évaluation d’une opération
d’une sémantique. Nous pouvons remarquer l’analogie entre la substitution d’une
expression préconditionnée et celle d’une opération : dans les deux cas il s’agit
d’une conjonction. Le typage des arguments dans une opération s’interprète donc
comme une forme de précondition.

Notation Définition Ident.


function f (x) ∈ u →
 v=
F [x := E]x ∈ u ∧ f (E) = [x := E]F (1.11.4)

Cette définition se généralise facilement au cas d’une opération à plusieurs ar-


guments.
Ci-dessous nous illustrons, à travers trois exemples, l’évaluation formelle
d’opérations.

Exemples
Reprenons l’opération double de la section 1.11.3 afin d’évaluer sa va-
leur au point 3. Appliquons la définition de l’évaluation d’une opération (for-
mule 1.11.4) :

[n := 3]n ∈ N ∧ double(3) = [n := 3]2 · n


⇔ Substitution
3 ∈ N ∧ double(3) = 2 · 3
⇔ Arithmétique et calcul propositionnel
double(3) = 6

Pour le second exemple, reprenons cette fois l’opération divis de la sec-


tion 1.11.3, page 48, appliquée au couple (4, 2). Instancier la formule 1.11.4 sur
cet exemple donne :
1. Mathématiques pour la spécification et les structures de données 51


⎨ [dd, ds := 4, 2]dd, ds ∈ N × N


divis(4, 2) = [dd, ds := 4, 2](pre ds = 0 then dd ÷ ds end)
⇔⎧ Substitution sur le premier conjoint
⎨ (4, 2) ∈ N × N


divis(4, 2) = [dd, ds := 4, 2](pre ds = 0 then dd ÷ ds end)
⇔ Arithmétique et calcul propositionnel
divis(4, 2) = [dd, ds := 4, 2](pre ds = 0 then dd ÷ ds end) (1.11.5)

Arrêtons-nous sur le membre droit de cette égalité afin d’instancier la définition


de la substitution dans une expression préconditionnée (formule 1.11.1) :


⎪ [dd, ds := 4, 2](ds = 0)


⎨ ⎧ ∧
⎨ [dd, ds := 4, 2](pre ds = 0 then dd ÷ ds end)



⎪ =
⎩ ⎩
[dd, ds := 4, 2](dd ÷ ds)
⇔⎧ Substitution
⎪ 2 = 0



⎨ ⎧ ∧
⎨ [dd, ds := 4, 2](pre ds = 0 then dd ÷ ds end)



⎪ =
⎩ ⎩
[dd, ds := 4, 2](dd ÷ ds)
⇔ Arithmétique et calcul propositionnel
[dd, ds := 4, 2](pre ds = 0 then dd ÷ ds end) = [dd, ds := 4, 2](dd ÷ ds)
⇔ Substitution
[dd, ds := 4, 2](pre ds = 0 then dd ÷ ds end) = 4 ÷ 2
⇔ Arithmétique
[dd, ds := 4, 2](pre ds = 0 then dd ÷ ds end) = 2

En reportant ce résultat dans la formule 1.11.5 nous obtenons :

divis(4, 2) = 2

Le troisième exemple considère l’opération f représentée par :

function f (v) ∈ N → N =

if v = 0 →
1
|v>0 →
v · f (v − 1)

Cette fonction est destinée à calculer la factorielle d’un entier naturel. Évaluons
f (3), pour cela instancions la formule 1.11.4, page ci-contre (qui fournit la défi-
nition de l’évaluation d’une opération), pour f (3) :
52 Structures de données et méthodes formelles

[v := 3]v ∈ N ∧ f (3) = [v := 3](if v = 0 → 1 | v > 0 → v · f (v − 1) fi)


⇔ Substitution sur le premier conjoint
3 ∈ N ∧ f (3) = [v := 3](if v = 0 → 1 | v > 0 → v · f (v − 1) fi)
⇔ Arithmétique et calcul propositionnel
f (3) = [v := 3](if v = 0 → 1 | v > 0 → v · f (v − 1) fi) (1.11.6)
Arrêtons-nous sur le second membre de l’égalité pour appliquer la définition de
la substitution dans les conditionnelles :
[v := 3](if v = 0 → 1 | v > 0 → v · f (v − 1) fi)
⇔ Substitution dans les conditionnelles, formule 1.4.4, page 25
[v := 3](v = 0 → 1) ∧ [v := 3](v > 0 → v · f (v − 1))
⇔⎧ Substitution dans les expressions gardées, formule 1.4.3, page 25
⎨ [v := 3]v = 0 ⇒ [v := 3](v = 0 → 1) = [v := 3]1


[v := 3]v > 0 ⇒ [v := 3](v > 0 → v · f (v − 1)) = [v := 3]v · f (v − 1)
⇔⎧ Substitution
⎨ 3 = 0 ⇒ [v := 3](v = 0 → 1) = 1


3 > 0 ⇒ [v := 3](v > 0 → v · f (v − 1)) = 3 · f (3 − 1)
⇔ Calcul propositionnel et arithmétique
[v := 3](v > 0 → v · f (v − 1)) = 3 · f (2) (1.11.7)
Nous pouvons enfin reporter ce dernier résultat dans la formule 1.11.6. Nous
obtenons : f (3) = 3 · f (2).

1.11.5 Propagation des préconditions


Jusqu’à présent les seules préconditions que nous avons considérées sont celles
explicitées dans les expressions préconditionnées. Se pose tout naturellement le
problème de la propagation des préconditions au sein d’une opération. Au-delà de
son intérêt théorique, cette préoccupation présente un intérêt pratique puisque,
comme nous le voyons ci-dessous, c’est par cet intermédiaire que nous pouvons
avoir la garantie qu’une conditionnelle n’échoue pas faute d’une garde vraie.
En général une expression hérite de la précondition de l’expression englo-
bante. Outre le fait que le typage d’une opération s’interprète comme une pré-
condition, il existe trois exceptions à cet héritage simple. Elles sont représentées
dans le tableau 1.1 ci-dessous dans lequel prec(E) est la fonction qui délivre la
précondition d’une expression E. Selon la propriété 1.11.8, la précondition de
l’expression qui constitue le corps de l’opération est le typage des arguments.
La formule 1.11.9 considère les expressions préconditionnées, elle nous apprend
que la précondition est enrichie de la formule P de la rubrique pre. La pro-
priété 1.11.10, qui traite du cas des expressions gardées, est similaire au cas des
expressions préconditionnées. Quant aux conditionnelles de la propriété 1.11.11,
un héritage simple s’applique à toutes les expressions gardées. Enfin, dans le cas
des expressions let de la propriété 1.11.12, l’héritage simple s’enrichit à partir
de la construction présente entre les mots-clés let et in. Le cas général d’une
conditionnelle comportant plus de deux expressions gardées se déduit facilement
de la propriété 1.11.11.
1. Mathématiques pour la spécification et les structures de données 53

Tableau 1.1 – Propagation des préconditions dans une opération.


Ce tableau présente les propriétés de la propagation des préconditions
depuis l’extérieur du texte d’une opération vers l’intérieur. Le cas des
opérations, des expressions préconditionnées et gardées, des condition-
nelles et des expressions let est considéré.

Propriété Condition Ident.


prec(function f (n) ∈ u →
 v=
 E)
= (1.11.8)
n∈u
prec(E) = P ∧ Q prec(pre P then E end) = Q (1.11.9)
prec(E) = P ∧ G prec(G → E) = P (1.11.10)
prec(CG1 ) = P ∧ prec(CG2 ) = P prec(if CG1 | CG1 fi) = P (1.11.11)
prec(F ) = P ∧ x = E prec(let x := E in F end) = P (1.11.12)

Exemple

Appliquons le principe de la propagation des préconditions à l’opération


mt(t, i) qui délivre le plus grand élément d’un tableau 1 .. i  t. Nous obte-
nons l’opération ci-dessous. Le code est annoté par les préconditions, qui sont
encadrées, et par les barres verticales, qui soulignent leur portée. Dans chaque
précondition la partie non héritée apparaît en gras.

function mt(t, i) ∈ (1 .. 10 → N) × 1 .. 10 →
⏐  N =
⏐ P =  (t, i) ∈ (1 .. 10 → N) × 1 .. 10


⏐ if i⏐= 1 →
⏐ ⏐ P ∧i=1
⏐ ⏐
⏐ ⏐ t(1)


⏐ |i>1 →
⏐ ⏐
⏐ ⏐ P∧ i > 1
⏐ ⏐
⏐ ⏐ let m := mt(t, i − 1) in
⏐ ⏐ ⏐
⏐ ⏐ ⏐ P ∧ i > 1 ∧ m = mt(t, i − 1)
⏐ ⏐ ⏐
⏐ ⏐ ⏐ if m ≤ t(i) →
⏐ ⏐ ⏐
⏐ ⏐ ⏐ ⏐
⏐ ⏐ ⏐ ⏐
⏐ ⏐ ⏐ ⏐ P ∧ i > 1 ∧ m = mt(t, i − 1) ∧ m ≤ t(i)
⏐ ⏐ ⏐ ⏐
⏐ ⏐ ⏐ ⏐ t(i)
⏐ ⏐ ⏐ | m ≥ t(i) →
⏐ ⏐ ⏐
⏐ ⏐ ⏐ ⏐
⏐ ⏐ ⏐ ⏐
⏐ ⏐ ⏐ ⏐ P ∧ i > 1 ∧ m = mt(t, i − 1) ∧ m ≥ t(i)
⏐ ⏐ ⏐ ⏐
⏐ ⏐ ⏐ ⏐ m
⏐ ⏐ ⏐ fi
⏐ ⏐
⏐ ⏐
⏐ ⏐ end

⏐ fi
54 Structures de données et méthodes formelles

Ainsi que nous l’avons déjà dit, dans la suite de l’ouvrage nous calculons le
code des opérations. Ceci nous conduit fréquemment à raisonner sur des fonctions
dont la représentation est incomplète. En particulier, le calcul des conditionnelles
est un processus itératif qui progresse expression gardée après expression gardée.
Se posent alors deux problèmes :
1. Quand arrêter le processus itératif de calcul des expressions gardées ?
2. Étant donné une conditionnelle incomplète, quelle garde est candidate à la
poursuite du calcul ?
La propriété du tableau ci-dessous (non démontrée) est destinée à fournir des
éléments de réponse à ces deux interrogations.

Propriété Condition Ident.


P ⇒ G 1 ∨ · · · ∨ Gn prec(if G1 → E1 | . . . | Gn → En fi) = P (1.11.13)

La précondition étant supposée connue et les premières gardes G1 , . . ., Gi−1


déterminées, rechercher une nouvelle garde se fait en exhibant un prédicat Gi
qui contribue à satisfaire la propriété 1.11.13. Par ailleurs, le processus s’achève
quand la propriété 1.11.13 est satisfaite.
La pratique quotidienne de la programmation fonctionnelle est souvent plus
empirique, mais nous ne pouvons que conseiller au lecteur de souscrire à cette
démarche.

Exemple
La médiane de trois entiers naturels est l’une des trois valeurs telle que l’une
des deux autres est inférieure ou égale et l’autre supérieure ou égale. Considé-
rons l’opération med(v1 , v2 , v3 ) qui, partant de trois entiers naturels, délivre leur
médiane. Ainsi med(89, 4, 12) renvoie 12 tandis que med(5, 76, 5) s’évalue à 5.
Supposons que le développement nous conduit à la version incomplète suivante :

function med(v1 , v2 , v3 ) ∈ N × N × N → N =

⏐ (v1 , v2 , v3 ) ∈ N × N × N

⏐ if v ≤ v →
⏐ ⏐1 2
⏐ ⏐
⏐ ⏐ (v1 , v2 , v3 ) ∈ N × N × N ∧v1 ≤ v2
⏐ ⏐
⏐ ⏐ if v3 ≤ v1 →
⏐ ⏐
⏐ ⏐
⏐ ⏐ v1
⏐ ⏐ |
⏐ ⏐ v 3 ≥ v2 →
⏐ ⏐
⏐ ⏐ v2
⏐ ⏐ .
⏐ ⏐ ..

Concernant la construction de la conditionnelle interne, nous pouvons consta-


ter que pour l’instant la propriété 1.11.13 n’est pas satisfaite : (v1 , v2 , v3 ) ∈
N × N × N ∧ v1 ≤ v2  v3 ≤ v1 ∨ v3 ≥ v2 . Quelle formule pourrait-on introduire
en disjonction pour nous rapprocher de l’implication ? v1 ≤ v3 ∧ v3 ≤ v2 est un
bon candidat. En outre la formule (v1 , v2 , v3 ) ∈ N × N × N ∧ v1 ≤ v2 ⇒ v3 ≤
v1 ∨ v3 ≥ v2 ∨ (v1 ≤ v3 ∧ v3 ≤ v2 ) est un théorème (il y a même équivalence). Le
1. Mathématiques pour la spécification et les structures de données 55

développement des gardes de l’expression gardée la plus interne est maintenant


achevé. Le prédicat v2 ≤ v1 est une garde convenable pour la prochaine expres-
sion gardée externe. Des considérations de symétrie peuvent alors être utilisées
pour compléter le code de l’opération. Nous obtenons :

function med(v1 , v2 , v3 ) ∈ N × N × N → N =

if v1 ≤ v2 →
if v3 ≤ v1 →
v1
| v3 ≥ v2 →
v2
| v1 ≤ v3 ∧ v 3 ≤ v 2 →
v3

| v2 ≤ v1 →
if v3 ≤ v2 →
v2
| v3 ≥ v1 →
v1
| v2 ≤ v3 ∧ v 3 ≤ v1 →
v3

Exercices

Exercice 1.11.1 Évaluer les expressions suivantes pour t ∈ 1..10→N et i, j, k entiers naturels :
1. pre i ∈ dom(t) then t(i) end = pre j = 0 then k ÷ j end
2. pre
∃i ·(i ∈ 1 .. 10 ∧ t(i) = 0)
then
j : (j ∈ N ∧ j ∈ 1 .. 10 ∧ t(i) = 0)
end
3. pre
i ∈ N ∧ i ∈ 1 .. 3 ∧ i = 2
then
if i = 1 →
3
|i=3 →
1

end

Exercice 1.11.2 Soit la fonction f représentée par :

function f (v) ∈ N → N =

if v ≥ 5 →
4
|v≤5 →
9

56 Structures de données et méthodes formelles

Évaluer f (3) puis f (5). Conclusion ?

Exercice 1.11.3 Soit l’opération dz(t) spécifiée par :

function dz(t) ∈ (1 .. 100 → N) → 1 .. 100 = 


pre
card(t  {0}) ≥ 10 ∧
then
q : (q ∈ 1 .. 100 ∧ t(q) = 0 ∧ card(1 .. q  t  0) = 10)
end

1. En utilisant le tableau 1.1, page 53, calculer la précondition de l’expression entre then et
end.
2. Fournir une représentation concrète de cette opération. Calculer la précondition des
expressions utilisées. En déduire s’il y a lieu que les conditionnelles satisfont la condi-
tion 1.11.13, page 54.

Exercice 1.11.4 Soit l’opération trie3(v1 , v2 , v3 ) destinée à rendre les trois valeurs dans l’ordre
croissant. Cette opération est ébauchée par :

function trie3(v1 , v2 , v3 ) ∈ N × N × N → N × N × N =

if v1 ≤ v3 →
..
.

Compléter cette opération en calculant les gardes à partir de la propriété 1.11.13. Démon-
trer que chaque conditionnelle satisfait cette propriété.

1.12 Conclusion et remarques bibliographiques


Ce chapitre pose les bases des mathématiques utilisées dans le reste de l’ou-
vrage. L’importance du raisonnement par récurrence/induction est à souligner.
La maîtrise des notations est indispensable, notamment celles se rapportant à la
théorie des ensembles.
À travers les notions de tableau et de structure inductive, ce chapitre fixe
également le cadre dans lequel nous nous apprêtons à définir ce que nous appelons
le support d’une structure de données, c’est-à-dire l’ensemble sur lequel une
structure de données prend ses valeurs.
Clairement inspiré des ouvrages et articles de J.-R. Abrial (principalement
[3, 5] mais aussi [6]), ce chapitre s’en écarte à plusieurs reprises. La raison prin-
cipale est que notre approche du développement est fonctionnelle et que les opé-
rations calculées sont en général récursives. Par contre, nous avons pris à notre
compte les principes de la méthode B qui consistent à séparer clairement la no-
tion de prédicat et celle d’expression, et à ne nous préoccuper que de l’aspect
démonstration en négligeant complètement l’aspect vérité.
Les ouvrages [119] et [50] réalisent une présentation plus vulgarisée de la
méthode B classique. De nombreux autres ouvrages traitent du thème de cette
section. Parmi ceux-ci citons l’ouvrage de J.-F. Monin [83] qui présente un pano-
rama des outils mathématiques pour les méthodes formelles. L’ouvrage d’A. Ar-
nold et I. Guessarian [9] élargit sa thématique à l’ensemble des mathématiques
1. Mathématiques pour la spécification et les structures de données 57

utiles à l’informaticien. Dans [51], H. Habrias, M. Frappier et les coauteurs réa-


lisent un tour d’horizon comparatif des principales méthodes formelles, qui sont
appliquées sur le même exemple. Pour les lecteurs plus attirés par la théorie,
nous pouvons citer les deux ouvrages [77] et [76]. Le premier est tourné vers la
logique et ses applications, tandis que le second est uniquement dédié à la théorie
des ensembles.
Chapitre 2

Spécifications + Fonction
d’abstraction + Calcul =
Programme

Ce chapitre présente les principes qui sont appliqués dans le reste de


l’ouvrage pour concevoir et mettre en œuvre des structures de données de ma-
nière rigoureuse. La première section fixe le cadre général de la démarche. Dans
la seconde section nous nous intéressons à la structure des unités constituant un
type abstrait ou concret. Un premier exemple complet est présenté. La troisième
section montre comment parvenir au code des opérations d’un type concret. Il
justifie à lui seul le titre du chapitre. La section 2.4 développe un point qui, dans
certains cas, est à la base de la dernière étape du développement : le renforcement
de support.

2.1 Cadre général de la démarche


Dans cet ouvrage, le principe conduisant à la réalisation de structures de
données consiste, partant de la fourniture de leurs spécifications, à parvenir ra-
tionnellement à des mises en œuvre (concrètes).
Spécifier, c’est fournir des propriétés, c’est dire le « quoi ? » ; mettre
en œuvre, c’est au contraire répondre au « comment ? » À l’opposition
quoi/comment s’ajoute le fait que l’activité de mise en œuvre, contrairement
à celle de spécification, a pour objectif d’atteindre une solution qui soit exécu-
table, si possible efficacement.
Commençons par préciser ce que nous appelons structure de données. Une
structure de données est une forme d’algèbre (cf. encadré page suivante pour un
rappel des notions d’algèbre et d’homomorphisme) constituée :
– d’un ensemble de valeurs possibles. Cet ensemble peut être défini en ex-
tension ou en compréhension. Dans cet ouvrage, cet ensemble est appelé
support ;
60 Structures de données et méthodes formelles

Algèbres et homomorphismes
Soit n ∈ N, soit s un ensemble et f tel que f ∈ sn →  s. Le couple
S = (s, f ) est appelé une algèbre.
Soit T = (t, o) et T  = (t , o ) deux algèbres où o et o sont de même
arité n. La fonction H, H ∈ t → t, est un homomorphisme de T  vers T
si, pour tout n-uplet v1 , . . . , vn ∈ dom(o ) :
H(o (v1 , . . . , vn )) = o(H(v1 ), . . . , H(vn ))

Si H est une fonction bijective, elle est appelée isomorphisme.


Par exemple, toute fonction f ∈ R∗+  R telle que f (a · b) = f (a)+f (b)
est un homomorphisme de (R∗+ , ×) vers (R, +) et puisque f est bijective,
c’est un isomorphisme. De telles fonctions f sont appelées fonctions loga-
rithmes. Dans la suite nous utilisons fréquemment une fonction f parti-
culière, définie par f (2) = 1 appelée logarithme en base 2 (ou logarithme
binaire). Habituellement représentée sous la forme log2 , dans la suite du
livre (sauf mention explicite de la base), elle est simplement notée log.
Les notions d’algèbre et d’homomorphisme s’étendent naturellement
au cas où il existe un jeu d’opérations (et non plus une seule opération)
dans chacune des structures, en relation biunivoque.
En informatique, il arrive fréquemment que des opérations intéres-
santes dérogent à la définition ci-dessus dans le sens où, soit l’ensemble
source de l’opération peut contenir un support externe, soit l’ensemble
destination est lui-même un support externe. Le terme d’algèbre de type
est parfois utilisé pour qualifier de telles algèbres et souligner la différence
avec les algèbres « strictes ». La notion d’homomorphisme s’étend natu-
rellement aux algèbres de type. Soit T = (t, o) avec o ∈ e × t →  t (e = t)
et T  = (t , o ) avec o ∈ e × t → t , (e = t ) deux algèbres de type. La
fonction H, H ∈ t → t est un homomorphisme de T  vers T si, pour tout
couple (w, v  ) tel que (w, v  ) ∈ dom(o ) :
H(o (w, v  )) = o(w, H(v  ))

 e, (e = t) et T  = (t , o ) avec o ∈
De même, soit T = (t, o) avec o ∈ tn →
t → e (e = t ) deux algèbres de type. La fonction H, H ∈ t → T est un
n 

homomorphisme de T  vers T si pour tout n-uplet v1 , . . . , vn ∈ dom(o ) :

o (v1 , . . . , vn ) = o(H(v1 ), . . . , H(vn )).

– d’un ensemble fini d’opérations qui se présentent ici sous la forme de fonc-
tions. L’un au moins des arguments et/ou le résultat de chaque fonction
appartient au support. Les opérations qui délivrent un résultat apparte-
nant au support sont qualifiées d’internes. Les autres sont les opérations
externes.
2. Spécifications + Fonction d’abstraction + Calcul = Programme 61

Le concept de structure de données ainsi défini se démarque de celui de base


de données, principalement par le fait que concevoir une base de données consiste
en général à définir un ensemble de valeurs en compréhension, en se focalisant
sur l’aspect normatif de sa structuration. Une autre différence tient au fait que
dans les bases de données, la consultation se fait par l’intermédiaire d’un langage
de requêtes générique et non par un jeu fini d’opérations ad hoc comme dans le
cas des structures de données.

spécification abstraite représentation concrète

support support
abstrait concret

opérations fonction
abstraites d’abstraction
spécification
des
opérations
concrètes

représentation
calcul des opérations
concrètes

Figure 2.1 – Schéma de principe de la méthode.


Ce schéma montre à partir de quelles informations nous calculons les
opérations concrètes d’une structure de données. La représentation abs-
traite de la structure de données (sur la gauche) décrit le support abs-
trait et spécifie les opérations abstraites. La représentation concrète (sur
la droite) définit le support concret, la fonction d’abstraction et la spé-
cification des opérations concrètes. Ces informations permettent en gé-
néral de calculer la représentation concrète des opérations.

La démarche de conception dont nous nous faisons l’avocat et que nous ap-
pliquons dans cet ouvrage se décompose en quatre étapes :
1. spécifier le type abstrait que l’on souhaite mettre en œuvre,
2. spécifier un type concret cible du développement,
3. calculer la représentation des opérations concrètes,
4. effectuer un éventuel raffinement informel.
Cette démarche est illustrée à la figure 2.1 ci-dessus.

2.2 Formalisme pour les types – Exemple


Dans cette section nous présentons la syntaxe des notations utilisées pour la
spécification abstraite (abstractType) et pour le raffinement (concreteType).
62 Structures de données et méthodes formelles

Ces notions sont illustrées à travers un exemple simple.


Le texte d’une spécification abstraite est décrit conformément à la catégorie
syntaxique typeAbst suivante :

typeAbst : := abstractType
profil
uses
listeIdentParam
constraints
prédicat
support
prédicat
operations
listeOpérations
auxiliaryOperationRepresentations
listeOpérations
end
listeIdent : := ident |
ident, listeIdent
listeIdentParam : := identParam |
identParam, listeIdentParam
identParam : := ident |
ident(listeIdent)
listeOpérations : := opération |
opération; listeOpérations
profil : :=  (identParam, (listeIdent), (listeIdent))
identParam = |
 (identParam, (listeIdent), ())
identParam =

Afin d’illustrer les formalismes utilisés et l’information qu’ils véhiculent, nous


allons considérer l’exemple suivant. Le type abstrait entnat (cf. figure 2.2,
page 64) vise à représenter l’ensemble des entiers naturels muni des deux opéra-
tions plus et inf (correspondant aux opérations + et < traditionnelles). Cette
figure contient les rubriques suivantes :
– abstractType qui identifie le type abstrait considéré (entnat), le support
(entN at 1 ), la liste des opérations internes (plus) et la liste des opérations
externes (inf ). Le nom du type abstrait peut être suivi d’un ou plusieurs
paramètres formels. Ceux-ci sont notés entre parenthèses et séparés par
des virgules. La rubrique constraints est destinée à typer ces paramètres
par un prédicat de typage ;
– uses qui mentionne les types utilisés pour décrire le type considéré. Si son
contenu est vide, cette rubrique n’est pas obligatoire ;
1. Nous adoptons la convention consistant à nommer le type par le même identificateur que
le support, le premier étant écrit en petites capitales, le second non.
2. Spécifications + Fonction d’abstraction + Calcul = Programme 63

– support qui définit l’ensemble qui sert de support au type considéré ;


– operations qui spécifie les opérations de l’en-tête (internes et externes).
Outre la rubrique constraints, la rubrique auxiliaryOperationRepresen-
tations n’est pas illustrée par la figure 2.2. Cette dernière rubrique correspond
au cas où des opérations auxiliaires sont nécessaires à la spécification des opéra-
tions principales (cf. section 8.2 pour un exemple).
La spécification concrète ainsi que le résultat du raffinement sont décrits par
la catégorie syntaxique typeConc :

typeConc : := concreteType
profil
uses
listeIdentParam
constraints
prédicat
refines
identParam
auxiliarySupports
prédicat
support
prédicat
auxiliaryAbstractionFunction
listeOpérations
abstractionFunction
opération
auxiliaryOperationSpecifications
listeOpérations
operationSpecifications
listeOpérations
auxiliaryOperationRepresentations
listeOpérations
operationRepresentations
listeOpérations
end

En guise d’exemple, le type entnat est raffiné par le type nat présenté à
la figure 2.3, page 65. L’idée qui prévaut dans ce raffinement est de représenter
les entiers naturels selon une approche « à la Peano ». Ceci se reflète dans la
rubrique support qui définit par induction l’ensemble dénommé nat.
Les rubriques que nous rencontrons sont les suivantes :
– concreteType qui est l’homologue de la rubrique abstractType de la
description précédente. Elle permet d’énumérer les mêmes informations :
nom du type (nat), liste des opérations internes et liste des opérations
64 Structures de données et méthodes formelles

externes. Pour éviter toute confusion, le nom des opérations est celui des
opérations abstraites, enrichi d’un suffixe discriminant (_n pour nat ici) ;
– refines qui mentionne le type abstrait raffiné et ses éventuels paramètres
effectifs (cf. page 173 pour un exemple mentionnant un paramètre effectif) ;
– uses qui a le même sens que précédemment ;
– support qui a déjà été commentée. Pour cet exemple, l’ensemble est défini
de manière inductive (cf. chapitre 3) ;
– abstractionFunction qui décrit la fonction d’abstraction dont le rôle est
d’associer à chaque valeur concrète son homologue abstrait. Cette fonction
calque en général sa structure sur celle du support concret. En particu-
lier dans l’exemple considéré elle est définie par « induction structurelle »
puisque le support concret est lui-même inductif. En outre, c’est une bijec-
tion totale (qui définit donc un isomorphisme), d’où l’utilisation du sym-
bole ;
– operationSpecifications qui possède la même signification que la ru-
brique operations du type abstrait. La différence tient à ce qu’ici l’opé-
ration est spécifiée par homomorphisme entre la structure concrète et la
structure abstraite (cf. encadré page 60 pour un rappel sur la notion d’ho-
momorphisme).
La rubrique operationRepresentations décrit la représentation des opé-
rations. Les calculs sont présentés à la section suivante.

abstractType entnat =  (entN at, (plus), (inf ))


uses
bool, N
support
x ∈ N ⇔ x ∈ entN at
operations
function plus(x, y) ∈ entN at × entN at  entN at =  x+y
;
function inf (x, y) ∈ entN at × entN at  bool =  bool(x < y)
end

Figure 2.2 – Spécification du type abstrait entnat.


Exemple de spécification abstraite. Cette spécification fournit une illus-
tration de la rubrique d’en-tête et des rubriques uses, support et ope-
rations.

2.3 Calcul des opérations – Exemple


À la section précédente, la rubrique operationRepresentations de la fi-
gure 2.3, page ci-contre, fournit, sans aucune justification, le code des deux
opérations plus_n et inf _n. L’intérêt et la puissance de la démarche que nous
préconisons est de construire (on dit aussi calculer ou dériver) ce code à par-
tir de son environnement. C’est le point sur lequel nous nous focalisons ici, en
déroulant ce calcul pour les deux opérations du type considéré.
2. Spécifications + Fonction d’abstraction + Calcul = Programme 65

concreteType nat =  (nat, (plus_n), (inf _n))


uses bool
refines entnat
support
1) zero ∈ nat
2) x ∈ nat ⇒ suiv(x) ∈ nat
abstractionFunction
function A(x) ∈ nat   entN at = 
if x = zero →
0
| x = suiv(v) →
plus(A(v), 1)

operationSpecifications
function plus_n(x, y) ∈ nat × nat  nat = 
v : (v ∈ nat ∧ A(v) = [x , y  := A(x), A(y)]plus(x , y  ))
;
function inf _n(x, y) ∈ nat × nat  bool = 
[x , y  := A(x), A(y)]inf (x , y  )
operationRepresentations
function plus_n(x, y) ∈ nat × nat  nat = 
if x = zero →
y
| x = suiv(v) →
suiv(plus_n(v, y))

;
function inf _n(x, y) ∈ nat × nat  bool = 
if y = zero →
false
| y = suiv(y  ) →
if x = zero →
true
| x = suiv(x ) →
inf _n(x , y  )


end

Figure 2.3 – Représentation du type concret nat.


Exemple de représentation concrète. De même que pour le cas de la
spécification abstraite (cf. figure 2.2, page ci-contre), on retrouve les
rubriques uses et support. La rubrique abstractionFunction éta-
blit la correspondance entre le support abstrait et le support concret. la
rubrique operationSpecifications fournit une spécification concrète
des opérations. Enfin la rubrique operationRepresentations récapi-
tule le résultat du calcul des différentes opérations.
66 Structures de données et méthodes formelles

2.3.1 Calcul d’une représentation de l’opération plus_n


L’opération concrète plus_n est spécifiée par :

 v : (v ∈ nat ∧ A(v) = [x , y  := A(x), A(y)]plus(x , y  ))


plus_n(x, y) =

plus_n(x, y) est inconnu. L’expression en partie droite délivre un élément v


quelconque de nat, tel que l’homologue abstrait de v est égal à l’expression
plus(x , y  ) dans laquelle A(x) (resp. A(y)) se substitue à x (resp. y  ). La valeur
abstraite correspondant à plus_n(x, y) est A(plus_n(x, y)). Entamons un calcul
à partir de cette dernière expression :

A(plus_n(x, y))
= Spécification concrète
A(v : (v ∈ nat ∧ A(v) = [x , y  := A(x), A(y)]plus(x , y  )))
= Substitution
A(v : (v ∈ nat ∧ A(v) = plus(A(x), A(y))))
= Propriété C.22
plus(A(x), A(y))
= Spécification abstraite
A(x) + A(y)

Cette séquence de calculs est longue et fastidieuse. En outre, elle apparaît


sous une forme proche dans le calcul de chaque opération interne. Pour éviter de
la répéter inutilement, d’une part nous supposons à l’avenir que la substitution
est systématiquement réalisée dès la spécification concrète (nous écrirons doré-
navant plus_n(x, y) =  A(v : (v ∈ nat ∧ A(v) = plus(A(x), A(y))))) et d’autre
part la séquence complète est maintenant intitulée « propriété caractéristique »,
et s’abrège sous une forme équivalente à :

A(plus_n(x, y))
= Propriété caractéristique
A(x) + A(y) (2.3.1)

Bien entendu, la détermination de l’inconnue plus_n(x, y) n’a pas encore


abouti. Partir de l’expression A(plus_n(x, y)) présente l’avantage de nous per-
mettre de raisonner dans « l’espace abstrait ». Notre calcul nous a temporaire-
ment conduit à l’expression (abstraite) A(x) + A(y) mais si nous parvenions à
l’issue du calcul à une formule (éventuellement conditionnée par un prédicat) du
type A(exp), nous pourrions conclure qu’une solution possible à notre équation
est plus_n(x, y) = exp.

Pour bien comprendre ce mode de résolution, considérons une fonction quel-


conque f et soit v ∈ dom(f ). v est solution de l’équation en x f (x) = f (v).
Formalisée, cette propriété se présente de la manière suivante :
2. Spécifications + Fonction d’abstraction + Calcul = Programme 67

Propriété 1 (de l’équation à membres identiques).


f ∈s→
 t ∧ v ∈ dom(f ) ⇒ v ∈ {x | x ∈ dom(f ) ∧ f (x) = f (v)} (2.3.2)
Dans le cas où f est injective, v est l’unique solution. Pour comprendre l’inté-
rêt de cette propriété dans le calcul de la représentation d’opérations concrètes,
débutons par un exemple numérique simple. Considérons la fonction sin. sin ∈
R  [−1 .. 1]. Nous savons que, sur R, sin n’est pas injective : étant donné
e ∈ [−1 .. 1], l’équation trigonométrique sin(x) = e possède plusieurs (une in-
finité en fait) solutions. Si l’on sait que sin(x) = sin( 174· Π ), la propriété 2.3.2
permet de conclure que x = 174· Π est solution. Compte tenu du caractère non
injectif de la fonction sin, la trigonométrie nous apprend que, sur R, x = Π4 ,
x = 3 ·4Π , x = Π4 + 2 · Π, etc. sont aussi solutions de l’équation.
Poursuivons notre calcul à partir de la formule 2.3.1. Nous pouvons difficile-
ment éviter de faire une hypothèse sur la structure de x et/ou celle de y. Nous
choisissons de procéder par induction sur la structure de x. Deux cas sont donc
à considérer : x = zero et x = suiv(v). Nous débutons par le cas x = zero.

A(x) + A(y)
= Hypothèse (x = zero)
A(zero) + A(y)
= Définition de A
0 + A(y)
= Arithmétique
A(y)

Nous avons donc pour le cas x = zero :


A(plus_n(x, y)) = A(y)
D’après la propriété ci-dessus de l’équation à membres identiques, une solution
à l’équation en plus_n(x, y) A(plus_n(x, y)) = A(y) est y. Ceci nous fournit la
première équation gardée de la représentation de la fonction plus_n :

x = zero →
plus_n(x, y) = y

Le cas inductif (x = suiv(v)) se traite de la manière suivante, en reprenant


la formule 2.3.1 ci-dessus :
A(x) + A(y)
= Hypothèse (x = suiv(v))
A(suiv(v)) + A(y)
= Définition de A
plus(A(v), 1) + A(y)
= Définition de plus
A(v) + 1 + A(y)
= Arithmétique
A(v) + A(y) + 1
= Propriété caractéristique de l’opération plus_n
68 Structures de données et méthodes formelles

A(plus_n(v, y)) + 1
= Spécification de plus
plus(A(plus_n(v, y)), 1)
= Définition de A (cas inductif)
A(suiv(plus_n(v, y)))
De même que ci-dessus (en appliquant la propriété de l’équation à membres
identiques, page précédente), suiv(plus_n(v, y)) est une solution de l’équation
en plus_n(x, y) A(plus_n(x, y)) = A(suiv(plus_n(v, y))).
D’où la seconde équation gardée de la représentation de la fonction plus_n :

x = suiv(v) →
plus_n(x, y) = suiv(plus_n(v, y))

Au total, en intégrant les deux équations gardées au sein d’une notation tra-
ditionnelle, nous avons calculé la représentation suivante de l’opération plus_n.

function plus_n(x, y) ∈ nat × nat  nat =



if x = zero →
y
| x = suiv(v) →
suiv(plus_n(v, y))

2.3.2 Calcul d’une représentation de l’opération inf _n


La trame générale du calcul de l’opération inf _n s’apparente à celle dévelop-
pée ci-dessus. Cependant il s’agit cette fois d’une opération externe (elle délivre
un booléen). Il n’est donc pas question d’appliquer la fonction d’abstraction à
l’inconnue inf _n(x, y) :

inf _n(x, y)
= Spécification concrète
[x , y  := A(x), A(y)]inf (x , y  )
= Substitution
inf (A(x), A(y))
= Spécification abstraite
bool(A(x) < A(y))

De même que pour les opérations internes, la substitution est à l’avenir déportée
dans la spécification concrète et le reste de la séquence est également dénommé
« propriété caractéristique » :

inf _n(x, y)
= Propriété caractéristique de l’opération inf _n
bool(A(x) < A(y)) (2.3.3)

Nous poursuivons le calcul en procédant à une induction sur y. Deux cas sont
donc à envisager : y = zero et y = zero. Débutons par y = zero.
2. Spécifications + Fonction d’abstraction + Calcul = Programme 69

bool(A(x) < A(y))


= Hypothèse y = zero
bool(A(x) < A(zero))
= Définition de A
bool(A(x) < 0) (2.3.4)
Nous pouvons alors réaliser une induction incidente sur x. Nous constaterons plus
tard que le résultat est identique dans les deux branches du calcul. Débutons
par le cas x = zero.
bool(A(x) < 0)
= Hypothèse x = zero
bool(A(zero) < 0)
= Définition de A
bool(0 < 0)
= Arithmétique
bool(⊥)
= Définition de bool
false
Le cas x = zero se traite de la manière suivante. Si x = zero c’est donc que,
d’après la définition du support nat, il existe un x tel que x = suiv(x ). Nous
reprenons le calcul à partir de la formule 2.3.4 ci-dessus.
bool(A(x) < 0)
= Hypothèse
bool(A(suiv(x )) < 0)
= Définition de A
bool(plus(A(x ), 1) < 0)
= Définition de la fonction plus
bool(A(x ) + 1 < 0)
A(x ) est un entier naturel, l’arithmétique nous apprend qu’il n’existe pas d’en-
tier naturel qui, ajouté à 1, soit négatif. A(x ) + 1 < 0 est donc identique à
⊥:
bool(A(x ) + 1 < 0)
= Remarque ci-dessus
bool(⊥)
= Définition de bool
false
Les gardes x = zero et x = zero conduisent donc au même résultat false. Les
deux équations gardées fusionnent pour donner :
y = zero →
inf _n(x, y) = false
Revenons sur la première induction en considérant le second cas, y = zero.
Posons y = suiv(y  ). Le développement reprend à partir de la formule 2.3.3,
page ci-contre.
70 Structures de données et méthodes formelles

bool(A(x) < A(y))


= Hypothèse
bool(A(x) < A(suiv(y  )))
= Définition de A
bool(A(x) < plus(A(y  ), 1))
= Définition de la fonction plus
bool(A(x) < A(y  ) + 1) (2.3.5)

Nous devons à nouveau procéder à une induction sur x. Débutons par le cas
x = zero.

bool(A(x) < A(y  ) + 1)


= Hypothèse
bool(A(zero) < A(y  ) + 1)
= Définition de A
bool(0 < A(y  ) + 1)
= Arithmétique
bool()
= Définition de bool
true

D’où l’équation gardée :

y = zero →
x = zero →
inf _n(x, y) = true

Pour la partie inductive, x = zero, nous pouvons faire l’hypothèse que x =


suiv(x ) puis repartir de la formule 2.3.5 ci-dessus.

bool(A(x) < A(y  ) + 1)


= Hypothèse
bool(A(suiv(x )) < A(y  ) + 1)
= Définition de A
bool(plus(A(x ), 1) < A(y  ) + 1)
= Définition de la fonction plus
bool(A(x ) + 1 < A(y  ) + 1)
= Arithmétique
bool(A(x ) < A(y  ))
= Propriété caractéristique de la fonction inf _n
inf _n(x , y  )

D’où l’équation gardée :

y = suiv(y  ) →
x = suiv(x ) →
inf _n(x, y) = inf _n(x , y  )

Au total, nous avons calculé la représentation suivante de l’opération inf _n :


2. Spécifications + Fonction d’abstraction + Calcul = Programme 71

function inf _n(x, y) ∈ nat × nat  bool =



if y = zero →
false
| y = suiv(y  ) →>
if x = zero →
true
| x = suiv(x ) →
inf _n(x , y  )

Remarque. Soit t le support du type concret T . Soit s, s ∈ t la structure


faisant l’objet du calcul lors de la recherche de la représentation d’une opéra-
tion. Il peut arriver que, momentanément, s n’appartienne plus au support t,
mais appartienne à t , sur-ensemble de t. Transformer s de façon à le faire ré-
intégrer t exige en général de disposer d’une fonction d’abstraction A dont le
domaine de définition soit t et non t. Plusieurs exemples illustrant cette néces-
sité apparaissent ci-dessous, en particulier dans les sections consacrées aux Avl
(cf. section 6.6, page 191) et aux B-arbres (cf. section 6.7, page 216).

Exercices

Exercice 2.3.1 Démontrer la propriété de l’équation à membres identiques rappelée ci-


dessous :

f ∈s→
 t ∧ v ∈ dom(f ) ⇒ v ∈ {x | x ∈ dom(s) ∧ f (x) = f (v)}

Exercice 2.3.2 Démontrer la propriété de l’équation à membres identiques dans le cas d’une
fonction injective :

f ∈s
 t ∧ v ∈ dom(f ) ⇒ {v} = {x | x ∈ dom(s) ∧ f (x) = f (v)}

2.4 Induction, support et renforcement


Malgré leurs différences superficielles, l’identité profonde entre d’une part la
réalisation d’une preuve par induction et d’autre part la construction de pro-
grammes itératifs, récursifs ou encore la mise en œuvre de structures de données
a été remarquée et exploitée depuis longtemps (cf. [10, 11, 32, 33, 80] pour des
approches tirant directement profit du raisonnement par induction en program-
mation impérative). Le tableau 2.1, page 73, rappelle la correspondance entre le
vocabulaire des mathématiques et celui de l’informatique.
Ainsi que le montre le tableau 2.1, pour des structures de données, le support
se comporte comme un invariant : les fonctions de construction, qui ont comme
codomaine le support, ont pour rôle d’instaurer cet invariant ; les fonctions de
72 Structures de données et méthodes formelles

Notations fonctionnelles
Le calcul et la manipulation des opérations d’un type donné nous
conduisent à utiliser deux sortes de notation pour représenter des fonc-
tions : la notation équationnelle et la notation traditionnelle (monoli-
thique). Ces deux notations sont bien sûr équivalentes mais présentent
chacune des avantages dans leur contexte d’utilisation. À l’exception des
cas les plus simples, le calcul de la représentation des fonctions se fait de
manière incrémentale, chaque étape produisant une « équation gardée ».
La notation équationnelle est la représentation la plus naturelle pour pré-
senter le résultat d’une étape de calcul. À l’issue du calcul il est alors
possible de regrouper l’ensemble des fragments obtenus sous la forme plus
classique d’une conditionnelle. Ci-dessous nous illustrons ces notations en
prenant comme exemple la fonction pgcd (le plus grand commun diviseur
entre deux entiers naturels, cet exemple est inspiré de [57]). Le calcul
fournit successivement les trois « équations gardées » suivantes :

x=y→
pgcd(x, y) = x

x>y→
pgcd(x, y) = pgcd(x − y, y)

x<y→
pgcd(x, y) = pgcd(y, x)

Nous pouvons alors rassembler ces trois fragments au sein d’une re-
présentation qui intègre l’en-tête de la fonction :
function pgcd(x, y) ∈ N × N → N =

if x = y →
x
|x>y→
pgcd(x − y, y)
|x<y→
pgcd(y, x)

Le cas de la spécification des opérations du type abstrait ainsi que


celui de la fonction d’abstraction se présentent différemment puisque ces
fonctions sont données et non calculées. Seule la forme monolithique est
alors utilisée. Une variante tabulaire de cette dernière notation est par-
fois utilisée. Les informations y sont présentées en trois colonnes : nota-
tion/définition/condition (cf. section 1.7).
2. Spécifications + Fonction d’abstraction + Calcul = Programme 73

mise à jour 2 (dans lesquelles le support apparaît à la fois comme partie du


domaine de définition et comme codomaine) préservent l’invariant.
Qu’en est-il alors de la situation de la technique de renforcement étudiée à la
section 1.10 ? Nous aurons l’occasion de l’appliquer strictement dans le cadre de
raisonnements de nature mathématique (cf. par exemple section 6.6) mais ce qui
nous intéresse le plus ici est sa transposition au cas des structures de données 3 .

Tableau 2.1 – Raisonnement par induction et programmation.


Équivalence approximative entre le vocabulaire utilisé dans une démons-
tration par induction et le vocabulaire de l’informatique (construction
de boucles, récursivité et structures de données).

Rais. par Structures


Boucles Récursivité
induction de données
Arguments
Base Initialisation de l’appel Constructeur
initial
Hypothèse Hypothèse
Invariant Support
d’induction d’induction
Étape Corps de Opérations
Progression
inductive l’opération de mise à jour

Renforcer le support d’une structure de données consiste en général à y intro-


duire explicitement une information latente. Du point de vue méthodologique,
cette information complémentaire n’a pas lieu d’être introduite dès la concep-
tion du support, elle pourrait se révéler sans objet ou compliquer inutilement
le calcul. C’est en général au cours du développement des opérations que l’on
prend conscience de son intérêt. Le plus souvent cette information complémen-
taire n’est autre que le résultat de l’évaluation d’une fonction particulière, qui
n’est encore qu’à l’état de spécification. Deux types de comportement sont alors
envisageables :
1. Soit on évalue cette fonction à chaque appel. Cette solution est toujours
possible mais elle peut se révéler coûteuse.
2. Soit on introduit explicitement le résultat de l’évaluation dans la structure
de données. Accéder à la valeur recherchée est alors immédiat, mais ceci se
fait au prix d’un accroissement de la place occupée et de la modification des
opérations de mise à jour, qui doivent préserver l’intégrité de la nouvelle
information. Pour se révéler compétitive, cette solution ne doit engendrer
qu’un faible surcoût.
Prenons par exemple le cas d’une liste d’entiers naturels qui est mise à jour
par des opérations d’adjonction et de suppression.
2. Cette dénomination est abusive dans la mesure où un programme fonctionnel pur ne
modifie pas les structures de données.
3. Pour le cas des boucles, on peut consulter par exemple [24, 30, 31, 66, 45]. Pour celui
des opérations récursives, voir par exemple [14, 79].
74 Structures de données et méthodes formelles

• 7 • 12 • 4 /

Supposons par ailleurs que parmi les opérations de consultation de la structure


de données nous ayons besoin d’une fonction moy qui délivre la moyenne des
valeurs présentes dans une liste l non vide. Cette fonction s’exprime par :

function moy(l) ∈ liste →


 R=

pre
l = [ ]
then
somme(l)/#(l)
end

où [ ] dénote la liste vide (cf. section 3.1 pour de plus amples développements
sur les listes), somme(l) est la somme des valeurs présentes dans la liste, tandis
que #(l) est la longueur de la liste. Si l’on s’interdisait de renforcer le support,
la mise en œuvre de la fonction moy ne pourrait éviter (au moins) un parcours
(coûteux) de la liste. Par contre, nous pouvons très bien enrichir la structure de
données (c’est-à-dire renforcer l’invariant) en y enregistrant les deux informations
complémentaires, la somme et la longueur. Nous obtenons alors :

23
• 7 • 12 • 4 /
3

Le calcul de la moyenne s’en trouve notablement simplifié. Cependant, le support


ayant été renforcé, les opérations d’initialisation et de mise à jour doivent tenir
compte des nouveaux champs. En général, les modifications induites par le ren-
forcement du support sont simples et n’exigent pas de recalculer les opérations.
Dans les cas les plus complexes, un nouveau calcul peut se révéler nécessaire.
Parmi les formes de renforcement les plus utiles, nous allons nous arrêter
sur celle qui est intitulée « décomposition d’une fonction sur la structure de
données ».

Décomposition d’une fonction sur la structure de données. L’exemple


de la liste est particulièrement simple. Il arrive fréquemment que la fonction à la
base du renforcement calque sa structure sur celle du support. Ça serait le cas
si dans l’exemple de la liste nous avions besoin de connaître pour chaque cellule
la somme des éléments du reste de la liste. Si nous souhaitons appliquer une
technique de renforcement (pour cet exemple simple, il existe d’autres solutions
tout aussi efficaces), il faut adjoindre un champ complémentaire à chaque cellule
et y placer la valeur de la fonction. Toute mise à jour se fait alors en général
en utilisant le nouveau champ de la cellule suivante (si elle existe). Nous avons
décomposé la fonction sur la structure de données 4 .
4. Ce type de démarche s’apparente à la technique utilisée en programmation dynamique
où l’on enregistre les valeurs dans un tableau afin d’éviter de refaire des calculs inutiles.
2. Spécifications + Fonction d’abstraction + Calcul = Programme 75

• 16 7 • 4 12 • 0 4 /

Pour être efficace, cette technique ne doit décomposer que les fonctions O(1)-
décomposables (cf. encadré page 188 et [68]).

La section 6.9 sur les treaps aléatoires fournit un exemple de renforcement


(sans décomposition). Les sections 6.5, 6.6 et 9.4 illustrent la technique du ren-
forcement par décomposition. D’autres exemples peuvent être rencontrés au fil
des exercices.

2.5 Conclusion
Que faire du type concret obtenu à l’issue de la phase de raffinement formel ?
En général, ce type peut être aisément traduit puis exécuté en utilisant un lan-
gage fonctionnel tel que Caml, Scheme, Lisp, Haskell, etc. Une traduction dans
un langage algorithmique classique à base de pointeurs (comme Pascal, Ada,
etc.) ou à base d’objet (comme Ada ou Java) se révèle un peu plus délicate.
Dans ce dernier cas, le principe qu’il convient d’appliquer à la lettre consiste à
s’assurer que l’on ne modifie jamais une structure (ou un objet) existant(e). Il
faut systématiquement en créer de nouvelles.
En toute hypothèse, à ce stade du développement la version disponible est
toujours de nature fonctionnelle. C’est le moment de s’intéresser à la complexité
des opérations (cf. chapitre 4). En effet, d’une part la version disponible se prête
bien aux calculs de complexité et d’autre part les développements ultérieurs
n’ont en général pas d’incidence sur le coût asymptotique, la différence avec une
version optimisée ne porte que sur un facteur multiplicatif.

Peut-on encore améliorer l’efficacité des opérations obtenues à l’issue des


étapes précédentes ? Si la question sous-entend « en préservant la sémantique
des opérations », la réponse est oui, mais la marge de manœuvre est étroite. Il
faut se tourner vers des techniques de transformation comme la suppression de la
récursivité, l’introduction des notions de variable, de pointeur, de séquentialité, la
transformation de fonctions en procédures par l’ajout d’un paramètre de sortie,
etc. tout en s’interdisant de modifier les structures de données existantes.
De telles manipulations peuvent se révéler insuffisantes quand, par exemple,
les structures de données traitées contiennent des tableaux qui sont donc sys-
tématiquement recopiés, entraînant un coût rédhibitoire. La solution est alors
d’utiliser une démarche de type « modification in situ » qui autorise à modifier
des structures de données existantes (cf. [57] pour une étude et une comparaison
de différents paradigmes de programmation). Le prix à payer est double. Tout
d’abord, la sémantique des opérations change dans la mesure où la configuration
de la structure de données telle qu’elle se présentait au début de l’opération
est définitivement perdue. Par ailleurs, il n’existe pas de techniques systéma-
tiques pour passer du cadre « persistant » au cadre « éphémère » : chaque cas
est particulier. Un exemple particulièrement instructif est celui des mises à jour
dans les Avl (cf. section 6.6, page 191). La solution fonctionnelle peut exploiter
76 Structures de données et méthodes formelles

simultanément l’arbre initial et l’arbre en cours d’élaboration. La solution éphé-


mère exige de gérer dans la structure de données des informations portant sur
la différence de rayon entre l’arbre initial (qui est perdu) et l’arbre en cours de
construction.
Chapitre 3

Étude de quelques structures


outils

La recherche de mises en œuvre efficaces pour les structures de données


fondamentales conduit à raffiner celles-ci par des structures de données secon-
daires. Comme par un système d’échafaudages, la réalisation de ce qui relève du
niveau abstrait s’appuie sur des structures de données concrètes. S’il est impor-
tant de définir avec précision ce que l’on veut réaliser (la structure de données
abstraite), il est tout aussi important de maîtriser les outils (les structures de
données concrètes) avant de se lancer dans le travail de mise en œuvre. C’est
notre objectif pour ce chapitre. En nous intéressant aux listes, aux arbres et
enfin aux sacs, nous nous donnons les moyens, selon les cas, de spécifier ou d’im-
planter des structures de données. Alors que les tableaux font partie intégrante
de la théorie des ensembles, dont les principes élémentaires sont rappelés au
chapitre 1 (un tableau à une dimension est une fonction totale définie sur un
intervalle d’entiers), ce n’est a priori pas le cas des listes, des arbres ni des sacs
qui résultent de l’ajout d’une « couche » supplémentaire qui enveloppe la théorie
des ensembles.

Il est bien entendu vain de chercher à répertorier toutes les structures de


données outils envisageables. C’est pourquoi dans ce chapitre nous limitons notre
étude aux structures qui forment le substrat de la seconde partie de l’ouvrage.
Concernant les arbres, nous nous arrêtons sur les trois catégories rencontrées
en partie II : les arbres non ordonnés, les arbres planaires et les arbres n-aires
(en insistant plus particulièrement sur deux variantes de ces derniers : les arbres
binaires et les arbres externes).

La construction de ces différentes structures de données se fait en appliquant


la théorie du point fixe introduite à la section 1.9. Il en résulte un schéma d’or-
ganisation qui s’appuie sur la théorie des ensembles et qui peut se représenter
par le « camembert » suivant :
78 Structures de données et méthodes formelles

arbres arbres
binaires
listes et
planaires arbres-
feuilles
ensembles
arbres
non sacs
ordonnés

3.1 Listes finies


Les listes (ou listes linéaires) finies sont des structures qui peuvent servir soit
à spécifier, soit à implanter des structures fondamentales comme les ensembles,
les files simples, etc.

3.1.1 Présentation informelle


Informellement, une liste finie est une succession (éventuellement vide) de
valeurs de même nature. Chaque valeur peut être identifiée par sa position dans
la liste, la première position (celle repérée par le nombre 1) désigne la tête de
liste. La notation de base est la suivante pour une liste l1 constituée des valeurs
3 (en tête) et 7 :

l1 = [3 | [7 | [ ]]]

Cette notation est appelée notation récursive. Il existe cependant une notation
« linéaire » équivalente, plus agréable à l’œil 1 que nous définissons plus tard et
qui permet de décrire l1 sous la forme [3, 7].
Trois opérations sont définies et étudiées : la concaténation de deux listes
(notée par l’opérateur infixé ), l’inversion d’une liste (notée par l’opérateur
postfixé ˜) et la longueur d’une liste.
Si l2 = [7 | [12 | [4 | | [ ]]]], la liste ci-dessous est la concaténation de l1
et de l2 :

l1  l2 = [3 | [7 | [7 | [12 | [4 | [ ]]]]]]

Notons qu’il est possible de rencontrer plusieurs fois la même valeur dans une
liste, c’est le cas de 7 dans l1  l2 . La liste ci-dessous est l’inverse de la liste l2 :

l2 ˜ = [4 | [12 | [7 | [ ]]]]

L’expression #(l2 ) représente la longueur de la liste l2 , soit 3.


1. Mais paradoxalement d’une pratique plus délicate.
3. Étude de quelques structures outils 79

Listes et fichiers séquentiels


Les tambours et disques magnétiques sont apparus précocement sur
les ordinateurs – dès les années 1950 – en tant que supports externes d’in-
formations. En dépit des avantages de ce type de support (liés à l’accès
direct), de nombreux « gros systèmes » dotés exclusivement de bandes
magnétiques (qui, elles, se caractérisent par un accès séquentiel : pour
accéder à une valeur enregistrée sur la bande, il faut avoir accédé à toutes
les valeurs qui précèdent !) ont subsisté jusqu’à la fin des années 1960.
L’histoire se répète puisqu’à la fin des années 1970, les premiers micro-
ordinateurs ne disposaient eux non plus que de supports séquentiels (des
lecteurs-enregistreurs de cassettes en général) en tant que mémoires per-
sistantes.
Les systèmes de fichiers à bandes magnétiques les plus rudimentaires
disposaient de six opérations de base pour la manipulation des données :
trois opérations liées à la lecture – ouverture du fichier en lecture, détec-
tion de la marque de « fin de fichier » et lecture d’un enregistrement –
et enfin de trois opérations dédiées à l’écriture : ouverture du fichier en
écriture, écriture d’un enregistrement et fermeture du fichier (provoquant
l’écriture d’une marque de fin de fichier détectable à la lecture). Ces fonc-
tionnalités subsistent dans les systèmes de gestion de fichiers actuels,
même si en général les fichiers séquentiels sont implantés sur des sup-
ports à accès direct.
Les informaticiens de l’époque ont dû déployer des trésors d’ingénio-
sité pour réaliser, avec de tels supports, des applications aussi complexes
que des systèmes d’information, des utilitaires de toutes sortes ou en-
core des systèmes d’exploitation. Dans [75], vol. 2, D. Knuth décrit de
manière encyclopédique des algorithmes contraints par ce type de limi-
tations matérielles. Il porte un effort particulier sur les « tris par bandes
magnétiques », pierre angulaire des premiers systèmes d’information.
Les listes sont des structures proches des fichiers séquentiels. À la fin
de ce chapitre le lecteur trouvera plusieurs exercices qui empruntent le
« style séquentiel » caractéristique des algorithmes destinés aux systèmes
« à bandes magnétiques ».

3.1.2 Ébauche de construction


Nous allons, pour la première fois, appliquer la théorie du point fixe étudiée à
la section 1.9 pour construire des listes sur l’ensemble T . Rappelons que le point
clé de la démarche est la recherche d’une fonction « pivot » f dont le plus petit
point fixe est l’ensemble que l’on cherche à définir. La démarche de construction
se présente en six points (cf. [3] pour un développement et pour le détail des
différentes démonstrations) :
1. Définition formelle des notations (ici [ ] et [. . . | . . .]) utilisées pour faciliter
les manipulations.
80 Structures de données et méthodes formelles

2. Définition de la fonction qui permet d’agrandir une structure existante (ici


c’est la fonction qui ajoute un élément en tête de liste, elle est notée [|]).
3. Définition de la fonction « pivot » f dont on recherche le plus petit point
fixe fix(f ). Ce point fixe existe si (c’est une condition suffisante) f est
monotone. Il est donc important de s’assurer de cette propriété.
4. Attribution d’un nom au point fixe en question (ici ce sera liste(T )). Ce
nom dénote l’ensemble que l’on cherche à construire. Le fait qu’il s’agisse
d’un point fixe permet alors de caractériser cet ensemble.
5. Recherche d’une propriété caractéristique « inductive » de l’ensemble
construit. Dans la seconde partie de l’ouvrage, ce type de propriété n’est
pas systématiquement déduit mais est admis sans en développer la cons-
truction.
6. Instanciation du principe d’induction. Ce principe se décline différemment
selon la nature de la structure de données.
Avant d’appliquer cette démarche, il nous faut décider de la représenta-
tion ensembliste des listes sur T . Nous optons pour le choix suivant : une
liste sur T est une fonction partielle 2 de N1 dans T . Ainsi la liste l3 =
[3 | [7 | [7 | [12 | [4 | [ ]]]]]] se représente par :

{1 → 3, 2 → 7, 3 → 7, 4 → 12, 5 → 4}

Déroulons à présent la démarche de construction :


1. Les notations [ ] (liste vide) et [t | q] (liste dont le premier élément est t
et la queue q) se définissent par :

Notation Définition Condition


[] ∅
[t | q] {1 → t} ∪ q  1 t ∈ T ∧ q ∈ N1 →
 T
2. La fonction [|](T ) qui construit une liste à partir d’un élément t et d’une
liste q se définit par :
[|](T ) = λ(t, q) ·(t ∈ T ∧ q ∈ N1 →  T | [t | q])
3. La fonction « pivot » f se définit alors par :
f (T ) = λz ·(z ∈ P(N1 →  T ) | {[ ]} ∪ [|](T )[T × z])
[|](T )[T × z] est l’image par la fonction [|](T ) du produit cartésien T × z. Il
s’agit bien d’une fonction de P(N1 → T ) dans P(N1 → T ). De plus f (T )(z)
est monotone en z. Cette dernière propriété résulte de la monotonie de
l’opérateur ∪, de celle de [|](T )[T × z] en T × z et enfin de la monotonie
du produit cartésien par rapport à son second argument. f possède donc
un plus petit point fixe noté fix(f (T )).
4. Posons liste(T ) = fix(f (T )). Par définition nous avons alors :
liste(T ) = {[ ]} ∪ [|](T )[T × liste(T )]
5. De l’égalité précédente nous déduisons les deux propriétés suivantes :
1) [ ] ∈ liste(T )
2) ∀(t, q) ·(t ∈ T ∧ q ∈ liste(T ) ⇒ [t | q] ∈ liste(T ))

2. Fonction dont le domaine est un intervalle 1 .. n.


3. Étude de quelques structures outils 81

Remarque. Pour ce qui concerne la seconde propriété, dans la suite nous


sacrifions à la tradition en n’explicitant pas la quantification :
2) t ∈ T ∧ q ∈ liste(T ) ⇒ [t | q] ∈ liste(T )

6. Il est alors possible de décliner le principe d’induction sous une forme plus
traditionnelle que celle présentée à la section 1.9 (la preuve est laissée en
exercice) :
[l := [ ]]P ∧
∀l ·(l ∈ liste(T ) ∧ P ⇒ ∀t ·(t ∈ T ⇒ [l := [t | l]]P ))

∀l ·(l ∈ liste(T ) ⇒ P )

Tableau 3.1 – Propriétés des listes.


La propriété 3.1.1 établit que la liste vide [ ] est élément neutre à droite
pour la concaténation. Depuis la définition de la concaténation nous
savons déjà que [ ] est élément neutre à gauche. La proposition 3.1.2
fournit une condition nécessaire et suffisante pour démontrer que la
concaténation de deux listes est la liste vide. La proposition 3.1.3 porte
sur l’associativité de la concaténation. La proposition 3.1.4 établit la
transposition de la concaténation par inversion. La proposition 3.1.8
montre que l’inversion est involutive tandis que la proposition 3.1.9
montre que toute liste ne comportant qu’un seul élément est sa propre
inverse. Enfin les deux propositions 3.1.11 et 3.1.12 portent sur la lon-
gueur des listes. Ci-dessous l, a, b et c sont des listes sur T .

Propriété Condition Ident.


l[ ] = l (3.1.1)
a = [ ]∧b = [ ] ⇔ ab = [ ] (3.1.2)
((a  b)  c) = (a  (b  c)) (3.1.3)
a  b = (b˜a˜)˜ (3.1.4)
ac = bc ⇔ a = b (3.1.5)
ca = cb ⇔ a = b (3.1.6)
(a  b)˜ = b˜a˜ (3.1.7)
(a˜)˜ = a (3.1.8)
[t | [ ]]˜ = [t | [ ]] t∈T (3.1.9)
a˜ = b˜ ⇔ a = b (3.1.10)
#(a  b) = #(a) + #(b) (3.1.11)
#(a˜) = #(a) (3.1.12)
[ ] = [t | l] t∈T (3.1.13)
82 Structures de données et méthodes formelles

3.1.3 Opérations sur les listes


Les opérations introduites à la section 3.1.1 peuvent maintenant être définies
rigoureusement comme suit :

Notation Définition Condition


[ ]l l l ∈ liste(T )
[t | q]  l [t | q  l] t ∈ T ∧ q ∈ liste(T ) ∧ l ∈ liste(T )
[ ]˜ []
[t | q]˜ q ˜[t | [ ]] t ∈ T ∧ q ∈ liste(T )
#([ ]) 0
#([t | q]) 1 + #(q) t ∈ T ∧ q ∈ liste(T )

Le tableau 3.1, page précédente, regroupe un certain nombre de propriétés


des listes en relation avec la liste vide, l’inversion, la concaténation et la longueur.
La démonstration de ces propriétés fait l’objet de l’exercice 3.1.4.

3.1.4 Notation linéaire


La notation linéaire présentée ci-dessus se définit formellement de la manière
suivante :

Notation Définition Condition


[t] [t | [ ]] t∈T
[t, l] [t | [l]] t ∈ T ∧ [l] ∈ liste(T )

Exercices

Exercice 3.1.1 Ci-dessus nous avons fourni une construction inductive des listes. Proposer à
présent une construction directe.

Exercice 3.1.2 Convertir en notation linéaire les listes suivantes sur N :


1. [12 | [17 | [15 | [ ]]]]
2. [45 | [1 | [57 | [0 | [ ]]]]]

Exercice 3.1.3 Convertir en notation récursive les listes suivantes sur N :


1. [6]
2. [4, 6, 23]

Exercice 3.1.4 Démontrer les propriétés des listes répertoriées dans le tableau 3.1, page
précédente.

Exercice 3.1.5 Cet exercice propose de définir et d’étudier des listes particulières sur N.
1. Construire les listes listeSD(T ) sans doublon sur T .
2. Construire les listes listeT triées sur N.
3. Construire les listes listeT SD triées et sans doublon sur N.
4. Soit l1 et l2 deux listes triées sans doublon, d’intersection vide. Définir la fonction union
qui délivre la liste triée sans doublon r représentant l’union des deux listes.
3. Étude de quelques structures outils 83

5. Soit l1 et l2 deux listes triées sans doublon. Définir la fonction inter qui délivre la liste
triée sans doublon r représentant l’intersection des deux listes.

Exercice 3.1.6 Considérons les listes définies sur N × N, triées sur le premier composant du
produit cartésien N × N. l = [4 → 25, [4 → 30, [5 → 6, [7 → 23, [ ]]]]] est un exemple d’une telle
liste.
1. Construire l’ensemble correspondant listeT .
2. Définir la fonction cumul qui délivre une liste r sans doublon telle qu’à chaque occur-
rence du premier composant est associée la somme des valeurs du second composant.
Ainsi, pour la liste l, nous aurons r = [4 → 55, [5 → 6, [7 → 23, [ ]]]].

Exercice 3.1.7 Définir l’opérateur conc de concaténation généralisée tel que si l =


[[7, 9], [1, 4, 6], [9, 5, 1]], conc(l) est la liste obtenue en concaténant toutes les listes qui consti-
tuent l : conc(l) = [7, 9, 1, 4, 6, 9, 5, 1].

3.2 Arbres finis


Les arbres constituent des outils incontournables dans la recherche de repré-
sentations concrètes efficaces pour des structures de données abstraites, telles que
la représentation des ensembles, des files de priorité ou des tableaux flexibles.
La nomenclature que nous proposons ici est orientée vers les objectifs visés :
nous n’étudions que les arbres utilisés comme structures de données secondaires
dans la partie II. Le premier critère que nous considérons concerne l’organisation
des sous-arbres d’un nœud donné. Ceci nous conduit à distinguer, en allant des
arbres les moins contraints vers les plus contraints :
– les arbres non ordonnés pour lesquels un nœud possède un ensemble de
sous-arbres ;
– les arbres planaires (ou ordonnés) pour lesquels les sous-arbres de chaque
nœud sont organisés en listes ;
– les arbres n-aires (pour n donné, n > 0) pour lesquels les sous-arbres sont
organisés en tableaux de longueur n.
Les arbres non ordonnés sont dans la suite utilisés pour la représentation d’en-
sembles de chaînes. Les arbres planaires permettent de représenter des files de
priorité. Enfin les arbres n-aires, et en particulier les arbres binaires (n = 2),
apparaissent dans la représentation concrète des ensembles, des files de priorité
et des tableaux flexibles.
Ce premier critère ne s’intéresse qu’à la structuration des sous-arbres. Cepen-
dant, l’informaticien doit en général disposer d’informations associées à tout ou
partie des nœuds d’un arbre. Le terme d’étiquette (label) est en général utilisé
pour désigner ces informations. Si la valeur de l’étiquette identifie le nœud où
elle apparaît, elle peut être appelée clé ou identifiant. Le second critère distingue
les arbres pour lesquels tous les nœuds sont étiquetés de ceux pour lesquels seuls
certains nœuds le sont. Les arbres totalement étiquetés sont les plus fréquents
mais les arbres partiellement étiquetés sont parfois utilisés avec quelques avan-
tages dans la représentation des ensembles et des tableaux flexibles.
Quel que soit le critère retenu, nous ne nous intéressons dans la suite qu’aux
arbres finis.
84 Structures de données et méthodes formelles

En théorie des graphes, un arbre est défini comme un graphe acyclique sim-
plement connexe 3 . Ainsi, le graphe suivant :




• •
• • •

est un arbre. Notons que cette définition ne distingue aucun sommet en particu-
lier et que les segments joignant deux sommets ne sont pas orientés, ce sont des
arêtes.
Cette notion (aussi appelée arbre libre ou arbre non enraciné) doit être enri-
chie pour se révéler utile à l’informaticien. En distinguant un nœud particulier,
appelé racine de l’arbre, nous admettons du même coup une orientation des
arêtes dans le sens racine/autre nœud, arêtes qui, dans la terminologie de la
théorie des graphes, deviennent des arcs. Ainsi, si dans le schéma ci-dessus nous
distinguons le sommet désigné par la flèche, nous obtenons l’arbre suivant


• • •
(1)
• • • • •

Ce type d’arbre est appelé arborescence dans la théorie des graphes et arbre
enraciné (ou simplement arbre) par l’informaticien. Bien qu’il s’agisse toujours
d’un objet de la théorie des graphes (un graphe orienté acyclique connexe par-
ticulier) il est plus intéressant pour l’informaticien de le construire (souvent par
un procédé inductif) à partir de la théorie des ensembles. Les avantages que l’on
en retire tiennent à ce que les définitions inductives se prêtent mieux aux dé-
monstrations, ainsi qu’aux développements algorithmiques. En outre, en toute
rigueur, le passage par la théorie des graphes exige d’expliciter l’ensemble qui
sert de support aux sommets ; les constructions inductives permettent de s’en
passer.
Sans rechercher une formalisation excessive, nous pouvons à présent préciser
quelques définitions utiles autour des arbres enracinés. Nous le savons déjà, le
sommet distingué est la racine. Le terme nœud est un synonyme courant pour
sommet. Lorsque l’orientation n’est pas pertinente, un arc est appelé branche.
Si le nœud b est relié au nœud a par un arc, b est un fils de a. Un nœud qui
ne possède aucun fils est une feuille. Le nombre de fils que possède un nœud est
son arité. Si b est un fils de a, l’arbre enraciné qui a comme racine b est un sous-
arbre de a. Si le nœud c appartient à un sous-arbre de a, c’est un descendant

3. Il existe plusieurs propriétés caractéristiques alternatives. Nous supposons le lecteur fa-


milier avec les rudiments de la théorie des graphes.
3. Étude de quelques structures outils 85

de a. Le nombre de nœuds d’un arbre est son poids 4 . Le nombre de branches


qui séparent un nœud b de la racine est la hauteur de b. La hauteur d’un arbre
est la plus grande hauteur obtenue en considérant toutes les feuilles. Une forêt
est un ensemble (structuré – ou non – en liste ou en tableau) d’arbres. Dans les
schémas ci-dessous le sens des arcs est omis. Par convention il va de haut en bas.
Nous allons à présent reprendre la nomenclature introduite ci-dessus afin
d’étudier chacun des types d’arbre.

3.3 Arbres non ordonnés


3.3.1 Introduction : les arbres non ordonnés non étiquetés
Informellement, un arbre non ordonné est un arbre tel que, pour chaque
nœud a, les sous-arbres dépendant directement de a sont organisés en ensemble 5 .
L’ordre dans lequel ils apparaissent n’est donc pas pertinent. Ainsi, les deux
schémas ci-dessous
• •
• • • • • •
(2)
• • • • • •
• •

représentent le même arbre non ordonné. Il ne sera donc pas possible de désigner
un sous-arbre par une expression telle que « le deuxième sous-arbre du nœud a »
dans la mesure où la notion d’ordre sur les sous-arbres n’est pas significative.
Nous admettons que l’arbre suivant :

• • •
• • • •

n’est pas un arbre non ordonné. Les sous-arbres de la racine ne constituent pas
un ensemble puisque le sous-arbre doté d’un seul fils apparaît deux fois 6 .

3.3.2 Arbres non ordonnés étiquetés


L’intérêt des arbres non ordonnés s’accroît si l’on attribue une étiquette à
chaque nœud. C’est le type d’arbre que nous proposons d’étudier à présent.
4. Le terme taille est parfois utilisé. Nous préférons le terme poids, celui de taille présente
un risque de confusion avec celui de hauteur.
5. Une variante, proposée à l’exercice 3.3.1 consiste à organiser les sous-arbres en sac.

• •
6. Une autre convention consisterait à admettre que cet arbre est identique à • • • .

86 Structures de données et méthodes formelles

Présentation informelle
La figure 3.1 ci-dessous montre, pour des arbres étiquetés sur N, trois exemples
dont un (l’arbre (3)) n’est pas un arbre non ordonné. Dans la suite, les arbres
non ordonnés servent de base à la notion d’arbre trie, notion qui est utilisée pour
représenter des ensembles de chaînes.

7 8 8
(1) (2) (3)
4 1 14 9 7 12 9 7 9
12 8 9 10 3 11 5 10 3 11 10
2 4 4

Figure 3.1 – Arbres non ordonnés étiquetés.


L’arbre (1) est une version étiquetée de l’arbre (2) ci-dessus. L’arbre (2)
est un arbre non ordonné étiqueté tandis que, selon notre convention,
l’arbre (3) n’est pas un arbre non ordonné étiqueté puisque deux des
sous-arbres de la racine sont identiques.

Ébauche de construction
La construction d’un arbre non ordonné se fait en enracinant une forêt non
ordonnée f à une racine v. Le résultat se note v, f  et se définit formellement
par :

Notation Définition Condition


v, f  v → f v ∈ T ∧ f ∈ f noe(T )

où f noe(T ) représente l’ensemble des forêts non ordonnées sur T . Nous pouvons
remarquer que cette définition interdit les arbres non ordonnés vides.

La nature des arbres non ordonnés exige de définir simultanément (de manière
croisée) les forêts non ordonnées (f noe(T )) et les arbres non ordonnés (anoe(T )) :

Notation Définition
anoe(T ) {v → f | v ∈ T ∧ f noe(T )}
f noe(T ) P({x | x ∈ anoe(T )})

Lorsque nous serons amené à utiliser les arbres non ordonnés, il sera né-
cessaire de réaliser un choix d’implantation pour les forêts puisque celles-ci se
définissent comme des ensembles (cf. section 7.1).
3. Étude de quelques structures outils 87

Exercice

Exercice 3.3.1 Nous avons défini formellement les arbres non ordonnés en considérant que les
sous-arbres d’un nœud constituent un ensemble d’arbres. Proposer une structure d’arbres dans
laquelle la collection de sous-arbres constitue un sac (cf. section 3.7 pour une introduction aux
sacs).

3.4 Arbres planaires


3.4.1 Introduction : les arbres planaires non étiquetés
Informellement, un arbre planaire est un arbre dans lequel les sous-arbres de
chaque nœud sont organisés en liste. Une telle liste est une forêt. Ainsi, l’arbre (3)
suivant :
• •
• • • • • •
(3) (4)
• • • • • • • • • •
• •

est un arbre planaire. Cette fois l’ordre d’apparition est significatif, il est possible
de désigner un sous-arbre d’un nœud a par l’expression « le 2e sous-arbre du
nœud a ». Et, si l’arbre (4) ci-dessus est également un arbre planaire, il est
différent du premier.

3.4.2 Arbres planaires étiquetés


De même que dans le cas des arbres non ordonnés, il est possible d’apposer
une valeur à chaque nœud de l’arbre.

Présentation informelle
La figure 3.2, page suivante, montre trois exemples d’arbres planaires étique-
tés sur N. Dans la suite, les arbres planaires servent de base à la notion d’arbre
binomial, notion qui est utilisée pour représenter les files de priorité.

Ébauche de construction
La construction des arbres planaires est assez similaire à celle des arbres
non ordonnés. Elle se fait en enracinant une forêt planaire f à une racine v. Le
résultat se note v, f  et se définit formellement par :

Notation Définition Condition


v, f  v → f v ∈ T ∧ f ∈ f ple(T )
88 Structures de données et méthodes formelles

où f ple(T ) représente l’ensemble des forêts planaires sur T . De même que pour
les arbres non ordonnés, nous pouvons remarquer que cette définition interdit
les arbres planaires vides.
La nature des arbres planaires exige de définir simultanément (de manière
croisée) les forêts d’arbres planaires (f ple(T )) et les arbres planaires (aple(T )) :

Notation Définition
aple(T ) {v → f | v ∈ T ∧ f ple(T )}
f ple(T ) liste(aple(T ))
Lorsque nous serons amené à utiliser les arbres planaires (cf. section 9.4 par
exemple), nous pourrons utiliser les représentations et les propriétés des listes
pour implanter les forêts planaires.

7 7 7
(1) (2) (3)
4 3 5 4 4 3 4 1 14
9 8 1 2 12 9 9 8 2 10 12 8 9
9 4 2

Figure 3.2 – Arbres planaires étiquetés.


Les trois arbres sont des arbres planaires. Malgré l’existence dans
4
l’arbre (2) de deux sous-arbres identiques : , il s’agit bien d’un
9
arbre planaire : la position relative de ces deux sous-arbres permet de
les distinguer.

3.5 Arbres n-aires


3.5.1 Introduction : les arbres n-aires non étiquetés
Informellement, un arbre n-aire est un arbre tel qu’à tout nœud est associé un
tableau de n sous-arbres (dont certains peuvent être vides). Ainsi, par exemple,
pour n = 2, l’arbre (5) ci-dessous
• •
• • (5) • • (6)
• • • • • •

est un arbre 2-aire (binaire). Il ne doit pas être confondu avec l’arbre (6) ci-
dessus (qui est également binaire) puisque dans l’arbre (6) le nœud désigné par
la flèche est tel que sa place gauche est un arbre vide. Dans un arbre binaire,
un nœud qui ne possède qu’un seul fils est appelé point simple. La suite de cette
section est consacrée à l’étude des arbres binaires. Les résultats se généralisent
facilement aux arbres n-aires quelconques.
3. Étude de quelques structures outils 89

3.5.2 Les arbres binaires étiquetés


Présentation informelle – Typologie
Dans le reste de l’ouvrage nous avons besoin de nommer quelques arbres
binaires particuliers, les quatre principaux étant les arbres filiformes, les arbres
complets, les arbres pleins et les arbres parfaits. La figure 3.3 ci-dessous illustre
chacun de ces types d’arbres. Un arbre filiforme est un arbre dont tous les nœuds
sont des points simples (à l’exception de l’unique feuille qui n’a pas de fils. . . ). Un
arbre complet est un arbre qui ne possède pas de points simples. Un arbre plein
est un arbre complet dont toutes les feuilles sont situées à la même hauteur. Un
arbre parfait est tel que : (i) toutes les feuilles sont situées sur au plus les deux
derniers (plus bas) niveaux, (ii) quand l’arbre parfait n’est pas un arbre plein
(c’est-à-dire quand les feuilles sont effectivement réparties sur deux niveaux), les
feuilles du dernier niveau sont groupées sur la gauche, (iii) il existe au plus un
point simple, il est situé sur l’avant-dernier niveau.

• • • •
(1) (2) (3) (4)
• • • • • • •
• • • • • • • • • • •
• • • • • • •• • • • • • •

Figure 3.3 – Quelques types particuliers d’arbres binaires.


L’arbre (1) est un arbre filiforme. L’arbre (2) est un arbre complet.
L’arbre (3) est un arbre plein. L’arbre (4) est un arbre parfait à gauche :
ses feuilles sont situées sur deux niveaux, il y a un point simple sur
l’avant-dernier niveau et les feuilles du plus bas niveau sont situées sur
la partie gauche. Notons que l’arbre (3) est également un arbre parfait
(à gauche comme à droite), c’est aussi un arbre complet.

Ainsi que nous l’avons déjà écrit, en général l’informaticien associe à tout ou
partie des nœuds une étiquette qui complète l’information purement structurelle
véhiculée par un arbre non étiqueté. Le cas des arbres partiellement étiquetés
est étudié séparément. Deux exemples d’arbres étiquetés sur N sont donnés par
le schéma ci-dessous :
6 1
4 8 (7) 2 3 (8)

1 3 9 5 2 9

Dans un tel arbre, un nœud qui possède un fils gauche (resp. droit) mais pas de
fils droit (resp. gauche) est appelé point simple à gauche (resp. à droite). Dans
l’arbre (7) ci-dessus, le nœud étiqueté 8 est un point simple à droite.
Citons deux classes importantes d’arbres binaires étiquetés : les arbres bi-
naires de recherche (ou abr) et les minimiers (ou tournois). L’arbre (7) est un
90 Structures de données et méthodes formelles

abr : pour tout nœud étiqueté n, le sous-arbre gauche (resp. droit) ne contient
que des valeurs inférieures (resp. supérieures) à n. L’arbre (8) est un minimier :
pour tout nœud n de l’arbre, n est un représentant de la plus petite valeur
présente dans l’arbre. Les structures étudiées ci-dessous sont, pour la plupart,
fondées sur des variantes ou des extensions de l’un de ces deux types d’arbre
(Avl, B-arbre, arbre binomial, etc.).

Ébauche de construction
Nous adoptons le mode de construction inductif déjà pratiqué pour la cons-
truction des listes. Avant d’appliquer la démarche proposée à la section 3.1, nous
devons décider de la représentation ensembliste des arbres binaires étiquetés sur
T . Un arbre binaire étiqueté sur T se représente par une fonction définie sur des
listes (de g ou de d pour gauche ou droite) et à valeurs dans T . La liste représente
les directions, gauche ou droite, que l’on doit emprunter depuis la racine avant
de rencontrer l’étiquette considérée. Ainsi l’arbre (7) ci-dessus est représenté par
la fonction suivante :

{ [ ] → 6,
[g] → 4,
[d] → 8,
[g, g] → 1,
[g, d] → 3,
[d, d] → 9
}

Développons à présent les six étapes de la démarche de construction pour les


arbres binaires étiquetés :
1. Les notations  (arbre vide) et l, v, r (arbre dont la racine est v, le sous-
arbre gauche l et le sous-arbre droit g) se définissent par :

Not. Définition Cond.


 ∅
⎛ ⎞
(λf ·(f ∈ dom(l) | [g | f ]))−1 ; l
⎜ ⎟
l, v, r ([ ] → v) ∪ ⎝ ∪ ⎠ (3.5.1)
−1
(λf ·(f ∈ dom(r) | [d | f ])) ; r

La définition de l, v, r est soumise à la condition 3.5.1 suivante : l ∈


liste({g, d}) →
 T ∧ r ∈ liste({g, d}) →
 T ∧v ∈T
2. La fonction , ,  qui construit un nouvel arbre à partir de v, élément de T
et de deux arbres l et r se définit par :
⎛  ⎞
l ∈ liste({g, d}) →
 T∧ 

, , (T ) = λ(l, v, r) · ⎝ v ∈ T ∧  l, v, r⎠

r ∈ liste({g, d}) → T 
3. Étude de quelques structures outils 91

3. La fonction « pivot » f se définit alors par :


  
l ∈ P(liste({g, d}) →
 T)∧ 
f (T ) = λ(l, r) ·  {} ∪ , , (T )[l × T × r]
r ∈ P(liste({g, d}) → T) 

, , (T )[l × T × r] est l’image par la fonction , , (T ) du produit carté-


sien l × T × r. Il s’agit bien d’une fonction de P(liste({g, d}) →  T ) dans
P(liste({g, d}) →  T ). De plus f (T )(l, r) est monotone en l et r. Cette
dernière propriété résulte de la monotonie de l’opérateur ∪, de celle de
, , (T )[l × T × r] en l × T × r et enfin de la monotonie du produit carté-
sien. f possède donc un plus petit point fixe noté fix(f (T )).
4. Posons abe(T ) = fix(f (T )). Par définition nous avons alors :
abe(T ) = {} ∪ , , (T )[abe(T ) × T × abe(T )]
5. De l’égalité précédente nous déduisons les deux propriétés suivantes :
1)  ∈ abe(T )
2) l ∈ abe(T ) ∧ v ∈ T ∧ r ∈ abe(T ) ⇒ l, v, r ∈ abe(T ))
qui caractérisent les arbres binaires étiquetés.
6. Il est alors possible de décliner le principe d’induction sous une forme plus
traditionnelle que celle présentée à la section 1.9 (la preuve est laissée en
exercice) :
[a := ]P⎛⎛∧ ⎞ ⎞
l ∈ abe(T ) ∧
⎜⎜ r ∈ abe(T ) ∧ ⎟ ⎟
∀(l, r) · ⎜⎜
⎝⎝ [a := l]P ∧
⎟ ⇒ ∀v ·(v ∈ T ⇒ [a := l, v, r]P )⎟
⎠ ⎠
[a := r]P

∀a ·(a ∈ abe(T ) ⇒ P )

Opérations sur les arbres binaires étiquetés


Le tableau 3.2, page suivante, fournit une définition formelle des principales
opérations sur les arbres binaires étiquetés. Intuitivement, moins un arbre est
haut, plus les opérations de consultation et de mise à jour sont efficaces. Cette
remarque sous-tend la recherche d’opérations tendant à diminuer la hauteur
d’un arbre tout en préservant ses « bonnes » propriétés. Les rotations constituent
l’archétype de telles opérations. rd, rg, rgd, rdg sont les principales opérations de
rotation rencontrées. Les rotations étant (en général mais pas toujours) destinées
à diminuer la hauteur, il nous faut définir formellement ce qu’est la hauteur d’un
arbre, c’est le but de la fonction h. L’équilibre en hauteur est quant à lui défini à
travers le prédicat hEq. Préserver les bonnes propriétés d’un arbre par rotation
consiste souvent à s’assurer que les arbres avant et après la rotation représentent
la même liste d’éléments. L’obtention de ce type de liste est le rôle dévolu aux
opérations pref ixe, inf ixe, postf ixe.
Ainsi que nous l’avons déjà dit ci-dessus, la hauteur d’un arbre est le nombre
de branches qui séparent la racine de la feuille la plus éloignée de la racine. La
hauteur n’est donc pas définie pour un arbre vide. Cette définition est conforme
92 Structures de données et méthodes formelles

à l’usage, ainsi qu’à la norme établie par le nist (cf. [2]). Cependant elle pré-
sente l’inconvénient de se traduire par une fonction partielle (puisqu’elle n’est
pas définie pour les arbres vides). Nous pouvons contourner cette limitation en
définissant la notion de rayon 7 qui est utilisée quand nécessaire et en particulier
justement dans la définition de la hauteur puisque, quand la hauteur est définie,
elle est égale au rayon moins 1.

Tableau 3.2 – Définitions portant sur les arbres binaires étiquetés.

Notation Définition
rd(gg , vg , dg , v, d) gd , vg , dg , v, d
rg(g, v, gd , vd , dd ) g, v, gd , vd , dd 
rgd(gg , vg , ggd , vgd , dgd , v, d) gg , vg , ggd , vgd , dgd , v, d
rdg(g, v, gdg , vdg , ddg , vd , dd ) g, v, gdg , vdg , ddg , vd , dd 
r() 0
r(g, v, d) max({r(g), r(d)}) + 1
pref ixe() []
pref ixe(g, v, d) [v]  pref ixe(g)  pref ixe(d)
inf ixe() []
inf ixe(g, v, d) inf ixe(g)  [v]  inf ixe(d)
postf ixe() []
postf ixe(g, v, d) postf ixe(g)  postf ixe(d)  [v]
w() 0
w(g, v, d) w(g) + 1 + w(d)
h(a) r(a) − 1 Si a n’est pas vide
hEq() 
hEq(g) ∧ hEq(d) ∧
hEq(g, v, d)
r(g) − r(d) ∈ −1 .. 1

La définition de la hauteur h est valide à condition que a ∈ abe(T ) − {} (la


hauteur n’est pas définie pour un arbre vide). Concernant les conditions pour
les autres opérations, tous les g . . . et les d . . . sont des abe(T ), et tous les v . . .
appartiennent à T .
Un équilibre parfait en hauteur n’est pas toujours possible (cf. exercice 3.5.3).
C’est pourquoi nous considérons qu’un arbre binaire est (simplement) équilibré
(nous disons h-équilibré) si, pour tout nœud de l’arbre, le rayon du sous-arbre
gauche diffère de celui du sous-arbre droit d’au plus 1 en valeur absolue.

7. Notion définie ici par analogie avec la notion de diamètre dans les graphes.
3. Étude de quelques structures outils 93

Exemples. Dans les exemples qui suivent la valeur de déséquilibre de chaque


nœud est apposée au nœud. 0 signifie que le nœud est parfaitement équilibré en
hauteur et, pour i > 0, i (resp. −i) signifie qu’il existe un déséquilibre de i en
faveur du sous-arbre gauche (resp. droit).

• +1 • -1

+1 • • -1 -1 • • -1

-1 • • +1 0• •0 0• • -1 -1 • • -1

0 • +1 • •0 0• •0 •0 0• 0• • -1

0• •0
Un arbre h-équilibré Un arbre h-équilibré

•0

+1 • • -2

-1 • •0 0• • +1

0 • +1 • •0 •0 0• •0

0• 0• •0
Un arbre qui n’est pas h-équilibré

Relation entre poids et rayon dans un arbre binaire. Pour un poids n


donné, il peut être intéressant de savoir entre quelles limites varie le rayon r d’un
arbre binaire. En effet, beaucoup d’opérations sur les arbres binaires consistent
à parcourir l’arbre à partir de la racine et à descendre jusqu’à une feuille ou
jusqu’à un nœud intermédiaire, en s’orientant à gauche ou à droite à chaque
étape. Le nombre de nœuds franchis est majoré par le rayon de l’arbre qui a été
parcouru.
Pour un rayon r donné, les arbres ayant le plus petit nombre de nœuds
sont des arbres filiformes. En effet, si nous supprimons un nœud d’un arbre
filiforme, soit son rayon diminue, soit la structure résultante n’est plus un arbre.
Un arbre filiforme possède r = n nœuds, un arbre quelconque est donc tel que
r ≤ n. Pour un rayon r donné, les arbres binaires ayant le plus grand nombre de
nœuds sont les arbres pleins. En effet, un tel arbre ne peut accueillir un nouveau
nœud sans voir son rayon augmenter de 1. Or, dans un arbre plein, nous avons
(cf. exercice 3.5.2) n = 2r − 1. Pour un arbre quelconque, nous avons donc
n ≤ 2r − 1. D’où le calcul suivant :
n ≤ 2r − 1
⇔ Arithmétique
n + 1 ≤ 2r
⇔ Propriété des logarithmes (base 2)
log(n + 1) ≤ r
94 Structures de données et méthodes formelles

⇒ Propriété D.35
log(n + 1) ≤ r
Au total nous avons donc :
log(n + 1) ≤ r ≤ n (3.5.2)
Cette formule nous apprend que la descente dans un arbre binaire de poids
n et de rayon r n’exige jamais moins de log(n + 1) et jamais plus de n com-
paraisons. La limite supérieure (n = r) est, si possible, à éviter compte tenu du
coût qui en résulte. Nous reviendrons sur cette conclusion.

Parcours d’arbres binaires. Il est parfois souhaitable d’extraire toutes les


valeurs associées aux nœuds dans un arbre étiqueté. Différentes stratégies de
parcours existent : parcours en profondeur (on extrait toutes les valeurs d’un
sous-arbre avant de parcourir en profondeur le sous-arbre frère), en largeur : on
effectue l’extraction par niveaux successifs en partant du niveau de la racine.
Concernant la stratégie de parcours en profondeur, il est possible de distinguer
le parcours gauche-droite du parcours droite-gauche. Pour le parcours gauche-
droite, trois cas sont envisageables : (i) le parcours préfixé pour lequel on extrait
la racine avant d’effectuer un parcours préfixé des deux sous-arbres, (ii) le par-
cours infixé pour lequel on effectue un parcours infixé du sous-arbre gauche
avant d’extraire la racine puis d’effectuer un parcours infixé du sous-arbre droit,
(iii) le parcours postfixé pour lequel on effectue un parcours postfixé des deux
sous-arbres avant d’extraire la racine.

Rotations. Cette notion de parcours infixé permet de définir une forme d’équi-
valence entre arbres binaires : les arbres a et b sont équivalents si inf ixe(a) =
inf ixe(b). Les opérations de rotation sont des opérations qui préservent cette
forme d’équivalence tout en permettant une restructuration de l’arbre. Dans la
pratique, trois types d’usage sont exploités :
1. monter une valeur à la racine,
2. descendre une valeur jusqu’aux feuilles,
3. rééquilibrer un arbre déséquilibré (typiquement un arbre dans lequel le
rayon de l’un des deux sous-arbres excède de 2 le rayon de l’autre).
La première fonctionnalité (monter une valeur à la racine) trouve son utilité
lorsque l’on effectue une insertion à la racine en commençant par une insertion
aux feuilles (cf. page 161 et exercice 6.3.1, page 168). Le second usage (descendre
une valeur aux feuilles) est exploité lors de la suppression dans un Avl. La
troisième fonctionnalité est mise à profit dans des structures de données telles
que les Avl, les arbres déployés, etc. Un effet secondaire des rotations peut
survenir dans certaines situations : il s’agit de la diminution du rayon de l’arbre.
À la section 6.6, page 191, nous verrons que dans le cas des Avl, deux
types de rotation sont nécessaires pour couvrir toutes les situations exigeant un
rééquilibrage : les rotations simples et les rotations doubles. Pour les rotations
simples, on distingue la rotation droite et la rotation gauche. La rotation droite
rd se présente comme le montre le schéma suivant 8 :
8. La lecture de droite à gauche du schéma correspond à la rotation gauche.
3. Étude de quelques structures outils 95

v vg
rd
vg d gg v
rg
gg dg dg d

Quant aux rotations doubles, on distingue la rotation gauche-droite et la rotation


droite-gauche. La rotation gauche-droite rgd se présente comme le montre le
schéma suivant :
v v gd

vg d vg v
rgd
gg v gd gg g dg ddg d

g dg ddg

Ainsi que le montre de manière informelle le schéma ci-dessous, la rotation


gauche-droite est la composition de deux rotations simples (une rotation gauche
suivie d’une rotation droite) ; d’où son nom.

v v rd v gd

vg d v gd d vg v

gg v gd rg vg ddg gg g dg ddg d

g dg ddg gg g dg

Il est bien sûr possible de définir directement la rotation rgd, c’est ce qui est
réalisé dans le tableau des définitions 3.2, page 92. La définition indirecte – à
partir de rg et rd – présente l’avantage de simplifier les démonstrations.

Exercices

Exercice 3.5.1 Construire les quatre types d’arbres binaires présentés à la figure 3.3 : les
arbres filiformes, complets, pleins et parfaits à gauche.

Exercice 3.5.2 Démontrer que, pour tout arbre binaire étiqueté a, la rotation droite de a
préserve le parcours préfixé, soit pref ixe(a) = pref ixe(rd(a)). Refaire la démonstration pour
les autres rotations et les autres parcours.

Exercice 3.5.3 Fournir une condition nécessaire de l’équilibre parfait en hauteur d’un arbre
binaire.
96 Structures de données et méthodes formelles

3.6 Arbres binaires partiellement étiquetés : les


arbres externes
3.6.1 Présentation informelle
Un arbre externe – aussi appelé arbre-feuille (leaf tree) – est un arbre par-
tiellement étiqueté dans lequel les « étiquettes » apparaissent uniquement sur
les feuilles. En outre, un arbre externe ne possède pas de point simple : tous les
nœuds sont dotés soit de zéro soit de deux fils (les arbres externes sont donc des
arbres complets).

Exemples. Ci-dessous sont présentés deux exemples d’arbres externes sur N :


• •
(9) (10)
7 • • •
• 4 • • • 19
12 8 13 3 14 7 5 21

Intéressons-nous à présent aux limites entre lesquelles varie le rayon r d’un


arbre externe binaire pour un nombre de feuilles f donné. Pour un rayon r
donné, les arbres externes qui contiennent le minimum de feuilles (cf. l’arbre (9)
ci-dessus) sont des arbres externes tels que chaque nœud interne possède au
moins une feuille comme fils. Pour de tels arbres, r = f . En effet, tout arbre
externe doté de f − 1 feuilles a un rayon inférieur à r. Pour les arbres externes
en général, nous avons donc r ≤ f . Pour un rayon r donné, les arbres externes
qui contiennent le plus de feuilles sont des arbres pleins. En effet, ajouter une
feuille ne peut se faire qu’en augmentant le rayon. Pour de tels arbres, si r > 0,
f = 2r−1 . Pour les arbres externes en général, f ≤ 2r−1 . D’où :

f ≤ 2r−1
⇔ Propriété des logarithmes (base 2)
log f ≤ r − 1
⇒ Propriété D.35
log f  ≤ r − 1
⇔ Arithmétique
log f  + 1 ≤ r

En rassemblant les deux inégalités nous avons :

log f  + 1 ≤ r ≤ f (3.6.1)

En comparant les formules 3.5.2 et 3.6.1, nous pouvons constater (ce n’est pas
une surprise) que le nombre maximum de valeurs utiles f qu’il est possible
d’enregistrer dans un arbre externe tend, quand r tend vers +∞, vers la moitié
du nombre maximum de valeurs n présentes dans un arbre binaire totalement
étiqueté (puisque dans ce cas f = n−1
2 ).
3. Étude de quelques structures outils 97

3.6.2 Ébauche de construction


De même que les arbres binaires étiquetés, les arbres externes sur T se re-
présentent par une fonction définie sur l’ensemble des listes finies sur {g, d} et à
valeur dans T . L’arbre (9) ci-dessus se représente par la fonction suivante :

{ [g] → 7,
[d, g, g] → 12,
[d, g, d] → 8,
[d, d] → 4
}

La qualité d’arbre externe se traduit par le fait que si le couple [X] → v est
présent dans la fonction, alors, ni [X, g, . . .] → v1 ni [X, d, . . .] → v2 n’appartient
à la fonction. Nous appliquons à présent la démarche de construction présentée
à la section 3.1.2.
1. Les notations v (arbre externe minimal, ne contenant qu’un seul nœud)
et l, r (arbre externe construit à partir de deux arbres externes l et r) se
définissent par :

Not. Définition Condition


v {[ ] → v} v∈T
−1
(λf ·(f ∈ dom(l) | [g | f ])) ;l
l ∈ liste({g, d}) →
 T∧
l, r ∪
r ∈ liste({g, d}) → T
(λf ·(f ∈ dom(r) | [d | f ]))−1 ; r

2. La fonction (T ) construit un arbre externe à partir d’un élément v, la


fonction , (T ) construit un arbre externe à partir de deux arbres externes.
Elles se définissent par :
(T ) = λv ·(v ∈ T | v)
, (T ) = λ(l, r) ·(l ∈ liste({g, d}) →
 T ∧ r ∈ liste({g, d}) →
 T | l, r)
3. La fonction « pivot » f se définit alors par :
⎛  ⎞
y ∈ P(liste({g, d}) → T)  {(T )[T ]}

f (T ) = λ(y, z) · ⎝ ∧  ∪ ⎠

z ∈ P(liste({g, d}) →
 T)  , (T )[y × z]

(T )[T ] (resp. , (T )[y, z]) est l’image par la fonction (T ) (resp. , (T ))
de T (resp. du produit cartésien y × z). Il s’agit bien d’une fonction de
P(liste({g, d}) →  T ) dans P(liste({g, d}) →  T ). De plus (exercice 3.6.3)
f (T )(y, z) est monotone en y × z.
4. Posons abe(T ) = fix(f (T )). Par définition nous avons alors :
abe(T ) = {(T )[T ]} ∪ , (T )[abe(T ) × T × abe(T )]
5. De l’égalité précédente nous déduisons les deux propriétés suivantes :
98 Structures de données et méthodes formelles

1) v ∈ T ⇒ v ∈ abe(T )
2) l, r ∈ abe(T ) × abe(T ) ⇒ l, r ∈ abe(T )
6. Il est alors possible de décliner le principe d’induction sous la forme sui-
vante :

∀v ·(v ∈⎛T⎛⇒ [a := v]P ) ∧ ⎞ ⎞


l ∈ abe(T ) ∧
∀(l, r) · ⎝⎝ r ∈ abe(T ) ∧ ⎠ ⇒ [a := l, r]P ⎠
[a := l]P ∧ [a := r]P

∀a ·(a ∈ abe(T ) ⇒ P )

3.6.3 Opérations sur les arbres externes


Le prédicat hEq et les opérations rg, rd, rgd, rdg, r, w et h se définissent
comme pour les arbres binaires étiquetés. L’opération parcours, qui renvoie la
liste des feuilles rencontrées dans un parcours gauche-droite, est définie dans le
tableau ci-dessous.

Notation Définition Condition


parcours(v) [v] v∈T
parcours(l, r) parcours(l)  parcours(r) l, r ∈ abe(T )

3.6.4 Conclusion
Il est à présent temps de répondre à une question que le lecteur ne peut man-
quer de se poser : « À quoi peut servir un arbre externe ? D’une part, en termes
de place, la comparaison des formules 3.5.2 et 3.6.1 ci-dessus suggère que les
arbres externes ne sont pas concurrentiels face aux arbres totalement étiquetés,
d’autre part, face à une racine non étiquetée, il semble impossible de prendre
une décision sur le sous-arbre à prendre en considération puisqu’on ne dispose
pas d’information portant sur les sous-arbres. » La réponse se fonde sur le fait
que le développement d’algorithmes tel que nous le préconisons s’effectue en gé-
néral en plusieurs étapes. Sans chercher pour l’instant à entrer dans les détails
de la démarche, que nous abordons au chapitre 5, imaginons que les étiquettes
soient des entiers naturels et que nous ayons affaire à un arbre externe de re-
cherche (le parcours gauche-droite produit une liste triée). Comment s’assurer
de la présence d’un élément v dans l’arbre en question ? Il n’existe certes pas
d’information explicite au niveau d’un nœud interne pour prendre une décision
mais il est possible de supposer disponible la fonction maxAbe qui délivre le
plus grand élément d’un arbre externe et, face à une racine, nous pouvons com-
parer la valeur v à la valeur maximale du sous-arbre gauche, afin d’orienter la
recherche soit vers ce sous-arbre si v est inférieur ou égal au résultat de l’appel
de la fonction soit rechercher dans le sous-arbre droit dans le cas contraire. Dans
une phase ultérieure du développement, il est possible de placer la valeur de la
fonction dans les nœuds internes de l’arbre externe, à condition que la fonction
3. Étude de quelques structures outils 99

maxAbe soit O(1)-décomposable (cf. page 188 pour une définition de cette no-
tion). Le caractère O(1) garantit que, ce faisant, la complexité asymptotique de
l’opération n’est pas affectée par le raffinement. Ainsi, par exemple, partant de
l’arbre externe de recherche :

3 •
• 10
7 9

l’étape d’implantation va conduire à placer à chaque nœud interne la plus grande


valeur du sous-arbre gauche (elle existe toujours), ce qui nous conduit à l’arbre
suivant :
3
3 9
7 10
7 9

Un exemple plus complet est présenté à la section 6.5, page 187. Quels avan-
tages retire-t-on de l’utilisation des arbres externes par rapport à celle des arbres
totalement étiquetés ? Ils sont de deux ordres. Tout d’abord, dans certains cas
(cf. section 10, page 377 sur les tableaux flexibles), il peut être plus facile d’impo-
ser et de préserver certaines formes d’équilibre. Le second avantage se manifeste
lorsque la structure de données est localisée sur un support secondaire. Prenons
le cas des arbres de recherche totalement étiquetés pour lesquels à chaque clé est
associée une information volumineuse (des millions de caractères). L’opération
de recherche va exiger de transférer en mémoire quantité d’informations inutiles
pour la recherche proprement dite (seul le dernier accès est de ce point de vue
profitable). Avec des arbres externes, seule la valeur utile pour l’orientation de
la recherche va être transférée en mémoire centrale (cf. exercice 3.6.4). C’est sur
ce type de raisonnement que les B+ -arbres sont préférés aux B-arbres dans la
mise en œuvre d’index dans les bases de données. Les B-arbres sont étudiés au
chapitre 6, le développement des B+ -arbres (version « externe » des B-arbres) y
est proposé à l’exercice 6.7.5.

Un autre avantage des arbres externes est qu’il est facile (tout au moins
dans une approche non fonctionnelle des structures de données) de relier les
feuilles entre elles dans une liste doublement chaînée. Le couple (arbre externe,
liste) constitue alors le support idéal pour une organisation de fichier appelée
« séquentiel indexé »(cf. encadré page 238). Par contre, un arbre externe tel
que défini ci-dessus ne peut être vide. Cette limitation est pénalisante pour,
par exemple, représenter des ensembles finis par des arbres externes. Elle est
cependant facile à contourner, il suffit de définir une structure abev(T ) par {} ∪
abe(T ).
100 Structures de données et méthodes formelles

Exercices

Exercice 3.6.1 Dénombrer les arbres non ordonnés, binaires, planaires ainsi que les arbres
externes de n nœuds, pour n donné, n ≥ 0.

Exercice 3.6.2 Fournir l’expression ensembliste représentant l’arbre externe (10) de la page 96.

Exercice 3.6.3 Montrer que la fonction f définie lors de la construction des arbres externes
est une fonction monotone.

Exercice 3.6.4 Nous voulons comparer le coût, en terme de quantité d’informations transférée,
entre une solution par arbres totalement étiquetés et une solution par arbres externes. Nous
considérons que les hypothèses suivantes sont satisfaites :
1. Chaque unité d’information est identifiée par une clé de longueur 1, elle s’accompagne
d’une information associée, de longueur i (i >> 1).
2. Les arbres sont supposés avoir le rayon le plus petit possible.
3. Dans le cas des arbres totalement étiquetés, chaque nœud s’accompagne d’une infor-
mation de longueur i + 1 (i pour l’information associée et 1 pour la clé, la place des
informations de structure – pointeurs – est négligée).
4. Dans le cas des arbres externes, aux nœuds internes est associée une information de
longueur 1 (pour la clé) et aux feuilles est associée une information de longueur i + 1.
La place des informations de structure est négligée.
5. (Hypothèse simplificatrice) Consulter un nœud exige de transférer toute l’information
localisée sur ce nœud.
Sachant qu’il y a 2r − 1 nœuds utiles, quel est le coût minimum (resp. maximum) du transfert
dans chacun des cas ?

3.7 Sacs finis


3.7.1 Présentation informelle
Intuitivement, un sac sur un ensemble T est une collection d’éléments de T
dans laquelle un élément donné peut apparaître plusieurs fois. Pour cette raison,
cette notion est parfois appelée multiensemble. Pour ce qui concerne les struc-
tures de données, ce concept est utile par exemple lorsque l’on cherche à réaliser
une modélisation élémentaire des files de priorité (cf. chapitre 9). Chaque client
de la file se présente alors avec une priorité de service ; si l’on fait abstraction de
l’identité du client, la file de priorité apparaît en première approximation comme
un sac dont les éléments sont les priorités des clients en attente. Dans la suite
nous nous intéressons aux sacs finis. Les symboles homologues de [ et ] pour les
listes sont  et . De même que pour les listes, nous adoptons deux notations
pour représenter un sac : la notation récursive et la notation linéaire. Ainsi, le
sac b1 , défini en notation récursive par :
b1 = 1 | 1 | 2 | 3 | 3 | 3 | 4 |  
s’écrit 1, 1, 2, 3, 3, 3, 4 en notation linéaire. De la même façon, le sac b2 défini
en notation récursive par :

b2 = 2 | 2 | 3 | 3 |  
3. Étude de quelques structures outils 101

s’écrit 2, 2, 3, 3 en notation linéaire. La notion de liste véhicule une idée d’ordre :
[2, 2, 3] = [2, 3, 2] (les trois valeurs sont présentes mais dans un ordre différent),
ce n’est pas le cas des sacs où 2, 2, 3 = 2, 3, 2 (les trois valeurs sont présentes).

3.7.2 Ébauche de construction


Dans cette section nous étudions comment la notion de sac peut se construire
inductivement à partir de la théorie des ensembles (cf. section 3.1.2 pour la
présentation de la démarche de construction). Un sac sur T est une fonction
partielle entre T et N1 telle que si v est présent dans le sac en n exemplaires
(n > 0), alors le couple v → n appartient à la fonction. Le sac b1 ci-dessus se
représente par la fonction suivante : {1 → 2, 2 → 1, 3 → 3, 4 → 1}.
1. Les notations  (sac vide) et v | s (sac résultant de l’ajout d’une occur-
rence de l’élément v au sac s) se définissent par :

Not. Définition Condition


 ∅
s∈T →  N1 ∧
v | s s − {v → s(v) + 1}
v ∈ dom(s)
s∈T → N1 ∧
v | s s − {v → 1}
v∈
/ dom(s)

2. La fonction |(T ) qui construit un sac à partir d’un élément v de T et


d’un sac s se définit par :
|(T ) = λ(v, s) ·(v ∈ T ∧ s ∈ T →
 N1 | v | s)
3. La fonction « pivot » f se définit alors par :
f (T ) = λz ·(z ∈ P(T →
 N1 ) | {
} ∪ |(T )[T × z])
|(T )[T × z] est l’image par la fonction |(T ) du produit cartésien T × z.
Il s’agit bien d’une fonction monotone de P(T →  N1 ) dans P(T → N1 ).
4. Posons sac(T ) = fix(f (T )). Par définition nous avons alors :
sac(T ) = {
} ∪ |(T )[T × sac(T )]
5. De l’égalité précédente nous déduisons les deux propriétés suivantes :
1)  ∈ sac(T )
2) v ∈ T ∧ s ∈ sac(T ) ⇒ v | s ∈ sac(T ))
6. Il est alors possible de décliner le principe d’induction sous la forme sui-
vante :
[b := ]P ∧
∀b ·(b ∈ sac(T ) ∧ P ⇒ ∀v ·(v ∈ T ⇒ [b := v | b]P ))

∀b ·(b ∈ sac(T ) ⇒ P )
102 Structures de données et méthodes formelles

3.7.3 Opérations sur les sacs

Afin de marquer l’analogie entre ensembles et sacs, mais également afin de


prévenir toute confusion, nous avons décidé d’adopter comme opérateurs sur les
sacs, des notations s’inspirant de celles des ensembles mais dans une version « à
angles droits ». Ainsi le sac vide se note  9 (resp. ∅ pour les ensembles), le
symbole d’appartenance  − (resp. ∈), le symbole d’union et d’intersection  et
(resp. ∪ et ∩), les symboles d’inclusion  et ! (resp. ⊂ et ⊆), etc. Notons
cependant que l’opérateur , souvent appelé addition multiensembliste, n’a pas
d’analogue parmi les opérateurs ensemblistes. Cet opérateur étant fréquemment
utilisé dans le reste de ce livre, nous reviendrons sur sa signification ci-dessous.
L’opérateur s1  s2 délivre un sac dans lequel toutes les valeurs de s1
ou de s2 sont présentes, avec leur plus grande occurrence. Ainsi b1  b2 =
1, 1, 2, 2, 3, 3, 3, 4. L’opérateur s1 s2 délivre un sac dans lequel les valeurs
communes à s1 et à s2 sont présentes, avec leur plus petite occurrence. Ainsi
b1 b2 = 2, 3, 3.
L’opérateur d’addition multiensembliste s1  s2 délivre un sac tel que pour
chaque valeur e présente dans s1 en n1 (n1 ≥ 0) exemplaires et pour chaque
valeur présente dans s2 en n2 (n2 ≥ 0) exemplaires, e est présente dans le
résultat en n1 + n2 exemplaires. L’expression b1  b2 délivre le sac 1, 1, 2, 2, 2,
3, 3, 3, 3, 3, 4.
L’opérateur s1 −̇ s2 délivre la différence entre les sacs s1 et s2 . Pour tout
élément e présent dans s1 en n1 (n1 ≥ 0) et dans s2 en n2 (n2 ≥ 0), le ré-
sultat contient e en max({0, n1 − n2 }) exemplaires. L’expression b1 −̇ b2 délivre
1, 1, 3, 4.
La fonction mult(e, s) délivre la multiplicité de la valeur e dans le sac s,
c’est-à-dire le nombre d’exemplaires de e présents dans s. Ainsi mult(3, b2 ) = 2.
La fonction bCard(s) délivre le nombre de valeurs présentes dans s. L’ex-
pression bCard(s1 ) délivre 7. Le prédicat s1  s2 (resp. s1 ! s2 ) est identique à
 si et seulement si pour tout élément e tel que e  − s1 , mult(e, s1 ) < mult(e, s2 )
(resp. mult(e, s1 ) ≤ mult(e, s2 )). Ainsi b2  b1 est identique à ⊥ tandis que
1, 1, 4  b1 est identique à . bRan est l’opérateur qui délivre le « codomaine
multiensembliste » de la relation en argument. Ainsi, si l’argument r est la rela-
tion {1 → 10, 2 → 20, 2 → 30, 3 → 10, 3 → 30, 4 → 10}, bRan(r) délivre le sac
10, 10, 10, 20, 30, 30.
Le tableau suivant définit les opérations décrites ci-dessus :

Notation Définition Condition


v−
s v ∈ dom(s)
v−
 s ¬ v−
s
v, b v  b b ∈ sac(T )

9. 
 et  sont synonymes, de même que ∅ et {} pour les ensembles.
3. Étude de quelques structures outils 103

s s
v | m  s v | m  s
s −̇  s
s −̇ v | m (s 
− {v → s(v) − 1}) −̇ m v−
s
s −̇ v | m s −̇ m v−
 s
 s

v | q s q (s −̇ v) v−
s
v | q s ⊥ v−
 s
st s t ∧ s = t
mult(v, ) 0
mult(v, w | s) 1 + mult(v, s) v=w
mult(v, w | s) mult(v,
⎧ s)
⎫ v = w
⎪ ⎪

⎨ v ∈ ran(f ) ∧ ⎪


bRan(f )
v → n n ∈ N1 ∧ f ∈S→T

⎪ ⎪

⎩ n = card(f  {v}) ⎭

bCard(s) ⎧ i ·(i ∈ dom(s) | s(i)) ⎫
⎪ ⎪

⎨ (v −
s∨v−  t) ∧ ⎪


s t v → n n ∈ N ∧

⎪ ⎪

⎩ n = max({mult(v, s), mult(v, t)}) ⎭
⎧ ⎫
⎪ ⎪

⎨ (v −
s∧v−  t) ∧ ⎪


st v → n n ∈ N ∧

⎪ ⎪

⎩ n = min({mult(v, s), mult(v, t)}) ⎭

Dans le tableau ci-dessus, S est un ensemble, v ∈ T , w ∈ T , s ∈ sac(T ),


t ∈ sac(T ) et m ∈ sac(T ).
Dans le cas de sacs définis sur des relatifs (ou sur un sous-ensemble de rela-
tifs), nous pouvons enrichir le jeu d’opérations par les opérations bMin et bMax
qui délivrent respectivement le plus petit et le plus grand élément du sac 10 .

Notation Définition Condition


)
bMax( −∞
bMax(v | s) max({v, bMax(s)}) v | s ∈ sac(Z)
)
bMin( ∞
bMin(v | s) min({v, bMin(s)}) v | s ∈ sac(Z)

3.7.4 Propriétés des sacs


Le tableau 3.3, page suivante, propose un échantillon intéressant des proprié-
tés des sacs. De nombreuses autres propriétés existent telle que (a  b)  c =
10. Pour des raisons de cohérence avec le choix réalisé pour les ensembles de relatifs, nous
admettons que bMax( ) = −∞ et que bMin( ) = +∞.
104 Structures de données et méthodes formelles

a  (b  c). Nous laissons au lecteur le soin de les découvrir et de les démontrer


si nécessaire.

Tableau 3.3 – Propriétés des sacs.


Ce tableau présente quelques-unes des propriétés des opérations , −̇,
bRan, mult et bMin. Dans ce tableau les conditions suivantes s’ap-
pliquent : a ∈ sac(T ), b ∈ sac(T ), c ∈ sac(T ), v ∈ T , r ∈ S ↔ T ,
t ∈ S ↔ T , e ∈ Z ↔ T , i ∈ Z, p ∈ sac(Z), q ∈ sac(Z).

Propriété Condition Ident.


ab = ba (3.7.1)
a = a (3.7.2)
(a  b)  c = a  (b  c) (3.7.3)
a −̇ a =  (3.7.4)
(a  b) −̇ c = a  (b −̇ c) c!b (3.7.5)
 −̇ a =  (3.7.6)
a −̇  = a (3.7.7)
bRan(∅) =  (3.7.8)
bRan({x → y}) = y x∈S∧y ∈T (3.7.9)
bRan(r ∪ t) = bRan(r)  bRan(t) r∩t=∅ (3.7.10)
bRan(r − t) = bRan(r) −̇ bRan(t) (3.7.11)
bRan(e  i) = bRan(e) (3.7.12)
mult(v, a  b) = mult(v, a) + mult(v, b) (3.7.13)
mult(v, a −̇ b) = mult(v, a) − mult(v, b) b!a (3.7.14)
bMin(p  q) = min({bMin(p), bMin(q)}) (3.7.15)

Exercice

Exercice 3.7.1 Démontrer les propriétés du tableau 3.3.

3.8 Conclusion et remarques bibliographiques


Ce chapitre nous a permis de définir des structures outils utiles à la seconde
partie. Nous avons pour cela appliqué une démarche en cohérence avec les cha-
pitres précédents, qui consiste à étendre la théorie des ensembles. Nous avons pu
alors définir des opérations sur ces structures et en déduire certaines propriétés.
Cette démarche est largement inspirée du chapitre 3 du B-Book [3] où les
justifications théoriques sont développées.
Il existe d’autres solutions pour construire des structures de données. Citons
pour mémoire l’approche purement axiomatique (cf. [44]), les types abstraits
3. Étude de quelques structures outils 105

algébriques (cf. par exemple [46, 27, 40, 83]), les relations bien fondées sur un
ensemble [44, 83, 3, 9, 5], les termes [77, 9]. J.-F. Monin [83] offre une vue critique
de ces différentes méthodes.
Notons à ce propos que la méthode du point fixe, que nous avons pratiquée
pour les structures inductives, et celle des termes, convergent vers le même ré-
sultat qui, dans le premier cas, constitue une propriété caractéristique et dans
le second une définition 11 . Dans la seconde partie de l’ouvrage, nous sommes en
permanence amené à spécialiser les structures étudiées ci-dessus. Des soucis de
concision nous conduisent en général à court-circuiter la construction formelle.
Tout se passe alors comme si nous utilisions directement la technique des termes.
Cette façon de procéder n’a pas d’incidence négative sur les développements réa-
lisés.

11. Rappelons toutefois que les définitions inductives comportent une clause de minimalité
qui stipule que l’ensemble visé est le plus petit ensemble satisfaisant les clauses de base et
inductive. Dans la démarche ensembliste, cette clause complémentaire est prise en compte par
la définition du plus petit point fixe.
Chapitre 4

Analyse d’algorithmes

La classe des algorithmes qui satisfont une spécification donnée, si elle


n’est pas vide, contient en général une infinité d’éléments. Mais alors sur quelle
base peut-on comparer ces algorithmes deux à deux ? Quel(s) critère(s) est-il
pertinent d’utiliser dans la pratique ? Typiquement, on distingue des critères
statiques (c’est-à-dire indépendants de l’exécution de l’algorithme) et des critères
dynamiques. Pour les premiers nous pouvons citer :
– le temps de développement d’une solution à partir de la spécification ;
– la lisibilité du programme ;
– sa maintenabilité ;
– la qualité de son ergonomie ;
– sa sécurité d’utilisation ;
– sa longueur ;
– etc.
Les quatre premiers points ne doivent pas être négligés, mais ils sont trop subjec-
tifs (ils dépendent autant du programmeur et/ou du lecteur que de l’algorithme
lui-même) pour permettre une comparaison impartiale. L’objectif de sécurité
est, du point de vue des outils qui permettent de l’évaluer, très lié à la notion
de correction du programme par rapport à sa spécification, puisqu’il s’agit alors
de démontrer qu’une propriété (comme par exemple « le programme ne modifie
que des fichiers locaux ») est satisfaite par le programme. Le dernier point, la
longueur, est à la base de la notion de complexité de Kolmogorov. Son étude sort
du cadre de cet ouvrage. Compte tenu de nos objectifs, nous penchons pour un
critère de nature dynamique portant soit sur la place mémoire utilisée pour une
exécution soit sur le temps d’exécution. Pour les algorithmes qui nous intéressent,
la place mémoire est souvent une fonction linéaire de la taille des données. Il est
plus intéressant de se focaliser sur le critère du temps d’exécution (la complexité
temporelle) 1 . Plusieurs arguments supplémentaires militent en faveur du critère
temporel plutôt que du critère spatial : pour occuper de la place il faut consom-
mer du temps (l’inverse n’est pas vrai) ; de plus, en supposant que l’exécution
se termine, la place occupée est libérée à l’issue de l’exécution et est à nouveau

1. Pour cette raison, dans la suite lorsque nous utilisons le terme complexité, il s’agit par
défaut de complexité temporelle.
108 Structures de données et méthodes formelles

disponible, le temps consommé est lui perdu à jamais. Le temps n’est pas une
ressource recyclable. Par ailleurs, la complexité asymptotique établit une ligne
de partage consensuelle entre les solutions praticables et les autres. De ce fait la
notion de complexité temporelle constitue une base théorique permettant de ju-
ger de la possibilité de mettre en œuvre (ou non) une solution dotée des qualités
requises.
De même que pour les algorithmes, et pour les mêmes raisons, toutes les mises
en œuvre d’un même type de données ne sont pas équivalentes. En outre, le cas
des structures de données est plus délicat. S’il est souvent possible de comparer
(asymptotiquement) la complexité de deux algorithmes, la comparaison est en
général impossible 2 pour deux structures de données, puisqu’elle devrait porter
sur plusieurs opérations. L’idéal serait de concevoir une mise en œuvre qui soit
la meilleure pour toutes les opérations. . .
Il existe cependant une particularité des structures de données qui a conduit
les chercheurs (cf. en particulier [113]) à définir une forme originale d’analyse de
complexité : l’analyse amortie. Cette particularité est liée au fait que lors de la
vie d’une structure de données, on est souvent amené à réaliser des séquences
d’appels à une opération et à constater que le coût important d’un appel parti-
culier peut être amorti sur les appels voisins.
Ce chapitre se compose de trois sections. La première présente les outils
traditionnels de la comparaison d’algorithmes : les notations O, Ω et Θ. La
seconde s’intéresse à la complexité « classique ». Le contenu de ces deux sections
est supposé familier au lecteur, il n’est présenté que pour mémoire. Les références
[40, 103, 9, 43] peuvent être consultées avec profit pour un approfondissement.
La troisième section est une introduction à l’analyse amortie par la méthode la
plus pratiquée, celle du potentiel. Bien que datant de la fin des années 1970, elle
est par nature principalement réservée au domaine des structures de données et
donc moins largement diffusée que la complexité classique. Elle est appliquée à
de nombreuses reprises dans le reste de l’ouvrage.

4.1 Notations asymptotiques


Dans la suite de ce chapitre nous nous intéressons aux fonctions de profil
N→R+ . Ce choix est justifié par le fait que la complexité d’un algorithme est une
fonction à valeurs réelles non négatives définie sur une grandeur scalaire discrète,
typiquement la taille de la « donnée ». Le caractère total de ces fonctions tient
au fait qu’en général toutes les tailles sont significatives.

Remarque. Il existe des situations pour lesquelles réduire le domaine aux


seuls entiers naturels est contraignant et trompeur. C’est par exemple le cas si
la donnée est une relation : le cardinal de l’ensemble source, celui de l’ensemble
destination et celui de l’ensemble des couples de la relation sont fortement in-
dépendants. Ce type de situation exige en général une analyse séparée de la
complexité pour chacun de ces paramètres. Il peut cependant être parfois traité
par des fonctions dont le domaine de définition est composite. Les notations et
2. Sauf à définir une structure d’ordre sur les opérations. . .
4. Analyse d’algorithmes 109

propriétés présentées ci-dessous s’étendent facilement au cas de plusieurs para-


mètres.

Considérons le problème du tri d’un tableau sans doublon. Ce problème est


spécifié informellement de la manière suivante : « la fonction tri(t) délivre un
tableau représentant la permutation triée par ordre croissant des valeurs de t. »
Supposons que nous cherchions à comparer la complexité de deux fonctions déter-
ministes tri1 (t) et tri2 (t) qui satisfont cette spécification. Supposons par ailleurs
que la 3 complexité de la fonction tri1 (resp. tri2 ) soit donnée par la fonction
f1 (n) (resp. f2 (n)), n étant la taille de t. Du point de vue de la complexité,
il serait intéressant de pouvoir comparer tri1 et tri2 à travers la situation re-
lative de f1 et f2 , et affirmer par exemple « tri1 est toujours au moins aussi
performante que tri2 » (qui traduit l’affirmation « la courbe de f1 n’est jamais
au-dessus de celle de f2 »). C’est ce que permettraient les fonctions f1 et f2 de
la partie (11) de la figure 4.1 ci-dessous. Cependant il est rare que les fonctions
ainsi comparées autorisent des affirmations aussi précises. Les courbes peuvent
se croiser indéfiniment. Une situation plus intéressante est celle où à partir d’une
certaine abscisse l’une des courbes passe au niveau ou au-dessus de l’autre. C’est
l’hypothèse qui est retenue pour la partie (12) de la figure 4.1, où la courbe de
f2 passe définitivement au-dessus de la courbe f1 après l’abscisse n0 .

f (n)
f2 f (n) f2
f1
(11) f1 (12)

n n
n0

Figure 4.1 – Comparaison de fonctions.


La partie (11) montre deux fonctions qui sont comparables sur la totalité
de leur domaine de définition. Ce n’est pas le cas pour les fonctions de
la partie (12) mais, à partir d’un certain rang n0 , la courbe f1 vient
« définitivement » sous la courbe f2 .

Révisons nos ambitions en nous limitant à des affirmations du type « f1 est


toujours au moins aussi performante que f2 , sauf pour un nombre fini de cas. »
Cette fois nous sommes à même de comparer les deux fonctions de la partie (12)
de la figure 4.1 : à partir de n0 , f1 est toujours au moins aussi performante que
f2 . Les seuls cas qui ne satisfont pas cette affirmation sont ceux qui précèdent
n0 ; compte tenu du domaine de définition (N), ils sont en nombre fini.
Cette notion se formalise facilement. Si l’on nomme # (f ) l’ensemble des
fonctions inférieures ou égales à f à partir d’une certaine abscisse, alors

 {t | t ∈ N → R+ ∧ ∃n0 ·(n0 ∈ N ∧ ∀n ·(n ∈ N ∧ n ≥ n0 ⇒ t(n) ≤ f (n)))}


# (f ) =
3. L’usage de l’article la est abusif dans la mesure où il existe en général plusieurs types de
complexité (complexité au pire, moyenne, etc.). Ces notions sont précisées à la section suivante.
Nous acceptons momentanément cet abus de langage.
110 Structures de données et méthodes formelles

Notons que la valeur n0 dépend en général de t. Munis de cette définition nous


sommes capable de démontrer des propositions telles que :
1. n2 ∈ # (2n + 2 · n)
2. 20 · n2 ∈ # (3 · n3 + 2 · n + 1)
3. 20 · n2 ∈ # (20 · n2 + 50 · n + 5)
4. 20 · n2 ∈
/ # (19 · n2 )
5. n ∈
2
/ # (200 · n)
La relation induite par la notation # est un préordre partiel (elle est réflexive
et transitive). Cette notion est le plus souvent considérée comme trop contrai-
gnante : si la proposition 5 ci-dessus est légitime, la proposition 4 est discutable.
On aimerait pouvoir affirmer que, pour tout réel c positif, f (n) ∈ # (c · f (n)). Ce
qui revient à négliger les constantes de proportionnalité. Cette notion est captée
par la notation O :
Définition 1 (Notation O). Soit f ∈ N → R+ .
   
 c, n0 ∈ R∗+ × N ∧

 t t ∈ N → R+ ∧ ∃(c, n0 ) ·
O(f ) =
∀n ·(n ∈ N ∧ n ≥ n0 ⇒ t(n) ≤ c ·f (n))

Cette notation induit également un préordre partiel sur l’ensemble des fonc-
tions considérées. Elle est illustrée à la figure 4.2 qui montre le rôle que joue la
constante multiplicative c.

f (n)
c·f
t
f

n
n0

Figure 4.2 – Comparaison de fonctions – Notation O.


La fonction t est en O(f ) puisqu’il existe une constante positive c et
un entier naturel n0 tels qu’à partir de l’abscisse n0 la courbe de t n’est
jamais au-dessus de celle de c · f.

Si f ∈ O(g) on écrit que « f est en O(g) », que « f est dominé asymptoti-


quement par g », que « g est supérieur asymptotiquement à f » ou encore, par
abus de langage, que f = O(g). Cette fois il est possible de montrer que :
1. n2 ∈ O(2n + 2 · n)
2. 20 · n2 ∈ O(3 · n3 + 2 · n + 1)
3. 20 · n2 ∈ O(20 · n2 + 50 · n + 5)
4. 20 · n2 ∈ O(19 · n2 )
5. n2 ∈/ O(200 · n)
Le tableau 4.1, page ci-contre, répertorie quelques-unes des propriétés de la
notation O en rappelant en particulier les propriétés de réflexivité et de transiti-
vité. Toutes ces propriétés peuvent être utilisées pour faciliter les démonstrations.
4. Analyse d’algorithmes 111

Tableau 4.1 – Quelques propriétés de la notation O.

Propriété Condition Ident.


n a
∈ O(n )
b
n ∈ N ∧ a ∈ R + ∧ b ∈ R+ ∧ a ≤ b (4.1.1)
f ∈ O(f ) f ∈ N → R+ (4.1.2)
f ∈ N → R + ∧ g ∈ N → R+ ∧ h ∈ N → R + ∧
f ∈ O(h) (4.1.3)
f ∈ O(g) ∧ g ∈ O(h)
f 1 ∈ N → R + ∧ f2 ∈ N → R+ ∧
f1 + f2 ∈
g1 ∈ N → R + ∧ g 2 ∈ N → R + ∧ (4.1.4)
O(max({g1 , g2 }))
f1 ∈ O(g1 ) ∧ f2 ∈ O(g2 )
f 1 ∈ N → R + ∧ f2 ∈ N → R+ ∧
f1 · f2 ∈
g1 ∈ N → R + ∧ g 2 ∈ N → R + ∧ (4.1.5)
O(g1 · g2 )
f1 ∈ O(g1 ) ∧ f2 ∈ O(g2 )
f ∈ N → R + ∧ g ∈ N → R+ ∧
f ∈ O(g) (4.1.6)
(n)
∧ limn→∞ fg(n) =0
f ∈ O(g) ∧ f ∈ N → R + ∧ g ∈ N → R+ ∧
(4.1.7)
g ∈ O(f ) ∃c ·(c ∈ R∗+ ∧ limn→∞ fg(n)
(n)
= c)

Il existe une notation duale de O, la notation Ω, qui est à O ce que l’opérateur


≥ est à ≤ . Si g ∈ Ω(f ), il existe une constante réelle positive c et une constante
entière n0 telles qu’à droite de n0 la courbe f (n) est toujours sur ou au-dessus
de f (n). Formellement, la définition de Ω est donnée par :
Définition 2 (Notation Ω). Soit f ∈ N → R+ .
   
 c, n0 ∈ R∗+ × N ∧
 t t ∈ N → R+ ∧ ∃(c, n0 ) ·
Ω(f ) =
∀n ·(n ∈ N ∧ n ≥ n0 ⇒ t(n) ≥ c ·f (n))

Ici aussi les formulations g = Ω(f ) ou « g est en Ω(f ) » sont admises. L’intérêt
majeur de cette notation est qu’elle permet de définir la notation Θ. Θ joue
schématiquement le rôle de l’égalité asymptotique, elle se définit par :
Définition 3 (Notation Θ). Soit f ∈ N → R+ et g ∈ N → R+ .
f ∈ Θ(g) =
 f ∈ O(g) ∧ f ∈ Ω(g)
Les formulations g = Θ(f ), « g est en Θ(f ) » ou encore « g est équivalent (ou
comparable) asymptotiquement à f » sont permises. La relation induite par Θ
est un ordre partiel. Il est possible de démontrer que :
1. n2 ∈/ Θ(2n + 2 · n)
2. 20 · n2 ∈
/ Θ(3 · n3 + 2 · n + 1)
3. 20 · n ∈ Θ(20 · n2 + 50 · n + 5)
2

4. 20 · n2 ∈ Θ(19 · n2 )
112 Structures de données et méthodes formelles

5. n2 ∈/ Θ(200 · n)
Le tableau 4.2 ci-dessous fournit quelques-unes des propriétés de la notation Θ.

Tableau 4.2 – Quelques propriétés de la notation Θ.

Propriété Condition Ident.


f ∈ Θ(f ) f ∈ N → R+ (4.1.8)
f ∈ N → R + ∧ g ∈ N → R+ ∧
f ∈ Θ(g) (4.1.9)
f ∈ O(g) ∧ g ∈ O(f )
f ∈ N → R + ∧ g ∈ N → R+ ∧
f ∈ Θ(h) (4.1.10)
h ∈ N → R+ ∧ f ∈ Θ(g) ∧ g ∈ Θ(h)
f 1 ∈ N → R + ∧ f2 ∈ N → R + ∧
f 1 + f2 ∈
g1 ∈ N → R + ∧ g 2 ∈ N → R + ∧ (4.1.11)
Θ(max({g1 , g2 }))
f1 ∈ Θ(g1 ) ∧ f2 ∈ Θ(g2 )
f 1 ∈ N → R + ∧ f2 ∈ N → R + ∧
f1 · f2 ∈
g1 ∈ N → R + ∧ g 2 ∈ N → R + ∧ (4.1.12)
Θ(g1 · g2 )
f1 ∈ Θ(g1 ) ∧ f2 ∈ Θ(g2 )
f ∈ N → R + ∧ g ∈ N → R+ ∧
f ∈ Θ(g) (4.1.13)
∃c ·(c ∈ R∗+ ∧ limn→∞ fg(n)
(n)
= c)
f ∈ N → R + ∧ g ∈ N → R+ ∧
f∈
/ Θ(g) (4.1.14)
(n)
∧ limn→∞ fg(n) =0
f ∈ N → R + ∧ g ∈ N → R+ ∧
f∈
/ Θ(g) (4.1.15)
(n)
∧ limn→∞ fg(n) =∞

Remarques.
1. Les notations O et Θ sont utilisées dans le contexte de l’évaluation des
algorithmes notamment pour comparer asymptotiquement deux fonctions
entre elles. Les mathématiques offrent un autre usage (qui justifie les abus
de langage f = O ou f = Ω(g)) lié à la majoration des erreurs faites
en assimilant une fonction à une autre. Ainsi écrire 20 · n2 + 50 · n + 5 =
20 · n2 + O(n) se lit de la manière suivante : la fonction 20 · n2 + 50 · n + 5
est égale à la fonction 20 · n2 + f (n) où f (n) est une fonction indéterminée
dont on sait simplement que f (n) = O(n). L’utilisation de la notation =
présente des risques d’erreurs dans certains calculs.
2. La comparaison de fonctions de complexité se fait en général en écrivant
f (n) ∈ O(g(n)) où f (n) est la fonction de complexité en question et où
g(n) est une fonction la plus simple possible, prise autant que faire se
peut dans l’échelle de complexité croissante suivante : 1, log n, n, n · log n,
n2 , n3 , . . . , 2n , 3n . . . , n!, nn . Les fonctions ni sont dites polynomiales
4. Analyse d’algorithmes 113

et constituent la limite au-delà de laquelle les solutions algorithmiques ne


sont pas praticables.

Exercices

Exercice 4.1.1 Soit f ∈ N → R+ et g ∈ N → R+ deux fonctions quelconques. Est-il toujours


possible de démontrer soit que f ∈ O(g) soit que g ∈ O(f ) ? Qu’en est-il si l’on remplace O
par Θ ?

1
Exercice 4.1.2 Sachant que pour x ∈ R∗+ , x + 1 = x ·(1 + x
), montrer que log(x + 1) ∈
O(log(x)).

Exercice 4.1.3 Soit f ∈ N → R+ .


1. Est-il toujours possible de trouver une fonction g telle que f ∈ O(g) ? Et f ∈ Θ(g) ?
2. Est-il toujours possible de trouver une fonction g de la « liste étalon » 1, log n, n,
n · log n, n2 , n3 , . . . , 2n , 3n . . . , n!, nn telle que f ∈ O(g) ? Et f ∈ Θ(g) ?

Exercice 4.1.4 Soit f ∈ N → R+ une fonction définie par :



nn si n est premier
f (n) =
n si n n’est pas premier

Proposer des fonctions g et h différentes de f telles que f ∈ O(g) et f ∈ Θ(h). Démontrer


votre affirmation.

Exercice 4.1.5 Certains auteurs (cf. par exemple [7]) utilisent la définition suivante de la
notation Ω : Ω(f ) est l’ensemble des fonctions t telles qu’il existe une constante positive c pour
laquelle t(n) ≥ c · f (n), ceci pour une infinité de valeurs de n.
– Formaliser la définition de cette notation.
– Discuter de ses avantages et inconvénients.

4.2 Analyse classique de la complexité


de fonctions
À la section précédente, nous nous sommes préoccupé de donner un sens
précis aux notions de comportement et de comparaison asymptotiques de (cer-
taines classes de) fonctions. Nous avons pour l’instant fait abstraction de la
nature réelle de la grandeur mesurée en supposant simplement qu’elle est liée au
temps d’exécution du programme. Il nous faut à présent préciser cet aspect avant
d’aborder la question de fond de cette section : l’évaluation du comportement
asymptotique d’un programme.
Deux types d’approche sont étudiés. Le premier consiste tout d’abord à pas-
ser de l’algorithme (exprimé par une fonction) à l’équation récurrente dont la
solution fournit une mesure de la complexité, à résoudre ensuite l’équation ré-
currente et enfin à en déduire son comportement asymptotique. Le second type
d’approche exploite le fait que nous ne sommes pas à la recherche d’une solu-
tion exacte pour l’équation récurrente afin de contourner, ou de simplifier, cette
étape.
114 Structures de données et méthodes formelles

4.2.1 Passage du texte d’une fonction à une équation


récurrente
Lorsque nous affirmons que la complexité d’un algorithme traitant une don-
née de taille n est fournie par la fonction T (n) telle que T (n) ∈ N → R+ , quelle
grandeur est ainsi mesurée ? Le temps processeur ? Ce choix est peu judicieux
car trop lié à l’état de la technologie. Le nombre d’instructions exécutées ? Ce
choix est plus pertinent mais, sauf à se baser sur un « ordinateur étalon », il est
aussi sujet à caution (instructions machines ? instructions d’un langage de haut
niveau ?, etc.). L’ordinateur étalon évoqué ci-dessus pourrait être une machine
de Turing (cf. par exemple [117]) mais sa nature séquentielle conduit à un ré-
sultat pessimiste par rapport aux machines dotées de mémoires à accès direct
(machines ram) que nous utilisons au quotidien 4 . Par ailleurs, cette solution exi-
gerait de traduire le programme obtenu en un programme pour une machine de
Turing à cette seule fin. . . Le contexte fonctionnel dans lequel nous nous plaçons
dans cet ouvrage nous offre une solution simple : nous dénombrons le nombre de
gardes évaluées par l’appel initial. Une autre méthode, qui fournit en général un
résultat asymptotique équivalent, consiste à dénombrer les appels à la fonction.
Prenons l’exemple de la fonction estT rie(n, t) qui délivre true si et seulement
si le tableau t, de longueur n, est trié par ordre croissant 5 :
function estT rie(n, t) ∈ N1 × (1 .. n → N) → bool =

if n = 1 →
true
|n>1 →
bool(t(n) ≥ t(n − 1)) ∧ estT rie(n − 1, t)

À la section 1.11, nous avons étudié l’évaluation d’une fonction comportant
une conditionnelle et la sémantique des conditionnelles. Pour ce qui concerne
l’analyse de complexité, de façon à ne pas introduire de biais dans les résultats,
nous faisons l’hypothèse d’une implantation déterministe : nous supposons que
l’évaluation des gardes est séquentielle.

Revenons à présent sur la recherche de la fonction de complexité pour la


fonction estT rie. Si le tableau est réduit à un seul élément, seule la première
garde (n = 1) est évaluée. Dans le cas contraire, il faut ajouter à la garde évaluée
directement toutes les gardes évaluées lors de l’appel récursif. Si T (n) représente
le nombre de gardes évaluées, pour n ≥ 1, T (n) satisfait l’équation récurrente
suivante :

T (1) = 1
(4.2.1)
T (n) = T (n − 1) + 1 Pour n > 1
Dans l’éventualité où une garde comptabilisée comporte un appel à une ou plu-
sieurs fonctions, il faut tenir compte des gardes qui sont ainsi évaluées indirec-
tement.
4. Un théorème montre que pour tout programme ram en O(f (n)) il existe une machine
de Turing équivalente, à 7 rubans, pour laquelle la complexité est en O(f 3 (n)).
5. Par abus de langage, et exceptionnellement, nous considérons ici et dans la fonction
estT rie que ∧ est un opérateur booléen et non un opérateur de la logique propositionnelle.
4. Analyse d’algorithmes 115

Des situations plus complexes peuvent survenir comme par exemple le cas de
fonctions mutuellement récursives : f se définit à partir de g qui elle-même se
définit à partir de f . Le principe de détermination de l’équation récurrente reste
le même. La seule différence étant que l’on travaille sur un vecteur d’équations
et non plus sur une équation unique.
De par sa fréquence dans les algorithmes liés aux arbres binaires, un cas
particulier mérite que l’on s’y arrête. Il s’agit de la récurrence appelée récurrence
de partition, dans laquelle le membre droit de la partie inductive de l’équation
s’exprime sur une taille des données qui est une fraction de la taille initiale.
Considérons le même problème que ci-dessus (déterminer si un tableau est trié)
mais en supposant (i) que le tableau est défini sur l’intervalle i .. s, (ii) qu’il
possède un nombre d’éléments qui est une puissance de 2 (s − i + 1 = 2k ).

function estT rie (i, s, t) ∈ N × N × (i .. s → N) →


 bool = 
pre
∃k ·(k ∈ N ∧ s − i + 1 = 2k )
then
if s − i = 0 →
true
|s−i>0 →
let m := (s + i) ÷ 2 in
bool(t(m) ≤ t(m + 1)) ∧ estT rie (i, m, t) ∧ estT rie (m + 1, s, t)
end

end

Si n est la longueur du tableau traité par la fonction estT rie et T (n) le


nombre de gardes évaluées, dans le cas où n = 1, la valeur recherchée est 1.
Par contre si n est plus grand que 1 (compte tenu de la précondition n est
une puissance de 2) le résultat est 1 plus deux fois T (n ÷ 2). D’où l’équation
récurrente suivante :

T (1) = 1
(4.2.2)
T (n) = 2 · T (n ÷ 2) + 1 pour n > 1 et n puissance de 2

Reste à résoudre cette équation. . .


Le contexte fonctionnel dans lequel nous nous situons introduit, du point de
vue de l’analyse de la complexité, un écueil dont il faut être conscient. Nous
allons l’illustrer à travers la fonction m(v, p, t) qui délivre un tableau identique
au tableau t à l’exception de la position p qui contient v :

function m(v, p, t) ∈ N × 1 .. 10000 × (1 .. 10000 → N) → (1 .. 10000 → N) =



t − {p → v}

Le schéma de calcul de complexité préconisé ci-dessus nous conduit à affirmer


abusivement que la fonction de complexité pour cette fonction est T (m) = 1
puisqu’on ne dénombre qu’un seul appel à la fonction. Pourtant, si l’on y re-
garde plus près, le résultat délivré est un tableau qu’il faut construire en copiant
116 Structures de données et méthodes formelles

9 999 des éléments de t ! Difficile de ne pas tenir compte du coût de cette cons-
truction ! C’est pour cette raison que parfois les tableaux ne sont pas considérés
comme des structures « fonctionnelles ». Nous n’adhérons pas à cette limitation
pour deux raisons. La première est qu’exclure les tableaux de notre éventail de
structures de base nous conduirait à écarter de notre étude des structures de
données importantes comme les techniques de hachage ou les B-arbres. La se-
conde raison est que la suite (cf. chapitre 10) nous montre qu’il est possible de
définir et de mettre en œuvre efficacement des structures fonctionnelles qui se
comportent comme des tableaux. Dans le reste de l’ouvrage, la complexité in-
duite par l’utilisation de tableaux n’est pas prise en compte. Nous invitons le
lecteur à réfléchir à ce problème à chaque fois qu’il est rencontré.

4.2.2 Complexité la meilleure, la pire, moyenne


Le lecteur attentif n’a pas manqué de remarquer que l’efficacité de la fonction
estT rie peut être améliorée. En effet, compte tenu de la définition de l’opérateur
∧, il est parfois inutile d’évaluer l’appel récursif pour décider du résultat. Cette
remarque se traduit dans le code par l’introduction d’une conditionnelle qui, si
nous sommes dans la situation t(n) < t(n−1), conclut directement que le tableau
n’est pas trié et, dans le cas contraire, assimile le résultat à celui obtenu sur la
partie du tableau limitée au domaine 1 .. n − 1. Soit estT rie cette fonction :

function estT rie (n, t) ∈ N1 × (1 .. n → N) → bool =



if n = 1 →
true
|n>1 →
if t(n) < t(n − 1) →
false
| t(n) ≥ t(n − 1) →
estT rie (n − 1, t)

Dans ce type de situation, il est courant de distinguer trois sortes de com-


plexité selon la configuration de l’argument t pour une taille donnée n : la com-
plexité la meilleure, la complexité la pire et la complexité moyenne. Les deux
premières se définissent facilement. La complexité la meilleure est obtenue par
une configuration des données qui minimise le coût. Dans notre exemple il est
clair que, pour une taille n donnée (n = 1), la complexité la meilleure est obtenue
avec un tableau tel que t(n) < t(n − 1). Formellement, pour une fonction f de
profil f (a) ∈ d → c, la complexité la meilleure pour un argument de taille n se
définit par :

 min({T f (a) (n) | a ∈ dom(f ) ∧ card(a) = n})


T min (n) =

La complexité la meilleure d’une fonction f (a) pour une donnée de taille n est
donc le plus petit coût trouvé lorsque l’argument parcourt le domaine de la
fonction. Ainsi, pour la fonction estT rie , nous avons trivialement T min (n) = 1
4. Analyse d’algorithmes 117

et ceci pour tous les tableaux dont les deux dernières valeurs sont dans un ordre
décroissant.
De même la complexité la pire est définie par :

 max({T f (a) (n) | a ∈ dom(f ) ∧ card(a) = n})


T max (n) =

Dans le cas de la fonction estT rie , ce coût le pire est obtenu pour un tableau
t trié. L’équation récurrente obtenue est :

T max (1) = 1
T max (n) = T max (n − 1) + 1 pour n > 1

La complexité moyenne se définit en sommant les coûts obtenus pour toutes


les configurations possibles et en divisant par le nombre de configurations consi-
dérées (à condition que celui-ci soit positif), soit :

a ·(a ∈ dom(f (a)) ∧ card(a) = n | T f (a) (n))

T moy (n) =
card({d | d ∈ dom(f (a)) ∧ card(d) = n})

Le calcul de la complexité moyenne sort en général du cadre de cet ouvrage.


La définition de T moy (n) revient à considérer que toutes les configurations sont
équiprobables. Il est possible de l’affiner en adoptant un modèle probabiliste. À
la difficulté technique du calcul vient alors s’ajouter celle du choix d’un modèle
probabiliste approprié. Pour ces raisons, dans la suite, nous nous limitons en gé-
néral à l’étude de la complexité la pire et nous n’abordons la complexité moyenne
équiprobabiliste que dans les cas simples (cf. section 6.3.4 pour un exemple).

4.2.3 Résolution d’équations récurrentes


Dans ce chapitre nous avons, dans une première section, étudié la notion
de comportement asymptotique de (certaines classes de) fonctions, puis, dans
cette seconde section, nous avons vu comment il est possible d’interpréter le
texte d’une fonction afin d’obtenir une équation récurrente destinée à rendre
compte de la complexité temporelle de l’algorithme codé par la fonction. Nous
avons ensuite précisé différents types de complexité selon la configuration des
données. Il nous reste une étape à franchir qui va nous permettre de déterminer
la complexité asymptotique d’un algorithme fonctionnel ; c’est celle qui, à partir
d’une équation récurrente, fournit la forme close équivalente. Il s’agit d’un sujet
difficile dont l’intérêt pratique dépasse largement l’analyse de complexité 6 et
pour l’étude duquel de nombreux ouvrages existent (cf. [40, 103, 9, 43, 25] par
exemple pour les ouvrages les plus orientés vers l’informatique). En conséquence,
notre objectif se limite ici à rappeler quelques méthodes simples et à fournir des
pistes pour un approfondissement.
Considérons tout d’abord l’équation récurrente 4.2.1, page 114. Abandonnons
cette notation pour la notation plus rigoureuse suivante :
6. Les équations récurrentes sont utilisées dans la plupart des sciences. Elles servent en
particulier de modèles approchés lorsque les modèles continus basés sur des équations différen-
tielles, intégrales ou aux dérivées partielles n’ont pas de solutions connues.
118 Structures de données et méthodes formelles

(T (1) = 1) ∧ ∀m ·(m ∈ N1 ∧ m > 1 ⇒ T (m) = T (m − 1) + 1) (4.2.3)

Une première façon, naïve, de résoudre cette équation consiste à appliquer


la méthode dite de la somme. Réécrivons la formule à tous les ordres compris
entre 1 et n. Les termes T (i), pour i ∈ 1 .. n − 1, s’annulent alternativement et
le résultat est immédiat :
T (n) = T (n − 1) + 1
+
T (n − 1) = T (n − 2) + 1
+
..
.
+
T (2) = T (1) + 1
+
T (1) = 1
T (n) = n

En toute rigueur, puisque ce résultat a plus été deviné que démontré, nous de-
vons réaliser une démonstration (par récurrence). Bien qu’à l’avenir nous ferons
confiance aux résultats obtenus par sommation (ou plus généralement par la mé-
thode des facteurs sommants utilisée ci-dessous), nous allons cette fois mettre en
pratique le théorème 1 pour effectuer cette démonstration. Cette démonstration
peut être sautée en première lecture.
Soit Q la formule 4.2.3. Posons P (n) =  (T (n) = n). Nous devons montrer
que Q ⇒ P (n). Nous pouvons déplacer Q en hypothèse pour nous focaliser sur la
démonstration de P (n). D’après le théorème 1, page 44, nous devons démontrer :
(i) la base [n := 1]P (n), (ii) la partie inductive [n := n + 1]P (n) sous les
hypothèses Q, P (n) et n ∈ N1 . Débutons par la preuve de la base.

[n := 1]P (n)
⇔ Définition de P (n) et substitution
T (1) = 1
⇔ Hypothèses
1=1
⇔ Arithmétique


Montrons à présent [n := n + 1]P (n) sous les hypothèses Q, P (n) et n ∈ N1 .


Au préalable, nous constatons que de l’hypothèse Q nous pouvons déduire [m :=
n + 1](m ∈ N1 ∧ m > 1 ⇒ T (m) = T (m − 1) + 1), soit encore (n + 1 ∈ N1 ∧ n + 1 >
1 ⇒ T (n + 1) = T (n) + 1), formule qui peut être ajoutée aux autres hypothèses.

[n := n + 1]P (n)
⇔ Définition de P
T (n + 1) = T (n) + 1
⇔ Hypothèse T (n + 1) = T (n) + 1

4. Analyse d’algorithmes 119

Ce qui achève la démonstration. Une autre solution pour résoudre l’équation


récurrente consiste à effectuer une somme partielle sur l’intervalle n − i .. n, pour
i ∈ 0 .. n − 1 :

T (n) = T (n − 1) + 1
+
T (n − 1) = T (n − 2) + 1
+
..
.
+
T (n − i + 1) = T (n − i) + 1
T (n) = T (n − i) + i

D’où, pour i = n − 1, T (n) = n. La preuve formelle de ce résultat peut se


faire par récurrence, comme ci-dessus. Il est également possible de prouver (par
récurrence) le résultat intermédiaire :

∀i ·(i ∈ 0 .. n − 1 ⇒ T (n − i + 1) = T (n − i) + 1)

(cf. exercice 4.2.2) avant d’effectuer la substitution i := n − 1 :

[i := n − 1](i ∈ 0 .. n − 1 ⇒ T (n − i + 1) = T (n − i) + 1)
⇔ Calculs non développés
T (n) = n

L’équation récurrente que nous venons de résoudre de plusieurs façons est


un exemple (très) simple d’une classe d’équations récurrentes – les équations
linéaires à coefficients constants – pour lesquelles il existe des méthodes plus
générales, en particulier la méthode de l’équation caractéristique et la méthode
des séries génératrices. Les références [80, 40, 9, 25] peuvent être consultées pour
de plus amples développements.

Considérons à présent l’équation récurrente 4.2.2, page 115, obtenue pour la


fonction estT rie . Elle se caractérise par le fait que le membre droit s’exprime à
partir d’une expression multiplicative de l’argument, le facteur multiplicatif est
ici 12 (et non plus comme dans les équations linéaires à partir d’une expression
additive de l’argument). Nous allons la résoudre en procédant au changement de
variable n = 2m et en posant T  (m) = T (2m ). L’équation 4.2.2 devient :
 
T (0) = 1
T  (m) = 2 · T  (m − 1) + 1 Pour m > 0

La méthode de sommation précédemment utilisée ne peut s’appliquer puisqu’en


raison du facteur 2, les termes en T  ne s’annulent pas en alternance. Nous allons
employer la méthode des facteurs sommants qui consiste à multiplier chaque
équation par une puissance de 2 appropriée afin d’être à même d’appliquer cette
fois la méthode de sommation :
120 Structures de données et méthodes formelles

20 · T  (m) = 20 ·(2 · T  (m − 1) + 1)
+
21 · T  (m − 1) = 21 ·(2 · T  (m − 2) + 1)
+
..
.
+
2m · T  (0) = 2m · 1

En sommant, nous obtenons T  (m) = Σj∈0..m 2j . Soit encore T  (m) = 2 · 2m − 1.


En effectuant le changement de variable réciproque, nous obtenons au final :

T (n) = 2 · n − 1

Gardons cependant à l’esprit le fait que nous avons résolu l’équation 4.2.2 uni-
quement pour des puissances de 2.

4.2.4 Contourner ou simplifier certaines étapes


intermédiaires
La section précédente nous a fourni quelques éléments pour résoudre une
équation récurrente de manière exacte. Il ne faut cependant pas perdre de vue
que notre objectif est moins ambitieux : il s’agit de déterminer un ordre de
grandeur des fonctions qui définissent la complexité temporelle d’un algorithme.
La solution exacte de l’équation récurrente nous importe peu. En particulier, si
nous ne sommes intéressés que par un résultat en O, il est possible soit de majorer
la fonction recherchée soit d’utiliser des théorèmes qui pour certaines formes
génériques d’équations récurrentes fournissent directement le résultat sans avoir
à passer par le calcul exact de la solution de l’équation. C’est ce que nous nous
efforçons de montrer dans cette section en nous penchant tout d’abord sur la
complexité de la fonction estT rie dans le cas où n est quelconque et non plus
une puissance de 2. Ensuite, nous étudierons, sans la démontrer, une forme
simplifiée du théorème dit « théorème maître » avant de l’appliquer au cas de la
fonction estT rie . Une troisième solution est évoquée ci-dessous. Elle consiste à
se fonder sur la structure de données plutôt que sur l’algorithme.

Rechercher une fonction majorante


L’équation récurrente 4.2.2, page 115, qui décrit la complexité de la fonction
estT rie lorsque n est une puissance de 2 n’est pas correcte pour un n quel-
conque. En effet, lorsque n est impair, couper le tableau t en deux parties par
une division euclidienne produit des sous-tableaux de longueurs différentes ; plus
précisément, le premier sous-tableau a une longueur de  n2 , tandis que le second
a une longueur de  n2 . Rappelons incidemment la propriété D.34 :  n2 + n2  = n.
Dans sa forme développée, l’équation récurrente 4.2.2 s’écrit maintenant :

(T (1) = 1) ∧ ∀p ·(p ∈ N1 − {1} ⇒ T (p) = T ( p2 ) + T ( p2 ) + 1)


4. Analyse d’algorithmes 121

Soit Q cette formule. Le résultat acquis pour les puissances de 2 nous suggère
de majorer T (n) par une fonction affine. Nous allons tenter de démontrer qu’il
existe des réels a et b tels que si a · n + b ∈ N → R+ alors, pour tout n de
N1 , T (n) ≤ a · n + b. Soit P (n) cette dernière formule. Nous devons démontrer
Q ⇒ P (n). Nous pouvons déplacer Q en hypothèse pour nous focaliser sur la
démonstration de la formule P (n).
Nous allons appliquer le principe de récurrence complète (théorème 2,
page 44) pour n ∈ N1 − {1} (n ≥ 2). Il nous suffit donc de démontrer P (n)
sous les hypothèses Q, n ≥ 2 et ∀m ·(m ∈ N1 − {1} ∧ m < n ⇒ [n := m]P (n)).
Cette dernière formule peut être instanciée de deux façons afin d’obtenir deux
nouvelles hypothèses intéressantes pour la démonstration :

[m :=  n2 ](m ∈ N1 − {1} ∧ m < n ⇒ [n := m]P (n))


⇔ Substitution
[m :=  n2 ](m ∈ N1 − {1} ∧ m < n ⇒ P (m))
⇔ Substitution
 n2  ∈ N1 − {1} ∧  n2  < n ⇒ P ( n2 )
⇔ Définition de P
 n2  ∈ N1 − {1} ∧  n2  < n ⇒ T ( n2 ) ≤ a · n2  + b (4.2.4)

De même, pour [m :=  n2 ], nous obtenons l’hypothèse suivante :

 n2  ∈ N1 − {1} ∧  n2  < n ⇒ T ( n2 ) ≤ a · n2  + b (4.2.5)

Nous sommes maintenant prêt à démontrer T (n) ≤ a · n + b :

T (n)
= Hypothèse [p := n]Q
T ( n2 ) + T ( n2 ) + 1
≤ Hypothèses 4.2.4 et 4.2.5
a · n2  + b + a · n2  + b + 1
= Propriété D.34
a · n + b + (b + 1)
≤ Sous réserve que b + 1 ≤ 0
a·n+b

Une possibilité consiste à choisir b = −1 et a = 2. Nous obtenons bien une


fonction de N dans R+ . Le cas n = 1 est trivial (notons qu’il ne s’agit pas d’un
cas de base, puisque l’induction complète n’en comporte pas). Nous avons donc
montré que la solution à l’équation récurrente qui décrit la complexité de la
fonction estT rie pour tout n est majorée par la fonction 2 · n − 1. Elle est donc
en O(n).

Utiliser le théorème maître


Une autre technique, qui relève de l’application d’un théorème particulier,
permet de s’affranchir de la recherche de la solution à l’équation récurrente ou
d’un majorant à cette solution : il s’agit du théorème maître (master theorem)
dont nous allons fournir (sans la démontrer) une version simplifiée.
122 Structures de données et méthodes formelles

Théorème 3 (Théorème maître). La solution à l’équation récurrente suivante :



S(1) = d
S(n) = a · S(n ÷ b) + c · nk

(où d ∈ N1 , a ∈ N1 , b ∈ N1 ∧ b ≥ 2, c ∈ N1 et k ∈ N) satisfait la propriété



⎨O(nlogb a ) Si a > bk
S(n) ∈ O(n · log n)
k
Si a = bk

O(n )k
Si a < bk

Ce théorème s’applique pour tout n et pas uniquement pour les puissances


de 2.
L’application à l’équation récurrente 4.2.2 est immédiate : a = 2, b = 2, c = 1,
et k = 0. Nous sommes donc dans le cas où a > bk (2 > 20 ). En conséquence,
T (n) ∈ O(nlog2 2 ) soit encore T (n) ∈ O(n). Nous retrouvons (facilement) le
résultat calculé en passant par une fonction majorante.

Utiliser les propriétés de la structure de données


Plutôt que d’interpréter la fonction à évaluer pour rechercher une équation
récurrente, il est parfois plus simple de dénombrer le nombre de comparaisons
qu’exige le traitement de la structure de données. Reprenons l’exemple de la
fonction estT rie. La structure de données considérée est un tableau de n élé-
ments, l’algorithme exige n − 1 comparaisons. Nous pouvons facilement conclure
que la fonction estT rie est en O(n) (et même en Θ(n)).
Cette technique, plus informelle en apparence que celles que nous avons étu-
diées ci-dessus, présente l’avantage de prendre en considération des propriétés
de la structure de données qui peuvent souvent être utilisées par plusieurs opé-
rations. Cependant, la recherche de ces propriétés est un travail de nature com-
binatoire qui conduit en général à résoudre des équations récurrentes (cf. [103]).
Elle est fréquemment utilisée dans le cas des arbres pour lesquels les principales
opérations dépendent de propriétés telles que la longueur de la branche la plus
longue, la longueur moyenne des branches, etc. Cette méthode est adoptée à
plusieurs reprises dans le reste de l’ouvrage.

4.2.5 Conclusion et remarques bibliographiques


Nous avons procédé à un tour d’horizon de l’analyse classique de la com-
plexité. Les exemples développés sont basés sur des fonctions très simples, qui
permettent cependant d’apprécier la difficulté du domaine. La démarche clas-
sique pour résoudre des équations récurrentes consiste à identifier la forme de
l’équation (équation linéaire à coefficients constants, équation de partition, etc.)
et à appliquer les méthodes spécifiques connues. Les équations récurrentes li-
néaires à coefficients constants se traitent en général assez facilement dans le
cadre théorique de l’algèbre linéaire. Pour le cas des équations de partitions, le
théorème maître présenté ci-dessus permet de résoudre de très nombreux cas
pratiques. Ce théorème maître est un cas particulier d’un théorème plus général,
démontré dans [25]. Il est lui-même un cas particulier du théorème d’Akra-Bazzi
4. Analyse d’algorithmes 123

publié en 1998. Dans [25] les auteurs présentent trois méthodes pour résoudre les
équations récurrentes. La première est appelée méthode de substitution. La sec-
tion intitulée « Rechercher une fonction majorante » ci-dessus en est une illustra-
tion. La seconde, dénommée « méthode arbre-récursion », de nature graphique,
permet d’obtenir une suggestion pour appliquer la méthode de substitution. En-
fin, le théorème maître est proposé comme troisième possibilité.
Pour le cas des équations de partition, les auteurs de l’ouvrage [17] utilisent
les notions de comportement asymptotique conditionnel et de fonctions asymp-
totiquement non décroissantes pour généraliser les résultats obtenus avec des
puissances de 2, à N tout entier.
Le lecteur souhaitant compléter son information sur le calcul de la com-
plexité moyenne peut par exemple consulter l’ouvrage [103] de R. Sedgewick et
Ph. Flajolet.

Exercices

Exercice 4.2.1 On considère les deux fonctions récursives suivantes :


function f (n) ∈ N → N =  function g(n) ∈ N → N = 
if n ≤ 1 → if n ≤ 1 →
1 et 1
|n>1 → |n>1 →
1 + f (n − 1) + g(n − 1) 2 + f (n − 1) · g(n − 1)
fi fi

Déterminer l’équation de récurrence qui exprime les complexités de f et de g. En déduire une


équation récurrente pour f .

Exercice 4.2.2 Démontrer par récurrence que la formule 4.2.1, page 114, implique ∀i ·(i ∈
0 .. n − 1 ⇒ T (n − i + 1) = T (n − i) + 1).

4.3 Analyse amortie de la complexité –


la méthode du potentiel
4.3.1 Introduction
Les complexités classiques conviennent bien au cas des algorithmes « auto-
nomes » (non liés à une structure de données). Par contre, dès que l’on cherche
à évaluer la complexité d’opérations sur une structure de données, on est fré-
quemment confronté au problème suivant : une opération (comme par exemple
une adjonction) est souvent itérée plusieurs fois consécutives sur la structure
de données ; calculer un majorant du coût d’une séquence se fait en sommant
sur un majorant de chaque opération. Il apparaît alors qu’en procédant ainsi le
résultat est très pessimiste, puisque dans la réalité les appels qui succèdent à
une importante réorganisation de la structure peuvent en tirer immédiatement
profit.
Une solution consisterait à élaborer un modèle probabiliste des données et
à procéder à un calcul classique de complexité. La modélisation est en général
124 Structures de données et méthodes formelles

difficile. Une autre solution consiste à réaliser une analyse amortie. Il existe plu-
sieurs formes d’analyse amortie [112, 113, 109, 101, 69, 93]. La plus pratiquée
est l’analyse amortie par la méthode du potentiel. Son principe est le suivant.
Plaçons-nous dans la situation où, pour un type d’opération donné, il existe des
réalisations qui sont intrinsèquement coûteuses (évaluées dans une « monnaie »
quelconque) alors qu’au contraire d’autres le sont très peu. Effectuer une opé-
ration peu coûteuse s’accompagne d’un geste qui consiste à « mettre de côté »
un certain montant (qui a pour effet d’augmenter le potentiel). Le coût amorti
est pour cette opération le coût intrinsèque de l’opération plus le montant thé-
saurisé. Inversement, il est possible, pour une opération coûteuse, de payer en
allant puiser dans la réserve (en diminuant d’autant le potentiel). Le coût amorti
est alors le coût intrinsèque moins le montant prélevé. Bien entendu, ces opé-
rations d’amortissement sont purement virtuelles, elles n’interviennent que pour
le calcul de la complexité amortie et n’ont aucune contrepartie dans le code de
l’opération.
Illustrons ce principe par l’exemple des systèmes de retraite par capitalisa-
tion. Supposons que le montant des dépenses nécessaire pour vivre une année
pour une personne active soit de 10 000 e et de 9 000 e pour un retraité. Sup-
posons par ailleurs que le revenu brut d’une personne active soit de 15 000 e et
qu’un retraité n’ait aucun revenu. Supposons enfin que la vie active dure 40 ans
et la retraite 20 ans. La personne pourra « mettre de côté » 5 000 e chaque année
en prévision de son départ en retraite. Le tableau 4.3 montre dans la colonne di
le coût réel de chacune des 60 années. La colonne gi fournit les gains annuels.
Les colonnes di et gi cumulent respectivement les valeurs de di et de gi sur
les 60 années.

Tableau 4.3 – Exemple de tableau d’amortissement.


Ce tableau montre comment les gains bruts (colonne gi ) d’un salarié
sur 40 ans lui permettent de vivre sa retraite pendant 20 ans, compte
tenu des dépenses annuelles représentées dans la colonne di .


Année di gi di gi
1 10 000 15 000 10 000 15 000
2 10 000 15 000 20 000 30 000
··· ··· ··· ···
40 10 000 15 000 400 000 600 000
41 9 000 0 409 000 600 000
··· ··· ··· ···
60 9 000 0 580 000 600 000

Le système fonctionne à condition que, pour tout n ∈ 1 .. 60, la somme des


gains soit supérieure ou égale à la somme des dépenses, soit :

n
n
gi ≥ di (4.3.1)
i=1 i=1
4. Analyse d’algorithmes 125

Par contre, il n’est pas nécessaire que pour toutes les années gi ≥ di : la personne
peut supporter une courte période de chômage pendant sa vie active à condition
que la formule 4.3.1 soit respectée.
En général, même dans le cas des retraites par capitalisation, le système est
géré par des « caisses de retraite ». Celles-ci y superposent d’ailleurs une forme
complémentaire d’amortissement afin que les centenaires puissent vivre grâce
aux cotisations des personnes qui décèdent précocement.

4.3.2 Fondements théoriques de la méthode du potentiel


Nous définissons tout d’abord les deux notions de fonction de potentiel et de
coût amorti avant de fournir une propriété du coût amorti.

Définition 4 (Fonction de potentiel). Soit un type T de support t. Soit Φ ∈


t → R+ , soit s0 la configuration initiale de la structure de données et sj une
configuration atteignable quelconque, alors Φ est une fonction de potentiel si et
seulement si Φ(s0 ) = 0 et Φ(sj ) ≥ 0.

Définition 5 (Coût amorti selon Φ). Soit Φ une fonction de potentiel pour le
type T , soit oi un appel à une opération interne o de T , soit T (oi ) le coût réel de
l’opération oi et soit si−1 (resp. si ) la configuration de la structure de données
avant (resp. après) l’appel, alors

M(oi ) = T (oi ) + Φ(si ) − Φ(si−1 ) pour i > 0 (4.3.2)

est le coût amorti selon Φ de l’opération oi .

Il est facile d’en conclure par sommation que :


k k
i=1 M(oi ) = i=1 T (oi ) + Φ(sk ).

D’où, puisque Φ(sk ) ≥ 0, la propriété fondamentale suivante du coût amorti :

Propriété 2 (du coût amorti).


k k
i=1 M(oi ) ≥ i=1 T (oi ) (4.3.3)

Cette formule montre donc que le coût réel des k premières opérations est majoré
par le coût amorti de ces k opérations. Il est important de noter que cette formule
ne peut en général s’appliquer à une séquence quelconque d’appels. En effet, les
préconditions des opérations doivent être respectées afin par exemple d’interdire
de retirer une valeur d’un ensemble vide. En général on évalue le coût amorti
d’une séquence valide de n opérations à partir du coût amorti de chaque type
d’opération. Le résultat constitue, d’après la formule 4.3.3, une borne supérieure
au coût réel de la séquence.
Le choix de la fonction de potentiel Φ est arbitraire, à condition de respecter
les propriétés requises. On peut alors calculer pour chaque opération (éventuel-
lement conditionnée par le contexte de l’appel) son coût amorti. Bien que le
résultat obtenu soit, selon la formule 4.3.3, un majorant du coût réel, il dépend
en général de la fonction de potentiel retenu : toutes les fonctions de potentiel
ne sont pas égales devant le résultat obtenu. Un coût amorti ajusté au mieux
126 Structures de données et méthodes formelles

s’obtient par un choix pertinent pour la fonction Φ. Alors que dans les exemples
historiques (cf. [109, 110]) ce choix résulte d’une certaine expérience doublée
d’une bonne intuition, on voit aujourd’hui émerger une approche, plus raison-
nable sur un plan scientifique, dans laquelle la fonction de potentiel est dérivée
conjointement avec le calcul du programme (cf. [69, 101]).
Une difficulté technique surgit parfois dans la définition de la fonction Φ.
Elle est liée au fait que l’opération sur laquelle porte l’analyse amortie n’est
pas une opération purement interne au type considéré. Ainsi par exemple si
nous travaillons sur des arbres, l’opération peut fusionner deux arbres pour n’en
former qu’un seul. Une telle opération (appelons-la f us), définie comme une
fonction, aurait le profil suivant :

f us(a, a ) ∈ arbre × arbre → arbre

Dans ce cas, il faut en principe plusieurs fonctions de potentiel, une pour chaque
type de structure de données considérée. Pour l’exemple ci-dessus nous aurions
Φ pour les arbres :

Φ(a) ∈ arbre → R+

et Φ pour les couples d’arbres :

Φ (a, a ) ∈ arbre × arbre → R+

La fonction M se définit alors par

M(f us(a, a )) = T (f us(a, a )) + Φ(f us(a, a )) − Φ (a, a )

Dans la pratique, lorsqu’il n’y a pas ambiguïté, et par abus de langage, nous
assimilons les Φ et Φ en une seule fonction.

4.3.3 Exemple
Cet exemple est pris dans le cadre de la vie quotidienne afin de montrer
la simplicité des concepts sous-jacents et avant de les appliquer à des cas plus
complexes (cf. pages 135, 251, 322, 351 et 368). Un détaillant en vins souhaite
évaluer le coût amorti de son activité. Celle-ci se décrit comme suit.
– Un employé travaillant à la cave remplit des bouteilles une à une (à partir
d’une citerne) avant de les stocker dans des porte-bouteilles d’une capacité
de six bouteilles. Cette opération d’embouteillement, dénommée emb, a un
coût réel de 1 (pour une bouteille).
– Au rez-de-chaussée, un porte-bouteilles est disponible pour la vente au
détail. Dès que le porte-bouteilles est vide, le vendeur va en chercher un
nouveau à la cave. Cette opération, dénommée vente, s’évalue, dans sa
version standard, à un coût réel de 1. Par contre, lorsqu’il faut descendre
chercher un nouveau porte-bouteilles plein, le coût se décompose en 1 (coût
réel de la vente) + 6 (coût du déplacement à la cave). Cette opération est
munie d’une précondition qui stipule qu’il reste des porte-bouteilles pleins
à la cave et que le porte-bouteilles du rez-de-chaussée est vide.
4. Analyse d’algorithmes 127

Le commerçant décide de choisir comme fonction de potentiel Φ(si ) le nombre


Bi de bouteilles pleines disponibles (en porte-bouteilles) à la cave. Cette fonction
satisfait les conditions requises : initialement, il y a 0 bouteille disponible et le
nombre de bouteilles disponibles n’est jamais négatif.
Le coût amorti M(embi ) d’une opération d’embouteillement est :

M(embi )
=
1+ Coût réel du remplissage
Φ(si ) − Φ(si−1 ) Amortissement
= Définition de Φ et arithmétique
1 + Bi − Bi−1
= Bi = Bi−1 + 1
1 + Bi−1 + 1 − Bi−1
= Arithmétique
2

Le coût amorti M(ventei ) d’une vente doit considérer les deux cas possibles.
Pour le cas standard nous avons :

M(ventei )
=
1+ Coût réel de la vente
Φ(si ) − Φ(si−1 ) Amortissement
= Définition de Φ et arithmétique
1 + Bi − Bi−1
= Bi = Bi−1
1 + Bi−1 − Bi−1
= Arithmétique
1

Par contre, le coût amorti d’une vente exigeant de descendre à la cave est :

M(ventei )
=
1+ Coût réel de la vente
6+ Coût réel du déplacement
Φ(si ) − Φ(si−1 ) Amortissement
= Définition de Φ et arithmétique
7 + Bi − Bi−1
= Bi = Bi−1 − 6
7 + Bi−1 − 6 − Bi−1
= Arithmétique
1

Au total, toute séquence valide de n opérations embi ou ventei a un coût


amorti inférieur ou égal à 2 · n (compte tenu du choix de la fonction de potentiel
Φ). Ces deux opérations ont un coût amorti constant. Si le commerçant avait
voulu évaluer le coût traditionnel d’une opération vente, il aurait dû majorer le
128 Structures de données et méthodes formelles

coût d’une vente par son coût au pire, soit 7. Il aurait alors obtenu un résultat
plus pessimiste que nécessaire. La fonction de potentiel Φ a été judicieusement
choisie pour obtenir le même coût dans les deux situations à envisager pour
l’opération vente. Il est bien sûr possible de faire des choix différents (cf. exer-
cice 4.3.2).

4.3.4 Conclusion et remarques bibliographiques


Historiquement, la notion d’analyse amortie est associée à celle de structures
autoadaptatives. Elles ont été introduites principalement par R.E. Tarjan au
tournant des années 1980. À l’issue d’un troisième cycle à Standford University,
sous la direction de J. Hopcroft, R.E. Tarjan a soutenu son phd en 1971. Le
sujet portait sur la planarité des graphes. Il a ensuite été enseignant-chercheur
à Cornell University, à Berkeley, à Standford puis chercheur aux laboratoires
at&t (tout en poursuivant en parallèle des activités d’enseignement à New-York
University). Il a obtenu, conjointement avec J. Hopcroft, la distinction Turing
Award en 1986 pour leurs contributions à la conception et l’analyse d’algorithmes
et de structures de données.
Ses travaux sur les algorithmes union-find (qui sont des algorithmes de main-
tenance d’une structure de partition sur des ensembles évolutifs) puis sur l’opti-
misation des flots dans les graphes l’ont conduit à l’idée d’analyse amortie de la
complexité.
Ainsi que nous l’avons écrit plus haut, il existe plusieurs façons de faire de
l’analyse amortie (cf. [113, 25]). La plus pratiquée est sans conteste la méthode
du potentiel, que nous avons utilisée ci-dessus. Elle est attribuée par R.E. Tarjan
à D.D. Sleator (cf. [113], page 308).
L’encadré de la page 324 explique pourquoi il faut être prudent lorsque l’on
utilise des structures de données persistantes dans un contexte d’analyse amortie.

Exercices

Exercice 4.3.1 Après l’installation de votre nouvelle chaudière, votre installateur vous propose
le contrat de maintenance annuel suivant (J pour janvier, F pour février, etc.) :
J F M A M J J A S O N D
0e 0e 100 e 0e 0e 60 e 0e 0e 200 e 0e 0e 100 e
En supposant que vous recherchiez une fonction de potentiel Φ constante, qu’allez-vous choisir
selon que votre contrat commence en
1. janvier,
2. septembre,
3. avril,
si vous recherchez la plus petite fonction possible ? Vérifier votre réponse à l’aide d’un tableur.

Exercice 4.3.2 Dans l’exemple de calcul amorti de la page 126 que se passe-t-il si le commer-
çant travaille avec des porte-bouteilles d’une capacité de 2 ? de 20 bouteilles ? Proposer une
fonction de potentiel Φ différente et refaire un calcul du coût amorti de chaque opération.
Chapitre 5

Exemples

Dans ce chapitre nous proposons de développer deux exemples simples


qui vont nous permettre de mieux appréhender la démarche utilisée dans le reste
de cet ouvrage.
Ces deux exemples portent sur les entiers naturels. Le premier concerne la
représentation d’un compteur qu’il est possible d’incrémenter à partir de sa va-
leur initiale 0. Nous adoptons une représentation concrète sous la forme d’une
liste d’éléments binaires. Le second exemple vise à représenter un sous-ensemble
d’entiers. La représentation retenue est fondée sur une liste triée (sans doublon)
et l’unique opération que nous développons est l’ajout d’un élément dans le
sous-ensemble. Dans chacun des cas, les calculs de représentation sont suivis de
considérations sur la complexité des opérations.

5.1 Premier exemple


L’objectif de cet exemple est double. Il s’agit tout d’abord de travailler sur
une structure de données concrète de type liste (cf. section 3.1, page 78) et
de calculer des opérations concrètes non triviales. Par ailleurs, et c’est le second
objectif, cette structure de données nous donne l’occasion d’effectuer une analyse
de complexité amortie tout en montrant l’intérêt de cette technique.
La structure abstraite qui nous intéresse est celle de compteur. Les deux
opérations abstraites retenues sont raz(), (« remise à zéro ») qui délivre 0 et
suc(v), qui délivre le successeur de v, soit v + 1. Le type abstrait est dénommé
compteur. La structure concrète est une liste, reflet de la représentation en
binaire de la valeur. Le type concret est appelé lb (pour liste binaire).

5.1.1 Spécification du type abstrait


Le type abstrait compteur est défini sur le support compteur (cf. figure 5.1).
130 Structures de données et méthodes formelles

abstractType compteur =  (compteur, (raz, suc), ())


uses N
support
e ∈ N ⇔ e ∈ compteur
operations
function raz() ∈ compteur =0
;
function suc(v) ∈ compteur  compteur =  v+1
end

Figure 5.1 – Spécification du type abstrait compteur.


Ce type abstrait définit la structure des entiers naturels dotée de deux
opérations, raz() qui délivre 0 et suc(v) qui délivre v + 1.

5.1.2 Définition du support concret


Nous abordons à présent la spécification du support concret du type lb qui
raffine le type compteur. Le support concret se dénomme lb, il se définit par :

1) [ ] ∈ lb
2) v ∈ {0, 1} ∧ l ∈ lb ⇒ [v | l] ∈ lb

La première formule introduit la notation pour les listes vides (cf. section 3.1,
page 78), la seconde précise que, si v est un élément binaire (0 ou 1) et l une
liste lb alors [v | l] est aussi une liste lb. Ainsi par exemple la liste k =
[0 | [1 | [1 | [ ]]]] est une liste lb. Rappelons que les listes possèdent également
une représentation linéaire externe qui permet de représenter k par [0, 1, 1].

5.1.3 Définition de la fonction d’abstraction


Toute liste lb représente en binaire un entier compteur, cependant, pour des
raisons de facilité de lecture, la représentation se lit de droite à gauche. La liste
l ci-dessus est donc la représentation de la valeur 0 · 20 + 1 · 21 + 1 · 22 , soit 6.
Formellement, cette conversion est définie par la fonction d’abstraction suivante :

function A(l) ∈ lb  compteur =



if l = [ ] →
0
| l = [t | q] →
t + 2 · A(q)

Cette fonction n’est pas bijective (cf. exercice 5.1.2).


5. Exemples 131

5.1.4 Spécification formelle des opérations


Rappelons qu’il s’agit ici, pour chacune des opérations concrètes, de spécifier
ce qu’elle représente du point de vue du type abstrait. Les opérations de lb
se dénomment raz_l() et suc_l(v), homologues respectivement de raz() et de
suc(v). La première, raz_l(), se spécifie par :

function raz_l() ∈ lb =
 l : (l ∈ lb ∧ A(l) = raz())

Cette spécification se paraphrase de la manière suivante. Toute invocation de


l’opération concrète raz_l() délivre une liste lb, c’est le sens du symbole ∈. La
formule à droite du symbole de définition =  nous apprend que la valeur l délivrée
par raz_l() est une liste lb telle que sa conversion par la fonction d’abstraction
A fournit le même résultat que l’appel de l’opération abstraite raz().
Il faut se garder de spécifier la fonction raz_l() de la manière suivante :

function raz_l() ∈ lb =
[]

et ceci pour différentes raisons qui justifient l’intérêt de la démarche utilisée ici :
– en procédant de cette façon, nous n’exprimerions pas ce qu’est raz_l() en
terme d’entiers naturels ;
– dans des cas plus complexes que celui de l’opération raz_l(), nous serions
conduit à une description inductive prématurée ;
– nous devrions prouver (puisque nous ne l’avons pas spécifié) que l’opération
raz_l() permet bien d’obtenir un résultat cohérent avec celui de l’opération
abstraite raz().
La seconde opération, suc_l(l), se spécifie par :

 k : (k ∈ lb ∧ A(k) = suc(A(l)))
function suc_l(l) ∈ lb → lb =

Cette spécification nous dit en particulier qu’un appel à la fonction suc_l(l)


délivre une entité k qui est une liste lb telle que sa conversion par la fonction
d’abstraction est égale à l’application de la fonction suc à la conversion de la
liste l.
À ce stade du développement, il est possible de fournir une description com-
plète du type spécifié lb. C’est ce qui est présenté à la figure 5.2, page 133.

5.1.5 Calcul de la représentation des opérations


Calcul d’une représentation de l’opération raz_l
Il s’agit ici, en partant de la spécification de l’opération concrète raz_l d’en
calculer une représentation fonctionnelle.

A(raz_l())
= Propriété caractéristique de l’opération raz_l
0
= Définition de A
A([ ])
132 Structures de données et méthodes formelles

Structures de données et systèmes de numération


À plusieurs occasions il est fait allusion ici à l’analogie qui peut exister
entre certains systèmes de numération et certaines structures de don-
nées (les files binomiales, section 9.4 ou encore les files gloutonnes, exer-
cice 10.4.7). Représenter une structure de données de n éléments se fait
en décomposant n dans la base considérée. Ainsi, pour représenter une
file de priorité de 13 éléments par une file binomiale, on décompose 13
en base 2 (soit (1101)2 ) et on construit une file contenant successivement
un arbre de 8 éléments, de 4 éléments et de 1 élément. Les opérations
de mise à jour s’assimilent alors à des additions ou à des soustractions
dans la base en question. Ce principe, appliqué à d’autres systèmes de
numération, peut permettre de systématiser la recherche de structures de
données équilibrées efficaces.
Un système de numération de position se définit par la donnée
d’un ensemble B fini de chiffres et d’un ensemble W de poids (W =
{w0 , w1 , w2 ,
. . .}) tels que tout entier n s’écrit de manière unique sous la
m
forme n = i=0 bi · wi , où bi ∈ B. La suite bm bm−1 . . . b0 est la repré-
sentation de n dans le système considéré. Pour la numération de position
binaire nous avons B = {0, 1} et W = {20 , 21 , 22 , . . .}. Un autre exemple,
le système de numération de Fibonacci, diffère du précédent par les va-
leurs de l’ensemble W , qui sont prises sur les nombres fi de la suite de
Fibonacci. 12 s’écrit alors (1010100), mais peut aussi s’écrire (10001110).
L’unicité de représentation est obtenue en normalisant. Pour le système
de Fibonacci, on impose cm = 1 (on ne fait pas intervenir de 0 non si-
gnificatifs) et ci · ci+1 = 0 (on interdit donc la présence de deux termes
consécutifs de la suite dans la représentation). Il existe de nombreux autres
systèmes de numération : Avizienis, factorielle, fractions continues, Mer-
senne, etc. (cf. [37]).
Le problème qui se pose alors au concepteur de structures de données
est de choisir ou de découvrir des arbres appropriés, c’est-à-dire des ar-
bres tels que la fusion de deux arbres de poids wi donne un arbre de poids
wi+1 .
Pour le système de numération binaire, trois types d’arbre sont candi-
dats : les arbres externes pleins, les arbres binomiaux et les arbres guidons.
Ces derniers sont des arbres pleins (qui possèdent donc 2m−1 nœuds) en-
racinés sur une valeur (soit au total 2m−1 +1 = 2m nœuds). Ces arbres ont


l’aspect suivant : • • . L’arbre de poids 4 a la forme d’un guidon.
•• ••
Dans [93], C. Okasaki passe en revue plusieurs structures de données
fondées sur les principes exposés ci-dessus.
5. Exemples 133

concreteType lb =  (lb, (raz_l, suc_l), ())


refines compteur
support
1) [ ] ∈ lb
2) v ∈ {0, 1} ∧ l ∈ lb ⇒ [v | l] ∈ lb
abstractionFunction
function A(l) ∈ lb  compteur =  ...
operationSpecifications
function raz_l() ∈ lb =  l : (l ∈ lb ∧ A(l) = raz())
;
function suc_l(l) ∈ lb → lb =  k : (k ∈ lb ∧ A(k) = suc(A(l)))
end

Figure 5.2 – Spécification du type concret lb.


Cette spécification raffine le type compteur présenté à la figure 5.1.
Elle est fondée sur un support qui est une liste d’éléments binaires. La
fonction d’abstraction A montre comment une liste se convertit dans un
entier naturel. La rubrique operationSpecifications spécifie les deux
opérations en mettant en évidence la relation qu’elles entretiennent avec
leurs homologues abstraits.

Nous pouvons alors appliquer la propriété de l’équation à membres identiques


(page 67) pour affirmer qu’une solution à l’équation A(raz_l()) = A([ ])
(équation en raz_l()) est [ ].
Au total nous avons donc calculé la représentation suivante pour l’opération
raz_l() :

function raz_l() ∈ lb =
[]

L’opération raz_l() délivre simplement une liste vide. Sa complexité est trivia-
lement en O(1).

Calcul d’une représentation de l’opération suc_l


Nous recherchons une représentation pour l’opération concrète suc_l(l).
Après la phase traditionnelle de passage par la « propriété caractéristique »
(cf. section 2.3), nous devons effectuer un raisonnement par induction dont la
partie inductive fait elle-même appel à une analyse par cas.

A(suc_l(l))
= Propriété caractéristique de l’opération suc_l
1 + A(l) (5.1.1)

Nous procédons à une induction structurelle sur l en commençant par le cas de


base l = [ ] :
134 Structures de données et méthodes formelles

1 + A(l)
= Hypothèse
1 + A([ ])
= Définition de A
1+0
= Arithmétique
1+2·0
= Définition de A
1 + 2 · A([ ])
= Définition de A
A([1 | [ ]])

Nous appliquons la propriété de l’équation à membres identiques (page 67) pour


proposer la première équation gardée pour l’opération suc_l :

l=[]→
suc_l(l) = [1 | [ ]]

Concernant le cas inductif, nous pouvons faire l’hypothèse que l = [v | q]. En


repartant de la formule 5.1.1 nous avons :

1 + A(l)
= Hypothèse
1 + A([v | q])
= Définition de A
1 + v + 2 · A(q) (5.1.2)

La suite du développement dépend de la valeur de v. Nous effectuons une analyse


par cas, selon que v = 0 ou que v = 1. Débutons par v = 0 :

1 + v + 2 · A(q)
= Hypothèse
1 + 0 + 2 · A(q)
= Arithmétique
1 + 2 · A(q)
= Définition de A
A([1 | q])

D’où, d’après la propriété de l’équation à membres identiques (page 67), l’équa-


tion gardée suivante :

l = [v | q] →
v=0→
suc_l(l) = [1 | q]

Pour le cas v = 1, le développement repart de la formule 5.1.2 et se poursuit


par :
5. Exemples 135

1 + v + 2 · A(q)
= Hypothèse
1 + 1 + 2 · A(q)
= Arithmétique
0 + 2 ·(A(q) + 1)
= Propriété caractéristique de l’opération suc_l
0 + 2 · A(suc_l(q))
= Définition de A
A([0 | suc_l(q)])

D’où, d’après la propriété de l’équation à membres identiques (page 67), la troi-


sième équation gardée :

l = [v | q] →
v=1→
suc_l(l) = [0 | suc_l(q)]

Au total, nous avons calculé la version suivante pour l’opération suc_l :

function suc_l(l) ∈ lb → lb =

if l = [ ] →
[1 | [ ]]
| l = [v | q] →
if v = 0 →
[1 | q]
|v=1 →
[0 | suc_l(q)]

5.1.6 Complexités de l’opération suc_l


Dans cette section nous cherchons à évaluer la complexité de l’opération
suc_l. Nous allons tout d’abord nous préoccuper de la complexité au pire puis
tenter d’effectuer une analyse amortie. Le critère choisi pour évaluer le coût est
le nombre d’appels à la fonction suc_l, pour un argument l donné.
La structure de la représentation de la fonction suc_l calculée ci-dessus nous
permet de fournir l’équation récurrente suivante pour le coût « réel » T (suc_l(l))
d’un appel :

⎨ T (suc_l([ ])) = 1
T (suc_l([0 | q])) = 1

T (suc_l([1 | q])) = 1 + T (suc_l(q))

Il est alors facile de majorer T par la fonction T  définie en ne retenant que la


première et la troisième équation ci-dessus :
 
T (suc_l([ ])) = 1
T  (suc_l([v | q])) = 1 + T  (suc_l(q))
136 Structures de données et méthodes formelles

Nous obtenons la forme close suivante pour l = [ ] : T  (suc_l(l)) = #(l) + 1


(rappel : #(l) est la fonction qui délivre la longueur de la liste). D’où nous tirons :

T (suc_l(l)) ∈ O(#(l))

qui nous permet d’affirmer que la complexité au pire de l’opération suc_l de


calcul de la représentation du successeur se comporte asymptotiquement comme
le nombre d’éléments de la liste.
Intuitivement nous ressentons que la majoration de T par T  est très péna-
lisante. Observons le tableau 5.1 ci-dessous qui, pour chacune des 18 premières
configurations de la liste, fournit (i) la valeur représentée (colonne A(l)), (ii) le
nombre d’appels à la fonction suc_l pour passer de la configuration précédente
de l à la configuration courante (colonne r(l)), (iii) le nombre d’appels cumulés
à la fonction suc_l depuis l’initialisation (colonne c(l)), (iv) le nombre moyen
d’appels (valeur tronquée), c’est-à-dire c(l)/A(l) (colonne m(l)). Nous consta-
tons (colonne c(l)) que le coût cumulé des opérations suc_l effectuées depuis
la situation initiale [ ] ne dépasse jamais la valeur 2 · A(l) : il faut par exemple
31 appels à la fonction pour faire passer la liste de la configuration initiale à la
configuration [0, 0, 0, 0, 1] qui représente la valeur 16.

Tableau 5.1 – Compteurs par listes binaires : estimation des coûts.


La colonne dénommée l montre comment évolue une liste initialement
vide par appels successifs à la fonction suc_l(l). La colonne suivante
fournit la valeur représentée par la configuration de l qui lui correspond.
La colonne r(l) quantifie le nombre d’appels à la fonction suc_l pour
passer de la configuration précédente de l à la configuration courante.
La colonne c(l) cumule les coûts individuels de la colonne r(l). Enfin la
colonne m(l) délivre la valeur approximative de c(l)/A(l).

l A(l) r(l) c(l) m(l) l A(l) r(l) c(l) m(l)


[] 0 0 [1, 0, 0, 1] 9 1 16 1,78
[1] 1 1 1 1,00 [0, 1, 0, 1] 10 2 18 1,80
[0, 1] 2 2 3 1,50 [1, 1, 0, 1] 11 1 19 1,73
[1, 1] 3 1 4 1,33 [0, 0, 1, 1] 12 3 22 1,83
[0, 0, 1] 4 3 7 1,75 [1, 0, 1, 1] 13 1 23 1,77
[1, 0, 1] 5 1 8 1,60 [0, 1, 1, 1] 14 2 25 1,79
[0, 1, 1] 6 2 10 1,67 [1, 1, 1, 1] 15 1 26 1,73
[1, 1, 1] 7 1 11 1,57 [0, 0, 0, 0, 1] 16 5 31 1,94
[0, 0, 0, 1] 8 4 15 1,88 [1, 0, 0, 0, 1] 17 1 32 1,88

Si, comme le laisse penser la courbe de la figure 5.3, page ci-contre, cette
règle se généralise, cela signifie qu’il doit être possible de mettre en évidence une
complexité amortie inférieure ou égale à 2, qui serait donc en O(1). C’est ce que
nous allons tenter de démontrer à présent.
5. Exemples 137

m(l)
2

A(l)
32 64 96 128

Figure 5.3 – Nombre moyen d’appels à la fonction suc_l.


Cette courbe fournit le nombre moyen d’appels à l’opération suc_l en
fonction de la valeur A(l) représentée par la liste. Elle semble présenter
une asymptote pour m(l) = 2.

Ainsi que nous l’avons vu à la section 4.3, effectuer une analyse amortie
nécessite de choisir une fonction de potentiel définie sur le type considéré et à
valeurs dans R+ . Nous nous décidons pour la fonction qui délivre le nombre de
1 présents dans la liste en argument. Cette fonction se définit par la récurrence
suivante :

Φ([ ]) = 0
Φ([t | q]) = t + Φ(q)

La formule de la complexité amortie (formule 4.3.2, page 125) s’instancie de la


manière suivante pour l’opération suc_l :

M(suc_l(l)) = T (suc_l(l)) + Φ(suc_l(l)) − Φ(l)

Elle se résout par induction sur la structure de l et, pour la partie inductive, par
une analyse par cas. Débutons par le cas de base l = [ ] :

M(suc_l([ ]))
= Définition de M(suc_l(l))
T (suc_l([ ])) + Φ(suc_l([ ])) − Φ([ ])
= Définition de T et représentation de suc_l
1 + Φ([1 | [ ]]) − Φ([ ])
= Définition de Φ
1 + 1 + Φ([ ]) − Φ([ ])
= Définition de Φ et arithmétique
2

Pour la partie inductive (l = [t | q]), nous distinguons les cas t = 0 et t = 1 ;


nous débutons par t = 0 :

M(suc_l([0 | q]))
= Définition de M(suc_l(l))
T (suc_l([0 | q])) + Φ(suc_l([0 | q])) − Φ([0 | q])
= Définition de T et représentation de suc_l
138 Structures de données et méthodes formelles

1 + Φ([1 | q]) − Φ([0 | q])


= Définition de Φ
1 + 1 + Φ(q) − Φ(q)
= Arithmétique
2

Enfin, le cas t = 1 se traite comme suit :

M(suc_l([1 | q]))
= Définition de M(suc_l(l))
T (suc_l([1 | q])) + Φ(suc_l([1 | q])) − Φ([1 | q])
= Définition de T et de suc_l
1 + T (suc_l(q)) + Φ([0 | suc_l(q)]) − Φ([1 | q])
= Définition de Φ
1 + T (suc_l(q)) + Φ(suc_l(q)) − Φ(q) − 1
= Calcul sur R+
T (suc_l(q)) + Φ(suc_l(q)) − Φ(q)
= Définition de M
M(suc_l(q))

Au total, nous avons calculé la version récurrente suivante de M(suc_l(l)) :



⎨ M(suc_l([ ])) = 2
M(suc_l([0 | q])) = 2

M(suc_l([1 | q])) = M(suc_l(q))

dont la solution immédiate est M(suc_l(l)) = 2. Nous avons donc le résultat


suivant pour l’analyse amortie de la complexité de l’opération suc_l selon la
fonction de potentiel Φ :

M(suc_l(l)) est en O(1)

En terme d’analyse amortie, l’opération suc_l est donc en temps constant.

5.1.7 Conclusion et remarques bibliographiques


Cet exemple nous a permis de nous familiariser avec une structure de don-
nées importante : les listes. Par ailleurs, il constitue un cas d’étude typique
de l’analyse amortie. Il est également utilisé dans ce but par T. Cormen et al.
dans [25]. Notre approche est cependant différente (le support concret est une
liste et nous calculons les opérations) et s’inspire plus de celle d’A. Kaldewaij
et B. Schoenmakers dans [69]. Ces derniers auteurs l’utilisent également comme
exemple introductif dans un article portant sur la dérivation de fonctions de
potentiel.
5. Exemples 139

Exercices

Exercice 5.1.1 Nous considérons le type de base N doté de l’opérateur +. Raffiner ce type
abstrait par une structure de données représentant la liste des chiffres décimaux significatifs.

Exercice 5.1.2 Dans l’exemple ci-dessus, l’ensemble lo des listes qu’il est possible d’obtenir à
partir des deux opérations raz_l et suc_l est strictement inclus dans l’ensemble lb des listes
décrites par le support. En effet, ces opérations n’engendrent pas de 0 non significatifs.
1. Donner un exemple d’une liste appartenant à lb mais pas à lo.
2. La fonction A n’est pas injective. Pourquoi ? Modifier le support lb afin qu’elle le soit.

Exercice 5.1.3 On considère le type abstrait natbool destiné à représenter des booléens. Le
support est constitué des deux entiers 0 et 1, et les opérations possibles sont non et et. On veut
implanter ce type abstrait en utilisant le support {V, F }. Proposer une spécification abstraite,
une spécification concrète et calculer les opérations concrètes.

Exercice 5.1.4 Définir un type abstrait permettant l’addition d’entiers. Mettre en œuvre ce
type abstrait par des listes binaires. Effectuer les calculs de complexité (classique et amortie)
de votre solution.

5.2 Second exemple


Dans cet exemple, nous souhaitons raffiner le type abstrait ensnat (qui per-
met de représenter des sous-ensembles finis d’entiers naturels définis en exten-
sion) par des listes triées. Nous limitons le jeu d’opérations à eV ide() qui délivre
un sous-ensemble vide et à eAjout(v, e) qui introduit l’entier v dans le sous-
ensemble e. Nous focalisons notre attention sur l’opération eAjout.

5.2.1 Spécification du type abstrait ensnat


Le type abstrait ensnat est défini sur le support ensN at. Celui-ci s’identifie
à l’ensemble des parties finies de N. En principe, cet exemple aurait sa place au
début du chapitre 6 consacré aux ensembles finis de scalaires, cependant ledit
chapitre est réservé à des mises en œuvre plus élaborées.

5.2.2 Définition du support concret


Nous abordons à présent la spécification du type concret dénommé lt (liste
triée) qui raffine le type ensnat. Le support lt se définit par :

1) [ ] ∈ lt
2) v ∈ N ⇒ [v | [ ]] ∈ lt
3) v ∈ N ∧ [w | q] ∈ lt ∧ v < w ⇒ [v | [w | q]] ∈ lt

La première formule introduit la notation pour les listes « vides ». La seconde


formule précise qu’une liste ne comportant qu’un seul élément est une liste lt.
La troisième formule affirme que la liste obtenue en plaçant en tête d’une liste
lt non vide [w | q] une valeur v plus petite que w est une liste lt.
140 Structures de données et méthodes formelles

abstractType ensnat =  (ensN at, (eV ide, eAjout), ())


uses N
support
e ∈ F(N) ⇔ e ∈ ensN at
operations
function eV ide() ∈ ensN at =
∅
;
function eAjout(v, e) ∈ N × ensN at → ensN at =  {v} ∪ e
end

Figure 5.4 – Spécification du type abstrait ensnat.


Spécification de sous-ensembles d’entiers naturels définis en extension.
Seules les deux opérations fondamentales eV ide et eAjout sont définies.

5.2.3 Définition de la fonction d’abstraction


Cette fonction se définit de manière inductive. Puisque nous réalisons un
raffinement de ensN at par lt, A a comme domaine le support lt défini ci-dessus
et comme codomaine ensN at. C’est une fonction surjective (tout élément de
ensN at doit pouvoir être représenté), totale (tout élément de lt représente un
ensemble) et injective (à un élément donné de ensN at ne correspond qu’une
seule liste possible). Il s’agit donc d’une bijection.

function A(l) ∈ lt 
 entN at =

if l = [ ] →

| l = [v | q] →
{v} ∪ A(q)

5.2.4 Spécification des opérations concrètes


Les identificateurs des opérations concrètes sont suffixés par _l. La spécifica-
tion des opérations se fait comme d’habitude en exploitant le fait que la fonction
d’abstraction est un homomorphisme entre le type concret et le type abstrait. À
ce stade du développement, il est possible de fournir une spécification complète
du type lt. C’est ce qui est présenté à la figure 5.5, page ci-contre.

5.2.5 Calcul d’une représentation de l’opération eAjout_l


Il s’agit ici, en partant de la spécification de l’opération concrète eAjout_l,
d’en calculer une représentation fonctionnelle.

A(eAjout_l(v, l))
= Propriété caractéristique de l’opération eAjout_l
{v} ∪ A(l) (5.2.1)
5. Exemples 141

concreteType lt =  (lt, (eV ide_l, eAjout_l), ())


uses N
refines ensnat
support
1) [ ] ∈ lt
2) v ∈ N ⇒ [v | [ ]] ∈ lt
3) v ∈ N ∧ [w | q] ∈ lt ∧ v < w ⇒ [v | [w | q]] ∈ lt
abstractionFunction
function A(l) ∈ lt   entN at =  ...
operations
function eV ide_l() ∈ lt =  e : (e ∈ lt ∧ A(e) = eV ide())
;
function eAjout_l(v, l) ∈ N × lt → lt = 
f : (f ∈ lt ∧ A(f ) = eAjout(v, A(l)))
end

Figure 5.5 – Spécification du type concret lt.


Il s’agit d’une spécification concrète du type ensnat de la figure 5.4,
page ci-contre. Le support est une liste triée de naturels. La fonction
d’abstraction est détaillée page ci-contre.

À ce stade, nous ne pouvons éviter un raisonnement par induction sur la structure


de l. Nous considérons successivement l = [ ] (pour l’étape de base) et l = [w | q]
(étape inductive). Débutons par l’étape de base :
{v} ∪ A(l)
= Hypothèse
{v} ∪ A([ ])
= Définition de A
A([v | [ ]])
D’où la première équation gardée, par application de la propriété de l’équation
à membres identiques (page 67) :
l=[] →
eAjout_l(v, l) = [v | [ ]]
L’étape inductive suppose que l = [w | q]. Nous poursuivons à partir de la
formule 5.2.1 :
{v} ∪ A(l)
= Hypothèse
{v} ∪ A([w | q]) (5.2.2)
= Définition de A
{v} ∪ {w} ∪ A(q) (5.2.3)

À ce stade du développement, progresser exige d’effectuer une analyse par cas,


selon que v = w, que v < w ou que v > w. Débutons par le cas v = w en
reprenant la formule 5.2.3 :
142 Structures de données et méthodes formelles

{v} ∪ {w} ∪ A(q)


= Hypothèse et propriété A.9
{w} ∪ A(q)
= Définition de A
A([w | q])
= Hypothèse
A(l)

Nous obtenons l’équation gardée :

l = [w | q] →
v=w →
eAjout_l(v, l) = l

Pour le cas v < w, nous repartons de la formule 5.2.2 :

{v} ∪ A([w | q])


= Définition de A
A([v | [w | q]])

Notons que l’application de la définition de A n’est possible que parce que v < w.
Nous obtenons l’équation gardée :

l = [w | q] →
v<w →
eAjout_l(v, l) = [v | [w | q]]

Le cas v > w se traite également en repartant de la formule 5.2.3 :

{v} ∪ {w} ∪ A(q)


= Commutativité de ∪
{w} ∪ {v} ∪ A(q)

Arrivé à ce stade du développement, il convient de noter que, certes, nous sa-


vons que v > w, mais nous ne savons rien de la position de v par rapport aux
(éventuels) éléments de q. Il est donc hors de question d’appliquer la définition
de A pour remplacer {v} ∪ A(q) par A([v | q]). Par contre, nous remarquons
que nous pouvons utiliser la propriété caractéristique pour introduire l’opération
eAjout_l :

{w} ∪ {v} ∪ A(q)


= Propriété caractéristique de l’opération eAjout_l
{w} ∪ A(eAjout_l(v, q))

Cette fois nous avons la certitude que w est inférieur à tous les éléments de
A(eAjout_l(v, q)). Nous pouvons exploiter la définition de A :

{w} ∪ A(eAjout_l(v, q))


= Définition de A
A([w | eAjout_l(v, q)])
5. Exemples 143

D’où l’équation gardée :

l = [w | q] →
v>w →
eAjout_l(v, l) = [w | eAjout_l(v, q)]

En rassemblant les quatre équations gardées calculées ci-dessus, nous obte-


nons la représentation suivante de l’opération eAjout_l :

function eAjout_l(v, l) ∈ N × lt → lt =

if l = [ ] →
[v | [ ]]
| l = [w | q] →
if v = w →
l
|v<w →
[v | [w | q]]
|v>w →
[w | eAjout_l(v, q)]

Concernant la complexité au pire, il est clair que l’ajout d’une valeur dans
une liste de n éléments peut conduire à traverser la totalité de la liste. Nous
avons donc une opération qui est au pire en O(n).

5.2.6 Conclusion
Cet exemple nous a permis de renforcer notre savoir-faire sur les listes. Il
nous a également fait toucher du doigt les limites des structures à base de listes,
en particulier en termes d’efficacité, mais il nous a fait acquérir des réflexes de
développement qui s’appliqueront sur des structures plus complexes comme les
arbres.

Exercices

Exercice 5.2.1 Enrichir le type abstrait ensnat en introduisant les opérations :


– eSupp(v, e) qui délivre l’ensemble e − {v}.
– eU nion(e, f ) qui délivre l’ensemble e ∪ f .
– eInter(e, f ) qui délivre l’ensemble e ∩ f .

Exercice 5.2.2 Mettre en œuvre le type abstrait de l’exercice 5.2.1 en utilisant successivement :
1. des listes triées,
2. des listes (non triées) sans doublon,
3. des listes quelconques.

Exercice 5.2.3 On considère les polynômes sur N dotés des opérations pV ide() (qui délivre
un polynôme vide), pAjout(m, p) (qui ajoute le monôme m au polynôme p), pAdd(p, q) (qui
additionne les deux polynômes p et q) et pAppli(v, p) (qui applique la valeur v à la fonction
polynôme p).
144 Structures de données et méthodes formelles

1. Spécifier le type abstrait comme un ensemble de couples c → e (coefficient/exposant),


avec c = 0.
2. Fournir une spécification concrète sous la forme d’une liste triée sur l’exposant, puis
calculer les différentes opérations.
Deuxième partie

Structures de données
fondamentales :
spécification et mises en
œuvre
Chapitre 6

Ensembles de clés scalaires

6.1 Présentation informelle


Au chapitre 1 nous avons étudié la théorie des ensembles sous son aspect
mathématique. Cet aspect ne satisfait pas complètement l’informaticien, qui a
besoin d’un « type abstrait » ensemble. C’est cette facette qui est explorée ici. Le
contexte général de ce chapitre est le suivant. Soit T un ensemble dénombrable.
Nous cherchons à représenter des sous-ensembles finis de T , dynamiques, définis
en extension. Explicitons les trois qualificatifs.
– Sous-ensembles finis de T : nous nous intéressons à des ensembles e tels
que e ∈ F(T ) où F(T ) dénote l’ensemble des parties finies de T .
– Sous-ensembles définis en extension : il s’agit du cas où les sous-ensembles
sont définis par l’énumération de leurs éléments et non par l’établissement
d’une propriété des éléments.
– Sous-ensembles dynamiques : le contenu des sous-ensembles est susceptible
d’évoluer dans le temps, par l’adjonction ou la suppression d’éléments.
Nous nous intéressons en particulier à des ensembles de scalaires (ou d’élé-
ments considérés comme tels). Certains choix de représentation nous obligeront
à nous limiter à des ensembles T dotés d’une relation d’ordre totale (notée ≤ ou
≥ dans la suite).
Le type abstrait ensabst défini ci-dessous permet de gérer l’adjonction et
la suppression d’éléments, en nommant les éléments qui sont manipulés. De
manière plus précise, nous disposons des cinq opérations suivantes :
– eV ide(), cette opération délivre un ensemble vide ;
– eAjout(v, e), cette opération délivre l’ensemble e ∪ {v} qui contient tous
les éléments déjà présents dans e plus (si v ∈
/ e) l’élément v ;
– eSupp(v, e), cette opération délivre l’ensemble contenant tous les éléments
de e moins l’élément v. Si e ne contient pas l’élément v, le résultat délivré
est e ;
– eApp(v, e), cette opération délivre true si et seulement si l’élément v est
présent dans l’ensemble e ;
– eEstV ide(e), cette opération délivre true si et seulement si l’ensemble e
est vide.
148 Structures de données et méthodes formelles

Pour ce qui concerne les opérations d’union, d’intersection et de différence,


leur mise en œuvre est en général proposée en exercice.
Quid des sous-ensembles définis en compréhension ? Un approfondissement
de ce thème – important en informatique – nous conduirait rapidement aux
notions de systèmes formels et de grammaires, dont l’étude sort du cadre de cet
ouvrage.

Pour ce qui concerne les ensembles définis en extension, en général, dans la


pratique, à chaque élément est associé un jeu de valeurs (le nom et l’âge pour un
ensemble d’individus, le prix d’achat et le prix de vente pour le code d’un pro-
duit, etc.) appelés parfois attributs. Cette association, de nature fonctionnelle,
est facile à représenter. Pour cette raison, dans la suite nous faisons systémati-
quement abstraction de ces informations secondaires tout en acceptant le terme
de clé ou d’identifiant pour désigner chaque élément de l’ensemble.

Ainsi que nous l’avons mentionné dans l’avant-propos, la référence chrono-


logique aux quatre époques des structures de données sert de fil rouge au déve-
loppement de chaque chapitre de la seconde partie. Concernant les ensembles de
clés scalaires, les trois premières mises en œuvre (arbres binaires de recherche,
hachage et arbres externes) concernent la première période, celle des pionniers.
Les deux suivantes, les Avl et les B-arbres, sont caractéristiques de la seconde
période, celle des « structures équilibrées ». La sixième mise en œuvre, par arbres
déployés, est l’archétype des structures autoadaptatives des années 1980. Enfin,
la dernière, les arbres randomisés, sert de prototype pour la quatrième période,
celle des structures aléatoires. Une autre méthode, datant de la première époque,
celle du vecteur caractéristique, aurait mérité d’être développée ici en raison de
sa simplicité. Par ailleurs, elle permet aisément de réaliser un calcul de com-
plexité moyenne. Cependant il ne s’agit pas d’une structure fonctionnelle pure.
Son principe est le suivant : pour représenter un sous-ensemble d’un intervalle
d’entiers i..s, il suffit de prendre un tableau t de booléens tel que t(k) vaut true
si et seulement si k est présent dans l’ensemble représenté par t.

6.2 Spécification du type abstrait ensabst


Le type abstrait ensabst est particulièrement simple à spécifier dans la me-
sure où nous utilisons directement les notations de la théorie des ensembles pour
définir les cinq opérations retenues. La spécification de ensabst est présentée à
la figure 6.1, page ci-contre.
eV ide() est une fonction sans argument délivrant un élément (toujours le
même : l’ensemble vide) de type ensabst(T ). eV ide() est donc une constante.
eAjout(v, e) est une fonction à deux arguments délivrant un élément du type
ensabst(T ). Il s’agit d’une fonction totale : tout élément v, qu’il soit déjà présent
ou non, peut être « ajouté » à l’ensemble e. Dans le premier cas (resp. le second),
nous parlons de fausse (resp. de vraie) insertion. Une remarque analogue peut
être réalisée pour le cas de la fonction eSupp(v, e).
6. Ensembles de clés scalaires 149

eApp(v, e) est une fonction à deux arguments délivrant un booléen. Si l’on


considère tous les couples du produit cartésien T × ensAbst(T ), pour certains
leur valeur à travers la fonction délivre true pour d’autres false. L’ensemble
bool est donc atteint dans son intégralité : c’est une fonction surjective. De
surcroît elle est totale. D’où la notation  utilisée pour préciser la nature de
cette fonction. Le constat est identique pour la fonction eEstV ide(e).

abstractType ensabst(T ) =  (ensAbst, (eV ide, eAjout, eSupp),


(eApp, eEstV ide))
uses
bool, T
support
e ∈ F(T ) ⇔ e ∈ ensAbst(T )
operations
function eV ide() ∈ ensAbst(T ) =
 ∅
;
function eAjout(v, e) ∈ T × ensAbst(T ) → ensAbst(T ) =  e ∪ {v}
;
function eSupp(v, e) ∈ T × ensAbst(T ) → ensAbst(T ) =  e − {v}
;
function eApp(v, e) ∈ T × ensAbst(T )  bool =  bool(v ∈ e)
;
function eEstV ide(e) ∈ ensAbst(T )  bool =  bool(e = ∅)
end

Figure 6.1 – Spécification du type abstrait ensabst.


Les cinq opérations spécifiées ci-dessus sont reprises plus tard dans
les mises en œuvre qui sont envisagées. Le support ensAbst est défini
comme étant un sous-ensemble fini d’éléments de T. eV ide et eAjout
sont les deux opérations à partir desquelles il est possible de construire
tout ensemble fini sur T : ce sont les constructeurs.

Exercices

Exercice 6.2.1 Fournir une définition alternative à la définition de l’opération eAjout de la


figure 6.1 ci-dessus, de façon à délivrer un second résultat par lequel l’utilisateur est informé
de l’absence ou non de la clé à insérer dans l’ensemble en argument.

Exercice 6.2.2 Enrichir le type abstrait ensabst en lui adjoignant les opérations suivantes :
– eInter qui calcule l’intersection de deux ensembles ;
– eU nion qui calcule l’union de deux ensembles ;
– eDif f qui calcule la différence entre deux ensembles ;
– eM ax qui délivre la plus grande valeur d’un ensemble non vide doté d’une structure
d’ordre total.
150 Structures de données et méthodes formelles

6.3 Méthodes arborescentes élémentaires :


les arbres binaires de recherche
La recherche dichotomique est connue pour son efficacité mais ne peut être
pratiquée sur une représentation séquentielle chaînée (une liste). Les ajouts et
suppressions, si l’on fait abstraction de la fonctionnalité de recherche que com-
portent ces opérations, ne sont souvent efficaces que pour des représentations
chaînées. L’objectif d’une bonne efficacité pour les trois opérations de recherche,
d’ajout et de suppression nous conduit à dépasser les représentations linéaires
pour adopter des méthodes arborescentes.
L’idée est la suivante : il faut rendre explicite l’arbre de recherche qui gou-
verne la recherche dichotomique. L’utilisation d’une représentation chaînée pour
cet arbre facilite les mises à jour, qui étaient problématiques dans le cas des
représentations linéaires. Le type concret étudié ci-dessous représente des sous-
ensembles finis d’entiers naturels, il se dénomme eabr. Il se base sur les abr
(cf. section 3.5.2, page 89).

6.3.1 Définition du support concret


Nous adoptons une représentation telle que, à tous les niveaux, le sous-arbre
gauche contient toutes les valeurs de l’ensemble inférieures à la racine et le sous-
arbre droit toutes les valeurs supérieures à la racine. Le caractère « sans dou-
blon » de la structure est trivialement lié au caractère strict des inégalités pré-
sentes dans la définition du support eAbr. A est la fonction d’abstraction, A(a)
est l’ensemble abstrait représenté par l’arbre a.

1)  ∈ eAbr
2) n ∈ N ∧ g ∈ eAbr ∧ d ∈ eAbr ∧ max(A(g)) < n ∧ min(A(d)) > n

g, n, d ∈ eAbr

Ainsi l’arbre binaire suivant :

, 4, , 7, , 10, , 15, , 20, , 24, , 27, , 30, , 33, 

se représente graphiquement par :

20

7 33

4 15 27

10 24 30
6. Ensembles de clés scalaires 151

6.3.2 Définition de la fonction d’abstraction


La fonction d’abstraction A a comme domaine l’ensemble eAbr et comme
codomaine ensAbst(N). La première expression gardée de la définition stipule
qu’un arbre vide représente l’ensemble vide tandis que la seconde décrit com-
ment, dans un arbre non vide, la racine et les ensembles issus des sous-arbres
gauche et droit se composent pour représenter un ensemble.

function A(a) ∈ eAbr  ensAbst(N) =



if a =  →

| a = g, n, d →
A(g) ∪ {n} ∪ A(d)

Théorème 4 (des arbres binaires de recherche). Soit a = g, n, d un eAbr et


v ∈ N.
1. v ≥ n ⇒ v ∈
/ A(g),
2. v ≤ n ⇒ v ∈
/ A(d).
Ce théorème établit que si une valeur v est supérieure ou égale (resp. inférieure
ou égale) à la racine, elle n’appartient pas au sous-arbre gauche (resp. droit). Sa
démonstration est laissée en exercice ; elle résulte directement des propriétés D.4
et D.12.

6.3.3 Spécification des opérations concrètes


Le nom des opérations concrètes dérive du nom des opérations abstraites en
le suffixant par _a. La spécification de chaque opération concrète s’obtient à
partir des opérations abstraites en considérant la fonction d’abstraction comme
un homomorphisme entre la structure concrète et la structure abstraite. La spé-
cification du type concret eabr est présentée à la figure 6.2, page suivante.

6.3.4 Calcul de la représentation des opérations concrètes


Nous allons développer le calcul de la représentation de deux des cinq opé-
rations du type eabr : eAjout_a et eSupp_a. Pour ce qui concerne l’opération
d’adjonction eAjout_a, deux démarches extrêmes sont à considérer : l’adjonc-
tion aux feuilles et l’adjonction à la racine (cette dernière se décline sous deux
formes : par partitionnement et par rotation). L’adjonction aux feuilles est la so-
lution classique qui, à l’inverse des deux formes d’adjonction à la racine, s’étend
à différentes variantes d’abr (aux Avl par exemple, cf. section 6.6) et s’adapte
aux cas d’arbres autres que les abr (aux B-arbres par exemple, cf. section 6.7).
L’existence de ces deux solutions extrêmes nous conduit à penser qu’il doit être
possible d’insérer une valeur dans un abr à tout endroit sur le chemin de la re-
cherche. Cette possibilité est exploitée dans les abr randomisés (cf. section 6.9.7).
Trois sections sont réservées aux insertions : la première est consacrée à l’inser-
tion aux feuilles, la seconde à l’insertion à la racine par partitionnement et la
troisième présente, sans la développer, l’insertion à la racine par rotation.
152 Structures de données et méthodes formelles

concreteType eabr =  (eAbr, (eV ide_a, eAjout_a, eSupp_a),


(eApp_a, eEstV ide_a))
uses bool, N
refines ensabst(N)
support
1)  ∈ eAbr
2) n ∈ N ∧ g ∈ eAbr ∧ d ∈ eAbr ∧ max(A(g)) < n ∧ min(A(d)) > n

g, n, d ∈ eAbr
abstractionFunction
function A(a) ∈ eAbr  ensAbst(N) =  ...
operationSpecifications
function eV ide_a() ∈ eAbr =  b : (b ∈ eAbr ∧ A(b) = eV ide())
;
function eAjout_a(v, a) ∈ N × eAbr → eAbr = 
b : (b ∈ eAbr ∧ A(b) = eAjout(v, A(a)))
;
function eSupp_a(v, a) ∈ N × eAbr → eAbr = 
b : (b ∈ eAbr ∧ A(b) = eSupp(v, A(a)))
;
function eApp_a(v, a) ∈ N × eAbr  bool =  eApp(v, A(a))
;
function eEstV ide_a(a) ∈ eAbr  bool =  eEstV ide(A(a))
end

Figure 6.2 – Spécification du type concret eabr.

Calcul d’une représentation de l’opération eAjout_a : insertion aux


feuilles
Nous partons de l’expression A(eAjout_a(v, a)) :

A(eAjout_a(v, a))
= Propriété caractéristique de l’opération eAjout_a
A(a) ∪ {v} (6.3.1)

Il est alors nécessaire de procéder à un raisonnement par induction en considérant


deux cas selon la structure de a, le cas de base pour lequel a =  et le cas inductif
pour lequel a = . Nous débutons par le cas de base.
A(a) ∪ {v}
= Hypothèse
A() ∪ {v}
= Proposition A.9
A() ∪ {v} ∪ ∅
= Définition de A
A() ∪ {v} ∪ A()
6. Ensembles de clés scalaires 153

= Application de A
A(, v, )

En appliquant la propriété de l’équation à membres identiques (page 67), nous


obtenons la première équation gardée de la fonction eAjout_a :

a =  →
eAjout_a(v, a) = , v, 

Pour le second cas, a = , nous considérons que a = g, n, d. Nous repartons
de la formule 6.3.1 ci-dessus.

A(a) ∪ {v}
= Hypothèse
A(g, n, d) ∪ {v}
= Définition de A
A(g) ∪ {n} ∪ A(d) ∪ {v}
= Propriété A.7 itérée
A(g) ∪ {v} ∪ {n} ∪ A(d) (6.3.2)

À ce stade du développement, il est nécessaire de distinguer trois cas : v = n,


v < n et v > n. Nous débutons par le cas v = n.

A(g) ∪ {v} ∪ {n} ∪ A(d)


= Propriété A.9
A(g) ∪ {n} ∪ A(d)
= Définition de A
A(g, n, d)

L’application de la propriété de l’équation à membres identiques (page 67) nous


permet d’obtenir l’équation gardée suivante :

a = g, n, d →
v=n →
eAjout_a(v, a) = g, n, d

Le cas v < n se traite en repartant de la formule 6.3.2 :

A(g) ∪ {v} ∪ {n} ∪ A(d)


= Propriété caractéristique de l’opération eAjout_a
A(eAjout_a(v, g)) ∪ {n} ∪ A(d)
= Définition de A
A(eAjout_a(v, g), n, d)

Ce résultat nous conduit à l’équation gardée suivante pour le cas v < n :

a = g, n, d →
v<n →
eAjout_a(v, a) = eAjout_a(v, g), n, d
154 Structures de données et méthodes formelles

Le résultat pour le cas v > n s’en déduit aisément par symétrie. Au total, en
rassemblant les différents résultats obtenus, pour cette version de l’opération
eAjout_a, nous avons calculé la représentation suivante :

function eAjout_a(v, a) ∈ N × eAbr → eAbr =



if a =  →
, v, 
| a = g, n, d →
if v = n →
g, n, d
|v<n →
eAjout_a(v, g), n, d
|v>n →
g, n, eAjout_a(v, d)

Qu’en est-il de la complexité temporelle ? Dans le reste de la section, l’opé-


ration qui tient lieu d’unité pour les mesures de complexité est la comparaison
impliquant un nœud de l’arbre. Cela signifie que nous faisons abstraction des
autres gardes qui pourraient être évaluées dans l’algorithme (comme les gardes
des conditionnelles dédiées à la sélection entre un arbre vide et un arbre non
vide) ainsi que des autres opérations, apparentes ou non (comme les recopies
de structures). Du point de vue asymptotique, cette approximation est justi-
fiée. La complexité de l’ajout dépend à l’évidence du rayon de l’arbre considéré.
Elle est de n pour l’insertion dans l’arbre de la n + 1e valeur d’une liste triée.
C’est le pire des cas possibles. Asymptotiquement l’opération est donc au pire
en O(n). À l’inverse, la complexité la meilleure est obtenue lors de l’insertion
dans un sous-arbre vide d’un abr de poids n. D’où la complexité asymptotique
la meilleure : O(1).
Qu’en est-il en moyenne ? Est-on plus près de O(n) ou de O(1) ? Excep-
tionnellement 1 , nous allons tenter de répondre à ces questions, les calculs étant
assez faciles à réaliser. Cependant, avant de nous lancer dans le développement,
il est nécessaire de préciser le modèle probabiliste sur lequel nous nous basons
pour analyser l’insertion aux feuilles dans un abr de n éléments. Nous consi-
dérons tout d’abord que toutes les valeurs destinées à apparaître dans l’arbre
sont différentes. Deux modèles équiprobabilistes sont en concurrence. Le premier
considère que tous les abr de n éléments sont équiprobables. C’est le modèle des
« arbres de Catalan » [103]. Le second considère que ce sont les n! permuta-
tions des n valeurs insérées qui sont équiprobables. C’est le modèle des « arbres
aléatoires ». Dans ce dernier cas, il est facile de comprendre que nous pouvons,
sans perte de généralité, considérer que les n valeurs prises en compte sont les
n premiers entiers positifs. La figure 6.3 montre les différents cas, pour n = 3.
Nous constatons que les 6 permutations produisent 5 arbres de Catalan. Nous
observons également que le modèle aléatoire semble donner plus de poids aux
1. Rappelons que nous avons décidé de limiter en général le calcul de la complexité au cas
le pire.
6. Ensembles de clés scalaires 155

arbres équilibrés puisque deux permutations partagent le troisième arbre (celui


qui est le mieux équilibré). Si cette intuition est confirmée, cela se traduirait par
le fait qu’il y a en moyenne plus d’arbres équilibrés que d’arbres non équilibrés.

• • • • •
• • • • • •
• • 2, 1, 3 • •
3, 2, 1 3, 1, 2 2, 3, 1 1, 3, 2 1, 2, 3

Figure 6.3 – Arbres de Catalan vs arbres aléatoires.


La figure montre d’une part tous les arbres de trois nœuds qu’il est
possible d’obtenir et d’autre part comment se répartissent les 3! permu-
tations possibles des 3 valeurs 1, 2, 3 sur les 5 arbres dans l’hypothèse
d’une insertion aux feuilles.

Le modèle aléatoire est en général considéré comme le plus utile dans la


pratique. C’est celui que nous retenons dans la suite pour évaluer la complexité.
Deux types de méthodes sont possibles : les méthodes directes (cf. [40]) et les
méthodes indirectes comme [102, 103, 104] ou [75]. Notre calcul se situe dans la
catégorie des méthodes indirectes, il est inspiré de [102, 103, 104]. Le principe
consiste à calculer le nombre moyen de comparaisons C(n) pour construire un
arbre de n nœuds. Il sera alors facile, en calculant la différence C(n + 1) − C(n),
de connaître le nombre moyen de comparaisons I(n) pour ajouter une n + 1e
valeur dans un arbre de n nœuds construit par insertion aux feuilles.
La première valeur i est insérée à la racine. La probabilité que i soit la ie plus
petite valeur de l’intervalle 1 .. n est la même pour tout i. Les deux sous-arbres
gauche et droit sont donc construits de la même façon avec un poids respectif
de i − 1 et de n − i. Dans la suite nous supposons que la fonction eAjout_a
est simplifiée afin de tenir compte de l’absence de doublons. Cette hypothèse n’a
pas d’incidence sur la complexité asymptotique. Construire un arbre de 0 nœud
coûte 0 comparaison. Au total, construire un arbre fini de n nœuds s’exprime
par l’équation récurrente suivante :

⎨C(0) = 0

1
n

⎩ C(n) = n − 1 + · (C(i − 1) + C(n − i)) pour n > 0
n i=1

Le terme n − 1 provient du fait qu’atteindre tout élément présent dans les sous-
arbres exige au préalable de « passer par la racine », ce qui conduit à ajouter une
comparaison pour chacun des n − 1 nœuds présents dans les deux sous-arbres.
La recherche d’une forme nclose pour C(n) débute
n
par le développement suivant,
qui exploite le fait que i=1 C(i − 1) = i=1 C(n − i) :

1
n
C(n) = n − 1 + · (C(i − 1) + C(n − i))
n i=1
156 Structures de données et méthodes formelles

⇔ Remarque ci-dessus
2
n
C(n) = n − 1 + · C(i − 1)
n i=1
⇔ Multiplication des 2 membres par n

n
n · C(n) = n ·(n − 1) + 2 · C(i − 1) (6.3.3)
i=1

À l’ordre n − 1, la formule 6.3.3 se réécrit :


n−1
(n − 1) · C(n − 1) = (n − 1) ·(n − 2) + 2 · C(i − 1) (6.3.4)
i=1

n n−1
Remarquons par ailleurs que, pour n > 0, i=1 C(i−1) = C(n−1)+ i=1 C(i−1)
avant de retrancher membre à membre la formule 6.3.4 de 6.3.3 :


n


⎨ n · C(n) = n ·(n − 1) + 2 ·
⎪ C(i − 1) −
i=1


n−1

⎪ (n − 1) · C(n − 1) = (n − 1) ·(n − 2) + 2 · C(i − 1)

i=1
⇔ Remarque ci-dessus, arithmétique et division par n.(n + 1) pour n > 0
C(n) (n − 1) C(n − 1)
=2· + (6.3.5)
n+1 n ·(n + 1) n

qui est une récurrence d’ordre 1 sur laquelle nous pouvons appliquer la mé-
thode des facteurs sommants. Celle-ci consiste à ajouter toutes les formules du
type 6.3.5 pour i situé dans l’intervalle 1..n et à simplifier. Nous obtenons alors :

C(n) n
(i − 1)
=2·
n+1 i=1
i ·(i + 1)

1
Avant de simplifier cette formule, nous pouvons rappeler d’une part que n ·(n+1) =
n − n+1 et d’autre part que la notation H(k) représente le nombre harmo-
1 1
k 1
nique. H(k) est tel que H(k) = i=1 i . On sait (cf. par exemple [43]) que
H(k) = ln(k) + O(1).

n
(i − 1)

i=1
i ·(i + 1)
=   Arithmétique

n
i
n
1
2· −
i=1
i ·(i + 1) i=1 i ·(i + 1)
=   Identité remarquable ci-dessus

n
1 1 1
n n
2· − +
i=1
i + 1 i=1 i i=1 i + 1
= Arithmétique
6. Ensembles de clés scalaires 157


n
1 n
1
4· −2·
i+1 i
i=1 i=1
=   Propriété de ,n>0

n
1 1
n
1
4· − 1+ −2·
i=1
i n+1 i=1
i
= Introduction de la notation H
4
2 · H(n) − 4 +
n+1

Nous avons alors

C(n) 4
= 2 · H(n) − 4 +
n+1 n+1
⇔ Arithmétique
C(n) = 2 ·(n + 1) · H(n) − 4 · n (6.3.6)

Le nombre moyen de comparaisons pour construire, par insertion aux feuilles,


un arbre aléatoire de n nœuds est donc :

C(n) = 2 ·(n + 1) · H(n) − 4 · n

Ainsi que nous l’avons mentionné ci-dessus, il est facile d’en déduire le nombre
moyen de comparaisons I(n) exigé par l’insertion aux feuilles d’une valeur dans
un arbre de n nœuds. Ce nombre est donné par la formule C(n + 1) − C(n)
qui s’interprète comme le nombre moyen de comparaisons pour construire par
insertion aux feuilles un arbre de n + 1 valeurs à partir d’un arbre de n valeurs :

I(n)
= Définition
C(n + 1) − C(n)
= Formule 6.3.6
2 ·(n + 2) · H(n + 1) − 4 ·(n + 1) − (2 ·(n + 1) · H(n) − 4 · n)
1
= H(n) = H(n + 1) −
  n + 1
1
2 ·(n + 2)·H(n + 1) − 4·(n + 1) − 2 ·(n + 1)· H(n + 1) − +4·n
n+1
= Arithmétique
2 · H(n + 1) − 2

Le nombre moyen de comparaisons pour ajouter un élément à un arbre aléa-


toire de n nœuds est donc :

I(n) = 2 · H(n + 1) − 2

Nous en déduisons que I(n) est en O(log n). Ce qui confirme que les arbres
plutôt équilibrés sont plus nombreux que les arbres déséquilibrés.
158 Structures de données et méthodes formelles

Calcul d’une représentation de l’opération eAjout_a : insertion à la


racine par partitionnement
Certaines applications se caractérisent par le fait que beaucoup de valeurs
recherchées sont parmi celles qui ont été introduites le plus récemment dans
l’ensemble 2 . Dans ce type de situation, puisque l’opération conserve près du
sommet les valeurs qui s’y trouvaient déjà, il peut être intéressant de procéder
à des insertions à la racine. Ci-dessous nous étudions une première façon de
procéder : en réalisant un partitionnement (aussi appelé coupure).
Le début du développement de l’opération eAjout_a se fait comme précé-
demment. Nous repartons de la formule 6.3.1, page 152, soit A(a) ∪ {v}. À ce
stade du développement, nous constatons que si nous disposions de deux arbres
l et r tels que :
– A(a) = A(l) ∪ A(r),
– max(A(l)) < v et v < min(A(r)),
nous serions en mesure de réécrire l’expression 6.3.1 sous la forme :
A(l) ∪ {v} ∪ A(r)

pour aboutir, en exploitant la fonction d’abstraction, à A(g, v, d). À l’évidence


cette solution ne s’applique que si v ∈ / A(a). C’est sous cette hypothèse que
nous débutons le développement. Il devient alors clair qu’il suffit de partitionner
l’arbre a en deux sous-arbres satisfaisant les conditions ci-dessus. Pour cela, nous
proposons de développer la fonction auxiliaire part répondant à ces objectifs.
Dans la mesure où la fonction de partitionnement délivre un couple d’arbres,
nous devons définir une fonction d’abstraction auxiliaire A qui renvoie l’en-
semble correspondant à un couple d’abr. Le type concret eabr s’enrichit de la
manière suivante :

 ···
concreteType eabr =
..
.
auxiliaryAbstractionFunction
function A ((a, b)) ∈ eAbr × eAbr  ensAbst(N) =
 A(a) ∪ A(b)
..
.
auxiliaryOperationSpecifications
function part(v, a) ∈ N × eAbr  eAbr × eAbr = 
pre
v∈ / A(a)
then ⎛ ⎛  ⎞⎞
A ((l, r)) = A(a) ∧
(l, r) : ⎝l, r ∈ eAbr × eAbr ∧ ⎝ max(A(l)) < v ∧ ⎠⎠
v < min(A(r))
end
end
2. Cette propriété est exploitée dans les antémémoires, avec une différence qui est que ce
qui se trouve dans une antémémoire est une duplication partielle du contenu d’une mémoire
plus lente.
6. Ensembles de clés scalaires 159

Dans la spécification de l’opération part, la composante l du couple (l, r) est un


abr quelconque contenant toutes les valeurs présentes dans a inférieures à v, tan-
dis que la composante r est un abr quelconque contenant toutes les valeurs de a
supérieures à v. Si nous parvenons à calculer une représentation (si possible effi-
cace) de cette opération, nous serons en mesure de poursuivre le développement
de l’opération eAjout_a de la manière suivante, en posant (l, r) = part(v, a) et
en repartant de la formule 6.3.1 :

A(a) ∪ {v}
= Spécification de part
A ((l, r)) ∪ {v}
= Définition de A et propriété A.7
A(l) ∪ {v} ∪ A(r)
= Définition de A
A(l, v, r)

D’où, d’après la propriété de l’équation à membres identiques (page 67), l’équa-


tion gardée suivante :

v∈
/ A(a) →
let (r, l) := part(v, a) in
eAjout_a(v, a) = l, v, r
end

Si nous admettons qu’une fausse insertion ne provoque pas de changement


dans a, le cas v ∈ A(a) est trivial. Il n’est pas développé. Au total, nous obtenons
la version suivante de l’opération eAjout_a :

function eAjout_a(v, a) ∈ N × eAbr → eAbr =



if v ∈ A(a) →
a
|v∈ / A(a) →
let (r, l) := part(v, a) in
l, v, r
end

Un raffinement supplémentaire est nécessaire afin de traduire les gardes. Il


peut se fonder sur l’opération eApp(v, a). Nous nous intéressons à présent à la
représentation de l’opération part dont le calcul débute par :

A (part(v, a))
= Propriété caractéristique
A(a) (6.3.7)

Nous poursuivons en réalisant une induction sur la structure de l’arbre a. Le cas


de base est trivial et conduit à l’équation gardée suivante :

a =  →
part(v, a) = (, )
160 Structures de données et méthodes formelles

Pour le cas inductif, puisque a = , nous pouvons poser a = l, n, r pour
repartir de la formule 6.3.7 :

A(a)
= Hypothèse
A(l, n, r)
= Définition de A
A(l) ∪ {n} ∪ A(r) (6.3.8)

À ce stade du développement, il nous faut distinguer deux cas selon la position


relative de n et de v. Les situations étant symétriques, nous nous restreignons
au cas v < n. Formulons l’hypothèse d’induction qui affirme que l’on sait parti-
tionner l’arbre l par rapport à v et posons (l , l ) = part(v, l). Nous avons alors,
d’après la spécification de l’opération part, A(l) = A(l ) ∪ A(l ).

A(l) ∪ {n} ∪ A(r)


= Spécification de part
A (part(v, l)) ∪ {n} ∪ A(r)
= Hypothèse
A ((l , l )) ∪ {n} ∪ A(r)
= Définition de A
 
A(l ) ∪ A(l ) ∪ {n} ∪ A(r)
= Définition de A
A(l ) ∪ A(l , n, r)
= Définition de A
  
A ((l , l , n, r))

L’étape qui permet d’obtenir l’arbre l , n, r est légitime puisque tous les élé-
ments de l sont inférieurs à n. Le couple final satisfait bien toutes les contraintes
de la spécification de l’opération part. L’élément v est bien supérieur à tout élé-
ment de l et inférieur à tout élément de l , n, r. D’où, d’après la propriété de
l’équation à membres identiques (page 67), la seconde équation gardée :

a = l, n, r →
v<n →
let (l , l ) := part(v, l) in
part(v, a) = (l , l , n, r)
end

Le cas v > n se traite par symétrie. Compte tenu de la précondition de l’opération


part, le cas v = n est à écarter. Au total, nous obtenons la représentation suivante
de l’opération part :
6. Ensembles de clés scalaires 161

function part(v, a) ∈ N × eAbr  eAbr × eAbr =



pre
v∈ / A(a)
then
if a =  →
(, )
| a = l, n, r →
if v < n →
let (l , l ) := part(v, l) in
(l , l , n, r)
end
|v>n →
let (r , r ) := part(v, r) in
(l, n, r , r )
end


end

On montre (cf. [116]) que le nombre de comparaisons réalisé pour effectuer


un partitionnement dans un arbre de poids n (n ≥ 1) est au moins de 1, au pire
de n et (pour des arbres aléatoires) de 2 · H(n + 1) − 2 en moyenne. Il s’en déduit
que le coût moyen de l’insertion par partitionnement est en O(log n).

Calcul d’une représentation de l’opération eAjout_a : insertion à la


racine par rotations
Il existe une autre façon de réaliser une insertion à la racine. Elle consiste
à effectuer l’insertion aux feuilles comme nous l’avons fait dans la section 6.3.4,
page 152, puis, en remontant le long du chemin de l’insertion, à itérer une rotation
simple (cf. section 3.5.2, page 91, pour une introduction aux rotations) – gauche
ou droite selon que l’insertion aux feuilles s’est elle-même faite à gauche ou à
droite – de façon à placer la valeur insérée à la racine du sous-arbre considéré. À
l’issue du traitement, la valeur insérée se trouve bien à la racine de l’arbre initial.
Cette méthode est proposée à l’exercice 6.3.1. Une technique similaire peut être
utilisée lors de la recherche (opération eApp_a) pour amener la valeur trouvée
à la racine.

Calcul d’une représentation de l’opération eSupp_a


Le cas des fausses suppressions nous oblige à considérer que, dans l’opération
eSupp_a(v, a), a est un abr quelconque. Le calcul démarre avec l’expression
A(eSupp_a(v, a)).

A(eSupp_a(v, a))
= Propriété caractéristique de l’opération eSupp_a
A(a) − {v} (6.3.9)
162 Structures de données et méthodes formelles

Procédons à une induction sur la structure de a. Le cas de base (a = ) est


trivial. Il conduit à la première équation gardée suivante :

a =  →
eSupp_a(v, a) = 

Abordons l’étape inductive en posant a = g, n, d. Repartons de la for-


mule 6.3.9 :

A(a) − {v}
= Hypothèse
A(g, n, d) − {v}
= Définition de A
(A(g) ∪ {n} ∪ A(d)) − {v}
= Propriété A.17
(A(g) − {v}) ∪ ({n} − {v}) ∪ (A(d) − {v}) (6.3.10)

Deux cas sont tout d’abord à considérer : v = n et v = n. Nous débutons par


le cas v = n. L’absence de doublon garantit alors que A(g) ∩ {v} = ∅ et que
A(d) ∩ {v} = ∅.

(A(g) − {v}) ∪ ({n} − {v}) ∪ (A(d)) − {v})


= Propriété A.16
A(g) ∪ ({n} − {v}) ∪ A(d)
= Hypothèse et propriété A.18
A(g) ∪ ∅ ∪ A(d)
= Propriété A.9
A(g) ∪ A(d) (6.3.11)

Si A(g) = ∅ (et donc si g = ) ou encore si A(d) = ∅ (soit si d = ) cette


expression se simplifie et la solution est triviale. Par contre, si les abr g et d
ne sont pas vides, l’expression doit être reconfigurée afin qu’elle se présente
sous la forme A(. . .). Trois sous-cas sont donc à considérer : g = , d =  et
g =  ∧ d = . Débutons par le cas g = .

A(g) ∪ A(d)
= Hypothèse
A() ∪ A(d)
= Définition de A
∅ ∪ A(d)
= Propriétés A.7 et A.23
A(d)

En appliquant la propriété de l’équation à membres identiques (page 67), nous


obtenons l’équation gardée suivante :

v=n→
g =  →
eSupp_a(v, g, n, d) = d
6. Ensembles de clés scalaires 163

Le cas d =  est symétrique. Reste à considérer le troisième sous-cas : g =


 ∧ d = . Il faut rechercher deux abr g  et d , et un entier naturel m tels que :
1. A(g  ) ∪ {m} ∪ A(d ) = A(g) ∪ A(d),
2. max(A(g  )) < m ∧ min(A(d )) > m.
Nous serons alors à même d’appliquer la définition de A pour obtenir
A(g  , m, d ), formule qui, via la propriété de l’équation à membres identiques
(page 67), permettra de conclure. Puisque g = , il existe dans g un plus grand
élément capable de tenir le rôle de m. Soit, en repartant de la formule 6.3.11 :

A(g) ∪ A(d)
= Propriété A.10
(A(g) − {max(A(g))}) ∪ {max(A(g))} ∪ A(d)
= Propriété caractéristique de l’opération eSupp_a
A(eSupp_a(max(A(g)), g)) ∪ {max(A(g))} ∪ A(d)
= Définition de A
A(eSupp_a(max(A(g)), g), max(A(g)), d)

D’où l’équation gardée :

v=n→
g =  ∧ d =  →
eSupp_a(v, g, n, d) = eSupp_a(max(A(g)), g), max(A(g)), d

En repartant de la formule 6.3.10, page ci-contre, il nous reste à prendre en


compte le cas où v = n, qui se décompose en deux cas symétriques : v < n et
v > n. Détaillons le premier cas. Puisque v < n, c’est donc que {n} ∩ {v} = ∅
et que ({n} ∪ A(d)) ∩ {v} = ∅ :

(A(g) − {v}) ∪ ({n} − {v}) ∪ (A(d) − {v})


= Propriété A.16
(A(g) − {v}) ∪ {n} ∪ A(d)
= Propriété caractéristique de l’opération eSupp_a
A(eSupp_a(v, g)) ∪ {n} ∪ A(d)
= Définition de A
A(eSupp_a(v, g), n, d)

D’où, d’après la propriété de l’équation à membres identiques (page 67), l’équa-


tion gardée suivante pour eSupp_a :
v<n→
eSupp_a(v, g, n, d) = eSupp_a(v, g), n, d
Le cas v > n se traite aisément par symétrie. Au total, nous avons calculé la
représentation suivante de l’opération eSupp_a :

function eSupp_a(v, a) ∈ N × eAbr → eAbr =



if a =  →

| a = g, n, d →
if v = n →
164 Structures de données et méthodes formelles

if g =  →
d
| d =  →
g
| g =  ∧ d =  →
eSupp_a(max(A(g)), g), max(A(g)), d

|v<n →
eSupp_a(v, g), n, d
|v>n →
g, n, eSupp_a(v, d)

Cette version de l’opération eSupp_a est une version intermédiaire qui doit
être raffinée afin de faire disparaître l’argument max(A(g)) au profit d’une ex-
pression implantable. Parmi les solutions envisageables, citons en deux.
1. Rendre disponible une opération auxiliaire concrète qui raffine l’opération
« abstraite » max et qui délivre la plus grande valeur présente dans un abr
non vide.
2. Renforcer le support en décomposant l’opération max sur l’abr. Le principe
de cette solution est appliqué à la section 6.5, page 187, dans le cas des
arbres externes (voir aussi l’exercice 6.3.3, page 169).
La première solution présente l’inconvénient de descendre deux fois dans
l’arbre g, une première fois pour y rechercher le plus grand élément et une se-
conde fois pour le supprimer. La seconde est coûteuse en place mémoire puis-
qu’elle exige de réserver de la place dans chaque nœud pour y accueillir le plus
grand élément de l’arbre. Nous allons emprunter une troisième voie qui écarte ces
inconvénients sans en introduire de nouveaux. C’est celle qui consiste en quelque
sorte à fusionner l’opération de recherche du plus grand élément et l’opération
de suppression du plus grand élément. Plus précisément, nous allons spécifier
formellement, puis calculer, l’opération auxiliaire maxRac(a) qui, par des rota-
tions simples, amène le plus grand élément de A(a) à la racine comme le montre
l’exemple suivant :

13 13 25
10 21 10 25 13
8 12 18 25 8 12 21 10 21
15 19 24 18 24 8 12 18 24
15 19 13 19

Cette opération est spécifiée dans la rubrique auxiliaryOperationSpecifica-


tions de la manière suivante :
6. Ensembles de clés scalaires 165

 ···
concreteType eabr =
..
.
auxiliaryOperationSpecifications
function maxRac(a) ∈ eAbr →  eAbr =
pre
A(a) = ∅
then
a : (a ∈ eAbr ∧ a = g  , m,  ∧ A(a ) = A(a))
end
end

La formule a = g  , m,  précise que le sous-arbre droit de l’arbre résultat a


est vide et donc, puisque c’est un eAbr, que m est le plus grand élément de
l’ensemble A(a). L’égalité A(a ) = A(a) spécifie que l’opération maxRac laisse
inchangé l’ensemble des valeurs présentes dans l’arbre.
Une fois cette fonction mise en œuvre, l’expression
eSupp_a(max(A(g)), g), max(A(g)), d peut être remplacée par g  , m, d
et l’équation gardée dans laquelle apparaît cette expression devient :

v=n→
g =  ∧ d =  →
let g  , m,  := maxRac(g) in
eSupp_a(v, g, n, d) = g  , m, d
end

La représentation de l’opération eSupp_a se raffine et se simplifie en :

function eSupp_a(v, a) ∈ N × eAbr → eAbr = 


if a =  →

| a = g, n, d →
if v = n →
if g =  →
d
| d =  →
g
| g =  ∧ d =  →
let g  , m,  := maxRac(g) in
g  , m, d
end

|v<n →
eSupp_a(v, g), n, d
|v>n →
g, n, eSupp_a(v, d)


166 Structures de données et méthodes formelles

Retour sur l’opération maxRac


Puisque A(a) = ∅, nous pouvons poser a = l, w, r. Nous avons alors :

A(maxRac(a))
= Spécification de maxRac
A(a)
= Hypothèse
A(l, w, r) (6.3.12)

Procédons à une induction sur la structure de r en débutant par le cas de base


r =  :

A(l, w, r)
= Hypothèse
A(l, w, )

Nous pouvons appliquer la propriété de l’équation à membres identiques


(page 67) puisque l’arbre l, w,  satisfait la spécification. Nous obtenons l’équa-
tion gardée suivante :

let l, w, r := a in
r =  →
maxRac(a) = a
end

Le cas inductif r =  se développe comme suit à partir de la formule 6.3.12 :

A(l, w, r)
= Définition de A
A(l) ∪ {w} ∪ A(r)
= Spécification de maxRac
A(l) ∪ {w} ∪ A(maxRac(r))

Posons, conformément à la spécification, maxRac(r) = l , m,  et poursuivons


le calcul :

A(l) ∪ {w} ∪ A(maxRac(r))


= Hypothèse
A(l) ∪ {w} ∪ A(l , m, )
= Définition de A
A(l) ∪ {w} ∪ A(l ) ∪ {m} ∪ A()
= Définition de A
A(l, w, l ) ∪ {m} ∪ A()
= Définition de A
A(l, w, l , m, )

Les deux dernières étapes reviennent à réaliser une rotation à gauche à la racine.
En appliquant la propriété de l’équation à membres identiques (page 67), nous
obtenons la seconde équation gardée :
6. Ensembles de clés scalaires 167

let l, w, r := a in
r =  →
let l , m,  := maxRac(r) in
maxRac(a) = l, w, l , m, 
end
end

Au total, nous avons calculé la version suivante de l’opération maxRac :

function maxRac(a) ∈ eAbr →  eAbr =



pre
A(a) = ∅
then
let l, w, r := a in
if r =  →
a
| r =  →
let l , m,  := maxRac(r) in
l, w, l , m, 
end

end
end

On montre (cf. [116]) que la complexité moyenne de la suppression d’une


valeur dans un arbre de recherche aléatoire de poids n est 2 ·(1 + 1/n) · H(n) − 3.
Asymptotiquement, le coût de la suppression est donc en O(log n). Cependant
la suppression ne préserve pas le caractère aléatoire de l’arbre.

Représentation de l’opération eApp_a


Le calcul d’une représentation de l’opération eApp_a ne présente pas de
difficulté, il est laissé en exercice. Une solution possible est :

function eApp_a(v, a) ∈ N × eAbr  bool =



if a =  →
false
| a = g, r, d →
if v = r →
true
|v<r →
eApp_a(v, g)
|v>r →
eApp_a(v, d)


168 Structures de données et méthodes formelles

On montre (cf. [116]) que la complexité moyenne de la recherche positive


d’une valeur dans un arbre de recherche aléatoire de poids n est de 2 ·(H(n +
1)−1) et, pour une recherche se terminant par un échec, de 2 ·(1+1/n) · H(n)−3.
Asymptotiquement, le coût moyen d’une recherche, quelle qu’en soit l’issue, est
donc en O(log n).

6.3.5 Conclusion et remarques bibliographiques


Les abr constituent la structure de base pour la représentation d’ensembles
de scalaires. Exception faite de la technique de hachage, toutes les autres mises
en œuvre étudiées dans ce chapitre sont des extensions ou des variantes des abr.
Le principal avantage des abr est sa simplicité algorithmique. Un autre point
fort à ne pas négliger est qu’un abr est une structure adéquate pour réaliser
des tris de manière raisonnablement efficace. Concernant la représentation d’en-
sembles, les abr constituent même la structure de prédilection à deux conditions :
il faut d’une part qu’il n’y ait pas de suppressions et d’autre part que les n va-
leurs insérées résultent d’un tirage aléatoire parmi les n! permutations possibles.
C’est le cas idéal des abr aléatoires.
L’inconvénient majeur est un risque de performances dégradées quand (au
moins) l’une de ces deux conditions n’est pas satisfaite. La seconde condition a
été étudiée ci-dessus. Le risque qu’elle soit violée n’est pas négligeable. Quant au
problème induit par les suppressions, il survient quand des séquences d’ajouts
et de suppressions s’entrelacent (cf. [75], pages 431-435, pour une analyse de cet
aspect).
Selon D. Knuth [75], la première description de l’insertion (aux feuilles) dans
un abr est à porter au crédit de P.F. Windley, de A.D. Booth et A.J.T. Colin, et
enfin de T.N. Hibbard. Ce dernier est aussi l’inventeur (en 1962) de la méthode
de suppression fondée sur la recherche de la clé la plus proche, cette méthode
est voisine de celle calculée ci-dessus. La méthode d’insertion à la racine par
partitionnement est due à C.J. Stephenson [111] en 1980, la description originale
est purement itérative. D’autres méthodes de suppression que celle étudiée ici
sont possibles, comme par exemple la méthode de fusion (cf. exercice 6.3.5).
Le problème du maintien du statut d’arbre aléatoire en présence de suppres-
sions est résolu dans les structures de données aléatoires que sont les treaps et
abr randomisés (cf. section 6.9).

Exercices

Exercice 6.3.1 L’opération de rotation droite (rd) d’un arbre binaire de recherche est définie
par :

function rd(gg , vg , dg , v, d) ∈ eAbr → eAbr =


 gg , vg , dg , v, d

La rotation gauche rg est l’opération duale de rd. On cherche à calculer une version de
l’opération d’insertion eAjout_a qui effectue une insertion à la racine par rotations.
1. Démontrer le théorème suivant :
Théorème 5. Si a = gg , vg , dg , v, d alors :
(a) rd(a) ∈ eAbr (la rotation droite préserve la propriété d’arbre binaire de re-
cherche).
6. Ensembles de clés scalaires 169

(b) A(a) = A(rd(a)) (la rotation droite préserve la structure abstraite).


2. Disséquer l’insertion de la valeur 15 dans l’arbre binaire de recherche suivant :
23

17 31

10 20 27 37

5 14 19 34 41

12
Faire de même pour l’insertion de la valeur 19.
3. Après avoir décidé d’une stratégie pour le cas des fausses insertions, calculer l’opération
eAjout_a.
Suggestion. On effectue tout d’abord une insertion aux feuilles puis, par des rotations appro-
priées, on remonte la valeur insérée jusqu’à la racine.

Exercice 6.3.2 Spécifier puis calculer une représentation de l’opération auxiliaire maxAbr
qui délivre le plus grand élément d’un abr non vide.

Exercice 6.3.3 Appliquer le principe du renforcement du support par décomposition de l’opé-


rateur max (cf. section 6.5, page 187, pour un exemple) sur l’abr pour calculer un raffinement
de la version de l’opération eSupp_a obtenue à la page 164.

Exercice 6.3.4 Spécifier par abr les opérations abstraites complémentaires décrites dans
l’exercice 6.2.2, page 149. On suppose en outre que les deux arbres à fusionner résultent d’un
partitionnement. Calculer une représentation de ces opérations. Suggestion pour l’opération
eU nion_a(a1, a2) : amener le plus grand élément de a1 à la racine puis enraciner a2.

Exercice 6.3.5 Soit a et b deux abr sans doublon tels que max(A(a)) < min(A(b)). Calculer
l’opération f us_a(a, b) qui délivre un abr représentant l’ensemble A(a) ∪ A(b). En déduire
une nouvelle version de l’opération de suppression.

Exercice 6.3.6 Le support eAbr est défini ci-dessus comme un arbre sans doublon. Il est pos-
sible d’implanter le type abstrait ensabst en utilisant des abr pouvant présenter des doublons.
Développer une version du type ensabst sur cette nouvelle base.

6.4 Méthodes de hachage


6.4.1 Introduction
Dans les méthodes étudiées jusqu’à présent, la place d’une clé est déterminée
par rapport à celle des clés déjà présentes dans l’ensemble. L’inconvénient qui
en résulte est que le temps d’accès pour la consultation ou pour la mise à jour
est une fonction croissante du cardinal de l’ensemble.
Les techniques de hachage visent à supprimer – ou du moins à limiter – cet
inconvénient. Elles se fondent sur l’hypothèse qu’il est préférable du point de
vue de la complexité temporelle de gérer un petit ensemble plutôt qu’un grand.
La technique de hachage que nous allons plus particulièrement détailler, ap-
pelée hachage externe, se fonde sur un partitionnement de l’ensemble à gérer, de
sorte que l’essentiel du travail se fait sur une classe de la partition a priori plus
170 Structures de données et méthodes formelles

petite que l’ensemble pris dans sa totalité. On a « haché » l’ensemble initial en


plusieurs petits sous-ensembles, d’où le nom de ce type de technique 3 .
Considérons l’ensemble N, que nous décidons de partitionner en trois classes,
identifiées respectivement par les indices 0, 1 et 2. Considérons par ailleurs le
sous-ensemble de clés {17, 28, 54, 127, 221, 241}. Il est facile de comprendre que si
nous sommes capable, de manière efficace, d’associer à chacun de ces six entiers
une classe de la partition, comme dans le schéma ci-dessous :

17 •

28 •
2
54 •
0
127 • 1

221 •

241 •

la recherche, l’ajout ou la suppression d’une de ces clés se fera dans un sous-


ensemble en moyenne trois fois plus petit que l’ensemble initial.
Il est important de prendre conscience que, certes nous avons progressé vers le
traitement de sous-ensembles plus petits, mais nous n’avons pris aucune décision
quant à la façon de mettre en œuvre ces sous-ensembles. Cette décision n’appar-
tient pas à la méthode de hachage externe proprement dite. Nous devons, dans
une phase de raffinement ultérieure, nous déterminer pour une méthode (incluant
celles étudiées ou mentionnées dans ce chapitre à l’exception de la méthode du
vecteur caractéristique (cf. section 6.1), pour laquelle la contiguïté exige une
certaine proximité des valeurs) : listes, abr, Avl, B-arbres, arbres externes, voire
une méthode de hachage.
La situation idéale du point de vue de la complexité serait celle qui attri-
buerait une classe par clé. Cette solution n’est en général pas viable du point
de vue de l’occupation : elle reviendrait, à l’instar de la méthode du vecteur
caractéristique, à attribuer un emplacement à chaque clé possible.
Pour la technique du hachage externe introduite ci-dessus, nous devons nous
arrêter sur deux questions :
1. Comment associer à toute clé susceptible d’être présente dans l’ensemble
un indice qui identifie la classe de la partition ? C’est la question de la
fonction de hachage.
2. Comment associer à chaque indice sa classe ? C’est la question de la table
de hachage.
Concernant la question de la fonction de hachage f h, l’efficacité de la méthode
est en partie liée à trois caractéristiques que doit posséder cette fonction :

3. Les libellés « adressage dispersé » ou « adressage associatif » sont des synonymes de


« hachage » parfois employés dans la littérature.
6. Ensembles de clés scalaires 171

1. La fonction de hachage doit être facile (et rapide) à calculer afin de garantir
que le surcoût occasionné par le partitionnement reste acceptable.
2. La fonction de hachage doit répartir uniformément les clés existantes sur
l’intervalle des indices de la partition. Comme en général le sous-ensemble
de clés à gérer n’est pas connu à l’avance le mieux est de choisir une fonction
de hachage qui répartisse uniformément l’ensemble des clés potentielles.
3. La fonction de hachage doit être déterministe. Le cas échéant la recherche
d’appartenance pourrait se faire dans une classe différente de la classe où
s’est effectuée l’insertion.
Nous reviendrons largement sur ces propriétés et sur les différentes techniques
qui garantissent leur existence.
Concernant maintenant la question de la table de hachage t, la réponse est
suggérée par la question : la fonction qui associe à chaque indice une classe de
la partition est une fonction totale surjective définie sur l’intervalle des indices
des classes. C’est précisément la définition d’une table.
Nous pouvons à présent reprendre le schéma ci-dessus en le précisant :
17 • table de hachage t
28 •
• 0 2
54 •
• 1
0
127 • 1
• 2
221 •

241 • fonction de hachage f h

Ainsi, si nous souhaitons insérer la clé v dans l’ensemble e, la technique de


hachage externe étudiée ici consiste à insérer v dans le sous-ensemble t(f h(v)).

6.4.2 Définition du support concret


Dans la suite, nous considérons que ehach(m) est le type concret que nous
définissons. m, paramètre du type, est un entier positif et les indices des classes
appartiennent à l’intervalle 0 .. m − 1. Nous rappelons par ailleurs que f h et t
sont respectivement la fonction de hachage et la table de hachage.

t ∈ 0 .. m − 1 → F(N) ∧
∀j ·(j ∈ 0 .. m − 1 ⇒ ∀v ·(v ∈ t(j) ⇒ f h(v) = j))

t ∈ eHach(m)

Cette définition nous affirme que (i) si t est une fonction totale de l’intervalle
0 .. m − 1 sur l’ensemble des parties finies de N et si (ii) toutes les clés d’une
classe t(j) ont comme valeur de hachage j, alors t est un élément de eHach(m).
Il est facile de montrer que la fonction f h induit un partitionnement du
codomaine de t (voir exercice 6.4.1).
172 Structures de données et méthodes formelles

Pour un t, t ∈ eHach(m) et pour une fonction de hachage f h donnés, chaque


t(i) est un ensemble abstrait. Une conséquence prévisible est que, contrairement
à ce que nous avons pu rencontrer jusqu’à présent, certaines des opérations
concrètes que nous calculerons feront appel aux opérations abstraites correspon-
dantes. Cela signifie, ainsi que nous l’avons déjà dit ci-dessus, qu’une décision
ultérieure doit être prise quant au choix de la représentation concrète pour les
classes de la partition.

6.4.3 Définition de la fonction d’abstraction


La fonction d’abstraction se définit facilement. Elle capte le fait que l’en-
semble abstrait représenté est l’union de tous les « petits » sous-ensembles consti-
tuant la partition.

function A(t) ∈ eHach(m) → ensAbst =  j ·(j ∈ 0 .. m − 1 | t(j))

6.4.4 Spécification des opérations concrètes


La figure 6.4, page ci-contre, représente le type concret ehach(m) avec, en
particulier, la spécification des opérations concrètes ainsi que l’ébauche de la
fonction de hachage.

6.4.5 Calcul de la représentation des opérations concrètes


Calcul d’une représentation de l’opération eV ide_h
Le tableau {0 → ∅, 1 → ∅, . . . , m − 1 → ∅} n’est pas vide puisque m ∈ N1 .
Chacun de ses éléments vaut ∅ (c’est par exemple le cas du m − 1e : {0 →
∅, 1 → ∅, . . . , m − 1 → ∅}(m − 1) = ∅). Leur union est l’ensemble vide. Plus
formellement :

j ·(j ∈ 0 .. m − 1 | ((0 .. m − 1) × {∅})(j)) = ∅
La démonstration de cette propriété est proposée à l’exercice 6.4.2.
A(eV ide_h())
= Propriété caractéristique de l’opération eV ide_h

= Remarque ci-dessus et exercice 6.4.2
j ·(j ∈ 0 .. m − 1 | ((0 .. m − 1) × {∅})(j))
= Définition de A
A((0 .. m − 1) × {∅})
D’où, d’après la propriété de l’équation à membres identiques (page 67), la re-
présentation suivante pour l’opération eV ide_h :

function eV ide_h() ∈ eHach(m) =


 (0 .. m − 1) × {∅}

Ce résultat s’interprète en disant qu’un ensemble vide s’obtient en créant un


tableau dont tous les éléments désignent l’ensemble vide. La complexité de cette
opération est en O(m).
6. Ensembles de clés scalaires 173

concreteType ehach(m) =  (eHach, (eV ide_h, eAjout_h, eSupp_h),


(eApp_h, eEstV ide_h))
uses bool, N, N1
constraints m ∈ N1
refines ensabst(N)
support
t ∈ 0 .. m − 1 → F(N) ∧
∀j ·(j ∈ 0 .. m − 1 ⇒ ∀v ·(v ∈ t(j) ⇒ f h(v) = j))

t ∈ eHach(m)
abstractionFunction
function A(t) ∈ eHach(m) → ensAbst =  ...
operationSpecifications
function eV ide_h() ∈ eHach(m) = 
h : (h ∈ eHach(m) ∧ A(h) = eV ide())
;
function eAjout_h(v, t) ∈ N × eHach(m) → eHach(m) = 
h : (h ∈ eHach(m) ∧ A(h) = eAjout(v, A(t)))
;
function eSupp_h(v, t) ∈ N × eHach(m) → eHach(m) = 
h : (h ∈ eHach(m) ∧ A(h) = eSupp(v, A(t)))
..
.
auxiliaryOperationRepresentations
function f h(v) ∈ N  0 .. m − 1 =
 ···
end

Figure 6.4 – Spécification du type concret ehach.


Ce type concret implante le type abstrait ensabst(N) par une technique
de hachage externe. L’ensemble des clés possibles est partitionné en
m sous-ensembles. Lors d’une recherche, il faut tout d’abord identifier
le sous-ensemble concerné puis effectuer une recherche « classique » à
l’intérieur de ce sous-ensemble.

Calcul d’une représentation de l’opération eAjout_h


A(eAjout_h(v, t))
= Propriété caractéristique de l’opération eAjout_h
A(t) ∪ {v}
= Définition de A
j ·(j ∈ 0 .. m − 1 | t(j)) ∪ {v}

Nous pouvons en principe ajouter v dans n’importe quelle classe t(j), cependant,
si nous souhaitons conserver invariante la propriété caractéristique du support,
il est judicieux d’introduire v dans t(f h(v)).

j ·(j ∈ 0 .. m − 1 | t(j)) ∪ {v}
174 Structures de données et méthodes formelles

= ⎧ Propriété A.14
⎨ j ·(j ∈ (0 .. m − 1) − {f h(v)} | ((0 .. m − 1) − {f h(v)}  t)(j))


j ·(j ∈ {f h(v)} | ({f h(v)}  t)(j)) ∪ {v}

Arrêtons-nous sur la seconde partie de cette formule.



j ·(j ∈ {f h(v)} | ({f h(v)}  t)(j)) ∪ {v}
= Propriété A.15
[j := f h(v)](({f h(v)}  t)(j)) ∪ {v}
= Substitution
({f h(v)}  t)(f h(v)) ∪ {v}
= Propriétés B.127 et C.17
t(f h(v)) ∪ {v}
= Définition de l’opération eAjout
eAjout(v, t(f h(v)))
= Propriété C.17
{f h(v) → eAjout(v, t(f h(v)))}(f h(v))
= Substitution
[j := f h(v)]{f h(v) → eAjout(v, t(f h(v)))}(j)
= Propriété A.15
j ·(j = f h(v) | {f h(v) → eAjout(v, t(f h(v)))}(j))

Avant d’intégrer ce résultat à la formule ci-dessus, posons s = t − {f h(v) →


eAjout(v, t(f h(v)))}. La formule ci-dessus devient :

⎨ j ·(j ∈ (0 .. m − 1) − {f h(v)} | ((0 .. m − 1) − {f h(v)}  s)(j))


j ·(j ∈ {f h(v)} | ({f h(v)}  s)(j))
= Propriété A.14
j ·(j ∈ 0 .. m − 1 | s(j)})
= Définition de s et de la fonction d’abstraction A
A(t − {f h(v) → eAjout(v, t(f h(v)))})

D’où, d’après la propriété de l’équation à membres identiques (page 67), la re-


présentation suivante pour l’opération eAjout_h :

function eAjout_h(v, t) ∈ N × eHach(m) → eHach(m) =



t − {f h(v) → eAjout(v, t(f h(v)))}

Cette représentation s’interprète comme suit : pour ajouter l’élément v dans


l’ensemble abstrait représenté par t, il suffit d’ajouter v dans l’ensemble abstrait
désigné par t(f h(v)).
Admettons que le nombre de comparaisons exigé par le calcul de la fonction de
hachage soit négligeable (il est en général est faible et constant), que la fonction
de hachage distribue uniformément les clés, et que la gestion des classes se fasse
par une opération eAjout_xx. Sous ces hypothèses, la complexité de l’opération
6. Ensembles de clés scalaires 175

eAjout_h est du même ordre que celle de eAjout_xx, mais avec un facteur
1
multiplicatif de m . Ce facteur reflète le fait que l’ensemble initial a été « haché »
en m sous-ensembles. La complexité au pire, qui survient lorsque la fonction de
hachage délivre une constante, est celle de eAjout_xx avec cette fois un facteur
multiplicatif de 1.
Le calcul des autres opérations, et notamment celui de l’opération de sup-
pression, est proposé en exercice (cf. exercice 6.4.3).

6.4.6 Fonction de hachage


Nous avons vu à la section 6.4.1 que la fonction de hachage doit satisfaire les
exigences suivantes :
1. être facile à calculer,
2. répartir de manière homogène les valeurs de hachage de l’ensemble sur les
m entrées de la table,
3. être déterministe.
La littérature abonde de conseils, d’études et de résultats concernant le choix
d’une bonne fonction de hachage (par exemple [40, 75, 7, 102, 104, 25]). L’une
des suggestions qui est régulièrement faite, et qu’il convient d’appliquer, est que,
étant donné l’argument c de la fonction f h, cette dernière doit, lors de son
évaluation, utiliser tous les éléments binaires présents dans c. Le cas échéant, il
y aurait un risque de ne pas garantir une répartition équitable des clés sur les
m sous-ensembles.
Une façon d’y parvenir – probablement la plus pratiquée – est de disposer
d’une table de hachage dont la longueur m est un nombre premier et de prendre
comme fonction de hachage le reste de la division entière de l’argument c par m :
f h(c) = c mod m. C’est ce que nous avons réalisé dans l’exemple de la page 170
en prenant m = 3 comme indiqué ci-dessous :

17 = 5·3 + 2
28 = 9·3 + 1
54 = 18 · 3 + 0
127 = 42 · 3 + 1
221 = 72 · 3 + 2
241 = 80 · 3 + 1

Dans le cas où c n’est pas un entier (mais par exemple une chaîne de caractères),
il est possible de prendre en compte l’entier représenté par la chaîne de bits
correspondante. Si la chaîne c se révèle trop longue pour représenter un entier
en machine, on peut la compresser en la découpant en portions de longueur
convenable puis en composant (par exemple au moyen d’opérations de type « ou
exclusif ») les différents fragments avant de procéder au calcul modulaire.

6.4.7 Autres méthodes de hachage


L’approche générique développée ci-dessus est qualifiée de hachage externe.
Le lecteur aura compris l’origine de ce qualificatif : les éléments sont enregistrés à
l’extérieur de la table de hachage. À l’inverse, les méthodes de hachage internes
176 Structures de données et méthodes formelles

se caractérisent par le fait que les clés sont enregistrées dans la table elle-même,
qui se présente non plus comme une fonction à valeurs dans F(N) mais une
fonction à valeurs dans N.
La première conséquence du choix d’un hachage interne est le problème des
collisions : la fonction de hachage f h n’étant pas, par nature, injective, que fait-
on d’une clé c2 si t(f h(c2)) = c1, c2 devant en principe venir se placer dans une
entrée de la table déjà occupée par une clé c1 ? En supposant que la table n’est
pas pleine, il s’agit de trouver un emplacement libre pouvant être occupé par
c2 (et pour lequel c2 deviendrait un « squatter »). Les méthodes se distinguent
selon la façon de trouver un emplacement libre pour c2. Deux types d’approche
sont en compétition :
– L’approche linéaire où l’on recherche en séquence (modulo m) un empla-
cement libre. L’un des inconvénients de cette approche est le risque d’ac-
cumulation (clustering) à partir de la position f h(c1) pour peu que l’em-
placement suivant (f h(c1) + 1) soit occupé par une clé « légitime ». Par
ailleurs, ce risque d’accumulation est d’autant plus important que le taux
d’occupation de la table est élevé : on a tout intérêt à surestimer la taille
de la table 4 . Un autre inconvénient, partagé par toutes les méthodes in-
ternes, est celui lié à la suppression : un emplacement qui devient libre
suite à une suppression doit être considéré comme occupé mais sans valeur
significative pour la recherche par contre, pour une opération d’insertion,
il est vraiment considéré comme libre.
– L’approche par double hachage vise à éviter le clustering. Pour cela, elle
impose que le pas de recherche d’un emplacement libre dépende à la fois de
la clé c2 faisant l’objet de la recherche et du nombre d’étapes de recherche
déjà réalisé. Par ailleurs, il faut garantir, malgré le caractère variable du
pas de recherche, que toutes les positions de la table de hachage sont acces-
sibles. L’approche par double hachage fait l’hypothèse que l’on dispose :
(i) d’une fonction de hachage classique, f h, (ii) d’une seconde fonction
de hachage f h surjective sur l’intervalle 1 .. m − 1. La suite de fonctions
si (c) = (f h(c) + f h (c) ·(i − 1)) mod m est telle qu’à la première tentative
on essaie la position s1 (c) = f h(c) mod m, à la seconde, la position s2 (c) =
(f h(c) + f h (c)) mod m, à la troisième s3 (c) = (f h(c) + 2 · f h (c)) mod m,
etc. La valeur 0 doit être exclue du codomaine de la fonction f h . Le cas
échéant, la suite de fonctions si se réduirait à si (c) = f h(c) mod m qui ne
dépend pas de i. En cas de collision, cette suite conduirait à une absence de
terminaison de l’algorithme. On montre que la suite si (c) définie ci-dessus
limite le clustering en nombre et en taille, et que, si m est premier, toutes
les valeurs de l’intervalle 0 .. m − 1 sont atteintes en m étapes.
Une troisième méthode, appelée hachage dynamique, proposée en 1978 par
P.-Å. Larson [78] et classée le plus souvent dans les techniques de hachage, relève
plutôt des « tries ». Elle est présentée dans la section consacrée à cette structure
de données (cf. section 7.1, page 273).

4. Ce qui constitue une forme de compromis espace/temps.


6. Ensembles de clés scalaires 177

6.4.8 Conclusion et remarques bibliographiques


Comparées aux méthodes arborescentes (abr, Avl, B-arbres, etc.), les mé-
thodes de hachage présentent l’avantage de l’efficacité. Elles n’exigent pas non
plus que l’ensemble soit doté d’une structure d’ordre. Par contre, elles interdisent
d’enrichir le type abstrait par une opération de tri ou d’accès séquentiel. Les fonc-
tions de hachage peuvent être utilisées à chaque fois que l’on souhaite associer à
une clé une valeur ayant un comportement proche de l’aléatoire (cf. section 6.9.6
pour une application aux treaps randomisés).
Si l’on considère les différentes méthodes de hachage, le hachage externe
présente beaucoup d’avantages : taille dynamique de l’ensemble (non limité par
la taille de la table de hachage), facilité des suppressions (qui en fait est reporté
sur la gestion secondaire des classes). Son principal inconvénient est d’exiger des
opérations secondaires indépendantes des opérations primaires, ce qui nécessite
un double travail de conception.
A priori hachage et programmation fonctionnelle ne font pas bon ménage :
une recopie entière du tableau représentant la table est exigée à chaque opération.
Nous verrons cependant, au chapitre 10, qu’il existe des mises en œuvre de
tableaux qui peuvent tempérer ce défaut.
L’idée du hachage doit être attribuée à deux équipes indépendantes de la
compagnie ibm, en 1953. L’une avait comme mission l’écriture d’un assembleur
pour la machine ibm 701. Cette équipe comprenait celui qui allait devenir l’un des
plus célèbres architectes de la compagnie, Gene M. Amdahl, qui après avoir quitté
ibm en 1970 allait créer sa propre compagnie, Amdahl Corporation. Cependant,
la première diffusion publique d’une technique de hachage (due à A.I. Dumey)
ne date que de 1956.

Exercices

Exercice 6.4.1 Montrer que si la fonction de hachage f h est une fonction totale, la définition
du support du type ehach induit un partitionnement de l’ensemble abstrait considéré.

Exercice 6.4.2 Pour n ∈ N, démontrer la propriété suivante :



j ·(j ∈ 0 .. n | ((0 .. n) × {∅})(j)) = ∅

Suggestion : utiliser la propriété A.15 pour effectuer une induction.

Exercice 6.4.3 Après avoir calculé la représentation des opérations non développées ci-dessus
(eSupp_h, eApp_h et eEstV ide_h), compléter le raffinement en choisissant une mise en
œuvre (liste non triée, liste triée, abr, Avl, B-arbre, etc.) pour la représentation des classes de
la partition.

Exercice 6.4.4 Spécifier le hachage linéaire (cf. page ci-contre). Calculer la représentation
fonctionnelle des opérations.

Exercice 6.4.5 Spécifier pour le hachage externe les opérations abstraites complémentaires
décrites dans l’exercice 6.2.2, page 149. Calculer une représentation de ces opérations.
178 Structures de données et méthodes formelles

6.5 Méthodes arborescentes élémentaires :


les arbres externes de recherche
À la page 96, nous avons défini les arbres externes binaires. Dans cette section
nous allons étudier comment – moyennant l’ajout de la caractéristique « arbre
de recherche » – il est possible d’utiliser ces arbres pour mettre en œuvre le type
abstrait ensabst sur un ensemble totalement ordonné. A priori ce type d’arbre
ne présente que des inconvénients par rapport aux arbres totalement étiquetés.
Il faut en effet s’attendre à des complexités moyennes supérieures puisque l’in-
formation utile se trouve sur les feuilles. Outre le fait que les algorithmes sont en
général plus simples à calculer, le principal avantage qu’il est possible d’en retirer
est que la recherche d’un élément ne s’encombre pas des informations auxiliaires
(attributs) qui sont en général associées à une clé. L’argument vaut principale-
ment pour les structures enregistrées sur supports auxiliaires, pour lesquels le
coût de transfert vers ou depuis la mémoire principale est critique.

6.5.1 Définition du support concret


Rappelons qu’informellement un arbre externe binaire fini est tel que tout
nœud interne possède exactement deux descendants (ce sont des arbres complets)
et que les valeurs sont situées aux feuilles. Superposer à ces propriétés la qualité
d’arbre de recherche signifie que pour tout nœud interne les valeurs contenues
dans le sous-arbre gauche sont inférieures à celles contenues dans le sous-arbre
droit. Formellement, le support concret ainsi obtenu, eAe, se décrit de la manière
suivante :

1)  ∈ eAe
2) n ∈ N ⇒ n ∈ eAe
3) g ∈ eAe − {} ∧ d ∈ eAe − {} ∧ (6.5.1)
max(A(g)) < min(A(d)) (6.5.2)

g, d ∈ eAe

Le conjoint 6.5.1 précise que tout nœud interne possède exactement deux
descendants tandis que le conjoint 6.5.2, en utilisant la fonction d’abstraction A
qui convertit un tel arbre en un ensemble, formalise le fait qu’il s’agit d’un arbre
de recherche.
L’arbre externe de recherche suivant :

4, 7, 10, 15, 20, 24, 27, 30, 35

se représente graphiquement par :


6. Ensembles de clés scalaires 179


• •
• • 27 •

4 7 10 • 30 35
• 24
15 20

6.5.2 Définition de la fonction d’abstraction


La fonction d’abstraction A décrit comment un arbre externe représente un
ensemble. L’arbre vide représente l’ensemble vide, une feuille n représente l’en-
semble {n}, enfin un arbre quelconque représente l’union des sous-ensembles
matérialisés par les sous-arbres gauche et droit.

function A(a) ∈ eAe  ensAbst(N) =



if a =  →

| a = n →
{n}
| a = g, d →
A(g) ∪ A(d)

Théorème 6 (des arbres externes binaires de recherche). Soit a = g, d ∈ eAe


et v ∈ N.
1. v ≤ max(A(g)) ⇒ v ∈
/ A(d),
2. v ≥ min(A(d)) ⇒ v ∈
/ A(g).

Ce théorème établit que si une valeur v est inférieure ou égale (resp. supérieure
ou égale) à la plus grande valeur du sous-arbre gauche (resp. à la plus petite
valeur du sous-arbre droit), elle n’appartient pas au sous-arbre droit (resp. au
sous-arbre gauche). Sa démonstration est laissée en exercice.

6.5.3 Spécification des opérations


La spécification du type concret eae limitée aux deux opérations concrètes
d’ajout et de suppression est présentée à la figure 6.5, page suivante. Elle cor-
respond au raffinement du type abstrait ensabst(N).

6.5.4 Calcul de la représentation des opérations concrètes


Le calcul de eAjout_ae va nous permettre de nous focaliser sur la ques-
tion suivante – qui se pose également dans le développement de l’opération
eSupp_ae : comment, à partir d’un nœud interne, savoir s’il faut se diriger
180 Structures de données et méthodes formelles

concreteType eae =  (eAe, (eV ide_ae, eAjout_ae, eSupp_ae),


(eApp_ae, eEstV ide_ae))
uses
bool, N
refines ensabst(N)
support
1)  ∈ eAe
2) n ∈ N ⇒ n ∈ eAe
3) g ∈ eAe − {} ∧ d ∈ eAe − {} ∧
max(A(g)) < min(A(d))

g, d ∈ eAe
abstractionFunction
function A(a) ∈ eAe  ensAbst(N) =  ...
operationSpecifications
..
.
function eAjout_ae(v, a) ∈ N × eAe → eAe =

b : (b ∈ eAe ∧ A(b) = eAjout(v, A(a)))
;
function eSupp_ae(v, a) ∈ N × eAe → eAe =

b : (b ∈ eAe ∧ A(b) = eSupp(v, A(a)))
..
.
end

Figure 6.5 – Spécification du type concret eae.


Il s’agit d’un spécification concrète du type ensabst de la figure 6.1,
page 149. Le support concret est un arbre externe de naturels. La fonc-
tion d’abstraction est détaillée page précédente.

vers le sous-arbre gauche ou vers le sous-arbre droit ? La technique utilisée est


typique de notre démarche (cf. sections 1.10 et 2.4) : nous supposons disponible
une certaine fonction (en l’occurrence ici la fonction qui délivre le plus grand
élément d’un arbre externe non vide). Nous vérifions que cette fonction est bien
O(1)-décomposable (cf. page 188). Le développement se déroule en utilisant cette
fonction. Au moment du raffinement final, un champ est ajouté à la structure de
données. Il est destiné à représenter la valeur de la fonction pour chaque nœud
de l’arbre considéré.

Calcul d’une représentation de l’opération eAjout_ae


A(eAjout_ae(v, a))
= Propriété caractéristique de l’opération eAjout_ae
A(a) ∪ {v} (6.5.3)

Pour poursuivre, nous procédons à une induction structurelle sur l’arbre a. Le


cas a =  est trivial et conduit à la première équation gardée :
6. Ensembles de clés scalaires 181

a =  →
eAjout_ae(v, a) = v

Pour le second cas de base, a = n, nous repartons de la formule 6.5.3


ci-dessus :

A(a) ∪ {v}
= Hypothèse
A(n) ∪ {v}
= Définition de A
A(n) ∪ A(v)

Trois cas sont à considérer selon la position relative de n et de v : n = v, n < v


et n > v. Si n = v, nous avons :

A(n) ∪ A(v)
= Définition de A
{n} ∪ {v}
= Hypothèse et propriété A.9
{n}
= Définition de A
A(n)

Soit encore A(a) d’après l’hypothèse. D’où l’équation gardée :

a = n →
n=v→
eAjout_ae(v, a) = a

Si n < v, nous avons :

A(n) ∪ A(v)
= Définition de A
A(n, v)

D’où l’équation gardée :

a = n →
v>n→
eAjout_ae(v, a) = n, v

Le cas v < n est symétrique.


Le cas inductif a = g, d se fonde sur l’hypothèse d’induction qui affirme
que nous savons insérer une valeur v dans le sous-arbre g aussi bien que dans le
sous-arbre d. Nous repartons de la formule 6.5.3 ci-dessus.

A(a) ∪ {v}
= Hypothèse
A(g, d) ∪ {v}
= Définition de A
A(g) ∪ A(d) ∪ {v}
182 Structures de données et méthodes formelles

A priori il est possible d’insérer v soit dans le sous-arbre g soit dans le sous-arbre
d. Cependant, il faut nous assurer que le conjoint 6.5.2 de la troisième clause
de la définition du support (cf. page 178) est satisfait. Pour cela, nous pouvons
distinguer les deux cas v ≤ max(A(g)) et v > max(A(g)). Considérons le cas
v ≤ max(A(g)).

A(g) ∪ A(d) ∪ {v}


= Propriété A.7
(A(g) ∪ {v}) ∪ A(d)
= Propriété caractéristique de l’opération eAjout_ae
A(eAjout_ae(v, g)) ∪ A(d)
= Définition de A
A(eAjout_ae(v, g), d)

D’où l’équation gardée :

a = g, d →
v ≤ max(A(g)) →
eAjout_ae(v, a) = eAjout_ae(v, g), d

Le cas v > max(A(g)) se traite facilement par symétrie. Nous obtenons :

function eAjout_ae(v, a) ∈ N × eAe → eAe = 


if a =  → /*vraie insertion*/
v
| a = n →
if v = n → /*fausse insertion*/
a
|v>n → /*vraie insertion à droite*/
n, v
|v<n → /*vraie insertion à gauche*/
v, n

| a = g, d →
if v ≤ max(A(g)) → /*insertion à gauche*/
eAjout_ae(v, g), d
| v > max(A(g)) → /*insertion à droite*/
g, eAjout_ae(v, d)

Pour le calcul de complexité, nous allons procéder comme nous l’avons fait
à la section 6.3.4, page 152, en passant par le calcul préalable de la complexité
moyenne C(n) de la construction d’un arbre externe de n valeurs (de n feuilles
donc), puis en calculant par différence la complexité moyenne I(n) de l’insertion
d’une valeur dans un arbre externe de n valeurs. La construction se fait selon le
modèle des arbres aléatoires déjà utilisé à la section 6.3.4 (dans un arbre de n
valeurs différentes, les n! permutations sont équiprobables).
6. Ensembles de clés scalaires 183

La première valeur i est insérée à la racine. La probabilité que i soit la ie plus


petite valeur de l’intervalle 1 .. n est la même pour tout i. Les deux sous-arbres
gauche et droit sont donc construits de la même façon avec un nombre de feuilles
respectif de i et de n−i. Dans la suite, nous supposons que la fonction eAjout_ae
est simplifiée afin de tenir compte de l’absence de doublons. Cette hypothèse n’a
pas d’incidence sur la complexité asymptotique. Construire un arbre d’un seul
nœud coûte une comparaison. Au total, le coût moyen de la construction d’un
arbre fini de n valeurs s’exprime par l’équation récurrente suivante :

⎨C(1) = 1

1
n−1

⎩ C(n) = n − 1 + · (C(i) + C(n − i)) pour n > 1
n−1 i=1

Le terme n − 1 provient du fait que pour toutes les insertions, sauf la première,
il faut ajouter une comparaison pour tenir compte du fait qu’il est nécessaire
d’évaluer une condition au niveau de la racine. Par ailleurs, puisque n > 1 et
qu’il s’agit d’arbres complets, l’insertion ne se fait jamais dans un sous-arbre
1
vide, ce qui justifie les bornes de la quantification ainsi que son facteur n−1 .
La recherche d’une forme close pour C(n) se fait, comme à la section 6.3.4,
en trois étapes. La première est la recherche d’une équation récurrente
n−1 d’ordre 1
pour C(n). Pour cela, nous exploitons d’une part le fait que i=1 (C(i) + C(n −
n−1 n−1
i)) = 2 · i=1 C(i) et d’autre part que, pour n−1 ≥ 1 (soit n ≥ 2), i=1 C(i) =
n−2
i=1 C(i) + C(n − 1). Nous avons alors, pour n ≥ 2 :

1
n−1
C(n) = n − 1 + · (C(i) + C(n − i))
n − 1 i=1
⇔ Première remarque ci-dessus
2
n−1
C(n) = n − 1 + · C(i)
n − 1 i=1
⇔ Arithmétique

n−1
(n − 1) · C(n) = (n − 1)2 + 2 · C(i) (6.5.4)
i=1
⇔ Seconde remarque ci-dessus

n−2
(n − 1) · C(n) = (n − 1)2 + 2 · C(i) + 2 · C(n − 1) (6.5.5)
i=1

À l’ordre n − 1, la formule 6.5.4 s’écrit :


n−2
(n − 2) · C(n − 1) = (n − 2)2 + 2 · C(i) (6.5.6)
i=1

Retranchons membre à membre 6.5.6 de 6.5.5 :

(n − 1) · C(n) − (n − 2) · C(n − 1) = (n − 1)2 − (n − 2)2 + 2 · C(n − 1)


⇔ Arithmétique
184 Structures de données et méthodes formelles

(n − 1) · C(n) = 2 · n − 3 + n · C(n − 1)
⇔ Division par n ·(n − 1) (n ≥ 2)
C(n) 2·n−3 C(n − 1)
= +
n n ·(n − 1) n−1

D’où (seconde étape) nous déduisons, par la méthode des facteurs sommants :

C(n) 2 · i − 3
n
= + 1 pour n ≥ 1
n i=2
i ·(i − 1)

Pour la troisième étape, et de même qu’à la section 6.3.4, nous exploitons


1
l’identité remarquable n ·(n−1) 1
= n−1 − n1 :

C(n)
n
= Arithmétique

n
1 n
1
2· −3· +1
i=2
i−1 i=2
i ·(i − 1)
= Identité remarquable

n
1 n
1 n
1
2· −3· +3· +1
i=2
i−1 i=2
i−1 i=2
i
= Arithmétique

n
1
n
1
− +3· +1
i=2
i−1 i=2
i
= Changement de variable

n−1
1 n
1
− + 3 ·( − 1) + 1
i=1
i i=1
i
k 1
Nous introduisons le nombre harmonique H(k) (qui est tel que H(k) = i=1 i ) :


n−1
1 n
1
− + 3 ·( − 1) + 1
i=1
i i=1
i
= Notation
−H(n − 1) + 3 · H(n) − 2
= Propriété de H(n) pour n > 1
−H(n − 1) + 3 ·(H(n − 1) + n1 ) − 2
= Arithmétique
3
2 · H(n − 1) + − 2
n
En multipliant chaque membre par n nous obtenons :

C(n) = 2 · n · H(n − 1) + 3 − 2 · n

Nous pouvons alors calculer le coût moyen de l’insertion I(n) en évaluant


l’expression C(n + 1) − C(n). Nous obtenons :
6. Ensembles de clés scalaires 185

2
I(n) = 2 · H(n − 1) +
n

Puisque H(k) = ln(k) + O(1) (cf. section 6.3.4), nous en déduisons que le coût
moyen de l’insertion est en O(log n). Le coût le meilleur est en O(1). Il est par
exemple atteint quand on insère 1 dans un arbre dans lequel on a déjà inséré
successivement les n − 1 valeurs de l’intervalle 2 .. n. Le coût le pire est en
O(n). Il est atteint quand on insère n dans un arbre dans lequel on a déjà inséré
successivement les n − 1 valeurs de l’intervalle 1 .. n − 1.

Calcul d’une représentation de l’opération eSupp_ae


A(eSupp_ae(v, a))
= Propriété caractéristique de l’opération eSupp_ae
A(a) − {v} (6.5.7)

À nouveau nous procédons à une induction sur la structure de a. Débutons


par le cas a = .

A(a) − {v}
= Hypothèse et définition de A
∅ − {v}

Nous avons déjà rencontré une situation semblable lors du calcul de l’opération
eSupp_a (cf. section 6.3.4, page 161). Nous obtenons l’équation gardée :

a =  →
eSupp_ae(v, a) = 

Abordons à présent le cas a = n.

A(a) − {v}
= Hypothèse et définition de A
{n} − {v} (6.5.8)

Il nous faut procéder à une analyse par cas, en distinguant les deux cas n = v
et n = v. Si v = n, nous avons :

{n} − {v}
= Hypothèse et propriété A.18

= Définition de A
A()

qui conduit à l’équation gardée suivante :

a = n →
n=v →
eSupp_ae(v, a) = 
186 Structures de données et méthodes formelles

Le cas n = v se traite de manière analogue en repartant de la formule 6.5.8 :


{n} − {v}
= Hypothèse et propriété A.16
{n}
= Définition de A
A(n)
Soit encore A(a) d’après l’hypothèse. D’où l’équation gardée :
a = n →
n = v →
eSupp_ae(v, a) = a
Le cas inductif a = g, d se traite comme suit, en repartant de la formule 6.5.7
ci-dessus :
A(a) − {v}
= Hypothèse a = g, d
A(g, d) − {v}
= Définition de A et propriété A.17
(A(g) − {v}) ∪ (A(d) − {v})
Deux cas sont à considérer, v ≤ max(A(g)) et v > max(A(g)). Débutons par
le cas v ≤ max(A(g)) (nous savons alors, d’après le théorème 6, page 179, que
v∈ / A(d)) :
(A(g) − {v}) ∪ (A(d) − {v})
= Propriété A.16 et théorème 6
(A(g) − {v}) ∪ A(d)
= Propriété caractéristique de l’opération eSupp_ae
A(eSupp_ae(v, g)) ∪ A(d) (6.5.9)

À ce stade nous ne pouvons cependant pas utiliser aveuglément la fonction d’abs-


traction car le support exige qu’aucun des deux arbres ne soit vide. Nous en
sommes sûr pour d, par contre, il faut procéder à une analyse par cas pour
eSupp_ae(v, g). Considérons tout d’abord le cas eSupp_ae(v, g) = .
A(eSupp_ae(v, g)) ∪ A(d)
= Hypothèse
A() ∪ A(d)
= Définition de A et propriétés A.7 et A.9
A(d)
D’où l’équation gardée :
a = g, d →
v ≤ max(A(g)) →
let r := eSupp_ae(v, g) in
r =  →
eSupp_ae(v, a) = d
end
6. Ensembles de clés scalaires 187

Le second cas, eSupp_ae(v, g) = , se fonde sur le cas général de la fonction


d’abstraction. Nous obtenons facilement l’équation gardée :
a = g, d →
v ≤ max(A(g)) →
let r := eSupp_ae(v, g) in
r =  →
eSupp_ae(v, a) = r, d
end
Le cas v > max(A(g)) s’obtient par symétrie. Au total, nous avons calculé la
représentation suivante de l’opération eSupp_ae(v, a) :

function eSupp_ae(v, a) ∈ N × eAe → eAe = 


if a =  → /*fausse suppression*/

| a = n →
if v = n → /*vraie suppression*/

| v = n → /*fausse suppression*/
a

| a = g, d →
if v ≤ max(A(g)) → /*suppression à gauche*/
let r := eSupp_ae(v, g) in
if r =  →
d
| r =  →
r, d

end
| v > max(A(g)) → /*suppression à droite*/
..
.

Ainsi qu’il était prévisible, l’algorithme de suppression est plus simple que dans
le cas des abr. En effet, contrairement aux abr, toutes les valeurs utiles sont
situées aux feuilles. La question du devenir des sous-arbres du nœud supprimé ne
se pose pas. Dans l’hypothèse où l’évaluation des gardes contenant l’expression
max(A(. . .)) coûte une comparaison, le coût moyen de la suppression est en
O(log(n)). Le coût le meilleur est en O(1) tandis que le coût le pire est en O(n).

6.5.5 Renforcement du support par décomposition de la


fonction max
Les représentations des opérations eAjout_ae et eSupp_ae calculées ci-
dessous ne peuvent être laissées en l’état. La raison est que la fonction max
188 Structures de données et méthodes formelles

Fonctions décomposables
Considérons par exemple les arbres eAbr définis dans le support du
type eavl (cf. section 6.6.2, page 192). À l’exception des arbres vides
() ces arbres présentent trois champs (g, n, d) : un champ sous-arbre
gauche g, un champ sous-arbre droit d et un troisième champ n, de type
entier naturel. Soit T un type quelconque ; une fonction f ∈ eAbr → T est
dite O(1)-décomposable sur eAbr (cf. [60, 61, 68]) s’il existe une opération
⊕ ∈ T → T dont la complexité temporelle est en O(1) telle que

f (g, n, d) = f (g) ⊕ f (d)

Ainsi par exemple la fonction w qui délivre le poids d’un arbre binaire
(c’est-à-dire le nombre de nœuds présents dans l’arbre, cf. section 3.5.2)
est O(1)-décomposable puisque l’opération ⊕, représentée ici par l’addi-
tion +, se comporte en O(1).
L’avantage de travailler avec une fonction O(1)-décomposable est qu’il
est possible de faire abstraction de sa mise en œuvre jusqu’au moment des
choix d’implantation, où il suffit d’enrichir la représentation d’un nœud
par un champ supplémentaire destiné à contenir la valeur de la fonc-
tion pour ce nœud (ou permettant d’y accéder aisément). Il s’agit d’une
opération intitulée « décomposition d’une fonction sur une structure de
données ». Toute mise à jour se fait en appliquant simplement la fonction
⊕ avec comme argument le nouveau champ des nœuds fils.
Cette technique est à la base des algorithmes de programmation dy-
namique. Nous l’appliquons également lors de la recherche de solutions
efficaces pour le type abstrait « tableaux fortement flexibles » (cf. sec-
tion 10.5), ainsi que dans les raffinements utilisant les arbres externes.

utilisée dans ces représentations conduit à des calculs trop coûteux puisqu’ils
obligent à descendre jusqu’à une feuille à chaque évaluation.
Nous avons affirmé ci-dessus, lors du calcul de l’opération eAjout_ae, que
nous pouvions différer la résolution du problème posé en renforçant le support
grâce au caractère O(1)-décomposable de la fonction max. C’est le moment d’y
revenir. L’évaluation de max(A(g)) délivre la plus grande valeur du sous-arbre
gauche d’un arbre eAe. Or la fonction max appliquée à un nœud interne g, d
d’un arbre eAe est telle que

max(A(g, d)) = max(A(d))

max est donc O(1)-décomposable. Nous pouvons modifier le support concret en


y ajoutant un champ qui représente la plus grande valeur présente dans l’arbre.
Sur l’exemple de la page 178, introduire cette modification revient à disposer de
l’arbre suivant (sur le schéma, seuls les nœuds internes sont modifiés, concernant
les feuilles, la valeur du nouveau champ est celle initialement présente dans le
nœud) :
6. Ensembles de clés scalaires 189

35
24 35
7 24 27 35
4 7 10 24 30 35
20 24
15 20

La nature de l’arbre facilite la définition du nouveau support : d’une part un


arbre g, d n’a jamais de sous-arbre vide et d’autre part la plus grande valeur
est toujours dans le sous-arbre droit.

1)  ∈ eAe
2) n ∈ N ⇒ (n, n) ∈ eAe
3) g ∈ eAe − {} ∧ d ∈ eAe − {} ∧
max(A(g)) < min(A(d)) ∧
m = max(A(d))

(g, d, m) ∈ eAe

La fonction d’abstraction est modifiée en conséquence. Son utilité étant à pré-


sent réduite, la nouvelle version n’est pas présentée. Sur le plan de la conception
des algorithmes, la modification est mineure et n’exige pas de refaire le calcul
des opérations. Nous présentons ci-dessous la nouvelle version de l’opération
eAjout_ae. Le cas des autres opérations est laissé en exercice.

function eAjout_ae(v, a) ∈ N × eAe → eAe =



if a =  →
(v, v)
| a = (n, n) →
if v = n →
a
|v>n →
(n, v, v)
|v<n →
(v, n, n)

| a = (g, d, m) →
190 Structures de données et méthodes formelles

let (l, mg) := g in


if v ≤ mg →
(eAjout_ae(v, g), d , m )
| v > mg →
(g, eAjout_ae(v, d) , max({v, m}) )

end

Une vraie adjonction au niveau des feuilles conduit à créer un nœud interne.
La valeur du nouveau champ pour ce nœud dépend de la position relative de
la feuille et de la valeur ajoutée v. Concernant l’ajout dans un nœud interne,
l’adjonction à gauche n’a pas d’impact sur le nouveau champ, par contre, pour
l’adjonction à droite, une mise à jour est nécessaire si la valeur ajoutée v est la
plus grande du nouvel arbre.

Remarque. Nous avons dérogé au principe consistant à enregistrer la valeur


de la fonction à décomposer dans un champ de la structure, puisque nous enregis-
trons max(A(a)) et non max(A(g)). Mais si nous voulions véritablement placer
la valeur max(A(g)) dans un nouveau champ de g, d, nous devrions (pour res-
ter efficace) enregistrer également la valeur de max(A(d)). En effet, ainsi que le
montre la figure ci-dessous, qui reprend l’arbre de la page précédente mais avec
strictement la valeur du sous-arbre gauche placée à chaque nœud interne, en cas
de suppression de la valeur 24, il serait nécessaire de remplacer la valeur 24 de
la racine par la nouvelle plus grande valeur du sous-arbre gauche, soit 20, valeur
qui n’est pas accessible en temps constant.

24
7 27
4 10 27 30
4 7 10 20 30 35
15 24
15 20

6.5.6 Conclusion et remarques bibliographiques


La technique des arbres externes pour représenter des sous-ensembles de Z est
originale et peu pratiquée sous cette forme. Son coût est en général légèrement
supérieur à celui des abr. Il suffit pour s’en convaincre de comparer les valeurs de
l’insertion moyenne I(n) dans chacun des cas. Mais, outre le fait que les arbres
externes constituent une base sur laquelle s’appuyer pour réaliser d’excellents
exercices sur les structures de données, le principe présente des avantages lorsqu’il
6. Ensembles de clés scalaires 191

s’agit de distinguer la partie « index » de la partie « données » dans une structure


enregistrée sur deux niveaux de mémoire (mémoire vive et disque par exemple).
De plus les suppressions sont facilitées par rapport aux arbres classiques puisque,
par construction, la valeur à éliminer est toujours située sur une feuille. Nous
verrons également que l’idée peut être exploitée avec profit pour la mise en œuvre
de tableaux fortement flexibles (cf. section 10.5) ou d’une variante des B-arbres.
La technique utilisée ci-dessus, consistant à réaliser un premier développe-
ment par arbres externes stricts, puis à raffiner explicitement en renforçant le
support par décomposition d’une information utile sur la structure de données,
a été proposée initialement par A. Kaldewaij et V.J. Dielissen [68]. Cependant
le principe de la décomposition est bien antérieur et est utilisé plus ou moins
consciemment, par exemple pour la mise en œuvre des B+ -arbres. Voir aussi
[116] pour un autre exemple concernant les minimiers de position.

Exercices

Exercice 6.5.1 Lors de l’étude des abr, nous nous sommes intéressés à différentes formes d’in-
sertions outre l’insertion aux feuilles. Ceci n’a pas lieu d’être dans le cas des arbres externes de
recherche. Pourquoi ? Nous pouvons tout de même nous poser la question du partitionnement
d’un ensemble en deux sous-ensembles par rapport à une valeur donnée v.
1. Spécifier, au niveau du type abstrait, l’opération de partitionnement.
2. Calculer une représentation de l’opération dans le cas d’une mise en œuvre par arbres
externes.

Exercice 6.5.2 Spécifier l’opération max puis calculer sa représentation max_ae dans le cas
d’une mise en œuvre par arbres externes.

6.6 Méthodes équilibrées : les Avl


6.6.1 Introduction
L’utilisation d’un arbre binaire de recherche ou d’un arbre externe de n
feuilles pour représenter un ensemble ne garantit pas que la complexité des opé-
rations ne dépassera pas O(log n). Comment obtenir cette assurance ? L’idéal
serait de disposer toujours d’un arbre p-équilibré (équilibré en poids) pour le-
quel la recherche s’effectue en O(log n). L’inconvénient qui en résulte est que la
conservation de cette propriété (très forte pour un abr) lors d’une mise à jour
est techniquement difficile et entraîne une complexité supérieure à O(log n).
L’idée qui sous-tend cette section est de conserver la notion d’abr tout en
lui superposant une forme affaiblie d’équilibre compatible avec des mises à jour
efficaces (c’est-à-dire en O(log n)).
Dans le cas d’arbres n-aires quelconques, deux types de solutions existent
selon que l’affaiblissement s’effectue en « largeur » ou en « hauteur ».
– En largeur : on admet un nombre variable de fils par nœud en optant pour
des :
– arbres 2-3,
– arbres 2-3-4,
192 Structures de données et méthodes formelles

– B-arbres (cf. section 6.7),


– etc.
– En hauteur : on admet un déséquilibre borné (par rapport à l’équilibre
idéal : le p-équilibre). On peut retenir un arbre de recherche h-équilibré
(équilibré en hauteur). C’est la solution Avl.

Le principe de base des Avl consiste à maintenir invariante, sur tous les
nœuds de l’arbre, la propriété d’équilibre suivante : « la différence de rayon
entre le sous-arbre gauche et le sous-arbre droit ne dépasse jamais 1 en valeur
absolue. »
Traditionnellement, en programmation impérative, la mise en œuvre des opé-
rations de mise à jour se fait selon une discipline « modification in situ » en in-
tégrant dans chaque nœud, non pas le rayon mais la différence entre les rayons.
Lors d’une insertion, un paramètre de sortie signale à l’appelant si l’opération a
ou non provoqué une augmentation du rayon. Si c’est le cas et que ceci conduit
à violer la propriété d’équilibre, une opération de rééquilibrage (rotation simple
ou double) est appliquée.
Nous adoptons ici une démarche différente, qui tend à ne pas faire de choix
prématurés. C’est en particulier le cas en ce qui concerne le mode d’exploitation
du rayon (cf. [47], ainsi que [28] pour une approche similaire). Pour l’algorithme
d’insertion eAjout_v, le résultat obtenu présente des originalités par rapport à
l’algorithme traditionnel 5 dans la mesure où, grâce au caractère persistant des
structures de données fonctionnelles, il est possible de connaître simultanément
le rayon d’un arbre avant et après l’insertion (le paramètre de sortie mentionné
plus haut devient inutile).

6.6.2 Définition et propriétés du support concret


Nous allons ci-dessous spécifier et représenter le type eavl, de support eAvl,
qui raffine par Avl le type abstrait ensabst appliqué aux entiers naturels. Un
Avl est un abr équilibré en hauteur. Formaliser le support correspondant d’une
part exige de définir ce qu’est un abr (cf. section 6.3, page 150) et d’autre part,
puisque la hauteur n’est en toute rigueur pas définie pour un arbre vide (cf. sec-
tion 3.5.2), de préférer l’usage de la fonction r qui délivre le rayon d’un arbre
binaire. Tout le développement est fait en considérant que cette fonction r est
disponible. Au stade de la mise en œuvre, après avoir constaté que l’invocation
fréquente de la fonction r est rédhibitoire sur le plan de la complexité tempo-
relle et après s’être assuré de son caractère O(1)-décomposable (cf. sections 1.10
et 2.4, et encadré page 188), nous décidons de renforcer le support en décompo-
sant cette fonction sur la structure de données afin que l’accès à cette information
s’effectue simplement en consultant le nouveau champ.
Un Avl étant un arbre binaire de recherche respectant une contrainte liée au
rayon, nous renvoyons le lecteur à la définition du support des abr, section 6.3.1
(voir également la rubrique auxiliarySupports de la figure 6.6, page 197). Le
support eAvl d’un Avl se définit inductivement par :

5. Il est cependant de même complexité asymptotique temporelle.


6. Ensembles de clés scalaires 193

1)  ∈ eAvl
2) g, n, d ∈ eAbr ∧ g ∈ eAvl ∧ d ∈ eAvl ∧ r(g) − r(d) ∈ −1 .. 1

g, n, d ∈ eAvl

La clause 2) formalise la condition qu’il faut imposer à un abr pour être aussi
un Avl. Le conjoint r(g) − r(d) ∈ −1 .. 1 rend compte du fait que l’arbre est
équilibré en hauteur.
Trois exemples sont présentés ci-dessous. Tous trois concernent des abr, mais
seuls les deux premiers sont des Avl. La valeur d’équilibre en hauteur est apposée
à chaque nœud : +1 (resp −1) pour un déséquilibre en faveur du sous-arbre
gauche (resp. droit), 0 pour un nœud équilibré. Concernant le troisième arbre,
le (seul) nœud en défaut d’équilibre Avl est désigné par une flèche grisée.

23+1 12−1

17+1 31−1 7−1 22−1

10−1 20+1 270 370 40 8−1 15−1 30−1

50 14+1 190 340 410 110 170 270 31−1

120 340
Un Avl Un Avl

190

10+1 27−2

5−1 140 230 34+1

30 8+1 110 160 300 380

70 290 320

Un arbre abr qui n’est pas un Avl

Les contraintes imposées aux arbres h-équilibrés permettent de déterminer les


limites entre lesquelles varie le rayon d’un Avl. Nous avons le théorème suivant :
Théorème 7 (Adelson-Velsky et Landis). Soit n le poids d’un arbre h-équilibré
et r son rayon.
log(n + 1) ≤ r < 1, 4403 · log(n + 1) (6.6.1)
Si nous parvenons à démontrer ce théorème 6 , nous saurons en particulier que
pour un arbre h-équilibré, son rayon est en O(log n) (et même en Θ(log n)) avec
de surcroît un facteur multiplicatif proche de 1. Nous aurons la certitude que
6. Rappelons que par convention log exprime un logarithme à base 2.
194 Structures de données et méthodes formelles

la recherche dans un Avl est toujours très efficace et qu’il est donc inutile de
nous lancer dans un calcul de complexité moyenne. Par contre, rien ne garantit
a priori qu’il existe des algorithmes de mise à jour permettant de préserver la
propriété d’équilibre ni, si c’était bien le cas, que ces algorithmes sont eux-mêmes
en O(log n).
Démontrons à présent la première inégalité de la propriété 6.6.1. Quels sont
les arbres h-équilibrés qui, pour un rayon r donné, possèdent un nombre de
nœuds maximum ? Ce sont les arbres pleins (cf. page 89). Un tel arbre satisfait
l’équation récurrente suivante, dans laquelle p (r) est le poids d’un arbre plein
de rayon r :
 
p (0) = 0
p (r) = p (r − 1) + p (r − 1) + 1 pour r > 0

En effet, le poids d’un arbre vide (de rayon 0) est 0 et par ailleurs la seconde
formule rend compte du fait que les deux sous-arbres d’un arbre plein sont eux-
mêmes des arbres pleins. La solution de cette équation s’obtient facilement par
la méthode des facteurs sommants : p (r) = i=0 2i , soit encore p (r) = 2r − 1.
r−1

Et donc tout arbre h-équilibré de rayon r est tel que son poids n est plus petit ou
égal à celui de l’arbre plein de même hauteur : n ≤ p (r). D’où le développement
suivant :
n ≤ p (r)
⇔ Calcul ci-dessus
n ≤ 2r − 1
⇔ Arithmétique
n + 1 ≤ 2r
⇔ Propriété du log
log(n + 1) ≤ r
Ce qui établit la première inégalité de la formule 6.6.1.

Pour démontrer la seconde inégalité de la formule, nous utilisons une ap-


proche séduisante par sa simplicité, inspirée de [26]. Quels sont les arbres
h-équilibrés qui, pour un rayon donné r, possèdent un nombre minimum de
nœuds ? Ce sont des arbres dans lesquels tout nœud (à l’exception des feuilles)
présente un déséquilibre de +1 ou de −1. En effet, supprimer une feuille dans un
tel arbre soit diminue son rayon soit augmente le déséquilibre d’un nœud et fait
perdre à l’arbre sa propriété de h-équilibre. Si p (r) est le poids d’un tel arbre
et r son rayon, p (r) satisfait l’équation récurrente suivante :
⎧ 
⎨p (0) = 0
p (1) = 1
⎩ 
p (r) = p (r − 1) + p (r − 2) + 1 pour r > 1
En effet, le poids d’un arbre vide est 0, celui d’un arbre de rayon 1 est 1. La
troisième formule rend compte du fait que dans un tel arbre les deux sous-arbres
sont de même nature, que l’un a un rayon de r − 1 et l’autre un rayon de r − 2.

1+ 5
Lemme 1. Soit φ = . Pour n ≥ 2 :
2
6. Ensembles de clés scalaires 195

φn = φn−1 + φn−2 (6.6.2)

La démonstration (par récurrence) est laissée en exercice. La valeur φ est appelée


nombre d’or (cf. [122] par exemple pour de plus amples développements sur les
propriétés de ce nombre) et est approximativement égale à 1, 6180. D’autre part
φ est tel que 1, 4402 < log1 φ < 1, 4403.

Lemme 2. Pour tout r ≥ 0 :

p (r) ≥ φr − 1 (6.6.3)

La démonstration se fait par récurrence sur r. Posons P =  p (r) ≥ φr − 1.



Compte tenu de la forme de l’équation récurrente pour p nous prenons comme
proposition à démontrer P ∧ [r := r + 1]P . Le principe de récurrence simple
(cf. théorème 2, page 44) nous conduit alors à démontrer :
1. l’étape de base [r := 0](P ∧ [r := r + 1]P ),
2. l’étape inductive [r := r + 1](P ∧ [r := r + 1]P ) sous les hypothèses r ∈ N
et P ∧ [r := r + 1]P .
Le cas de base est simple à démontrer :

[r := 0](P ∧ [r := r + 1]P )
⇔ Substitution
[r := 0]P ∧ [r := 1]P
⇔ Définition de P et substitution
p (0) ≥ φ0 − 1 ∧ p (1) ≥ φ1 − 1
⇔ Définition de p et substitution
0≥1−1 ∧ 1≥φ−1
⇔ Définition de φ et calcul sur R+


Concernant l’étape inductive de la démonstration, les hypothèses sont d’une part


r ∈ N et d’autre part (hypothèse d’induction) P ∧ [r := r + 1]P , tandis que nous
devons démontrer [r := r + 1](P ∧ [r := r + 1]P ) :

[r := r + 1](P ∧ [r := r + 1]P )
= Définition de la substitution et arithmétique
[r := r + 1]P ∧ [r := r + 2]P

Nous constatons que le premier facteur à démontrer est déjà en hypothèse, il suffit
donc de démontrer [r := r + 2]P , soit encore p (r + 2) ≥ φr+2 − 1. Débutons le
calcul avec p (r + 2) :

p (r + 2)
= Définition de p (r ∈ N)
 
p (r + 1) + p (r) + 1
≥ Propriété 6.6.3
φr+1 − 1 + φr − 1 + 1
= Calcul sur R+ et propriété 6.6.2
φr+2 − 1
196 Structures de données et méthodes formelles

Cette inégalité vaut pour un arbre h-équilibré ne contenant aucun nœud interne
équilibré. Pour un arbre h-équilibré quelconque de poids n nous avons n ≥
p (r) ≥ φr − 1. D’où :

φr − 1 ≤ n
⇔ Calcul sur R+
φr ≤ n + 1
⇔ Propriété du logφ
r ≤ logφ (n + 1)
⇔ Propriété du log
log(n + 1)
r≤
log φ
⇒ Valeur de 1
log φ et calcul sur R+
r < 1, 4403 · log(n + 1)

Le théorème est prouvé. Nous pouvons donc affirmer que le rayon d’un arbre
h-équilibré (et donc d’un Avl) ne fait jamais plus de 45% de plus que la hauteur
de son homologue p-équilibré.

6.6.3 Définition de la fonction d’abstraction


Alors que jusqu’à présent les fonctions d’abstraction rencontrées avaient
comme domaine le support du type considéré, nous sommes contraint cette fois
de déroger à cette règle. En effet, lors des mises à jour, l’arbre manipulé, tout en
restant un abr, peut momentanément ne plus être un Avl. Il faut malgré tout
raisonner sur la structure abstraite qu’il représente. C’est pourquoi nous retrou-
vons ici la fonction d’abstraction A déjà rencontrée dans la section portant sur
la représentation des ensembles par abr (cf. page 151).

function A(a) ∈ eAbr  ensAbst(N) =



if a =  →

| a = g, n, d →
A(g) ∪ {n} ∪ A(d)

Notons que A est surjective : tout élément de ensAbst(N) peut être représenté
par un arbre eAbr. Il en est de même de sa restriction au support eAvl. Par
ailleurs sa corestriction à ∅ est injective (la seule solution à l’équation A(x) = ∅
est x = ).

6.6.4 Spécification des opérations concrètes


La figure 6.6, page ci-contre, synthétise les principales informations sur la
spécification du type eavl. Le nom des opérations dérive du nom des opéra-
tions abstraites par ajout du suffixe _v. La fonction auxiliaire r, qui délivre le
rayon d’un arbre binaire, est supposée disponible telle qu’elle a été définie à la
section 3.5.2.
6. Ensembles de clés scalaires 197

concreteType eavl =  (eAvl, (eV ide_v, eAjout_v, eSupp_v),


(eApp_v, eEstV ide_v))
uses bool, N
refines ensabst(N)
auxiliarySupports
1)  ∈ eAbr
2) n ∈ N ∧ g ∈ eAbr ∧ d ∈ eAbr ∧ max(A(g)) < n ∧ min(A(d)) > n

g, n, d ∈ eAbr
support
1)  ∈ eAvl
2) g, n, d ∈ eAbr ∧ g ∈ eAvl ∧ d ∈ eAvl ∧ r(g) − r(d) ∈ −1 .. 1

g, n, d ∈ eAvl
abstractionFunction
function A(a) ∈ eAbr  ensAbst(N) =  ...
operationSpecifications
..
.
function eAjout_v(v, a) ∈ N × eAvl → eAvl =

b : (b ∈ eAvl ∧ A(b) = eAjout(v, A(a)))
;
function eSupp_v(v, a) ∈ N × eAvl → eAvl =

b : (b ∈ eAvl ∧ A(b) = eSupp(v, A(a)))
..
.
end

Figure 6.6 – Spécification du type concret eavl.


Il s’agit d’un spécification concrète du type ensabst de la figure 6.1,
page 149. Cette spécification exige un support auxiliaire puisque la fonc-
tion d’abstraction est définie sur les abr, et non sur les Avl. La fonction
d’abstraction est détaillée page ci-contre.

6.6.5 Rotations
À la section 3.5.2, page 94, nous avons étudié le principe des rotations ap-
pliquées aux arbres binaires. Notre motivation première était la préservation du
parcours infixé. Nous avons cependant mentionné d’autres usages dont certains
sont en relation avec les Avl. C’est le moment d’y revenir.
Cette section est organisée de la manière suivante. Nous revenons tout
d’abord sur la définition de la rotation droite 7 pour un abr (et donc pour un
Avl) en rappelant la propriété de l’invariance du parcours infixé. Nous nous
focalisons ensuite sur les propriétés des rotations appliquées aux Avl, selon le
contexte d’utilisation.

7. La rotation gauche se définit se manière similaire et possède des propriétés analogues.


198 Structures de données et méthodes formelles

Rotation simple droite

L’opération de rotation droite rd introduite à la section 3.5.2, page 91, est


présentée ci-dessous sous sa forme monolithique pour des abr :

function rd(g, u, d) ∈ eAbr → eAbr =



pre
g = 
then
let gg , ug , dg  := g in
gg , ug , dg , u, d
end
end

Théorème 8. Soit a ∈ eAbr, a = gg , ug , dg , u, d alors A(a) = A(rd(a)) (la


rotation droite préserve la structure abstraite).

La démonstration de ce théorème et des théorèmes 9, 10, 11 et 12 est l’objet de


l’exercice 6.6.1, page 215.

Le contexte d’application de l’opération rd que nous considérons est celui


où l’arbre présente un déséquilibre de 2 en faveur du sous-arbre gauche (soit
r(g) − r(d) = 2). Nous verrons que c’est la valeur maximale que peut atteindre
le déséquilibre lors d’une mise à jour. L’arbre considéré n’est alors plus un Avl. Si
la seule entorse à la propriété d’être un Avl se limite à ce déséquilibre de 2, il est
possible de rétablir l’équilibre par une rotation. Trois sous-cas de rotation droite
doivent alors être envisagés selon le propre déséquilibre de g. Ils correspondent
aux trois théorèmes qui suivent.

Théorème 9. Soit a ∈ eAbr, a = g, u, d, g ∈ eAvl, d ∈ eAvl, r(g) − r(d) = 2,


g = gg , ug , dg  et r(gg ) = r(dg ) + 1. Alors :
1. rd(a) ∈ eAvl (la rotation droite rétablit l’équilibre Avl),
2. r(rd(a)) = r(a) − 1 (la rotation droite fait diminuer de 1 le rayon de
l’arbre).

Le schéma ci-dessous illustre cette situation ainsi que le résultat de l’applica-


tion de l’opération rd. Cette situation se caractérise par le fait que le déséquilibre
à la racine est de 2 et que le déséquilibre du sous-arbre gauche est du même signe.
L’arbre de racine ug ainsi que les trois arbres gg , dg et d sont supposés être des
Avl. Le rayon des arbres gg , dg et d est noté en bas des rectangles les maté-
rialisant, et la valeur d’équilibre est apposée à chaque nœud. Le résultat de la
rotation est représenté dans la partie droite du schéma. L’arbre est devenu un
Avl, son rayon diminue de 1.
6. Ensembles de clés scalaires 199

Le rôle des schémas en spécification


Si l’on considère la situation des schémas en tant qu’outil de repré-
sentation de concepts (et notamment de structures de données), les in-
formaticiens se classent selon deux catégories : les « doctrinaires » et les
« pragmatiques ». Les premiers considèrent que, puisqu’un schéma ne re-
présente jamais qu’une situation (et non un ensemble de situations), leur
utilisation est trompeuse : ils sont donc à proscrire dans toute modélisa-
tion. E.W. Dijkstra [30] (1930-2002) était un ardent défenseur de ce point
de vue.
À l’inverse, les « pragmatiques » considèrent que dans toute démarche
de développement le point de départ est une description informelle (pou-
vant contenir des schémas) et que, même après avoir formalisé l’applica-
tion, la description initiale ne doit pas être rejetée, car elle présente encore
un intérêt (au moins pour faciliter la compréhension du problème). J.-R.
Abrial, à travers les cours dispensés à l’iut de Nantes [4], montre qu’il
est un tenant de ce point de vue. Nous nous plaçons également dans cette
catégorie.

u 2 ug 0

ug 1 u 0

d gg
dg dg d
gg k−1
k−1 k k−1 k−1
k
Avant rotation Après rotation

Abordons maintenant le cas où le sous-arbre gauche est équilibré (r(gg ) =


r(dg )).

Théorème 10. Soit a ∈ eAbr, a = g, u, d, g ∈ eAvl, d ∈ eAvl, r(g)−r(d) = 2,


g = gg , ug , dg  et r(gg ) = r(dg ). Alors :
1. rd(a) ∈ eAvl (la rotation droite rétablit l’équilibre Avl),
2. r(rd(a)) = r(a) (la rotation droite ne change pas le rayon de l’arbre).

Le schéma ci-dessous illustre ce théorème. Nous verrons que ce théorème


ne s’applique pas dans le cas d’une adjonction (puisque le sous-arbre excéden-
taire est toujours déséquilibré). Par contre, il peut s’appliquer dans le cas d’une
suppression.
200 Structures de données et méthodes formelles

u 2 ug -1

ug 0 u 1

d gg
gg d
dg k−1 dg
k k−1
k k k
Avant rotation Après rotation

Le dernier cas, qui se caractérise par un sous-arbre gauche déséquilibré en


faveur de la droite, est pris en compte par le théorème suivant.

Théorème 11. Soit a ∈ eAbr, a = g, u, d, g ∈ eAvl, d ∈ eAvl, r(g)−r(d) = 2,


g = gg , ug , dg  et r(gg ) = r(dg ) − 1. Alors rd(a) ∈
/ eAvl (l’arbre résultant de la
rotation n’est pas un Avl).

Le schéma ci-dessous illustre informellement ce théorème.

u 2 ug -2

ug -1 u 1

d gg
gg d
dg k−1 k−1 dg
k−1 k−1
k k
Avant rotation Après rotation

Cette situation peut survenir aussi bien dans le cas d’une adjonction que dans
celui d’une suppression. La rotation simple n’est donc pas une solution. Il faut
nous tourner vers une rotation double.

Rotation double gauche-droite


La rotation gauche-droite rgd utilisée dans la gestion des Avl se définit
comme suit (cf. section 3.5.2 pour une présentation générale de la rotation
gauche-droite) :

function rgd(gg , ug , dg , u, d) ∈ eAbr →


 eAbr =

pre
r(gg , ug , dg ) − r(d) = 2 ∧ gg = 
then
rd(rg(gg , ug , dg ), u, d)
end
6. Ensembles de clés scalaires 201

La précondition implique que le sous-arbre gauche n’est pas vide. Le théorème


suivant montre que, dans le cas d’un déséquilibre de 2 de la racine en faveur du
sous-arbre gauche, lorsqu’il est impossible de rééquilibrer par une rotation simple
rd alors une rotation double rgd permet le rééquilibrage Avl ; en outre l’arbre
diminue de rayon.

Théorème 12 (De la rotation gauche-droite des Avl). Soit a = g, u, d, a ∈


eAbr, g = gg , ug , dg , g ∈ eAvl, d ∈ eAvl et a satisfait la précondition de
l’opération rgd. Alors :
1. A(rgd(a)) = A(a) (la rotation gauche-droite préserve la structure abs-
traite),
2. rgd(a) ∈ eAvl (le résultat de la rotation gauche-droite est un Avl),
3. r(rgd(a)) = r(a) − 1 (la rotation gauche-droite fait diminuer de 1 le rayon
de l’arbre).

Les trois schémas qui suivent illustrent les trois cas qui justifient une rotation
gauche-droite. Ils se caractérisent par le fait que le déséquilibre à la racine est
de 2 et que le déséquilibre du sous-arbre gauche est de signe opposé. Les arbres
ayant comme racine ug et ugd ainsi que les quatre arbres gg , ggd , dgd et d sont
supposés être des Avl. Le rayon des arbres gg , ggd , dgd et d est noté en bas du
rectangle les matérialisant et la valeur d’équilibre est apposée à chaque nœud.
Le résultat de la rotation est représenté dans la partie droite du schéma. Dans
chacun des cas, l’arbre est devenu un Avl et son rayon diminue de 1.
Le premier schéma illustre le cas où, avant rotation, le sous-arbre droit du
sous-arbre gauche, bien que toujours un Avl, est déséquilibré en faveur de la
droite.

u 2 u gd 0

ug -1 ug 1 u 0
ugd -1
d g gd
gg gg d gd d
g gd
d gd k k−1
k k−1 k k k
k
Avant rotation Après rotation

Le schéma ci-dessous illustre le cas où, avant rotation, le sous-arbre droit du


sous-arbre gauche est équilibré.
202 Structures de données et méthodes formelles

u 2 u gd 0

ug -1 ug 0 u 0
ugd 0
d
gg gg g gd d gd d
g gd d gd k
k k k k k
k k
Avant rotation Après rotation

Enfin le schéma ci-dessous illustre le cas où, avant rotation, le sous-arbre


droit du sous-arbre gauche, bien que toujours un Avl, est déséquilibré en faveur
de la gauche.
u 2 u gd 0

ug -1 ug 0 u -1
ug d 1
d
d gd
gg gg g gd d
d gd
g gd k k−1
k k−1 k k k
k
Avant rotation Après rotation

6.6.6 Calcul de la représentation des opérations concrètes


Le calcul d’une représentation des opérations eV ide_v, eApp_v et
eEstV ide_v est laissé en exercice. Nous nous limitons au calcul des opérations
eAjout_v et eSupp_v.

Calcul d’une représentation de l’opération eAjout_v


Le calcul de l’opération eAjout_v(v, a) se fait par induction sur la structure
de a. Cependant, le développement traditionnel ne peut se faire sans une hypo-
thèse d’induction complémentaire. Pratiquer une démarche purement construc-
tive se ferait en constatant qu’une hypothèse manque pour poursuivre le déve-
loppement et en réitérant le calcul après avoir intégré la nouvelle hypothèse, ceci
jusqu’à temps que le développement puisse être réalisé avec succès (ou doive être
abandonné !). Cette démarche est longue et les calculs fastidieux mais elle est
difficile à éviter au quotidien. Nous proposons d’emblée l’hypothèse complémen-
taire nécessaire, mais invitons le lecteur à réaliser un développement constructif
intégral. Cette hypothèse complémentaire est dénommée Aug(a).
6. Ensembles de clés scalaires 203

Hypothèse d’induction 1 (Aug(a)).


 r(eAjout_v(v, a)) − r(a) ∈ {0, 1}
Aug(a) =
Cette propriété affirme qu’effectuer une insertion dans un Avl provoque une
augmentation du rayon d’au plus 1. Le calcul d’une représentation de l’opération
eAjout_v débute comme suit :

A(eAjout_v(v, a))
= Propriété caractéristique de eAjout_v
A(a) ∪ {v} (6.6.4)

Procédons à une induction structurelle sur a. Le cas de base a =  est trivial.


Il conduit à la première équation gardée :

a =  →
eAjout_v(v, a) = , v, 

La démonstration a posteriori de la propriété Aug(a) est aisée.


Considérons à présent le cas inductif a = , posons a = g, u, d. Nous
repartons de la formule 6.6.4 ci-dessus.

A(a) ∪ {v}
= Hypothèse
A(g, u, d) ∪ {v}
= Définition de A et propriété A.7
A(g) ∪ {v} ∪ {u} ∪ A(d) (6.6.5)

À ce stade du développement, nous ne pouvons poursuivre le calcul sans distin-


guer les trois cas v = u, v < u et v > u. Le cas v > u se déduit par symétrie du
cas v < u. Concernant le cas v = u, nous avons :

A(g) ∪ {v} ∪ {u} ∪ A(d)


= Hypothèse et propriété A.9
A(g) ∪ {u} ∪ A(d)
= Définition de A
A(g, u, d)
= Hypothèse
A(a)

Il s’agit d’un cas de fausse insertion, qui conduit à l’équation gardée suivante :

a = g, u, d →
v=u →
eAjout_v(v, a) = a

La figure 6.7, page suivante, illustre le cas v < u. Elle montre les différents sous-
cas qui peuvent être répertoriés après insertion d’une valeur dans le sous-arbre
gauche d’un Avl, selon que le rayon du sous-arbre gauche augmente de 1 ou de
0 (hypothèse Aug(g)) après insertion.
Pour le cas v < u, repartons de la formule 6.6.5 :
204 Structures de données et méthodes formelles

Avant insertion Après insertion Détails


•0
(a)

(1) g d
•0
k k
•1
g d (b)

k k
g d
k
k+1
• −1 (c)

g d
• −1 (2)
k−1
k
g •0 (d)
d
k−1 • 2 (e )
k 
g d •1
k •2 k d
gg dg
(e) k−1
k−1
d k (e )
g •2
•1 (3) k−1
• −1
k+1
d
g d •1 (f ) gg dg
k−1
k−1 k−1
k  d k
g
k−1
k

Figure 6.7 – Insertion dans le sous-arbre gauche d’un Avl.


Cette figure montre les différents cas d’insertion dans le sous-arbre
gauche d’un Avl. Le rayon des sous-arbres est noté sous chaque sous-
arbre, l’équilibre des nœuds est apposé à chaque nœud (négatif s’il est en
faveur de la droite, positif s’il est en faveur de la gauche, nul si l’arbre
est équilibré). La première colonne montre les différentes situations pos-
sibles avant l’insertion, la colonne centrale montre, pour chacun des
trois cas avant insertion, les deux cas possibles selon que le sous-arbre
gauche a vu son rayon augmenter ou non (hypothèse Aug(g)). Les cinq
sous-cas (a), (b), (c), (d) et (f ) montrent des arbres h-équilibrés. Le
cas (e) montre un arbre qui n’est plus h-équilibré. La troisième colonne
détaille les deux sous-cas possibles (le troisième sous-cas, celui où le
sous-arbre gauche serait équilibré, n’est pas possible).
6. Ensembles de clés scalaires 205

A(g) ∪ {v} ∪ {u} ∪ A(d)


= Propriété A.7
(A(g) ∪ {v}) ∪ {u} ∪ A(d)
= Propriété caractéristique de l’opération eAjout_v
A(eAjout_v(v, g)) ∪ {u} ∪ A(d)
= Définition de A
A(eAjout_v(v, g), u, d)

Nous savons que eAjout_v(v, g) ∈ eAvl (c’est l’hypothèse d’induction « natu-


relle » de la démonstration). Nous savons également que r(eAjout_v(v, g)) −
r(g) ∈ {0, 1} (c’est l’hypothèse d’induction Aug(g)). Ainsi que le montre la
colonne « Après insertion » de la figure 6.7, page ci-contre, en général l’arbre
résultant de l’insertion est un Avl (cas (a), (b), (c), (d) et (f )) mais il subsiste
un cas (le cas (e) de la figure) où l’arbre eAjout_v(v, g), n, d n’est pas un Avl.
Dans ce cas, d’après l’hypothèse d’induction Aug(g), le rayon gauche a aug-
menté et le déséquilibre à la racine est donc de 2 : r(eAjout_v(v, g)) − r(d) = 2.
La décision sur la suite du développement se fait donc selon deux possibili-
tés : r(eAjout_v(v, g)) − r(d) ∈ −1 .. 1 (qui recouvre les cinq cas précités) ou
r(eAjout_v(v, g))−r(d) = 2. Si la première condition est satisfaite, nous sommes
en mesure de conclure puisque le résultat est un Avl. Sinon, il faut procéder à
une rotation.
Peut-on poursuivre le développement en utilisant la fonction r sans compro-
mettre l’efficacité de l’opération ? Autrement dit, la fonction r est-elle décompo-
sable sur un Avl (cf. encadré page 188) ? La réponse à cette dernière question
est à l’évidence positive. Nous devrons donc raffiner le développement dans une
étape ultérieure (cf. section 6.6.7, page 213).
Revenons au développement de l’opération en considérant tout d’abord le cas
r(eAjout_v(v, g)) − r(d) ∈ −1 .. 1. Il nous conduit immédiatement à l’équation
gardée suivante :

a = g, u, d →
v<u →
r(eAjout_v(v, g)) − r(d) ∈ −1 .. 1 →
eAjout_v(v, a) = eAjout_v(v, g), u, d

Démontrer a posteriori la propriété Aug(a) est facile. Nous nous li-


mitons à ébaucher cette démonstration. Nous devons évaluer l’expression
r(eAjout_v(v, a)) − r(a). Nous savons (hypothèse d’induction) que Aug(g), et
donc r(eAjout_v(v, g)) − r(g), est égal soit à 0 soit à 1. Le premier cas est tri-
vial. Le second cas, celui où le rayon du sous-arbre gauche augmente (cas (b) et
(d) de la figure, où r(eAjout_v(v, g)) − r(g) = 1) conduit à réaliser une analyse
par cas incidente pour laquelle deux sous-cas sont à considérer vis-à-vis de la
configuration de l’arbre avant l’insertion :
– r(g) = r(d) (cas (1) de la figure),
– r(g) = r(d) − 1 (cas (2) de la figure).
Le premier sous-cas fournit comme résultat une augmentation de rayon de 1.
Pour le deuxième cas, le rayon ne change pas. Le troisième cas, r(g) = r(d) + 1
n’est pas à envisager, car dans cette hypothèse la condition r(eAjout_v(v, g)) −
206 Structures de données et méthodes formelles

r(d) ∈ −1 .. 1 ne serait pas satisfaite. La propriété Aug(a) est donc établie pour
cette équation gardée.

Revenons au calcul de l’opération eAjout_v. Le second cas, celui où


r(eAjout_v(v, g)) − r(d) = 2, est représenté en partie (e) de la figure 6.7. Il
se décompose en deux sous-cas, qui sont ceux représentés dans la colonne « Dé-
tails » de la figure. L’arbre résultant de l’insertion n’est pas h-équilibré à la
racine : nous devons donc appliquer une rotation. Nous savons, depuis la sec-
tion 6.6.5, page 197, que la nature de la rotation dépend de la valeur du dés-
équilibre du sous-arbre gauche : s’il est du même signe que le déséquilibre à la
racine, une rotation simple (droite ici) suffit, sinon il faut appliquer une rotation
double (gauche-droite ici). Le cas où le sous-arbre gauche est équilibré ne peut
survenir. Nous pouvons démontrer cette dernière affirmation par l’absurde : si le
sous-arbre gauche g  était équilibré après une insertion qui s’est soldée par l’aug-
mentation (de 1) du rayon de l’un de ses sous-arbres c’est que l’autre sous-arbre
avait un rayon supérieur au premier et donc le rayon de g n’a pas augmenté.
Posons eAjout_v(v, g) = gg , ng , dg  et considérons le cas où les déséquilibres
sont de même signe (r(eAjout_v(v, g)) − r(d) = 2 et r(gg ) − r(dg ) = 1, cf. cas
(e ) de la figure). L’équation gardée correspondante s’écrit :

a = g, u, d →
v<u →
r(eAjout_v(v, g)) − r(d) ∈ / −1 .. 1 →
let gg , ug , dg  := eAjout_v(v, g) in
r(gg ) − r(dg ) = 1 →
eAjout_v(v, a) = rd(gg , ug , dg , u, d)
end

La démonstration de la propriété Aug(a) est laissée en exercice (cf. exercice 6.6.4,


page 215).

Le cas où les déséquilibres sont de signes différents (r(eAjout_v(v, g)) −


r(d) = 2 et r(gg ) − r(dg ) = −1, cf. cas (e ) de la figure) conduit à l’équation
gardée suivante :

a = g, u, d →
v<n →
r(eAjout_v(v, g)) − r(d) ∈ / −1 .. 1 →
let gg , ug , dg  := eAjout_v(v, g) in
r(gg ) − r(dg ) = −1 →
eAjout_v(v, a) = rgd(gg , ug , dg , u, d)
end

Il est facile de montrer que la propriété Aug(a) est vérifiée pour ce dernier cas.
La démonstration est proposée en exercice (cf. exercice 6.6.4, page 215).
Les équations gardées correspondant à la garde v > n se déduisent par symé-
trie. Elles ne sont pas calculées ici. En intégrant les différentes équations gardées
calculées, nous obtenons la représentation suivante de l’opération eAjout_v :
6. Ensembles de clés scalaires 207

function eAjout_v(v, a) ∈ N × eAvl → eAvl = 


if a =  → /*vraie insertion*/
, v, 
| a = g, u, d →
if v = u → /*fausse insertion*/
a
|v<u → /*insertion à gauche*/
if r(eAjout_v(v, g)) − r(d) ∈ −1 .. 1 →
eAjout_v(v, g), u, d
| r(eAjout_v(v, g)) − r(d) ∈ / −1 .. 1 →
let gg , ug , dg  := eAjout_v(v, g) in
if r(gg ) − r(dg ) = 1 →
rd(gg , ug , dg , u, d)
| r(gg ) − r(dg ) = −1 →
rgd(gg , ug , dg , u, d)

end

|v>u → /*insertion à droite*/
..
.

L’interprétation opérationnelle de cette solution est la suivante. L’insertion


s’effectue sur une feuille. En remontant, si l’équilibre Avl est violé, une rotation,
simple ou double selon le cas, le rétablit.
Abordons à présent le problème de la complexité de cette opération. Faisons
l’hypothèse que le rayon d’un Avl est O(1)-décomposable (cf. sections 1.10 et 2.4,
et encadré page 188) sur la structure de données (cf. section 6.6.7). Ensuite,
notons qu’il est possible de démontrer qu’une insertion dans un Avl exige au
plus une rotation (cf. exercice 6.6.5). Par ailleurs, le théorème d’Adelson-Velsky
et Landis (cf. formule 6.6.1, page 193) nous apprend que le rayon d’un Avl de
poids n est inférieur à 1, 45 · log(n + 1). Enfin, les rotations, simples ou doubles,
entraînent un nombre constant de comparaisons. Il est facile d’en conclure que
la complexité temporelle d’une insertion est en O(log n).

Calcul d’une représentation de l’opération eSupp_v


De même que pour l’opération eAjout_v, le développement de l’opération
eSupp_v(v, a) se fait en utilisant une hypothèse d’induction complémentaire.
Celle-ci affirme que suite à une suppression, le rayon diminue au plus de 1.
Hypothèse d’induction 2 (Dim(a)).
 r(a) − r(eSupp_v(v, a)) ∈ {0, 1}
Dim(a) =
Cette hypothèse doit être démontrée séparément à chacune des étapes de
calcul. Ces démonstrations ne sont pas réalisées ici, mais sont proposées à l’exer-
cice 6.6.4, page 215. Nous réalisons une induction sur a. Le cas de base a = 
208 Structures de données et méthodes formelles

est trivial. Il correspond à une fausse suppression et se matérialise par l’équation


gardée suivante :

a =  →
eSupp_v(v, a) = 

Pour le cas inductif, posons a = g, u, d.

A(eSupp_v(v, a))
= Propriété caractéristique de l’opération eSupp_v
A(a) − {v}
= Hypothèse (a = g, u, d)
A(g, u, d) − {v}
= Définition de A
(A(g) ∪ {u} ∪ A(d)) − {v}
= Propriété A.8
(A(g) − {v}) ∪ ({u} − {v}) ∪ (A(d) − {v}) (6.6.6)

La suite du calcul distingue le cas v = u du cas v = u. Débutons par le cas


v = u.

(A(g) − {v}) ∪ ({u} − {v}) ∪ (A(d) − {v})


= Cf. développement de la propriété 6.3.11, page 162
A(g) ∪ A(d) (6.6.7)

Deux cas sont à considérer : A(g) ∪ A(d) = ∅ et A(g) ∪ A(d) = ∅. Le premier


cas se traduit facilement en g =  ∧ d = . Considérons le cas A(g) ∪ A(d) = ∅ :

A(g) ∪ A(d)
= Hypothèse

= Définition de A
A()

D’où, d’après la propriété de l’équation à membres identiques (page 67), puisque


l’arbre  est un Avl, la première équation gardée :

a = g, u, d →
v=u →
g =  ∧ d =  →
eSupp_v(v, a) = 

En supposant toujours que v = u, le second cas est conditionné par l’expres-


sion g =  ∨ d = . Il se traite à la manière de la suppression dans les abr
(cf. page 161 et suivantes), en mettant en évidence le plus grand élément du
sous-arbre gauche g ou le plus petit élément du sous-arbre droit d. Cependant,
le choix du sous-arbre à considérer ici n’est pas indifférent. En effet, il faut éviter
de rechercher une valeur dans un sous-arbre vide. Une solution consiste à sélec-
tionner le sous-arbre ayant le plus grand rayon. Ce choix présente un avantage
6. Ensembles de clés scalaires 209

supplémentaire : suite à la suppression, et compte tenu de l’hypothèse d’induc-


tion Dim, l’arbre reste équilibré. Dans la suite nous traitons uniquement le cas
r(g) ≥ r(d), le cas r(g) ≤ r(d) s’en déduit par symétrie 8 . De r(g) ≥ r(d) et de
g =  ∨ d =  nous déduisons que g = . A(g) possède donc un plus grand
élément. Nous repartons de la formule 6.6.7 ci-dessus.
A(g) ∪ A(d)
= Propriété A.10
(A(g) − {max(A(g))}) ∪ {max(A(g))} ∪ A(d)
= Propriété caractéristique de eSupp_v
A(eSupp_v(max(A(g)), g)) ∪ {max(A(g))} ∪ A(d)
= Définition de A
A(eSupp_v(max(A(g)), g), max(A(g)), d)
De même que la fonction r, la fonction max est décomposable sur un Avl.
Nous poursuivons donc le développement en faisant abstraction du raffinement
ultérieur qu’il faudra réaliser pour disposer du maximum d’un Avl. Il faut main-
tenant nous assurer que l’argument de la fonction A est bien un Avl. Posons
g  = eSupp_v(max(A(g), g)). Nous sommes dans le cas où r(g) ≥ r(d) (et donc,
puisque a est un Avl, 0 ≤ r(g) − r(d) ≤ 1). D’après l’hypothèse d’induction
Dim(g), 0 ≤ r(g) − r(g  ) ≤ 1, soit encore −1 ≤ r(g  ) − r(g) ≤ 0. En ajoutant
les deux doubles inégalités, nous obtenons −1 ≤ r(g  ) − r(d) ≤ 1. Soit encore
r(eSupp_v(max(A(g), g))) − r(d) ∈ −1 .. 1. L’arbre résultant de la suppression
est bien un Avl. D’où la seconde équation gardée :
a = g, u, d →
v=u →
g =  ∨ d =  →
r(g) ≥ r(d) →
eSupp_v(v, a) = eSupp_v(max(A(g)), g), max(A(g)), d
Le cas r(g) ≤ r(d) s’obtient par des considérations de symétrie. Abordons à
présent le cas v = u. Deux sous-cas sont à considérer : v > u et v < u. Dévelop-
pons le premier sous-cas en repartant de la formule 6.6.6, page ci-contre. Ce cas
est analysé à la figure 6.8 (page suivante), qui montre les différentes situations
possibles suite à la suppression d’une valeur dans le sous-arbre droit (colonne
« Après suppression »).
v > u implique que {u} ∩ {v} = ∅. Par ailleurs, d’après le théorème 4 des
abr (page 151), A(g) ∩ {v} = ∅ :
(A(g) − {v}) ∪ ({u} − {v}) ∪ (A(d) − {v})
= Propriété A.16
A(g) ∪ {u} ∪ (A(d) − {v})
= Propriété caractéristique de eSupp_v
A(g) ∪ {u} ∪ A(eSupp_v(v, d))
= Définition de A
A(g, u, eSupp_v(v, d))
8. Depuis le calcul de l’opération eAjout_v nous savons que nous pouvons décomposer la
fonction r sur un Avl ; nous utilisons donc cette fonction sans autres précautions.
210 Structures de données et méthodes formelles

Avant suppression Après suppression Détails


•0 (a)

g d
•0 (1)
k k
g d •1 (b)

k k g d
k−1
k
• −1 (c)

g
d • 2 (e )
• −1 (2) k−1
k •1
g
d •0 (d) d
gg dg
k−1 k−1
k g d k−1
k
k−1 k−1 • 2 (e )
• 2 (e) •0
d
•1 (3) g d gg dg
k−1
k−1
k k
g d k+1 • 2 (e(3) )
• 1 (f )
k • −1
k+1
gg d
g d dg
k−1
k k−1
k+1 k

Figure 6.8 – Suppression dans le sous-arbre droit d’un Avl.


Cette figure montre les différents cas de suppression dans le sous-arbre
droit d’un Avl. Le rayon des sous-arbres est noté sous chaque sous-
arbre, l’équilibre des nœuds est apposé à chaque nœud (négatif s’il est
en faveur de la droite, positif s’il est en faveur de la gauche, nul si
l’arbre est équilibré). La première colonne montre les différentes situa-
tions possibles avant la suppression ; la colonne centrale montre, pour
chacun des trois cas avant suppression, les deux cas possibles selon
que le sous-arbre droit a vu son rayon diminuer ou non (hypothèse
Dim(d)). Les cinq sous-cas ((a), (b), (c), (d) et (f )) montrent des ar-
bres h-équilibrés. Le cas (e) montre un arbre qui n’est plus h-équilibré.
La troisième colonne détaille les trois sous-cas possibles pour (e).
6. Ensembles de clés scalaires 211

L’arbre g, u, eSupp_v(v, d) est représenté dans la colonne centrale de la fi-


gure 6.8. En général, le résultat est h-équilibré (c’est ce que montrent les cinq
cas (a), (b), (c), (d) et (f ) pour lesquels r(g) − r(eSupp_v(v, d)) ∈ −1 .. 1). Le
cas (e) est par contre problématique dans la mesure où la suppression dans le
sous-arbre d fait diminuer le rayon et conduit à un déséquilibre inacceptable
(r(g) − r(eSupp_v(v, d)) = 2). Il faut alors procéder à une rotation.
Abordons tout d’abord le cas où l’arbre résultant de la suppression est un
Avl (cas (a), (b), (c), (d) et (f )). L’équation gardée est immédiate :

a = g, u, d →
v = u →
v>u →
r(g) − r(eSupp_v(v, d)) ∈ −1 .. 1 →
eSupp_v(v, a) = g, u, eSupp_v(v, d)

Abordons maintenant le cas où, suite à la suppression dans le sous-arbre


droit, l’arbre n’est plus h-équilibré (cas (e) de la figure). Il faut procéder à une
rotation 9 .
Ainsi que nous l’avons vu à la section 6.6.5, page 197, la nature de la rotation
dépend de l’équilibre du sous-arbre excédentaire (g ici). Posons g = gg , ug , dg .
Si r(gg ) ≥ r(dg ) nous pouvons procéder, d’après les théorèmes 9 et 10, à une
rotation droite rd (cas (e ) et (e ) de la figure). D’où l’équation gardée :

a = g, u, d →
v>u →
r(g) − r(eSupp_v(v, d)) ∈ / −1 .. 1 →
let gg , ug , dg  := g in
r(gg ) ≥ r(dg ) →
eSupp_v(v, a) = rd(g, u, eSupp_v(v, d))
end

Sinon, d’après le théorème 12, page 201, nous pouvons rétablir l’équilibre Avl
par une rotation double gauche-droite. D’où l’équation gardée :

a = g, u, d →
v>u →
r(g) − r(eSupp_v(v, d)) ∈ / −1 .. 1 →
let gg , ug , dg  := g in
r(gg ) < r(dg ) →
eSupp_v(v, a) = rgd(g, u, eSupp_v(v, d))
end

Le cas v < u se traite de façon analogue. Nous obtenons au total la version


suivante de l’opération eSupp_v :

9. Contrairement à l’insertion, le cas où le sous-arbre gauche de g est équilibré peut survenir.


Il suffit que g soit équilibré avant la suppression puisque celle-ci, par construction, ne modifie
pas g (cf. théorème 10, page 199).
212 Structures de données et méthodes formelles

function eSupp_v(v, a) ∈ N × eAvl → eAvl = 


if a =  → /*fausse suppression*/

| a = g, u, d →
if v = u → /*suppression de la racine*/
if g =  ∧ d =  → /*la racine est une feuille*/

| g =  ∨ d =  → /*la racine n’est pas une feuille*/
if r(g) ≥ r(d) →
let m := max(A(g)) in
eSupp_v(m, g), m, d
end
| r(g) ≤ r(d) →
let m := min(A(d)) in
g, m, eSupp_v(m, d)
end


| v>u→ /*suppression à droite*/
let l := eSupp_v(v, d) in
if r(g) − r(l) ∈ −1 .. 1 →
g, u, l
| r(g) − r(l) ∈ / −1 .. 1 →
let gg , ug , dg  := g in
if r(gg ) ≥ r(dg ) →
rd(g, u, l)
| r(gg ) < r(dg ) →
rgd(g, u, l)

end

end
|v<u → /*suppression à gauche*/
..
.

L’interprétation opérationnelle de cette version est la suivante. Si la valeur à


supprimer v est une feuille, le résultat est un arbre vide. Si la valeur à supprimer
est un nœud interne, on supprime la valeur la plus proche (qui est une feuille).
Celle-ci vient se substituer à v. Si la valeur à supprimer n’est pas la racine u, la
suppression se fait dans le sous-arbre gauche ou droit, selon la position relative
de v et de u. L’équilibre Avl est rétabli par rotations (simples ou doubles) en
remontant.
L’exercice 6.6.6 illustre le fait que la suppression dans un Avl peut exiger
une rotation sur tous les niveaux situés entre la valeur à supprimer et la racine.
Sur le plan de la complexité, nous sommes donc dans une situation légèrement
6. Ensembles de clés scalaires 213

différente de l’insertion. Cependant, toujours dans l’hypothèse d’un renforcement


du support par une O(1)-décomposition de l’opération r sur la structure de
données, la complexité de la suppression est à nouveau en O(log n) puisque
le nombre de comparaisons par rotation est constant. Par ailleurs, on montre
empiriquement que pour la suppression, le nombre de rotations est en moyenne
de une pour cinq suppressions alors qu’il est de une pour deux adjonctions !

6.6.7 Renforcement du support par décomposition


du rayon
Les deux opérations calculées ci-dessus ne peuvent être laissées en l’état.
Toutes deux utilisent la fonction r qui délivre le rayon d’un Avl. Dans sa version
de la section 3.5.2, r est une opération qui exige d’accéder à toutes les feuilles
à chaque invocation. Le coût qui en résulte est excessif et incompatible avec
l’objectif d’efficacité visé.
Par ailleurs, l’opération eSupp_v fait usage des fonctions max et min ap-
pliquées à des ensembles représentés par des Avl. Nous sommes là dans une
situation similaire à celle rencontrée lors de l’étude de l’opération eSupp_a de
suppression dans les abr : il est nécessaire de transformer ces appels. Les trois
solutions évoquées ou implantées alors (raffiner les opérations max et min sur des
ensembles en des opérations sur des Avl, renforcer le support en décomposant
les fonctions max et min sur la structure de données, ou fusionner l’opération de
suppression avec l’opération de recherche du max au sein d’une seule opération,
cf. page 164) restent d’actualité.
Concernant le cas de la fonction r, nous proposons ci-dessous de renforcer
le support en décomposant le rayon sur la structure de données. Quant à la
transformation des appels aux fonctions max et min, nous laissons le choix au
lecteur dans le cadre de l’exercice 6.6.8. Décomposer le rayon sur la structure
de données est une opération qui se scinde en deux parties. Pour cela, il faut
bien entendu enrichir chaque nœud d’un champ contenant la valeur du rayon
de l’arbre considéré. C’est ce que nous faisons en considérant qu’un nœud non
vide est une structure de la forme g, u → s, d où g et d sont des sous-arbres,
u la clé et s le rayon de l’arbre. Il faut également remplacer la fonction r de la
section 3.5.2 par la description r  suivante :

function r (a) ∈ eAvl → N =



if a =  →
0
| a = g, u → s, d →
s

Reste le problème des rotations. Compte tenu de la nouvelle structure des


nœuds, qui comprend un champ pour le rayon, les quatre opérations de rotation
doivent être aménagées. Les nouvelles versions sont dénommées rg  , rd , rgd ,
rdg  . Dans la suite, elles sont supposées disponibles.
Nous sommes maintenant à même de proposer un raffinement pour l’opéra-
tion eAjout_v. Les modifications à apporter sont d’une part la prise en compte
214 Structures de données et méthodes formelles

de la nouvelle structure incluant le rayon des arbres et d’autre part l’utilisation


des nouvelles versions pour le rayon et les rotations. L’opération eAjout_v se
raffine alors comme suit :

function eAjout_v(v, a) ∈ N × eAvl → eAvl =



if a =  →
, v → 1, 
| a = g, u → s, d →
if v = u →
a
|v<u →
if r (eAjout_v(v, g)) − r (d) ∈ −1 .. 1 →
 eAjout_v(v, g), 
 
u → max({r (eAjout_v(v, g)), r (d)}) + 1,
d
| r (eAjout_v(v, g)) − r (d) ∈
/ −1 .. 1 →
let gg , ug → s , dg  := eAjout_v(v, g) in
if r (gg ) − r (dg ) = 1 →
rd( gg , ug → s , dg , u → s , d )
| r (gg ) − r (dg ) = −1 →
rgd( gg , ug → s , dg , u → s , d )

end

|v>u →
..
.

Les appels multiples à la fonction eAjout_v peuvent facilement être évités en


utilisant une expression let. Le cas de l’opération eSupp_v est laissé en exercice.

6.6.8 Conclusion et remarques bibliographiques


Le lecteur averti note que les algorithmes de mise à jour des Avl présentés
ci-dessus, outre le fait qu’il s’agisse de versions fonctionnelles et calculées, se
démarquent de ceux traditionnellement proposés dans la littérature, par l’usage
qui est fait de la persistance, qui donne accès au rayon courant ainsi qu’au
rayon précédant la mise à jour. L’utilisation explicite du rayon (plutôt que de la
différence de rayons) facilite le calcul en supprimant le dernier niveau d’analyse.
C’est pédagogiquement parlant un avantage, qui se paye en terme de complexité
spatiale. Un autre inconvénient est la difficulté que présente la traduction d’une
version fonctionnelle persistante en une version impérative.
6. Ensembles de clés scalaires 215

Les Avl nous ont permis d’atteindre l’un de nos objectifs en ce qui concerne
la représentation des ensembles (de scalaires dotés d’une structure d’ordre) :
une bonne efficacité dans toutes les situations. Cette qualité résulte d’une part
de l’absence de dégénérescence de la structure de données (cf. le théorème
d’Adelson-Velsky et Landis) et d’autre part d’un coût constant des opérations
de rotation, qui dotent les algorithmes de mise à jour d’une complexité asymp-
totique identique à celle de la recherche. Par contre, certaines situations mettent
en lumière l’un des défauts des Avl (et des arbres binaires en général). Il s’agit
de leur utilisation dans le contexte des mémoires auxiliaires (comme les disques
magnétiques ou les disques ssd 10 ) pour lesquelles l’unité de mémoire est grande
(typiquement un secteur de 512 octets, parfois plus). Il n’est alors pas raison-
nable, tant sur le plan de l’occupation que sur celui de la vitesse de transfert
entre mémoire centrale et mémoire auxiliaire, d’occuper une unité d’allocation
par nœud. C’est pourquoi des structures de données adaptées, qui contournent
ces inconvénients ont été mises au point. L’exemple le plus typique est celui des
B-arbres (cf. section 6.7).
Découverte en 1962 par les mathématiciens soviétiques G.M. Adelson-Velsky
et E.M. Landis, la forme d’arbre binaire de recherche dénommée Avl par C.C. Fos-
ter [36] est depuis considérée comme l’archétype des arbres équilibrés. L’opéra-
tion de suppression n’apparaît pas dans l’article de 1962. Il faut attendre 1965 et
un rapport interne de la société Goodyear Aerospace (cité dans C.C. Foster [36])
pour la parution d’une première solution au problème de la suppression. Bien que
concurrencés par d’autres formes d’arbres équilibrés, les Avl font souvent partie
du catalogue de structures de données avancées offert aux étudiants en informa-
tique. La découverte des Avl a initié une longue liste de structures de données
explicitement équilibrées comme les B-arbres, les arbres équilibrés en hauteur,
les arbres semi-équilibrés [94], les arbres 2-3, les arbres 2-3-4, les arbres équilibrés
en poids [89], etc. Dans un article récent [108], S. Sen et R. Tarjan proposent
une forme relâchée d’Avl (les « relaxed Avl ») qui, tout en restant efficaces,
permettent des suppressions sans rééquilibrage. Cette simplification s’obtient au
prix d’une reconstruction périodique de l’arbre. Pour ce qui concerne l’analyse
des algorithmes, dans [75], D. Knuth présente une analyse détaillée de la com-
plexité de la recherche et de l’insertion dans un Avl.

Exercices

Exercice 6.6.1 Démontrer les théorèmes 9, 10, 11 et 12, page 198 et suivantes.

Exercice 6.6.2 Dans le texte nous montrons par l’absurde que lors d’une insertion dans un
Avl, lorsque le rayon augmente, alors le déséquilibre de la racine est de 1 en valeur absolue.
Faire une démonstration par induction de cette proposition.

Exercice 6.6.3 Montrer directement (sans utiliser le théorème 8) que la rotation droite rd
préserve la structure abstraite. Faire de même pour la rotation double rgd.

Exercice 6.6.4 Démontrer l’hypothèse d’induction Aug(a) (resp. Dim(a)) dans le cas où une
rotation est exigée dans le calcul de l’opération eAjout_v (resp. eSupp_v).

10. ssd : Solid State Drive, unité de stockage permanent sans pièce mobile.
216 Structures de données et méthodes formelles

Exercice 6.6.5 Montrer que l’insertion dans un Avl exige au plus une rotation.

Exercice 6.6.6 Fournir un exemple d’un Avl de poids minimum 12 dans lequel la suppression
d’une feuille exige une rotation à chaque nœud du chemin de la recherche.

Exercice 6.6.7 Certains auteurs suggèrent d’effectuer la suppression d’un nœud interne en
descendant la valeur à supprimer sur une feuille, par rotations simples ou doubles (de façon
à préserver l’équilibre Avl), puis à supprimer la feuille avant de rééquilibrer par rotations en
remontant. Cette solution est erronée : la première étape n’est pas toujours possible. Identifier
au moins un cas où l’équilibre Avl est violé quelle que soit la rotation (simple ou double) que
l’on effectue en descendant la valeur vers les feuilles.

Exercice 6.6.8 Poursuivre le développement de l’opération de suppression dans un Avl


(eSupp_v) en transformant l’utilisation des fonctions min et max par l’une des méthodes
évoquées page 213.

Exercice 6.6.9 Spécifier par Avl les opérations abstraites complémentaires décrites dans
l’exercice 6.2.2, page 149. Calculer une représentation de ces opérations.

Exercice 6.6.10 Définir et étudier la notion d’Avl appliquée aux arbres externes. Calculer
les opérations concrètes. Discuter par rapport à la solution développée ci-dessous.

Exercice 6.6.11 Jusqu’à présent, nous avons recherché une représentation efficace pour les
ensembles de scalaires. Il est cependant fréquent d’avoir à représenter et à mettre à jour
des ensembles de couples de scalaires (en d’autres termes des relations binaires de scalaires).
C’est le thème de la section 7.2. Étudier une solution où chaque relation est représentée par
un couple d’Avl. Le premier Avl (resp. le second) représente l’ensemble des origines (resp.
des destinations). En outre, à chaque nœud est associé l’ensemble des valeurs qui constituent
l’image (resp. l’image inverse) de la coordonnée considérée.

Exercice 6.6.12 Un « 1-2 arbre frère » (cf. [96]) est soit un arbre binaire étiqueté (par un
entier) soit un arbre unaire non étiqueté (cf. section 3.5) satisfaisant aux contraintes suivantes :
1. le frère d’un arbre unaire est un arbre binaire,
2. toutes les feuilles sont à la même hauteur,
3. c’est un arbre de recherche.
Ainsi par exemple l’arbre ci-dessous est un « 1-2 arbre frère » :

20
7 30
• 10 27 •

4 • 15 24 • 37

Sur cette base, proposer une spécification concrète pour le raffinement des ensembles dotés
d’une relation d’ordre. Calculer les différentes opérations concrètes et leurs complexités.

6.7 Méthodes équilibrées : les B-arbres


6.7.1 Introduction
Les Avl constituent une bonne solution pour une représentation efficace des
ensembles en mémoire centrale. Cette solution n’est cependant pas viable lors-
qu’on la transpose aux mémoires auxiliaires, pour au moins deux raisons com-
plémentaires : d’une part l’unité d’allocation (dans les années 2010 : un secteur
6. Ensembles de clés scalaires 217

disque 11 ou un bloc selon les cas) est en général trop volumineuse pour représen-
ter un seul nœud d’un Avl. D’autre part, dans les représentations arborescentes,
le nombre d’accès disque varie avec la hauteur : celle des Avl est en général
trop élevée par rapport au nombre de valeurs représentées pour pouvoir utiliser
raisonnablement un support auxiliaire. Il est donc nécessaire d’adapter l’unité
d’allocation logique (typiquement un nœud de l’arbre) à la taille des unités phy-
siques (les secteurs ou les blocs).
Le h-équilibre des Avl préserve la largeur (le nombre de clés) des nœuds qui
reste constante (et égale à 1), au détriment d’un équilibre en hauteur où un léger
déséquilibre est autorisé. Au contraire, l’équilibre des B-arbres est tel que toutes
les feuilles sont à la même hauteur mais la largeur des nœuds est légèrement
variable.
De même que dans le cas des Avl, les B-arbres modélisent des ensembles
dotés d’une structure d’ordre total. Dans la suite nous allons (sans sacrifier la
généralité du propos) considérer des ensembles d’entiers naturels.
Les B-arbres d’ordre n (n > 0) non vides se caractérisent par les propriétés
suivantes :
1. Pour un nœud donné, tous les sous-arbres ont le même rayon.
2. Chaque nœud, à l’exception de la racine, contient entre n et 2 · n valeurs
organisées en tableau.
2 bis. La racine contient entre 1 et 2 · n valeurs.
3. Les valeurs situées dans les nœuds sont triées strictement par ordre croissant.
4. Chaque nœud constitué de p valeurs désigne p + 1 B-arbres, tous de même
rayon.
4 bis. Chaque feuille constituée de p valeurs « désigne » p + 1 B-arbres vides.
5. Un B-arbre est un arbre de recherche : le parcours infixé gauche-droite ren-
contre les valeurs dans l’ordre strictement croissant.
Un arbre respectant à la lettre ces propriétés sera dans la suite appelé B-
arbre strict. Un B-arbre strict est donc, selon la terminologie de la section 3.2,
un arbre 2 · n-aire. Afin de mettre en œuvre les opérations de mise à jour, nous
sommes amené à assouplir certaines de ces contraintes. Lorsque la condition 2
vaut également pour la racine, nous avons affaire à un B-arbre régulier.
En l’absence de la propriété 2 bis, un B-arbre devrait soit ne posséder aucune
valeur (cas d’un B-arbre vide) soit au moins n valeurs. Cette limitation est
contraire à la spécification abstraite et serait inacceptable. Cette propriété 2 bis
permet donc de représenter des ensembles ayant un nombre fini quelconque de
valeurs. En revanche, le fait que les nœuds ne soient pas uniformément de même
nature (la racine est un cas particulier) complexifie la description du support
concret comme nous le verrons ci-dessous.
Concernant l’aspect recherche dans un B-arbre, la technique utilisée est une
extension de celle appliquée dans les arbres binaires de recherche. L’insertion
quant à elle se fait toujours aux feuilles. Une insertion pouvant provoquer le
débordement d’un nœud, il peut être nécessaire d’éclater un nœud plein. Cet
éclatement peut se faire « en remontant » (insertion dite ascendante) ou « en
11. Que ce soit un disque « dur » classique ou un ssd.
218 Structures de données et méthodes formelles

descendant » (insertion descendante). Nous développons la démarche ascendante


et proposons d’étudier l’autre démarche à l’exercice 6.7.4.

Sur l’exemple de la figure 6.9, qui représente un B-arbre d’ordre 2, nous


notons que :
– la largeur des nœuds est supérieure ou égale à deux, sauf celle de la racine
qui est de un ;
– toutes les feuilles sont à la même hauteur 2 ;
– les valeurs situées dans chaque nœud sont triées strictement par ordre
croissant ;
– tout nœud différent d’une feuille et contenant p valeurs désigne p + 1 sous-
arbres non vides ;
– le parcours infixé de l’arbre rencontre successivement les valeurs 2, 3, 5, 7,
10, 11, 12, 14, 16, 17, 23, 24, 25, 29, 30, 32, 34, 37, 38, 39, 40, 43, 44, 49,
51 et 57, qui constituent l’ensemble représenté par l’arbre.

30

5 14 24 37 43

2 3 7 10 11 12 16 17 23 25 29 32 34 38 39 40 44 49 51 57

Figure 6.9 – Un exemple de B-arbre strict d’ordre 2.


Dans un B-arbre strict d’ordre 2 non vide, tous les nœuds ont entre 2 et
4 clés, à l’exception de la racine qui possède entre 1 (c’est le cas ici) et 4
clés. Le parcours infixé rencontre les clés dans l’ordre croissant. Toutes
les feuilles sont à la même distance de la racine (2 ici). Le rayon de
cet arbre est de 3 (3 niveaux). À l’exception des feuilles, un nœud de p
clés désigne p + 1 sous-B-arbres.

6.7.2 Définition du support concret


Définir rigoureusement le support nBA des B-arbres est une entreprise com-
plexe que nous allons réaliser en trois étapes. La première définition que nous
allons fournir tient compte de ce que dans un B-arbre non vide d’ordre n, la
racine possède un statut particulier par le fait que sa largeur varie entre 1 et
2 · n alors que les autres nœuds ont tous entre n et 2 · n éléments. Pour tenir
compte de cette particularité, nous proposons de définir le type nba en le do-
tant de deux paramètres n et inf. n représente l’ordre du B-arbre tandis que
inf représente la largeur minimale de la racine. L’en-tête se présente alors de la
manière suivante :
6. Ensembles de clés scalaires 219

concreteType nba(n, inf ) . . .


constraints
n ∈ N1 ∧
inf ∈ {1, n}

Chaque nœud est un triplet constitué :


– d’un tableau de valeurs, de taille physique 2 · n et de taille logique lg, défini
sur l’intervalle 1 .. (2 · n). lg est le troisième élément du triplet ;
– d’un tableau défini sur l’intervalle 0 .. (2 · n) désignant lg + 1 arbres. Dans
le cas de feuilles, les arbres désignés sont vides ;
– de lg, largeur du nœud représenté (la taille logique de ce nœud).

Le support du tableau de valeurs se définit par tt(lg), où lg est la largeur du


tableau. Une formalisation possible pour tt(lg) est :

t ∈ 1 .. 2 · n → N ∧ lg ∈ inf .. 2 · n ∧ ∀i ·(i ∈ 1 .. lg − 1 ⇒ t(i) < t(i + 1))



t ∈ tt(lg)

Le premier conjoint précise qu’un tableau du type tt(lg) est une fonction totale
définie sur l’intervalle 1 .. 2 · n. Le deuxième conjoint fournit le domaine de va-
riation de la largeur lg (1 .. 2 · n dans le cas de la racine, n .. 2 · n dans les autres
cas). Le troisième conjoint formalise le fait que la partie utile du tableau (sa res-
triction à l’intervalle 1..lg) est triée strictement par ordre croissant. Remarquons
que ces conditions impliquent que, si t ∈ tt(lg), alors 1 .. lg  t ∈ 1 .. lg  N : la
restriction à 1 .. lg est injective (il n’y a pas de doublon dans la partie utile du
tableau).

La formalisation de la définition du nœud d’un B-arbre exige de disposer


de deux fonctions qui sont présentées ci-dessous : r, qui délivre le rayon d’un
B-arbre, et A, qui délivre l’ensemble des clés présentes dans un B-arbre. Cette
dernière fonction n’est autre que la fonction d’abstraction.
Le type nBA(n, inf ) (un nœud d’un B-arbre) se définit alors inductivement
par :

1) a =  ⇒ a ∈ nBA(n, inf ) (6.7.1)


2) a = te, tl, lg ∧ (6.7.2)
lg ∈ inf .. 2 · n ∧ (6.7.3)
te ∈ tt(lg) ∧ (6.7.4)
tl ∈ 0 .. 2 · n → nBA(n, n) ∧ (6.7.5)
ran(0⎛ .. lg  tl ; r) = {r(tl(0))} ∧ ⎞ (6.7.6)
i ∈ 1 .. lg
∀i · ⎝ ⇒ ⎠ (6.7.7)
max(A(tl(i − 1))) < te(i) ∧ min(A(tl(i))) > te(i)

a ∈ nBA(n, inf )

La formule 6.7.1 définit un B-arbre vide d’ordre n. Le conjoint 6.7.2 précise


qu’un B-arbre non vide est un triplet. Le conjoint 6.7.3 précise le domaine de
220 Structures de données et méthodes formelles

variation de la largeur. Le conjoint 6.7.4 type le premier constituant du triplet, le


tableau des clés. Le conjoint 6.7.5 type le second constituant comme un tableau
qui désigne des B-arbres (dont la largeur des racines est au moins égale à n, si
ces B-arbres ne sont pas vides). Le conjoint 6.7.6 formalise le fait que toutes les
feuilles sont à la même hauteur. Enfin le conjoint 6.7.7 exprime que le parcours
infixé rencontre les valeurs dans le sens croissant 12 .

11 24 34 43 (1)

3 5 7 10 12 14 16 17 25 29 30 32 35 38 39 40 44 49 51 57

11 24 34 43 (2)

3 5 7 10 12 14 16 17 25 29 30 32 35 36 38 39 40 44 49 51 57

11 24 34 38 43 (3)

3 5 7 10 12 14 16 17 25 29 30 32 35 36 39 40 44 49 51 57

34 (4)

11 24 38 43

3 5 7 10 12 14 16 17 25 29 30 32 35 36 39 40 44 49 51 57

Figure 6.10 – Insertion par éclatement dans un B-arbre strict.


Cette figure montre l’évolution d’un B-arbre strict d’ordre deux lors
de l’insertion de la valeur 36. La partie (1) montre la localisation de
la feuille où se fait l’insertion. La partie (2) illustre l’insertion propre-
ment dite. La feuille modifiée devient excédentaire. La partie (3) montre
l’éclatement de la feuille excédentaire suivi de l’insertion de la médiane
dans le nœud père. La racine devient excédentaire. Dans la partie (4)
elle est éclatée, ce qui provoque l’augmentation de la hauteur de l’arbre.

La fonction r, qui délivre le rayon d’un B-arbre, se spécifie par :

function r(x) ∈ nBA(n, inf ) → N =



if x =  →
12. À cette occasion, nous rappelons que nous admettons que max(∅) = −∞ et que
min(∅) = +∞.
6. Ensembles de clés scalaires 221

0
| x = te, tl, lg →
r(tl(0)) + 1

Cette spécification exploite le fait que les sous-arbres d’un même nœud ont tous
le même rayon (celui par exemple de tl(0)) pour définir le rayon d’un B-arbre
non vide comme le rayon de son sous-arbre gauche plus un.
Les opérations de mise à jour (ajout et suppression d’un élément) rendent
cependant le paramétrage du type nba plus délicat qu’il n’y paraît. Débutons
notre réflexion par les incidences de l’adjonction d’une valeur dans un B-arbre
d’ordre n. Considérons le B-arbre d’ordre deux de la figure 6.10, page ci-contre,
partie (1), dans lequel nous souhaitons introduire la valeur 36.
Les parties (1) et (2) montrent que la clé 36 est ajoutée dans la quatrième
feuille de la racine, entre 35 et 38, afin de respecter la propriété qui veut qu’un
nœud soit trié. Ce faisant, la feuille devient momentanément excédentaire : elle
contient 5 = 2 · n + 1 valeurs. Elle ne peut rester plus longtemps en l’état, une
adjonction ultérieure risquant d’augmenter à nouveau sa largeur.
La transition entre les parties (2) et (3) illustre l’éclatement de la feuille
excédentaire : sa valeur médiane, 38, est remontée vers le père, entre 34 et 43.
Cependant, nous avons simplement déplacé le problème : c’est maintenant le
père (la racine) qui devient excédentaire.
Il est certes possible d’éclater la racine en deux nœuds autour de la valeur
médiane 34, par contre nous ne pouvons remonter 34 dans le nœud père, celui-ci
n’existant pas. Il est nécessaire de le créer de toutes pièces, mais ceci se fait au
prix d’un traitement ad hoc ! C’est ce qui est montré dans la partie (4).
Il est cependant parfois possible d’éviter un éclatement. En effet, si le nœud
excédentaire est frère d’un nœud pouvant encore recevoir un élément sans lui-
même devenir excédentaire, il est possible d’effectuer une rotation (droite ou
gauche, selon que l’on s’accorde avec le frère gauche ou avec le frère droit) qui
aura pour effet de retirer un élément du nœud considéré et d’en ajouter un au
frère choisi. L’avantage de cette solution est qu’une fois la rotation effectuée, il
n’y aura plus aucun éclatement dans la suite du traitement puisque les rotations
délivrent des B-arbres stricts. C’est ce que montre la figure 6.11, page suivante,
dans laquelle (partie (2)) les deux frères du nœud excédentaire peuvent accueillir
une valeur. La partie (3) montre le résultat d’une rotation avec le frère droit.
L’arbre obtenu est un B-arbre strict. Cette décision de composer si possible avec
un frère n’est pas exploitée dans le développement ci-dessous. Par contre, elle fait
l’objet de l’exercice 6.7.3, page 244, dans lequel l’étude de la stratégie « rotation
d’abord » est proposée.
Ce qu’il faut retenir de cette réflexion à propos de l’insertion c’est d’une part
qu’un nœud doit pouvoir contenir momentanément 2 · n + 1 valeurs et d’autre
part qu’il est pratique de faire l’hypothèse que l’adjonction dans un B-arbre ne
change pas son rayon. Ceci est manifestement faux dans deux cas particuliers :
le cas de l’insertion dans un arbre vide (le rayon passe de zéro à un) et le cas où,
après insertion, la racine devient excédentaire (il faut un traitement ad hoc afin
de remonter la valeur médiane, ce qui entraîne une augmentation du rayon). Les
conséquences portent à la fois sur la définition du support et sur l’intérêt qu’il y
222 Structures de données et méthodes formelles

a à utiliser une opération auxiliaire. Pour ce qui concerne l’opération auxiliaire


eAjoutAux_b(v, b), celle-ci exploite l’hypothèse que l’insertion ne provoque pas
d’augmentation du rayon de l’arbre mais, en contrepartie, elle délivre non pas
un B-arbre strict mais un B-arbre dans lequel la contrainte sur la largeur de la
racine est abandonnée pour accepter une racine de largeur maximum 2 · n + 1.

11 24 34 43 (1)

3 5 7 10 12 14 16 17 25 29 30 35 38 39 40 44 49 51

11 24 34 43 (2)

3 5 7 10 12 14 16 17 25 29 30 35 36 38 39 40 44 49 51

11 24 34 40 (3)

3 5 7 10 12 14 16 17 25 29 30 35 36 38 39 43 44 49 51

Figure 6.11 – Insertion par rotation dans un B-arbre.


Cette figure montre l’évolution d’un B-arbre d’ordre deux lors de l’in-
sertion, par la méthode des rotations, de la valeur 36. la partie (1)
montre la localisation de la feuille où se fait l’insertion. La partie (2)
illustre l’insertion proprement dite. La feuille ainsi modifiée devient ex-
cédentaire. La partie (3) montre comment, par une rotation droite, la
feuille rétablit son équilibre en s’allégeant d’une clé qui est transférée
vers le père, alors que le père transmet une valeur au frère droit (qui
pouvait encore recevoir une clé sans devenir lui-même excédentaire).

Du point de vue du support concret, ceci nous oblige à ajouter un argument


au type nba, afin de rendre compte de la possibilité pour un nœud d’avoir une
borne supérieure qui peut être soit 2 · n soit 2 · n + 1 :

concreteType nba(n, inf, sup) . . .


constraints
n ∈ N1 ∧ inf ∈ N ∧ sup ∈ N ∧
inf .. sup ∈ {1 .. 2 · n, n .. 2 · n, 1 .. (2 · n + 1), n .. (2 · n + 1)}
6. Ensembles de clés scalaires 223

t ∈ 1 .. 2 · n + 1 → N ∧ lg ∈ inf .. sup ∧ ∀i ·(i ∈ 1 .. lg − 1 ⇒ t(i) < t(i + 1))



t ∈ tt(lg)

1) a =  ⇒ a ∈ nBA(n, inf, sup)


2) a = te, tl, lg ∧
lg ∈ inf .. sup ∧
te ∈ tt(lg) ∧
tl ∈ 0 .. (2 · n + 1) → nBA(n, n, 2 · n) ∧
ran(0⎛ .. lg  tl ; r) = {r(tl(0))} ∧ ⎞
i ∈ 1 .. lg
∀i · ⎝ ⇒ ⎠
max(A(tl(i − 1))) < te(i) ∧ min(A(tl(i))) > te(i)

a ∈ nBA(n, inf, sup)

Considérons enfin le cas de l’opération de suppression. Soit à supprimer la


valeur 19 du B-arbre d’ordre deux de la figure 6.12, page suivante, partie (1).
Supprimer simplement la valeur 19 du nœud où elle est située est impossible :
d’une part il resterait encore trois sous-arbres dont l’un n’aurait pas de père et
d’autre part la contrainte sur la largeur du nœud serait violée. À l’instar de ce
qui a été fait dans la suppression d’un arbre binaire de recherche, nous déter-
minons le plus grand élément du sous-arbre à gauche de la valeur à supprimer
ainsi que le sous-arbre gauche privé de ce plus grand élément. Cette valeur (18
ici) vient remplacer la valeur 19 supprimée (cf. figure 6.12, partie (2)). Cepen-
dant, la suppression de la plus grande valeur du sous-arbre gauche produit un
sous-arbre qui n’est pas un B-arbre strict : sa racine est déficitaire 13 (cf. tou-
jours figure 6.12, partie (2)). La structure ne peut rester en l’état. Une solution
consiste à fusionner le nœud déficitaire avec l’un de ses frères en y incluant le père
commun (cf. figure 6.12, partie (3)) 14 . Ce faisant, nous n’avons fait que déplacer
le problème puisque le nœud contenant 23 est momentanément déficitaire. Nous
pouvons à nouveau appliquer la technique de la fusion (cf. figure 6.12, partie (4)).
Une conséquence est que la racine ne contient maintenant plus aucun élément !
Il faut, par une technique ad hoc, supprimer cette racine devenue inutile afin
d’obtenir l’arbre de la figure 6.12, partie (4), qui est un B-arbre strict.
Il faut retenir de cet exemple de suppression qu’un nœud peut devenir défi-
citaire. Il faut également garder présent à l’esprit qu’une opération auxiliaire est
nécessaire quand la suppression ne se fait pas sur une feuille. Cette opération
délivre le plus grand élément d’un B-arbre strict ainsi que le B-arbre corres-
pondant privé de son plus grand élément. Enfin il faut prendre conscience que
nous avons implicitement fait l’hypothèse que la suppression dans un B-arbre
ne change pas son rayon. Cette affirmation est mise en défaut dans la situation
où la largeur de la racine est de un, l’un des fils est déficitaire et la largeur de
13. Un nœud est déficitaire s’il contient n − 1 valeurs (ou aucune valeur pour la racine
« initiale »).
14. Cette fusion pourrait produire un nœud excédentaire, elle n’est donc pas toujours pos-
sible. Il faut envisager une autre solution. Là encore c’est vers une « rotation » que nous nous
tournerons.
224 Structures de données et méthodes formelles

14 (1)

4 10 19 23

1 3 5 7 8 9 11 12 13 15 18 20 22 25 29 31 34

14 (2)

4 10 18 23

1 3 5 7 8 9 11 12 13 15 20 22 25 29 31 34

14 (3)

4 10 23

1 3 5 7 8 9 11 12 13 15 18 20 22 25 29 31 34

(4)

4 10 14 23

1 3 5 7 8 9 11 12 13 15 18 20 22 25 29 31 34

4 10 14 23 (5)

1 3 5 7 8 9 11 12 13 15 18 20 22 25 29 31 34

Figure 6.12 – Suppression dans un B-arbre.


Cette figure décrit la suppression de la valeur 19 dans un B-arbre
d’ordre deux. La partie (1) montre comment se fait la localisation de
la valeur à supprimer. La partie (2) décrit la recherche et la suppres-
sion de la plus grande clé du sous-arbre gauche de la clé supprimée. La
feuille ainsi modifiée devient déficitaire. La partie (3) montre comment
ce nœud fusionne avec son frère droit et leur père en un seul nœud.
La partie (4) montre la nécessité de la suppression de la racine quand
celle-ci devient vide.
6. Ensembles de clés scalaires 225

l’autre est exactement n 15 puisque nous sommes conduit à effectuer une fusion
qui entraîne une diminution du rayon. Les incidences sur la structure du sup-
port concret se limitent au fait que la rubrique constraints est modifiée afin de
rendre compte des nouvelles possibilités pour l’intervalle inf .. sup :

concreteType nba(n, inf, sup) . . .


constraints
n ∈ N1 ∧ inf  ∈ N ∧ sup ∈ N ∧ 
1 .. 2 · n, n .. 2 · n, 1 .. 2 · n + 1,
inf .. sup ∈
n .. (2 · n + 1), 0 .. 2 · n, n − 1 .. 2 · n
Par contre, la définition du support concret reste inchangée.
Le cas général de la suppression est traité par l’opération auxiliaire
eSuppAux_b(v, b) qui exploite notamment l’hypothèse que, pour cette opéra-
tion, le rayon ne change pas. Le cas particulier mentionné ci-dessus est traité de
manière ad hoc par l’opération eSupp_b.
Théorème 13 (des B-arbres). Soit a ∈ nBA(n, inf, sup) tel que a = te, tl, lg,
soit v ∈ N et i ∈ 1 .. lg.
1. v ≥ te(i) ⇒ v ∈ / j ·(j ∈ 0 .. i − 1 | A(tl(j))),
2. v ≤ te(i) ⇒ v ∈ / j ·(j ∈ i .. lg | A(tl(j))).
Ce théorème montre que si une valeur v est supérieure ou égale (resp. inférieure
ou égale) à la clé située en position i dans te, alors v n’est pas situé dans les
sous-arbres placés à gauche (resp. à droite) de cette clé. La démonstration de ce
théorème est laissée en exercice. Un corollaire intéressant de ce théorème est le
suivant :
Corollaire 1 (du théorème 13). Soit a ∈ nBA(n, inf, sup) tel que a = te, tl, lg,
soit v ∈ N et i ∈ 1 .. lg.
(v < min(te[i .. lg]) ∧ v > max(te[1 .. i − 1])) ⇒ (v ∈ A(a) ⇔ v ∈ A(tl(i − 1)))
Ce corollaire affirme que si dans un B-arbre a, la valeur v est supérieure à toutes
les clés de la racine situées avant la position i et inférieure à toutes les clés de
la racine situées après la position i − 1, alors, si l’élément est dans l’arbre, il
est dans le sous-arbre tl(i − 1). La démonstration de ce corollaire est laissée en
exercice.
Dans les calculs de complexité réalisés sur les structures de données implan-
tées sur support physique externe, les opérations les plus coûteuses sont les
opérations de lecture et d’écriture depuis/vers ce support physique 16 . Il est en
général légitime de négliger les opérations réalisées sur des données présentes en
mémoire centrale pour se focaliser sur le nombre d’entrées-sorties. Dans le cas
d’un B-arbre, ce nombre est (c’est pour le moment une simple intuition) lié au
rayon de l’arbre. C’est pourquoi nous proposons d’étudier les relations qu’entre-
tiennent le poids et le rayon d’un B-arbre. Nous avons le théorème suivant.
15. Cette dernière contrainte est nécessaire car, comme nous le verrons ci-dessous, si le frère
de la racine déficitaire possède plus de n éléments, il peut en céder un. Une opération de
rotation permet alors de préserver le rayon.
16. Ces opérations sont de plusieurs ordres de grandeur plus coûteuses que les opérations en
mémoire centrale.
226 Structures de données et méthodes formelles

Théorème 14. Dans un B-arbre d’ordre n de rayon r et de poids p


(p + 1)
r ≤ logn+1 + 1. (6.7.8)
2
Démontrons ce théorème. Soit p(r) le poids minimum d’un B-arbre b d’ordre
n et de rayon r. Soit p (r) le poids minimum d’un B-arbre régulier (c’est-à-dire
un B-arbre dont la racine possède au moins n clés, cf. page 217) de rayon r.
Nous avons la relation suivante :
p(r) = 1 + 2 · p (r − 1) (6.7.9)
puisque b n’a qu’une seule clé à la racine et qu’elle désigne deux B-arbres réguliers
de rayon r − 1. p satisfait l’équation récurrente suivante :
 
p (0) = 0
p (r) = (n + 1) · p (r − 1) + n pour r > 0
En effet, un B-arbre de rayon 0 ne contient aucune clé et un B-arbre régulier de
poids minimum et de rayon r possède n + 1 descendants de rayon r − 1 tandis
que la racine héberge n clés. Il est facile d’obtenir la forme close suivante :

r−1
p (r) = n · (n + 1)i
i=0
k ak+1 −1
Et, puisque, pour a = 1, i=0 ai = a−1 , nous obtenons :

p (r) = (n + 1)r − 1
D’où, en reportant ce résultat dans la formule 6.7.9 :
p(r) = 2 ·(n + 1)r−1 − 1
Pour un poids p quelconque, p est supérieur ou égal à p(r) :
2 ·(n + 1)r−1 − 1 ≤ p
⇔ Arithmétique
p+1
(n + 1) r−1

2
⇔ Passage au logn+1 et arithmétique
p+1
r ≤ logn+1 +1
2
Pour n = 100, le tableau ci-dessous fournit le nombre minimum de clés (le
poids minimum) qu’il est possible d’enregistrer dans un B-arbre de rayon r.

r Poids min.
2 201
3 20 401
4 2 060 601

Nous pouvons constater qu’un B-arbre est en général très plat (c’était l’un des
objectifs, il est atteint), il n’exige donc qu’un petit nombre d’accès pour être
consulté.
6. Ensembles de clés scalaires 227

6.7.3 Définition de la fonction d’abstraction


La fonction A, qui délivre l’ensemble des valeurs présentes dans un B-arbre,
se définit par :

function A(x) ∈ nBA(n, inf, sup)  ensAbst = 


if x =  →

| x = te, tl, lg →
te[1 .. lg] ∪ i ·(i ∈ 0 .. lg | A(tl(i)))

Dans le cas d’un B-arbre non vide, cette définition affirme que l’ensemble repré-
senté est l’union de l’ensemble des valeurs à la racine (te[1 .. lg]) et
de l’union de
toutes les valeurs présentes dans les sous-arbres de cette racine ( i ·(i ∈ 0 .. lg |
A(tl(i)))).

concreteType nba(n, inf, sup) =  (nBA, (eV ide_b, eAjout_b, eSupp_b),


(eApp_b, eEstV ide_b))
uses bool, N, N1
constraints  
1 .. 2 · n, n .. 2 · n, 1 .. 2 · n + 1,
n ∈ N1 ∧ inf .. sup ∈
n .. 2 · n + 1, 0 .. 2 · n, n − 1 .. 2 · n
refines ensabst(N)
auxiliarySupports
t ∈ 1..2 · n + 1 → N ∧ lg ∈ inf..sup ∧ ∀i ·(i ∈ 1 .. lg − 1 ⇒ t(i) < t(i + 1))

t ∈ tt(lg)
support
1) a =  ⇒ a ∈ nBA(n, inf, sup)
2) a = te, tl, lg ∧ lg ∈ inf .. sup ∧ te ∈ tt(lg) ∧
tl ∈ 0 .. 2 · n + 1 → nBA(n, n, 2 · n + 1) ∧
ran(0 .. lg  tl ; r) = {r(tl(0))} ∧
∀i ·(i ∈ 1 .. lg ⇒ max(A(tl(i − 1))) < te(i) ∧ min(A(tl(i))) > te(i))

a ∈ nBA(n, inf, sup)
abstractionFunction
function A(x) ∈ nBA(n, inf, sup)  ensAbst =  ...
operationSpecifications
..
.
end

Figure 6.13 – Spécification du type concret nba – partie 1.


Cette partie concerne la définition du support.
228 Structures de données et méthodes formelles

6.7.4 Spécification des opérations concrètes


Les figures 6.13 (page précédente) et 6.14 (page ci-contre) définissent formel-
lement le type nba. Elles comprennent en particulier la spécification des deux
opérations eAjout_b et eSupp_b. Nous y retrouvons également la spécification
du support concret ainsi que les rubriques auxiliaryOperationSpecifications
et auxiliaryOperationRepresentations contenant la représentation de l’opé-
ration auxiliaire r (qui fournit le rayon d’un B-arbre) et la spécification des opéra-
tions eAjoutAux_b et eSuppAux_b. Notons que la précondition de l’opération
eAjoutAux_b exclut, conformément à la remarque formulée à la section 6.7.2,
que b soit un arbre vide. Son codomaine précise que la racine du résultat peut
être excédentaire. De même, le codomaine de l’opération eSuppAux_b stipule
que le résultat peut posséder une racine déficitaire.

6.7.5 Calcul de la représentation des opérations concrètes


Dans cette section nous développons le calcul de la représentation de l’opé-
ration eAjout_b. Le principe de l’opération eSupp_b est évoqué et son calcul
est proposé en exercice.

Calcul d’une représentation de l’opération eAjout_b


Ainsi que nous l’avons dit ci-dessus, nous allons procéder à une insertion
ascendante, c’est-à-dire à une insertion qui effectue les rééquilibrages lors de la
remontée vers la racine. Nous calculons tout d’abord la représentation de l’opéra-
tion auxiliaire eAjoutAux_b(v, b). Deux hypothèses d’induction accompagnent
ce calcul et y sont exploitées. Elles doivent être démontrées à chaque étape de
calcul. La première hypothèse postule que suite à l’insertion d’une valeur par
l’opération eAjoutAux_b, la largeur de la racine s’accroît au plus de 1.

Hypothèse d’induction 3. Soit v ∈ N et b ∈ nBA(n, 1, 2 · n) − {}. Posons


b = te, tl, sz et eAjoutAux_b(v, b) = te , tl , sz   alors sz  ∈ {sz, sz + 1}.

La seconde hypothèse d’induction postule que le rayon reste constant.

Hypothèse d’induction 4. Soit v ∈ N et b ∈ nBA(n, 1, 2 · n) − {}. Nous


avons r(eAjoutAux_b(v, b)) = r(b).

Par ailleurs nous avons besoin d’une fonction (dénommée place) qui localise
la place où devrait venir s’insérer un élément v dans un nœud t de façon à
préserver l’ordre croissant des clés. Cette fonction se spécifie dans la rubrique
auxiliaryOperationSpecifications de la manière suivante :

 ···
concreteType nba(n, inf, sup) =
..
.
auxiliaryOperationSpecifications
function place(v, l, t) ∈ N × (inf .. sup) × tt(l) → 1 .. l + 1 = 
p : (p ∈ 1 .. l + 1 ∧ v > max(t[1 .. p − 1]) ∧ v ≤ min(t[p .. l]))
end
6. Ensembles de clés scalaires 229

 ···
concreteType nba(n, inf, sup) =
..
.
operationSpecifications
..
.
function eAjout_b(v, b) ∈ N × nBA(n, 1, 2 · n) → nBA(n, 1, 2 · n) =

a : (a ∈ nBA(n, 1, 2 · n) ∧ A(a) = eAjout(v, A(b)))
;
function eSupp_b(v, b) ∈ N × nBA(n, 1, 2 · n) → nBA(n, 1, 2 · n) =

a : (a ∈ nBA(n, 1, 2 · n) ∧ A(a) = eSupp(v, A(b)))
..
.
auxiliaryOperationSpecifications
⎛ ⎞
N × nBA(n, 1, 2 · n)
function eAjoutAux_b(v, b) ∈ ⎝ →
 ⎠=
nBA(n, 1, 2 · n + 1)
pre
b = 
then
a : (a ∈ nBA(n, 1, 2 · n + 1) ∧ A(a) = eAjout(v, A(b)))
end
; ⎛ ⎞
N × nBA(n, 1, 2 · n)
function eSuppAux_b(v, b) ∈ ⎝ → ⎠=
nBA(n, 1, 2 · n)
a : (a ∈ nBA(n, 1, 2 · n) ∧ A(a) = eSupp(v, A(b)))
auxiliaryOperationRepresentations
function r(a) ∈ nBA(n, inf, sup) → N =  ...
end

Figure 6.14 – Spécification du type concret nba – partie 2.


La rubrique operationSpecifications du type nba spécifie deux des
opérations concrètes. La rubrique auxiliaryOperationSpecifications
spécifie deux opérations : eAjoutAux_b et eSuppAux_b, qui sont des
opérations auxiliaires pour l’adjonction et la suppression d’une clé. La
rubrique auxiliaryOperationRepresentations fournit la signature
de l’opération r, qui délivre le rayon d’un B-arbre.

Notons que le codomaine de cette fonction précise que la valeur délivrée par
l’appel peut être la borne supérieure du tableau t plus 1. C’est le cas si v est
supérieur à tous les éléments du t. Le raffinement de cette fonction est laissé en
exercice.
Compte tenu de la précondition de l’opération eAjoutAux_b (b = ), il est
possible de poser b = te, tl, lg. Le calcul débute par :

A(eAjoutAux_b(v, b))
230 Structures de données et méthodes formelles

= Propriété caractéristique de eAjoutAux_b


A(b) ∪ {v}
= Hypothèse
A(te, tl, lg) ∪ {v}
= Définition de A
te[1 .. lg] ∪ j ·(j ∈ 0 .. lg | A(tl(j))) ∪ {v} (6.7.10)

Nous pouvons réaliser une analyse par cas selon que v ∈ te[1 .. lg] (v est
présent dans le nœud) ou non. Si v ∈ te[1 .. lg] nous avons :

te[1 .. lg] ∪ j ·(j ∈ 0 .. lg | A(tl(j))) ∪ {v}
= Propriétés A.7 et A.9
te[1 .. lg] ∪ j ·(j ∈ 0 .. lg | A(tl(j)))
= Définition de A
A(te, tl, lg)
= Hypothèse
A(b)

D’où l’équation gardée :

let te, tl, lg := b in


v ∈ te[1 .. lg] →
eAjoutAux_b(v, b) = a
end

Dans le cas où v ∈ / te[1 .. lg], nous réalisons une induction sur la structure de
b. Repartons
de la formule 6.7.10 en supposant que le nœud considéré est une
feuille ( j ·(j ∈ 0 .. lg | A(tl(j))) = ∅ ou si l’on préfère tl(0) = ). Découpons
le calcul en deux parties. La première se préoccupe de la formule te[1 .. lg] ∪ {v}
et la seconde de j ·(j ∈ 0 .. lg | A(tl(j))). De façon à conserver un nœud trié,
l’insertion se fait à la position d’indice i = place(v, lg, te). Il faut donc « décaler »
les clés situées entre i et lg afin de laisser un emplacement pour y enregistrer v.
Commençons par éclater le tableau en deux parties :

te[1 .. lg] ∪ {v}


= Propriétés B.52, D.17, B.57 puis B.121
(1 .. i − 1  te)[1 .. lg] ∪ (i .. lg  te)[1 .. lg] ∪ {v} (6.7.11)

Le nouveau domaine sera l’intervalle 1 .. lg + 1. Nous allons tenter d’exprimer


chacune des trois sous-formules de la formule 6.7.11 comme une image du même
intervalle 1 .. lg + 1. Débutons par la première sous-formule :

(1 .. i − 1  te)[1 .. lg]
= Propriétés B.52 et A.26
(1 .. i − 1  te)[1 .. lg + 1] (6.7.12)

Concernant la seconde formule, la restriction doit elle-même être décalée. Nous


avons :
6. Ensembles de clés scalaires 231

(i .. lg  te)[1 .. lg]
= Propriété B.74
((i .. lg  te)  1)[(1 .. lg)  1]
= Propriété D.24
((i .. lg)  1  (te  1))[(1 .. lg)  1]
= Définition 1.7.1, page 38
((i + 1 .. lg + 1)  (te  1))[2 .. lg + 1]
= Propriété B.52
(te  1)[i + 1 .. lg + 1 ∩ 2 .. lg + 1]
= Propriété A.26
(te  1)[i + 1 .. lg + 1 ∩ 1 .. lg + 1]
= Calcul inverse
((i .. lg  te)  1)[1 .. lg + 1] (6.7.13)

Traitons enfin l’ensemble {v} :

{v}
= Propriété B.60
{i → v}[1 .. lg + 1] (6.7.14)

Nous pouvons à présent reprendre la formule 6.7.11 et y substituer chaque sous-


formule par les résultats 6.7.12, 6.7.13 et 6.7.14 :

(1 .. i − 1  te)[1 .. lg] ∪ (i .. lg  te)[1 .. lg] ∪ {v}


= ⎛ ⎞ Calculs ci-dessus
(1 .. i − 1  te)[1 .. lg + 1]
⎜ ∪ ⎟
⎜ ⎟
⎜ {i → v}[1 .. lg + 1] ⎟
⎜ ⎟
⎝ ∪ ⎠
((i .. lg  te)  1)[1 .. lg + 1]
= Propriété B.57
((1 .. i − 1  te) ∪ {i → v} ∪ (i .. lg  te)  1)[1 .. lg + 1] (6.7.15)

Préoccupons-nous à présent de la seconde partie de la formule 6.7.10, j ·(j ∈
0 .. lg | A(tl(j))). En prolongeant le tableau 0 .. lg  tl par un élément identique
aux autres éléments (valant ) nous ne changeons pas la valeur de l’expression :

j ·(j ∈ 0 .. lg + 1 | A((tl − {j + 1 → }(j)))) (6.7.16)
= Calculs non détaillés
j ·(j ∈ 0 .. lg | A(tl(j)))

La formule 6.7.10 peut maintenant se réécrire à partir des deux résultats 6.7.15
et 6.7.16 :

te[1 .. lg] ∪ j ·(j ∈ 0 .. lg | A(tl(j))) ∪ {v}
= ⎛ Calculs incidents
⎞ ci-dessus
((1 .. i − 1)  te ∪ {i → v} ∪ (i .. lg  te  1))[1 .. lg + 1]
⎝ ⎠

j ·(j ∈ 0 .. lg + 1 | A(tl − {lg + 1 → })(j))
232 Structures de données et méthodes formelles

= ⎛ ⎞ Définition de A
(1 .. i − 1)  te ∪ {i → v} ∪ (i .. lg  te  1), 
A⎝ tl − {lg + 1 → }, ⎠
lg + 1

Les contraintes imposées par le type nBA sont respectées, en particulier celles
portant sur les rayons et sur le caractère « arbre de recherche » de la struc-
ture. D’où, d’après la propriété de l’équation à membres identiques (page 67), la
première équation gardée pour l’opération eAjoutAux_b :

let te, tl, lg := b in


v∈/ te[1 .. lg] →
let i := place(v, lg, te) in
tl(0) =  →
eAjoutAux_b(v, b) =
 
(1 .. i − 1  te) ∪ {i → v} ∪ (i .. lg  te  1),
tl − {lg + 1 → },
lg + 1
end
end

Les hypothèses d’induction 3 et 4 (page 228) sont satisfaites puisque d’une part
la largeur du nœud s’accroît de un et d’autre part le rayon ne change pas.
Le cas inductif se caractérise par le fait que les sous-arbres ne sont pas vides.
En particulier tl(0) = . Soit i = place(v, lg, te) la position d’insertion de v dans
le tableau 1 .. lg  te. En repartant de la formule 6.7.10 nous avons donc :

te[1 .. lg] ∪ j ·(j ∈ 0 .. lg | A(tl(j))) ∪ {v}
= ⎛ ⎞ Propriété A.12
te[1 .. lg]
⎜ ⎟
⎜ ∪ ⎟
⎜ j ·(j ∈ 0 .. lg − {i − 1} | A(tl(j))) ⎟
⎜ ⎟
⎜ ⎟
⎜ ∪ ⎟
⎜ j ·(j = i − 1 | A(tl(j))) ⎟
⎜ ⎟
⎝ ∪ ⎠
{v}
= Propriété A.15
te[1 .. lg] ∪ j ·(j ∈ 0 .. lg − {i − 1} | A(tl(j))) ∪ A(tl(i − 1)) ∪ {v}
= ⎛ ⎞ de eAjoutAux_b
Propriété caractéristique
te[1 .. lg] ∪ j ·(j ∈ 0 .. lg − {i − 1} | A(tl(j)))
⎝ ∪ ⎠ (6.7.17)
A(eAjoutAux_b(v, tl(i − 1)))

Posons te , tl , lg   = eAjoutAux_b(v, tl(i−1)). Deux cas sont alors à considérer
selon la situation de lg  par rapport à 2 · n + 1. Si lg  = 2 · n + 1, le sous-arbre
te , tl , lg   est un B-arbre strict, il peut être « accroché » à la position i − 1,
« à la place » de tl(i − 1) puisque, d’après l’hypothèse d’induction 4, le rayon de
te , tl , lg   est égal à celui de tl(i − 1). C’est ce que nous exprimons ci-dessous :
6. Ensembles de clés scalaires 233

⎛ ⎞
te[1 .. lg] ∪ j ·(j ∈ 0 .. lg − {i − 1} | A(tl(j)))
⎝ ∪ ⎠
A(eAjoutAux_b(v, tl(i − 1)))
= ⎛ ⎞ Propriété A.15
te[1 .. lg] ∪ j ·(j ∈ 0 .. lg − {i − 1} | A(tl(j)))
⎝ ⎠

j ·(j = i − 1 | A(eAjoutAux_b(v, tl(i − 1))(j)))
= ⎛ Propriété A.12 et propriété de⎞A
te[1 .. lg]
⎝ ⎠

j ·(j ∈ 0 .. lg | A(tl − {i − 1 → eAjoutAux_b(v, tl(i − 1))}(j)))

Il est alors possible d’appliquer la fonction A à l’ensemble, car l’arbre « accroché »


en i − 1 a le même rayon que les autres sous-arbres, le tout est bien un B-arbre
strict.
⎛ ⎞
te[1 .. lg]
⎝ ⎠

j ·(j ∈ 0 .. lg | A(tl − {i − 1 → eAjoutAux_b(v, tl(i − 1))}(j)))
= Définition de A
A(te, tl − {i − 1 → eAjoutAux_b(v, tl(i − 1))}, lg)

D’où, d’après la propriété de l’équation à membres identiques (page 67), la se-


conde équation gardée pour l’opération eAjoutAux_b :

let te, tl, lg := b in


v∈/ te[1 .. lg] →
tl(0) =  →
let i, te , tl , lg   := place(v, lg, te), eAjoutAux_b(v, tl(i − 1)) in
lg  = 2 · n + 1 →
eAjoutAux_b(v, b) =
te, tl − {i − 1 → eAjoutAux_b(v, tl(i − 1))}, lg
end
end

Les deux hypothèses d’induction sont satisfaites : ni la largeur ni le rayon n’aug-


mentent.
Si par contre lg  = 2 · n + 1, l’arbre te , tl , lg   n’est pas un B-arbre strict.
Nous ne pouvons pas simplement « accrocher » te , tl , lg   « à la place » de
tl(i − 1). Une solution consiste, comme nous l’avons vu dans l’exemple de la
figure 6.10, page 220, à éclater l’arbre te , tl , lg   autour de sa valeur médiane,
celle-ci venant s’insérer en position i de la racine. Cette opération se spécifie de
la manière suivante :

function eclatement(ge, gl, lg, pe, pl, 2 · n + 1, i) ∈


nBA(n, 1, 2·n) × nBA(n, n, 2·n+1) × 1 .. lg+1 →  nBA(n, 1, 2·n+1) =

pre
r(ge, gl, lg) = r(pe, pl, 2 · n + 1) + 1 ∧
i = place(pe(n + 1), lg, ge, gl, lg) ∧
234 Structures de données et méthodes formelles

 
v ∈ N ∧ place(v, lg, ge, gl, lg) = i ∧
∃v ·
A(pe, pl, 2 · n + 1) = A(gl(i − 1)) ∪ {v}
then
any c, re, rl where
c ∈ nBA(n, 1, 2 · n + 1) ∧ (6.7.18)
re, rl, lg + 1 ∈ nBA(n, 1, 2 · n + 1) ∧
c = re, rl,⎛lg + 1 ∧ ⎞ (6.7.19)
A(ge, gl, lg) − A(gl(i − 1))
A(c) = ⎝ ∪ ⎠ ∧ (6.7.20)
A(pe, pl, 2 · n + 1)
r(c) = r(ge, gl, lg) (6.7.21)
then
c
end
end

Le premier paramètre de l’opération eclatement est l’arbre objet de l’insertion.


C’est la racine. Le second paramètre est l’arbre objet de l’éclatement. Cet arbre
est excédentaire, il possède une racine de longueur 2 · n + 1. L’éclatement se fait
en coupant cet arbre en deux moitiés plus la médiane. Le troisième paramètre
est la position à laquelle se fait l’insertion de la médiane dans la racine. Dans la
précondition, le premier conjoint exprime que les rayons diffèrent de un, le second
conjoint exprime la relation qui lie le troisième paramètre aux deux premiers :
c’est la position où se ferait l’insertion de la médiane dans la racine. Le troisième
conjoint concerne une valeur v qui « aurait sa place en position i de ge ». Il
exprime que le second arbre résulte de l’insertion de cette valeur dans le sous-
arbre gauche de la position d’insertion dans la racine (gl(i − 1)). Cette valeur
est bien entendu celle qui a été insérée.
L’expression any exprime que l’opération délivre un B-arbre c
(conjoint 6.7.18), non vide, de largeur lg + 1 (conjoint 6.7.19), représentant un
ensemble correspondant au premier arbre moins l’ensemble correspondant au
sous-arbre gauche de la position d’insertion, union l’ensemble correspondant
au second arbre (conjoint 6.7.20). Le conjoint (6.7.21) exprime que le résultat
recherché est un arbre ayant le même rayon que l’arbre ge, gl, lg.

Cette spécification est peut-être trop forte pour avoir une solution algorith-
mique ! C’est pourquoi il est important d’en exhiber une. C’est ce que nous
ferons ci-dessous. Pour le moment nous revenons au développement principal,
en supposant disposer d’une mise en œuvre de l’opération eclatement.
Pour ce second cas, lg  = 2 · n + 1, en repartant de la formule 6.7.17, nous
avons le développement suivant :
⎛ ⎞
te[1 .. lg] ∪ j ·(j ∈ 0 .. lg − {i − 1} | A(tl(j)))
⎝ ∪ ⎠
A(eAjoutAux_b(v, tl(i − 1)))
= Propriété A.13
6. Ensembles de clés scalaires 235

⎛ ⎞
te[1 .. lg] ∪ ( j ·(j ∈ 0 .. lg | A(tl(j))) − j ·(j = i − 1 | A(tl(j)))
⎝ ∪ ⎠
A(eAjoutAux_b(v, tl(i − 1))))
= ⎛ ⎞Propriété A.15
te[1 .. lg] ∪ ( j ·(j ∈ 0 .. lg | A(tl(j))) − A(tl(i − 1)))
⎝ ∪ ⎠
A(eAjoutAux_b(v, tl(i − 1)))
= ⎛ Propriété A.16

te[1 .. lg] − A(tl(i − 1)) ∪ ( j ·(j ∈ 0 .. lg | A(tl(j))) − A(tl(i − 1)))
⎝ ∪ ⎠
A(eAjoutAux_b(v, tl(i − 1)))
= ⎛ ⎞Propriété A.17
(te[1 .. lg] ∪ j ·(j ∈ 0 .. lg | A(tl(j)))) − A(tl(j − 1))
⎝ ∪ ⎠
A(eAjoutAux_b(v, tl(i − 1)))
= Définition de A
A(b) − A(tl(i − 1)) ∪ A(eAjoutAux_b(v, tl(i − 1)))
= Spécification de l’opération eclatement
A(eclatement(b, eAjoutAux_b(v, tl(i − 1)), i))

D’où, d’après la propriété de l’équation à membres identiques (page 67), l’équa-


tion gardée suivante pour l’opération eAjoutAux_b :

let te, tl, lg := b in


v∈/ te[1 .. lg] →
let i := place(v, lg, te) in
tl(0) =  →
let te , tl , lg   := eAjoutAux_b(v, tl(i − 1)) in
lg  = 2 · n + 1 →
eAjoutAux_b(v, b) =
eclatement(b, eAjoutAux_b(v, tl(i − 1)), i)
end
end
end

En outre, de par la spécification de l’opération eclatement, le résultat satisfait


les hypothèses d’induction 3 et 4 : la largeur de l’arbre augmente de 1, son rayon
ne change pas.
Au total, sous réserve de l’existence d’une mise en œuvre de l’opé-
ration eclatement, nous avons calculé la version suivante de l’opération
eAjoutAux_b(v, b) :
236 Structures de données et méthodes formelles

function eAjoutAux_b(v, b) ∈ N × nBA(n, 1, 2 · n) →  nBA(n, 1, 2 · n + 1) =



pre
b = 
then
let te, tl, lg := b in
if v ∈ te[1 .. lg] → /*fausse insertion*/
a
|v∈ / te[1 .. lg] →
let i := place(v, lg, te) in
if tl(0) =  → /*insertion aux feuilles*/
 (1 .. i − 1  te) ∪ {i → v} ∪ (i .. lg  te  1), 
tl − {lg + 1 → },
lg + 1
| tl(0) =  → /*insertion nœud interne*/
let te , tl , lg   := eAjoutAux_b(v, tl(i − 1)) in
if lg  = 2 · n + 1 →
te, tl − {i − 1 → eAjoutAux_b(v, tl(i − 1))}, lg
| lg  = 2 · n + 1 →
eclatement(b, eAjoutAux_b(v, tl(i − 1)), i)

end

end

end
end

La garde v ∈ te[1 .. lg] n’est bien entendu pas implantable directement, mais il
est facile de la raffiner (par exemple en utilisant la fonction place).

Recherche d’une mise en œuvre pour l’opération eclatement


Revenons à présent sur l’opération eclatement dans le but de mettre en
évidence une mise en œuvre qui satisfasse la spécification. Schématiquement, la
spécification se présente sous la forme 17 :

1 i lg 1 n+1 2·n+1
geG geD peG m peD

gl(0) gl(i − 1) gl(lg) pl(0) pl(n) pl(n + 1) pl(2 · n + 1)

où l’arbre de gauche (ge, gl, lg) représente l’arbre dans sa configuration ini-
tiale tandis que celui de droite (pe, pl, 2 · n + 1) est le sous-arbre résultant de
l’adjonction dans gl(i − 1). Le schéma ci-dessous fournit une solution pour par-
venir à un arbre c = re, rl, lg + 1 répondant à la spécification de l’opération
d’éclatement.
17. Voir encadré page 199.
6. Ensembles de clés scalaires 237

1 i lg + 1
re geG m geD

gl(0) peG peD gl(lg)

rl(i − 1) pl(0) pl(n) pl(n + 1) pl(2·n + 1) rl(i)

Les arbres rl(i − 1) et rl(i) sont des B-arbres stricts ayant même rayon que
gl(0) puisque l’arbre pe, pl, 2 · n + 1 a le même rayon que gl(0). Par ailleurs
ce sont des arbres de recherche, c satisfait donc la spécification de l’opération
d’éclatement. Dans le cas où l’invocation de l’opération eAjoutAux_b se réduit
à l’appel de l’opération d’éclatement, c satisfait également les hypothèses d’in-
duction 3 et 4 (page 228) puisque la largeur de c est égale à celle de ge, gl, lg
plus un, et que le rayon de c est égale à celui de ge, gl, lg. La mise en œuvre
de l’opération d’éclatement se présente comme suit :

function eclatement(ge, gl, lg, pe, pl, 2 · n + 1, i) ∈ nBA(n, 1, 2 · n)×


nBA(n, n, 2 · n + 1) × 1 .. lg + 1 →  nBA(n, 1, 2 · n + 1)
pre
r(ge, gl, lg) = r(pe, pl, 2 · n + 1) + 1 ∧
i = place(pe(n
⎛ + 1), lg, ge, gl, lg) ∧ ⎞
v∈N∧
∃v · ⎝ place(v, lg, ge, gl, lg) = i ∧ ⎠
A(pe, pl, 2 · n + 1) = A(gl(i − 1)) ∪ {v}
then

(1 .. i − 1  ge) ∪ {i → pe(n + 1)} ∪ (i .. lg  ge  1),⎞

0 .. i − 2  gl
⎜ ∪ ⎟
⎜ ⎟
⎜ {i − 1 → 1 .. n  pe, 0 .. n  pl, n} ⎟
⎜ ⎟
⎜ ⎟
⎜ ⎧ ∪   ⎫ ⎟
⎜ ⎨
⎜ (n + 2 .. 2 · n + 1  pe)  −(n + 1), ⎬ ⎟ ⎟,
⎜ i → (n + 1 .. 2 · n + 1  pl)  −(n + 1), ⎟
⎜ ⎩
⎜ ⎭ ⎟

⎜ n ⎟
⎝ ∪ ⎠
(i .. lg  gl)  1
lg + 1

end

Pour ce qui concerne la complexité, nous négligeons les traitements réalisés en


mémoire principale pour ne retenir que le nombre de nœuds transférés entre
deux niveaux de mémoire et qui est en O(1).
238 Structures de données et méthodes formelles

Fichiers séquentiels indexés


Si aujourd’hui réaliser un système d’information ne se conçoit pas sans
avoir à sa disposition un système de base de données, il n’en a pas été tou-
jours ainsi. Nous avons vu (cf. encadré page 79) que les premiers systèmes
d’information réalisés furent totalement « séquentiels » puisqu’implantés
sur bandes magnétiques. La transition vers les bases de données s’est dé-
roulée progressivement. Les mémoires secondaires à accès aléatoire (les
disques ou tambours magnétiques) ont rendu possible l’accès direct à l’in-
formation. On a alors voulu marier les avantages des deux systèmes en
autorisant pour un même fichier l’accès séquentiel et l’accès direct. C’est
l’origine du mode séquentiel indexé apparu dans les systèmes de gestion
de fichiers au cours des années 1960.

Les schémas ci-dessus, fondés sur la métaphore de l’organisation d’un


appartement de quatre pièces, illustrent les trois modes d’accès cités ci-
dessus. Le schéma de gauche porte sur l’accès séquentiel : depuis le couloir
on accède à la première pièce qui elle-même permet l’accès à la seconde
pièce, etc. Pour accéder à une pièce donnée de l’appartement il est né-
cessaire de traverser toutes les pièces qui précèdent. Le schéma du milieu
représente l’accès direct : quelle que soit la pièce, il est possible de s’y
introduire directement depuis le couloir. Enfin le schéma de droite illustre
le mariage des deux types d’accès : séquentiel ou direct, au choix. Notons
que l’existence de l’accès direct permet de simuler efficacement l’accès sé-
quentiel. L’inverse n’est pas vrai.
Il ne restait plus, si l’on peut dire, qu’à implanter efficacement ces fi-
chiers séquentiels indexés. Différentes techniques ont été proposées ayant
le plus souvent en commun l’utilisation d’une variante des B-arbres. Celle
connue sous le nom de B+ -arbre (cf. section 6.5.6) présente de ce point
de vue quelques avantages : un accès direct efficace rendu possible grâce
à l’index, un accès séquentiel possible en tant que sous-produit du chaî-
nage des enregistrements. Les fichiers séquentiels indexés n’ont pas pour
autant disparu à l’avènement des bases de données, ils ont simplement
quitté le devant de la scène, puisque l’implantation physique d’une re-
lation dans une base de données relationnelle se fait en général par un
fichier séquentiel indexé comportant un index pour l’identifiant et, selon
le degré d’efficacité souhaité, un index pour les attributs les plus sollicités.
6. Ensembles de clés scalaires 239

Retour sur l’opération eAjout_b(v, b)


Souvenons-nous que l’opération eAjoutAux_b est une opération auxiliaire
traitant le cas général de l’insertion dans un B-arbre à l’exclusion de l’inser-
tion dans un B-arbre vide et que le résultat n’est en général pas un B-arbre
strict. Nous allons maintenant réaliser le développement de l’opération eAjout_b
en introduisant, ainsi que nous l’avons prévu, un appel à la fonction auxiliaire
eAjoutAux_b. Nous réalisons une analyse par cas.
A(eAjout_b(v, b))
= Spécification de l’opération eAjout_b
eAjout(v, A(b))
Nous considérons tout d’abord le cas b =  :
eAjout(v, A(b))
= Hypothèse
eAjout(v, A())
= Spécification de A et de eAjout et propriété A.9
{v}
= Définition de A
A({1 → v}, {0 → , 1 → }, 1)
D’où, d’après la propriété de l’équation à membres identiques (page 67), la pre-
mière équation gardée :
b =  →
eAjout_b(v, b) = {1 → v}, {0 → , 1 → }, 1
Le second cas de l’analyse se caractérise par la condition b = . Posons
b = te, tl, lg. Par hypothèse b ∈ nBA(n, 1, 2 · n).
eAjout(v, A(b))
= Spécification de l’opération eAjoutAux_b
A(eAjoutAux_b(v, b))

À ce stade, nous ne devons pas commettre l’erreur d’en conclure que, sous
l’hypothèse de la garde b = , eAjout_b(v, b) = eAjoutAux_b(v, b) puisque
l’expression eAjoutAux_b(v, b) peut ne pas être du type requis par l’opération
eAjout_b(v, b) : en effet, par hypothèse, nous avons d’une part eAjout_b(v, b) ∈
nBA(n, 1, 2 · n) et d’autre part eAjoutAux_b(v, b) ∈ nBA(n, 1, 2 · n + 1).
Il convient de réaliser une (sous-)analyse par cas selon que la racine de
l’arbre eAjoutAux_b(v, b) est ou non excédentaire. Posons te , tl , lg   =
eAjoutAux_b(v, b). Si lg  = 2 · n + 1, la racine n’est pas excédentaire, nous
avons affaire à un B-arbre strict, d’où l’équation gardée :
b = te, tl, lg →
let te , tl , lg   := eAjoutAux_b(v, b) in
lg  = 2 · n + 1 →
eAjout_b(v, b) = te , tl , lg  
end
240 Structures de données et méthodes formelles

Si par contre lg  = 2 · n + 1, la racine est excédentaire, il faut ériger la médiane


de l’arbre te , tl , lg   en racine, comme le montre la figure 6.15 ci-dessous.

1 n+1 2·n+1
te teG m teD (1)

tl (0) tl (n) tl (n + 1) tl (2 · n + 1)

1
m (2)
1 n 1 n
teG teD

tl (0) tl (n) tl (n + 1) tl (2 · n + 1)

Figure 6.15 – Opération eAjout_b, cas particulier.


L’un des cas particuliers à traiter lors de l’adjonction d’une clé est
celui où, après l’ajout, la racine est excédentaire. Il faut alors éclater
la racine en deux parties autour de la médiane. Ces deux parties sont
des B-arbres stricts, ils deviennent fils de la médiane qui elle-même
devient la nouvelle racine. Le rayon augmente de un. La partie (1)
présente la précondition de ce traitement tandis que la partie (2) montre
la postcondition.

Il est facile d’en déduire l’équation gardée suivante :

b = te, tl, lg →


let te , tl , lg   := eAjoutAux_b(v, b) in
lg  = 2 · n + 1 →
eAjout_b(v, b) =


{1
⎧ → te (n + 1)},  ⎫

⎪ 0 → 1 .. n  te , 0 .. n  tl , n, ⎪
⎨  (n + 2 .. 2 · n + 1  te  ⎪

)  −(n + 1),
,
⎪ 1 →
⎪ (n + 1 .. 2 · n + 1  tl )  −(n + 1), ⎪

⎩ ⎭
n
1

end

Au total, nous avons calculé la représentation suivante de l’opération


eAjout_b :
6. Ensembles de clés scalaires 241

function eAjout_b(v, b) ∈ N × nBA(n, 1, 2 · n) → nBA(n, 1, 2 · n) = 


if b =  →
{1 → v}, {0 → , 1 → )}, 1
| b = te, tl, lg →
let te , tl , lg   := eAjoutAux_b(v, b) in
if lg  = 2 · n + 1 →
te , tl , lg  
| lg  = 2 · n + 1 →


{1
⎧ → te (n + 1)},  ⎫
⎪ 0 → 
⎪ 1 .. n  te , 0 .. n  tl , n, ⎪

⎨ ⎬
(n + 2 .. 2 · n + 1  te )  −(n + 1), 
,
⎪ 1 →
⎪ (n + 1 .. 2 · n + 1  tl )  −(n + 1), ⎪

⎩ ⎭
n
1


end

Notons que l’accroissement d’un B-arbre se fait toujours par la racine. Lorsque
celle-ci est excédentaire, elle est éclatée en deux nœuds qui deviennent fils de la
médiane, cette dernière devenant la nouvelle racine.
Le nombre de nœuds lus ou écrits lors d’une insertion dans un B-arbre d’ordre
n et de poids p est en O(logn+1 p). En effet, l’insertion se faisant aux feuilles, pour
insérer un élément il faut descendre dans l’arbre en traversant tous les niveaux.
Lors de la remontée, il peut être nécessaire de réaliser un éclatement à chaque
niveau, ce qui exige jusqu’à quatre opérations de lecture/écriture. Une écriture
supplémentaire peut être nécessaire si l’arbre voit son rayon augmenter. Compte
tenu des propriétés des logarithmes, cette complexité s’exprime également sous la
forme O(log p), cependant le facteur multiplicatif sous-jacent doit nous mettre
en garde pour le cas où nous serions tentés d’assimiler les performances d’un
B-arbre avec ceux d’un Avl dans le cas d’une utilisation pour des supports
physiques externes : pour une même cardinalité, le rayon d’un B-arbre est en
général très inférieur à celui d’un Avl, c’est donc une solution incomparablement
plus efficace en nombre d’accès mémoire.

L’opération de suppression eSupp_b


Cette opération n’est pas calculée ici. Elle est proposée à l’exercice 6.7.1.
Cependant plusieurs remarques sont à formuler. À la section 6.7.2, nous avons
disséqué une suppression particulière (cf. figure 6.12, page 224). Nous voulons
insister ici sur les différences qui existent entre adjonction et suppression. Alors
qu’il est toujours possible (mais pas forcément judicieux) d’effectuer les adjonc-
tions en ne procédant que par éclatement, il n’est pas toujours possible de réali-
ser des suppressions en n’utilisant que des fusions. La figure 6.16, page suivante,
montre :
242 Structures de données et méthodes formelles

1. un premier schéma où une fusion est impossible (les nœuds frères sont
pleins) mais une rotation est alors possible (l’un des nœuds frères peut
céder une valeur) ;
2. un second schéma où une rotation est impossible (les nœuds frères sont au
minimum) mais une fusion l’est (avec n’importe lequel des frères) ;
3. enfin un troisième schéma où fusion et rotation sont toutes deux possibles.
À partir des deux premiers schémas de la figure 6.16, nous pouvons conclure
que si une opération (fusion ou rotation) n’est pas possible, l’autre l’est. Et à
partir du dernier schéma, nous déduisons qu’il est possible d’adopter (au moins)
deux types de stratégie : « fusion d’abord » ou « rotation d’abord ». De même
que dans le cas des adjonctions, l’avantage d’une rotation est qu’elle bloque toute
nécessité de rééquilibrage au-dessus du nœud pivot de la rotation puisque l’arbre
obtenu est un B-arbre strict. La stratégie « rotation d’abord » est donc, dans le
cas des suppressions ascendantes, la meilleure des deux.

(1) 17 22 Rotation 17 22

7 8 12 14 20 27 29 32 41 7 8 12 14 20 27 29 32 41

(2) 10 14 Fusion 14

7 8 12 15 19 7 8 10 12 15 19

10
on
Fusi
(3) 10 14 7 8 9 12 14 15 19
Rot
atio
n
9 14
7 8 9 12 15 19

7 8 10 12 15 19

Figure 6.16 – Rééquilibrage après suppression dans un B-arbre.


Cette figure décrit différentes façons de rééquilibrer un B-arbre d’ordre
2 présentant un nœud déficitaire suite à une suppression. La partie (1)
décrit une situation où une fusion n’est pas possible mais une rotation
l’est. La partie (2) montre au contraire une situation où une rotation
n’est pas possible mais une fusion l’est. Enfin la partie (3) décrit une
situation où fusion et rotation sont possibles. La rotation permet tou-
jours d’obtenir un B-arbre strict. Ce n’est pas toujours le cas de la
fusion ainsi que le montrent les parties (2) et (3).
6. Ensembles de clés scalaires 243

6.7.6 Conclusion et remarques bibliographiques


Les B-arbres ont été découverts en 1970 par R. Bayer et E. McCreight, et
publiés en 1972 [12]. Ils constituent une bonne solution pour implanter des en-
sembles sur mémoires secondaires, notamment sur le plan de la complexité tem-
porelle. L’administrateur doit cependant être vigilant sur l’adéquation entre la
taille des unités physiques de la mémoire et la taille des nœuds. L’inconvénient
principal concerne la place occupée puisque, dans le pire des cas, seule la moi-
tié de la place allouée est véritablement occupée par de l’information utile (en
moyenne le taux de remplissage après insertion aléatoire est cependant de ln 2,
cf. [75], page 489). De nombreuses variantes ont émergé depuis la découverte des
B-arbres. Certaines sont proposées en exercice ci-dessous, d’autres ont déjà été
évoquées en introduction.
Rappelons tout d’abord la stratégie consistant, pour une adjonction, à ef-
fectuer si possible une rotation de préférence à un éclatement (cf. figure 6.11
et exercice 6.7.3). Cette variante a été suggérée par R. Bayer et E. McCreight
(cf. [12], p. 256). Elle présente le double avantage de mieux remplir les nœuds
et de bloquer le processus d’éclatement en remontant le long du chemin de la
recherche puisque dans ce cas la largeur de la racine n’augmente pas. Dans un
article récent [107], S. Sen et R. Tarjan proposent une forme relâchée de B-arbres
qui, tout en restant efficaces, permet des suppressions sans rééquilibrage. Cette
simplification s’obtient au prix d’une reconstruction périodique de l’arbre.
Nous avons vu ci-dessus la méthode des arbres externes appliquée aux ar-
bres binaires. Il est possible de retenir cette technique dans le cas des B-arbres
(cf. exercice 6.7.5 dans lequel nous proposons de réaliser les calculs d’opérations
avec des arbres externes, puis de renforcer le support en décomposant le min et le
max sur la structure de données lors d’un ultime raffinement). Le résultat, connu
sous le nom de B+ -arbre, présente plusieurs avantages sur les B-arbres classiques.
Le premier concerne le volume (et donc le temps de lecture ou d’écriture) d’un
nœud : les niveaux intermédiaires n’ont pas besoin de contenir l’information (en
général) associée à chaque clé. À taille égale, un nœud interne est à même de
recevoir plus de clés.
Le second avantage concerne l’ouverture vers un autre type d’accès : comme
toute l’information est contenue aux feuilles (les niveaux internes constituent
un index), il est possible de chaîner entre elles toutes les feuilles (chaînage uni-
directionnel ou bidirectionnel). Cette structure complémentaire ajoute à l’accès
direct (ou indexé) une possibilité d’accès séquentiel aux clés depuis une clé quel-
conque. Cependant, la structure ainsi complétée n’est pas « fonctionnelle » : il
est probablement impossible de la définir par un support inductif.

Exercices

Exercice 6.7.1 Calculer une version ascendante de la suppression dans les B-arbres selon la
stratégie « rotation d’abord ».

Exercice 6.7.2 Considérons des B-arbres d’ordre 2 dans lesquels les mises à jour se font
de manière ascendante, l’insertion se fait par éclatement et la suppression selon la stratégie
« rotation d’abord ».
244 Structures de données et méthodes formelles

1. Montrer l’évolution que subit un B-arbre vide lors de l’ajout successif des valeurs 10,
21, 32, 27, 16, 8, 19, 29, 39, 35, 23, 30, 31, 15, 17, 18 et 20.
2. Montrer l’évolution depuis un B-arbre vide en prenant la liste inverse de la liste ci-
dessus.
3. On considère de manière générale les B-arbres obtenus par insertion de deux permuta-
tions différentes d’une même liste de valeurs. Que peut-on dire à propos de :
(a) l’identité des deux arbres,
(b) le nombre de nœuds des deux arbres,
(c) le taux d’occupation des deux arbres,
(d) le rayon des deux arbres ?
Justifier vos réponses.
4. On considère l’arbre obtenu à l’issue de la première question. Montrer comment évolue
cet arbre si l’on supprime les clés dans l’ordre inverse de l’ordre d’insertion.

Exercice 6.7.3 À la page 221 nous avons vu qu’il est parfois possible et intéressant d’effectuer
des rotations quand, lors d’une insertion, un nœud devient excédentaire. Dans cet exercice,
nous proposons de réfléchir à cette solution.
1. Spécifier les opérations de rotation droite et gauche.
2. Fournir une mise en œuvre de ces deux opérations.
3. Dans le cas où l’insertion conduit à un nœud excédentaire, calculer une solution fondée
sur la stratégie « rotation d’abord ».

Exercice 6.7.4 Pour ce qui concerne l’insertion dans un B-arbre, ci-dessus nous n’avons déve-
loppé que la méthode dite de l’insertion ascendante (celle dans laquelle l’éclatement éventuel
se fait en remontant, après l’insertion dans une feuille). Il est possible d’envisager une insertion
descendante, dans laquelle les éclatements se font lors de la descente. Nous nous proposons
d’étudier cette méthode ici. Le principe est le suivant. Lors de la recherche dans un nœud a,
si le fils de a dans lequel va se poursuivre la recherche est plein, la valeur médiane de ce nœud
fils est remontée dans a et le nœud fils est éclaté en 2 nœuds contenant les valeurs restantes,
qui deviennent les fils de la valeur remontée. Lorsque l’on atteint une feuille, celle-ci n’est pas
pleine (le cas échéant elle aurait été éclatée), on peut donc y effectuer l’insertion directement.
1. Montrer, sur l’exemple de la figure 6.10, page 220, comment s’effectue l’insertion de la
valeur 36.
2. La structure du support des B-arbres doit être remise en cause. Pourquoi ? Définir le
nouveau support.
3. Calculer la représentation de l’opération eAjout_b qui réalise l’insertion descendante.
4. L’insertion descendante revient à éclater un nœud dès que c’est possible (et non plus
dès que c’est nécessaire comme dans l’insertion ascendante). On pourrait de même
définir une suppression descendante dans laquelle la fusion de 2 nœuds se ferait dès
que c’est possible. Discuter de l’utilisation conjointe de l’insertion et de la suppression
descendante.

Exercice 6.7.5 La version « arbre externe » des B-arbres se dénomme B+ -arbres. Spécifier
les B+ -arbres puis calculer les différentes opérations. Raffiner par renforcement de support, en
décomposant les fonctions auxiliaires utilisées sur la structure de données.

Exercice 6.7.6 (B∗ -arbres) On appelle B∗ -arbre d’ordre n un arbre 3 · n-aire ayant les
mêmes propriétés que les B-arbres (cf. page 216) à l’exception des points 2 et 2 bis qui sont
remplacés par :

2. Chaque nœud contient entre n et 3 · n valeurs organisées en tableau, à l’exception de la


racine.
2 bis. La racine contient entre 1 et 3 · n valeurs.

Étudier les B∗ -arbres (spécification concrète, calcul des principales opérations) et discuter des
avantages et inconvénients par rapport aux B-arbres.
6. Ensembles de clés scalaires 245

6.8 Structures autoadaptatives et analyse amor-


tie : les arbres déployés
6.8.1 Introduction
Si l’on écarte les B-arbres et les techniques connexes, qui sont destinés aux
ensembles implantés sur des supports externes, l’intérêt des arbres équilibrés ré-
side principalement dans une complexité faible et uniforme pour les opérations
classiques. Par contre, on peut leur reprocher de conduire à des algorithmes dif-
ficiles à construire qui nécessitent souvent de l’espace mémoire supplémentaire
pour accueillir une information sur l’équilibre des nœuds. Est-il possible d’éviter
cet inconvénient tout en préservant les avantages ? La réponse est oui à condition
d’avoir une exigence différente (et un peu affaiblie) pour ce qui concerne la com-
plexité. Il faut alors se tourner vers des structures de données autoadaptatives
auxquelles est en général associée la notion d’analyse amortie de la complexité.
L’objectif de cette section est d’étudier un archétype de ces méthodes : les ar-
bres déployés. Un autre exemple est étudié au chapitre 9 portant sur les files de
priorité : les minimiers obliques.
Dans les arbres déployés, le rééquilibrage se fait de manière aveugle et sys-
tématique. Nul besoin alors d’une information complémentaire sur l’état d’équi-
libre du nœud. Concernant la complexité des opérations, celle-ci est calculée
en réalisant une (forme de) moyenne s’appliquant sur une séquence d’appels de
l’opération considérée. Le prix à payer est une complexité non uniforme : cer-
tains appels, qui se traduisent par une profonde réorganisation de la structure
de données, se révèlent alors très coûteux, mais les appels ultérieurs tirent profit
de la restructuration effectuée. En moyenne, sur plusieurs appels consécutifs à
l’opération, les coûts se compensent, la complexité devient acceptable. C’est le
principe de la complexité amortie (cf. section 4.3).
Nous allons tout d’abord étudier les fondamentaux de l’insertion dans un
arbre déployé. Nous calculons ensuite une représentation de l’opération d’inser-
tion avant d’effectuer une analyse amortie de sa complexité. L’étude des autres
opérations est proposée en exercice.

6.8.2 Principe de l’insertion dans un arbre déployé


Précisons d’emblée que le qualificatif « déployé » porte, non pas sur le support
de l’arbre, qui est celui des arbres binaires de recherche (cf. section 6.3), mais
sur le principe de la réorganisation qui s’applique à chaque opération. L’insertion
dans un arbre déployé s’effectue toujours à la racine. Cependant, de même que
dans le cas des abr quelconques (cf. section 6.3) elle peut se réaliser :
– soit par rotations après une insertion temporaire aux feuilles ;
– soit en partitionnant par rapport à la valeur à insérer.
Mais dans un cas comme dans l’autre, le déploiement se caractérise par l’uti-
lisation de rotations particulières. Dans cette section, nous nous intéressons à
l’insertion déployée par partitionnement.
Une fois le partitionnement déployé réalisé, l’adjonction proprement dite est
obtenue en enracinant les deux arbres résultant du partitionnement sur la valeur
246 Structures de données et méthodes formelles

à insérer.
Il ne s’agit pas pour l’instant de démontrer que le partitionnement réalise
effectivement une partition de l’arbre a par rapport à la valeur v. Notre objectif
se limite à comprendre comment se fait la réorganisation dans le cas général.
Le calcul de l’opération de partitionnement part(v, a) réalisé à la section 6.8.4
montre, par induction, que le résultat est bien celui souhaité.
Dans le cas général, le partitionnement d’un arbre déployé saute d’une géné-
ration à chaque étape lors de la remontée. On considère le nœud n, grand-père
de l’arbre qui a subi le partitionnement par rapport à v. Le résultat du partition-
nement à la racine n (par rapport à v) dépend de la position relative du petit-fils
et de son aïeul. Quatre cas peuvent être répertoriés : petit-fils gauche-gauche,
gauche-droit, droit-droit et droit-gauche. Des considérations de symétrie auto-
risent à n’envisager que deux cas. Pour cette raison, nous limitons notre étude
aux cas gauche-gauche (également appelé zig-zig) et gauche-droit (zig-zag).
La figure 6.17 considère le cas gauche-gauche (zig-zig) et se fonde sur l’hy-
pothèse que v < m < n et donc que c’est l’arbre g qui a été partitionné par
rapport à v, en fournissant le couple (g  , g  ). Le résultat est le couple à droite
de la figure.

a
a n zig-zig ( g , m )
l m r g  n l
g d d r

Figure 6.17 – Déploiement de type zig-zig pour un partitionnement.


Il s’agit du cas v < m < n. Le partitionnement de g a fourni le couple
d’arbres (g  , g  ). Le partitionnement de l’arbre a (de racine n) fournit
un couple dont les composants sont d’une part l’arbre g  et d’autre part
l’arbre g  , m, d, n, r.

Le cas gauche-droite (zig-zag) est représenté à la figure 6.18, page ci-contre,


et se fonde sur l’hypothèse que, puisque m < v < n, c’est le sous-arbre d qui a
subi le partitionnement déployé en fournissant le couple (d , d ).
Ces quatre cas font tous l’hypothèse qu’il existe bien un petit-fils par rapport
auquel partitionner. Si ce n’est pas le cas, aucune réorganisation n’est réalisée
et l’un des arbres du couple obtenu est vide, comme le montre le calcul de la
section 6.8.4.

6.8.3 Support concret et fonction d’abstraction


Rappelons tout d’abord que le type abstrait que nous cherchons à raffiner
par des arbres déployés est celui des ensembles de scalaires présenté à la fi-
gure 6.1, page 149. Comme annoncé ci-dessus, la différence entre arbres binaires
de recherche et arbres déployés ne porte pas sur la nature du support (ni sur la
fonction d’abstraction) mais uniquement sur le traitement réalisé lors de chaque
opération. En conséquence, le type concret que nous allons utiliser est celui déjà
6. Ensembles de clés scalaires 247

a a
a n zig-zag ( m , n )
l m r g   r
d d
g d

Figure 6.18 – Déploiement de type zig-zag pour un partitionnement.


Il s’agit du cas m < v < n. Le partitionnement de d a fourni le couple
d’arbres (d , d ). Le partitionnement de l’arbre a (de racine n) fournit
un couple dont les composants sont les arbres a = g, m, d  et a =
d , n, r.

décrit pour les arbres binaires de recherche. La version dédiée aux arbres déployés
est présentée à la figure 6.19, page 248. Celle-ci se complète de la figure 6.20,
page 249, qui introduit l’opération de partitionnement part dans la spécification.
Une remarque doit être formulée à propos de l’opération eApp_ad dont le
profil n’est pas celui attendu. En principe, les opérations eApp ont comme co-
domaine bool puisqu’elles délivrent soit true soit false. Ici, le domaine est
bool × ead. La raison est que, comme c’est souvent le cas pour les structures
autoadaptatives, les opérations qui consultent l’état de la structure en profitent
pour également réorganiser la structure. Dans le cas présent, eApp_ad effectue
une consultation déployée. Elle doit donc délivrer l’arbre résultant de cette réor-
ganisation. Pour des raisons de cohérence, la spécification abstraite devrait être
modifiée en conséquence.

6.8.4 Calcul de l’opération eAjout_ad


De même que pour l’opération eAjout_a (cf. page 158), il est pratique de
considérer que le partitionnement par rapport à v s’effectue sur un arbre privé de
la clé v. Le développement de l’opération eAjout_ad se fait alors comme celui
de l’opération eAjout_a, en utilisant l’opération de partitionnement part. Nous
obtenons :
function eAjout_ad(v, a) ∈ N × ead → ead =

if v ∈ A(a) →
a
|v∈ / A(a) →
let (l, r) := part(v, a) in
l, v, r
end

Ici aussi un raffinement ultérieur est nécessaire afin de supprimer les références
à la fonction d’abstraction. Cette étape est laissée en exercice.
Concernant l’opération part(v, a), dans le cas d’un arbre vide, le calcul de
l’équation gardée est identique à celui réalisé pour l’opération part des abr
(cf. page 158) et donne :
248 Structures de données et méthodes formelles

concreteType ead =  (ead, (eV ide_ad, eAjout_ad, eSupp_ad),


(eApp_ad, eEstV ide_ad))
uses bool, N
refines ensabst(N)
support
1)  ∈ ead
2) n ∈ N ∧ g ∈ ead ∧ d ∈ ead ∧ max(A(g)) < n ∧ min(A(d)) > n

g, n, d ∈ ead
abstractionFunction
function A(a) ∈ ead  ensAbst(N) = 
if a =  →

| a = g, n, d →
A(g) ∪ {n} ∪ A(d)

operationSpecifications
..
.
function eAjout_ad(v, a) ∈ N × ead → ead =

b : (b ∈ ead ∧ A(b) = eAjout(v, A(a)))
;
function eSupp_ad(v, a) ∈ N × ead → ead =

b : (b ∈ ead ∧ A(b) = eSupp(v, A(a)))
;
function eApp_ad(v, a) ∈ N × ead  bool × ead =

(eApp(v, A(a)), b : (b ∈ ead ∧ A(b) = A(a)))
..
.
end

Figure 6.19 – Spécification du type concret ead – partie 1.


Cette figure définit ou spécifie les constituants du type concret ead
permettant de représenter des ensembles par des arbres déployés. Cette
spécification est complétée par la figure 6.20.

a =  →
part(v, a) = (, )

Dans le cas d’un arbre non vide, posons a = l, n, r :

A (part(v, l, n, r))


= Spécification de part
A(l, n, r)
= Définition de A
A(l) ∪ {n} ∪ A(r) (6.8.1)
6. Ensembles de clés scalaires 249

 ···
concreteType ead =
..
.
auxiliaryAbstractionFunction
function A ((a, b)) ∈ ead × ead  ensAbst(N) =
 A(a) ∪ A(b)
..
.
auxiliaryOperationSpecifications
function part(v, a) ∈ N × ead   ead × ead =
pre
v∈/ A(a)
then ⎛ ⎛  ⎞⎞
A ((g, d)) = A(a)
⎜ ⎜ ∧ ⎟⎟
⎜ ⎜ ⎟⎟
(g, d) : ⎜
⎜ g, d ∈ ead × ead ∧ ⎜ max(A(g)) < v

⎟⎟
⎟⎟
⎝ ⎝ ∧ ⎠⎠
v < min(A(d))
end
end

Figure 6.20 – Spécification du type concret ead – partie 2.


Cette figure complète la figure 6.19 et présente la spécification de l’opé-
ration part et la fonction d’abstraction A qui l’accompagne.

Nous procédons à une première analyse par cas, selon la position relative de n et
de v. Si v < n nous coupons « à gauche ». Une seconde analyse par cas s’impose
alors, selon que l =  ou non. Si l = , nous poursuivons par :

A(l) ∪ {n} ∪ A(r)


= Hypothèse et propriété A.9
A() ∪ ∅ ∪ {n} ∪ A(r)
= Définition de A
A() ∪ A() ∪ {n} ∪ A(r)
= Définition de A
A() ∪ A(, n, r )
= Définition de A

A ((, , n, r ))

Nous obtenons bien une partition de l’arbre a par rapport à la valeur v. D’où
l’équation gardée :

a = l, n, r →
v<n →
l =  →
part(v, a) = (, , n, r)

Pour le cas l = , posons l = g, m, d et repartons de la formule 6.8.1 :


250 Structures de données et méthodes formelles

A(l) ∪ {n} ∪ A(r)


= Hypothèse
A(g, m, d) ∪ {n} ∪ A(r)
= Définition de A
A(g) ∪ {m} ∪ A(d) ∪ {n} ∪ A(r) (6.8.2)

Afin d’obtenir une partition par rapport à v, nous allons partitionner soit l’arbre
g soit l’arbre d selon la position relative de v et de m. Si v < m, nous supposons
(hypothèse d’induction) que part(v, g) = (g  , g  ) :

A(g) ∪ {m} ∪ A(d) ∪ {n} ∪ A(r)


= Définition de A
A(g) ∪ {m} ∪ A(d, n, r)
= Spécification de part, hypothèse
A ((g  , g  )) ∪ {m} ∪ A(d, n, r)
= Définition de A
 
A(g ) ∪ A(g ) ∪ {m} ∪ A(d, n, r)
= Définition de A
A(g  ) ∪ A(g  , m, d, n, r)
= Définition de A
  
A ((g , g , m, d, n, r))

Il s’agit bien d’une partition de l’arbre a par rapport à v. D’où l’équation gardée :

a = l, n, r →
v<n →
let g, m, d := l in
v<m →
let (g  , g  ) := part(v, g) in
part(v, a) = (g  , g  , m, d, n, r)
end
end

Le cas v > m se traite de manière similaire en posant (d , d ) = part(v, d) et en


repartant de la formule 6.8.2 :

A(g) ∪ {m} ∪ A(d) ∪ {n} ∪ A(r)


= Spécification de part, hypothèse
A(g) ∪ {m} ∪ A ((d , d )) ∪ {n} ∪ A(r)
= Définition de A
 
A(g) ∪ {m} ∪ A(d ) ∪ A(d ) ∪ {n} ∪ A(r)
= Définitions de A et de A
  
A ((g, m, d , d , n, r))

Nous obtenons bien une partition de l’arbre a par rapport à v. D’où l’équation
gardée :

a = l, n, r →
v<n →
6. Ensembles de clés scalaires 251

let g, m, d := l in
v>m →
let (d , d ) := part(v, d) in
part(v, a) = (g, m, d , d , n, r)
end
end

Le cas v > n se traite de manière analogue. Le cas v = m ne peut survenir


(v n’est pas dans l’arbre) pas plus que le cas v = n. Au total nous avons calculé
la représentation suivante de part :

function part(v, a) ∈ N × ead   ead × ead =



pre
v∈ / A(a)
then
if a =  →
(, )
| a = l, n, r →
if v < n →
if l =  →
(, , n, r)
| l = g, m, d →
if v < m →
let (g  , g  ) := part(v, g) in
(g  , g  , m, d, n, r)
end
|v>m →
let (d , d ) := part(v, d) in
(g, m, d , d , n, r)
end


|v>n →
..
.


end

6.8.5 Analyse amortie de l’opération eAjout_ad


Le calcul de la complexité amortie de l’opération eAjout_ad se fait bien
entendu à partir du calcul de la complexité amortie de l’opération part, qui
constitue notre point de départ.
Après avoir introduit les notations et rappelé les définitions, une première
étape consiste à mettre sous forme récurrente la fonction M(part(v, a)) qui
donne la complexité amortie de l’opération part(v, a) (cf. section 4.3). La se-
conde étape calcule un majorant de M(part(v, a)). Nous pourrons alors nous
252 Structures de données et méthodes formelles

préoccuper de la complexité amortie de l’opération eAjout_ad. Au préalable,


démontrons le lemme suivant qui est appliqué dans la seconde étape.

Lemme 3. Si x, y et z sont des réels positifs tels que y + z ≤ x alors

1 + log y + log z < 2 · log x (6.8.3)

Nous pouvons supposer que y ≤ z, le cas dual se traitant de manière analogue.

y+z ≤x
⇒ Hypothèse, minoration de z
y+y ≤x
⇔ Propriétés du logarithme
1 + log y ≤ log x (6.8.4)

Par ailleurs :

y+z ≤x
⇒ Calcul sur R+ , y > 0
z<x
⇔ Propriétés du logarithme
log z < log x (6.8.5)

En ajoutant les deux inégalités 6.8.4 et 6.8.5 nous obtenons la propriété 6.8.3,
ce qui achève la démonstration.

Notation. Si f est un abr, nous appelons f la fonction f ∈ ead → N1 qui


délivre le poids de f plus 1 : f = w(f ) + 1.
Une conséquence de cette définition est que la fonction  peut aussi être
définie de la manière suivante :


∈ ead → N1
a
 =1

(6.8.6)

g, r, d = g + d

Selon la définition 4.3.2, page 125, le coût amorti M(part(v, a)) de l’opération
de partitionnement de l’arbre a par rapport à la valeur v est donné par :

M(part(v, a)) = T (part(v, a)) + Φ(part(v, a)) − Φ(a) (6.8.7)

où T (part(v, a)) est le coût « réel » d’une opération de partitionnement. Φ la


fonction de potentiel à valeur dans R+ est telle que Φ(part(v, a)) est le potentiel
de la structure de données résultant du partitionnement et Φ(a) est le potentiel
de l’arbre a. Nous définissons la fonction T comme la fonction qui délivre le
nombre d’appels à la fonction part. La représentation calculée ci-dessus de la
fonction part permet de fournir une définition formelle de T (part(v, a)) par
l’équation récurrente suivante :
6. Ensembles de clés scalaires 253



⎪ T (part(v, )) = 1



⎪ T (part(v, , n, r)) = 1 Si v < n


⎨T (part(v, l, n, )) = 1  Si v > n
T (part(v, g)) Cas zig-zig

⎪ T (part(v, g, m, d, n, r)) = 1 +


⎪  T (part(v, d)) Cas zig-zag

⎪ T (part(v, d)) Cas zig-zig

⎩T (part(v, l, n, g, m, d)) = 1 +
T (part(v, g)) Cas zig-zag

Quant à la fonction de potentiel Φ(a), nous choisissons :



⎨ Φ() = 0
Φ(g, r, d) = Φ(g) + ϕ(g, r, d) + Φ(d)

Φ((g, d)) = Φ(g) + Φ(d)

où ϕ(a) est le logarithme de  a. Dans le cas d’un arbre unique a, Φ(a) se définit
donc comme la somme, pour tous les arbres et sous-arbres s de a, de la valeur
log(s). La fonction Φ(a) satisfait les conditions imposées par la définition de
l’analyse amortie (cf. formule 4.3.2, page 125).
Nous sommes prêt à rechercher une forme récurrente de M(part(v, a)). En
utilisant la représentation de l’opération part calculée ci-dessus, nous procédons
à une induction sur la structure de a. Pour le cas de base a =  nous avons :

M(part(v, a))
= Hypothèse
M(part(v, ))
= Définition de M
T (part(v, )) + Φ(part(v, )) − Φ()
= Définition de T et représentation de part
1 + Φ((, )) − 0
= Définition de Φ et calcul sur R+
1 + Φ(()) + Φ(())
= Définition de Φ et calcul sur R+
1

Le cas de base a = , n, r conduit au calcul ci-dessous pour v < n :

M(part(v, a))
= Hypothèse
M(part(v, , n, r))
= Définition de M
T (part(v, , n, r)) + Φ(part(v, , n, r)) − Φ(, n, r)
= Définition de T et représentation de part
1 + Φ((, , n, r)) − Φ(, n, r)
= Définition de Φ et calcul sur R+
1 + Φ() + Φ(, n, r) − Φ(, n, r)
= Définition de Φ et calcul sur R+
1

Le cas inductif zig-zig, pour lequel a = g, m, d, n, r et v < m < n se traite de
la manière suivante (les notations utilisées sont celles de la figure 6.17, page 246) :
254 Structures de données et méthodes formelles

M(part(v, a))
= Définition de M
T (part(v, a)) + Φ(part(v, a)) − Φ(a) (6.8.8)
= Définition de T et de part
1 + T (part(v, g)) + Φ((g  , a )) − Φ(a)
= Définition de Φ et calcul sur R+
1 + T (part(v, g)) + Φ(g  ) + Φ(a ) − Φ(a)

La définition de M(part(v, a)) nous permet d’exprimer T (part(v, g)) à partir de


M(part(v, g)) :

T (part(v, g)) = M(part(v, g)) − Φ(part(v, g)) + Φ(g)

dont la partie droite vient se substituer à T (part(v, g)) dans la formule précé-
dente :

1 + T (part(v, g)) + Φ(g  ) + Φ(a ) − Φ(a)


= Substitution suite à la remarque ci-dessus
1 + M(part(v, g)) − Φ(part(v, g)) + Φ(g) + Φ(g  ) + Φ(a ) − Φ(a)
= Hypothèses de la figure 6.17
1 + M(part(v, g)) − Φ((g  , g  )) + Φ(g) + Φ(g  ) + Φ(a ) − Φ(a)
= Définition de Φ et calcul sur R+
1 + M(part(v, g)) − Φ(g  ) − Φ(g  ) + Φ(g) + Φ(g  ) + Φ(a ) − Φ(a)
= Calcul sur R+
1 + M(part(v, g)) − Φ(g  ) + Φ(g) + Φ(a ) − Φ(a)
= ⎛ Définition de Φ et calcul⎞ sur R+
(−Φ(g  ) + Φ(g))
1 + M(part(v, g)) + ⎝ +(Φ(g  ) + ϕ(a ) + Φ(d) + ϕ(l ) + Φ(r)) ⎠
−(Φ(g) + ϕ(l) + Φ(d) + ϕ(a) + Φ(r))
= Calcul sur R+
1 + M(part(v, g)) + ϕ(a ) + ϕ(l ) − ϕ(a) − ϕ(l)

Le cas inductif zig-zag, pour lequel a = g, m, d, n, r et m < v < n se traite
de manière similaire en utilisant les notations de la figure 6.18, page 247, et en
repartant de la formule 6.8.8. Nous obtenons au total :


⎪ M(part(v, )) = 1

M(part(v, , n, r)) 
=1

⎪ M(part(v, g)) + ϕ(a ) + ϕ(l ) − ϕ(a) − ϕ(l) Zig-zig
⎩M(part(v, a)) = 1 +
M(part(v, d)) + ϕ(a ) + ϕ(a ) − ϕ(a) − ϕ(l) Zig-zag

Pour le cas du partitionnement à droite (zag-zag et zag-zig), l’expression de


M(part(v, a)) est analogue, elle n’est pas représentée.

Passons à la seconde étape pour tenter de démontrer que

M(part(v, a)) < 2 + 2 · ϕ(a)

à partir de la forme récurrente de M(part(v, a)). Pour le cas de base a =  nous


avons :
6. Ensembles de clés scalaires 255

M(part(v, a)) < 2 + 2 · ϕ(a)


⇔ Hypothèse
M(part(v, )) < 2 + 2 · ϕ()
⇔ Propriétés de M(part(v, a)) et de ϕ
1<2+2·0
⇔ Arithmétique


Pour le second cas de base, a = , n, r, le calcul se développe comme suit :

M(part(v, a)) < 2 + 2 · ϕ(a)


⇔ Hypothèse
M(part(v, , n, r)) < 2 + 2 · ϕ(, n, r)
⇔ Propriétés de M(part(v, a)) et définition de ϕ

1 < 2 + 2 · log(, n, r)
⇔ Définition de 
 + r)
1 < 2 + 2 · log(
⇔ Définition de 
1 < 2 + 2 · log(1 + r)
⇔ Arithmétique r ≥ 1


Pour la partie inductive zig-zig, en utilisant les notations de la figure 6.17,


page 246, et en exploitant l’hypothèse d’induction M(part(v, g)) < 2 + 2 · ϕ( g)
nous avons :

M(part(v, a))
= Hypothèse, cas zig-zig
1 + M(part(v, g)) + ϕ(a ) + ϕ(l ) − ϕ(a) − ϕ(l)
< Hypothèse d’induction
1 + 2 + 2 · ϕ(g) + ϕ(a ) + ϕ(l ) − ϕ(a) − ϕ(l)
< ϕ(g) < ϕ(l) et ϕ(a ) < ϕ(a)

1 + 2 + ϕ(g) + ϕ(l) + ϕ(a) + ϕ(l ) − ϕ(a) − ϕ(l)
= Calcul sur R+
1 + 2 + ϕ(g) + ϕ(l )
< Définition de ϕ
1 + 2 + log g + log l
< g + l ≤ 
Propriété 6.8.3, page 252 ( a)
2 + 2 · log 
a
= Définition de ϕ
2 + 2 · ϕ(a)

Le cas inductif zig-zag se traite de manière similaire en utilisant les notations


de la figure 6.18, page 247 :

M(part(v, a))
= Hypothèse, cas zig-zag
1 + M(part(v, d)) + ϕ(a ) + ϕ(a ) − ϕ(a) − ϕ(l)
256 Structures de données et méthodes formelles

< Hypothèse d’induction


1 + 2 + 2 · ϕ(d) + ϕ(a ) + ϕ(a ) − ϕ(a) − ϕ(l)
< ϕ(d) < ϕ(l) et ϕ(d) < ϕ(a)
1 + 2 + ϕ(l) + ϕ(a) + ϕ(a ) + ϕ(a ) − ϕ(a) − ϕ(l)
= Calcul sur R+
1 + 2 + ϕ(a ) + ϕ(a )
= Définition de ϕ

1 + 2 + log a + log a 

< Propriété 6.8.5 (a + a ≤ 


a)
2 + 2 · log 
a
= Définition de ϕ
2 + 2 · ϕ(a)

Les cas zag-zag et zag-zig sont analogues aux cas zig-zig et zig-zag. Nous
avons donc démontré que

M(part(v, a)) < 2 + 2 · ϕ(a)

Il est alors facile, en exploitant la représentation de l’opération


eAjout_ad(v, a) de la page 247, d’obtenir un majorant de son coût amorti.

M(eAjout_ad(v, a))
= Partitionnement + enracinement
M(part(v, a)) + 1 + Φ(l, v, r) − Φ(l) − Φ(r)

où le couple (l, r) est le résultat du partitionnement. Comment cette dernière


formule est-elle obtenue ? L’opération eAjout_ad se décompose en deux étapes,
la première est le partitionnement, la seconde l’enracinement. La complexité
amortie est donc la somme de la complexité amortie de chacune des deux étapes.
Celle du partitionnement M(part(v, a)) a été évaluée. Celle de l’enracinement
est donnée par la formule générale (formule 4.3.2, page 125). Elle est égale au
coût réel de l’enracinement (qui est constant, nous considérons qu’il vaut 1) plus
le potentiel de la structure finale, Φ(l, v, r), moins le potentiel de la structure
sur laquelle s’applique l’enracinement (qui est aussi la structure résultant du
partitionnement), soit Φ(l) + Φ(r).

M(part(v, a)) + 1 + Φ(l, v, r) − Φ(l) − Φ(r)


= Définition de Φ
M(part(v, a)) + 1 + Φ(l) + ϕ(l, v, r) + Φ(r) − Φ(l) − Φ(r)
= Calcul sur R+
M(part(v, a)) + 1 + ϕ(l, v, r)
< Complexité amortie de part
2 + 2 · ϕ(a) + 1 + ϕ(l, v, r)
= 
Définition de ϕ, l, v, r = 
a + 1, et arithmétique
3 + 2 · log 
a + log(
a + 1)
< Calcul sur R+ et propriété du logarithme
3 + 3 · log(
a + 1)
6. Ensembles de clés scalaires 257

Ce dernier résultat nous permet de conclure que, si n représente le poids de


l’arbre a :

M(eAjout_ad(v, a)) ∈ O(log n)

6.8.6 Conclusion et remarques bibliographiques


Une première version des arbres déployés, présentée dans l’ouvrage [112] de
R. Tarjan, est améliorée sur le plan de l’analyse de la complexité et publiée
collectivement avec D.D. Sleator, l’un de ses anciens étudiants de 3e cycle à
Standford University (cf. [109]).
Les versions algorithmiques proposées dans [109] utilisent un lien explicite
vers le père, ce qui fait perdre à la structure de données son statut de struc-
ture « fonctionnelle ». Dans [93] C. Okasaki utilise une version fonctionnelle des
arbres déployés pour mettre en œuvre une file de priorité (cf. section 9). Cette
solution est séduisante par sa simplicité et son élégance, principalement impu-
tables au caractère inductif du support. C’est la raison pour laquelle nous nous
sommes inspirés de cette démarche dans le cadre d’une mise en œuvre d’un en-
semble de clés. Dans [101] B. Schoenmakers présente une version apparentée
ayant comme but d’exhiber le meilleur majorant pour la complexité amortie.
Les développements réalisés vont au-delà des objectifs de cet ouvrage.
L’approche calculatoire dont nous nous faisons l’avocat est en partie remise
en cause par le modèle de construction sous-jacent aux arbres déployés. En effet,
calculer les opérations d’une structure de données se fait en recherchant une
solution qui garantit le respect des contraintes imposées par le support. Or dans
le cas des arbres déployés (et plus généralement dans le cas des structures au-
toadaptatives), ce n’est plus cet objectif qui dirige le développement mais plutôt
la recherche d’une solution peu coûteuse en terme d’analyse amortie.
Il est clair que le concepteur de structures de données autoadaptatives adopte
une démarche empirique principalement fondée sur la découverte intuitive de la
structure de données, des heuristiques d’équilibrage qui l’accompagnent et d’une
fonction de potentiel convenable. À terme cette approche n’est pas raisonnable
car peu constructive et laissant trop de place à l’empirisme. On note actuelle-
ment une tendance à dériver la fonction de potentiel plutôt que de la deviner
(cf. [69, 101, 62]). Cette orientation va selon nous dans le bon sens, mais elle est
scientifiquement exigeante et nous sommes encore loin d’une véritable méthodo-
logie.

Exercices

Exercice 6.8.1 Calculer une représentation des opérations eApp_ad, eSupp_ad. Effectuer
une analyse amortie de chaque opération.

Exercice 6.8.2 Dans l’article [109], les auteurs proposent une version semi-adaptative du
déploiement. Celui-ci s’effectue comme décrit ci-dessus (cf. section 6.8.2) sauf dans le cas zig-
zag (et dans le cas symétrique zag-zig) où le déploiement s’effectue comme le montre le schéma
ci-dessous :
258 Structures de données et méthodes formelles

z y

y D x z

x C A B C D

A B

et se poursuit sur y.
1. Montrer que la profondeur des nœuds rencontrés sur le chemin de la recherche diminue
au moins de moitié à chaque déploiement.
2. Mettre en œuvre cette solution en calculant l’opération eAjout_ad.
3. Quel est le coût amorti d’un déploiement ?

Exercice 6.8.3 Dans les articles [8, 16], les auteurs proposent deux heuristiques pour
construire des arbres binaires de recherche autoadaptatifs. La première consiste à effectuer
une seule rotation simple sur le père d’un nœud auquel on a accédé (pour le consulter, l’insérer
ou supprimer l’un des fils). La seconde consiste à amener le nœud en question à la racine par
des rotations simples réalisées sur le chemin de la recherche.
1. Comparer l’anatomie de l’insertion des valeurs successives 40, 23, 56, 36, 10, 7, 34, 62,
16, 24, 55, 19, 29 et 32 pour les deux heuristiques.
2. Effectuer une analyse amortie la plus ajustée possible. Conclusion ?

6.9 Méthodes aléatoires : les treaps randomisés


Cette section est consacrée à une méthode de représentation d’ensembles de
scalaires qui s’avère simple, bien que relativement récente puisqu’elle a vu le
jour au début de la quatrième période de l’histoire des structures de données,
celle des structures aléatoires. Nous débutons par une définition de la notion
de treap, indépendamment de l’aspect aléatoire qui est traité ensuite. Le terme
treap est un mot valise issu de tree (arbre) et heap (tas). Un treap randomisé
est fondamentalement un abr. Cependant, comme les abr ont une « tendance
naturelle » à perdre leur (éventuel) caractère aléatoire au fil des mises à jour,
l’idée qui prévaut ici est d’ajouter artificiellement une composante aléatoire à
l’abr afin de garantir, avec une très forte probabilité, de bonnes performances
pour toutes les opérations du type abstrait.
Plus précisément le principe des treaps est le suivant. On définit une fonction
injective prio (pour priorité) sur le domaine des clés et à valeur dans N. Un
treap est alors un abr sur la clé et un minimier sur la valeur de la fonction prio.
Ainsi, par exemple, avec les 16 clés du tableau 6.1, page ci-contre, et les priorités
associées nous obtenons le treap de la figure 6.21, page ci-contre.

6.9.1 Définition du support concret


Le support des treaps, eT rp, possède les mêmes caractéristiques que le sup-
port des abr, auxquelles s’ajoute le fait que l’arbre est un minimier sur la fonction
prio. Cette dernière fonction n’est pas précisée pour l’instant, elle s’ébauche par :

function prio(v) ∈ N  N =
 ...

et soit la fonction P rio(a), telle que :


6. Ensembles de clés scalaires 259

Tableau 6.1 – Clés avec leurs priorités.


Le tableau représente 16 clés et leurs priorités. Celles-ci sont toutes
différentes.

num. clé prio. num. clé. prio.


1 4 18 9 18 13
2 7 15 10 20 20
3 9 29 11 22 7
4 10 21 12 24 24
5 12 10 13 27 12
6 13 28 14 30 22
7 14 17 15 34 8
8 16 27 16 39 19

227
1210 348
715 1813 2712 3919
418 1021 1417 2020 2424 3022
929 1328 1627

Figure 6.21 – Treap de 16 nœuds.


Ce treap correspond aux clés du tableau 6.1. Les priorités sont placées
en indice des clés. L’arbre est un abr selon la clé et un minimier selon
la priorité.

function P rio(a) ∈ eT rp → N =
 ...

qui délivre la priorité de la racine de a (ou ∞ si l’arbre est vide).

1)  ∈ eT rp
2) n ∈ N ∧ g ∈ eT rp ∧ d ∈ eT rp ∧
max(A(g)) < n ∧ min(A(d)) > n ∧ prio(n) < min({P rio(g), P rio(d)})

g, n, d ∈ eT rp

Dans cette définition, A(a) représente la fonction qui délivre l’ensemble des clés
de l’arbre a. C’est aussi la fonction d’abstraction du support eT rp.

6.9.2 Définition de la fonction d’abstraction


La fonction d’abstraction A est identique à la fonction d’abstraction des abr :
260 Structures de données et méthodes formelles

function A(a) ∈ eT rp  ensAbst(N) =



if a =  →

| a = g, n, d →
A(g) ∪ {n} ∪ A(d)

6.9.3 Spécification des opérations concrètes


La figure 6.22 ci-dessous synthétise la spécification concrète du type etrp.
Cette spécification est similaire à celle des abr. Elle inclut en particulier la spé-
cification concrète des deux opérations de mise à jour.

concreteType etrp =  (eT rp, (eV ide_tp, eAjout_tp, eSupp_tp),


(eApp_tp, eEstV ide_tp))
uses bool, N
refines ensabst(N)
support
1)  ∈ eT rp
2) n ∈ N ∧ g ∈ eT rp ∧ d ∈ eT rp ∧ max(A(g)) < n ∧ min(A(d)) > n
prio(n) < min({P rio(g), P rio(d)})

g, n, d ∈ eT rp
abstractionFunction
function A(a) ∈ eT rp  ensAbst(N) =  ...
operationSpecifications
..
.
function eAjout_tp(v, a) ∈ N × eT rp → eT rp =

b : (b ∈ eT rp ∧ A(b) = eAjout(v, A(a)))
;
function eSupp_tp(v, a) ∈ N × eT rp → eT rp =

b : (b ∈ eT rp ∧ A(b) = eSupp(v, A(a)))
..
.
end

Figure 6.22 – Spécification du type concret etrp.


Il s’agit d’un spécification concrète du type ensabst de la figure 6.1,
page 149. Le support concret est un treap de naturels. La fonction d’abs-
traction est détaillée ci-dessus.

6.9.4 Calcul de la représentation des opérations concrètes


Nous ne développons que les deux opérations de mise à jour eAjout_tp et
eSupp_tp. Les autres opérations sont identiques à celles obtenues pour les abr.
Parmi tous les abr possibles pour un ensemble donné de clés, il faut retenir le
seul qui soit aussi un minimier sur la priorité de chaque clé.
6. Ensembles de clés scalaires 261

Calcul d’une représentation de l’opération eAjout_tp


Partant de la formule A(eAjout_tp(v, a)), nous obtenons pour le cas de base
où a =  la même équation gardée que dans le cas des abr :

a =  →
eAjout_tp(v, a) = , v, 

Pour le cas inductif (a = g, n, d), si v = n (cas d’une fausse insertion), le


résultat obtenu est trivial :

a = g, n, d →
v=n →
eAjout_tp(v, a) = a

Si v < n, le début du calcul est identique au calcul réalisé pour les abr jusqu’à
la formule :

A(eAjout_tp(v, g)) ∪ {n} ∪ A(d) (6.9.1)

Cependant, compte tenu de la nature du support qui impose à l’arbre d’être un


minimier sur la priorité, il est impossible d’appliquer systématiquement la défi-
nition de la fonction d’abstraction à cette expression. Il nous faut procéder à une
analyse par cas selon la situation relative des valeurs P rio(eAjout_tp(v, g)) et
prio(n). Si P rio(eAjout_tp(v, g)) > prio(n), la définition de la fonction d’abs-
traction s’applique et nous avons :

A(eAjout_tp(v, g) ∪ {n} ∪ A(d))


= Définition de A
A(eAjout_tp(v, g), n, d)

Nous avons alors calculé l’équation gardée suivante :

a = g, n, d →
v<n →
let l, u, r := eAjout_tp(v, g) in
prio(u) > prio(n) →
eAjout_tp(v, a) = l, u, r, n, d
end

Par contre, si P rio(eAjout_tp(v, g)) < prio(n) 18 , nous devons transformer l’ex-
pression de façon à être en mesure d’appliquer la fonction d’abstraction. Pour
cela, posons l, u, r = eAjout_tp(v, g). En repartant de la formule 6.9.1 nous
avons :

A(eAjout_tp(v, g)) ∪ {n} ∪ A(d)


= Hypothèse
A(l, u, r) ∪ {n} ∪ A(d)
18. Le cas P rio(eAjout_tp(v, g)) = prio(n) est impossible compte tenu du caractère injectif
de la fonction prio.
262 Structures de données et méthodes formelles

= Définition de A
(A(l) ∪ {u} ∪ A(r)) ∪ {n} ∪ A(d)
= Propriété A.8
A(l) ∪ {u} ∪ (A(r) ∪ {n} ∪ A(d))
= Définition de A
A(l, u, r, n, d)

Ce faisant, nous avons réalisé une rotation droite 19 . D’où l’équation gardée :

a = g, n, d →
v<n →
let l, u, r := eAjout_tp(v, g) in
prio(u) < prio(n) →
eAjout_tp(v, a) = l, u, r, n, d
end

Le cas v > n se traite de manière identique. Au total, nous avons calculé la


représentation suivante de l’opération eAjout_tp :

function eAjout_tp(v, a) ∈ N × eT rp → eT rp = 
if a =  → /*insertion aux feuilles*/
, v, 
| a = g, n, d →
if v = n → /*fausse insertion*/
a
|v<n → /*insertion à gauche*/
let l, u, r := eAjout_tp(v, g) in
if prio(u) < prio(n) → /*rotation droite*/
l, u, r, n, d
| prio(u) > prio(n) →
l, u, r, n, d

end
|v>n → /*insertion à droite*/
..
.

Cette fonction s’interprète de la manière suivante : pour insérer une clé v dans
un treap, nous commençons par effectuer une insertion aux feuilles. En général
l’arbre n’est alors pas encore un treap. Il faut remonter la valeur le long du
chemin de la racine jusqu’à obtention d’un treap. Cette remontée se fonde sur
la position relative des priorités de la clé et du nœud père pour éventuellement
réaliser une rotation. C’est ce que montre la figure 6.23, page 267, lue de manière
ascendante : la valeur 12 étant supposée insérée aux feuilles (partie (6)) sa priorité
19. Nous savons, depuis la section 6.3.4, que les rotations préservent à la fois les valeurs dans
leur ensemble et le caractère abr de l’arbre.
6. Ensembles de clés scalaires 263

est comparée à celle de son père 9. Il apparaît que l’arbre n’est pas un treap,
nous réalisons donc une rotation gauche et obtenons l’arbre (5). Le processus est
réitéré jusqu’à obtention d’un treap (partie (1)).
Nous reviendrons sur la question de la complexité mais pour l’instant il est
facile de se convaincre que si l’arbre était aléatoire le coût de l’insertion serait
celui de l’insertion dans un abr auquel s’ajouterait le coût de la remontée partielle
de la clé. La complexité serait en O(log n) pour un arbre de poids n.

Calcul d’une représentation de l’opération eSupp_tp


À la section 6.3.4, nous avons affirmé que la suppression dans les abr détruit
l’éventuel caractère aléatoire de l’abr considéré. Si, comme c’est le cas ici, notre
objectif est de préserver cette propriété, nous devons être vigilants dans le calcul
de cette opération. Ainsi que nous le verrons, l’existence de la priorité nous
permet d’arriver à notre fin. Le principe que nous appliquons est le suivant : soit
a un treap, soit a le treap résultat de l’insertion de v dans a. Après suppression
de v dans a , nous devons retrouver l’arbre initial a intact. Le cas de base où
a =  (fausse suppression) est trivial. Il conduit à l’équation gardée :

a =  →
eSupp_tp(v, a) = a

Pour l’étape inductive, le début du calcul est similaire au cas des abr. Posons
a = g, n, d :

A(eSupp_tp(v, a))
= Propriété caractéristique de eSupp_tp
A(g, n, d) − {v} (6.9.2)
= Définition de A
(A(g) ∪ {n} ∪ A(d)) − {v} (6.9.3)

Procédons à une analyse par cas en distinguant les deux cas g =  ∧ d =  et


g =  ∨ d = . Débutons par le premier cas :

(A(g) ∪ {n} ∪ A(d)) − {v}


= Hypothèse et définition de A
(∅ ∪ {n} ∪ ∅) − {v}
= Propriété A.9
{n} − {v} (6.9.4)

Deux cas sont à considérer : v = n (vraie suppression) et v = n (fausse suppres-


sion). Débutons par le cas v = n :

{n} − {v}
= Hypothèse et propriété A.18

= Définition de A
A()

D’où la seconde équation gardée :


264 Structures de données et méthodes formelles

a = g, n, d →
g =  ∧ d =  →
v=n →
eSupp_tp(v, a) = 

Quant au cas v = n, il se développe à partir de la formule 6.9.4 :

{n} − {v}
= Hypothèse et propriété A.16
n
= Propriété A.9
(∅ ∪ {n} ∪ ∅) − {v}
= Définition de A
A(, n, )

D’où l’équation gardée suivante :

a = g, n, d →
g =  ∧ d =  →
v = n →
eSupp_tp(v, a) = , n, 

Le cas g =  ∨ d =  se développe en effectuant une analyse par cas incidente


sur la position relative de v et de n. Débutons par le cas v = n. Ce cas exige de
distinguer la situation où v < n de celle où v > n. Le développement est alors
similaire à celui réalisé pour les abr. Nous obtenons (pour v < n par exemple)
l’équation gardée suivante :

a = g, n, d →
g =  ∨ d =  →
v = n →
v<n →
eSupp_tp(v, a) = eSupp_tp(v, g), n, d

Considérons à présent le cas v = n. Trois sous-cas sont à étudier : g = , d = ,


et g =  ∧ d = . Pour le cas g = , nous pouvons poser d = ld , ud , rd 
puisque d n’est pas vide. En repartant de la formule 6.9.2 nous avons alors :

A(g, n, d) − {v}


= Hypothèse (v = n)
A(, v, ld , ud , rd ) − {v}
= Rotation (non détaillée)
A(, v, ld , ud , rd ) − {v}
= Définition de A et v ∈ / ({ud } ∪ A(rd ))
(A(, v, ld ) − {v}) ∪ {ud } ∪ A(rd )
= Propriété caractéristique de eSupp_tp
A(eSupp_tp(v, , v, ld )) ∪ {ud } ∪ A(rd )
= Définition de A
A(eSupp_tp(v, , v, ld ), ud , rd )
6. Ensembles de clés scalaires 265

D’où l’équation gardée :

a = g, n, d →
g =  ∨ d =  →
v=n →
g =  →
let ld , ud , rd  := d in
eSupp_tp(v, a) = eSupp_tp(v, , v, ld ), ud , rd 
end

Le cas d =  se traite de manière analogue. Reste à considérer le cas g =


 ∧ d = . Nous pouvons poser g = lg , ug , rg  et d = ld , ud , rd . En repartant
de la formule 6.9.2 nous avons :

A(g, n, d) − {v}


= Hypothèse (v = n)
A(lg , ug , rg , v, ld , ud , rd ) − {v} (6.9.5)

De façon à préserver le caractère de minimier à l’arbre (exception faite du nœud


v qui est destiné à disparaître) nous devons distinguer deux cas : prio(ug ) >
prio(ud ) et prio(ug ) < prio(ud ). Dans le premier cas, nous effectuons une rota-
tion gauche à la racine :

A(lg , ug , rg , v, ld , ud , rd ) − {v}


= Hypothèse
A(g, v, ld , ud , rd ) − {v}
= Rotation
A(g, v, ld , ud , rd ) − {v}
= Définition de A
(A(g, v, ld ) − {v}) ∪ {ud } ∪ A(rd )
= Propriété caractéristique de eSupp_tp
A(eSupp_tp(v, g, v, ld ) ∪ {ud } ∪ A(rd )
= Définition de A
A(eSupp_tp(v, g, v, ld ), ud , rd )

D’où l’équation gardée :

a = g, n, d →
g =  ∧ d =  →
v=n →
let lg , ug , rg , ld , ud , rd  := g, d in
prio(ug ) > prio(ud ) →
eSupp_tp(v, a) = eSupp_tp(v, g, v, ld ), ud , rd 
end

Le cas prio(ug ) < prio(ud ) se traite de manière similaire. Au total, nous obtenons
la version suivante de l’opération eSupp_tp :
266 Structures de données et méthodes formelles

function eSupp_tp(v, a) ∈ N × eAbr → eAbr = 


if a =  → /*fausse suppression*/
a
| a = g, n, d in
if g =  ∧ d =  →
if v = n → /*vraie suppression*/

| v = n → /*fausse suppression*/
, n, 

| g =  ∨ d =  →
if v = n →
if v < n → /*suppression à gauche*/
eSupp_tp(v, g), n, d
|v>n → /*suppression à gauche*/
g, n, eSupp_tp(v, d)

|v=n →
if g =  →
let ld , ud , rd  := d in
eSupp_tp(v, , v, ld ), ud , rd 
end
| d =  →
let lg , ug , rg  := g in
lg , ug , eSupp_tp(v, rg , v, )
end
| g =  ∧ d =  →
let lg , ug , rg , ld , ud , rd  := g, d in
if prio(ug ) > prio(ud ) →
eSupp_tp(v, g, v, ld ), ud , rd 
| prio(ug ) < prio(ud ) →
lg , ug , eSupp_tp(v, rg , v, d)

end



Cette fonction commence par localiser la clé à supprimer en partant de la racine.


Une fois localisée, cette valeur est amenée en position de feuille par rotations
soit gauches soit droites de façon à préserver le caractère minimier de l’arbre.
La figure 6.23, page ci-contre, lue de haut en bas dissèque la suppression de la
valeur 12 suite à la localisation du nœud dans l’arbre. Les rotations nécessaires
sont grisées. Il s’agit donc du « détricotage » de ce qui a été réalisé lors de
l’insertion de la valeur 12. Cette technique présente l’avantage de permettre de
ne raisonner que sur l’une des deux opérations pour, par exemple, déterminer
6. Ensembles de clés scalaires 267

leurs complexités. Ainsi, si sous certaines conditions l’insertion est en O(log n),
la suppression l’est également.

(1) 227
1210 348
715 1813 2712 3919
227 (2)
418 1021 1417 2020 2424 3022
1813 348
929 1328 1627
1210 2020 2712 3919
715 1417 2424 3022
418 1021 1328 1627
(3) 227 929
1813 348
227 (4)
1417 2020 2712 3919
1813 348
1210 1627 2424 3022
1417 2020 2712 3919
715 1328
715 1627 2424 3022
418 1021
418 1210
929
1021 1328
(5) 227 929
1813 348
227 (6)
1417 2020 2712 3919
1813 348
715 1627 2424 3022
1417 2020 2712 3919
418 1021
715 1627 2424 3022
1210 1328
418 1021
929
929 1328
1210

Figure 6.23 – Ajout/suppression de la clé 12 dans un treap.


Le treap est celui correspondant aux clés du tableau 6.1. Les priorités
sont placées en indice des clés. Selon que l’on lise la figure en descen-
dant, du schéma (1) jusqu’au schéma (6) ou au contraire en remontant,
du schéma (6) jusqu’au schéma (1), on dissèque la suppression ou l’ad-
jonction de la valeur 12. Les branches grisées représentent les rotations.
268 Structures de données et méthodes formelles

6.9.5 Renforcer ou non le support ?


Nous disposons à présent d’un type concret pour les treaps. Ainsi que nous
l’avons vu aux sections 1.10 et 2.4 portant sur le renforcement du support, la
fonction prio peut soit être décomposée sur le support, soit être recalculée à
chaque fois qu’elle est appliquée. Il existe un cas cependant où il est impossible
de choisir cette seconde solution, c’est celui où la fonction est non déterministe :
nous devons calculer la fonction en un point une fois pour toutes et enregistrer
sa valeur dans la structure de données.
Dans ce cas, le renforcement du support nous conduit à ajouter un champ
associé à la clé, puis à aménager les opérations.

Version aménagée de l’opération eAjout_tp


Pour l’opération eAjout_tp nous obtenons :

function eAjout_tp(v, a) ∈ N × eT rp → eT rp =

if a =  →
, (v, prio(v)) , 
| a = g, (n, p) , d →
if v = n →
a
|v<n →
let l, (u, q) , r := eAjout_tp(v, g) in
if q < p →
l, (u, q) , r, (n, p) , d
| q>p →
l, (u, q) , r, (n, p) , d

end
|v>n →
..
.

La priorité d’une clé est introduite dès que la clé est ajoutée comme feuille dans
l’arbre. Les conditions qui s’exprimaient précédemment à partir de la fonction
prio s’expriment maintenant directement à partir du nouveau champ.
Nous sommes à présent prêt à introduire l’aspect aléatoire et à en étudier les
conséquences.

6.9.6 Treaps randomisés


Pour l’instant nous n’avons fait aucune hypothèse sur la nature de la fonc-
tion prio autre que son caractère injectif. Si prio délivre des valeurs qui sont
6. Ensembles de clés scalaires 269

des variables aléatoires continues indépendantes et identiquement distribuées, le


treap est qualifié de randomisé. Dans [105], puis dans [106], C. Aragon et R. Sei-
del montrent que la forme des arbres obtenus présente la même distribution de
probabilité que celle des abr aléatoires (cf. section 6.3.4). Le caractère aléatoire
de prio est en quelque sorte transmis par osmose à l’arbre tout entier.
Si, pour un arbre de poids n, la fonction prio résulte d’une permutation
aléatoire uniforme des valeurs de l’intervalle 1 .. n, l’arbre est appelé arbre car-
tésien (d’après [115]). Là aussi les arbres obtenus se comportent comme des abr
aléatoires. Concrètement, cela signifie que la hauteur d’un treap randomisé de
poids n comme celle d’un arbre cartésien de même poids se comporte asympto-
tiquement comme le logarithme de n, avec une très forte probabilité. Mettre en
œuvre des treaps randomisés exige cependant de surmonter quelques difficultés.
La première tient tout d’abord à l’aspect continu de la fonction prio, qui ne
peut être réalisé que de manière très approximative dans le contexte discret de
l’informatique 20 . Mais l’exigence de continuité n’est présente que pour garantir
l’absence absolue de doublon lors du tirage de la priorité. C. Aragon et R. Seidel
montrent que les résultats obtenus restent valables si la probabilité d’obtenir des
doublons est faible. Cela signifie qu’il est possible d’effectuer un tirage aléatoire
uniforme sur un intervalle d’entiers tel que 0 .. 232 − 1.
Par ailleurs, par son caractère non déterministe, le procédé utilisé pour créer
des treaps randomisés oblige à avoir recours à la technique du renforcement
du support appliquée ci-dessus (puisque deux tirages différents pour une même
clé vont fournir avec une quasi-certitude deux valeurs différentes). Ce qui du
point de vue de la complexité spatiale est une solution pénalisante. C. Aragon et
R. Seidel montrent qu’il est possible de réduire la taille de ce nouveau champ en
effectuant des calculs modulo. Une méthode astucieuse a été suggérée aux auteurs
par D. Sleator afin d’éliminer purement et simplement le champ supplémentaire.
Il s’agit d’utiliser comme fonction prio une fonction de hachage h ayant comme
argument la clé c. Cette valeur de hachage n’exige en effet pas d’être mémorisée
puisqu’elle peut être recalculée quand nécessaire. Certes, on a alors échangé de la
place contre du temps puisque h(c) sera calculée fréquemment, mais le problème
principal est alors de disposer d’une fonction h suffisamment bonne pour simuler
l’aléatoire 21 .
Pour ce qui concerne l’utilisation des arbres cartésiens, l’obstacle principal
est qu’a priori la taille de l’arbre à construire n’est en général pas connue. Si
cette contrainte est sans objet, il est facile, à partir d’un générateur de nombres
aléatoires quelconque 22 , de simuler une permutation aléatoire de l’intervalle 1..n.
Voir par exemple [116], section 2.3, page 11, pour un algorithme très performant.

20. Si l’on écarte l’exploitation de phénomènes physiques extérieurs comme la radioactivité.


21. La fonction suggérée à la section 6.4.6 ne présente pas de ce point de vue de garanties
suffisantes. Cf. [75] pour un développement sur ce sujet.
22. On dispose aujourd’hui de puces spécialisées dans la génération de nombres aléatoires
(cf. [100] par exemple) permettant de s’affranchir du caractère pseudo-aléatoire des générateurs
logiciels.
270 Structures de données et méthodes formelles

6.9.7 Conclusion et remarques bibliographiques


La notion d’algorithme aléatoire (ou probabiliste) est ancienne. Outre le do-
maine de la simulation où l’aléatoire est omniprésent, de nombreux algorithmes
très efficaces sont fondés sur une démarche probabiliste : c’est le cas de la mé-
thode de Monte-Carlo (pour le calcul d’intégrales en particulier), de certains
tests de primalité, de la recherche de patrons dans les chaînes, etc. (cf. [17] par
exemple pour plus de détails). L’un des spécialistes du domaine, M.O. Rabin, a
obtenu (avec D. Scott) la distinction Turing Award en 1976. Il n’est donc pas
surprenant que l’idée d’appliquer une démarche probabiliste aux structures de
données ait émergé.
L’avantage de la technique étudiée ci-dessus est qu’elle permet de s’affranchir
de l’hypothèse de l’équiprobabilité des n! tirages d’entrée tout en évitant les
arbres dégénérés avec une forte probabilité.
Les arbres cartésiens ont été découverts par J. Vuillemin en 1980 [115]. Une
variante, dans laquelle la priorité est remplacée par le poids de l’arbre – faisant
d’eux des minimiers (ou tournois) de position – a été étudiée dans le même
article (ainsi que dans [116]) afin de mettre en œuvre des tableaux fortement
flexibles (cf. section 10). Les treaps randomisés ont été découverts et étudiés
par C. Aragon et R. Seidel en 1989. D. Knuth a mené des expérimentations
afin de comparer le comportement des treaps randomisés et des arbres déployés
(cf. section 6.8). Il montre que les premiers ont de meilleures performances que
les seconds (cf. [75], page 478).
Les treaps autorisent une mise en œuvre efficace des opérations duales de
partitionnement et de fusion. Partitionner un treap par rapport à une valeur v
(et obtenir ainsi deux treaps) se fait en ajoutant v dans le treap avec la prio-
rité la plus faible possible (la priorité −1 permet d’éviter des conflits) puis en
supprimant la racine (c’est le couple (v, −1)) du treap résultat. Le caractère abr
du treap assure que l’on obtient bien une partition par rapport à v et l’aspect
minimier assure que toutes les valeurs sont prises en considération. L’opération
de fusion a comme précondition le fait que les deux treaps à fusionner pourraient
résulter du partitionnement d’un treap. Il suffit alors d’enraciner les deux treaps
sur une valeur quelconque v (avec une priorité quelconque) et d’effectuer une
suppression de cette valeur. Le processus de suppression descend la valeur sur
une feuille où elle est facile à éliminer. Le résultat est le couple de treaps attendu.
L’exercice 6.9.1 propose d’étudier ces deux opérations.
Il existe d’autres méthodes aléatoires pour la mise en œuvre d’ensembles de
scalaires, comme par exemple les listes de sauts (skip lists). Cependant, cette
structure de données n’est pas de nature fonctionnelle, elle n’est donc pas étu-
diée ici. L’une des structures de données (aléatoires) les plus intéressantes par
son rapport performances/concision de l’algorithme est celle dénommée abr ran-
domisé. Découverte en 1998, elle est due à C. Martinez et S. Roura (cf. [81]).
Nous avons vu à la section 6.3 qu’il est possible d’effectuer une insertion soit aux
feuilles soit à la racine. Ceci suggère qu’il est possible d’effectuer l’insertion sur
toute position du chemin de la recherche entre la racine et la feuille. Il suffit pour
cela de décider, au moment de l’insertion, si l’on effectue l’insertion à la racine
courante (selon la méthode du partitionnement, cf. section 6.3.4) ou si l’on pour-
suit la descente dans l’arbre. C’est le principe qui est exploité en décidant, pour
6. Ensembles de clés scalaires 271

un arbre de poids n, de placer la nouvelle valeur à la racine avec une probabilité


1
de n+1 . C. Martinez et S. Roura montrent que l’arbre construit en appliquant
cette stratégie est un abr aléatoire. La suppression débute par une phase de lo-
calisation de la valeur à supprimer puis par une fusion des deux sous-arbres g
et d restants. Cette fusion se fait en prenant comme racine du résultat la racine
w(g)
du sous-arbre gauche (resp. droite) avec une probabilité de w(g)+w(d) (resp. de
w(d)
w(g)+w(d) ).Là aussi, on montre qu’une telle suppression, réalisée à partir d’un
arbre aléatoire, produit un arbre aléatoire. Une phase ultime de décomposition
du poids sur la structure de données est nécessaire afin de préserver les bonnes
performances temporelles. L’exercice 6.9.2 propose une mise en œuvre de cette
solution.
Les treaps randomisés et les abr randomisés se comportent tous deux comme
des abr aléatoires. Ils ont donc les mêmes performances 23 . L’un des critères de
choix éventuel entre les deux solutions est que lors d’une insertion, les treaps
randomisés ne font appel qu’une seule fois au générateur de nombres aléatoires,
les abr randomisés réclament un appel à chaque position d’insertion. Tous deux
exigent de renforcer le support, le premier par la priorité (qui a comme unique
rôle de randomiser l’arbre) l’autre par le poids (ce renforcement peut être néces-
saire pour d’autres raisons). Concernant les treaps randomisés, la solution par
hachage évite ces écueils mais le choix de la fonction de hachage est critique.

Exercices

Exercice 6.9.1 Introduire les opérations de partitionnement et de fusion dans la spécification


du type abstrait ensabst. Ajouter à la spécification du type concret etrp la spécification
concrète de ces opérations et calculer une représentation sur la base des descriptions informelles
de la section 6.9.7. En déduire le coût de ces opérations.

Exercice 6.9.2 Calculer les opérations de mise à jour dans un abr randomisé. Effectuer la
décomposition du poids sur la structure de données. Mettre en œuvre en utilisant un générateur
de nombres aléatoires.

23. Bien que les arbres construits par l’un et l’autre à partir d’une même séquence de clés
soient en général différents.
Chapitre 7

Ensembles de clés structurées

Le chapitre 6 nous a permis d’étudier quelques représentations intéres-


santes pour les ensembles de clés scalaires. Il est cependant fréquent que la clé soit
composite. En nous limitant toujours au cas des ensembles définis en extension, il
s’agit ici d’étudier la composition de deux structures de données : d’une part les
ensembles de chaînes (ou de listes) et d’autre part les ensembles de couples. Les
solutions étudiées précédemment (abr, Avl, etc.) peuvent bien sûr s’appliquer.
Il est cependant possible de tirer parti du caractère composite de la clé soit pour
enrichir le jeu d’opérations soit pour améliorer l’efficacité temporelle ou spatiale
des algorithmes. Pour les ensembles de chaînes, nous nous proposons d’étudier
les mises en œuvre à base de tries 1 , dont l’une des retombées est le hachage
dynamique. Concernant les ensembles de couples, nous explorons la solution par
kd-arbres.

7.1 Ensembles de chaînes, représentation par tries


7.1.1 Introduction
Nous allons ici considérer le cas d’ensembles de chaînes (typiquement, et
pour fixer les idées, de caractères) de longueurs positives. Avec les méthodes
« scalaires » comme celles étudiées au chapitre 6, le préfixe commun (comme
chaîne dans chaînes et chaînette) est mémorisé – et souvent consulté – plusieurs
fois. L’idée qui prévaut ici est de factoriser tous les plus longs préfixes communs
(plpc), de façon à améliorer l’efficacité des opérations ensemblistes 2 .

1. Le terme « trie » vient de « retrieval » et se prononce « traïe ».


2. Les méthodes algorithmiques qui se fondent sur une prise en compte par morceaux des
valeurs à traiter sont qualifiées de méthodes radix. Ainsi la méthode de tri qui prend successi-
vement en compte les chiffres d’une clé pour ventiler chaque clé dans l’un des dix réceptacles
(à la manière des trieuses de cartes perforées mécaniques du début de la mécanographie) est
appelée tri radix. La technique des tries est également une méthode radix.
274 Structures de données et méthodes formelles

Exemple. Considérons l’ensemble de mots suivant :


– le – les – lapider – lapin
– mai – maia – main – mare
– mas – me – misera – moi
Nous constatons par exemple que main partage un plpc avec mas (c’est ma),
mais qu’il a aussi un plpc avec mais (c’est mai). Une façon de factoriser ces plpc
consiste à organiser les caractères des différentes chaînes sous forme de forêt-trie.
Une forêt-trie est un ensemble d’arbres-tries, un arbre-trie est une forme d’arbre
non ordonné (cf. section 3.3, page 85) particulier. C’est ce que montre la forêt
ci-dessous pour l’exemple introductif.

{ l , m }
e a a e i o
s p i r s s i
i a n e e

d n r
e a
r

Pourtant, si l’on ne prend pas de précautions particulières pour la représenta-


tion, il est impossible de remarquer que la chaîne mis (par exemple) n’appartient
pas à l’ensemble, alors que la chaîne mai (par exemple) lui appartient. La solu-
tion consiste à accompagner chaque nœud de la forêt-trie d’une information qui
mentionne ou non l’appartenance à l’ensemble du mot s’achevant en ce nœud.
C’est le sens qu’il faut attribuer aux nœuds encerclés dans le schéma ci-dessus.
Si la forêt-trie ne contient pas d’information inutile, c’est bien entendu le cas de
toutes les feuilles.

Remarque. Nous avons affirmé ci-dessus vouloir ne nous intéresser qu’aux


chaînes de longueur positive, excluant ainsi la chaîne vide [ ]. Qu’est-ce qui
justifie une telle restriction ? Si l’on considère une forêt-trie, les racines de tous
les arbres de la forêt constituent l’ensemble des premiers caractères de toutes les
chaînes représentées. Or la chaîne vide est, par définition, dépourvue de premier
caractère, elle ne peut donc être représentée dans la forêt-trie. Dans certaines
représentations, on contourne la difficulté en convenant que tous les arbres de
la forêt sont enracinés sur un nœud dépourvu d’étiquette. La chaîne vide peut
alors être représentée en encerclant cette racine. Cette solution entraîne une
perte d’homogénéité de la structure.

7.1.2 Définition des supports concrets


Notations, rappel. Les clés considérées ici sont des listes (de caractères).
Celles-ci sont introduites à la section 3.1 sous une double notation : la notation
7. Ensembles de clés structurées 275

récursive et la notation linéaire. Ainsi la liste [c | [ ]] peut également être notée


[c], la liste [c | [d | [ ]]] s’écrit également [c, d].
Le type concret que nous décrivons, ft, est un raffinement du type ensabst
paramétré par des chaînes de caractères non vides. Par souci de concision, ce
type est appelé chaine, son support est noté chaine dans la suite de cette
section. La définition formelle du support concret des forêts-tries f t se fait de
manière croisée avec la définition des arbres-tries at. Débutons par la définition
du support at.

c ∈ char ∧ b ∈ bool ∧ f ∈ f t ∧
(b = true ∨ f = ∅)

c, b, f  ∈ at

Un arbre-trie est donc un triplet dont le premier composant est un caractère c,


le second composant est un booléen qui mentionne si c est (cas true) ou non
(cas false) un caractère qui achève l’une des chaînes représentées. Le troisième
composant est la forêt-trie f associée à la racine. Le conjoint (b = true∨f = ∅)
permet de garantir que si un nœud est une feuille, le booléen b a la valeur true
(les feuilles terminent bien une chaîne).
Revenons au support des forêts-tries. Ainsi que nous l’avons dit ci-dessus,
un arbre-trie est un arbre non ordonné. Les fils d’un nœud constituent un en-
semble. Cette contrainte se transpose aux forêts-tries de la manière suivante : les
racines des différents arbres d’une forêt-trie constituent un ensemble. L’intégra-
tion de cette contrainte à la spécification du support se fait par l’intermédiaire
de l’opération ensRac, opération auxiliaire du type ft qui délivre l’ensemble des
racines d’une forêt. La définition du support f t du type ft s’effectue de manière
inductive :

1) ∅ ∈ ft
2) c, b, f  ∈ at ∧ g ∈ f t ∧ c ∈
/ ensRac(g) ⇒ {c, b, f } ∪ g ∈ f t

La première clause affirme que l’ensemble vide est une forêt-trie. La seconde
clause précise que si c, b, f  est un arbre-trie et g une forêt-trie, et que si c
n’appartient pas à l’ensemble des racines de g alors {c, b, f } ∪ g est une forêt-
trie. Il va de soi qu’un tel arbre est un arbre non ordonné.
La fonction d’abstraction se définit à partir de l’opération pref ixer(c, e).
Celle-ci délivre toutes les chaînes de l’ensemble e préfixées par le caractère e.
Ainsi par exemple si e = {[a], [d, d, j]}, pref ixer(c, e) délivre {[c, a], [c, d, d, j]}.
pref ixer(c, ∅) délivre ∅ pour tout caractère c. Conceptuellement, l’opération
pref ixer fait partie du type abstrait « ensemble de chaînes ». N’étant pas utilisée
en dehors du calcul, cette opération n’a pas à être implantée. Une définition
inductive possible de cette opération est la suivante :
276 Structures de données et méthodes formelles

function pref ixer(c, e) ∈ char × ensAbst(chaine) → ensAbst(chaine) =



if e = ∅ →

| e = ∅ →
any i where
i∈e
then
{[c | i]} ∪ pref ixer(c, e − {i})
end

Le tableau 7.1 représente quelques propriétés de cette opération. En particu-


lier la propriété 7.1.1 affirme que si une chaîne constituée de plusieurs caractères
appartient à l’ensemble pref ixer(c, e) alors la chaîne débute par le caractère c et
la chaîne qui suit c appartient à l’ensemble e. La démonstration de ces propriétés
est laissée en exercice.
Tableau 7.1 – Propriétés de l’opération pref ixer.
Ce tableau répertorie quelques-unes des propriétés de l’opération
pref ixer(c, e). Cette opération délivre l’ensemble constitué de toutes
les chaînes de e précédées du caractère c.

Propriété Condition Ident.


[a | q] ∈ pref ixer(c, e) a ∈ char ∧ c ∈ char ∧
⇔ e ⊆ ensAbst(chaine) ∧ (7.1.1)
a=c∧q ∈e q ∈ ensAbst(chaine)
pref ixer(c, e) ∪ {[c | q]} c ∈ char ∧
= e ⊆ ensAbst(chaine) ∧ (7.1.2)
pref ixer(c, e ∪ {q}) q ∈ ensAbst(chaine)
pref ixer(c, e) − {[c | q]} c ∈ char ∧
= e ⊆ ensAbst(chaine) ∧ (7.1.3)
pref ixer(c, e − {q}) q ∈ ensAbst(chaine)
c ∈ char ∧ d ∈ char ∧
pref ixer(d, e) − {[c | q]}
c = d ∧
= (7.1.4)
e ⊆ ensAbst(chaine) ∧
pref ixer(d, e)
q ∈ ensAbst(chaine)
c ∈ char ∧
[c] ∈
/ pref ixer(c, e) (7.1.5)
e ⊆ ensAbst(chaine)

7.1.3 Définition des fonctions d’abstraction


Il s’agit de définir les fonctions d’abstraction A et A (des types concrets
respectifs at et ft). La première, A , délivre l’ensemble des chaînes qui sont
représentées dans un arbre-trie. Elle se définit par :
7. Ensembles de clés structurées 277

function A (c, b, f )
⎛∈ at → ensAbst(chaine)
⎞ 
=
if b = true →
⎜ {[c]} ⎟
⎜ ⎟
pref ixer(c, A(f )) ∪ ⎜ | b = false → ⎟


⎝ ∅ ⎠

Nous notons que A se définit à la fois à partir de la fonction d’abstraction A


du type ft et de l’opération pref ixer.
La fonction d’abstraction A délivre l’ensemble des chaînes représentées par
une forêt-trie. Une représentation inductive possible est :

function A(f ) ∈ f t → ensAbst(chaine) =


if f = ∅ →

| f = ∅ →
any i where i ∈ f then A (i) ∪ A(f − {i}) end

7.1.4 Spécification des opérations concrètes


La description du type at est proposée à la figure 7.1. La figure 7.2, page
suivante, reprend toutes les informations concernant le type concret ft et spé-
cifie concrètement les deux opérations de mise à jour. Elle est complétée par la
figure 7.3, page 279.

concreteType at =  (at, (), ())


uses char, ft, chaine, bool
refines ensabst(chaine)
support
c ∈ char ∧ b ∈ bool ∧ f ∈ f t ∧
(b = true ∨ f = ∅)

c, b, f  ∈ at
abstractionFunction
function A (c, b, f ) ∈ at → ensAbst(chaine) =
 ...
end

Figure 7.1 – Spécification du type at.


Ce type concret raffine le type abstrait ensabst. Cependant il s’agit
d’un type auxiliaire utilisé par le type concret ft (cf. figure 7.2). En
conséquence, seuls le support et la fonction d’abstraction sont décrits.

7.1.5 Calcul de la représentation des opérations concrètes


Nous limitons notre étude au calcul de l’opération eAjout_f t et à la pré-
sentation de l’opération eSupp_f t. Le calcul des autres opérations est proposé
à l’exercice 7.1.2, page 292.
278 Structures de données et méthodes formelles

concreteType ft =  (f t, (eV ide_f t, eAjout_f t, eSupp_f t),


(eApp_f t, eEstV ide_f t))
uses bool, at, char, chaine
refines ensabst(chaine)
support
1) ∅ ∈ f t
2) c, b, f  ∈ at ∧ g ∈ f t ∧ c ∈/ ensRac(g) ⇒ {c, b, f } ∪ g ∈ f t
abstractionFunction
function A(f ) ∈ f t → ensAbst(chaine) =  ...
operationSpecifications
..
.
function eAjout_f t(l, f ) ∈ chaine × f t → f t =

g : (g ∈ f t ∧ A(g) = eAjout(l, A(f )))
;
function eSupp_f t(l, f ) ∈ chaine × f t → f t =

g : (g ∈ f t ∧ A(g) = eSupp(l, A(f )))
..
.
auxiliaryOperationRepresentations
function ensRac(f ) ∈ f t  F(char) =
 ···
end

Figure 7.2 – Spécification du type concret ft – partie 1.


Le type concret ft (forêt-trie) raffine le type abstrait ens-
abst(chaine). Il est défini de manière croisée avec le type at (arbre-
trie, cf. figure 7.1). Il permet de représenter des ensembles de chaînes
sans duplication des préfixes communs.

Tableau 7.2 – Propriétés de l’opération ensRac.


Ce tableau répertorie deux des propriétés de l’opération ensRac(f ).
Cette opération délivre l’ensemble constitué de toutes les racines des
arbres-tries présents dans f. La propriété 7.1.6 précise qu’il existe au
plus dans f un arbre-trie ayant c comme racine. La propriété 7.1.7 ex-
prime que si le caractère c n’est pas dans l’ensemble des racines de la
forêt-trie f , alors il n’y a pas dans A(f ) de chaîne débutant par c.

Propriété Condition Ident.


c∈
/ ensRac(f − {c, b, g}) f ∈ f t ∧ c, b, g ∈ at (7.1.6)
f ∈ f t ∧ c ∈ char ∧
c∈
/ ensRac(f ) ⇒ [c | q] ∈
/ A(f ) (7.1.7)
(q ∈ chaine ∨ q = [ ])
7. Ensembles de clés structurées 279

 ···
concreteType ft =
..
.
auxiliaryOperationRepresentations
function ensRac(f ) ∈ f t  F(char) =
if f = ∅ →

| f = ∅ →
any c, b, g where
c, b, g ∈ f
then
{c} ∪ ensRac(f − {c, b, g})
end

end

Figure 7.3 – Spécification du type concret ft – partie 2.


La rubrique auxiliaryOperationRepresentations fournit la repré-
sentation de l’opération auxiliaire ensRac, qui délivre l’ensemble des
racines de la forêt f .

Calcul d’une représentation de l’opération eAjout_f t


Compte tenu du typage du paramètre l de l’opération eAjout_f t(l, f ) (l =
[ ]), nous pouvons poser l = [c | q].

A(eAjout_f t(l, f ))
= Propriété caractéristique
{l} ∪ A(f )
= Hypothèse
{[c | q]} ∪ A(f ) (7.1.8)

Procédons à une induction sur f en débutant par le cas f = ∅.

{[c | q]} ∪ A(f )


= Hypothèse
{[c | q]} ∪ A(∅)
= Définition de A
{[c | q]} ∪ ∅
= Définition de pref ixer
{[c | q]} ∪ pref ixer(c, A(∅)) (7.1.9)

Procédons à une analyse par cas sur q, selon que q = [ ] ou non. Débutons par
q = [ ].

{[c | q]} ∪ pref ixer(c, A(∅))


= Hypothèse
280 Structures de données et méthodes formelles

{[c]} ∪ pref ixer(c, A(∅))


= Définition de A , cas b = true

A (c, true, ∅)
= A(∅) = ∅, propriété A.9
A (c, true, ∅) ∪ A(∅)
= Définition de A
A({c, true, ∅} ∪ ∅)
= Propriété A.9
A({c, true, ∅})

D’où, d’après la propriété de l’équation à membres identiques (page 67), l’équa-


tion gardée :

f =∅ →
let [c | q] := l in
q=[] →
eAjout_f t(l, f ) = {c, true, ∅}
end

Considérons à présent le cas q = [ ] en partant de la formule 7.1.9 ci-dessus.

{[c | q]} ∪ pref ixer(c, A(∅))


= Propriété 7.1.2, page 276
pref ixer(c, {q} ∪ A(∅))
= Propriété caractéristique de eAjout_f t
pref ixer(c, A(eAjout_f t(q, ∅)))
= Définition de A dans le cas b = false

A (c, false, eAjout_f t(q, ∅))
= Propriété A.9 et définition de A
A (c, false, eAjout_f t(q, ∅)) ∪ A(∅)
= Définition de A
A({c, false, eAjout_f t(q, ∅)} ∪ ∅)
= Propriété A.9
A({c, false, eAjout_f t(q, ∅)})

D’où, d’après la propriété de l’équation à membres identiques (page 67), la se-


conde équation gardée :

f =∅→
let [c | q] := l in
q = [ ] →
eAjout_f t(l, f ) = {c, false, eAjout_f t(q, ∅)}
end

Le cas f = ∅ étant complètement analysé, abordons le cas f = ∅. Soit i


et h tels que i ∈ f et h = f − {i}. Posons i = d, b, g. Nous repartons de la
formule 7.1.8.
7. Ensembles de clés structurées 281

{[c | q]} ∪ A(f )


= Hypothèse
{[c | q]} ∪ A({d, b, g} ∪ h)
= Définition de A
{[c | q]} ∪ A (d, b, g) ∪ A(h) (7.1.10)

Procédons à nouveau à une analyse par cas selon que c = d ou que c = d.


Débutons par c = d.

{[c | q]} ∪ A (d, b, g) ∪ A(h)


= Hypothèse
{[c | q]} ∪ A (c, b, g) ∪ A(h) (7.1.11)

Pour développer l’expression A (c, b, g) nous devons distinguer les deux cas
b = true et b = false, conformément à la définition de la fonction A . Débutons
par le cas b = true.

{[c | q]} ∪ A (c, b, g) ∪ A(h)


= Définition de A
{[c | q]} ∪ {[c]} ∪ pref ixer(c, A(g)) ∪ A(h) (7.1.12)

Nous devons effectuer une analyse par cas selon que q = [ ] ou que q = [ ].
Débutons par le cas q = [ ] :

{[c | q]} ∪ {[c]} ∪ pref ixer(c, A(g)) ∪ A(h)


= Hypothèse
{[c]} ∪ {[c]} ∪ pref ixer(c, A(g)) ∪ A(h)
= Propriété A.9
{[c]} ∪ pref ixer(c, A(g)) ∪ A(h)
= Définition de A

A (c, b, g) ∪ A(h)
= Définition de A
A({c, b, g} ∪ h)

D’où l’équation gardée suivante :

f = ∅ →
let [c | q] := l in
any i, h where
i ∈ f ∧ h = f − {i}
then
let d, b, g := i in
c=d →
b = true →
q=[] →
eAjout_f t(l, f ) = {c, b, g} ∪ h
end
end
end
282 Structures de données et méthodes formelles

Pour le cas q = [ ], nous repartons de la formule 7.1.12 :

{[c | q]} ∪ {[c]} ∪ pref ixer(c, A(g)) ∪ A(h)


= Propriété 7.1.2, page 276
{[c]} ∪ pref ixer(c, {q} ∪ A(g)) ∪ A(h)
= Propriété caractéristique de eAjout_f t
{[c]} ∪ pref ixer(c, A(eAjout_f t(q, g))) ∪ A(h)
= Définition de A , cas b = true

A (c, true, eAjout_f t(q, g)) ∪ A(h)
= Définition de A
A({c, true, eAjout_f t(q, g)} ∪ h)

D’où l’équation gardée :

f = ∅ →
let [c | q] := l in
any i, h where
i ∈ f ∧ h = f − {i}
then
let d, b, g := i in
c=d →
b = true →
q = [ ] →
eAjout_f t(l, f ) = {c, true, eAjout_f t(q, g)} ∪ h
end
end
end

Nous abordons à présent le cas b = false. Nous repartons de la formule 7.1.11 :

{[c | q]} ∪ A (c, b, g) ∪ A(h)


= Hypothèse
{[c | q]} ∪ A (c, false, g) ∪ A(h)
= Définition de A
{[c | q]} ∪ pref ixer(c, A(g)) ∪ A(h) (7.1.13)

Nous devons à nouveau réaliser une analyse par cas, en distinguant le cas q = [ ]
de sa négation. Débutons par q = [ ] :

{[c | q]} ∪ pref ixer(c, A(g)) ∪ A(h)


= Hypothèse
{[c]} ∪ pref ixer(c, A(g)) ∪ A(h)
= Définition de A

A (c, true, g) ∪ A(h)
= Définition de A
A({c, true, g} ∪ h)

D’où l’équation gardée :


7. Ensembles de clés structurées 283

f = ∅ →
let [c | q] := l in
any i, h where
i ∈ f ∧ h = f − {i}
then
let d, b, g := i in
c=d →
b = false →
q=[] →
eAjout_f t(l, f ) = {c, true, g} ∪ h
end
end
end

Le cas q = [ ] se traite comme suit, en repartant de la formule 7.1.13 :

{[c | q]} ∪ pref ixer(c, A(g)) ∪ A(h)


= Propriété 7.1.2, page 276
pref ixer(c, {q} ∪ A(g)) ∪ A(h)
= Propriété caractéristique de eAjout_f t
pref ixer(c, eAjout_f t(q, g)) ∪ A(h)
= Définition de A

A (c, false, eAjout_f t(q, g)) ∪ A(h)
= Définition de A
A({c, false, eAjout_f t(q, g)} ∪ h)

L’équation gardée suivante s’en déduit :

f = ∅ →
let [c | q] := l in
any i, h where
i ∈ f ∧ h = f − {i}
then
let d, b, g := i in
c=d →
b = false →
q = [ ] →
eAjout_f t(l, f ) = {c, false, eAjout_f t(q, g)} ∪ h
end
end
end

d. Nous repartons de la formule 7.1.10 :


Reste à traiter le cas c =

{[c | q]} ∪ A (d, b, g) ∪ A(h)


= Propriété A.7
A (d, b, g) ∪ {[c | q]} ∪ A(h)
= Propriété caractéristique de l’opération eAjout_f t
A (d, b, g) ∪ A(eAjout_f t([c | q], h))
284 Structures de données et méthodes formelles

= Définition de A, de [c | q] et de d, b, g
A({i} ∪ eAjout_f t(l, h))

D’où l’équation gardée :

f = ∅ →
let [c | q] := l in
any i, h where
i ∈ f ∧ h = f − {i}
then
let d, b, g := i in
c = d →
eAjout_f t(l, f ) = {i} ∪ eAjout_f t(l, h)
end
end
end

Au total, nous avons calculé la représentation suivante de l’opération


eAjout_f t :

function eAjout_f t(l, f ) ∈ chaine × f t → f t = 


let [c | q] := l in
if f = ∅ → /*vraie insertion*/
if q = [ ] →
{c, true, ∅}
| q = [ ] →
{c, false, eAjout_f t(q, ∅)}

| f = ∅ →
any i, h where
i ∈ f ∧ h = f − {i}
then
let d, b, g := i in
if c = d →
if b = true →
if q = [ ] → /*fausse insertion*/
{c, true, g} ∪ h
| q = [ ] →
{c, true, eAjout_f t(q, g)} ∪ h

| b = false →
if q = [ ] → /*vraie insertion*/
{c, true, g} ∪ h
| q = [ ] →
{c, false, eAjout_f t(q, g)} ∪ h

7. Ensembles de clés structurées 285


| c = d →
{i} ∪ eAjout_f t(l, h)

end
end

end

Nous constatons qu’il s’agit d’une version qui utilise une expression non dé-
terministe (any). En outre elle s’exprime à partir de l’opérateur ensembliste ∪.
Ceci est imputable au fait que nous n’avons pour l’instant pas pris de décision
concernant la représentation des forêts-tries en tant qu’ensembles. Il sera donc
nécessaire de poursuivre le développement en réalisant un choix pour la mise en
œuvre de l’ensemble f .
La complexité au pire de cet algorithme dépend a priori de deux paramètres :
la longueur #(l) de la chaîne et la taille n du vocabulaire utilisé. Cependant,
pour une application donnée, le vocabulaire est figé, n est donc constant. Le trai-
tement réalisé pour chaque caractère de la chaîne conduit au pire à descendre
#(l) branches dans la forêt-trie. À chaque niveau, des comparaisons sont néces-
saires, leur nombre dépend à la fois de n et du choix de mise en œuvre réalisé
pour raffiner les forêts-tries. La complexité de l’algorithme eAjout_f t est donc
en O(f (n) · #(l)). Ainsi, par exemple, pour une représentation des forêts-tries
par listes f (n) = n, pour des Avl f (n) = log n et pour des tables d’indexa-
tion alphabétique (c’est-à-dire des tableaux à n entrées, chacune désignant le
descendant éventuel), f (n) = 1. En tout état de cause, quel que soit le choix
réalisé, f (n) est en O(n) et, comme n est constant, eAjout_f t est en O(#(l)).
Une remarque mérite cependant d’être formulée à propos du facteur multipli-
catif. Les codes de caractères utilisés aujourd’hui (Unicode, UTF-32, UTF-8)
peuvent comporter plusieurs dizaines de milliers de caractères, nombre qui dans
le pire des cas constitue la constante multiplicative f (n) ci-dessus. Dans ce type
de situation, le choix de la stratégie de développement et de la mise en œuvre
des forêts-tries n’est pas indifférent.

Représentation de l’opération eSupp_f t


Le calcul de cette opération ne présente pas de difficultés par rapport au
calcul précédent. Une représentation possible est la suivante :

function eSupp_f t(l, f ) ∈ chaine × f t → f t =



if f = ∅ → /*fausse suppression*/

| f = ∅ →
any i, h where
i ∈ f ∧ h = f − {i}
then
286 Structures de données et méthodes formelles

let d, b, g, [c | q] := i, l in


if c = d →
if b = true →
if q = [ ] → /*vraie suppression*/
if g = ∅ →
h
| g = ∅ →
{c, false, g} ∪ h

| q = [ ] →
{c, true, eSupp_f t(q, g)} ∪ h

| b = false →
if q = [ ] → /*fausse suppression*/
{c, false, g} ∪ h
| q = [ ] →
{c, false, eSupp_f t(q, g)} ∪ h


| c = d →
{i} ∪ eSupp_f t(l, h)

end
end

Ici encore un raffinement ultérieur s’impose après avoir décidé d’une repré-
sentation pour les forêts-tries.
Pour ce qui concerne la complexité, les remarques formulées lors de l’étude
de l’opération eAjout_f t s’appliquent ; la complexité de la suppression d’une
chaîne de #(l) caractères est en O(#(l)). Dans [75], D. Knuth propose une ana-
lyse approfondie des complexités spatiales et temporelles des tries (cf. pages 500-
507).

7.1.6 Variantes des tries


Nous allons étudier, sans les développer complètement, deux variantes à la
méthode présentée ci-dessus. La première, les Patricia tries, vise à limiter le
nombre de pointeurs en fusionnant certains nœuds sur un chemin entre la racine
et les feuilles. La seconde est dénommée « hachage dynamique ». Elle est, pour
les supports externes, une bonne alternative aux B-arbres et aux B+ -arbres
présentés au chapitre 6.

Patricia tries
L’idée de base des Patricia tries part du constat que lorsqu’il existe une
séquence de nœuds consécutifs suivis par un nœud terminal ou non et qu’aucun
des nœuds ne donne lieu à une bifurcation, il est possible de fusionner ces nœuds
7. Ensembles de clés structurées 287

en un seul. Le statut (terminal ou non) du nœud résultant de la fusion est hérité


de celui du dernier nœud. Dans un Patricia trie, les nœuds représentent donc en
général des chaînes et non plus de simples caractères. Ainsi le trie de la page 274
conduit au Patricia trie suivant :

{ l , m }
e ap a e isera oi
s i i re s
n der a n

Le gain en terme de complexité spatiale se paye par une complexité accrue des
algorithmes (cf. exercice 7.1.8).

Hachage dynamique

Le hachage dynamique est une technique, fondée sur les tries, adaptée à la
représentation d’ensembles sur supports secondaires à accès direct (typiquement
des disques magnétiques ou des ssd en 2010) pour lesquels on cherche à limiter
le nombre d’accès – que ce soit en lecture ou en écriture – au détriment éventuel
du coût des opérations en mémoire centrale. Un ensemble géré par hachage
dynamique se présente selon deux niveaux de mémoire :
– l’index, situé en mémoire centrale et structuré en forêt-trie, qui permet
l’accès au niveau inférieur, le niveau page ;
– les « pages », de taille fixe n, qui contiennent les clés de l’ensemble. Afin de
permettre une évolution de la structure (ajout et suppression d’éléments),
un système d’allocation/récupération dynamique de pages est supposé dis-
ponible.

niveau index forêt-trie

niveau page ···

Pour simplifier notre représentation, dans la suite nous considérons que les
clés sont des chaînes de caractères de taille fixe (quatre ci-dessous). Le principe
est le suivant. Tout chemin de la racine à une feuille d’un arbre-trie de l’index
donne accès à une page. Par ailleurs tout chemin représente une chaîne. Toutes
les clés d’une page ont comme préfixe commun la chaîne qui donne accès à la
page. Dans l’exemple ci-dessous, le chemin mis désigne la page qui contient les
deux éléments mise et miss, de préfixe commun mis.
288 Structures de données et méthodes formelles

{ m , n }
e i a i
e m s

mena mien mime mise nain nies


miss nais nias
nina

Lors de l’ajout d’une clé c, le trie est utilisé conjointement avec c pour iden-
tifier la page dans laquelle c devra venir s’insérer. Si la page est pleine, toutes
ses clés sont ventilées sur autant de pages que nécessaire, selon la valeur de la
nouvelle position dans les clés. La nouvelle clé est insérée dans la structure ainsi
modifiée. Les suppressions se traitent de manière duale : dès que l’ensemble des
pages filles d’un nœud passe de n + 1 clés à n clés, les pages sont fusionnées en
une seule et le trie est mis à jour.

Exemple. Dans l’exemple traité ci-dessous, nous considérons des pages de


quatre emplacements. Le tableau 7.3 est un synoptique de la mise à jour. Chaque
élément est une clé de longueur fixe de quatre caractères. Onze clés sont tout
d’abord insérées puis trois d’entre elles sont supprimées.

Tableau 7.3 – Séquence de mises à jour par hachage dynamique.


Le tableau représente une séquence de onze ajouts consécutifs suivis de
trois suppressions dans un ensemble de clés à quatre caractères. Cet
ensemble est géré par hachage dynamique. Son évolution est développée
dans le texte.

type clé type clé


1 A mise 8 A nain
2 A nies 9 A nais
3 A miss 10 A mima
A : ajout
4 A nias 11 A mena
S : suppression
5 A mime 12 S nain
6 A nina 13 S mena
7 A mien 14 S mime

1. Ajout de la clé mise. Le trie est créé, une page est allouée, elle va contenir
la clé mise.
7. Ensembles de clés structurées 289

{ m }

mise

2. Ajout de la clé nies, qui conduit à la création d’un nouveau nœud et d’un
nouvel arbre-trie ainsi qu’à l’allocation d’une page. Les six ajouts suivants
(jusqu’à nain compris) se font sans modification de la structure.
{ m , n }

mise nies
miss nias
mime nina
mien nain

On note que les deux pages sont maintenant pleines.


3. Ajout de la clé nais. L’ensemble des clés existant dans la page de préfixe n
({nies, nias, nina, nain}) est partitionné selon le second caractère. Le trie
est mis à jour et la clé nais est insérée dans la nouvelle structure, dans la
page de préfixe na.
{ m , n }
a i

mise nain nies


miss nais nias
mime nina
mien

4. Ajout de la clé mima. Cette fois encore on atteint une page déjà pleine.
L’ensemble {mise, miss, mime, mien} est pris en compte et partitionné
selon le second caractère. i n’étant pas discriminant on obtient :
{ m , n }

i a i

mise nain nies


miss nais nias
mime nina
mien
290 Structures de données et méthodes formelles

Cette situation ne permet toujours pas d’insérer la clé mima. On procède


à un nouveau partitionnement, cette fois selon le troisième caractère. La
clé mima peut alors être insérée.

{ m , n }

i a i
e m s

mien mime mise nain nies


mima miss nais nias
nina

5. Ajout de la clé mena. Une nouvelle branche de préfixe me est créée. On


obtient :

{ m , n }
e i a i
e m s

mena mien mime mise nain nies


mima miss nais nias
nina

qui n’est autre que la configuration présentée en introduction, section 7.1.1.


6. Suppression de la clé nain. Le nombre de clés dépendant du nœud n passe
de cinq à quatre, permettant l’utilisation d’une seule page : on fusionne les
deux pages, le trie est mis à jour, une page est désallouée.

{ m , n }
e i
e m s

mena mien mime mise nais


mima miss nies
nias
nina

7. Suppression de la clé mena : le chemin me est supprimé et la page corres-


pondant désallouée.
7. Ensembles de clés structurées 291

{ m , n }

i
e m s

mien mime mise nais


mima miss nies
nias
nina

8. Suppression de la clé mime. Le nombre de clés dépendant de la chaîne mi


devient égal à quatre. On fusionne les trois pages et on met à jour le trie.
Dans une première phase, le préfixe mi est conservé puis, le i se révélant
inutile, seul le m est conservé, conduisant à :

{ m , n }

mien nais
mima nies
mise nias
miss nina

7.1.7 Conclusion et remarques bibliographiques


Les tries sont souvent introduits comme une solution pour implanter des
chaînes de bits. Dans ce cas, étant donné un nœud, celui-ci possède au plus deux
fils (l’un pour le 0 et l’autre pour le 1). Nous avons d’emblée généralisé à des
chaînes quelconques.
Nous n’avons représenté qu’un nombre limité d’opérations. L’une des opé-
rations les plus utiles sur les chaînes est largement facilitée par l’utilisation des
tries. C’est l’opération de complétion. Étant donné un ensemble de chaînes e
et une chaîne particulière c, il s’agit de rechercher le sous-ensemble de e qui a
comme préfixe la chaîne s. Cette opération peut être utilisée pour l’amorçage de
mots dans un éditeur de textes, pour la complétion de requêtes dans un naviga-
teur Web ou la complétion de commandes lignes dans une application interactive
(comme par exemple un système d’exploitation). L’exercice 7.1.5 propose d’étu-
dier cette opération.
L’un des aspects de la mise en œuvre a été simplement évoqué dans ce qui
précède. C’est celui de la représentation d’une forêt-trie. Puisqu’une forêt-trie
est un ensemble, il faut à un moment ou un autre prendre une décision quant à
la représentation de cet ensemble. Le plus souvent c’est la méthode du vecteur
caractéristique (cf. section 6.1) qui est utilisée ou décrite dans la littérature.
Dans le cas des tries binaires, cette solution s’impose : elle ne présente que
des avantages. Dans le cas des tries « multivoies » comme ceux que nous avons
utilisés, la solution du vecteur caractéristique (cf. section 6.1) – au demeurant
292 Structures de données et méthodes formelles

excellente sur le plan de la complexité temporelle pour les opérations calculées


ci-dessus – est coûteuse en espace puisqu’à chaque nœud sera associé un tableau
de n emplacements (avec n = 256 pour un codage Ascii des caractères) quel que
soit le nombre de fils. Parmi les n emplacements, le plus souvent, peu d’entre eux
sont réellement utilisés, le tableau est en général très creux. Les autres solutions
connues pour représenter des ensembles de scalaires (liste, abr, Avl, etc.) sont
alors des alternatives envisageables à celle du vecteur caractéristique (cf. sec-
tion 6.1). Des solutions mixtes peuvent également être prises en considération.
Selon D.E. Knuth [75], les tries ont été initialement proposés par le mathé-
maticien norvégien A. Thue en 1912 (voir la préface cependant pour une trace
plus ancienne). La première référence informatique est à rechercher auprès de
R. de la Briandais qui, en 1959, propose de raffiner les ensembles de forêts-tries
par des listes. Peu de temps après, en 1960, E. Fredkin forge le terme trie [38].
Les Patricia tries sont dus à D. Morrison en 1968 [86]. Le terme est un acronyme
pour « Practical Algorithm to Retrieve Information Coded in Alphanumeric ».
Le hachage dynamique est décrit initialement en 1978 par P.-Å. Larson dans
[78] et détaillé dans [34]. De nombreuses autres variantes ont été proposées.
C’est par exemple le cas des burst tries [52] qui, à l’instar des structures à ha-
chage dynamique, sont des structures à deux niveaux. Le premier niveau est un
trie classique et le second est une structure ensembliste quelconque (comme des
abr par exemple) ne contenant que les suffixes. Dans [54], R. Hinze propose une
généralisation à des composants quelconques.

Exercices

Exercice 7.1.1 La technique des tries telle qu’elle est utilisée à la section 7.1 ne permet pas
de représenter des ensembles de chaînes contenant la chaîne vide. Proposer des solutions pour
contourner cette limitation.

Exercice 7.1.2 Calculer la représentation des opérations eV ide_f t, eApp_f t, eSupp_f t et


eEstV ide_f t dans le cas des tries.

Exercice 7.1.3 Après avoir décidé d’une implantation ensembliste, compléter les calculs d’opé-
rations réalisés dans cette section pour les tries standard en raffinant les opérateurs ensemblistes
qui subsistent dans la représentation des opérations.

Exercice 7.1.4 L’opération eAjout_f t(l, f ) a été calculée en réalisant une induction sur
l’ensemble f . Une autre solution consiste à rechercher dans l’ensemble A(f ) les chaînes qui
débutent par le même caractère que l. Développer cette solution. Discuter par rapport à la
solution présentée à la section 7.1.

Exercice 7.1.5 Enrichir la spécification abstraite des tries de façon à introduire l’opération
de complétion comp(p, e) qui délivre le sous-ensemble des chaînes de e qui ont comme préfixe
commun la chaîne p. Proposer une spécification concrète sous forme de tries. Calculer une
représentation de cette opération.

Exercice 7.1.6 Le cas des ensembles de chaînes de longueur variable permet d’envisager une
opération qui, dans le cas des ensembles de scalaires, serait dépourvue de sens. Il s’agit de
l’opération d’appariement. Étant donné une chaîne de caractères c d’une part et un ensemble
e de chaînes d’autre part, il s’agit de découvrir (si elle existe) la plus longue chaîne de e qui
s’identifie à un préfixe de la chaîne c. Spécifier et calculer cette opération. À votre avis, quels
types d’application peuvent être demandeurs d’une telle fonctionnalité ?
7. Ensembles de clés structurées 293

Exercice 7.1.7 Spécifier, pour les tries, les opérations concrètes complémentaires décrites
dans l’exercice 6.2.2, page 149. Calculer une représentation de ces opérations.

Exercice 7.1.8 Spécifier le type concret Patricia trie. Calculer les cinq opérations correspon-
dant aux opérations abstraites de la figure 6.1, page 149.

Exercice 7.1.9 Montrer, sur l’exemple suivant, comment évolue une structure de hachage
dynamique dans laquelle les pages ont une taille de quatre emplacements :

type clé type clé


1 A rien 8 A sale
2 A rive 9 A rein
3 A role 10 A rode
A : ajout
4 A rois 11 A rude
S : suppression
5 A sien 12 A sous
6 A soin 13 S sien
7 A sain 14 A soit

Exercice 7.1.10 Spécifier un type concret pour un hachage dynamique avec des chaînes
de longueur fixe n et des pages de p emplacements. Calculer les cinq opérations concrètes
correspondant aux opérations abstraites de la figure 6.1, page 149.

Exercice 7.1.11 On considère le hachage dynamique dans le cas de clés de longueurs quel-
conques positives. Développer l’évolution d’une telle structure sur l’exemple de la page 274.
Quels sont les problèmes nouveaux qui se posent ? Proposer des solutions.

Exercice 7.1.12 Spécifier pour le hachage dynamique les opérations abstraites complémen-
taires décrites dans l’exercice 6.2.2, page 149. Calculer une représentation de ces opérations.

Exercice 7.1.13 On souhaite créer un dictionnaire partitionnant l’ensemble des mots du


français de sorte que chaque entrée répertorie les mots constitués du même sac de lettres. Ainsi
l’entrée contenant le mot varie contiendra également le mot ravie. Les opérations suivantes
sont visées :
– entree qui, à partir d’un mot, délivre tous les mots de la même entrée,
– ajout, qui introduit un nouveau mot dans le dictionnaire,
– supp, qui supprime un mot existant du dictionnaire.
Proposer une spécification abstraite de la structure de données ainsi définie. Rechercher une
mise en œuvre qui soit efficace pour toutes les opérations visées.

7.2 Ensembles de couples, représentation par kd-


arbres
7.2.1 Ensembles de couples, spécification abstraite
Le problème de la représentation de clés scalaires étudié tout au long du
chapitre 6 se généralise au cas de la représentation de clés composées de plusieurs
constituants (c’est-à-dire de sous-ensembles de produits cartésiens, soit encore de
relations). On ne perd pas en généralité en ne considérant uniquement que des
produits cartésiens de deux ensembles (pour lesquels les éléments en question
sont appelés paires ordonnées, 2-uplets ou couples). Par contre, nous limitons
notre étude au cas d’ensembles dotés d’une structure d’ordre totale et de sous-
ensembles finis définis en extension. Les domaines d’application couverts sont
294 Structures de données et méthodes formelles

vastes, allant des bases de données à l’informatique géométrique ou géographique,


en passant par la robotique, l’astronomie, la physique des particules, etc. En guise
d’illustration (cf. tableau 7.4) nous considérons un exemple pris dans un système
d’informations hypothétique dans lequel il existe un ensemble de demandeurs
d’emploi (chacun d’eux identifié par une clé entière) D, un ensemble d’offres
d’emploi (également identifiées par une clé entière) O et un sous-ensemble du
produit cartésien D × O constitué de couples d’entiers (d, o) tels que chacun
d’eux représente l’intérêt potentiel du demandeur d’emploi d pour l’offre o. Ainsi
(couples n◦ 2 et 3) le demandeur inscrit sous le numéro 7 exprime son intérêt
pour les offres d’emploi 5 et 11, tandis que (couples no 2 et 5) l’offre d’emploi 5
est convoitée par les demandeurs 7 et 12 .

Tableau 7.4 – Relation entre demandeurs et offres d’emploi.


Ce tableau représente 10 couples d’entiers. Le premier élément du
couple (dem.) identifie un demandeur d’emploi tandis que le second
(off.) symbolise une offre d’emploi. Chaque couple (d, o) représente l’in-
térêt potentiel du demandeur d envers une offre o. Ces 10 couples re-
présentent un sous-ensemble de N × N, soit encore une relation sur
N × N.

num. dem. off. num. dem. off.


1 3 10 6 15 3
2 7 5 7 15 13
3 7 11 8 17 15
4 9 14 9 20 11
5 12 5 10 21 8

Pourquoi alors traiter le cas des relations différemment du cas des sous-
ensembles de scalaires ? Les opérations définies dans le type abstrait ensabst
(cf. figure 6.1, page 149) ne suffisent-elles pas ? Rechercher si un demandeur
d’emploi est intéressé par une offre, introduire un demandeur donné pour une
offre particulière ou encore supprimer un couple parce qu’un demandeur se dé-
siste par rapport à une offre particulière peuvent se faire via les versions concrètes
des opérations eApp, eAjout et eSupp en considérant chaque couple comme un
scalaire. Cependant il faut en général dépasser ces fonctionnalités élémentaires.
On doit par exemple pouvoir retrouver toutes les offres visées par un demandeur
ou, de manière duale, tous les demandeurs d’emploi intéressés par une offre don-
née. Des opérations complémentaires sont alors nécessaires. Elles font partie de
la spécification abstraite du nouveau type.
La notion de relation est très riche. Ses applications s’étendent bien au-
delà des systèmes d’information. Ainsi, si chaque couple s’interprète comme les
coordonnées cartésiennes d’un plan, il peut être intéressant de rechercher le point
le plus proche d’un point donné ou encore l’ensemble des points situés à l’intérieur
d’une figure géométrique du plan. La relation du tableau 7.4 s’interprète de la
manière suivante sur un plan :
7. Ensembles de clés structurées 295

off.
16
×
14 ×
×
12
× ×
10 ×
8 ×
6
× ×
4
×
2
dem.
2 4 6 8 10 12 14 16 18 20 22

Si l’on ne considère que des relations évolutives, la représentation efficace de


ce type de données reste à ce jour un défi. Les représentations naïves (tableau de
couples ou listes) sont bien entendu simples à mettre en œuvre mais leur efficacité
laisse à désirer. Elles ne doivent cependant pas être négligées comme nous le
verrons plus tard. Les représentations arborescentes butent sur le problème de
l’équilibre, qu’il s’agisse de l’équilibre en poids ou en hauteur. S’il est assez facile
de construire un arbre équilibré à partir d’une représentation naïve, maintenir
cet équilibre lors d’opérations de mise à jour est problématique. La raison est que
l’on ne connaît pas d’opération de rééquilibrage (comme le sont les rotations dans
le cadre scalaire) préservant les propriétés exprimées par le support. En outre,
plus que d’autres, il s’agit d’un type abstrait de données ouvert : il est souvent
nécessaire d’enrichir le type de base par des opérations propres à l’application
sous-jacente.
Notre choix de représentation se porte vers les kd-arbres (aussi appelés arbres
multidirectionnels ou multidimensionnels). En outre, nous ajoutons aux opéra-
tions classiques sur les ensembles, l’opération de coupure qui, à partir d’une abs-
cisse x (resp. d’une ordonnée y), délivre l’ensemble des ordonnées y (resp. des
abscisses x) tel que (x, y) est un couple appartenant à la relation. Ainsi, pour
l’exemple ci-dessus, la coupure selon l’abscisse 7 délivre l’ensemble {5, 11}. La
spécification abstraite ecabst (ensemble de couples abstrait) est présentée à
la figure 7.4. Posé en ces termes, le problème que nous cherchons à résoudre s’ap-
parente au problème de la recherche de solutions efficaces pour les « tableaux
creux » (c’est-à-dire les tableaux ne contenant qu’une faible proportion d’élé-
ments significatifs ou comportant une forte proportion d’éléments identiques).

7.2.2 Ensembles de couples, introduction aux kd-arbres


Dans kd-arbres, kd signifie k d imensions. Compte tenu de notre hypothèse
de ne travailler que sur des relations binaires entre scalaires, k = 2. Notre repré-
sentation se dénomme donc précisément 2d-arbres (ou arbres binaires bidirec-
tionnels). Le type concret correspondant est ec2da. Notre spécification concrète
se fait de manière croisée entre deux spécifications outils, l’une pour le niveau x
296 Structures de données et méthodes formelles

abstractType ecabst = (ecAbst, (ecV ide, ecAjout, ecSupp),


(ecCoupeX, ecCoupeY, ecApp, ecEstV ide))
uses
bool, N
support
c ∈ N × N ⇔ c ∈ ecAbst
operations
..
.
function ecAjout((x, y), e) ∈ (N × N) × ecAbst → ecAbst =

e ∪ {(x, y)}
;
function ecSupp((x, y), e) ∈ (N × N) × ecAbst  ecAbst =

e − {(x, y)}
;
function ecCoupeX(x, e) ∈ N × ecAbst → ensAbst(N) =

e[{x}]
;
function ecCoupeY (y,