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, e) ∈ N × ecAbst → ensAbst(N) =

e−1 [{y}]
..
.
end

Figure 7.4 – Spécification du type abstrait ecabst.


Le type abstrait ecabst formalise la représentation de relations sur
N×N. Outre les opérations ecV ide, ecAjout, ecSupp, ecApp, ecEstV ide,
homologues des opérations eV ide, eAjout, eSupp, eApp, eEstV ide pour
les ensembles de scalaires, deux opérations spécifiques, ecCoupeX et
ecCoupeY sont spécifiées. ecCoupeX(x, e) (resp. ecCoupeY (y, e)) dé-
livre l’image par e (resp. e−1 ) de l’ensemble {x} (resp. {y}).

(x2da) et l’autre pour le niveau y (y2da) 3 . Les niveaux x et y alternent depuis


la racine jusqu’aux feuilles. Par convention, la racine d’un arbre du type ec2da
est à un niveau x.

7.2.3 Définition du support concret


Un 2d-arbre non vide est un arbre binaire dont chaque nœud est un couple
d’entiers (x, y). C’est un x2d-arbre et ses deux sous-arbres sont des y2d-arbres
(dont les éventuels sous-arbres sont eux-mêmes des x2d-arbres). À cela vient se
superposer le fait qu’il s’agit d’arbres de recherche 4 . Plus précisément :
1. Un arbre vide est un x2d-arbre (resp. un y2d-arbre).

3. Dans la suite nous utilisons parfois le préfixe x- (resp. y-) pour désigner une entité du
niveau x (resp. y).
4. Un 1d-arbre est donc un abr.
7. Ensembles de clés structurées 297

2. Un arbre binaire de couples, dont la racine représente le couple (x, y) est


un x2d-arbre (resp. un y2d-arbre) si et seulement si :
– les couples représentés dans le sous-arbre gauche sont des points diffé-
rents de la racine d’abscisses (resp. d’ordonnées) inférieures ou égales à
x (resp. y) ;
– les couples représentés dans le sous-arbre droit sont des points d’abscisses
(resp. d’ordonnées) supérieures à x (resp. y) ;
– les deux sous-arbres sont des y2d-arbres (resp. x2d-arbres).
Nous pouvons fournir une interprétation « planaire » d’un 2d-arbre : la racine
d’un x2d-arbre (resp. d’un y2d-arbre) divise sa portion du plan (ou le plan
complet pour la racine initiale) en deux sous-espaces selon une droite parallèle
à l’axe des y (resp. des x). C’est ce que montre la figure 7.5 pour la relation du
tableau 7.4, page 294, en fournissant deux interprétations possibles ainsi que les
2d-arbres correspondants.

y
16
×
14 × (12, 5)
×
12
× × (3, 10) (20, 11)
10 ×
8 × (7, 5) (9, 14) (15, 3) (17, 15)
6
× × (7, 11) (21, 8) (15, 13)
4
×
2
x
2 4 6 8 10 12 14 16 18 20 22
y
16
× (15, 3)
14 ×
×
12 (7, 11) (20, 11)
× ×
10 ×
(3, 10) (9, 14) (21, 8) (17, 15)
8 ×
6 (7, 5) (15, 13)
× ×
4
× (12, 5)
2
x
2 4 6 8 10 12 14 16 18 20 22

Figure 7.5 – 2d-arbres et interprétation planaire.


Cette figure montre deux 2d-arbres possibles pour la relation du ta-
bleau 7.4, page 294, ainsi que l’interprétation planaire associée.

Dans la suite nous définissons deux types concrets auxiliaires, x2da pour le
niveau x et y2da pour niveau y. Le type ec2da s’identifie à x2da. Le support
du type concret x2da se définit formellement par :
298 Structures de données et méthodes formelles

1)  ∈ x2da
2) / Ay (g) ∧
g ∈ y2da ∧ d ∈ y2da ∧ (x, y) ∈ N × N ∧ (x, y) ∈
max(dom(Ay (g))) ≤ x ∧ min(dom(Ay (d))) > x

g, (x, y), d ∈ x2da

où y2da est le support concret dual de x2da (cf. figure 7.7, page 300, pour une
description plus complète de ce type) et Ay la fonction d’abstraction correspon-
dante, qui délivre la relation abstraite à partir de sa représentation sous la forme
d’un arbre y2da.
Compte tenu de cette définition, le support ec2da du type concret ec2da est
un simple renommage du support x2da :

a ∈ x2da ⇔ a ∈ ec2da

Théorème 15 (des kd-arbres). Soit a = g, (x , y  ), d un kd-arbre et (x, y) ∈


N × N.
1. x > x ⇒ (x, y) ∈
/ Ay (g),
2. x ≤ x ⇒ (x, y) ∈
/ Ay (d).

La démonstration de ce théorème est laissée en exercice.

7.2.4 Définition de la fonction d’abstraction


La fonction d’abstraction Ax du type auxiliaire x2da se définit également de
manière croisée avec la fonction d’abstraction Ay du type auxiliaire y2da :

function Ax (a) ∈ x2da  ecAbst =



if a =  →

| a = g, (x, y), d →
Ay (g) ∪ {(x, y)} ∪ Ay (d)

Tandis que la fonction d’abstraction A du type concret ec2da s’identifie à Ax :

function A(a) ∈ ec2da  ecAbst =


 Ax (a)

7.2.5 Spécification des opérations concrètes


Les figures 7.6, 7.7 et 7.8 (resp. page ci-contre, 300, 301) spécifient les types
concrets ec2da, x2da et y2da. ec2da est le type principal qui se définit à
partir du type outil x2da. Les types x2da et y2da se définissent de manière
croisée. Ainsi que nous l’avons dit, le type ec2da est un simple renommage du
type x2da.
7. Ensembles de clés structurées 299

concreteType ec2da =  (ec2da, (ecV ide_2d, ecAjout_2d, ecSupp_2d),


(ecCoupeX_2d, ecCoupeY _2d, ecApp_2d, ecEstV ide_2d))
uses
bool, x2da, N
refines
ecabst
support
a ∈ x2da ⇔ a ∈ ec2da
abstractionFunction
function A(a) ∈ ec2da  ecAbst =  Ax (a)
operationSpecifications
..
.
function ecAjout_2d((x, y), a) ∈ (N × N) × ec2da → ec2da =

ecAjout_x((x, y), a)
;
function ecSupp_2d((x, y), a) ∈ (N × N) × ec2da  ec2da =

ecSupp_x((x, y), a)
;
function ecCoupeX_2d(x, a) ∈ N × ec2da  ensAbst(N) =

ecCoupeX_x(x, a)
;
function ecCoupeY _2d(y, a) ∈ N × ec2da  ensAbst(N) =

ecCoupeY _x(y, a)
..
.
end

Figure 7.6 – Spécification du type concret ec2da.


Le type concret ec2ad raffine le type abstrait ecabst. Il met en œuvre
des ensembles de couples par la méthode des 2d-arbres. Ce type s’iden-
tifie au type auxiliaire x2ad tant pour le support et la fonction d’abs-
traction que pour les opérations. La fonction d’abstraction est présentée
page ci-contre.

7.2.6 Calcul d’une représentation des opérations concrètes

Le calcul des opérations du type principal ec2da se base sur celui des opé-
rations du type x2da. Pour cette raison, nous nous intéressons tout d’abord
à ce dernier type : nous développons complètement le calcul des opérations
ecAjout_x, ecCoupeX_x (et par conséquent ecCoupeX_y) et nous présen-
tons l’opération ecSupp_x. Le calcul des opérations ecV ide_x, ecApp_x et
ecEstvide_x est laissé en exercice. Celui des opérations ecCoupeY _x et
ecCoupeY _y est similaire aux calculs de ecCoupeX_x et de ecCoupeX_y. La
représentation des autres opérations peut être déduite de leurs homologues par
symétrie.
300 Structures de données et méthodes formelles

concreteType x2da =  (x2da, (ecV ide_x, ecAjout_x, ecSupp_x),


(ecCoupeX_x, ecCoupeY _x, ecApp_x, ecEstV ide_x))
uses
bool, y2da, N
refines ecabst
support
1)  ∈ x2da
2) g ∈ y2da ∧ d ∈ y2da ∧ (x, y) ∈ N × N ∧ (x, y) ∈ / Ay (g) ∧
max(dom(Ay (g))) ≤ x ∧ min(dom(Ay (d))) > x

g, (x, y), d ∈ x2da
abstractionFunction
function Ax (a) ∈ x2da  ecAbst =  ...
operationSpecifications
..
.
function ecAjout_x((x, y), a) ∈ (N × N) × x2da → x2da =

b : (b ∈ x2da ∧ Ax (b) = ecAjout((x, y), Ax (a)))
;
function ecSupp_x((x, y), a) ∈ (N × N) × x2da  x2da =

b : (b ∈ x2da ∧ Ax (b) = ecSupp((x, y), Ax (a)))
;
function ecCoupeX_x(x, a) ∈ N × x2da  ensAbst(N) =

ecCoupeX(x, Ax (a)))
;
function ecCoupeY _x(y, a) ∈ N × x2da  ensAbst(N) =

ecCoupeY (y, Ax (a))
..
.
end

Figure 7.7 – Spécification du type concret x2da.


Le type concret x2ad raffine le type abstrait ecabst. Il représente les
niveaux x.

Calcul d’une représentation des opérations ecAjout_x et ecAjout_2d


L’opération ecAjout_x((x, y), a) ajoute le couple (x, y) à l’ensemble repré-
senté par le 2d-arbre a. Le calcul est comparable à celui de l’insertion aux feuilles
dans un abr (cf. section 6.3.4), la principale différence portant sur l’existence
d’une alternance entre deux niveaux consécutifs : niveau x, puis niveau y, etc.
Ax (ecAjout_x((x, y), a))
= Propriété caractéristique de ecAjout_x
Ax (a) ∪ {(x, y)} (7.2.1)
Nous réalisons une induction sur la structure de l’arbre a en débutant par
a =  :
7. Ensembles de clés structurées 301

concreteType y2da =  (y2da, (ecV ide_y, ecAjout_y, ecSupp_y),


(ecCoupeX_y, ecCoupeY _y, ecApp_y, ecEstV ide_y))
uses
bool, x2da, ecabst, N
support
1)  ∈ y2da
2) g ∈ x2da ∧ d ∈ x2da ∧ (x, y) ∈ N × N ∧ (x, y) ∈ / Ax (g) ∧
max(ran(Ax (g))) ≤ y ∧ min(ran(Ax (d))) > y

g, (x, y), d ∈ y2da
abstractionFunction
function Ay (a) ∈ y2da  ecAbst =  ...
operationSpecifications
..
.
function ecAjout_y((x, y), a) ∈ (N × N) × y2da → y2da =

b : (b ∈ y2da ∧ Ay (b) = ecAjout((x, y), Ay (a)))
;
function ecSupp_y((x, y), a) ∈ (N × N) × y2da  y2da =

b : (b ∈ y2da ∧ Ay (b) = ecSupp((x, y), Ay (a)))
;
function ecCoupeX_y(x, a) ∈ N × y2da  ensAbst(N) =

ecCoupeX(x, Ay (a))) ;
;
function ecCoupeY _y(y, a) ∈ N × y2da  ensAbst(N) =

ecCoupeY (y, Ay (a)) ;
..
.
end

Figure 7.8 – Spécification du type concret y2da.


Le type concret y2da est un type auxiliaire pour la définition du type
x2da. Il représente les niveaux y.

Ax (a) ∪ {(x, y)}


= Hypothèse
Ax () ∪ {(x, y)}
= Définition de Ax
∅ ∪ {(x, y)}
= Définition de Ay
Ay () ∪ {(x, y)}
= Propriété A.9
Ay () ∪ {(x, y)} ∪ ∅
= Définition de Ay
Ay () ∪ {(x, y)} ∪ Ay ()
= Définition de Ax
302 Structures de données et méthodes formelles

Ax (, (x, y), )

D’où, d’après la propriété de l’équation à membres identiques (page 67), la pre-


mière équation gardée :

a =  →
ecAjout_x((x, y), a) = , (x, y), 

Concernant la partie inductive, nous pouvons poser a = g, (x , y  ), d. Les
sous-arbres g et d se situent sur un niveau y. Ce sont des y2da. En repartant de
la formule 7.2.1, nous avons immédiatement :

Ax (a) ∪ {(x, y)}


= Hypothèse
Ax (g, (x , y  ), d) ∪ {(x, y)}
= Définition de Ax
Ay (g) ∪ {(x , y  )} ∪ Ay (d) ∪ {(x, y)} (7.2.2)

Il nous faut alors procéder à une analyse par cas selon que (x, y) = (x , y  ) ou
non. Dans le premier cas, nous avons :

Ay (g) ∪ {(x , y  )} ∪ Ay (d) ∪ {(x, y)}


= Propriété A.18
Ay (g) ∪ {(x , y  )} ∪ Ay (d)
= Définition de Ax
Ax (g, (x , y  ), d)
= Hypothèse
Ax (a)

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

a = g, (x , y  ), d →
(x, y) = (x , y  ) →
ecAjout_x((x, y), a) = a

Si au contraire (x, y) = (x , y  ), nous procédons à une analyse par cas selon
que x ≤ x ou que x > x . Développons le premier cas en repartant de la
formule 7.2.2 :

Ay (g) ∪ {(x , y  )} ∪ Ay (d) ∪ {(x, y)}


= Propriété A.7
Ay (g) ∪ {(x, y)} ∪ {(x , y  )} ∪ Ay (d)
= Propriété caractéristique de ecAjout_y
Ay (ecAjout_y((x, y), g)) ∪ {(x , y  )} ∪ Ay (d)
= Définition de Ax
Ax (ecAjout_y((x, y), g), (x , y  ), d)

Cette dernière formule nous donne, d’après la propriété de l’équation à membres


identiques (page 67), l’équation gardée suivante :
7. Ensembles de clés structurées 303

a = g, (x , y  ), d →
(x, y) = (x , y  ) →
x ≤ x →
ecAjout_x((x, y), a) = ecAjout_y((x, y), g), (x , y  ), d

Le cas x > x se traite de manière analogue. Au total, nous avons calculé la


représentation suivante de l’opération ecAjout_x :

function ecAjout_x((x, y), a) ∈ (N × N) × x2da → x2da = 


if a =  → /*vraie insertion*/
, (x, y), 
| a = g, (x , y  ), d →
if (x, y) = (x , y  ) → /*fausse insertion*/
a
| (x, y) = (x , y  ) →
if x ≤ x → /*insertion à gauche*/
ecAjout_y((x, y), g), (x , y  ), d
| x > x → /*insertion à droite*/
g, (x , y  ), ecAjout_y((x, y), d)


Il est alors facile d’en déduire une représentation de l’opération de base


ecAjout_2d :

ecAjout_2d((x, y), a)
= Spécification de ecAjout_2d
ecAjout_x((x, y), a)

D’où la représentation de l’opération ecAjout_2d :

function ecAjout_2d((x, y), a) ∈ (N × N) × ec2da → ec2da =



ecAjout_x((x, y), a)

Concernant la complexité de l’insertion dans un kd-arbre aléatoire, J. Bentley


(cf. [13], page 511) montre une identité de comportement avec celui de l’insertion
aux feuilles dans un abr aléatoire. Pour y parvenir, il ramène le calcul sur les kd-
arbres à celui sur les abr (cf. section 6.3.4). Il en conclut que le coût de l’insertion
dans un kd-arbre de poids n est en O(log n). Il en est de même pour le coût de
l’opération de recherche ecApp_2d. Cependant, dans le cas de 2d-arbres qui ne
sont pas aléatoires, la complexité au pire est en O(n).

Calcul d’une représentation des opérations ecCoupeX_x et ecCoupeX_y


Rappelons que l’opération ecCoupeX_x(x, a) s’applique à un x2d-arbre et
délivre l’ensemble des valeurs y telles que (x, y) est un couple de l’ensemble
représenté par l’arbre a. Nous verrons que sur un x-niveau il est possible de
304 Structures de données et méthodes formelles

sélectionner le sous-arbre vers lequel poursuivre la recherche, mais que sur un


y-niveau il est nécessaire d’explorer le sous-arbre gauche et le droit. Insistons par
ailleurs sur le fait que la spécification de l’opération ecCoupeX_x(x, a) ne prend
pas position sur la représentation de l’ensemble constituant le résultat. Celui-ci
est exprimé par le type abstrait ensabst(N). Un raffinement ultime est donc
nécessaire afin de préciser la représentation choisie pour ensabst(N). Celle-ci
devra permettre d’exprimer l’union de deux ensembles.

ecCoupeX_x(x, a)
= Spécification de ecCoupeX_x
Ax (a)[{x}] (7.2.3)

Procédons à une induction sur la structure de a, en débutant par le cas de


base a = .

Ax (a)[{x}]
= Hypothèse
Ax ()[{x}]
= Définition de Ax
∅[{x}]
= Propriété B.62

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

a =  →
ecCoupeX_x(x, a) = ∅

La partie inductive considère que a = g, (x , y  ), d. Nous repartons de la


formule 7.2.3 :

Ax (a)[{x}]
= Hypothèse
Ax (g, (x , y  ), d)[{x}]
= Définition de Ax
(Ay (g) ∪ {(x , y  )} ∪ Ay (d))[{x}]
= Propriété B.57
Ay (g)[{x}] ∪ {(x , y  )}[{x}] ∪ Ay (d)[{x}]

Dans l’hypothèse où x ≤ x , nous pouvons en conclure que par transitivité


min(dom(Ay (d))) > x et donc la propriété B.62 s’applique. Elle permet d’élimi-
ner le troisième terme :

Ay (g)[{x}] ∪ {(x , y  )}[{x}] ∪ Ay (d)[{x}]


= Remarque ci-dessus et propriété B.62
Ay (g)[{x}] ∪ {(x , y  )}[{x}] ∪ ∅
= Propriété A.9
Ay (g)[{x}] ∪ {(x , y  )}[{x}]
7. Ensembles de clés structurées 305

Nous pouvons alors développer séparément chacun des deux termes. Débutons
par Ay (g)[{x}] :

Ay (g)[{x}]
= Propriété caractéristique de ecCoupeX_y
ecCoupeX_y(x, g)

Le terme {(x , y  )}[{x}] exige une analyse par cas selon que x = x ou non. Si
x = x nous avons :

{(x , y  )}[{x}]
= Propriété B.60
{y  }

Sinon nous avons :

{(x , y  )}[{x}]
= Propriété B.62

En regroupant le résultat des calculs, nous obtenons l’équation gardée suivante :

a = g, (x , y  ), d →
ecCoupeX_x(x, a) =
ecCoupeX_y(x, g) ∪ if x = x → {y  } | x = x → ∅ fi

Le cas x > x se traite de manière analogue, mais se limite à effectuer la x-


recherche sur la droite. Au total, nous obtenons la représentation suivante de
l’opération ecCoupeX_x :

function ecCoupeX_x(x, a) ∈ N × x2da  ensAbst = 


if a =  →

| a = g, (x , y  ), d →
if x ≤ x →
ecCoupeX_y(x, g) ∪ if x = x → {y  } | x = x → ∅ fi
| x > x →
ecCoupeX_y(x, d)

Reste à calculer la représentation de l’opération ecCoupeX_y. Celle-ci ne se


déduit pas de celle de ecCoupeX_x par symétrie. En effet, ecCoupeX_y coupe
un y2d-arbre selon la première coordonnée. Or un tel arbre n’est pas un arbre
de recherche en x. Il nous faut donc reprendre le calcul :

ecCoupeX_y(x, a)
= Spécification de ecCoupeX_y
Ay (a)[{x}] (7.2.4)
306 Structures de données et méthodes formelles

La suite du calcul se fait par induction sur la structure de a. Pour le cas de base,
nous sommes dans la même situation que pour l’opération ecCoupeX_x. Nous
avons donc l’équation gardée :

a =  →
ecCoupeX_y(x, a) = ∅

Par contre, pour ce qui concerne la partie inductive, puisque a est un y2d-
arbre, la racine ne partitionne pas les points selon l’abscisse. Il est donc nécessaire
de poursuivre la recherche systématiquement dans les deux sous-arbres, gauche
et droit. En repartant de la formule 7.2.4 et après avoir posé a = g, (x , y  ), d,
nous avons :

Ay (a)[{x}]
= Hypothèse
Ay (g, (x , y  ), d)[{x}]
= Définition de Ax
(Ax (g) ∪ {(x , y  )} ∪ Ax (d))[{x}]
= Propriété B.57
Ax (g)[{x}] ∪ {(x , y  )}[{x}] ∪ Ax (d)[{x}]

Le second terme, {(x , y  )}[{x}], se traite comme ci-dessus mais l’analogie s’arrête
là puisqu’il faut conserver les deux autres termes (le premier et le dernier). Nous
obtenons la seconde et dernière équation gardée :

a = g, (x , y  ), d →
ecCoupeX_y(x, a) =
ecCoupeX_x(x, g) ∪ ecCoupeX_x(x, d) ∪
if x = x → {y  } | x = x → ∅ fi

Au total, nous avons calculé la version suivante de l’opération ecCoupeX_y :

function ecCoupeX_y(x, a) ∈ N × x2da  ensAbst =



if a =  →

| a = g, (x , y  ), d → ⎛ ⎞
if x = x →
⎜ {y  } ⎟
⎜ ⎟
ecCoupeX_x(x, g) ∪ ecCoupeX_x(x, d) ∪ ⎜
⎜ | x = x →
 ⎟

⎝ ∅ ⎠

La représentation des opérations ecCoupeX_2d et ecCoupeY _2d s’en déduit


facilement.
Qu’en est-il de la complexité de l’opération de coupure (évaluée en nombre
d’appels aux fonctions ecCoupeX_x et ecCoupeX_y et en faisant abstraction
des opérations sur les ensembles) ? Pour un arbre de n nœuds, le cas le pire est
7. Ensembles de clés structurées 307

clairement en O(n). Par contre, dans le cas d’un arbre plein 5 (cf. section 3.5)
pour lequel n = 2m − 1, les complexités Cx (n) et Cy (n) s’expriment par les équa-
tions récurrentes ci-dessous. Celles-ci sont obtenues à partir de la représentation
des deux opérations :

Cx (0) = 1
Cx (2m − 1) = Cy (2m−1 − 1) pour m > 0
 (7.2.5)
Cy (0) = 1
Cy (2m − 1) = 2 · Cx (2m−1 − 1) pour m > 0

1 1
On montre alors que Cx (n) = (n + 1) 2 et donc que Cx (n) est en n 2 (cf. exer-
cice 7.2.1).

Représentation de l’opération ecSupp_x


Le calcul des opérations auxiliaires ecSupp_x et ecSupp_y ne présente pas
de difficulté par rapport aux calculs déjà effectués. Ils sont proposés à l’exer-
cice 7.2.3. Une représentation possible pour ecSupp_x est la suivante :

function ecSupp_x((x, y), a) ∈ (N × N) × x2da  x2da = 


if a =  → /*fausse suppression*/
a
| a = g, (x , y  ), d →
if (x, y) = (x , y  ) →
if x ≤ x → /*suppression à gauche*/
ecSupp_y((x, y), g), (x , y  ), d
| x > x → /*suppression à droite*/
g, (x , y  ), ecSupp_y((x, y), d)

| (x, y) = (x , y  ) →
if g =  ∧ d =  → /*suppression sur une feuille*/

| g =  → /*remplacement par le max. gauche*/
let (xg , yg ) := maxX_x(g) in
ecSupp_y((xg , yg ), g), (xg , yg ), d
end
| d =  → /*remplacement par le min. droit*/
let (xd , yd ) := minX_x(d) in
g, (xd , yd ), ecSupp_y((xd , yd ), d)
end


5. Un arbre plein est un arbre particulièrement bien équilibré en hauteur. Le résultat ci-
dessous s’étend aux arbres p-équilibrés quelconques.
308 Structures de données et méthodes formelles

maxX_x(a) (resp. minX_x(a)) est la fonction qui délivre l’un des couples x-
maximum (resp. x-minimum) dans l’arbre non vide a. L’opération ecSupp_y,
qui supprime un couple d’un arbre de niveau y, s’obtient par symétrie. Elle
n’est pas représentée ici. Outre l’existence d’une alternance de niveaux, la prin-
cipale différence avec la suppression dans les abr porte sur le fait qu’ici nous
ne savons pas fusionner la recherche d’un extremum avec la suppression. Nous
sommes à présent face au problème de la représentation des fonctions maxX_x
et minX_x (et des fonctions maxX_y et minX_y pour l’opération ecSupp_y).
Nous avons vu (cf. sections 1.10 et 2.4) que deux types de réaction sont alors
possibles : soit nous évaluons quand nécessaire les opérations en question (avec
le risque d’être confronté à une complexité temporelle excessive), soit nous ren-
forçons le support en décomposant ces fonctions sur la structure de données. Le
risque étant cette fois d’accroître la complexité spatiale. Cette dernière solution
n’est cependant viable que si la fonction est O(1)-décomposable (cf. encadré
page 188). La première solution est proposée en exercice (cf. exercice 7.2.2). La
seconde solution nous conduit à enrichir le support par des champs contenant
la valeur de chacune des fonctions. Ainsi, sur un x-nœud, nous aurons à notre
disposition le couple x-maximum à gauche et le couple x-minimum à droite.
Trouver le couple qui viendra remplacer la racine lors d’une suppression se fait
en consultant les nouvelles informations situées sur le nœud considéré (inutile de
descendre dans l’arbre). Il suffit ensuite de supprimer ce couple dans le sous-arbre
correspondant. Ces fonctions sont bien O(1)-décomposables (la vérification est
immédiate). Appliquée à un arbre équilibré de poids n, ecSupp_x (de même que
ecSupp_y) est en O(log n). Ce raffinement est proposé à l’exercice 7.2.3. Notons
que ces modifications, par les conséquences qu’elles entraînent sur le support,
ont un impact sur toutes les opérations de mise à jour. La représentation de
l’opération de base ecSupp_2d s’en déduit facilement :

function ecSupp_2d((x, y), a) ∈ (N × N) × ec2da 


 ec2da =

ecSupp_x((x, y), a)

Il est clair que la suppression telle que nous l’avons présentée ci-dessus a
tendance à déséquilibrer l’arbre considéré. L’évaluation de complexité réalisée
ci-dessus perd de son intérêt. D’autres solutions, plus radicales mais plus simples
à mettre en œuvre sont parfois utilisées pour effectuer une suppression. L’une
d’elles consiste, dès que l’on a identifié le nœud à supprimer, à collecter tous les
nœuds des deux sous-arbres puis à reconstruire un 2d-arbre équilibré (cf. exer-
cice 7.2.4). Dans son principe ce type de solution revient à une conversion entre
supports. Un support « naïf » comme un tableau peut alors convenir comme
support intermédiaire. Une autre solution consiste à marquer les nœuds suppri-
més (cf. [84, 123]), puis lorsque l’on décide de reconstruire le 2d-arbre, à ignorer
les nœuds marqués. La décision de reconstruire l’arbre peut par exemple être
prise sur un critère de rapport entre le nombre de nœuds « morts » et le nombre
total de nœuds. Là aussi il est possible d’obtenir un arbre équilibré après la
reconstruction. Ce type de technique se prête a priori bien à une analyse amor-
tie. Cependant, en considérant que la reconstruction se fasse dès qu’il y a (au
moins) la moitié de nœuds morts, une analyse amortie, avec comme fonction de
potentiel le nombre de nœuds morts, fournit un résultat en O(n · log n).
7. Ensembles de clés structurées 309

7.2.7 Conclusion et remarques bibliographiques


La notion de kd-arbre est due à J. Bentley dans un article initial de 1975 [13].
La bibliographie rassemblée ici (outre la référence précédente, [84, 120, 88, 18, 39,
22, 123]) sous-évalue les énormes développements auxquels les kd-arbres et plus
généralement les « structures spatiales » ont donné naissance. Malgré ces efforts,
le défi de l’efficacité reste d’actualité. Nous avons vu que l’on sait traiter efficace-
ment la plupart des opérations classiques mais uniquement dans le cas d’arbres
équilibrés. Toutefois, on ne sait pas maintenir cet équilibre autrement que par
une reconstruction – coûteuse – de la structure. De plus on ne connaît pas de
structures autoadaptatives satisfaisantes. Le problème de la complexité spatiale
ne doit pas non plus être négligé. La solution consistant à renforcer le support
en décomposant les fonctions de recherche d’extremum conduit grossièrement à
doubler la taille des nœuds. Compte tenu des applications concernées, comme
l’astronomie, les limites de la mémoire risquent d’être rapidement atteintes. Il
est alors intéressant de se tourner vers des structures partiellement sur mémoires
secondaires. La technique des arbres externes, qui permet de séparer l’index des
données, est alors avantageuse (cf. l’article de C. Yu [120]).
D’autres mises en œuvres concrètes peuvent être envisagées sur la base des
vecteurs caractéristiques (cf. section 6.1) ou de ce qui a été réalisé pour les en-
sembles de scalaires : arbres, table de hachage. La méthode du vecteur caracté-
ristique se transpose en matrice (ou tableau à deux dimensions) caractéristique.
La représentation planaire de la page 295 fournit une bonne idée d’une telle
représentation. Les qualités et défauts de cette méthode se retrouvent ici : limi-
tation des bornes et complexité spatiale du côté des inconvénients. Efficacité des
opérations pour les avantages.
Les techniques de hachage s’adaptent sans difficulté (mais pas sans inconvé-
nients) à la gestion d’ensembles de couples (cf. exercice 7.2.9). Le problème de
la gestion des collisions subsiste mais l’inconvénient le plus pénalisant concerne
la mise en œuvre d’opérations exigeant de prendre en compte la distance entre
deux points. Ce qui se manifestait dans le cas d’ensembles de scalaires par la
quasi-impossibilité d’un traitement séquentiel se traduit ici par la difficulté à
retrouver le point le plus proche d’un point donné. Compte tenu de l’impor-
tance de ce type d’opération, le hachage est une solution qui n’est que rarement
adoptée.
Concernant les représentations arborescentes, il faut citer les treaps (cf. sec-
tion 6.9) qui, sous la dénomination d’arbres de recherche à priorité, sont utilisés
par E. McCreight [82] mais en imposant des limites sur les bornes d’une des
dimensions. Ils sont également utilisés par J. Vuillemin dès 1978 sous la déno-
mination d’arbres cartésiens [115].
Mais parmi les solutions arborescentes les plus utilisées en pratique, outre les
kd-arbres, on trouve les quad trees (ou arbres quaternaires). Il s’agit de précur-
seurs aux kd-arbres, proposés par R.A. Finkel et J. Bentley en 1974 (cf. [35]).
Le support est un arbre 4-aire dont la racine représente un couple de coordon-
nées (x, y) tandis que chacun des quatre sous-arbres représente un quadrant du
plan (ou de la portion du plan) considéré. Là aussi, étant donné un ensemble de
couples, il existe en général plusieurs représentations possibles. Ainsi la relation
du tableau 7.4, page 294, pourrait par exemple se représenter comme le montre
310 Structures de données et méthodes formelles

la figure 7.9 ci-dessous. Dans les quad trees, la suppression est une opération
délicate. L’article original [35] propose de réintroduire tous les descendants du
nœud supprimé l’un après l’autre. Cette méthode est bien entendu très coûteuse.
Dans [99], H. Samet étudie une optimisation qui consiste à prendre comme nou-
velle racine le nœud qui minimise le nombre de réintroductions. Les quad trees
peuvent être utilisés pour représenter non plus des points mais des régions d’un
plan (on parle alors de « quad trees zone »). Lorsque l’on représente des points
d’un espace à trois dimensions, on utilise des « octrees » qui découpent l’espace
en 8 octants. 2d-arbres et quad trees ont des performances assez semblables
(cf. [18] pour une évaluation expérimentale). Par contre, la suppression est plus
simple dans les 2d-arbres.

y
16
×
14 × (12, 5)
×
12
× ×
10 × (7, 11) (15, 3) (15, 13)

8 ×
6 (9, 14) (3, 10) (20, 11)(17, 15)
× ×
4
×
2 (7, 5) (21, 7)

x
2 4 6 8 10 12 14 16 18 20 22

Figure 7.9 – Quad tree et interprétation planaire.


Cette figure montre un quad tree possible pour la relation du tableau 7.4,
page 294, ainsi que l’interprétation planaire associée. Dans l’arbre ci-
dessus, les quadrants sont représentés dans l’ordre nord-ouest, sud-est,
nord-est, sud-ouest.

Concluons par une citation de D. Knuth dans [75], page 579, (section consa-
crée à la recherche sur clés secondaires) : en conséquence il n’est pas improbable
qu’une approche complètement nouvelle de la conception de machines soit dé-
couverte, qui résoudrait une fois pour toutes le problème de la recherche sur clés
secondaires, rendant obsolète la totalité de cette section.

Exercices

Exercice 7.2.1 Montrer que pour m pair et n = 2m − 1 la solution au système d’équations


1
récurrentes 7.2.5, page 307, est Cx (n) = (n + 1) 2 .

Exercice 7.2.2 Calculer les fonctions auxiliaires maxX_x, minX_x, maxX_y et minX_y.
Évaluer la complexité la meilleure et la pire.

Exercice 7.2.3 Calculer l’opération ecSupp_x puis raffiner les opérations calculées en décom-
posant les opérations maxX_x, minX_x, maxX_y et minX_y sur la structure de données.
7. Ensembles de clés structurées 311

Toutes les opérations de mise à jour doivent être revues afin que les propriétés du support
demeurent invariantes.

Exercice 7.2.4 (Construction d’un 2d-arbre) On dispose d’un ensemble de couples


d’entiers dans un tableau t. Étudier le problème de la construction d’un 2d-arbre équilibré à
partir de t. Évaluer la complexité.

Exercice 7.2.5 La démarche utilisée ci-dessus consistant à construire une fonction (d’ajout,
de suppression, etc.) par dimension n’est pas tenable lorsque le nombre de dimensions est élevé.
Il est alors nécessaire de fusionner les fonctions similaires mais travaillant sur des « niveaux »
différents en une seule fonction véhiculant le niveau considéré comme paramètre. Effectuer
l’analyse, la formalisation et le calcul de façon à prendre en compte ce type de généralisation.

Exercice 7.2.6 Refaire l’analyse, la formalisation et le calcul des opérations en utilisant un


support du type arbre externe.

Exercice 7.2.7 Enrichir le type abstrait ecabst par les opérations suivantes :
– recherche du plus proche voisin d’un point donné pour une distance euclidienne,
– recherche des points à l’intérieur d’un cercle, d’un rectangle.
Calculer une représentation avec une mise en œuvre sur un support du type kd-arbre. Évaluer
la complexité la meilleure et la pire.

Exercice 7.2.8 Fournir la spécification concrète de la mise en œuvre d’ensembles de couples


par quad trees (support, fonction de raffinement et opérations du type abstrait à l’exception
de la suppression). Calculer une représentation pour chacune des opérations.

Exercice 7.2.9 (Ensemble de couples par table de hachage) Étant donné un couple
d’entiers (x, y), il est possible de calculer une valeur de hachage pour chaque composant du
couple. (x, y) peut donc être utilisé pour accéder directement à une cellule d’un tableau à deux
dimensions. Le problème des collisions (cf. section 6.4.1) interdit de réserver cette cellule à
ce seul élément puisque plusieurs couples peuvent entrer en compétition pour l’occupation de
l’emplacement. Une cellule représente donc un sous-ensemble de l’ensemble initial. Pour les
couples du tableau 7.4, page 294, pour un tableau de hachage défini sur 0 .. 2 × 0 .. 2 et pour
une fonction de hachage délivrant le composant (x ou y) de la clé modulo 3, nous obtenons le
tableau ci-dessous (x verticalement, y horizontalement) :

0 1 2
0 {(15,3)} {(3,10),(15,13)} {(9,14),(12,5),(21,8)}
1 {(7,5),(7,11)}
2 {(17,5)} {(20,11)}

Ainsi, par exemple, le couple (3, 10) donne (0, 1) par un calcul modulo 3. (0, 1) constitue les
coordonnées de la cellule destinée à recevoir le couple (3, 10). Cette représentation convient
bien pour toutes les opérations du type abstrait de la figure 7.4. Fournir une spécification
concrète pour cette mise en œuvre. Calculer les différentes opérations. Cette solution exige
cependant d’être raffinée puisque chaque entrée du tableau est un ensemble ecAbst. Raffiner
cette solution en utilisant un 2d-arbre afin d’obtenir une solution implantable.
Chapitre 8

Files simples

Dans ce chapitre nous étudions les files simples. Nous avons décidé de
partir d’une spécification abstraite fondée sur une liste. Par contre nous avons
écarté la mise en œuvre classique sous forme de tableau. Sa nature peu fonction-
nelle complexifie les calculs. Nous avons retenu la solution par « double liste » (ou
double pile si l’on préfère) qui, dans notre contexte, présente plusieurs avantages.
C’est une structure fonctionnelle qui illustre de manière simple et convaincante
la notion de complexité amortie ; en outre, le résultat se caractérise à la fois
par son originalité et son élégance. Nous supposons le lecteur familiarisé avec
la notion de pile ainsi qu’avec ses deux principales mises en œuvre (contiguë et
chaînée). Ci-dessous une liste est considérée comme une représentation concrète
correcte d’une pile.

8.1 Présentation informelle des files simples


Dans cette section nous considérons les files 1 Fifo (First In First Out :
premier entré/premier sorti). Les éléments sont pris dans un ensemble T (dont le
support est t) qui constitue le paramètre du type fileabst. Les éléments placés
dans une file sont considérés comme des identifiants ; pour une file donnée, ils
sont donc tous différents.
– f V ide(), opération qui délivre une file vide ;
– f Ajout(v, f ), opération qui « insère » l’élément v en queue de la file f ;
– f Supp(f ), opération qui « supprime » l’élément en tête de la file f, selon
la stratégie Fifo. Cette opération est préconditionnée par le fait que la file
f n’est pas vide ;
– f P remier(f ), opération qui délivre (sans le supprimer) l’élément en tête
de la file f, déterminé par la stratégie Fifo. Cette opération est précondi-
tionnée par le fait que la file n’est pas vide ;
– f EstV ide(f ), opération qui délivre true si et seulement si la file f est
vide.
1. Nous ne nous intéressons pas ici à la théorie des files d’attente qui a elle comme objectif
de quantifier le temps d’attente en fonction de la loi d’arrivée des entrants.
314 Structures de données et méthodes formelles

Spécifier une file d’attente en utilisant la théorie des ensembles peut se faire
de plusieurs façons. L’une d’elle consiste à considérer qu’une file est une fonc-
tion totale d’un intervalle quelconque de Z dans T. L’intervalle constituant
le domaine de définition ne joue alors aucun rôle particulier (seule sa lon-
gueur est significative) et, si nous supposons que T = N, l’ensemble suivant :
{−2 → 3, −1 → 12, 0 → 17, 1 → 1} représente la même file que l’ensemble
{153 → 3, 154 → 12, 155 → 17, 156 → 1}.
Cette solution est séduisante dans la mesure où elle ne conduit pas à sur-
spécifier le concept de file en imposant un intervalle particulier. Par contre, par
rapport à notre approche, elle introduit un inconvénient quasi rédhibitoire qui
est que lors du raffinement, l’association entre le support concret et le support
abstrait est de nature relationnelle et non pas fonctionnelle, puisqu’à chaque file
concrète correspond une infinité de files abstraites.
Pour contourner cette difficulté (imputable à notre modèle de raffinement),
il est possible d’envisager une solution où le domaine de définition de la file
abstraite débute en une position fixe, par exemple 1.
Si l’on considère la file suivante :
1 → 3
2  → 12
3  → 17
4  → 1

représentée en notation ensembliste par la fonction {1 → 3, 2 → 12, 3 → 17, 4 →


1}, l’adjonction de la valeur v à la file se fait par l’adjonction à la fonction du
couple (5, v), tandis que la suppression de la file se fait en supprimant le couple
ayant 1 comme première composante, puis en décalant les autres valeurs « vers
les indices bas », de façon à préserver la propriété qui veut que le domaine de
définition débute en 1.
1 → 3 1 → 3
1 → 12 suppression 2  → 12 ajout de 4 2 → 12
2 → 17 ←− 3 → 17 −→ 3  → 17
3 → 1 4 → 1 4  → 1
5  → 4

Une autre solution consiste à spécifier les files simples à partir des listes telles
que définies à la section 3.1, page 78. C’est cette solution que nous développons
ici.

8.2 Spécification du type abstrait fileabst


Le type abstrait fileabst se définit à partir du type liste de la section 3.1.
Il hérite des opérations  et ˜, qui deviennent des opérations auxiliaires, et du
support, qui cependant s’enrichit du fait qu’ici il n’existe pas de doublon, puisque
les éléments d’une file sont des identifiants. Spécifier cette caractéristique exige de
spécifier et d’utiliser la fonction ens qui délivre l’ensemble des éléments présents
dans la file. Nous avons donc la définition suivante pour le support de fileabst :
8. Files simples 315

1) [ ] ∈ f ileAbst(T )
2) v ∈ t ∧ l ∈ f ileAbst(T ) ∧ v ∈
/ ens(l) ⇒ [v | l] ∈ f ileAbst(T )

La figure 8.1, page suivante, (complétée par la figure 8.2) représente la spéci-
fication du type abstrait fileabst. L’opération f Ajout(v, f ) exige une précon-
dition. Celle-ci stipule que l’élément ajouté n’est pas déjà présent dans la file.
Pour spécifier cette précondition, nous utilisons la fonction auxiliaire, ens, déjà
introduite ci-dessus. Cette fonction n’a pas besoin d’être implantée 2 puisqu’elle
n’est utilisée que dans la partie précondition. L’ajout se fait « en queue », ceci
s’obtient en affirmant que la file résultante est l’inverse de la file obtenue en
insérant la valeur v dans l’inverse de la file f initiale.
Remarquons que si nous disposions de tableaux fortement flexibles (cf. cha-
pitre 10), nous pourrions utiliser directement ce type de structure de données
pour mettre en œuvre les files simples.

8.3 Mise en œuvre « chaînée »


Dans la spécification de la figure 8.1, page suivante, l’élément en tête de liste
se superpose à l’élément en tête de file, tandis que l’élément de queue de la file
est le « dernier » élément de la liste. Une implantation fidèle à la spécification
présenterait l’inconvénient d’exiger que l’insertion parcoure toute la liste à la
recherche de son extrémité finale avant de procéder à l’insertion proprement
dite. La complexité temporelle qui en résulterait pour l’opération d’adjonction
serait rédhibitoire. Il est nécessaire de rechercher une solution plus efficace.
Un autre type de solution « chaînée » existe, décrit dans la plupart des ou-
vrages dédiés aux structures de données. Il se caractérise par le fait que l’élément
de queue (s’il existe) est identifié et accessible (« pointé ») directement. Son ef-
ficacité est remarquable (toutes les opérations sont en O(1)) mais son caractère
« peu fonctionnel » le rend difficile à calculer à partir de la spécification.
Une troisième solution constitue un bon compromis entre facilité de dériva-
tion et efficacité. C’est la solution qui consiste à représenter la file par un couple
de listes 3 similaires à celles utilisées pour la spécification du type fileabst. La
première liste, la « liste de tête », représente le début de la file, la seconde, la
« liste de queue », la fin. Cependant cette seconde liste est inversée de façon à
effectuer l’insertion en queue de file par une insertion en tête de liste. La sup-
pression comme l’insertion se font donc toujours en tête d’une liste, c’est a priori
un gage d’efficacité. Lors d’une suppression, quand la « liste de tête » est vide,
elle est remplacée par l’inverse de la « liste de queue », celle-ci devenant elle-
même vide. Nous verrons que, bien que la complexité la pire de l’opération de
suppression (f Supp_dl) soit importante, la complexité amortie (cf. page 123)
est excellente. L’exemple ci-dessous montre l’évolution d’une file à la fois dans sa
représentation concrète par une double liste et dans sa représentation abstraite.

2. Son implantation pourrait cependant se révéler nécessaire dans l’utilisation du type


considéré.
3. Qui, compte tenu de leur usage, se comportent en piles.
316 Structures de données et méthodes formelles

abstractType fileabst(T ) =  (f ileAbst, (f V ide, f Ajout, f Supp),


(f P remier, f EstV ide))
uses bool
support
1) [ ] ∈ f ileAbst(T )
2) v ∈ t ∧ l ∈ f ileAbst(T ) ∧ v ∈/ ens(l) ⇒ [v | l] ∈ f ileAbst(T )
operations
function f V ide() ∈ f ileAbst(T ) = []
;
function f Ajout(v, f ) ∈ t × f ileAbst(T ) →  f ileAbst(T ) =
pre
v∈ / ens(f )
then
[v | f ˜]˜
end
;
function f Supp(f ) ∈ f ileAbst(T )   f ileAbst(T ) = 
pre
f = [ ]
then
let [t | q] := f in q end
end
;
function f P remier(f ) ∈ f ileAbst(T ) →  T = 
pre
f = [ ]
then
let [h | q] := f in h end
end
;
function f EstV ide(f ) ∈ f ileAbst(T )  bool =  bool(f = [ ])
auxiliaryOperationRepresentations
function ens(f ) ∈ f ileAbst(T )  F(T ) =  ···
;
function l1  l2 ∈ f ileAbst(T ) × f ileAbst(T )  f ileAbst(T ) =  ···
;
function l ˜ ∈ f ileAbst(T )   f ileAbst(T ) =  ···
end

Figure 8.1 – Spécification du type abstrait fileabst – partie 1.


La file abstraite est définie par une liste simple. Cette solution doit être
raffinée car les ajouts ont un coût rédhibitoire : ils exigent le parcours
complet de la liste.
8. Files simples 317

 ···
abstractType fileabst(T ) =
..
.
auxiliaryOperationRepresentations
function ens(f ) ∈ f ileAbst(T )  F(T )
if f = [ ] →

| f = [h | q] →
ens(q) ∪ {h}

;
function l1  l2 ∈ f ileAbst(T ) × f ileAbst(T )  f ileAbst(T )
pre
ens(l1) ∩ ens(l2) = ∅
then
if l1 = [ ] →
l2
| l1 = [h | q] →
[h | q  l2]

end
;
function l ˜ ∈ f ileAbst(T ) 
 f ileAbst(T )
if l = [ ] →
[]
| l = [h | q] 
q ˜[h | [ ]]

end

Figure 8.2 – Spécification du type abstrait fileabst – partie 2.


Représentation des opérations auxiliaires. L’opération ens(f ) délivre
l’ensemble des valeurs présentes dans la file abstraite f . L’opération
infixée  délivre la concaténation de deux listes et l’opération postfixée ˜
inverse la liste en argument.

Opérations Représentation concrète File abstraite


([4,8],[7,3]) → [4,8,3,7]
Insertion de 2
([4,8],[2,7,3]) → [4,8,3,7,2]
Suppression
([8],[2,7,3]) → [8,3,7,2]
Suppression
([ ],[2,7,3]) → [3,7,2]
Suppression
([3,7,2],[ ])
([7,2],[ ]) → [7,2]
318 Structures de données et méthodes formelles

Nous notons en particulier, dans la cas de la dernière suppression, que cette


opération est réalisée en deux phases, la première consistant à inverser la liste
de queue dans la liste de tête et la seconde à effectuer la suppression proprement
dite.

8.3.1 Définition du support concret


Le support concret est constitué d’un couple de listes. Nous allons pour cela
utiliser le type fileabst spécifié à la figure 8.1, page 316, en tant que type
auxiliaire nécessaire à la définition du type filedl (file double liste) et de son
support f ileDL 4 . Nous obtenons :

a ∈ f ileAbst(T ) ∧ b ∈ f ileAbst(T ) ∧ ens(a) ∩ ens(b) = ∅



(a, b) ∈ f ileDL(T )

Notons que cette définition exige que les éléments présents dans une telle file
concrète soient tous différents : c’est déjà le cas pour chaque file a et b, ça l’est
également pour ces deux files prises dans leur ensemble en raison de la propriété
ens(a) ∩ ens(b) = ∅ qui est requise par la définition du support.

8.3.2 Définition de la fonction d’abstraction


La fonction d’abstraction A capte le fait que la file f ileDL qui correspond
à la double liste est la concaténation de la liste de tête et de l’inverse de la liste
de queue, soit :

function A((a, b)) ∈ f ileDL(T )  f ileAbst(T ) =


 a  b˜

8.3.3 Spécification formelle des opérations


Le nom des opérations concrètes dérive du nom des opérations abstraites
en le suffixant par _dl. La figure 8.3, page 320, récapitule les informations qui
constituent le type filedl. Nous remarquons que le nom du type abstrait, fi-
leabst, apparaît à la fois comme type raffiné (rubrique refines) et comme type
outil (rubrique uses). Ceci matérialise la remarque formulée lors de la définition
du support concret.

8.3.4 Calcul de la représentation des opérations concrètes


Nous restreignons notre développement au calcul des trois opérations
f V ide_dl, f Ajout_dl et f Supp_dl. Le cas de l’opération f P remier_dl est
très proche de celui de l’opération f Supp_dl, tandis que celui de l’opération
f EstV ide_dl se résout facilement. La dérivation de ces deux dernières opéra-
tions est laissée en exercice.

4. Le fait que le type abstrait soit utilisé comme type auxiliaire dans la spécification de la
mise en œuvre constitue un cas très particulier qui ne doit pas entraîner de confusions.
8. Files simples 319

Calcul d’une représentation de l’opération f V ide_dl


A(f V ide_dl())
= Propriété caractéristique de l’opération f V ide_dl()
[]
= [ ] est élément neutre de la concaténation
[ ][ ]
= Propriété de l’opération ˜
[ ]  [ ]˜
= Définition de A
A(([ ], [ ]))
D’où, d’après la propriété de l’équation à membres identiques (page 67), la re-
présentation suivante de l’opération f V ide_dl() :

function f V ide_dl() ∈ f ileDL(T ) =


 ([ ], [ ])

Calcul d’une représentation de l’opération f Ajout_dl


La traduction de la précondition est triviale, elle fournit le prédicat v ∈
/
ens(a) ∧ v ∈
/ ens(b). Le calcul de la représentation proprement dite se présente
de la manière suivante :
A(f Ajout_dl(v, (a, b)))
= Propriété caractéristique de l’opération f Ajout_dl
[v | (a  b˜)˜]˜
= Propriété 3.1.4, page 81
[v | b˜˜  a˜]˜
= Propriété 3.1.8, page 81 (involution de ˜)
[v | b  a˜]˜
= Définition de la concaténation (cf. figure 8.2, page 317)
([v | b]  a˜)˜
= Propriété 3.1.4, page 81
a˜˜[v | b]˜
= Propriété 3.1.8, page 81 (involution de ˜)
a  [v | b]˜
= Définition de A
A((a, [v | b]))
D’où, d’après la propriété de l’équation à membres identiques (page 67), la
représentation suivante de l’opération f Ajout_dl :

function f Ajout_dl(v, (a, b)) ∈ T × f ileDL(T ) →


 f ileDL(T ) =

pre
v∈/ ens(a) ∧ v ∈
/ ens(b)
then
(a, [v | b])
end

L’ajout se fait donc toujours en tête de la liste de queue.


320 Structures de données et méthodes formelles

concreteType
filedl(T ) =  (f ileDL, (f V ide_dl, f Ajout_dl, f Supp_dl),
(f P remier_dl, f EstV ide_dl))
uses fileabst(T ), bool
refines fileabst(T )
support
a ∈ f ileAbst(T ) ∧ b ∈ f ileAbst(T ) ∧ ens(a) ∩ ens(b) = ∅

(a, b) ∈ f ileDL(T )
abstractionFunction
function A((a, b)) ∈ f ileDL(T )  f ileAbst(T ) =  a  b˜
operationSpecifications
function f V ide_dl() ∈ f ileDL(T ) = 
q : (q ∈ f ileDL(T ) ∧ A(q) = f V ide())
;
function f Ajout_dl(v, (a, b)) ∈ t × f ileDL(T ) →  f ileDL(T ) =

pre
v∈ / ens(A((a, b)))
then
q : (q ∈ f ileDL(T ) ∧ A(q) = f Ajout(v, A((a, b))))
end
;
function f Supp_dl((a, b)) ∈ f ileDL(T )   f ileDL(T ) =  ...
pre
A((a, b)) = [ ]
then
q : (q ∈ f ileDL(T ) ∧ A(q) = f Supp(A((a, b))))
end
;
function f P remier_dl((a, b)) ∈ f ileDL(T ) →  T =  ...
;
function f EstV ide_dl((a, b)) ∈ f ileDL(T )  bool =  ...
end

Figure 8.3 – Spécification du type concret filedl.


Une file concrète est définie par une double liste, la première sert pour
les suppressions et la seconde pour les ajouts. Quand la file de suppres-
sion est vide, on y inverse la file des ajouts.

Calcul d’une représentation de l’opération f Supp_dl


La précondition doit être transformée :

A((a, b)) = [ ]
= Définition de A
a  b˜ = [ ]
8. Files simples 321

= Propriété 3.1.1, page 81 et spécification de l’opération ˜


a = [ ] ∨ b = [ ]

Le calcul de la représentation proprement dite se présente comme suit :

A(f Supp_dl((a, b)))


= Propriété caractéristique de l’opération f Supp_dl
f Supp(a  b˜) (8.3.1)

D’après la précondition a = [ ] ∨ b = [ ], soit a = [ ] et b = [ ] soit a = [ ].


Effectuons une analyse par cas en débutant par a = [ ]. Posons a = [h | q].

f Supp(a  b˜)
= Hypothèse (a = [t | q])
f Supp([h | q]  b˜)
= Définition de la concaténation (cf. figure 8.2, page 317)
f Supp([h | q  b˜])
= Définition de l’opération f Supp
q  b˜
= Définition de A
A((q, b))

D’où, d’après la propriété de l’équation à membres identiques (page 67), l’équa-


tion gardée suivante :

a = [h | q] →
f Supp_dl((a, b)) = (q, b)

Intéressons-nous maintenant au second cas, a = [ ] et b = [ ]. Nous repartons


de la formule 8.3.1 ci-dessus :

f Supp(a  b˜)
= Hypothèse (a = [ ])
f Supp([ ]  b˜)
= [ ] est élément neutre à gauche, et propriété 3.1.1, page 81
f Supp(b˜[ ])
= Définition de ˜
f Supp(b˜[ ]˜)
= Définition de A
f Supp(A((b˜, [ ])))
= Propriété caractéristique de l’opération f Supp_dl
A(f Supp_dl((b˜, [ ])))

D’où, d’après la propriété de l’équation à membres identiques (page 67), la se-


conde équation gardée :

a=[] →
f Supp_dl((a, b)) = f Supp_dl((b˜, [ ]))
322 Structures de données et méthodes formelles

Cette équation gardée s’interprète en disant que pour supprimer un élément


dans une file dont le premier composant est une liste vide, il suffit de supprimer
un élément dans une file dont le premier composant est l’inverse du second
composant et le second élément est une liste vide.
Au total, nous obtenons la représentation suivante pour la fonction
f Supp_dl :

function f Supp_dl((a, b)) ∈ f ileDL(T ) →


 f ileDL(T ) =

pre
¬(a = [ ] ∧ b = [ ])
then
if a = [h | q] →
(q, b)
|a=[] →
f Supp_dl((b˜, [ ]))

end

8.3.5 Complexité de la mise en œuvre chaînée


La mise en œuvre par double liste de la structure de données abstraite « file
simple » se prête particulièrement bien à l’analyse amortie de sa complexité, tant
sur le plan de l’intérêt de la démarche que de son principe (cf. page 123). Il est
en effet facile de comprendre que l’inversion de la liste de queue par l’opération
f Supp_dl n’est pas systématique mais, quand elle est réalisée, constitue un
investissement vis-à-vis des appels à venir de f Supp_dl.
Nous proposons de choisir comme fonction de potentiel la fonction Φ de
profil :
Φ ∈ f ileDL → R+
et telle que :
Φ((a, b)) = #(b)
Φ((a, b)) représente donc le nombre d’éléments présents dans la seconde liste.
Cette fonction satisfait les conditions requises et en particulier Φ(([ ], [ ])) = 0.
Sachant qu’une insertion dans la file se limite à une insertion en tête
de la première liste, il est possible de considérer que le coût réel de cette
opération est de 1 : T (f Ajout_dli (v, (a, b))) = 1. La complexité amortie
M(f Ajout_dli (v, (a, b))) de l’opération f Ajout_dli ((a, b)) par rapport à Φ est
alors :

M(f Ajout_dli (v, (a, b)))


= Définition de la complexité amortie (page 123)
T (f Ajout_dli (v, (a, b))) + Φ(f Ajout_dli (v, (a, b))) − Φ((a, b))
= Représentation de f Ajout_dl
T (f Ajout_dli ((a, b))) + Φ((a, [v | b])) − Φ((a, b))
= Coût réel d’une insertion et définition de Φ
8. Files simples 323

1 + #(b) + 1 − #(b)
= Arithmétique
2

Donc M(f Ajout_dli (v, (a, b))) est en O(1) pour la fonction de potentiel Φ consi-
dérée.
Pour l’analyse amortie de la complexité de l’opération f Supp_dl((a, b)), il
est nécessaire de considérer les deux situations types, conformément à ce que
nous apprend l’algorithme : a = [ ] et a = [ ]. Prenons tout d’abord en compte
le cas a = [ ] et posons a = [h | q]. Dans ce cas, la suppression dans la file se
limite à une suppression en tête de la première liste. Nous pouvons considérer
que T (f Supp_dli ((a, b)) = 1.

M(f Supp_dli ((a, b)))


= Définition du coût amorti pour l’hypothèse considérée
T (f Supp_dli ((a, b))) + Φ(f Supp_dli ((a, b))) − Φ(([h | q], b))
= Représentation de f Supp_dl
T (f Supp_dli ((a, b))) + Φ((q, b)) − Φ(([h | q], b))
= Définition de la fonction de potentiel Φ et coût réel
1 + #(b) − #(b)
= Arithmétique
1

Dans ce cas M(f Supp_dli ((a, b)) est en O(1). Si a = [ ], nous savons, d’après
l’algorithme, que f Supp_dl(([ ], b)) = f Supp_dl((b˜, [ ])). Posons b˜ = [h | q] :

M(f Supp_dli ((a, b)))


= Définition du coût amorti pour l’hypothèse considérée
T (f Supp_dli ((a, b))) + Φ(f Supp_dli ((a, b))) − Φ(([ ], b))
= Représentation de f Supp_dl
T (f Supp_dli ((a, b))) + Φ((q, [ ])) − Φ(([ ], b))

Le coût réel de l’opération est celui d’une inversion (#(b)) plus celui de la sup-
pression proprement dite (soit 1).

T (f Supp_dli ((a, b))) + Φ((q, [ ])) − Φ(([ ], b))


= D’après la remarque ci-dessus
#(b) + 1 + Φ((q, [ ])) − Φ(([ ], b))
= Définition de la fonction de potentiel Φ
#(b) + 1 + #([ ])) − #(b)
= Arithmétique
#(b) + 1 + 0 − #((b)
= Arithmétique
1

Nous tombons à nouveau sur une complexité amortie en O(1) par rapport à la
fonction Φ choisie. Le coût amorti des deux opérations étudiées est en O(1). On
atteindrait le même résultat pour les autres opérations non développées ici.
324 Structures de données et méthodes formelles

Persistance et analyse amortie : les frères ennemis


L’utilisation effective de la persistance conduit en général à invalider les
résultats acquis par une analyse amortie. Pour le comprendre considérons
l’exemple de la file simple mise en œuvre par double liste. Soit b une liste
de n éléments et soit l’opération f Supp_dl(([ ], b)), dont la complexité
amortie est en O(1). Supposons que nous réalisions n appels consécutifs
à cette opération. En interprétant le code de l’opération f Supp_dl, il est
facile de constater que nous allons réaliser n fois la même inversion de la
liste b, ce qui au total nous coûtera n2 opérations élémentaires (et non n
comme le laisse penser l’analyse amortie).
Où est l’erreur ? Elle se trouve dans le fait que nous avons considéré
que la structure ([ ], b) est persistante et donc disponible à volonté. Le pa-
radigme fonctionnel pur interdit un tel usage. L’argument d’un appel doit
être le résultat d’un autre appel. Au contraire, une expression telle que
f Supp_dl(. . . f Supp_dl(([ ], b)) . . .), qui évite d’exploiter la persistance,
fournit un résultat compatible avec la complexité amortie. L’ouvrage de
C. Okasaki [93] développe plus largement ce point. Moralité : la persis-
tance est à proscrire dans un contexte d’analyse amortie.

8.3.6 Conclusion et remarques bibliographiques


La solution par double liste est une excellente représentation. Son inconvé-
nient principal est que le coût des opérations est très variable selon les appels.
Une utilisation dans un contexte de temps réel est donc à éviter.
Cette solution a probablement été découverte à l’Université Cornell par
R. Hood et R. Melville [59] en 1980, publiée en 1981, puis reprise par D. Gries
dans le célèbre ouvrage [45], page 250 (cf. également [93], page 16). Une solution
légèrement différente est proposée par F.W. Burton [20] en 1982.

Exercices

Exercice 8.3.1 Dans la représentation par double liste d’une file d’attente, calculer la repré-
sentation de l’opération f P remier_dl. Quel est son coût amorti vis-à-vis de la fonction de
potentiel Φ utilisée à la section 8.3.5 ?

Exercice 8.3.2 (Tourniquet) Spécifier une « file à temps de service » telle que chaque
client qui s’introduit dans la file le fait en possession de n (n > 0) jetons. Lorsqu’un client se
présente devant le « guichet » il consomme l’un des jetons et est réintroduit dans la file si son
quota de jetons est encore positif, sinon il quitte la file. Proposer un raffinement par double
liste.

Exercice 8.3.3 (File amicale) Une file amicale est une file d’attente Fifo dans laquelle,
lorsqu’un client se présente, il transmet sa « liste d’achats » au client qui le précède dans la
file à condition que ce dernier soit un « ami ». Le client disparaît alors de la file, son « ami »
effectuera ses achats à sa place. Deux clients sont « amis » s’ils sont représentés par le même
identificateur, la « liste d’achats » est représentée par un entier naturel positif, la transmission
8. Files simples 325

d’une « liste d’achats » se traduit par une addition d’entiers. Spécifier les files amicales. Raffiner
par une méthode de votre choix.

Exercice 8.3.4 fileabst, le type abstrait défini ci-dessus pour les files d’attente Fifo, impose
l’absence de doublon dans une file. S’assurer que l’opération f Ajout n’est invoquée que si sa
précondition est satisfaite peut exiger d’enrichir le type abstrait d’une opération f Existe(v, f )
qui délivre true si et seulement si v apparaît dans la file f . Spécifier cette opération. Calculer
sa représentation dans le cas d’un raffinement par double liste.
Chapitre 9

Files de priorité

9.1 Présentation informelle


Une file de priorité est une structure de données qui gère les adjonctions
et les suppressions selon la discipline suivante : lors d’une arrivée, le client se
présente avec une valeur représentant une priorité (définie sur un ensemble doté
d’une relation d’ordre total tel que les entiers naturels comme c’est le cas ici) ;
lors d’une suppression, l’élément qui a la plus forte priorité est supprimé de la
file. Par convention ici, plus l’élément est petit plus la priorité est grande. En
cas de conflit lors d’une suppression (cas où plusieurs candidats ont la même
plus forte priorité), le choix de l’élu n’est pas précisé par la spécification. Les
opérations suivantes sont définies :
– f pV ide(), opération qui délivre une file de priorité vide ;
– f pAjout(v, f ), opération d’adjonction de la priorité v qui délivre une file
de priorité résultant de l’ajout de la priorité v dans la file f ;
– f pP rio(f ), opération qui délivre (sans la supprimer de la file f ) la valeur
prioritaire. Cette opération est préconditionnée par le fait que f ne soit
pas vide ;
– f pSupp(f ), opération qui délivre une file de priorité identique à la file f à
l’exception d’une occurrence de la valeur prioritaire de f qui est supprimée.
Cette opération est préconditionnée par le fait que f ne soit pas vide ;
– f pEstV ide(f ), opération qui délivre la valeur true si et seulement si la
file f ne contient aucun élément.
Lorsque l’opération suivante est définie, nous qualifions la structure de données
de file de priorité fusionnable :
– f pF us(f, f  ), opération qui, à partir des deux files de priorité f et f  ,
délivre une file représentant l’addition multiensembliste () des éléments
de f et de f  .

Les files de priorité sont utilisées dans des applications telles que :
– la gestion de processus dans un système d’exploitation ;
– la gestion de mémoire dans un système d’exploitation (afin de permettre
de sélectionner l’emplacement libre le plus adapté à la taille requise) ;
328 Structures de données et méthodes formelles

– la simulation ;
– la compression de fichiers (par exemple par codage de Huffman, cf. encadré
page 359) ;
– la recherche dans les graphes (l’algorithme de Prim pour la recherche
d’arbres recouvrants ou l’algorithme de Dijkstra pour la recherche de plus
courts chemins) ;
– le tri (pour trier un sac de valeurs il suffit d’itérer l’opération f pAjout pour
introduire les valeurs dans la file puis d’itérer la séquence f pP rio; f pSupp
pour obtenir toutes les valeurs de la file dans l’ordre croissant) ;
– la gestion de messages dans les réseaux ;
– le filtrage bayésien de spams ;
– la géométrie algorithmique (la recherche d’intersections) ;
– etc.
Les représentations naïves des files de priorité de n éléments ont, quelle que
soit la technique employée, au moins une opération de base (différente de la fu-
sion) qui est en O(n). Nous allons chercher à améliorer cette situation. Plusieurs
techniques s’offrent à nous parmi lesquelles les trois suivantes.
– Les minimiers parfaits binaires (tas), solution qui est bonne pour les opé-
rations de base mais pas pour la fusion. Pour permettre une fusion efficace,
il faut disposer d’une structure de données moins contrainte, au risque de
perdre en efficacité sur une ou plusieurs opérations.
– Les minimiers binomiaux (la structure de données concrète est une file bi-
nomiale, triée sur les rayons décroissants). Cette représentation a un bon
comportement pour toutes les opérations, y compris la fusion. Cependant,
les opérations f pP rio (recherche de la valeur de l’élément prioritaire) et
f pSupp (suppression de l’élément prioritaire) sont légèrement moins effi-
caces que dans la solution par tas.
– Les minimiers obliques (skew heaps). Cette solution est simple à mettre en
œuvre et possède un bon comportement amorti.
Dans le cas où les opérations de fusion sont exceptionnelles ou réalisées sur
des files de petites tailles, une solution par tas est suffisante.

9.2 Spécification du type abstrait fpabst


Fondamentalement, une file de priorité est un sac (un multiensemble) d’en-
tiers naturels doté de deux opérations particulières par rapport aux sacs : la
suppression d’une occurrence du plus petit élément du sac (suppression d’un
élément prioritaire de la file) et la recherche d’une occurrence du plus petit élé-
ment (recherche d’un élément prioritaire de la file). Le support abstrait est donc
sac(N), ensemble des sacs finis de N. Le type fpabst est décrit à la figure 9.1.
Les notations . . ., , , −̇ et bMin représentent respectivement la définition
en extension d’un sac, un sac vide, l’addition multiensembliste, la différence mul-
tiensembliste et la fonction qui délivre le plus petit élément d’un sac de relatifs.
Ces notations et toutes celles concernant les sacs sont introduites au chapitre 3,
section 3.7.
9. Files de priorité 329

abstractType fpabst =  (f pAbst, (f pV ide, f pAjout, f pSupp, f pF us),


(f pP rio, f pEstV ide))
uses
bool, N
support
f ∈ sac(N) ⇔ f ∈ f pAbst
operations
function f pV ide() ∈ f pAbst = 
;
function f pAjout(v, f ) ∈ N × f pAbst → f pAbst =  v  f
;
function f pSupp(f ) ∈ f pAbst   f pAbst = 
pre
f = 
then
f −̇ bMin(f )
end
;
function f pF us(f, f  ) ∈ f pAbst × f pAbst  f pAbst =  f  f
;
function f pP rio(f ) ∈ f pAbst   N= 
pre
f = 
then
bMin(f )
end
;
function f pEstV ide(f ) ∈ f pAbst  bool =  bool(f = )
end

Figure 9.1 – Spécification du type abstrait file de priorité.


Ce type, dénommé fpabst, comprend six opérations : 1) f pV ide qui
délivre une file vide, 2) f pAjout qui ajoute un élément dans une file,
3) f pSupp qui supprime l’élément prioritaire d’une file, 4) f pF us qui
fusionne deux files en une seule, 5) f pP rio qui délivre l’élément prio-
ritaire sans altérer la file et 6) f pEstV ide qui délivre true si la file
est vide.

9.3 Méthodes équilibrées : les tas


9.3.1 Principe
Un minimier binaire (cf. section 3.5.2, page 89) est un arbre binaire étiqueté
par des valeurs prises dans un ensemble doté d’une structure d’ordre total et tel
que, pour tout nœud, son étiquette est inférieure ou égale aux valeurs situées
dans les sous-arbres gauche et droit. L’arbre suivant est un minimier sur N :
330 Structures de données et méthodes formelles

2
3 2
5 12
18 8 15

Rappelons par ailleurs qu’un arbre binaire parfait à gauche (cf. chapitre 3,
section 3.5) est un arbre tel que toutes les feuilles sont situées sur le dernier
niveau ou sur les deux derniers niveaux de l’arbre et que les feuilles du dernier
niveau sont toutes situées sur la gauche de l’arbre. Les deux arbres ci-dessous
sont des arbres binaires parfaits à gauche :

• •
• • • •
• • • • • • • •
• • • •• • • • • • •

Le mariage entre minimier binaire et arbre binaire parfait à gauche s’appelle


un tas 1 (à gauche). La figure 9.2 présente trois exemples de tas (à gauche).

5 (1) 2 (2) 3 (3)

6 8 4 7 10 6
7 6 10 9 5 5 9 8 12 11 8 9
8 10 14 21 12 14 9

Figure 9.2 – Trois tas à gauche.


Dans le tas (1), toutes les feuilles sont situées au dernier niveau. Par
contre, dans les tas (2) et (3), les feuilles sont situées sur les deux
derniers niveaux. On peut remarquer que les feuilles du dernier niveau
sont calées sur la gauche. Le tas (3) possède un point simple (étiqueté
par la valeur 8), les tas (1) et (2) n’en ont pas.

Une telle structure se prête bien à la représentation des files de priorité (non
fusionnables). En effet :
– la recherche de l’élément prioritaire (opération abstraite f pP rio) est tri-
viale, cet élément est à la racine :
– la suppression de l’élément prioritaire (opération abstraite f pSupp) con-
siste à supprimer la racine puis à rétablir la structure de tas ;
1. Le terme tas est très surchargé dans le vocabulaire informatique. Selon les auteurs, il
peut désigner aussi bien un minimier, un maximier que la zone de mémoire où sont alloués les
structures dynamiques ou les objets. Pour ce qui nous concerne, nous réservons ce terme aux
minimiers parfaits binaires.
9. Files de priorité 331

– l’adjonction d’un élément (opération abstraite f pAjout) consiste à insérer


cet élément en tant que feuille puis à le remonter vers la racine jusqu’à ce
que la structure de tas soit rétablie.
Intuitivement, ces opérations exigent, pour être efficaces, d’accéder facile-
ment :
– à la racine du tas ;
– au père d’un nœud afin d’effectuer une insertion ascendante lors d’une
adjonction ;
– aux sous-arbres d’un nœud afin d’effectuer une insertion descendante lors
d’une suppression.
La structure ainsi caractérisée n’est pas une structure fonctionnelle : elle
exige des pointeurs vers le père et les sous-arbres gauche et droit. Par contre, il
est possible de mettre en œuvre un tas par un tableau.
Pour ce qui est de l’efficacité des opérations, nous pouvons prédire un bon
comportement dans la mesure où le rayon d’un tas de poids n est log n + 1
et où les opérations d’adjonction et de suppression n’exigent rien d’autre que le
parcours (partiel ou complet) du chemin joignant une feuille à la racine.

9.3.2 Conclusion et remarques bibliographiques


La structure de données tas a été proposée initialement par J.W.J. Williams
en 1964 en tant que support pour un algorithme de tri. Son utilisation en tant
que support pour des files de priorité est plus récente. Cette structure de données
se prête particulièrement bien à un double raffinement : le premier conduisant de
la structure abstraite aux minimiers parfaits et le second des minimiers parfaits
aux tableaux représentant des tas.
La représentation des minimiers par tas est une solution très efficace quand
les files sont petites ou quand il s’agit de files non fusionnables. Cependant, elle
présente dans certaines situations un inconvénient qui peut être rédhibitoire : il
s’agit d’une représentation implicite (les pointeurs ne sont pas matérialisés). Il
peut être difficile, voire impossible, d’implanter cette structure de données au
sein d’une structure de données préexistante. C’est par exemple le cas si l’on
veut implanter une file de priorité pour gérer une mémoire : il faut distribuer les
constituants de la file de priorité dans les différents « trous » présents dans la
mémoire. Ces trous ne sont pas contigus et la solution « tas » doit être écartée.

Exercice

Exercice 9.3.1 Fournir une spécification concrète des files de priorité (non fusionnables) par
tas. Calculer les différentes opérations.

9.4 Méthodes équilibrées : les files binomiales


Nous proposons tout d’abord deux définitions alternatives, équivalentes sur
le plan de la spécification, des minimiers binomiaux et des files (de priorité)
332 Structures de données et méthodes formelles

binomiales. La première, abstraite et intuitive, est celle que l’on rencontre com-
munément dans la littérature, tandis que la seconde peut être vue comme un raf-
finement opérationnel de la première. Celle-ci est utilisée dans la suite, lorsqu’il
s’agit de définir formellement le support et de réaliser le calcul des opérations.

Une feuille étiquetée par un entier naturel est un minimier binomial B1 de


rayon 1. Si Bi et Bi sont deux minimiers binomiaux de rayon i, et si la racine
de Bi est inférieure à celle de Bi , alors l’arbre représenté par

Bi

Bi
! "# $
Bi+1

est un minimier binomial Bi+1 de rayon i + 1. Dans les exemples de minimiers


ci-dessous, les rayons des arbres sont, de la gauche vers la droite, 1, 3 et 4.

4 1 4
1 4 5 8 7
5 6 6 12
14

Par contre, les deux arbres ci-dessous ne sont pas des minimiers binomiaux. Le
premier car ce n’est tout simplement pas un minimier, le second parce qu’il est
construit à partir de deux minimiers binomiaux de rayons différents.

4 4
1 1 5 7
5 6 6
14

Une file binomiale est une liste (finie) de minimiers binomiaux triée sur les
rayons décroissants. Par définition, le rayon i d’une file binomiale est le rayon
de son plus grand minimier s’il existe, 0 sinon.

Bk
Bj
Bi
9. Files de priorité 333

Ci-dessous la structure (13) est une file binomiale de longueur 2 et de rayon 3,


la structure (14) est une file de longueur 4 et de rayon 4.

1 2 3 2 7 1
2 3 8 4 7 5 6 5
4 (13) 8 9 5 (14) 9
9

Afin d’harmoniser les représentations des minimiers et des files binomiales,


nous décidons d’adopter la représentation suivante. Un minimier binomial se
construit à partir d’une file binomiale complète (dans laquelle tous les rayons in-
termédiaires sont présents) en enracinant la file sur une valeur inférieure ou égale
à toutes les valeurs de la file. Une conséquence de ces définitions est que les deux
supports correspondants (les files binomiales complètes f BC(n) et les minimiers
binomiaux mB(n), cf. figure 9.5, page 338) se définissent de manière mutuelle-
ment récursive comme le montre le schéma suivant dans lequel un minimier de
rayon i + 1 est construit à partir d’une file complète de rayon i.

B1
B2
Bi−1
Bi

En adoptant cette représentation, la file (14) (qui est une file complète, contrai-
rement à la file (13)) se présente de la manière suivante :

3 2 7 1

8 4 7 5 6 5

8 9 5 9

Cette représentation s’applique aussi bien aux files complètes qu’aux files incom-
plètes.

9.4.1 Définition des supports concrets


Une file de minimiers binomiaux est donc constituée d’une liste, triée sur les
rayons décroissants, de minimiers binomiaux. La définition formelle du support
334 Structures de données et méthodes formelles

se fait donc, à l’instar des listes classiques (cf. section 3.1, page 78), de manière
inductive.

1) [ ] ∈ f B(0)
2) i, j ∈ N1 × N ∧ m ∈ mB(i) ∧ f ∈ f B(j) ∧ i > j

[m | f ] ∈ f B(i)

La clause de base spécifie que la liste vide est une file binomiale de rayon 0. La
clause inductive affirme que si m est un minimier binomial de rayon i, f une file
binomiale de rayon j tel que i > j, alors nous obtenons une file binomiale de
rayon i en plaçant le minimier binomial m en tête de la file.

Définir formellement le support des minimiers binomiaux exige de définir


au préalable la notion de file binomiale complète (f BC). Informellement, une
file binomiale complète de rayon r est une file binomiale dans laquelle il existe
un minimier de rayon j, pour tout j compris entre 1 et r. Une autre façon de
spécifier cette contrainte consiste à affirmer que dans une file binomiale complète,
la longueur, notée #(. . .), est égale au rayon. La formalisation du support f BC
se présente comme suit :

j ∈ N ∧ f ∈ f B(j) ∧ #(f ) = j

f ∈ f BC(j)

La définition de mB(i) s’en déduit :

i ∈ N ∧ f ∈ f BC(i) ∧ a ∈ N ∧ a ≤ bMin(A(f ))

a, f  ∈ mB(i + 1)

Cette définition utilise la fonction d’abstraction A qui, à partir d’une file bi-
nomiale, délivre le sac de ses valeurs. Elle permet d’établir les propriétés du
tableau 9.1.

Tableau 9.1 – Files et minimiers binomiaux : propriétés.


Ce tableau regroupe certaines des propriétés des poids et des longueurs
de files ou de minimiers binomiaux.

Propriété Condition Ident.


w(f ) = 2i − 1 f ∈ f BC(i) (9.4.1)
w(m) = 2i m ∈ mB(i + 1) (9.4.2)
w(f ) ≥ 2i−1 f ∈ f B(i) (9.4.3)
#(f ) = log(w(f ) + 1) f ∈ f BC(i) (9.4.4)
#(f ) ≤ log(w(f ) + 1) f ∈ f B(i) ∧ i > 0 (9.4.5)
9. Files de priorité 335

Le qualificatif de binomial qui accompagne cette structure de données pro-


vient du fait que, dans un arbre binomial de rayon r,le nombre
 de nœuds présents
r−1
au niveau k est donné par la formule du binôme ainsi que l’illustre le
k
schéma ci-dessous.

niveau
 
3
0 1=
0
 
3
1 3=
1
 
3
2 3=
2
 
3
3 1=
3

9.4.2 Définition des fonctions d’abstraction


La fonction d’abstraction A qui, à partir d’une file binomiale, délivre une
file de priorité abstraite, est une fonction totale (toute file binomiale peut se
transformer en file de priorité), surjective (toute file de priorité a (au moins)
une représentation sous la forme d’une file binomiale). Elle n’est pas injective :
une file de priorité possède en général plusieurs représentations différentes sous
la forme de files binomiales. La définition de A utilise la fonction d’abstraction
A associée aux minimiers binomiaux qui délivre également une file de priorité
abstraite.

function A(f ) ∈ f B(i)  f pAbst =



if f = [ ] →

| f = [t | q] →
A (t)  A(q)

Le support f BC(i) étant inclus dans le support f B(i), la fonction d’abstrac-


tion A liée à f B(i) peut, sans réserve, être appliquée au support f BC(i). Cette
propriété est exploitée dans la définition de A qui fait usage de la fonction A.

function A (m) ∈ mB(i)  f pAbst =



let r, g := m in r  A(g) end

À l’instar de A, la fonction A est totale, surjective, mais pas injective. La file


binomiale obtenue à partir d’un minimier binomial est l’addition multiensem-
bliste du sac ayant pour seul élément la racine du minimier et du sac obtenu à
partir de la file binomiale complète qui se trouve sous la racine du minimier.
336 Structures de données et méthodes formelles

9.4.3 Spécification des opérations concrètes


Comme d’habitude, la spécification des opérations concrètes se fait par homo-
morphisme de type dans la rubrique operationSpecifications du type fb(n)
(cf. figures 9.3 et 9.4). Il nous faut cependant rappeler les opérations auxiliaires
ainsi que les opérations portant sur les minimiers binaires (du type mb(n)) (cf. fi-
gure 9.5).

concreteType fb(n) = 
(f B, (f pV ide_f b, f pAjout_f b, f pSupp_f b, f pF us_f b),
(f pP rio_f b, f pEstV ide_f b))
uses bool, mb(i), N
constraints n ∈ N
refines fpabst
support
1) [ ] ∈ f B(0)
2) i, j ∈ N1 × N ∧ m ∈ mB(i) ∧ f ∈ f B(j) ∧ i > j

[m | f ] ∈ f B(i)
abstractionFunction
function A(f ) ∈ f B(i)  f pAbst =  ...
operationSpecifications
function f pV ide_f b() ∈ f B(0) =  ···
; function f pAjout_f b(v, f ) ∈ N × f B(i) → f B(n) =  ···
; function f pSupp_f b(f ) ∈ f B(i)   f B(j) = ···
; function f pF us_f b(f, f  ) ∈ f B(i) × f B(j)  f B(n) =  ···
; function f pP rio_f b(f ) ∈ f B(i)   N=  ···
; function f pEstV ide_f b(f ) ∈ f B(i)  bool =  ···
auxiliaryOperationRepresentations
function #(f ) ∈ f B(i) → N =  ···
; function r(f ) ∈ f B(i) → N =  ···
end

Figure 9.3 – Spécification du type concret file binomiale – partie 1.


Le support « file binomiale », co-défini avec le support « minimier bi-
nomial », permet d’implanter des « files de priorité » ( fpabst). Les
principales opérations de fpabst ainsi que les opérations auxiliaires
sont spécifiées ou définies à la figure 9.4.

Pour le type fb, la fonction # fournit, comme nous l’avons déjà vu, la lon-
gueur de la file. L’opération r fournit le rayon du minimier binomial en tête de
la file ou 0 si la file est vide.
Concernant le type mb(n), l’opération r délivre le rayon de l’arbre en ar-
gument. Si l’arbre se réduit à sa racine, son rayon vaut 1, sinon c’est le rayon
de la file des descendants plus 1. Bien que le type mb soit déclaré en tant que
raffinement du type abstrait fpabst, seules les opérations abstraites f pP rio et
f pF us sont raffinées.
9. Files de priorité 337

concreteType
 ···
fb(n) =
..
.
operationSpecifications
function f pV ide_f b() ∈ f B(0) =  ...
;
function f pAjout_f b(v, f ) ∈ N × f B(i) → f B(n) = 
q : (q ∈ f B(n) ∧ A(q) = f pAjout(v, A(f )))
;
function f pSupp_f b(f ) ∈ f B(i)   f B(j) =

pre
i>0
then
q : (q ∈ f B(j) ∧ A(q) = f pSupp(A(f )))
end
;
function f pF us_f b(f, f  ) ∈ f B(i) × f B(j)  f B(n) =
q : (q ∈ f B(n) ∧ A(q) = f pF us(A(f ), A(f  )))
;
function f pP rio_f b(f ) ∈ f B(i)   N=  ...
;
function f pEstV ide_f b(f ) ∈ f B(i)  bool =  ...
auxiliaryOperationRepresentations
function #(f ) ∈ f B(i) → N = 
if f = [ ] →
0
| f = [t | q] →
1 + #(q)

; function r(f ) ∈ f B(i) → N = 
if f = [ ] →
0
| f = [t | q] →
r(t)

end

Figure 9.4 – Spécification du type concret file binomiale – partie 2.


Trois des six opérations concrètes sont spécifiées. Deux opérations auxi-
liaires : # (longueur de la file) et r (rayon de la file) sont représentées.

Le tableau 9.2 propose deux propriétés des files et minimiers binomiaux qui
précisent les relations qu’entretiennent sacs, files et arbres binomiaux. La dé-
monstration de ces deux propriétés fait l’objet de l’exercice 9.4.5.
338 Structures de données et méthodes formelles

concreteType mb(n) =  (mB, (f pF us_mb), (f pP rio_mb))


uses
fb(i), N, N1
constraints n ∈ N1
refines fpabst
auxiliarySupports
j ∈ N ∧ f ∈ f B(j) ∧ #(f ) = j

f ∈ f BC(j)
support
i ∈ N ∧ f ∈ f BC(i) ∧ a ∈ N ∧ a ≤ bMin(A(f ))

a, f  ∈ mB(i + 1)
abstractionFunction
function A (m) ∈ mB(i)  f pAbst =  ...
operationSpecifications
function f pP rio_mb(t) ∈ mB(i) → N =  bMin(A (t))
;
function f pF us_mb(t, t ) ∈ mB(i − 1) × mB(i − 1) → mB(i) =

m : (m ∈ mB(i) ∧ A(m) = f pF us(A (t), A (t )))
auxiliaryOperationRepresentations
function r(v, g) ∈ mB(i) → N1 = 
if g = [ ] →
1
| g = [ ] →
r(g) + 1

end

Figure 9.5 – Spécification du type concret minimier binomial.


Le support « minimier binomial » ( mb) est défini en conjonction avec
le support « file binomiale ». Le type mb est un type auxiliaire indirec-
tement utilisé pour la mise en œuvre de files de priorité : seules deux
opérations du type abstrait ( fpabst), f pP rio et f pF us sont définies
ici (resp. f pP rio_mb et f pF us_mb). Une opération auxiliaire est dis-
ponible, il s’agit de r, qui délivre le rayon d’un minimier. Le support
f BC (file binomiale complète) est défini comme support auxiliaire.

Tableau 9.2 – Files et minimiers binomiaux : propriétés.

Propriété Condition Ident.


A (t) = A([t | [ ]]) t ∈ mB(i) (9.4.6)
v = A (v, [ ]) v∈N (9.4.7)
9. Files de priorité 339

9.4.4 Calcul de la représentation des opérations concrètes


Nous nous préoccupons tout d’abord du type mb en calculant l’opéra-
tion f pF us_mb. Nous abordons ensuite quatre des opérations du type fb,
f pP rio_f b, f pF us_f b, f pAjout_f b et f pSupp_f b.

Calcul d’une représentation de l’opération f pF us_mb


Nous savons que, compte tenu du domaine de définition de cette fonction, les
deux arguments t et t sont des minimiers binomiaux de même rayon.

A (f pF us_mb(t, t ))
= Propriété caractéristique de f pF us_mb
A (t)  A (t ) (9.4.8)

Posons t = v, g et t = v  , g  . Deux cas seront à considérer selon la position


relative de v et de v  . Nous débutons par v ≤ v  :

A (t)  A (t )
= Hypothèse
A (v, g)  A (t )
= Définition de A
 
v  A(g)  A (t )
= Définition de A
v  A([t | g])
= Définition de A
 
A (v, [t | g])

D’où la première équation gardée pour l’opération f pF us_mb :

let v, g, v  , g   := t, t in


v ≤ v →
f pF us_mb(t, t ) = v, [t | g]
end

Le cas v  ≤ v se traite de manière similaire. Au total, nous obtenons la repré-


sentation suivante de l’opération f pF us_mb :

function f pF us_mb(t, t ) ∈ mB(i − 1) × mB(i − 1) → mB(i) =



let v, g, v  , g   := t, t in
if v ≤ v  →
v, [t | g]

|v ≤v→
v  , [t | g  ]

end

Le rayon du minimier résultant est bien égal à celui des deux arguments plus
un. Fusionner deux minimiers de même rayon consiste donc : 1) à construire une
340 Structures de données et méthodes formelles

file ayant comme tête le minimier ayant la racine la plus grande et comme queue
la file de l’autre minimier ; 2) à enraciner la tête avec la plus petite racine. Le
schéma ci-dessous fournit un exemple d’une telle fusion pour des minimiers de
rayon 3.

4 + 5 = 4
4 7 8 6 5 4 7
5 9 8 6 5
9

Cette opération est en O(1). Nous allons à présent aborder le calcul de la repré-
sentation des opérations principales du type concret fb.

Calcul d’une représentation de l’opération f pP rio_f b


La précondition stipule que le rayon i de la file f est strictement positif. Cette
contrainte est équivalente au fait que f n’est pas vide. Nous pouvons donc poser
f = [t | q] comme hypothèse du calcul.
f pP rio_f b(f )
= Propriété caractéristique
bMin(A(f ))
= Hypothèse
bMin(A([t | q]))
= Définition de A
bMin(A (t)  A(q)) (9.4.9)

À ce stade, nous procédons à une induction sur la structure de la file q en


débutant pas le cas q = [ ].
bMin(A (t)  A(q))
= Hypothèse
bMin(A (t)  A([ ]))
= Définition de A
bMin(A (t)  )
= Propriété 3.7.2, page 104
bMin(A (t))
= Application de f pP rio
f pP rio(A (t))
= Propriété caractéristique de f pP rio_mb
f pP rio_mb(t)
Nous avons donc obtenu l’équation gardée suivante pour f pP rio_f b :
let [t | q] := f in
q=[] →
f pP rio_f b(f ) = f pP rio_mb(t)
end
9. Files de priorité 341

Le cas inductif se caractérise par q = [ ]. Nous repartons de la formule 9.4.9 :

bMin(A (t)  A(q))


= Propriété 3.7.15, page 104
min({bMin(A (t)), bMin(A(q))})
= Propriété caractéristique de f pP rio_mb et de f pP rio_f b
min({f pP rio_mb(t), f pP rio_f b(q)})

D’où la seconde équation gardée pour f pP rio_f b :

let [t | q] := f in
q = [ ] →
f pP rio_f b(f ) = min({f pP rio_mb(t), f pP rio_f b(q)})
end

Au total, nous avons calculé la version suivante de l’opération f pP rio_f b :

function f pP rio_f b(f ) ∈ f B(i)  N =


pre
i>0
then
let [t | q] := f in
if q = [ ] →
f pP rio_mb(t)
| q = [ ] →
min({f pP rio_mb(t), f pP rio_f b(q)})

end
end

La recherche de la valeur prioritaire dans une file binomiale se fait donc en


prenant le plus petit élément du minimier en tête de liste (dont on sait que
c’est la racine) si la liste ne comprend qu’un seul élément et, le cas échéant, en
retenant le minimum entre l’élément prioritaire du minimier en tête de liste et
l’élément prioritaire de la liste de queue.

Nous allons à présent calculer la complexité asymptotique au pire de l’opé-


ration f pP rio_f b. Le coût de l’opération f pP rio_mb est de un (une condition
est évaluée systématiquement).
Soit f la file argument de l’opération f pP rio_f b. La précondition impose que
f n’est pas vide. Soit l sa longueur et n son poids. Soit P(l) le nombre maximum
de conditions évaluées lors de l’exécution de l’opération f pP rio_f b(f ). P(l)
satisfait l’équation de récurrence suivante :

P(1) = 1 Garde de la conditionnelle


P(l) = 1+ Pour atteindre la seconde branche de la conditionnelle
1+ Évaluation du min
1+ Évaluation de f pP rio_mb
P(l − 1) Appel récursif
342 Structures de données et méthodes formelles

Soit encore :

P(1) = 1
P(l) = P(l − 1) + 3 si l > 1

qui est une récurrence linéaire d’ordre 1. Elle a comme solution, pour l > 0 :

P(l) = 3 · l − 2 (9.4.10)

Nous avons alors :

P(l) = 3 · l − 2
⇒ Propriété 9.4.5, page 334
P(l) ≤ 3 · log(n + 1) − 2

D’où nous déduisons (d’après le résultat démontré à l’exercice 4.1.2) que la


complexité au pire de l’opération f pP rio_f b est en O(log(n)).

Calcul d’une représentation de l’opération f pF us_f b


L’opération f pF us_f b est une opération appartenant au type fb. C’est
aussi une opération autour de laquelle s’articule la mise en œuvre de l’opéra-
tion f pSupp_f b. C’est la raison pour laquelle nous dérogeons à notre habitude
de présenter les développements dans l’ordre de la spécification en nous penchant
dès à présent sur le calcul de cette opération.
Développer le calcul de la représentation de f pF us_f b va nous conduire à
utiliser l’hypothèse d’induction suivante, qui est démontrée conjointement au
développement :
Hypothèse d’induction 5 (Aug(f, f  )). f, f  ∈ f B(i) × f B(j) ⇒
r(f pF us_f b(f, f  )) − max({r(f ), r(f  )}) ∈ {0, 1}
Cette hypothèse d’induction postule que lorsque l’on fusionne deux files bino-
miales, le résultat est une file binomiale dont le rayon augmente au plus de un
par rapport au plus grand rayon des files en arguments.

A(f pF us_f b(f, f  ))


= Propriété caractéristique
A(f )  A(f  ) (9.4.11)

Nous procédons à une induction sur la structure de f en débutant par le cas


de base, f = [ ].

A(f )  A(f  )
= Hypothèse f = [ ]
A([ ])  A(f  )
= Définition de A
  A(f  )
= Propriétés 3.7.1 et 3.7.2, page 104
A(f  )
9. Files de priorité 343

D’où la première équation gardée :


f =[]→
f pF us_f b(f, f  ) = f 
Nous remarquons (i) que le résultat est bien une file binomiale, (ii) que son rayon
est égal au plus grand rayon des deux arguments (puisqu’il est égal à celui de
f  ). L’hypothèse d’induction 5 est donc vérifiée dans ce cas.
Pour le cas inductif, f = [ ], nous procédons à une induction secondaire sur
f  , en débutant par le cas de base f  = [ ]. Partant de A(f )  A(f  ) (calculs non
développés), nous arrivons à A(f ) qui nous fournit la seconde équation gardée :
f = [ ] →
f = [ ] →
f pF us_f b(f, f  ) = f
Le résultat est bien une file binomiale, son rayon n’augmente pas par rapport à
celui de f , l’hypothèse d’induction 5 est donc bien vérifiée.
Nous abordons à présent, pour l’induction secondaire, le cas inductif f  = [ ].
Posons f = [t | q] et f  = [t | q  ]. Nous repartons de la formule 9.4.11
ci-dessus :
A(f )  A(f  )
= Hypothèse
A([t | q])  A([t | q  ]) (9.4.12)
Nous procédons à une analyse par cas, en considérant les trois cas suivants :
(i) r(t) > r(t ), (ii) r(t) = r(t ) et (iii) r(t) < r(t ). Les cas (i) et (iii) sont
symétriques, seuls les cas (i) et (ii) sont développés ci-dessous. Débutons par
r(t) > r(t ).
A([t | q])  A([t | q  ])
= Définition de A
A (t)  A(q)  A(f  )
= Propriété caractéristique de f pF us_f b
A (t)  A(f pF us_f b(q, f  )) (9.4.13)
Arrivé à ce stade, il serait tentant d’exploiter la partie inductive de
la définition de A pour conclure que l’expression ci-dessus est égale à
A([t | f pF us_f b(q, f  )]). Cependant rien ne garantit que cette expression
est une file binomiale puisqu’il est possible (cf. l’hypothèse d’induction 5) que le
rayon de la file f pF us_f b(q, f  ) et le rayon de t soient égaux. Il faut donc réaliser
une analyse par cas secondaire. Posons f pF us_f b(q, f  ) = [t | q  ]. D’après
l’hypothèse d’induction 5, les deux seuls cas à envisager sont (i) r(t) > r(t ) et
(ii) r(t) = r(t ). Débutons par le premier cas :
A (t)  A(f pF us_f b(q, f  ))
= Hypothèse
A (t)  A([t | q  ])
= Définition de A
A([t | [t | q  ]])
344 Structures de données et méthodes formelles

D’où la troisième équation gardée :


f = [t | q] →
f  = [t | q  ] →
r(t) > r(t ) →
let [t | q  ] := f pF us_f b(q, f  ) in
r(t) > r(t ) →
f pF us_f b(f, f  ) = [t | [t | q  ]]
end
Il s’agit bien d’une file binomiale. Par ailleurs, son rayon n’augmente pas par
rapport à celui de la plus longue file : l’hypothèse d’induction 5 est donc bien
vérifiée. Traitons à présent le second cas r(t) = r(t ). Nous repartons de la
formule 9.4.13 ci-dessus.
A (t)  A(f pF us_f b(q, f  ))
= Hypothèse
A (t)  A([t | q  ])
= Définition de A
A (t)  A (t )  A(q  )
= Propriété caractéristique de f pF us_mb
A (f pF us_mb(t, t ))  A(q  )
= Définition de A
A([f pF us_mb(t, t ) | q  ])
D’où la quatrième équation gardée :
f = [t | q] →
f  = [t | q  ] →
r(t) > r(t ) →
let [t | q  ] := f pF us_f b(q, f  ) in
r(t) = r(t ) →
f pF us_f b(f, f  ) = [f pF us_mb(t, t ) | q  ]
end
Le résultat est bien une file binomiale. D’après l’hypothèse d’induction 5, la
fusion de q et f  produit une file binomiale dont le rayon atteint au pire celui de
f tandis que, toujours d’après l’hypothèse d’induction, la fusion de [t | q  ] et
de [t | q  ] délivre une file dont le rayon dépasse au pire de un celui de [t | q  ].
L’hypothèse d’induction est donc satisfaite.
Après avoir traité le cas r(t) > r(t ), nous abordons à présent le second cas
r(t) = r(t ). Repartons de la formule 9.4.12 ci-dessus :
A([t | q])  A([t | q  ])
= Définition de A
A (t)  A(q)  A (t )  A(q  )
= Propriété 3.7.1, page 104
A (t)  A (t )  A(q)  A(q  )
= Propriété caractéristique de f pF us_mb et de f pF us_f b
A (f pF us_mb(t, t ))  A(f pF us_f b(q, q  ))
9. Files de priorité 345

Nous savons que r(f pF us_mb(t, t )) = r(t) + 1. Par ailleurs, selon l’hypothèse
d’induction 5, le rayon de f pF us_f b(q, q  ) est inférieur ou égal au rayon de t
(et donc à celui de t ). Nous sommes donc dans les conditions d’application de
la branche inductive de la définition de la fonction d’abstraction A. Le calcul se
poursuit donc par :

A (f pF us_mb(t, t ))  A(f pF us_f b(q, q  ))


= Définition de A
A([f pF us_mb(t, t ) | f pF us_f b(q, q  )])

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

f = [t | q] →
f  = [t | q  ] →
r(t) = r(t ) →
f pF us_f b(f, f  ) = [f pF us_mb(t, t ) | f pF us_f b(q, q  )]

Le résultat est bien une file binomiale, son rayon augmente de 1 par rapport au
rayon des arguments, l’hypothèse d’induction 5 est donc bien vérifiée.
Le cas r(t) < r(t ) se traite comme le cas r(t) > r(t ). Nous obtenons au total
la représentation suivante de l’opération f pF us_f b :

function f pF us_f b(f, f  ) ∈ f B(j) × f B(k) → f B(i) = 


if f = [ ] →
f
| f = [t | q] →
if f  = [ ] →
f
| f  = [t | q  ] →
if r(t) > r(t ) →
let [t | q  ] := f pF us_f b(q, f  ) in
if r(t) > r(t ) →
[t | [t | q  ]]
| r(t) = r(t ) →
[f pF us_mb(t, t ) | q  ]

end
| r(t) = r(t ) →
[f pF us_mb(t, t ) | f pF us_f b(q, q  )]
| r(t) < r(t ) →
..
.


Cette fonction s’interprète de la manière suivante : si l’une au moins des files


considérées est vide, le résultat est l’autre file, sinon par hypothèse il existe un
minimier binomial en tête de chaque file. Il faut alors distinguer trois cas selon
346 Structures de données et méthodes formelles

la position relative des rayons de ces deux minimiers. Si les rayons sont égaux,
le résultat est une file dont la tête est la fusion des deux minimiers de tête et la
queue la fusion des deux files binomiales de queue. Si les rayons sont différents,
nous mettons de côté le minimier le plus grand et fusionnons la queue avec l’autre
file pour obtenir la file f  . Si le minimier écarté est (encore) plus grand que la
tête de f  alors le résultat est une file qui a comme tête le minimier écarté et
comme queue f  , sinon (les deux minimiers ont même rayon), le résultat est une
file qui a comme tête la fusion du minimier écarté et de la tête de f  et comme
queue la queue de f  .

Nous allons à présent évaluer la complexité temporelle au pire de l’opération


f pF us_f b. Un premier obstacle surgit : nous constatons que la complexité de
f pF us_f b dépend en principe du rayon des files considérées puisque la version
calculée de f pF us_f b invoque à plusieurs occasions la fonction r correspon-
dante. Nous proposons de faire l’hypothèse que r est en O(1). Nous verrons ci-
dessous comment faire de cette hypothèse une réalité. Par ailleurs, la complexité
de l’opération f pF us_mb est de 1 puisque son exécution se limite à évaluer une
seule conditionnelle. Nous pouvons à nouveau considérer que la complexité au
pire est atteinte pour des files complètes.
Soit f et f  les deux files objets de la fusion, de poids respectifs n et n . Posons
#(f ) + #(f  ) = l et n + n = N . Soit F(l) le nombre maximum de conditions
évaluées pour fusionner f et f  . F(l) satisfait l’équation de récurrence suivante :

F(0) = 2 Conditions pour s’assurer de la vacuité des deux files


F(l) = 4+ Pour atteindre le dernier appel récursif
F(l − 1)+ Appel récursif sur des files complètes
1+ Pour atteindre la dernière condition
1 Appel de f pF us_mb

Soit encore :

F(0) = 2
F(l) = F(l − 1) + 6 si l > 0

qui est une récurrence linéaire d’ordre 1. Elle a comme solution :

F(l) = 6 · l + 2 (9.4.14)

La propriété 9.4.4, page 334, appliquée aux files f et f  (supposées non vides)
donne #(f ) ≤ log(n + 1) et #(f  ) ≤ log(n + 1). Soit en additionnant membre
à membre :

l ≤ log(n + 1) + log(n + 1)
⇔ Propriété du log
l ≤ log((n + 1)·(n + 1))

Or nous savons (exercice 9.4.7) que, si a = 0 et b = 0, log(a · b) < 2 · log(a + b).


Reprenons le calcul :
9. Files de priorité 347

l ≤ log((n + 1)·(n + 1))


⇒ Propriété du log
l < 2 · log(n + n + 2)
⇔ Notation
l < 2 · log(N + 2) (9.4.15)

Reportons ce résultat dans la formule 9.4.14 :

F(l) = 6 · l + 2
⇒ Inégalité (9.4.15)
F(l) < 6 ·(2 · log(N + 2)) + 2
⇔ Arithmétique
F(l) < 12 · log(N + 2) + 2

Le résultat de l’exercice 4.1.2 permet de conclure que (sous l’hypothèse où le


calcul de rayon présente un coût constant et en considérant qu’aucune des files
n’est vide) l’opération f pF us_mb est en O(log N ). Dans le cas où l’une au moins
des files est vide le coût est trivialement constant.

Calcul d’une représentation de l’opération f pAjout_f b


Le calcul de cette opération nous conduit à utiliser l’opération auxiliaire
f aa(m, f ) (fpajoutaux_f b) qui délivre une file dans laquelle le minimier m est
ajouté (au sens multiensembliste) à f . La spécification de cette opération vient
compléter le type concret fb de la manière suivante :

concreteType
 ···
fb(n) =
..
.
auxiliaryOperationSpecifications
function f aa(m, f ) ∈ mB(i) × f B(j) →  f B(k) =

g : (g ∈ f B(k) ∧ A(g) = f pF us(A (m), A(f )))
end

Une fois cette opération disponible, il suffit, pour mettre en œuvre l’opération
eAjout_f b, de convertir au préalable la valeur à insérer en un minimier m. C’est
ce que nous calculons ci-dessous. Développer le calcul de la représentation de f aa
va nous conduire à utiliser l’hypothèse d’induction suivante, qui est démontrée
conjointement au développement :

Hypothèse d’induction 6 (Aug(f )). m, f ∈ mB(j)×f B(i) ⇒ r(f aa(m, f ))−


max({r(m), r(f )}) ∈ {0, 1}.

Commençons par le développement de cette opération auxiliaire.

A(f aa(m, f ))
= Propriété caractéristique
A (m)  A(f ) (9.4.16)
348 Structures de données et méthodes formelles

Nous procédons alors à une induction sur f en débutant par le cas f = [ ] :

A (m)  A(f )
= Hypothèse
A (m)  A([ ])
= Définition de A
A (m)  
= Propriété 3.7.2, page 104
A (m)
= Propriété 9.4.6, page 338
A([m | [ ]])

D’où la première équation gardée pour l’opération f aa :

f =[]→
f aa(m, f ) = [m | [ ]]

L’hypothèse d’induction est bien satisfaite : la file obtenue a le même rayon que
le minimier en argument. Pour le cas inductif (f = [ ]), posons f = [t | q] avant
de reprendre la formule 9.4.16 :

A (m)  A(f )
= Hypothèse
A (m)  A([t | q])
= Définition de A
A (m)  A (t)  A(q) (9.4.17)

Si les rayons des minimiers m et t sont égaux, le développement peut se pour-


suivre par :

A (m)  A (t)  A(q)


= Propriété caractéristique de f pF us_mb
A (f pF us_mb(m, t))  A(q)
= Définition de A
A([f pF us_mb(m, t) | q])

D’où la seconde équation gardée pour l’opération f aa :

f = [t | q] →
r(m) = r(t) →
f aa(m, f ) = [f pF us_mb(m, t) | q]

Le rayon du résultat est supérieur de 1 à ceux de m et de f : l’hypothèse d’in-


duction est toujours satisfaite. Le cas r(m) > r(t) se traite en repartant de la
formule 9.4.17 ci-dessus :

A (m)  A (t)  A(q)


= Définition de A
A([m | f ])
9. Files de priorité 349

D’où la troisième équation gardée pour l’opération f aa :

f = [t | q] →
r(m) > r(t) →
f aa(m, f ) = [m | f ]

Le rayon du résultat est égal à celui de m : l’hypothèse d’induction est toujours


satisfaite. Le cas r(m) < r(t) se traite toujours en repartant de la formule 9.4.17
ci-dessus :

A (m)  A (t)  A(q)


= Propriété 3.7.1, page 104 et propriété caractéristique de f aa
A (t)  A(f aa(m, q))

Le rayon de f aa(m, q) peut atteindre celui de t. Il est impossible de conclure


immédiatement. À ce stade nous pouvons affirmer que la file f aa(m, q) n’est
pas vide (elle contient au moins les éléments de m). Posons f  = f aa(m, q) puis
[t | q  ] = f  :

A (t)  A(f aa(m, q))


= Première hypothèse
A (t)  A([t | q  ])
= Définition de A
A (t)  A (t )  A(q  ) (9.4.18)

Selon l’hypothèse d’induction, deux cas seulement sont à envisager : r(t ) = r(t)
et r(t ) < r(t) (cf. exercice 9.4.8). Débutons par le cas r(t ) = r(t) :

A (t)  A (t )  A(q  )


= Propriété caractéristique de f pF us_mb
A (f pF us_mb(t , t))  A(q  )
= Définition de A
A([f pF us_mb(t , t) | q  ])

D’où la quatrième équation gardée pour l’opération f aa :

f = [t | q] →
r(m) < r(t) →
let [t | q  ] := f aa(m, q) in
r(t ) = r(t) →
f aa(m, f ) = [f pF us_mb(t , t) | q  ]
end

Le rayon du résultat est supérieur de un au rayon de m et de f : l’hypothèse


d’induction est satisfaite. Quant au cas r(t ) < r(t), il se traite en repartant de
la formule 9.4.18 :

A (t)  A (t )  A(q  )


= Hypothèse et définition de A
A([t | f  ])
350 Structures de données et méthodes formelles

D’où enfin la cinquième équation gardée pour l’opération f aa :


f = [t | q] →
r(m) < r(t) →
let f  := f aa(m, q) in
let [t | q  ] := f  in
r(t ) < r(t) →
f aa(m, f ) = [t | f  ]
end
end
Le rayon du résultat est égal au rayon de la file : l’hypothèse d’induction est
toujours satisfaite.
Au total, nous avons calculé la représentation suivante de l’opération f aa :

function f aa(m, f ) ∈ mB(i) × f B(j) →  f B(k) =



if f = [ ] →
[m | [ ]]
| f = [t | q] →
if r(m) = r(t) →
[f pF us_mb(m, t) | q]
| r(m) > r(t) →
[m | f ]
| r(m) < r(t) →
let f  := f aa(m, q) in
let [t | q  ] := f  in
if r(t ) = r(t) →
[f pF us_mb(t , t) | q  ]
| r(t ) < r(t) →
[t | f  ]

end
end

Nous sommes maintenant prêt à calculer une représentation pour l’opération


f pAjout_f b :
A(f pAjout_f b(v, f ))
= Propriété caractéristique
v  A(f )
= Propriété 9.4.7, page 338
A (v, [ ])  A(f )
= Propriété caractéristique de f aa
A(f aa(v, [ ], f ))
D’où la représentation suivante pour l’opération f pAjout_f b(v, f ) :

function f pAjout_f b(v, f ) ∈ N × f B(i) → f B(n) =


 f aa(v, [ ], f )
9. Files de priorité 351

Analyse amortie de l’opération f pAjout_f b


La similitude existant entre l’ajout d’un élément dans une file binomiale
et l’ajout de 1 à un compteur binaire représenté par une liste (cf. exemple
de la page 129) nous incite à penser que la complexité amortie de l’opération
f pAjout_f b(v, f ) puisse être en temps constant. Nous allons tenter de confirmer
cette intuition en appliquant la méthode du potentiel. Pour cela, commençons
par proposer une version récurrente de T , fonction qui évalue le coût réel d’un
appel. Ce coût est donné par le nombre d’appels à l’opération f pF us_mb. En-
suite nous choisirons une fonction de potentiel Φ. Nous serons alors à même
d’appliquer la formule 4.3.2, page 125, afin de calculer le coût amorti M d’un
appel à l’opération f pAjout_f b(v, f ).
Le coût réel d’un appel à l’opération f pAjout_f b(v, f ) est donné, d’après la
représentation calculée ci-dessus, par :

T (f pAjout_f b(v, f )) = T (f aa(v, [ ], f ))

Si nous considérons que le coût « réel » d’un appel à la fonction f pF us_mb


est de un, la structure de la fonction f aa nous permet de fournir l’équation
récurrente suivante pour le coût « réel » T d’un appel à f aa(m, f ) :


⎪ T (f aa(m, [ ])) = 1 ⎧


⎨ ⎪
⎪ 2 si r(m) = r(t)

1 si r(m) > r(t)

⎪ T (f aa(m, [t | q])) = 

⎪ ⎪ T (f aa(m, q)) + 1
⎪ si r(m) < r(t) ∧ r(t ) = r(t)
⎩ ⎩
T (f aa(m, q)) si r(m) < r(t) ∧ r(t ) < r(t)

Nous choisissons comme fonction de potentiel la fonction qui délivre la lon-


gueur de la file : Φ(f ) = #(f ). Appliquée à f aa(m, f ), celle-ci s’exprime sous la
forme d’une équation récurrente :


⎪ Φ(f aa(m, [ ])) = 1 ⎧


⎨ ⎪
⎪ Φ(q) + 1 si r(m) = r(t)

Φ([t | q]) + 1 si r(m) > r(t)

⎪ Φ(f aa(m, [t | q])) = 

⎪ ⎪
⎪ Φ(f aa(m, q)) si r(m) < r(t) ∧ r(t ) = r(t)
⎩ ⎩
Φ(f aa(m, q)) + 1 si r(m) < r(t) ∧ r(t ) < r(t)

L’équation qui fournit la complexité amortie de l’opération f aa est alors (cf. for-
mule 4.3.2, page 125) :

M(f aa(m, f )) = T (f aa(m, f )) + Φ(f aa(m, f )) − Φ(f ) (9.4.19)

Elle se résout par induction sur la structure de f et, pour la partie inductive,
par une analyse par cas. Débutons par le cas de base f = [ ] :

T (f aa(m, [ ])) + Φ(f aa(m, [ ])) − Φ([ ])


= Définition de T et de Φ
1 + 1 − 0
= Arithmétique
2
352 Structures de données et méthodes formelles

Pour la partie inductive (f = [t | q]), nous distinguons les quatre cas apparais-
sant dans les définitions de T et de Φ. Nous débutons par r(m) > r(t) :
T (f aa(m, [t | q])) + Φ(f aa(m, [t | q])) − Φ([t | q])
= Définition de T et de Φ
1 + Φ([t | q]) + 1 − Φ([t | q])
= Définition de Φ et arithmétique
2
Poursuivons par r(m) = r(t) :
T (f aa(m, [t | q])) + Φ(f aa(m, [t | q])) − Φ([t | q])
= Définition de T et de Φ
2 + Φ(q) + 1 − (1 + Φ(q))
= Définition de Φ et arithmétique
2
Le cas r(m) < r(t) ∧ r(t ) = r(t) se traite comme suit :
T (f aa(m, [t | q])) + Φ(f aa(m, [t | q])) − Φ([t | q])
= Définition de T et de Φ
T (f aa(m, q)) + 1 + Φ(f aa(m, q)) − (1 + Φ(q))
= Arithmétique et définition de M
M(f aa(m, q))
Enfin, le cas r(m) < r(t) ∧ r(t ) < r(t) se développe par :
T (f aa(m, [t | q])) + Φ(f aa(m, [t | q])) − Φ([t | q])
= Définition de T et de Φ
T (f aa(m, q)) + Φ(f aa(m, q)) + 1 − (1 + Φ(q))
= Arithmétique et définition de M
M(f aa(m, q))
Au total, nous avons calculé la version récurrente suivante de M(f aa(m, f )) :

⎨M(f aa(m, [ ])) = 2
M(f aa(m, [t | q])) = 2 si r(m) ≥ r(t)

M(f aa(m, [t | q])) = M(f aa(m, q)) si r(m) < r(t)
dont la solution immédiate est M(f aa(m, f )) = 2. La complexité amortie de
f aa(m, f ) est donc en temps constant. Il s’en déduit :

M(f pAjout_f b(v, f )) ∈ O(1)

Nous aurions pu calculer une représentation de f pAjout_f b(v, f ) à partir


de l’opération de fusion f pF us_f b. Nous serions parvenu au résultat suivant :
function f pAjout_f b(v, f ) ∈ N × f B(j) → f B(i) =

f pF us_f b([v, [ ] | [ ]], f )
Cependant, la complexité amortie de cette version n’est pas en temps constant
(cf. exercice 9.4.9).
9. Files de priorité 353

Calcul d’une représentation de l’opération f pSupp_f b


Ce calcul nous conduit à l’utilisation de l’opération de fusion de files. Nous
démarrons le calcul avec comme argument [t | q] (c’est possible car la précondi-
tion de f pSupp_f b(f ) est équivalente à f = [ ]). Cependant, le développement
doit s’accompagner de l’hypothèse d’induction suivante, qui est utilisée et prou-
vée conjointement au développement.

Hypothèse d’induction 7 (Dim(f)). Si f ∈ f B(i) alors r(f ) −


r(f pSupp_f b(f )) ∈ {0, 1}.

Cette hypothèse exprime que la suppression dans une file binomiale diminue le
rayon de la file d’au plus 1. Débutons le calcul de l’opération f pSupp_f b :

A(f pSupp_f b([t | q]))


= Propriété caractéristique de f pSupp_f b
A([t | q]) −̇ bMin(A([t | q]))
= Définition de A
(A (t)  A(q)) −̇ bMin(A([t | q]))
= Propriété caractéristique de f pP rio_f b
(A (t)  A(q)) −̇ f pP rio_f b([t | q]) (9.4.20)

Posons t = v, g :

(A (t)  A(q)) −̇ f pP rio_f b([t | q])


= Hypothèse
(A (v, g)  A(q)) −̇ f pP rio_f b([t | q])
= Définition de A
(v  A(g)  A(q)) −̇ f pP rio_f b([t | q])

Effectuons une analyse par cas, selon que v = f pP rio_f b([t | q]) ou que
v > f pP rio_f b([t | q]) (ce sont bien sûr les deux seuls cas possibles). Nous
débutons par v = f pP rio_f b([t | q]).

(v  A(g)  A(q)) −̇ f pP rio_f b([t | q])


= Hypothèse
(v  A(g)  A(q)) −̇ v
= Propriété 3.7.5, page 104
(A(g)  A(q))  (v −̇ v)
= Propriétés 3.7.4 et 3.7.2, page 104
A(g)  A(q)
= Propriété caractéristique de l’opération f pF us_f b
A(f pF us_f b(g, q))

D’où la première équation gardée :

let v, g := t in
f pP rio_f b([t | q]) = v →
f pSupp_f b([t | q]) = f pF us_f b(g, q)
end
354 Structures de données et méthodes formelles

Soit r([t | q]) = n. Nous avons alors r(g) = n − 1 et r(q) ≤ n − 1. D’après


l’hypothèse d’induction 7, r(f pF us_f b(g, q)) ∈ {n, n − 1}. Après suppression, le
rayon de la file binomiale diminue donc au plus de 1. L’hypothèse d’induction est
satisfaite dans ce cas. Pour le second cas, v > f pP rio_f b([t | q]), il est facile
de démontrer que dans cette hypothèse f pP rio_f b([t | q]) = f pP rio_f b(q).
En repartant de la formule 9.4.20 ci-dessus, nous avons :

(A (t)  A(q)) −̇ f pP rio_f b([t | q])


= Propriété 3.7.5, page 104
A (t)  (A(q) −̇ f pP rio_f b([t | q]))
= Propriété caractéristique de f pSupp_f b
A (t)  A(f pSupp_f b(q))

Compte tenu de l’hypothèse d’induction 7, le rayon de f pSupp_f b(q) est infé-


rieur à celui de t, nous sommes donc en mesure d’appliquer la fonction d’abs-
traction A :

A (t)  A(f pSupp_f b(q))


= Définition de A
A([t | f pSupp_f b(q)])

D’où la seconde équation gardée :


let v, g := t in
f pP rio_f b([t | q]) = v →
f pSupp_f b([t | q]) = [t | f pSupp_f b(q)]
end
Le résultat est bien une file binomiale dont le rayon, égal à celui de t, reste
inchangé. L’hypothèse d’induction 7 est bien satisfaite. Au total, nous avons
calculé la représentation suivante de l’opération f pSupp_f b :
function f pSupp_f b([t | q]) ∈ f B(i)   f B(j) =

pre
i>0
then
let v, g, p := t, f pP rio_f b([t | q]) in
if v = p →
f pF us_f b(g, q)
|v>p→
[t | f pSupp_f b(q)]

end
end
L’hypothèse d’induction 7 permet en outre d’affirmer que j ∈ {i − 1, i}.
Quelle est la complexité au pire de cette version de l’opération f pSupp_f b ?
Tentons de répondre à cette question. Soit S(l) la fonction qui délivre le nombre
maximum de conditions de conditionnelles évaluées pour une file f de longueur
l et de poids n. S(l) satisfait l’équation récurrente suivante dans laquelle P et F
9. Files de priorité 355

sont les fonctions 9.4.10, page 342, et 9.4.14, page 346, associées respectivement
à la recherche de l’élément prioritaire et à la fusion :

S(1) = P(1)+ Recherche de l’élément prioritaire


1 Garde de la conditionnelle
S(l) = P(l)+ Recherche de l’élément prioritaire
1+ Garde de la conditionnelle
max({ Recherche de la pire des branches de la conditionnelle
F(l − 1 + (l − 1)), Première branche : fusion
S(l − 1), Seconde branche : appel récursif
})

Il est a priori difficile de déterminer la pire solution. Celle pour laquelle l’élément
prioritaire est en tête de file ? Ou celle qui parcourt toute la file ? Dans le premier
cas, après le parcours initial, la fusion se fait au pire sur deux listes de longueur
l − 1, dans le second cas la fusion se fait sur deux listes vides mais la recherche
de l’élément prioritaire est réalisée plusieurs fois. Le calcul montre que dans le
premier cas nous obtenons la solution suivante : S(l) = 14 · l − 10 et dans le
second S(l) = l2 + l. C’est donc cette seconde solution qu’il faut retenir pour un
calcul de complexité au pire. Nous en déduisons immédiatement que l’opération
f pSupp_f b calculée ci-dessus a une complexité au pire en O(l2 ), soit d’après la
propriété 9.4.4, page 334, en O(log2 (n)).

Est-il possible d’améliorer cette situation ? Nous avons ci-dessus fourni un


indice en constatant que, dans le cas où la valeur prioritaire n’est pas située
en tête de liste, la recherche de cette valeur est réalisée à chaque invocation de
l’opération. Il est bien entendu préférable de n’effectuer cette recherche qu’une
seule fois. Pour cela, il faut définir une opération auxiliaire f pSuppAux_f b(f, w)
dans laquelle f est la file considérée et w est la valeur prioritaire, supposée
connue. Plus précisément, cette opération se spécifie comme suit dans la rubrique
auxiliaryOperationSpecifications :

 ···
concreteType fb(n) =
..
.
auxiliaryOperationSpecifications
function f pSuppAux_f b(f, w) ∈ f B(i) × N   f B(j) =

pre
w = bMin(A(f )) ∧ i > 0
then
q : (q ∈ f B(j) ∧ A(q) = f pSupp(A(f )))
end
end

Calculons une représentation de cette opération. À l’instar de f pSupp_f b


nous pouvons considérer que f = [t | q] puisque la file n’est pas vide.

A(f pSuppAux_f b([t | q], w))


= Propriété caractéristique de f pSupp_f b
356 Structures de données et méthodes formelles

(A (t)  A(q)) −̇ bMin(A([t | q]))


= Spécification de f pSuppAux_f b
(A (t)  A(q)) −̇ w (9.4.21)

Posons t = v, g. Procédons à une analyse par cas selon que w ! A (t) ou
non. Si w ! A (t), alors v = w. Dans cette hypothèse nous avons :

(A (t)  A(q)) −̇ w


= Hypothèse
(A (v, g)  A(q)) −̇ w
= Définition de A
(v  A(g)  A(q)) −̇ w
= Propriétés 3.7.5, 3.7.4 et 3.7.2, page 104
A(g)  A(q)
= Propriété caractéristique de l’opération f pF us_f b
A(f pF us_f b(g, q))

Nous obtenons la première équation gardée :

let v, g := t in
v=w→
f pSuppAux_f b([t | q], w) = f pF us_f b(g, q)
end

Le cas v = w a comme conséquence v


−A(q). Nous repartons de la formule 9.4.21 :

(A (t)  A(q)) −̇ w


= Propriété 3.7.5, page 104
A (t)  (A(q) −̇ w)
= Propriété caractéristique de f pSuppAux_f b
A (t)  A(f pSuppAux_f b(q, w))
= Définition de A
A([t | f pSuppAux_f b(q, w)])

Ce calcul nous fournit la seconde équation gardée :

let v, g := t in
v = w →
f pSuppAux_f b([t | q], w) = [t | f pSuppAux_f b(q, w)]
end

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


f pSuppAux_f b :
9. Files de priorité 357

function f pSuppAux_f b([t | q], w) ∈ f B(i) × N 


 f B(j) =

pre
w = bMin(A(f ))
then
let v, g := t in
if v = w →
f pF us_f b(g, q)
| v = w →
[t | f pSuppAux_f b(q, w)]

end
end

Cette version nous apprend que, connaissant la valeur prioritaire à supprimer, si


cette valeur est en tête de file, alors le résultat est la fusion de la queue de file et
de la file qui reste quand on écrête le minimier de tête, sinon, le résultat est une
file constituée du minimier de tête et de la file obtenue en supprimant la valeur
prioritaire de la queue de file.

Préoccupons-nous à présent d’évaluer la complexité au pire de cette opéra-


tion. Soit S  (l) la fonction qui délivre le nombre de conditions de conditionnelles
évaluées pour une file f de longueur l et de poids n. S  (l) satisfait l’équation
récurrente suivante :

S  (1) =1+ Garde de la conditionnelle


F(0) Fusion de deux files vides
S  (l) = 1+ Garde de la conditionnelle
max({ Recherche de la pire des branches de la conditionnelle
F(l − 1 + (l − 1)), Première branche : fusion
S  (l − 1), Seconde branche : appel récursif
})

Par un calcul et des hypothèses similaires à ceux effectués pour l’opération


f pSupp_f b, il est facile de montrer que la solution la plus coûteuse est :

S  (l) = 12 · l − 9 (9.4.22)

Cette formule va nous être utile pour l’évaluation de la complexité de la nouvelle


version de f pSupp_f b. Nous pouvons à présent reprendre le calcul de l’opération
f pSupp_f b :

A(f pSupp_f b(f ))


= Spécification de f pSupp_f b
f pSupp(A(f ))
= Spécification de f pSuppAux_f b
A(f pSuppAux_f b(f, bMin(A(f ))))
= Propriété caractéristique de f pP rio_f b
A(f pSuppAux_f b(f, f pP rio_f b(f )))
358 Structures de données et méthodes formelles

D’où la nouvelle version de l’opération f pSupp_f b :

function f pSupp_f b(f ) ∈ f B(i)  f B(j) =



pre
i>0
then
f pSuppAux_f b(f, f prio_f b(f ))
end

Cette fois, la recherche de la valeur prioritaire n’est effectuée qu’une seule fois.
La complexité devrait s’en trouver améliorée : soit S(l) la fonction qui délivre
le nombre maximum de conditions évaluées lors de la suppression de la valeur
prioritaire dans une file f de longueur l et de poids n. S(l) se définit par :

S(l) = P(l)+ Recherche de l’élément prioritaire (9.4.10)


S  (l) Opération auxiliaire (9.4.22)

Soit, sous forme close, S(l) = 14 · l − 10. En utilisant la propriété 9.4.4, page 334,
nous obtenons S(l) < 14 · log(n+1)−10. Sous l’hypothèse que le calcul du rayon
est « gratuit », le coût au pire de cette version de l’opération est cette fois en
O(log n).

9.4.5 Renforcement du support par décomposition


du rayon
Revenons sur le problème de l’évaluation de la fonction rayon évoquée dans
la représentation de la fonction f pF us_f b. Lors de l’évaluation de la complexité
de cette opération, nous avons fait l’hypothèse que le calcul de r(f ) présente un
coût négligeable. À l’évidence ce n’est pas le cas. Nous avons déjà eu l’occasion
d’appliquer à ce type de problème la technique du renforcement de support par
décomposition (cf. sections 1.10 et 2.4). Celle-ci consiste à échanger du temps
contre de la place. Plus précisément, il s’agit de modifier légèrement la structure
de données de façon à introduire dans chaque nœud un champ (appelons-le r
également) représentant la valeur du rayon pour le nœud considéré et à exploiter
cette valeur en consultant directement le champ en question. La technique exige
également de modifier les opérations de façon à mettre à jour ou à consulter
le nouveau champ. Ce raffinement peut sans danger être réalisé directement de
manière empirique. Le support mB devient alors (les ajouts sont encadrés) :

i ∈ N ∧ f ∈ f BC(i) ∧ a ∈ N ∧ a ≤ bMin(A (f )) ∧
r ∈ N1 ∧ r = r(f ) + 1

 a → r , f  ∈ mB(i + 1)

Parmi les opérations qui demandent à être modifiées, il y a f pF us_f b que nous
ébauchons ici :
9. Files de priorité 359

Application des files de priorité : le codage d’Huffman


Compresser un texte consiste à exploiter la redondance qu’il recèle afin
de lui faire occuper moins de place. Une technique de compression consiste
à coder chacun des caractères du vocabulaire sur une chaîne de bits de
longueur variable, contrairement au codage ascii où chaque caractère est
codé par 8 bits. Considérons le texte suivant : t = elleaimelemiel.
Ce texte s’exprime sur le vocabulaire V = {e,l,,a,i,m}. En ascii t
occupe 136 bits. Un code est dit préfixe si aucun de ses caractères n’est
codé par une chaîne de bits qui serait le début du codage d’un autre ca-
ractère, au contraire de 110 et 11011. Un code préfixe est optimal pour
t s’il n’existe pas d’autres codages offrant une meilleure compression de
t. Le codage d’Huffman fournit un code préfixe optimal. De plus, il pré-
sente, pour ce qui nous concerne, un double intérêt puisque la structure
de données qui aboutit à l’arbre de codage est une file de priorité de tries
(sur 0/1).

3 4 5 5
1 2 2 3 4 5 2 3 3 4 5  l m • e
a i m  l e m a i  l e a i
17
0 1
7 10 • •
0 1 0 1
5 5 7  l • e  l • e
0 1
m • e  l m • m •
0 1
a i a i a i

L’algorithme d’Huffman débute avec une file de priorité dans laquelle


chaque élément du vocabulaire est présent avec sa fréquence dans le texte t
en guise de priorité (schéma en haut à gauche). L’étape suivante regroupe
les deux éléments prioritaires en un arbre binaire ayant comme priorité
la somme des priorités initiales : les lettres a et i sont regroupées dans
un arbre-trie de priorité 3. Le processus se répète jusqu’à obtention d’une
file réduite à un seul élément qui est l’arbre de codage recherché. Il suffit
alors d’étiqueter chaque branche pour doter chaque feuille d’un code sur
1/0. Par exemple, le code de i est 1011. Le codage du texte initial se fait
alors sur 55 bits (au lieu de 136).
Historiquement, D. Huffman a été initié à la théorie de l’information
au mit au début des années 1950, en tant que doctorant, par C. Shannon
et R.M. Fano. En 1952, ce dernier a présenté un algorithme de codage non
optimal que D. Huffman s’est empressé d’améliorer sous la forme qu’on
lui connaît aujourd’hui (cf. [65, 63, 74]).
360 Structures de données et méthodes formelles

function f pF us_f b(f, f  ) ∈ f B(j) × f B(k) → f B(i) =



if f = [ ] →
f
| f = [t | q] →
if f  = [ ] →
f
| f  = [t | q  ] →
let v → r, g, v  → r , g   := t, t in
if r > r →
let [t | q  ] := f pF us_f b(q, f  ) in
let v  → r , g   := t in
if r > r →
..
.

La fusion augmente de un le rayon du résultat. Il suffit donc, lors de la création


du nœud racine, d’associer l’ancien rayon plus un à la valeur à la racine. Rap-
pelons que la fusion de minimiers considère deux minimiers de même rayon :
r = r . L’opération f pF us_f b ne modifie pas directement le champ r. Le pari
de disposer un rayon « gratuit » est donc gagné : les coûts les pires obtenus
précédemment sont confirmés, ils sont tous en O(log(n)).

9.4.6 Conclusion et remarques bibliographiques


Les files binomiales ont été découvertes par J. Vuillemin [114, 116]. Différentes
représentations et propriétés ont été étudiées par M.R. Brown [19], y compris des
versions non fonctionnelles. Dans [73], D.J. King effectue la présentation d’une
mise en œuvre fonctionnelle incluant l’opération (non étudiée ici) de décrémen-
tation d’une valeur donnée de la file. Enfin, dans [53], R. Hinze décrit une mise
en œuvre en Haskell. Sur le site [118], M. Woo présente un arbre généalogique
des files de priorité et montre que les files binomiales ont inspiré plusieurs autres
représentations telles que les arbres de Fibonacci ou encore les files maigres et
épaisses (« thin and thick ») de H. Kaplan et R.E. Tarjan.
Dans [116], J. Vuillemin décrit une autre structure de données concrète per-
mettant la fusion de files de priorité : les pagodes. Cette structure n’est pas
fonctionnelle. Elle n’est donc pas étudiée ici.

Exercices

Exercice 9.4.1 Quel serait l’inconvénient d’utiliser des listes triées par ordre croissant pour
représenter les files binomiales ? Justifier votre réponse.

Exercice 9.4.2 Spécifier puis calculer les trois représentations naïves des files de priorité : par
tableau, par listes non triées puis triées.

Exercice 9.4.3 Démontrer les propriétés du tableau 9.1, page 334.


9. Files de priorité 361

Exercice 9.4.4 Démontrer que dans un arbre  binomial


 de rayon r, le nombre de nœuds
r−1
présents au niveau k est donné par la formule (cf. page 335).
k
Exercice 9.4.5 Démontrer les deux propriétés 9.4.6 et 9.4.7, page 338.

Exercice 9.4.6 Effectuer le calcul de l’opération f pP rio_mb.

Exercice 9.4.7 Le calcul de complexité de l’opération f pF us_f b nous a conduit à utiliser la


propriété suivante : soit a et b deux réels strictement positifs, alors
log(a.b) < 2 · log(a + b).
Démontrer cette propriété. Suggestion : le log est une fonction concave. Utiliser une propriété
des fonctions concaves.

Exercice 9.4.8 Lors du calcul de la représentation de f pAjout_f b, nous avons supposé que,
sous les hypothèses f = [t | q], [t | q  ] = f aa(m, q) et r(m) < r(t), seuls deux cas sont à
envisager : r(t ) < r(t) et r(t ) = r(t). Démontrer que le cas r(t ) > r(t) ne peut survenir.

Exercice 9.4.9 Calculer une représentation de l’opération f pAjout_f b sans utiliser d’opéra-
tion auxiliaire, à partir de l’opération de fusion f pF us_f b. Effectuer une analyse amortie de
cette représentation.

Exercice 9.4.10 La spécification des files de priorité fpabst de la figure 9.1, page 329, est
neutre vis-à-vis de la politique de service quand plusieurs clients sont candidats à être servis
(les clients ne sont pas discernables). Modifier la spécification de façon à adopter une politique
« premier entré, premier sorti » pour les clients de même priorité. En adoptant le support
concret de votre choix, fournir une spécification concrète et calculer la représentation des
opérations. Calculer leur complexité au pire.

Exercice 9.4.11 Mettre en œuvre les files de priorité en utilisant les arbres de Braun (cf. sec-
tion 10.4, page 378).

Exercice 9.4.12 (Files de priorité de trous fusionnables) Dans les structures de données
traditionnelles (ensembles, files, etc.) l’insertion d’un nouvel élément n’a pas d’influence sur
l’existence ou les propriétés des éléments déjà présents. Ce caractère d’indépendance n’est pas
toujours vérifié. Considérons en effet le cas d’une mémoire segmentée débutant à l’adresse 1
et de taille indéterminée pour laquelle on répertorie les emplacements libres (trous) tel que la
suppression d’un emplacement libre (l’acquisition d’un segment de mémoire par le système)
porte sur le plus grand élément. Dans une telle structure de données, l’adjonction d’un nouvel
intervalle peut avoir des conséquences sur les trous 2 existants puisqu’elle peut entraîner des
fusions.
1. Proposer une spécification du type abstrait « mémoire à trous fusionnables ».
2. Mettre en œuvre cette spécification sur la base d’un support représenté par une structure
d’arbre binaire qui est un abr sur l’indice de début des trous et un maximier sur la
longueur des trous.

9.5 Méthodes autoadaptatives : les minimiers


obliques (skew heaps)
La mise en œuvre des files de priorité fusionnables par minimiers obliques est
une solution autoadaptative : aucun équilibre explicite n’est recherché. Malgré
2. Par convention, nous réservons le terme de trou aux emplacements disponibles dans
la structure de données et celui d’intervalle aux éléments à introduire dans la structure de
données.
362 Structures de données et méthodes formelles

cela l’heuristique utilisée garantit une bonne complexité amortie de toutes les
opérations présentes dans le type abstrait.
Dans cette mise en œuvre, toutes les opérations se fondent sur une opération
de base : la fusion. On y exploite la propriété des minimiers qui affirme qu’échan-
ger les deux sous-arbres d’un minimier binaire préserve sa nature de minimier.
La fusion se faisant entre branches droites, l’heuristique de la fusion tente donc
d’obtenir une branche droite aussi courte que possible. Et, bien qu’un minimier
oblique puisse être arbitrairement déséquilibré 3 , l’objectif est atteint ainsi que
le montre l’analyse amortie développée à la section 9.5.5.
Un minimier oblique est un minimier sans point simple à droite. Dans un
minimier oblique, il n’existe donc pas de nœud qui aurait un fils droit mais pas
de fils gauche.

1 1
(1) (2)
7 2 6 2
9 8 5 3 7 8 5 3
6 8 4 9 6 8 4

Dans le schéma ci-dessus, (1) est un minimier oblique tandis que (2) ne l’est
pas : il possède un point simple à droite (désigné par la flèche). Cette forme
de déséquilibre en faveur de la gauche n’interdit cependant pas à un minimier
oblique d’être déséquilibré en faveur de la droite, que ce soit en poids ou en rayon
ainsi que le montre l’arbre (1) ci-dessus dont le sous-arbre droit a un poids (resp.
un rayon) de 6 (resp. de 3) alors que le sous-arbre gauche a un poids (resp. un
rayon) de 3 (resp. de 2).

9.5.1 Définition du support concret


Nous définissons formellement le support mo du type concret mo de la ma-
nière suivante :

1)  ∈ mo
2) r ∈ N ⇒ , r,   ∈ mo
3) g ∈ mo − {} ∧ d ∈ mo ∧ r ∈ N ∧ r = bMin(A(g, r, d))

g, r, d ∈ mo

La clause 2) précise qu’une feuille est un minimier oblique. La clause 3) utilise


la fonction d’abstraction A afin de préciser que la racine d’un minimier oblique
est un représentant du plus petit élément du sac mis en œuvre par le minimier
g, r, d. Par ailleurs, cette clause exclut les points simples à droite en interdisant,
pour cette clause, qu’un sous-arbre gauche soit vide.

3. Ou presque comme nous le verrons ci-dessous.


9. Files de priorité 363

9.5.2 Définition de la fonction d’abstraction


La fonction d’abstraction délivre le sac représenté par le minimier oblique.
Elle s’obtient par induction structurelle sur l’argument ; cependant, les clauses
2) et 3) du support conduisent à la même expression :

function A(f ) ∈ mo  f pAbst =



if f =  →

| f = g, r, d →
A(g)  r  A(d)

La fonction A est totale, surjective mais pas injective : toute file de priorité peut
être représentée par un minimier oblique mais, pour une file donnée, il existe en
général plusieurs représentations possibles.

9.5.3 Spécification des opérations concrètes


Afin de bien distinguer les deux fonctionnalités de la fusion (en tant qu’opé-
ration du type abstrait d’une part et en tant qu’outil pour représenter les autres
opérations d’autre part), nous introduisons l’opération auxiliaire f us. Compte
tenu de son rôle central, f us est la première opération à calculer. L’ensemble
des informations sur la spécification du type mo se retrouve à la figure 9.6, page
suivante.

9.5.4 Calcul d’une représentation des opérations concrètes


Intuitivement, ajouter un élément dans une file représentée par un mini-
mier oblique f (opération f pAjout_mo) se fait en érigeant l’élément en mini-
mier oblique avant d’effectuer une fusion avec f . Supprimer l’élément prioritaire
d’une file représentée par un minimier oblique (opération f pSupp_mo) se fait
en supprimant la racine puis en fusionnant les deux sous-arbres restants. Ces
deux opérations s’appuient donc sur l’opération de fusion f us dont nous allons
à présent calculer une représentation.

Calcul d’une représentation de l’opération f us


La principale difficulté dans ce calcul est de choisir parmi les nombreuses
possibilités qui s’offrent à nous. Ce choix est dicté en priorité par le fait que le
résultat de la fusion est un minimier oblique.

A(f us(f, f  ))
= Propriété caractéristique de f us
A(f )  A(f  ) (9.5.1)

À ce stade, nous procédons à une induction primaire sur f puis à une induc-
tion secondaire sur f  . Considérons tout d’abord le cas f =  :
364 Structures de données et méthodes formelles

concreteType
mo =  (mo, (f pV ide_mo, f pAjout_mo, f pSupp_mo, f pF us_mo),
(f pP rio_mo, f pEstV ide_mo))
uses bool, N
refines fpabst
support
1)  ∈ mo
2) r ∈ N ⇒ , r,   ∈ mo
3) g ∈ mo − {} ∧ d ∈ mo ∧ r ∈ N ∧ r = bMin(A(g, r, d))

g, r, d ∈ mo
abstractionFunction
function A(f ) ∈ mo  f pAbst =  ...
operationSpecifications
function f pV ide_mo() ∈ mo =  q : (q ∈ mo ∧ A(q) = f pV ide())
; function f pAjout_mo(v, f ) ∈ N × mo → mo = 
q : (q ∈ mo ∧ A(q) = f pAjout(v, A(f )))
; function f pSupp_mo(f ) ∈ mo   mo = 
pre
A(f ) = 
then
q : (q ∈ mo ∧ A(q) = f pSupp(A(f )))
end
; function f pF us_mo(f, f  ) ∈ mo × mo  mo = 
q : (q ∈ mo ∧ A(q) = f pF us(A(f ), A(f  )))
; function f pP rio_mo(f ) ∈ mo   N=  ...
; function f pEstV ide_mo(f ) ∈ mo  bool =  f pEstV ide(A(f ))
auxiliaryOperationSpecifications
function f us(f, f  ) ∈ mo × mo  mo = 
q : (q ∈ mo ∧ A(q) = f pF us(A(f ), A(f  )))
end

Figure 9.6 – Spécification du type concret « minimier oblique ».


Un minimier oblique est un minimier particulier. Les opérations ne
véhiculent aucune contrainte de spécification, seul le souci d’atteindre
une solution efficace (en analyse amortie) guide le calcul.

A(f )  A(f  )
= Hypothèse
A()  A(f  )
= Définition de A et propriétés 3.7.1 et 3.7.2, page 104
A(f  )

D’où la première équation gardée pour l’opération f us :

f =  →
f us(f, f  ) = f 
9. Files de priorité 365

Pour la partie inductive, nous posons f = g, r, d et procédons à l’induction


secondaire sur f  . Le cas de base (f  = ) se développe comme ci-dessus et
conduit à l’équation gardée :
f =  →
f  =  →
f us(f, f  ) = f
Pour la partie inductive secondaire, nous posons f  = g  , r , d  et repartons de
la formule 9.5.1 :
A(f )  A(f  )
= Hypothèses
A(g, r, d)  A(g  , r , d )
= Définition de A
A(g)  r  A(d)  A(g  )  r   A(d )

À ce stade, nous devons procéder à une analyse par cas selon que r ≤ r ou que
r < r. Considérons tout d’abord le premier cas. La commutativité de l’opérateur
 (propriété 3.7.1, page 104) permet de réorganiser les termes à notre convenance
sous réserve que le résultat soit un minimier oblique. Cependant, obtenir un
minimier n’est garanti que si A(g  )  r   A(d ) est regroupé en A(f  ) :
A(g)  r  A(d)  A(g  )  r   A(d )
= Définition de A
A(g)  r  A(d)  A(f  )
Il subsiste alors trente-six solutions. Douze d’entre elles sont des solutions
du type r  (. . .), où r apparaît au début de l’expression. À l’évidence, ces
solutions conduisent à des minimiers qui ne sont pas obliques (le sous-arbre
gauche est vide alors que le sous-arbre droit ne l’est pas). Elles sont à rejeter.
Douze autres solutions sont issues d’expressions de la forme (. . .)r. Il est facile
de montrer que, construites à partir d’un ajout ou d’une fusion, ces solutions
produisent des arbres filiformes à gauche (cf. exercice 9.5.5). Elles ne peuvent
conduire à des opérations performantes et doivent donc être écartées.

Tableau 9.3 – Les 12 parenthésages possibles.


La commutativité et l’associativité de l’opérateur  permettent de
prendre en considération 12 variantes de l’expression initiale.

1 A(g)  r  (A(d)  A(f  )) 2 (A(g)  A(d))  r  A(f  )


3 A(g)  r  (A(f  )  A(d)) 4 (A(g)  A(f  ))  r  A(d)
5 A(d)  r  (A(g)  A(f  )) 6 (A(d)  A(g))  r  A(f  )
7 A(d)  r  (A(f  )  A(g)) 8 (A(d)  A(f  ))  r  A(g)
9 A(f  )  r  (A(g)  A(d)) 10 (A(f  )  A(g))  r  A(d)
11 A(f  )  r  (A(d)  A(g)) 12 (A(f  )  A(d))  r  A(g)

Restent enfin les douze solutions qui sont répertoriées dans le tableau 9.3 ci-
dessus. Six d’entre elles, les solutions 1, 2, 3, 5, 6 et 7, peuvent donner naissance
366 Structures de données et méthodes formelles

à des minimiers qui ne sont pas obliques. En effet, pour ces six cas, le sous-
arbre gauche peut être vide sans que le sous-arbre droit ne le soit. Restent les
solutions 4, 8, 9, 10, 11 et 12. Les solutions 4 et 10 sont à l’origine de minimiers
filiformes (cf. exercice 9.5.6). Il reste à considérer les quatre solutions 8, 9, 11 et
12. Ces solutions produisent en général des minimiers obliques non dégénérés.
Nous retenons la solution 8 pour laquelle nous achevons le développement avant
d’effectuer l’analyse amortie. Le cas des trois solutions restantes est laissé en
exercice (cf. exercice 9.5.7).

A(g)  r  A(d)  A(f  )


= Propriété 3.7.1, page 104
(A(d)  A(f  ))  r  A(g)
= Spécification de f pF us
(f pF us(A(d), A(f  ))  r  A(g)
= Spécification de f us
A(f us(d, f  ))  r  A(g)
= Définition de A
A(f us(d, f  ), r, g)

Nous avons donc calculé l’équation gardée suivante :

let g, r, d, g  , r , d  := f, f  in


r ≤ r →
f us(f, f  ) = f us(d, f  ), r, g
end

Une solution au second cas (r < r) s’obtient par des considérations de sy-
métrie. Au total, nous avons calculé la représentation suivante de l’opération
f us :

function f us(f, f  ) ∈ mo × mo → mo = 
if f =  →
f
| f =  →
if f  =  →
f
| f  =  →
let g, r, d, g  , r , d  := f, f  in
if r ≤ r →
f us(d, f  ), r, g

|r <r→
f us(d , f ), r , g  

end


9. Files de priorité 367

La complexité au pire classique de cette opération est en O(n) si n est la somme


des poids de f et de f  . En effet, pour un minimier oblique de poids m, le
nombre d’éléments présents sur la branche droite est inférieur ou égal à  m 2
(cf. exercice 9.5.2). Pour ce qui concerne la complexité amortie, nous y consacrons
la section 9.5.5 ci-dessous.

Calcul d’une représentation de l’opération f pAjout_mo


Le calcul ci-dessous valide l’intuition que l’adjonction peut s’obtenir à partir
d’une fusion :

A(f pAjout_mo(v, f ))
= Propriété caractéristique
A(f )  v
= Propriétés 3.7.2 et 3.7.1, page 104
A(f )    v  
= Définition de A
A(f )  A()  v  A()
= Définition de A
A(f )  A(, v, )
= Propriété caractéristique de f us
A(f us(f, , v, ))

Nous en déduisons la version suivante de la représentation de l’opération


f pAjout_mo :

function f pAjout_mo(v, f ) ∈ N × mo → mo =
 f us(f, , v, )

En terme de complexité classique, cet algorithme est en O(n) si n est le poids


de l’arbre f .

Calcul d’une représentation de l’opération f pSupp_mo


Nous savons déjà, d’après la définition du support, que dans un minimier non
vide f de racine r, r = bMin(f ). Nous savons par ailleurs, d’après la précondition
de l’opération, que la file de priorité considérée ne peut être vide et donc que le
minimier f qui le représente est différent de . Posons f = g, r, d. Le calcul de
l’opération f pSupp_mo s’en déduit :

A(f pSupp_mo(f ))
= Propriété caractéristique
A(f ) −̇ bMin(f )
= Hypothèse
A(g, r, d) −̇ bMin(f )
= Définition du support
A(g, r, d) −̇ r
= Définition de A
(A(g)  r  A(d)) −̇ r
368 Structures de données et méthodes formelles

= Propriété 3.7.5, page 104


(A(g)  A(d))  (r −̇ r)
= Propriété 3.7.4, page 104
(A(g)  A(d))  
= Propriété 3.7.4, page 104
A(g)  A(d)
= Propriété caractéristique de f us
A(f us(g, d))

En admettant que la précondition abstraite se traduise par f = , nous en


déduisons la version suivante de la représentation de l’opération f pSupp_mo :

function f pSupp_mo(f ) ∈ mo 
 mo =

pre
f = 
then
let g, r, d := f in
f us(g, d)
end
end

En terme de complexité classique, si n est le poids de l’arbre f , cet algorithme


est également en O(n).

L’opération f pF us_mo est identique à f us. Pour les autres opérations, le


calcul de la représentation est simple, il est laissé en exercice.

9.5.5 Analyse amortie de l’opération f us


Si nous en restions là en ce qui concerne la complexité, cette structure de
données ne présenterait que peu d’intérêt. Elle serait, sur de nombreux points,
de moins bonne qualité que la représentation par files binomiales. Mais outre la
simplicité de sa mise en œuvre, son avantage réside dans une propriété que nous
n’avons fait qu’évoquer : la complexité amortie de l’opération de fusion f us (et
par ricochet celle des opérations de mise à jour), qui est en O(log n) si n est la
somme des poids des arbres en arguments.
Nous proposons un « programme de démonstration » qui se présente en trois
points principaux :
1. définition de la fonction de potentiel Φ,
2. recherche d’une forme récurrente pour la fonction M(f us(f, f  )) qui four-
nit le coût amorti de la fusion,
3. majoration de cette fonction par une fonction logarithmique.

Notation. Si f est un minimier oblique, nous appelons f le poids de f plus 1 :


f = w(f ) + 1.
Une conséquence de cette définition est que la fonction  telle que 
a ∈ mo →
N1 peut être définie de la manière suivante :
9. Files de priorité 369

Notation Définition Condition


 1

g, r, d g + d g ∈ mo ∧ d ∈ mo ∧ r ∈ N

b) = 
Notons immédiatement la propriété suivante (non démontrée) : f us(a, a +b.

Selon la définition 4.3.2, page 125, le coût amorti M(f us(f, f  )) de l’opéra-
tion de fusion est donné par :

M(f us(f, f  )) = T (f us(f, f  )) + Φ(f us(f, f  )) − Φ((f, f  ))

où T (f us(f, f  )) est le coût « réel » d’une opération de fusion, Φ la fonction de


potentiel à valeurs dans R+ qui est telle que Φ(f us(f, f  )) est le potentiel de
la structure de données obtenue par la fusion et Φ((f, f  )) le potentiel avant la
fusion. Le potentiel du couple (f, f  ) est bien entendu égal à la somme du po-
tentiel de chaque arbre : Φ((f, f  )) = Φ(f ) + Φ(f  ). Nous définissons la fonction
T comme la fonction qui délivre le nombre d’appels à la fonction f us. La repré-
sentation calculée ci-dessus de la fonction f us permet de fournir une définition
formelle de T par l’équation récurrente suivante :


⎪ T (f us(, f  )) = 1

T (f us(f, )) = 1 

⎪    T (f us(d, f  )) Si r ≤ r
⎩ T (f us(g, r, d, g , r , d )) = 1 + 
T (f us(d , f )) Si r < r

Quant à la fonction de potentiel Φ(f ) retenue, elle dénombre les arbres p-


déséquilibrés (déséquilibrés du point de vue du poids) en faveur de la droite. Elle
se définit par :

Φ() = 0
(9.5.2)
Φ(g, r, d) = Φ(g) + ϕ(g, d) + Φ(d)

où la fonction ϕ se définit par 4 :

ϕ(a, b) ∈ mo
 × mo → R+
1 a < b
si 
ϕ(a, b) = (9.5.3)
0 a ≥ b
si 

Nous sommes prêt à aborder la seconde étape de la démonstration : la mise


sous forme d’équation récurrente de M(f us(f, f  )). En utilisant la représentation
de l’opération f us calculée ci-dessus, nous procédons à une induction sur la
structure des arguments. Pour le cas de base f =  nous avons :

M(f us(f, f  ))
= Hypothèse
M(f us(, f  ))
= Définition de M

4. On montre (cf. [101]) que cette définition est telle que ϕ(a, b) = max({log( 2 · b ), 0}).
a
+b
370 Structures de données et méthodes formelles

T (f us(, f  )) + Φ(f us(, f  )) − Φ() − Φ(f  )


= Définition de T et représentation de f us
1 + Φ(f  ) − Φ() − Φ(f  )
= Définition de Φ
1 + Φ(f  ) − 0 − Φ(f  )
= Calcul sur R
1

Le cas f  =  se développe de la même façon et fournit le même résultat. Pour


le cas inductif, f =  et f  =
, nous posons f = g, r, d et f  = g  , r , d .

M(f us(f, f  ))
= Hypothèse
M(f us(g, r, d, g  , r , d ))
= Définition de M
T (f us(g, r, d, g  , r , d )) + Φ(f us(g, r, d, g  , r , d ))
−Φ(f ) − Φ(f  )

Nous devons procéder à une analyse par cas selon que r ≤ r ou que r < r. Les
deux cas étant symétriques, seul le premier cas est développé.

T (f us(g, r, d, g  , r , d )) + Φ(f us(g, r, d, g  , r , d ))


−Φ(f ) − Φ(f  )
= Définition de T
1 + T (f us(d, f  )) + Φ(f us(g, r, d, g  , r , d )) − Φ(f ) − Φ(f  )
= Représentation de f us, hypothèse (r ≤ r )
1 + T (f us(d, f )) + Φ(f us(d, f ), r, g) − Φ(f ) − Φ(f  )
 

= Définition de Φ
1 + T (f us(d, f  )) + Φ(f us(d, f  )) + ϕ(f us(d, f  ), g) + Φ(g)
−Φ(g) − ϕ(g, d) − Φ(d) − Φ(f  )
= Calcul sur R+ et définition de M
1 + M(f us(d, f  )) + ϕ(f us(d, f  ), g) − ϕ(g, d)

Nous pouvons exploiter la symétrie et proposer la formulation récurrente suivante


de M (la troisième équation fait l’hypothèse que f =  et f  = ) :


⎪ M(f us(, f  )) = 1

M(f us(f, )) = 1 

⎪ M(f us(d, f  )) + ϕ(f us(d, f  ), g) − ϕ(g, d) Si r ≤ r
⎩M(f us(f, f  )) = 1 +
M(f us(d , f )) + ϕ(f us(d , f ), g ) − ϕ(g , d ) Si r < r
    

Nous abordons la troisième partie de la démonstration, celle qui consiste


à majorer la fonction M(f us(f, f  )). Nous choisissons de ne pas tenter de ré-
soudre directement cette équation récurrente. Nous proposons de démontrer par
induction que :

M(f us(f, f  )) ≤ log(f + f ) + log f + log f + 1 (9.5.4)

Débutons par le cas de base f = . D’après l’équation récurrente ci-dessus,


nous avons M(f us(, f  )) = 1. Par ailleurs :
9. Files de priorité 371

log(f + f ) + log f + log f + 1


= Hypothèse
 + f ) + log 
log(  + log f + 1
= Définition de 
log(1 + f ) + log 1 + log f + 1
≥  est positive et propriétés du log
log(1) + 1
= Propriétés du log
1

D’où, par transitivité, la propriété 9.5.4 pour le cas f = . Le second cas de
base (f  = ) se traite de la même façon et conduit au même résultat. Les deux
cas de la partie inductive sont également symétriques. Nous ne traitons que le
cas r ≤ r . Le calcul se fait sous l’hypothèse d’induction :

M(f us(d, f  )) ≤ log(d + f ) + log d + log f + 1

Compte tenu de la structure de la définition de la fonction ρ, ce calcul nous


conduit à considérer (a priori) quatre sous-cas : deux cas pour chaque sous-
formule du type ϕ(. . .) dans la définition récurrente de M. C’est ce que montre le
tableau 9.4. Le sous-cas (15) devrait respecter simultanément les deux inégalités
d+ f < g et g < d,
 ce qui entraînerait f < 0, qui est impossible. Pour les trois
autres cas, le tableau fournit la valeur des expressions ϕ(. . .) ainsi que l’inégalité
qui synthétise la condition représentée.

Tableau 9.4 – Analyse amortie de l’opération f us – les sous-cas.


Ce tableau présente les quatre cas possibles pour les deux expressions
de la forme ϕ(. . .) présentes dans l’équation récurrente définissant la
valeur de la complexité amortie de l’opération f us. Parmi ces quatre
cas, seuls trois sont possibles, le cas (15) conduit à une incohérence.
Chaque cellule du tableau fournit les valeurs des deux expressions ϕ(. . .)
ainsi qu’une synthèse des conditions sous la forme d’inégalités.


f us(d, f  ) < g 
f us(d, f  ) ≥ g
Cas (16) :
Cas (15) : ϕ(f us(d, f  ), g) = 0
g < d
impossible ϕ(g, d) = 1
1 ≤ g < d < d + f
Cas (17) : Cas (18) :
ϕ(f us(d, f  ), g) = 1 ϕ(f us(d, f  ), g) = 0
g ≥ d
ϕ(g, d) = 0 ϕ(g, d) = 0
1 ≤ d < d + f < g 1 ≤ d ≤ g ≤ d + f

Le calcul débute comme suit :


372 Structures de données et méthodes formelles

M(f us(f, f  ))
= Équation récurrente
1 + M(f us(d, f  )) + ϕ(f us(d, f  ), g) − ϕ(g, d)
≤ Hypothèse d’induction
    
2 + log(d + f ) + log d + log f + ϕ(f us(d, f ), g) − ϕ(g, d) (9.5.5)

À ce stade, il faut distinguer les trois cas significatifs qui sont présentés dans le
tableau 9.4, page précédente. Débutons par le cas (16) :

2 + log(d + f ) + log d + log f + ϕ(f us(d, f  ), g) − ϕ(g, d)


= Cas (16)
2 + log(d + f ) + log d + log f + 0 − 1
= Calcul sur R+
log(d + f ) + log d + log f + 1
≤ Propriétés de  et du log
g + d + f ) + log(
log(  + log f + 1
g + d)
= Définition de 
log(f + f ) + log(f) + log f + 1

Ce qui démontre l’inégalité pour le cas (16). Abordons à présent le cas (17) en
repartant de la formule 9.5.5 :

2 + log(d + f ) + log d + log f + ϕ(f us(d, f  ), g) − ϕ(g, d)


= Cas (17)
2 + log(d + f ) + log d + log f + 1 − 0
= Calcul sur R+
3 + log(d + f ) + log d + log f

Nous devons montrer que cette formule est inférieure ou égale à log(f + f ) +
log f + log f + 1 :

3 + log(d + f ) + log d + log f ≤ log(f + f ) + log f + log f + 1
⇔ Calcul sur R+ définition de 
2 + log(d + f ) + log d ≤ log( g + d + f ) + log( 
g + d)
⇔ Calcul sur R+ et propriétés du log
+d+
g  f +d
g
2 ≤ log   + log 
d+f d
⇔ Calcul sur R+ et propriétés du log

g 
g
2 ≤ log(1 +   ) + log(1 + )
d+f d
(9.5.6)

Les inégalités du tableau 9.4, cas (17), permettent de minorer les expressions

g 
g
  et  par 1. Puisque la fonction log est croissante, nous avons donc :
d+f d


log(1 + g
 f ) + log(1 + g)
d+ d
≥ Propriétés du log
log(1 + 1) + log(1 + 1)
= Propriétés du log
2
9. Files de priorité 373

Ce qui démontre l’inégalité 9.5.6 et achève la démonstration du cas (17). Abor-


dons enfin le cas (18) en repartant de la formule 9.5.5 :

2 + log(d + f ) + log d + log f + ϕ(f us(d, f  ), g) − ϕ(g, d)


= Cas (18)
2 + log(d + f ) + log d + log f + 0 − 0
= Calcul sur R+
2 + log(d + f ) + log d + log f

Nous devons montrer que cette formule est inférieure ou égale à log(f + f ) +
log f+log f +1. De manière analogue au cas (17), ceci nous conduit à démontrer
que :

1 ≤ log(1 + g
 f ) + log(1 + g)
d+ d


g 
g
Les inégalités du cas (18) nous permettent de minorer  f par 0 et par 1. En
d+ d
conséquence, nous avons :

log(1 + g
 f ) + log(1 + g)
d+ d
≥ Propriétés du log
log(1) + log(1 + 1)
= Propriétés du log
1

Ce qui achève la démonstration du cas (18). Nous avons donc démontré l’in-
égalité 9.5.4 pour r ≤ r . Par symétrie nous en déduisons que l’inégalité est
également démontrée pour le cas r > r . Le calcul se poursuit et s’achève par :

M(f us(f, f  ))
≤ Propriété 9.5.4
log(f + f ) + log f + log f + 1
≤ log est une fonction croissante
log(f + f ) + log(f + f ) + log(f + f ) + 1
= Calcul sur R+
3 · log(f + f ) + 1

D’où le résultat recherché : M(f us(f, f  )) est en O(log(f+ f )). Il est facile d’en
déduire que le coût amorti des opérations f pAjout_mo et f pSupp_mo sur un
arbre f est en O(log f) et celui de l’opération f pF us_mo(f, f  ) en O(log(f+f )).

9.5.6 Conclusion et remarques bibliographiques


À condition d’accepter les restrictions inhérentes à l’analyse amortie, la re-
présentation par minimiers obliques constitue une très bonne solution pour la
mise en œuvre de files de priorité fusionnables. Cette solution a été proposée
initialement par D.D. Sleator et R.E. Tarjan en 1986 [110] dans le cadre de leurs
travaux sur les structures autoadaptatives, sur la complexité amortie et les re-
lations qu’entretiennent ces deux concepts. L’article initial est plutôt de nature
374 Structures de données et méthodes formelles

descriptive et informelle. En particulier, à l’instar d’ailleurs de la majorité des


articles sur le sujet, un minimier oblique est défini comme étant soit un arbre
binaire vide soit le résultat de la fusion de deux minimiers obliques, ignorant de
ce fait les incidences que cette définition peut avoir sur la structure des arbres
et en particulier sur l’absence de point simple à droite (la figure 1 de la page 55
de [110] présente d’ailleurs comme minimiers obliques des arbres dotés de points
simples à droite). Concernant l’analyse amortie, celle-ci est introduite selon une
approche « itérative ». Nous avons adopté la fonction de potentiel de D.D. Slea-
tor et R.E. Tarjan, cependant, par son caractère inductif, notre analyse amortie 5
s’inspire plus des travaux d’A. Kaldewaij et B. Schoenmakers [69, 101]. L’objec-
tif de ces derniers auteurs est non seulement de déterminer un comportement
asymptotique pour la fonction M(f us(f, f  )) mais aussi de rechercher la plus
petite borne possible. Ils montrent, par une analyse sophistiquée, que, si φ est le
nombre d’or,

M(f us(f, f  )) ≤ logφ (w(f ) + w(f  ))

Dans un article ultérieur, B. Schoenmakers s’est attaché à rechercher une


borne minimale la plus grande possible. Ces développements vont bien entendu
au-delà de nos objectifs qui, rappelons-le, se limitent à la recherche d’un com-
portement asymptotique (pour une analyse amortie ou classique).
L’article de D.D. Sleator et R.E. Tarjan [110] étudie une version ascendante
des minimiers obliques. En terme d’analyse amortie, cette version est très effi-
cace (temps constant pour toutes les opérations sauf pour l’opération f pP rio).
Cependant, cette structure n’est pas de nature fonctionnelle (cf. [101] pour une
étude fonctionnelle des minimiers obliques ascendants).
Les minimiers gauchistes, qui font l’objet de l’exercice 9.5.9, sont souvent
considérés comme des précurseurs des minimiers obliques. Il s’agit cependant
d’arbres explicitement équilibrés. Selon D. Knuth [75], ils doivent être attribués
à C.A. Crane, en 1972.
Il existe des files de priorité fusionnables probabilistes, dotées d’excellentes
performances. C’est le cas des trois structures de données SBSH (Simple Bottom-
up Sampled Heap), BSH1 et BSH1 décrites par R. Sridhar et al. dans [98].
Cependant, ces structures de données ne sont pas de nature fonctionnelle. Elles
ne sont donc pas étudiées ici.

Exercices

Exercice 9.5.1 On appelle arbre oblique un arbre non étiqueté satisfaisant les propriétés
structurelles du support mo. Énumérer les arbres obliques de 1, 2, 3, . . . nœuds.

Exercice 9.5.2 Montrer que dans un arbre oblique de poids n la branche de droite contient
un nombre de nœuds inférieur ou égal à  n
2
.

Exercice 9.5.3 Quel est le minimier oblique obtenu par l’ajout successif des valeurs suivantes :
10, 7, 14, 1, 8, 19, 14, 2, 5, 17 et 7 dans un minimier vide ?

Exercice 9.5.4 Réaliser l’anatomie de la fusion des deux minimiers obliques ci-dessous :

5. Présentée initialement dans [48].


9. Files de priorité 375

1 6

35 12 21 13

14 22 50 30 19

17 26 31

Exercice 9.5.5 Dans le calcul de l’opération f us, montrer que les expressions du type (. . .)r
conduisent à des arbres filiformes.

Exercice 9.5.6 Dans le calcul de l’opération f us, montrer que les solutions 4 et 10 du ta-
bleau 9.3, page 365, conduisent à des arbres filiformes.

Exercice 9.5.7 En utilisant la même fonction de potentiel que celle utilisée dans l’analyse de
complexité ci-dessous, montrer
1. que la complexité amortie du parenthésage 12 du tableau 9.3, page 365, est la même
que celle développée pour le parenthésage 8,
2. que la complexité amortie des parenthésages 9 et 11 du tableau 9.3 n’est pas en O(log(n+
n )).

Exercice 9.5.8 Dans cet exercice on cherche à représenter les files de priorité par des minimiers
quelconques. Fournir une spécification concrète puis calculer la représentation de la fusion qui
interclasse la branche droite de l’arbre gauche et la branche gauche de l’arbre droit. Que
peut-on dire de la complexité des opérations ?

Exercice 9.5.9 (arbres gauchistes/leftist trees) L’objectif de l’exercice est d’implanter


des files de priorité en utilisant un type particulier de minimiers appelé minimiers gauchistes.
Compte tenu de la nature des minimiers binaires, il est possible d’échanger leurs deux sous-
arbres sans altérer la propriété d’être un minimier. Cette possibilité peut être exploitée pour
rétablir si nécessaire une propriété qui aurait été perdue au cours de manipulations. Dans un
arbre binaire, on appelle nœud externe (au sens large) un nœud qui n’a pas deux descendants
(qui est une feuille ou un point simple donc). Étant donné un arbre non vide, on considère
tous les chemins entre la racine et les nœuds externes. Parmi tous ces chemins, il en existe
(au moins un) de longueur inférieure ou égale à tous les autres en terme de nombre de nœuds
franchis. On appelle S la fonction qui, appliquée à un arbre binaire, délivre cette valeur. Le
support des arbres gauchistes se définit par :
1) a =  ⇒ a ∈ mg
2) a = g, r, d ∧ g, d ∈ mg × mg ∧ r ∈ N ∧ r = bMin(A(a)) ∧ S(g) ≥ S(d) ⇒ a ∈ mg
1. Définir la fonction S.
2. Montrer que, dans un arbre gauchiste, parmi tous les chemins entre la racine et les
nœuds externes, le chemin droit est le plus court.
3. Fournir une spécification concrète des files de priorité fusionnables par arbres gauchistes.
4. L’opération centrale de la mise en œuvre des files de priorité fusionnables à partir de
minimiers gauchistes est l’opération de fusion entre deux minimiers gauchistes. Elle se
définit comme suit. On considère le cas général où les deux arbres à fusionner ne sont
pas vides. Soit a celui des deux arbres qui a la racine la plus petite et b l’autre arbre.
L’un des deux sous-arbres du résultat est le sous-arbre gauche de a, l’autre sous-arbre
du résultat est l’arbre obtenu par la fusion du sous-arbre droit de a avec l’arbre b. Ces
deux sous-arbres sont positionnés à gauche et à droite de la racine de l’arbre résultat
de façon à préserver la propriété d’être un arbre gauchiste. Calculer une représentation
de l’opération de fusion. En déduire la représentation des autres opérations.
5. Quel est selon vous l’avantage des minimiers gauchistes sur les simples minimiers pour
la mise en œuvre de files de priorité ?

Exercice 9.5.10 Dans [93], C. Okasaki développe les files de priorité avec le support et les
heuristiques des arbres déployés (cf. section 6.8). Faire de même après avoir étudié cette sec-
tion 6.8 sur les arbres déployés en calculant la représentation des opérations puis en effectuant
une analyse amortie.
Chapitre 10

Tableaux flexibles

10.1 Introduction
Dans la plupart des langages de programmation, le concept de tableau
s’identifie à celui de fonction définie sur un intervalle constant de relatifs, inter-
valle connu dès la compilation (Pascal, C, etc.) ou au plus tard au moment où
s’exécute la déclaration du tableau (comme en Ada par exemple). On parle alors
de tableaux statiques. L’inconvénient bien connu de cette construction est que
le programmeur doit connaître, au plus tard quand l’exécutif réserve les empla-
cements pour le tableau, la taille maximale que peuvent avoir ses données (la
taille physique du tableau). En outre, il est parfois obligé de gérer séparément
la taille logique, quand celle-ci peut être inférieure ou égale à la taille physique.
Cet inconvénient, connu depuis longtemps, a incité les concepteurs de langages
de programmation à introduire plus de souplesse dans la gestion des tableaux
tout en préservant l’efficacité (par des opérations qui s’exécutent en temps quasi
constant). Pour simplifier, nous nous intéressons uniquement à des tableaux à
une seule dimension. Le terme de tableau flexible recouvre les différentes pos-
sibilités offertes au programmeur de faire varier le domaine de définition i .. s
d’un tableau au cours de son existence. Nous pouvons distinguer deux princi-
pales formes de flexibilité, la faible et la forte. La flexibilité faible se caractérise
par le fait que la modification n’est possible qu’aux bornes, soit en ajoutant soit
en supprimant une valeur. Si la variation ne peut avoir lieu que sur la borne
supérieure (resp. inférieure), nous parlons de flexibilité faible à droite (resp. à
gauche). La flexibilité forte se définit par la possibilité d’ajouter ou de suppri-
mer une valeur en toute position. Là aussi nous pouvons parler de flexibilité à
gauche ou à droite. Une opération complémentaire, la concaténation de tableaux,
est parfois intégrée au type abstrait « tableau flexible ».
Ce chapitre s’intéresse d’une part à une mise en œuvre particulière de la
flexibilité faible, celle par arbres de Braun, et d’autre part au principe de la
flexibilité forte par minimiers. Nous verrons que l’on sait parfaitement mettre en
œuvre la flexibilité faible sans concaténation alors que le problème de la flexibilité
forte n’est résolu que de façon imparfaite.
378 Structures de données et méthodes formelles

10.2 Flexibilité faible à droite : présentation


informelle
Outre les opérations usuelles sur les tableaux à une dimension (consultation
de la valeur en un point du domaine de définition, modification d’une valeur),
un tableau faiblement flexible à droite autorise la création d’un tableau vide,
l’allongement du domaine de définition par ajout d’un élément au-delà de la
borne supérieure, le raccourcissement du domaine de définition d’une position
de la borne supérieure et la consultation de la longueur courante du tableau. Par
convention, la borne inférieure est fixée à 1. Les six opérations suivantes sont
donc définies :
– twCreer, (tableau faiblement (weak ) flexible, créer) opération qui délivre
un tableau faiblement flexible vide (dont le domaine de définition est 1..0) ;
– twAll(v, t), (tableau faiblement flexible, allonger) opération d’allongement
d’une unité de la borne supérieure, qui délivre un tableau identique à t sur
le domaine de définition de t et valant v pour la nouvelle borne supérieure ;
– twRac(t), (tableau faiblement flexible, raccourcir) opération de raccourcis-
sement d’une unité de la borne supérieure, qui délivre un tableau identique
à t sur le nouveau domaine de définition. Cette opération exige que le do-
maine de définition initial de t ne soit pas vide ;
– twAf f (v, p, t), (tableau faiblement flexible, affectation) opération qui dé-
livre un tableau identique à t sauf au point p où il vaut v. Cette opération
exige que p appartienne au domaine de définition de t ;
– twV al(p, t), (tableau faiblement flexible, valeur) opération qui délivre la
valeur située à la position p du tableau t. Cette opération exige que p
appartienne au domaine de définition de t ;
– twLong(t), (tableau faiblement flexible, longueur) opération qui délivre la
longueur du tableau flexible t.
Notons qu’un tableau faiblement flexible à droite n’est autre qu’une pile
permettant l’adressage sur toute la pile.

10.3 Flexibilité faible à droite : spécification du


type abstrait twabst
Cette spécification est présentée à la figure 10.1. Dans la spécification des
opérations twAf f et twV al, la précondition implique que t = ∅. Le type est
paramétré par E, type des éléments du tableau, dont le support est e.

10.4 Flexibilité faible à droite : mise en œuvre


par arbres de Braun
Une forme d’arbres binaires particulièrement bien équilibrés, les arbres de
Braun, peut être utilisée pour mettre en œuvre les tableaux faiblement flexibles
à droite, de manière particulièrement efficace.
10. Tableaux flexibles 379

abstractType
twabst(E) =  (twAbst, (twCreer, twAll, twRac, twAf f ),
(twV al, twLong))
uses
N
support
s ∈ N ∧ t ∈ 1 .. s → e ⇔ t ∈ twAbst(E)
operations
function twCreer() ∈ twAbst(E) =  1 .. 0  ∅
;
function twAll(v, t) ∈ e × twAbst(E)   twAbst(E) =
t − {max(dom(t)) + 1 → v}
;
function twRac(t) ∈ twAbst(E) →  twAbst(E) = 
pre
t = ∅
then
1 .. max(dom(t)) − 1  t
end
;
function twAf f (v, p, t) ∈ e × N1 × twAbst(E) →  twAbst(E) =

pre
p ∈ dom(t)
then
t − {p → v}
end
;
function twV al(p, t) ∈ N1 × twAbst(E) →  e=

pre
p ∈ dom(t)
then
t(p)
end
;
function twLong(t) ∈ twAbst(E) → N =  max(dom(t))
end

Figure 10.1 – Spécification du type abstrait twabst.


Le type abstrait twabst spécifie les tableaux faiblement flexibles à
droite. e est le support du type E. L’adjonction et la suppression ne
sont autorisées qu’à la borne supérieure du tableau. twAll est l’opération
d’allongement, twRac l’opération de raccourcicement, twAf f l’opéra-
tion d’affectation, twV al l’opération de consultation, enfin twLong dé-
livre la longueur courante du tableau.
380 Structures de données et méthodes formelles

Flexibilité faible et langages de programmation


Pour ce qui concerne les langages de programmation universels in-
cluant le concept de tableau faiblement flexible, le premier en date est
Algol 68 [1]. Avec ce langage, le programmeur a la possibilité de dé-
clarer des tableaux dotés ou non de bornes flexibles. La déclaration
[0 flex : 10 flex] int t1 définit un tableau d’entiers t1 dont l’intervalle
de définition vaut 0 .. 10 lors de l’exécution de cette déclaration.
Dans [30] (page 99 et suivantes), E.W. Dijkstra définit un langage
disposant d’une forme de flexibilité permettant d’étendre ou de réduire
explicitement les bornes de l’intervalle de définition d’un tableau. Si t est
déclaré par t int array := (1, 13, −1), t est un tableau dont la borne
inférieure du domaine de définition est 1, la taille 2 (car 2 valeurs, 13 et
−1, sont présentes après la mention de la borne inférieure) et les valeurs
initiales de t[1] et t[2] sont respectivement 13 et −1. Le domaine de dé-
finition initial est donc 1 .. 2. L’exécution de t.hiext(x) (resp. t.loext(x))
a pour effet d’ajouter l’élément x immédiatement après (resp. avant) la
borne supérieure (resp. inférieure) courante (soit à la position d’indice 3
– resp. 0), tout en redimensionnant le domaine de définition (qui devient
1 .. 3 – resp. 0 .. 2).
Le langage Java [121] offre la possibilité de faire varier la borne
supérieure d’un tableau en utilisant la classe ArrayListe. Le texte
ArrayListe t= new ArrayListe(); déclare un tableau flexible défini
sur un intervalle débutant en 0 et ne contenant initialement aucun élé-
ment. t.add(new Integer(5)); a pour effet d’allonger la borne supérieure
du domaine de définition de un et de placer l’objet 5 dans le nouvel em-
placement. L’instruction t.remove(i) supprime (si i appartient bien au
domaine de définition de t) l’emplacement d’indice i tout en décalant les
cellules d’indices supérieurs à i vers le bas. La borne supérieure diminue
de un. Il s’agit donc d’une flexibilité faible pour les ajouts et forte pour
les suppressions.
Pour les deux langages Algol 68 et Java, la forme de flexibilité pro-
posée impose ou suggère la même stratégie de mise en œuvre. C’est celle
qui consiste à abandonner la zone de mémoire contiguë devenue trop ré-
duite pour en allouer une plus grande avant d’y effectuer la recopie des
valeurs initiales. Les actions d’allocation, de désallocation et de copie sont
automatiques et confiées à l’exécutif. Cette stratégie n’est pas aussi pé-
nalisante qu’il y paraît. En effet, il est possible de montrer qu’avec une
définition appropriée des structures de données et des opérations, une af-
fectation dans un contexte de recopie possède un coût amorti en O(1).
C’est également sans doute l’une des raisons du désintérêt relatif envers
la flexibilité faible : hormis le contexte du temps réel, la solution naïve de
réallocation et recopie est efficace (cf. exercice 10.4.11).
10. Tableaux flexibles 381

10.4.1 Flexibilité faible à droite : définition du support


concret
Un arbre de Braun est un arbre binaire étiqueté tel que, pour tous les nœuds,
la différence de poids entre le sous-arbre gauche et le sous-arbre droit est comprise
dans l’intervalle 0 .. 1. Formellement, si w est la fonction qui délivre le poids d’un
arbre, un tel arbre se décrit de la manière suivante :

1)  ∈ twBr(E)
2) r ∈ e ∧ g, d ∈ twBr(E) × twBr(E) ∧ w(g) − w(d) ∈ {0, 1}

g, r, d ∈ twBr(E)

Le schéma ci-dessous montre un arbre de Braun de 8 éléments, pour E = N.

12
14 7
3 10 3 15
7

L’une des caractéristiques des arbres de Braun est que le nombre de nœuds
(le poids) détermine la structure de l’arbre de manière unique. Ainsi le schéma ci-
dessous montre la structure des douze arbres de Braun depuis l’arbre de poids 1
jusqu’à l’arbre de poids 12 (les valeurs associées aux nœuds sont ignorées pour
le moment).

•1 •1 •1 •1 •1 •1

2• 2• •3 2• •3 2• •3 2• •3

4• 4• •5 4• 6• •5

•1 •1 •1

2• •3 2• •3 2• •3

4• 6• •5 •7 4• 6• •5 •7 4• 6• •5 •7

8• 8• •9

•1 •1 •1

2• •3 2• •3 2• •3

4• 6• •5 •7 4• 6• •5 •7 4• 6• •5 •7

8• 10 • •9 8• 10 • •9 • 11 8 • 12 • 10 • •9 • 11
382 Structures de données et méthodes formelles

Tableau 10.1 – Propriétés des arbres de Braun.


Ce tableau présente quelques-unes des relations qui lient le poids de
l’arbre et celui de ses sous-arbres. a est un arbre de Braun non vide et
w est la fonction qui délivre le poids d’un arbre.

Propriété Condition Ident.


w(g) = w(a) ÷ 2 a = g, r, d (10.4.1)
w(d) = (w(a) − 1) ÷ 2 a = g, r, d (10.4.2)
w(a) = 2 · w(g) + 1 ∧ w(d) = w(g) a = g, r, d ∧ w(a) mod 2 = 1 (10.4.3)
w(a) = 2 · w(g) ∧ w(d) = w(g) − 1 a = g, r, d ∧ w(a) mod 2 = 0 (10.4.4)

Dans un arbre de Braun, connaître le poids de l’arbre est équivalent à


connaître le poids de chacun des sous-arbres. Le tableau 10.1 regroupe plusieurs
des propriétés des arbres de Braun.
Les propriétés 10.4.1 et 10.4.2 peuvent être utilisées pour calculer le poids des
sous-arbres à partir du poids de l’arbre. L’ensemble de ces propriétés est mis à
profit lors du calcul des opérations ainsi que lors de la phase de raffinement visant
à s’affranchir des appels à la fonction w (cf. chapitre 10.4.5). La démonstration
de ces propriétés est laissée en exercice (cf. exercice 10.4.2).
La relation qui lie le poids n et le rayon r dans un arbre de Braun est la
suivante : r = log n + 1. Nous en concluons qu’à la condition d’éliminer les
appels aux fonctions auxiliaires éventuelles (cf. sections 1.10 et 2.4), toutes les
opérations qui parcourent tout ou partie du chemin de la racine vers une feuille
sont en O(log n). C’est le cas de toutes les opérations de mises à jour définies
dans le type abstrait.

10.4.2 Flexibilité faible à droite : définition de la fonction


d’abstraction
Plus qu’ailleurs, la compréhension de la fonction d’abstraction conditionne la
compréhension de cette mise en œuvre. La fonction d’abstraction est telle qu’un
arbre de Braun a représente un tableau flexible à droite t défini sur l’intervalle
1 .. w(a). La racine représente t(1), le sous-arbre gauche (resp. droit) représente
les valeurs aux positions d’indices pairs à partir de 2 (resp. impairs à partir de 3).
Sur le schéma ci-dessus, les valeurs apposées aux nœuds représentent les indices
dans le tableau. Compte tenu de ces conventions, nous avons la correspondance
suivante entre l’arbre étiqueté ci-dessous et le tableau qu’il représente :

12 1 2 3 4 5 6 7 8
14 7 12 14 7 3 2 10 15 7
3 10 2 15
7
10. Tableaux flexibles 383

Pour obtenir ce résultat, il suffit d’appliquer inductivement le schéma de


construction ci-dessous (le cas de base est trivial) :
1. placer la racine en t(1),
2. construire un tableau tg (resp. td) à partir du sous-arbre gauche (resp.
droit),
3. ventiler les valeurs du tableau tg (resp. td) sur les positions paires (resp.
impaires et différentes de un) d’un tableau tg  (resp. td ),
4. fusionner les trois tableaux t, tg  et td par des surcharges.

1 2 3 7
(1)
td 7 2 15
2 15
14 1 2 3 4
(1) ventilation
14 3 10 7 tg
3 10
3 4 5 6 7
7 ventilation 
td 7 2 15
2 3 4 5 6 7 8

tg 14 3 10 7

surcharge
1 2 3 4 5 6 7 8
(1) : hyp. d’induction t 12 14 7 3 2 10 15 7

Il n’est pas inutile d’insister sur le fait que la fonction d’abstraction n’a pas
à être implantée, elle ne sert que pour calculer les opérations. Ce processus de
passage de l’arbre au tableau se représente par la fonction d’abstraction A définie
ci-dessous. L’arbre vide correspond à un tableau vide. Un arbre non vide g, r, d
délivre un tableau dont le premier élément est r et dont les éléments situés
aux positions paires (resp. impaires) s’obtiennent par ventilation du tableau
correspondant au sous-arbre gauche (resp. droit) sur les positions paires (resp.
impaires).

function A(a) ∈ twBr(E)   twAbst(E) =



if a =  →
1 .. 0  ∅
| a = g, r, d →
{1 → r}
−
A(g) ◦ (λi ·(i ∈ N | 2 · i))−1
−
A(d) ◦ (λi ·(i ∈ N | 2 · i + 1))−1

Les expressions de surcharge méritent d’être explicitées. Prenons par exemple


l’expression A(g) ◦ (λi ·(i ∈ N | 2 · i))−1 . Ainsi que le montre la première tabu-
lation du tableau 10.2, la lambda expression λi ·(i ∈ N | 2 · i) représente la
384 Structures de données et méthodes formelles

Tableau 10.2 – Tabulations.


Tabulation de la fonction λi ·(i ∈ N | 2 · i) et de sa réciproque.

λi ·(i ∈ N | 2 · i) (λi ·(i ∈ N | 2 · i))−1


Argument Valeur Argument Valeur
0 → 0 0 → 0
1 → 2 2 → 1
2 → 4 4 → 2
3 → 6 6 → 3
4 → 8 8 → 4
5 → 10 10 → 5
.. ..
. .

Tableau 10.3 – Tabulation de la fonction A(g).

A(g)
Argument Valeur
1 → 14
2 → 3
3 → 10
4 → 7

fonction qui à tout entier associe son double. Sa réciproque est la fonction qui à
tout entier pair associe sa moitié (cf. la seconde tabulation).
D’après l’hypothèse d’induction, A(g) est le tableau obtenu à partir du sous-
arbre gauche. La représentation en extension de cette fonction est donnée au
tableau 10.3.
La composition des deux fonctions A(g) et (λi ·(i ∈ N | 2 · i))−1 est une
fonction de 4 éléments. Le tableau 10.4 détaille son calcul.

10.4.3 Flexibilité faible à droite : spécification des opéra-


tions concrètes

La figure 10.2, page 386, fournit la description du type concret twbr et


en particulier la spécification des opérations concrètes. De même que le type
abstrait, ce type est paramétré par E, qui est le type des éléments du tableau. En
effet la technique des arbres de Braun présente l’avantage de n’imposer aucune
contrainte sur ce type, qui peut tout à fait être un type structuré quelconque.
10. Tableaux flexibles 385

Tableau 10.4 – Tabulation de la fonction A(g) ◦ (λi ·(i ∈ N | 2 · i))−1 .


Ce tableau montre d’une part comment se composent les deux fonctions
A(g) et (λi ·(i ∈ N | 2 · i))−1 et d’autre part la tabulation du résultat de
la composition.

A(g) ◦ (λi ·(i ∈ N | 2 · i))−1


Argument Valeur
A(g) ◦ (λi ·(i ∈ N | 2 · i))−1
0 → 0
2 → 1 → 14 Argument Valeur
4 → 2 → 3 2 → 14
6 → 3 → 10 4 → 3
8 → 4 → 7 6 → 10
10 → 5 8 → 7
..
.

10.4.4 Flexibilité faible à droite : calcul des opérations


concrètes
L’expression abstraite dom(A(a)) apparaît dans certaines préconditions ainsi
que dans certains développements. Le calcul de sa contrepartie concrète 1 .. w(a)
est proposé à l’exercice 10.4.1. Nous développons successivement le calcul des
opérations twV al_br et twAll_br. Le calcul d’une représentation de l’opération
twRac_br est semblable à celui de l’opération twAll_br, seul le résultat est
proposé. Enfin, le calcul des autres opérations est laissé en exercice.

Calcul d’une représentation de l’opération twV al_br


L’opération twV al_br(p, a) délivre la valeur située à la position p du tableau
représenté par l’arbre a. La précondition p ∈ dom(A(a)) se réécrit p ∈ 1 .. w(a).
Elle implique que w(a) > 0 et donc que a = . Nous pouvons poser a = g, r, d.

twV al_br(p, a)
= Propriété caractéristique
A(a)(p)
= Hypothèse
A(g, r, d)(p)
= ⎛ ⎞ Définition de A
{1 → r}−
⎝ A(g) ◦ (λi ·(i ∈ N | 2 · i))−1 − ⎠ (p) (10.4.5)
A(d) ◦ (λi ·(i ∈ N | 2 · i + 1))−1

Procédons à une induction selon que p = 1 ou que p = 1. Débutons par le


cas de base p = 1.

({1 → r} − A(g) ◦ (λi ·(· · · ))−1 − A(d) ◦ (λi ·(· · · ))−1 )(p)
386 Structures de données et méthodes formelles

concreteType
twbr(E) =  (twBr, (twCreer_br, twAll_br, twRac_br, twAf f _br),
(twV al_br, twLong_br))
uses N, N1
refines twabst(E)
support
1)  ∈ twBr(E)
2) r ∈ e ∧ g, d ∈ twBr(E) × twBr(E) ∧ w(g) − w(d) ∈ {0, 1}

g, r, d ∈ twBr(E)
abstractionFunction
function A(a) ∈ twBr(E)   twAbst(E) =  ...
operationSpecifications
function twCreer_br() ∈ twBr(E) =  ...
; function twAll_br(v, a) ∈ e × twBr(E)  twBr(E) = 
b : (b ∈ twBr(E) ∧ A(b) = twAll(v, A(a)))
; function twRac_br(a) ∈ twBr(E) →  twBr(E) = 
pre
A(a) = ∅
then
b : (b ∈ twBr(E) ∧ A(b) = twRac(A(a)))
end
; function twAf f _br(v, p, a) ∈ e × N1 × twBr(E) →  twBr(E) = ...
; function twV al_br(p, a) ∈ N1 × twBr(E) →  e=
pre
p ∈ dom(A(a))
then
twV al(p, A(a))
end
; function twLong_br(a) ∈ twBr(E) → N =  twLong(A(a))
end

Figure 10.2 – Spécification du type concret twbr.


Le type concret twbr implante des tableaux faiblement flexibles à droite
par des arbres de Braun. La fonction d’abstraction est détaillée à la
section 10.4.2. Observons que ce type est paramétré par le type E, de
support e. w est la fonction qui délivre le poids d’un arbre de Braun.

= Hypothèse
({1 → r} − A(g) ◦ (λi ·(· · · ))−1 − A(d) ◦ (λi ·(· · · ))−1 )(1)
= Propriété C.13
{1 → r}(1)
= Propriété C.17
r

D’où la première équation gardée :


10. Tableaux flexibles 387

let g, r, d := a in
p=1→
twV al_br(p, a) = r
end
L’étape inductive, p = 1, se décompose en deux sous-cas : p pair et p impair.
Débutons par le cas où p est pair (p mod 2 = 0) en repartant de la formule 10.4.5
ci-dessus.
({1 → r} − A(g) ◦ (λi ·(· · · ))−1 − A(d) ◦ (λi ·(· · · ))−1 )(p)
= p ∈ dom(A(g) ◦ (λi ·(i ∈ N | 2 · i))−1 ) : propriétés C.14 et C.13
(A(g) ◦ (λi ·(i ∈ N | 2 · i))−1 )(p)
= Propriété C.7
A(g)((λi ·(i ∈ N | 2 · i))−1 )(p)
= Définition de −1 , section 1.6.3
A(g)(λi ·(i ∈ N ∧ i mod 2 = 0 | i ÷ 2))(p)
= Propriété C.3 et substitution
A(g)(p ÷ 2)
= Propriété caractéristique de l’opération twV al_br
twV al_br(p ÷ 2, g)
D’où l’équation gardée suivante :
let g, r, d := a in
p = 1 →
p mod 2 = 0 →
twV al_br(p, a) = twV al_br(p ÷ 2, g)
end
Pour le cas où p est impair et différent de 1, le calcul est similaire. Au total,
nous avons calculé la version suivante de l’opération twV al_br :

function twV al_br(p, a) ∈ N1 × twBr(E) → e =



pre
p ∈ 1 .. w(a)
then
let g, r, d := a in
if p = 1 →
r
| p = 1 →
if p mod 2 = 0 →
twV al_br(p ÷ 2, g)
| p mod 2 = 0 →
twV al_br(p ÷ 2, d)


end
end

La complexité de cette opération est en O(log n) pour un arbre de poids n.


388 Structures de données et méthodes formelles

Calcul d’une représentation de l’opération twAll_br


L’opération twAll_br(v, a) permet d’allonger le tableau représenté par
l’arbre a à l’extrémité de la borne supérieure et de placer la valeur v dans le
nouvel emplacement.

A(twAll_br(v, a))
= Propriété caractéristique de l’opération twAll_br
A(a) − {max(dom(A(a))) + 1 → v}
= dom(A(a)) = 1 .. w(a)
A(a) − {w(a) + 1 → v} (10.4.6)

À ce stade, il faut procéder à une induction sur la structure de a. Soit a = 


soit a = . Débutons par le cas de base a = .

A(a) − {w(a) + 1 → v}
= Hypothèse et définition de w
A() − {1 → v}
= Définition de A
{1 → v}
= Propriétés B.161 et B.162
{1 → v} − ∅ − ∅
= Propriété B.87
{1 → v} − (∅ ◦ (λi ·(i ∈ N | 2 · i))−1 ) − (∅ ◦ (λi ·(i ∈ N | 2 · i + 1))−1 )
= Définition de A
{1 → v} − A() ◦ (λi ·(i ∈ N | 2 · i))−1 − A() ◦ (λi ·(i ∈ N | 2 · i + 1))−1
= Définition de A
A(, v, )

D’où la première équation gardée :

a =  →
twAll_br(v, a) = , v, 

Pour l’étape inductive (a = ), nous pouvons poser a = g, r, d et repartir de


la formule 10.4.6 ci-dessus :

A(a) − {w(a) + 1 → v}
= Hypothèse
A(g, r, d) − {w(a) + 1 → v}
= ⎛ ⎞ Définition de A
{1 → r} −
⎝ A(g) ◦ (λi ·(i ∈ N | 2 · i))−1 − ⎠ − {w(a) + 1 → v} (10.4.7)
A(d) ◦ (λi ·(i ∈ N | 2 · i + 1))−1

Nous réalisons une analyse par cas selon la parité de w(a). Considérons tout
d’abord le cas w(a) mod 2 = 0 en supposant démontré le lemme suivant (cf. exer-
cice 10.4.3) :
Lemme 4. {w(a) + 1 → v} = {w(d) + 1 → v} ◦ (λi ·(i ∈ N | 2 · i + 1))−1
10. Tableaux flexibles 389

Poursuivons le calcul :

⎛ ⎞
{1 → r} −
⎝ A(g) ◦ (λi ·(i ∈ N | 2 · i))−1 − ⎠ − {w(d) + 1 → v}
A(d) ◦ (λi ·(i ∈ N | 2 · i + 1))−1
= ⎛ Propriété B.159

{1 → r} −
⎝ A(g) ◦ (λi ·(i ∈ N | 2 · i))−1 − ⎠ (10.4.8)
−1
(A(d) ◦ (λi ·(i ∈ N | 2 · i + 1)) ) − {w(d) + 1 → v}

Arrêtons-nous sur la dernière sous-formule afin de la simplifier :

(A(d) ◦ (λi ·(i ∈ N | 2 · i + 1))−1 ) − {w(d) + 1 → v}


=     Lemme 4
A(d)◦ {w(d) + 1 → v}◦
−
(λi ·(i ∈ N | 2 · i + 1))−1 (λi ·(i ∈ N | 2 · i + 1))−1
= Propriété B.82
(A(d) − {w(d) + 1 → v}) ◦ (λi ·(i ∈ N | 2 · i + 1))−1
= Propriété caractéristique de twAll_br
A(twAll_br(v, d)) ◦ (λi ·(i ∈ N | 2 · i + 1))−1

Reportons ce dernier résultat dans la formule 10.4.8 ci-dessus :

⎛ ⎞
{1 → r} −
⎝ A(g) ◦ (λi ·(i ∈ N | 2 · i))−1 − ⎠
(A(d) ◦ (λi ·(i ∈ N | 2 · i + 1))−1 ) − {w(d) + 1 → v}
= ⎛ ⎞ Calcul ci-dessus
{1 → r} −
⎝ A(g) ◦ (λi ·(i ∈ N | 2 · i))−1 − ⎠
A(twAll_br(v, d)) ◦ λi ·(i ∈ N | 2 · i + 1)−1
= Définition de A
A(g, r, twAll_br(v, d))

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

a = g, r, d →
w(a) mod 2 = 0 →
twAll_br(v, a) = g, r, twAll_br(v, d)

Dans le cas ou w(a) est impair, des considérations de symétrie nous conduisent
au résultat. Au total, nous obtenons la version suivante de l’opération twAll_br :
390 Structures de données et méthodes formelles

function twAll_br(v, a) ∈ e × twBr(E) → twBr(E) =



if a =  →
, v, 
| a = g, r, d →
if w(a) mod 2 = 0 →
g, r, twAll_br(v, d)
| w(a) mod 2 = 0 →
twAll_br(v, g), r, d

Reste à régler le problème de l’évaluation de la fonction w. Un renforcement


par décomposition est possible puisqu’à l’évidence la fonction w est décompo-
sable sur la structure de données. Sous cette hypothèse, la complexité de cette
opération est en O(log n) pour un arbre de poids n. Nous verrons cependant,
à la section 10.4.5, qu’il existe une solution préférable à la décomposition pour
raffiner la version ci-dessus de l’opération twAll_br.

Une représentation de l’opération twRac_br

L’opération twRac_br(a) supprime la cellule de droite du tableau représenté


par l’arbre a. Un calcul similaire au précédent nous conduit à la représentation
suivante :

function twRac_br(a) ∈ twBr(E) →  twBr(E) =



pre
w(a) = 0
then
if w(a) = 1 →

| w(a) = 1 →
let g, r, d := a in
if w(a) mod 2 = 0 →
twRac_br(g), r, d
| w(a) mod 2 = 0 →
g, r, twRac_br(d)

end

end

Si l’opération w est en temps constant, cette opération est en O(log(n) pour


un arbre de poids n. Concernant l’évaluation de la fonction w, la situation est
identique à celle rencontrée lors de l’opération twAll_br. Nous y consacrons la
prochaine section.
10. Tableaux flexibles 391

10.4.5 Décomposer la fonction w ?


Les deux opérations twRac_br et twAll_br ne sont pas utilisables en l’état :
le coût induit par l’évaluation de la fonction w est rédhibitoire. Ainsi que nous
l’avons vu aux sections 1.10 et 2.4, il est parfois possible d’améliorer la situation
en renforçant le support afin d’y héberger la valeur d’une fonction telle que
w. Cette solution est envisageable dans le cas des arbres de Braun (puisque
la fonction w est O(1)-décomposable). Elle conduit cependant à échanger un
problème de temps contre un problème de place. Compte tenu des propriétés des
arbres de Braun et en particulier du fait que la connaissance du poids d’un arbre
est équivalente à la connaissance de celui de ses sous-arbres, il est possible de
limiter le renforcement à l’ajout du poids de l’arbre « principal ». Nous sommes
alors à même de calculer quand nécessaire le poids de chaque sous-arbre. Ce
calcul se limite à une division par deux 1 , la complexité est préservée et de
plus le surcoût entraîné en terme d’espace mémoire est négligeable : un seul
emplacement pour le poids de l’arbre principal.
En guise d’exemple, nous présentons ci-dessous une version aménagée de
l’opération twAll_br exploitant la remarque ci-dessus. Le traitement de l’opé-
ration twRac_br serait similaire. Pour des raisons de lisibilité, nous n’avons pas
tenu compte de la modification exigée par le renforcement du support.

function twAll_br(v, a , w ) ∈ e × twBr(E) ×N → twBr(E) =



if a =  →
, v, 
| a = g, r, d →
if w mod 2 = 0 →
g, r, twAll_br(v, d, w ÷ 2 − 1 )
| w mod 2 = 0 →
twAll_br(v, g, w ÷ 2 ), r, d

Moyennant ce type d’aménagement, la complexité de toutes les opérations


est en O(log n) pour des arbres de poids n.

10.4.6 Conclusion et remarques bibliographiques


Relativement peu d’auteurs se sont penchés sur les tableaux faiblement
flexibles. Les chercheurs ont préféré se consacrer soit à la recherche de repré-
sentations fonctionnelles efficaces de tableaux statiques [29, 95, 23] soit (qui
peut le plus peut le moins) aux tableaux fortement flexibles (cf. section 10.5 et
suivantes). Il est vrai que face aux performances des arbres de Braun, apporter
une amélioration est un défi de taille. Une solution intéressante, bien que légè-
rement moins efficace, est proposée par V.J. Dielsen et A. Kaldewaij [28]. Elle
consiste à utiliser des arbres externes particuliers (cf. exercice 10.4.9). Cet article
1. Qui pour des programmes critiques peut être factorisée et réalisée par décalage. Il est
également possible de déléguer ce travail d’optimisation à un bon compilateur.
392 Structures de données et méthodes formelles

se présente comme un plaidoyer pour les arbres externes dont les avantages sont
illustrés à travers l’exemple des tableaux faiblement flexibles.
Dans [21], D.J. Challab s’intéresse également à la mise en œuvre de tableaux
faiblement flexibles à droite. Il utilise des arbres 2-3 particuliers (précurseurs des
B-arbres) pour lesquels les 3-nœuds sont situés sur la branche droite. L’extension
aux tableaux faiblement flexibles à gauche n’est pas envisagée et le résultat, bien
qu’en O(log n), est de qualité médiocre par rapport aux arbres de Braun.
D’autres tentatives méritent d’être citées [87, 90]. La solution proposée par
C. Okasaki dans [90] se situe dans le cadre de listes à accès direct. Elle peut
facilement être adaptée aux tableaux flexibles à droite. À l’évidence inspirée des
files binomiales de J. Vuillemin (cf. [114, 115] et section 9.4), il s’agit malgré
tout d’une solution originale qui peut donner lieu à un excellent projet sur les
structures de données (cf. exercice 10.4.7).
Les arbres de Braun ont été découverts en 1983 par W. Braun et M. Rem.
La présentation la plus accessible est cependant celle de R. Hoogerwoord [61].
C. Okasaki présente dans [92] une étude des arbres de Braun qui inclut la re-
cherche d’un algorithme efficace de construction d’un arbre de Braun à partir
d’une liste. L’un des seuls inconvénients qui puisse être opposé aux arbres de
Braun est qu’ils violent facilement le principe de localité (des accès mémoire
consécutifs peuvent être très dispersés), ce qui peut être préjudiciable à une
gestion efficace de la mémoire.

Exercices

Exercice 10.4.1 Calculer la représentation concrète de l’expression dom(A(a)).

Exercice 10.4.2 Démontrer les propriétés du tableau 10.1.

Exercice 10.4.3 Démontrer le lemme 4.

Exercice 10.4.4 Calculer l’équation gardée de l’opération twRac_br(a) dans le cas où w(a) =
1 ∧ w(a) mod 2 = 0

Exercice 10.4.5 Mettre en œuvre les files simples (cf. section 8.1)
1. par arbres de Braun,
2. par tableaux faiblement flexibles à droite.

Exercice 10.4.6 Fournir une spécification abstraite des tableaux faiblement flexibles aux
deux extrémités. Compléter pour cela la spécification de la figure 10.1. Mettre en œuvre cette
spécification par arbres de Braun.

Exercice 10.4.7 (Tableaux flexibles à droite par listes gloutonnes) Étant donné un
entier naturel n, il est possible de le décomposer de manière unique en un sac d’entiers naturels
v1 , v2 ,v3 · · · , vm  tel que :
m
1. i=1 vi = n et v1 ≤ v2 < v3 · · · < vm (seuls éventuellement les deux plus petits
éléments sont identiques),
2. tous les vi sont de la forme 2k − 1 (éléments de la suite de Mersenne 0, 1, 3, 7, 15, . . .).
3, 3, 7 est une décomposition correcte de 13, 1, 1, 1, 3, 7 ne l’est pas. 1, 3, 3, 7 n’est pas une
décomposition correcte de 14.
Il est possible de s’inspirer de ce système de numération (voir [90], ainsi que l’encadré
de la page 132) pour définir des « listes gloutonnes » destinées à représenter des tableaux
faiblement flexibles. Une liste gloutonne est une liste triée sur les poids croissants d’arbres
pleins (cf. figure 3.3, page 89). Ainsi la structure suivante :
10. Tableaux flexibles 393

5 4 10

12 13 8 2 7 11

9 6 15 1

est une liste gloutonne. En effet, le sac des poids, 3, 3, 7, est une décomposition correcte de
13. Un arbre plein de poids 2k − 1 peut représenter un tableau flexible de 2k − 1 éléments,
il suffit d’adopter par exemple un parcours postfixé droite-gauche. Par extension, une liste
gloutonne de n éléments peut représenter un tableau de longueur n quelconque.
1. Montrer que la décomposition définie ci-dessus est unique.
2. Spécifier le type concret twlg(E) (tableaux faiblement flexibles par listes gloutonnes)
qui raffine, par des listes gloutonnes, le type abstrait twabst de la page 379. Calculer
une représentation de ses opérations.
3. Implanter une version finale des opérations exige – pour des raisons d’efficacité – d’éli-
miner les appels à la fonction auxiliaire w. Proposer plusieurs méthodes. Discuter de
leur efficacité.
4. Calculer la complexité au pire des opérations du type twlg.

Exercice 10.4.8 (Tableaux faiblement flexibles à droite par files binomiales) Les
files binomiales ont été utilisées à la section 9.4, page 331, pour mettre en œuvre les files de
priorité fusionnables. En définissant une variante des files binomiales dans laquelle la propriété
de minimier disparaît, il est possible de mettre en œuvre efficacement des tableaux flexibles
à droite. Spécifier le type concret correspondant, calculer les opérations et en déduire leurs
complexités.

Exercice 10.4.9 (Tableaux faiblement flexibles par arbres externes) Les arbres ex-
ternes ont été introduits à la section 3.6. Dans cet exercice (inspiré de [28]), nous souhaitons
implanter les tableaux faiblement flexibles à droite par des arbres externes particuliers : les
arbres externes parfaits à gauche.
Un arbre externe plein est un arbre dont toutes les feuilles sont à la même hauteur. Un
arbre externe parfait à gauche est un arbre dans lequel le sous-arbre gauche est un arbre plein,
le sous-arbre droit est un arbre parfait à gauche et le rayon du sous-arbre gauche est supérieur
ou égal à celui du sous-arbre droit.
Il est possible de montrer qu’étant donné n ∈ N, il n’existe qu’un seul arbre parfait (non
étiqueté) à gauche possédant n feuilles. Ci-dessous sont représentés les 8 arbres parfaits à
gauche correspondant aux entiers de l’intervalle 1 .. 8.

Questions
1. Fournir la spécification concrète de la mise en œuvre des tableaux faiblement flexibles
selon la méthode des arbres parfaits à gauche.
2. Calculer la représentation fonctionnelle des opérations et leur complexité.
3. Sur la base des développements réalisés, réfléchir à une extension aux tableaux faible-
ment flexibles aux deux extrémités. Suggestion : utiliser des arbres externes tels que le
sous-arbre gauche (resp. droit) est un arbre parfait à droite (resp. à gauche).
394 Structures de données et méthodes formelles

Exercice 10.4.10 (Tableaux flexibles à droite par listes d’arbres externes


pleins) Dans cet exercice, nous proposons de mettre en œuvre des tableaux flexibles sur le
type E de support e en utilisant des files d’arbres externes pleins (f aeP ). Le support aeP (E)
des arbres externes pleins se définit par :

1) v ∈ e ⇒ v ∈ aeP (E)


2) g, d ∈ aeP (E) × aeP (E) ∧ r(g) = r(d) ⇒ g, d ∈ aeP (E)

où r(a) représente le rayon de l’arbre a.


1. Compléter la définition du type concret aep(E).
2. Définir le support f aeP (E) des files d’arbres externes pleins. Ce support spécifie que
la file est triée sur les rayons décroissants. Montrer qu’il existe une relation biunivoque
entre le nombre d’éléments du tableau flexible et la structure de la file.
3. Compléter la spécification concrète faep(E).
4. Calculer les opérations concrètes. En déduire leurs complexités.

Exercice 10.4.11 (Tableaux flexibles à droite par réallocation) On cherche à évaluer


la complexité amortie des tableaux faiblement flexibles gérés par réallocation. Le principe est
le suivant. Un tableau t possède une longueur logique n courante et une longueur physique
max (max ≥ n). On s’intéresse aux deux opérations twAll_r(v, t) et twRac_r(t) qui respec-
tivement introduit v en t(n + 1) et réduit de un la longueur logique de t. Initialement, n = 0
et max = 1. Le coût réel de twAll_r est de 1, cependant, si avant exécution de twAll_r,
n = max alors le tableau est réalloué dans une zone de taille 2 · max et les n valeurs sont
recopiées, auquel cas le coût réel est de n. Le coût réel de twRacc_r est de 1 sauf si après
l’opération, n ≤ max ÷ 4, auquel cas le tableau est réalloué dans une zone de longueur max ÷ 2
et les n éléments y sont recopiés. Dans cette éventualité, le coût réel de l’opération est de n.
Montrer que la complexité amortie des deux opérations est en temps constant.

10.5 Flexibilité forte : présentation informelle


Les tableaux fortement flexibles sont des tableaux ayant les fonctionnalités
de leurs homologues faiblement flexibles auxquelles s’ajoute la possibilité d’in-
sérer ou de supprimer une cellule à une position quelconque. Nous complétons
ces fonctionnalités par l’introduction de la concaténation de deux tableaux. Le
contraste entre le potentiel de ce type abstrait et l’échec dans la recherche d’une
mise en œuvre efficace en fait le Graal des structures de données. Les opéra-
tions d’insertion et de suppression généralisent les opérations twRac et twAll
des tableaux faiblement flexibles. Ces dernières disparaissent donc de la spéci-
fication abstraite. Nous n’avons pas défini l’équivalent de l’opération twAf f .
Elle s’obtient en composant les opérations tsSupp et tsIns. Elle peut également
apparaître en tant qu’opération primitive (cf. exercice 10.7.2).
Le type abstrait définit les opérations suivantes :
– tsCreer, opération qui délivre un tableau flexible vide ;
– tsIns(v, p, t), si t est un tableau de n éléments, c’est une opération qui
délivre un tableau identique à t à l’exception de la position p qui contient
la valeur v. La position p est définie si 1 ≤ p ≤ n + 1, expression qui
préconditionne cette opération. Cette opération provoque, dans le résultat,
le décalage « vers la droite » de la position des éléments qui étaient situés
aux positions p, p + 1, . . . , n ;
– tsSupp(p, t), si t est un tableau de n éléments, c’est une opération qui dé-
livre un tableau identique à t à l’exception la cellule située à la position p
10. Tableaux flexibles 395

qui disparaît du résultat. La position p est définie si 1 ≤ p ≤ n, expression


qui préconditionne cette opération. Cette opération provoque, dans le ré-
sultat, le décalage « vers la gauche » de la position des éléments qui étaient
situés aux positions p + 1, . . . , n ;
– tsConc(t, t ), opération qui délivre un tableau flexible résultant de la conca-
ténation des deux tableaux fortement flexibles t et t ;
– tsV al(p, t), si t est un tableau de n éléments, c’est une opération qui délivre
l’élément situé en position p dans le tableau flexible t (position définie si
1 ≤ p ≤ n, expression qui préconditionne cette opération) ;
– tsLong(t), opération qui délivre la longueur du tableau flexible t.

10.6 Flexibilité forte à droite : spécification du


type abstrait tsabst
Le type abstrait tsabst est spécifié formellement à la figure 10.3, page sui-
vante. Le support abstrait définit tsAbst comme une fonction totale sur un
intervalle d’entiers naturels débutant à 1. La spécification des opérations re-
prend les descriptions ci-dessus en les formalisant. Ainsi par exemple l’opération
tsIns(v, p, t) délivre un tableau défini en trois parties. La première partie est
identique au fragment de t qui va de 1 à p − 1. La deuxième partie se réduit à
la cellule rajoutée, qui contient v. La troisième partie est identique au second
fragment de t sur lequel est opéré un décalage vers la droite d’une position.

10.7 Flexibilité forte à droite : mise en œuvre par


minimiers
10.7.1 Principe
Cette solution exige que les valeurs du tableau proviennent d’un ensemble sur
lequel est définie une structure d’ordre. Pour simplifier, nous considérons qu’il
s’agit d’entiers naturels. Nous assimilons pour cela le support N au « type » N. Il
s’agit d’une contrainte forte, qui ne facilite pas la généralisation à des tableaux
à deux dimensions ou plus.
Le support concret de cette représentation est un minimier sur N. Il se dé-
nomme tsM b et se spécifie formellement par :

1)  ∈ tsM b
2) r ∈ N ∧ g, d ∈ tsM b × tsM b ∧ a = g, r, d ∧ r = min(ens(a))

a ∈ tsM b

La fonction ens(a) délivre l’ensemble des valeurs présentes dans le minimier


a. Le schéma ci-dessous montre deux exemples de minimiers. L’exemple de droite
montre un minimier (très) déséquilibré puisqu’il s’agit d’un arbre filiforme. Le
fait que le concept de minimier ne véhicule pas de notion d’équilibre laisse pré-
sager des algorithmes dont la complexité au pire est en O(n).
396 Structures de données et méthodes formelles

abstractType
tsabst(E) =  (tsAbst, (tsCreer, tsIns, tsSupp, tsConc), (tsV al, tsLong))
uses N, N1
support
n ∈ N ∧ t ∈ 1 .. n → e ⇔ t ∈ tsAbst(E)
operations
function tsCreer() ∈ tsAbst(E) =  1 .. 0  ∅
;
function tsIns(v, p, t) ∈ e × N1 × tsAbst(E) →  tsAbst(E) =

pre
p ∈ 1 .. max(dom(t)) + 1
then
1 .. p − 1  t − {p → v} − ((p .. max(dom(t))  t)  1)
end
;
function tsSupp(p, t) ∈ N1 × tsAbst(E)   tsAbst(E) =
pre
p ∈ dom(t)
then
1 .. p − 1  t − ((p + 1 .. max(dom(t))  t)  −1)
end
;
function tsConc(t, t ) ∈ tsAbst(E) × tsAbst(E)  tsAbst(E) = 
t − (t  card(dom(t)))
;
function tsV al(p, t) ∈ N1 × tsAbst(E) →  e= 
pre
p ∈ dom(t)
then
t(p)
end
;
function tsLong(t) ∈ tsAbst(E) → N =  max(dom(t))
end

Figure 10.3 – Spécification du type abstrait tsabst.


tsabst représente les tableaux fortement flexibles à droite. Les inser-
tions sont possibles sur toute position existante ainsi qu’à l’extrémité
droite. Les suppressions sont possibles sur tout l’intervalle de définition.
L’opération de concaténation de deux tableaux est également spécifiée.

7 7
9 7 7
17 10 10 19 8
14 15 17 19
23
10. Tableaux flexibles 397

La fonction d’abstraction A associe à chaque élément du support tsM b un


tableau dont le domaine est un intervalle qui débute en position 1. La fonction
w, définie comme fonction auxiliaire du type tsmb, délivre le poids (le nombre
d’éléments) d’un arbre tsM b. Un arbre vide correspond à un tableau vide. Le
schéma ci-dessous montre comment un minimier non vide représente un tableau
fortement flexible à droite. En supposant que nous sachions obtenir les tableaux
représentés par les sous-arbres gauche et droit (hypothèse d’induction), le tableau
final s’obtient en concaténant le premier tableau, la valeur issue de la racine et
le second tableau décalé vers la droite de la longueur du premier tableau plus
un.
7
9 7
17 10 10 19
14 13 17

hyp. d’induction
1 2 3 4 1 2 3 4 5
17 9 14 10 13 10 17 7 19
concaténation

17 9 14 10 7 13 10 17 7 19
1 2 3 4 5 6 7 8 9 10

C’est cette conversion qui est décrite par la fonction d’abstraction ci-dessous.

function A(a) ∈ tsM b  tsAbst =



if a =  →
1 .. 0  ∅
| a = g, r, d →
A(g) − {w(g) + 1 → r} − A(d)  w(g) + 1

Le calcul complet de cette solution est proposé à l’exercice 10.7.1. La seule


fonction utilisée dans la représentation des opérations est la fonction w qui dé-
livre le poids d’un arbre. Ainsi que nous l’avons déjà vu à plusieurs reprises,
nous devons songer à renforcer la structure de données (cf. 6.5.5). La fonction
w est en O(1). Elle est donc décomposable sur la structure de données : il suf-
fit d’enrichir chaque nœud par un champ destiné à recevoir le poids de l’arbre
correspondant. Nous obtenons ainsi une solution pour les tableaux fortement
flexibles à droite dont la complexité est au pire de O(n). En retenant l’hypo-
thèse suivante : tableaux sans doublon de n éléments pour lesquels les n! ordres
398 Structures de données et méthodes formelles

relatifs sont équiprobables, on montre (cf. [115, 116]) que la complexité moyenne
des opérations est en O(log n).

10.7.2 Conclusion et remarques bibliographiques


La solution ébauchée ci-dessus pour les tableaux fortement flexibles avec
concaténation est due à J. Vuillemin [115, 116]. Dans ces documents fondateurs,
la décomposition du poids sur la structure de données est faite d’emblée, et non
comme ici par raffinement. Dans [115] le type abstrait est appelé (abusivement
selon notre terminologie) « liste linéaire » tandis que la structure de données
concrète est dénommée « tournoi binaire de position (ou de rang) ». Dans [116]
elle est présentée comme une variante d’arbres « à tout faire » : les arbres car-
tésiens (cf. section 6.9.6).
L’atout principal de cette solution est de permettre une mise en œuvre rai-
sonnablement efficace d’une structure aussi complexe que les tableaux fortement
flexibles. Dans l’absolu, ses points faibles sont tout d’abord la complexité des opé-
rations mais également l’obligation qu’il y a à travailler sur des valeurs dotées
d’une structure d’ordre total. Cependant on ne sait guère faire mieux aujour-
d’hui.
À défaut de mettre la main sur le Graal des structures de données, les cher-
cheurs se sont tournés vers des types abstraits moins ambitieux, en supprimant
une ou plusieurs opérations du type abstrait « tableaux fortement flexibles »,
ou en les remplaçant parfois par d’autres opérations mais toujours en recher-
chant des performances les meilleures possibles. Le cas le plus typique est celui
où l’on supprime les opérations à adressage direct (consultation, modification
insertion ou suppression d’un élément quelconque) tout en préservant les autres
opérations. Le type abstrait obtenu, qui n’est plus un tableau, est traditionnel-
lement dénommé « deque » (Double-Ended Queue) concaténable (si l’opération
de concaténation est autorisée). L’objectif étant alors d’obtenir des opérations
en temps constant, soit par une analyse de complexité classique soit par une
analyse amortie. Parmi les références les plus intéressantes, citons [91, 72, 70].
Dans le cas où l’affaiblissement du type abstrait se fait simplement en suppri-
mant l’opération de concaténation, certains des supports déjà utilisés à d’autres
fins peuvent être réemployés. C’est par exemple le cas des Avl (cf. section 6.6),
des files binomiales (cf. section 9.4), des files gloutonnes (cf. ce chapitre, exer-
cice 10.4.7), etc.

Exercices

Exercice 10.7.1 Compléter la spécification et le calcul des opérations de la mise en œuvre


des tableaux fortement flexibles par minimiers. Réfléchir à la décomposition de la fonction w
sur la structure de données.

Exercice 10.7.2 Nous voulons introduire l’opération tsAf f (v, p, t) dans le type abstrait
tsabst. Cette opération délivre un tableau flexible dans lequel l’élément en position p a comme
valeur v, le reste du tableau étant inchangé. Spécifier l’opération abstraite. Spécifier l’opération
concrète correspondante tsAf f _mb(v, p, a), puis calculer une représentation en utilisant un
calcul direct.
10. Tableaux flexibles 399

Exercice 10.7.3 Après avoir spécifié le type abstrait « tableau fortement flexible sans conca-
ténation », développer une mise en œuvre sur la base de l’un des supports suivants : Avl, file
binomiale ou file gloutonne (cf. ce chapitre, exercice 10.4.7 pour les files gloutonnes).
Annexes
Répertoire de propriétés
Annexe A

Propriétés générales des


ensembles

Avertissement. La plupart des théorèmes et propriétés regroupés dans les


annexes ont été publiés sous cette forme dans l’ouvrage [3].

Propriétés des ensembles Condition Ident.


v ∈ (a ∪ b) ⇔ v ∈ a ∨ v ∈ b v∈s ∧ a⊆s ∧ b⊆s (A.1)
v ∈ (a ∩ b) ⇔ v ∈ a ∧ v ∈ b v∈s ∧ a⊆s ∧ b⊆s (A.2)
v ∈ (a − b) ⇔ v ∈ a ∧ v ∈
/b v∈s ∧ a⊆s ∧ b⊆s (A.3)
v ∈ {w} ⇔ (v = w) v∈s ∧ w∈s (A.4)
v ∈ a ⇔ {v} ⊆ a v∈s ∧ a⊆s (A.5)
¬(v ∈ ∅) v∈s (A.6)
a∪b = b∪a a⊆s ∧ b⊆s (A.7)
(a ∪ b) ∪ c = a ∪ (b ∪ c) a⊆s ∧ b⊆s ∧ c⊆s (A.8)
a∪b = a a⊆s∧b⊆s∧b⊆a (A.9)
((b − a) ∪ a) = b a⊆s ∧ b⊆s ∧ a⊆b (A.10)
(a ∪ b = ∅) ⇔ (a = ∅ ∧ b = ∅) a⊆s ∧ b⊆s (A.11)
⎛  ⎞
i ·(i ∈ u | E) u ⊆ s ∧ v ⊆ s∧
 ⎜ ⎟
i ·(i ∈ u ∪ v | E) = ⎝ ⎜ ∪ ⎟ E expression (A.12)


i ·(i ∈ v | E) ensembliste
⎛  ⎞
i ·(i ∈ u | E) u ⊆ s ∧ v ⊆ s∧
 ⎜ ⎟
i ·(i ∈ u − v | E) = ⎜ ⎝ − ⎟
⎠ E expression (A.13)

i ·(i ∈ v | E) ensembliste

⎛ i·(i ∈ u ∪ v | R[{i}]) = ⎞
i ·(i ∈ u | (u  R)[{i}]) u ⊆ s ∧ v ⊆ s∧
⎜ ⎟
⎜ ∪ ⎟ dom(R) ⊆ s ∧ (A.14)
⎝ ⎠

i ·(i ∈ v | (v  R)[{i}]) R relation

i ·(i = j | E) = [i := j]E i∈s∧j ∈s (A.15)
404 Structures de données et méthodes formelles

Propriétés des ensembles Condition Ident.


a⊆s ∧ b⊆s
a−b = a (A.16)
∧a∩b = ∅
(a ∪ b) − c = (a − c) ∪ (b − c) a⊆s ∧ b⊆s ∧ c⊆s (A.17)
a⊆s ∧ b⊆s
a−b = ∅ (A.18)
∧a⊆b
a⊆b ∧ b⊆c ⇒ a⊆c a⊆s ∧ b⊆s ∧ c⊆s (A.19)
a∩b = b∩a a⊆s ∧ b⊆s (A.20)
(a ∩ b) ∩ c = a ∩ (b ∩ c) a⊆s ∧ b⊆s ∧ c⊆s (A.21)
a∩a = a a⊆s (A.22)
a∩∅ = ∅ a⊆s (A.23)
b⊆a ∧ a∩c=∅ ⇒ b∩c=∅ a⊆s ∧ b⊆s ∧ c⊆s (A.24)
a ⊆ (b ∪ c) ∧ a ∩ c = ∅ ⇒ a ⊆ b a⊆s ∧ b⊆s ∧ c⊆s (A.25)
a∩b=a a⊆s ∧ b⊆s ∧ a⊆b (A.26)
{x | x ∈ u ∧ P } ∩ {x | x ∈ u ∧ ¬P } = ∅ P prédicat quelconque (A.27)
{x | x ∈ s ∧ x ∈ a} = a a⊆s (A.28)
Annexe B

Propriétés des relations


binaires

Propriétés des relations binaires Condition Ident.


u  {i, j | i ∈ s ∧ j ∈ t ∧ P }
⇔ u⊆s (B.1)
{i, j | i ∈ s ∧ j ∈ t ∧ i ∈ u ∧ P }

Propriétés de l’inverse Condition Ident.


−1−1
r = r r ∈s↔t (B.2)
−1 −1 −1
(p ; q) = q ;p p∈s↔t∧q ∈t↔u (B.3)
−1
id(s) = id(s) (B.4)
−1 −1
(u  r) = r u u⊆ s∧ r ∈s↔t (B.5)
(r  v)−1 = v  r−1 r ∈s↔t∧v ⊆ t (B.6)
−1 −1
(u 
− r) = r −u
 u⊆ s∧ r ∈s↔t (B.7)
−1 −1
(r 
− v) = v
−r r ∈s↔t∧v ⊆ t (B.8)
−1 −1 −1
(p − q) = (p − dom(q)) ∪ q
 p∈s↔t∧q ∈s↔t (B.9)
−1 −1 −1
(p ∪ q) = p ∪q p∈s↔t∧q ∈s↔t (B.10)
−1 −1 −1
(p ∩ q) = p ∩q p∈s↔t∧q ∈s↔t (B.11)
−1 −1 −1
(p − q) = p −q p∈s↔t∧q ∈s↔t (B.12)
−1
{x → y} = {y → x} x∈s∧y ∈t (B.13)
−1
r = ∅ r ∈s↔t∧r =∅ (B.14)
(s × t)−1 = t × s (B.15)
406 Structures de données et méthodes formelles

Propriétés du domaine Condition Ident.


dom(f ) = s f ∈s→t (B.16)
−1
dom(r ) = ran(r) r ∈s↔t (B.17)
−1
dom(p ; q) = p [dom(q)] p∈s↔t∧q ∈t↔u (B.18)
dom(p ◦ q) = q −1 [dom(p)] p∈t↔u ∧ q ∈s↔t (B.19)
p ∈ s ↔ t ∧ q ∈ t ↔ u∧
dom(p ; q) = dom(p) (B.20)
ran(p) ⊆ dom(q)
dom(id(s)) = s (B.21)
dom(u  r) = u ∩ dom(r) u⊆s∧ r ∈s↔t (B.22)
−1
dom(r  v) = r [v] r ∈s↔t∧v ⊆s (B.23)
dom(u 
− r) = dom(r) − u u⊆s∧r ∈s↔t (B.24)
−1
dom(f 
− v) = dom(f ) − f [v] r ∈s↔t∧v ⊆s (B.25)
dom(p − q) = dom(p) ∪ dom(q) p∈s↔t ∧ q ∈s↔t (B.26)
dom(p ∪ q) = dom(p) ∪ dom(q) p∈s↔t ∧ q ∈s↔t (B.27)
f ∈s→  t ∧ g ∈s→  t∧
dom(f ∩ g) = dom(f ) ∩ dom(g) (B.28)
dom(f )  g = dom(g)  f
f ∈s→  t ∧ g ∈s→  t∧
dom(f − g) = dom(f ) − dom(g) (B.29)
dom(f )  g = dom(g)  f
dom({x → y}) = {x} x∈s ∧ y∈t (B.30)
dom(r) = ∅ x∈s↔t ∧ r =∅ (B.31)
dom(s × t) = s t = ∅ (B.32)

Propriétés du codomaine Condition Ident.


ran(f ) = t f ∈s
 t (B.33)
−1
ran(r ) = dom(r) r ∈s↔t (B.34)
ran(p ; q) = q[ran(p)] p∈s↔t∧q ∈t↔u (B.35)
p∈s↔t∧q ∈t↔u
ran(p ; q) = ran(p) (B.36)
dom(q) ⊆ ran(p)
ran(id(s)) = s (B.37)
ran(u  r) = r[u] u⊆s∧r ∈s↔t (B.38)
ran(u  v) = ran(r) ∩ v r ∈s↔t∧v ⊆t (B.39)
−1
ran(u 
− r) = ran(r) − r[u] u⊆s∧r ∈t→
 s (B.40)
ran(u 
− v) = ran(r) − v r ∈s↔t∧v ⊆t (B.41)
ran(p 
− q) = ran(dom(q) 
− p) ∪ ran(q) p∈s↔t∧q ∈s↔t (B.42)
ran(p ∪ q) = ran(p) ∪ ran(q) p∈s↔t∧q ∈s↔t (B.43)
p−1 ∈ t →
 s ∧ q −1 ∈ t →
 s
ran(p ∩ q) = ran(p) ∩ ran(q) (B.44)
q  ran(p) = p  ran(q)
B. Propriétés des relations binaires 407

Propriétés du codomaine Condition Ident.


−1 −1
p ∈t→
 s∧q ∈t→
 s
ran(p − q) = ran(p) − ran(q) (B.45)
q  ran(p) = p  ran(q)
ran({x → y}) = {y} x∈s∧y ∈t (B.46)
ran(r) = ∅ r ∈s↔t∧r =∅ (B.47)
ran(s × t) = t s = ∅ (B.48)

Propriétés de l’image Condition Ident.


(p ; q)[u] = q[p[u]] p∈s↔t∧q ∈t↔v∧u⊆s (B.49)
−1 −1
(r ; r )[u] = u r ∈t→
 s ∧ u ⊆ dom(r) (B.50)
id(u)[v] = u ∩ v u⊆s∩v⊆s (B.51)
(u  r)[v] = r[u ∩ v] u⊆s∧r ∈s↔t∧v ⊆s (B.52)
(r  v)[u] = r[u] ∩ v r ∈s↔t∧v ⊆t∧u⊆s (B.53)
(u 
− r)[v] = r[u − v] u⊆s∧r ∈s↔t∧v ⊆s (B.54)
(r 
− v)[u] = r[u] − v r ∈s↔t∧v ⊆t∧u⊆s (B.55)
(p − q)[u] =
p∈s↔t∧q ∈s↔t∧u⊆s (B.56)
(dom(q) − p)[u] ∪ q[u]
(p ∪ q)[u] = p[u] ∪ q[u] p∈s↔t∧q ∈s↔t∧u⊆s (B.57)
−1 −1
p ∈t→  s∧q ∈t→  s ∧ u ⊆ s∧
(p ∩ q)[u] = p[u] ∩ q[u] (B.58)
(p  ran(q)) = (q  ran(p))
p−1 ∈ t →
 s ∧ q −1 ∈ t →
 s ∧ u ⊆ s∧
(p − q)[u] = p[u] − q[u] (B.59)
(p  ran(q)) = (q  ran(p))
{x → y}[u] = {y} x∈s∧y ∈t∧u⊆s∧x∈u (B.60)
{x → y}[u] = ∅ x∈s∧y ∈t∧u⊆s∧x∈
/u (B.61)
r[u] = ∅ r ∈ s ↔ t ∧ (dom(r) ∩ u) = ∅ (B.62)
(u × t)[v] = t u ⊆ s ∧ v ⊆ s ∧ u ∩ v = ∅ (B.63)
(u × t)[v] = ∅ u⊆s∧v ⊆s∧u∩v =∅ (B.64)
r[u ∪ v] = r[u] ∪ r[v] r ∈s↔t∧u⊆s∧v ⊆s (B.65)
−1
r[u ∩ v] = r[u] ∩ r[v] r ∈t→
 s∧u⊆s∧v ⊆s (B.66)
−1
r[u − v] = r[u] − r[v] r ∈t→
 s∧u⊆s∧v ⊆s (B.67)
f [{x}] = {f (x)} f ∈s→
 t ∧ x ∈ dom(f ) (B.68)
r[∅] = ∅ r ∈s↔t (B.69)
r[dom(r)] = ran(r) r ∈s↔t (B.70)
−1
r [ran(r)] = dom(r) r ∈s↔t (B.71)
(p  v)[u] = p[u  −v] p∈s↔t∧v ∈Z∧u⊆s (B.72)
p[u] = (p  v)[u  v] p∈s↔t∧v ∈Z∧u⊆s (B.73)
f [u] = ran(f ) f ∈s→
 t ∧ dom(f ) ⊆ u (B.74)
408 Structures de données et méthodes formelles

Propriétés de la composition Condition Ident.


r ; (p ; q) = r ; p ; q r ∈s↔t∧p∈t↔u∧q ∈u↔v (B.75)
r ; id(v) = r  v r ∈s↔t∧v ⊆t (B.76)
r ; id(t) = r r ∈s↔t (B.77)
r ; (v  p) = (r  v) ; p r ∈s↔t∧v ⊆t∧p∈t↔u (B.78)
r ; (p  w) = (r ; p)  w r ∈s↔t∧p∈t↔u∧w ⊆t (B.79)
r ; (v 
− p) = (r 
− v) ; p r ∈s↔t∧v ⊆t∧p∈t↔u (B.80)
r ; (p 
− w) = (r ; p) 
−w r ∈s↔t∧p∈t↔u∧w ⊆t (B.81)
f ; (p 
− q) = (f ; p) 
− (f ; q) f ∈s→
 t∧p∈t↔u∧q ∈t↔u (B.82)
r ; (p ∪ q) = (r ; p) ∪ (r ; q) r ∈s↔t∧p∈t↔u∧q ∈t↔u (B.83)
f ; (p ∩ q) = (f ; p) ∩ (f ; q) f ∈s→
 t∧p∈t↔u∧q ∈t↔u (B.84)
f ; (p − q) = (f ; p) − (f ; q) f ∈s→
 t∧p∈t↔u∧q ∈t↔u (B.85)
−1
r ; {x → y} = r [{x}] × {y} r ∈s↔t∧x∈t∧y ∈u (B.86)
r;p = ∅ r ∈s↔t∧p∈t↔u∧p=∅ (B.87)
−1
r ; (u × v) = r [u] × v r ∈s↔t∧u⊆t (B.88)
id(u) ; r = u  r u⊆s∧r ∈s↔t (B.89)
id(s) ; r = r r ∈s↔t (B.90)
(u  p) ; r = u  (p ; r) u⊆s∧p∈s↔t∧r ∈t↔u (B.91)
(p  v) ; r = p ; (v  r) p∈s↔t∧v ⊆t∧r ∈t↔u (B.92)
(u 
− p) ; r = u 
− (p ; r) u⊆s∧p∈s↔t∧r ∈t↔u (B.93)
(p 
− v) ; r = p ; (v 
− r) p∈s↔t∧v ⊆t∧r ∈t↔u (B.94)
p ∈ s ↔ t ∧ q ∈ s ↔ t∧
(p 
− q) ; r = (p ; r) 
− (q ; r) (B.95)
r ∈ t ↔ u ∧ ran(q) ⊆ dom(r)
(p ∪ q) ; r = (p ; r) ∪ (q ; r) p∈s↔t∧q ∈s↔t∧r ∈t↔u (B.96)
−1
(p ∩ q) ; r = (p ; r) ∩ (q ; r) p∈s↔t∧q ∈s↔t∧r ∈u→
 t (B.97)
(p − q) ; r = (p ; r) − (q ; r) p ∈ s ↔ t ∧ q ∈ s ↔ t ∧ r−1 ∈ u →
 t (B.98)
{x → y} ; r = {x} × r[{y}] x∈s∧y ∈t∧r ∈t↔u (B.99)
p;r = ∅ p∈s↔t∧r ∈t↔u∧p=∅ (B.100)
(u × v) ; r = u × r[v] v ⊆s∧r ∈s↔t (B.101)
−1
f ; f = id(ran(f )) f ∈s→
 t (B.102)
r ; r−1 = id(dom(r)) r−1 ∈ t →
 s (B.103)
id(u) ; id(v) = id(u ∩ v) u⊆s∧v ⊆s (B.104)

Propriétés de la restriction Condition Ident.


ur = r u ⊆ s ∧ r ∈ s ↔ t ∧ dom(r) ⊆ u (B.105)
u  r = r  r[u] u ⊆ s ∧ r−1 ∈ t →
 s (B.106)
u  (r ; p) = (u  r) ; p u⊆s∧r ∈s↔t∧p∈t↔u (B.107)
u  id(v) = id(u ∩ v) u⊆s∧u⊆s (B.108)
u  (v  r) = (u ∩ v)  r u⊆s∧v ⊆s∧r ∈s↔t (B.109)
u  (r  w) = (u  r)  w u⊆s∧r ∈s↔t∧w ⊆t (B.110)
B. Propriétés des relations binaires 409

Propriétés de la restriction Condition Ident.


u  (v 
− r) = (u − v)  r u⊆s∧v ⊆s∧r ∈s↔t (B.111)
u  (r 
− w) = (u  r)  −w u⊆s∧r ∈s↔t∧w ⊆t (B.112)
u  (p − q) = (u  p)  − (u  q) u⊆s∧p∈s↔t∧q ∈s↔t (B.113)
u  (p ∪ q) = (u  p) ∪ (u  q) u⊆s∧p∈s↔t∧q ∈s↔t (B.114)
u  (p ∩ q) = (u  p) ∩ (u  q) u⊆s∧p∈s↔t∧q ∈s↔t (B.115)
u  (p − q) = (u  p) − q u⊆s∧p∈s↔t∧q ∈s↔t (B.116)
u  {x → y} = {x → y} u⊆s∧x∈s∧y ∈t∧x∈u (B.117)
u  {x → y} = ∅ u⊆s∧x∈s∧y ∈t∧x∈ /u (B.118)
u ⊆ s ∧ r ∈ s ↔ t∧
ur = ∅ (B.119)
dom(r) ∩ u = ∅
u  (v × t) = (u ∩ v) × t u⊆s∧v ⊆s (B.120)
(u ∪ v)  r = (u  r) ∪ (v  r) u⊆s∧v ⊆s∧r ∈s↔t (B.121)
(u ∩ v)  r = (u  r) ∩ (v  r) u⊆s∧v ⊆s∧r ∈s↔t (B.122)
(u − v)  r = (u  r) − (v  r) u⊆s∧v ⊆s∧r ∈s↔t (B.123)
dom(r)  r = r r ∈s↔t (B.124)
f −1 [v]  f = f  v f ∈s→  t∧v ⊆t (B.125)
{x}  r = {x} × r[{x}] x∈s∧r ∈s↔t (B.126)
{x}  f = {x → f (x)} x∈s∧f ∈s→  t ∧ x ∈ dom(f ) (B.127)
∅r = ∅ r ∈s↔t (B.128)

Propriétés de l’antirestriction Condition Ident.


u ⊆ s ∧ r ∈ s ↔ t∧
u
−r = r (B.129)
dom(r) ∩ u = ∅
u
−r = r
− r[u] u ⊆ s ∧ r−1 ∈ t →
 s (B.130)
u
− (r ; p) = (u 
− r) ; p u⊆s∧r ∈s↔t∧p∈t↔u (B.131)
u
− id(v) = id(v − u) u⊆s∧v ⊆s (B.132)
u
− (v  r) = (v − u)  r u⊆s∧v ⊆s∧r ∈s↔t (B.133)
u
− (r  w) = (u 
− r)  w u⊆s∧r ∈s↔t∧w ⊆t (B.134)
u
− (v 
− r) = (u ∪ v) 
−r u⊆s∧v ⊆s∧r ∈s↔t (B.135)
u
− (r 
− w) = (u 
− r) 
−w u⊆s∧r ∈s↔t∧w ⊆t (B.136)
u
− (p 
− q) = (u 
− p) 
− (u 
− q) u⊆s∧p∈s↔t∧q ∈s↔t (B.137)
u
− (p ∪ q) = (u 
− p) ∪ (u 
− q) u⊆s∧p∈s↔t∧q ∈s↔t (B.138)
u
− (p ∩ q) = (u 
− p) ∩ (u 
− q) u⊆s∧p∈s↔t∧q ∈s↔t (B.139)
u
− (p − q) = (u 
− p) − q u⊆s∧p∈s↔t∧q ∈s↔t (B.140)
u
− {x → y} = {x → y} u⊆s∧x∈s∧y ∈t∧x∈
/u (B.141)
u
− {x → y} = ∅ u⊆s∧x∈s∧y ∈t∧x∈u (B.142)
u
−r = ∅ u ⊆ s ∧ r ∈ s ↔ t ∧ dom(r) ⊆ u (B.143)
u
− (v × t) = (v − u) × t u⊆s∧v ⊆s (B.144)
(u ∪ v) 
− r = (u 
− r) ∩ (v 
− r) u⊆s∧v ⊆s∧r ∈s↔t (B.145)
(u ∩ v) 
− r = (u 
− r) ∪ (v 
− r) u⊆s∧v ⊆s∧r ∈s↔t (B.146)
(u − v) 
− r = (u 
− r) ∪ (v  r) u⊆s∧v ⊆s∧r ∈s↔t (B.147)
410 Structures de données et méthodes formelles

Propriétés de l’antirestriction Condition Ident.


dom(r) 
−r = ∅ r ∈s↔t (B.148)
f −1 [v] 
−f = f 
−v f ∈s→
 t∧v ⊆t (B.149)
∅
−r = r r ∈s↔t (B.150)

Propriétés de la corestriction Condition Ident.


u⊆t ∧ f ∈s↔t ∧
f u = f (B.151)
ran(f ) ⊆ u
(f ∪ g)  u = (f  u) ∪ (g  u) f ∈s↔t ∧ g ∈s↔t ∧ u⊆t (B.152)
(f − g)  u = (f  u) − g f ∈s↔t ∧ g ∈s↔t ∧ u⊆t (B.153)
{x → y}  u = {x → y} x∈s ∧ y∈t ∧ u⊆t ∧ y∈u (B.154)
{x → y}  u = ∅ x∈s ∧ y∈t ∧ u⊆t ∧ y∈
/u (B.155)
(f  u) 
− (g  u) = (f − g)  u f ∈s↔t ∧ g ∈s↔t ∧ u⊆t (B.156)
∅f = ∅ f ∈s↔t (B.157)
ran(f  v) = ran(f ) v ∈Z∧f ∈s↔t (B.158)

Propriétés de la surcharge Condition Ident.


p
− (q 
− r) = p 
−q
−r p∈s↔t ∧q ∈s↔t ∧r ∈s↔t (B.159)
p ∈ s ↔ t ∧ q ∈ s ↔ t∧
p
−q = p∪q (B.160)
dom(q)  p = dom(p)  q
∅
−r = r r ∈s↔t (B.161)
r
−∅ = r r ∈s↔t (B.162)
({x} × v) 
− {x → y} = {x → y} x∈s∧v ⊆t∧y ∈t (B.163)
(u × {y}) 
− {x → y} =
u⊆s∧y ∈t∧x∈s (B.164)
(u ∪ {x}) × {y}
p∈s↔t ∧q ∈s↔t ∧
p
−q = q (B.165)
dom(p) ⊆ dom(q)
p∈s↔t ∧ q ∈s↔t ∧
(p − q) 
−r =p
−r r ∈ s ↔ t∧ (B.166)
dom(q) ⊆ dom(r)

Propriétés de l’identité Condition Ident.


id(u ∪ v) = id(u) ∪ id(v) u⊆s∧v ⊆s (B.167)
id(u ∩ v) = id(u) ∩ id(v) u⊆s∧v ⊆s (B.168)
id(u − v) = id(u) − id(v) u⊆s∧v ⊆s (B.169)
id({x}) = {x → x} x∈s (B.170)
id(u) = ∅ u⊆s∧u=∅ (B.171)
Annexe C

Propriétés des fonctions

Propriétés de l’évaluation Condition Ident.


(x, f (x)) ∈ f f ∈s→
 t ∧ x ∈ dom(f ) (C.1)
λx ·(x ∈ s | E) ∈ s → t ∀x ·(x ∈ s ⇒ E ∈ t) (C.2)
λx ·(x ∈ s | E)(V ) = [x := V ]E ∀x ·(x ∈ s ⇒ E ∈ t) ∧ V ∈ s (C.3)
∀x ·(x ∈ s ∧ P ⇒ E ∈ t) ∧
λx ·(x ∈ s ∧ P | E)(V ) = [x := V ]E (C.4)
V ∈ s ∧ [x := V ]P
(x, y) ∈ f ⇔ (x ∈ dom(f ) ∧ y = f (x)) f ∈s→
 t (C.5)
f −1 (f (x)) = x f ∈s
 x ∈ dom(f ) (C.6)
(p ; q)(x) = q(p(x)) p∈s→
 t∧q ∈t→
 u ∧ x ∈ dom(p ; q) (C.7)
id(s)(x) = x x∈s (C.8)
(u  f )(x) = f (x) u⊆s∧f ∈s→
 t ∧ x ∈ u ∩ dom(f ) (C.9)
(f  v)(x) = f (x)  t ∧ v ⊆ t ∧ x ∈ f −1 [v]
f ∈s→ (C.10)
(u −
 f )(x) = f (x) u⊆s∧f ∈s→
 t ∧ x ∈ dom(f ) − u (C.11)
(f −
 v)(x) = f (x)  t ∧ v ⊆ t ∧ x ∈ dom(f ) − f −1 [v]
f ∈s→ (C.12)
f ∈s→
 t∧g ∈s→
 t∧
(f 
− g)(x) = f (x) (C.13)
x ∈ dom(f ) − dom(g)
(f 
− g)(x) = g(x) f ∈s→
 t∧g ∈s→
 t ∧ x ∈ dom(g) (C.14)
f ∈s→
 t∧g ∈s→
 t
(f ∪ g)(x) = f (x) (C.15)
dom(g)  f = dom(f )  g ∧ x ∈ dom(g)
(f − g)(x) = f (x) f ∈s→
 t∧g ∈s→
 t ∧ x ∈ dom(f − g) (C.16)
{x → y}(x) = y x∈s∧y ∈t (C.17)
(u × {y})(x) = y u⊆s∧x∈u∧y ∈t (C.18)
x ∈ dom(f ) ⇔ (x, f (x)) ∈ f x∈s ∧ f ∈s→
 t (C.19)
x ∈ dom(f  u)⇔
f ∈s→
 t ∧ x∈s ∧u⊆t (C.20)
(x ∈ dom(f ) ∧ f (x) ∈ u)
(f  v)(p) = f (p − v) v ∈ Z ∧ f ∈ s ↔ t ∧ p ∈ dom(f  v) (C.21)
f (v : (v ∈ dom(f ) ∧ f (v) = c)) = c c ∈ ran(f ) ∧ f ∈ s →
 t (C.22)
Annexe D

Propriétés des entiers

Propriétés des relatifs Condition Ident.


min({a}) = a a∈Z (D.1)
min({a, b}) = a a∈Z∧b∈Z∧a≤b (D.2)
min({a, b}) = b a∈Z∧b∈Z∧b≤b (D.3)
a < min(u) ⇒ a ∈
/u u⊂Z∧a∈Z (D.4)
min({a}) = min({∞, a}) a∈Z (D.5)
min({a, min({b, c})})
a∈Z∧b∈Z∧c∈Z (D.6)
= min({min({a, b}), c})
max({a}) = a a∈Z (D.7)
max({a, b}) = a a∈Z∧b∈Z∧b≤a (D.8)
max({a, b}) = b a∈Z∧b∈Z∧a≤b (D.9)
max(u ∪ v) = max({max(u), max(v)}) u⊂Z∧v ⊂Z (D.10)
max(u ∪ {a}) = max({max(u), a}) u⊂Z∧a∈Z (D.11)
a > max(u) ⇒ a ∈
/u u⊂Z∧a∈Z (D.12)
max({a}) = max({−∞, a}) a∈Z (D.13)
max({a, max({b, c})})
a∈Z∧b∈Z∧c∈Z (D.14)
= max({max({a, b}), c})
m .. m = {m} m∈Z (D.15)
m∈Z∧n∈Z ∧
m .. n = ∅ (D.16)
m≥n+1
m ∈ Z ∧ n ∈ Z∧
m .. n = (m .. k) ∪ (k + 1 .. n) (D.17)
k ∈ Z ∧ k ∈ m − 1 .. n
m .. n ∩ p .. q = m ∈ Z ∧ n ∈ Z∧
(D.18)
max({m, p}) .. min({n, q}) p ∈ Z ∧ q ∈ Z∧
414 Structures de données et méthodes formelles

Dans le tableau ci-dessous s ⊆ Z et v ∈ Z.

Propriétés du décalage Condition Ident.


p0 = p p∈s↔t (D.19)
pv = ∅ p∈s↔t∧p=∅ (D.20)
(p  v)  v  = p (v + v  ) v ∈ Z ∧ v ∈ Z ∧ p ∈ s ↔ t (D.21)
(p  v)−1 = p−1 ; (id(dom(p))  v)−1 p∈s↔t (D.22)
(p ; q)  v = (p  v) ; q p∈s↔t∧q ∈t↔r (D.23)
(u  p)  v = (u   v)  (p  v) u⊆Z∧p∈s↔t (D.24)
(p  u)  v = (p  v)  u u⊆t∧p∈s↔t (D.25)
(u 
− p)  v = (u   v) 
− (p  v) u⊆Z∧p∈s↔t (D.26)
(p 
− u)  v = (p  v)  −u u⊆t∧p∈s↔t (D.27)
(p − q)  v = (p  v)  − (q  v) p∈s↔t∧q ∈s↔t (D.28)
λx ·(x ∈ s | E)  v =
E une expression (D.29)
λx ·(x ∈ (s  v) | [x := x − v]E)
λx ·(x ∈ s ∧ P | E)  v = E une expression,
(D.30)
λx ·(x ∈ (s  v) ∧ [x := x − v]P | [x := x − v]E) P un prédicat

Dans le tableau ci-dessous s ⊆ Z, v ∈ Z et v  ∈ Z.

Propriétés de la translation Condition Ident.


a  0 = a a⊆s (D.31)
(a  v)  v  = a (v + v  ) a⊆s (D.32)
dom(p)  v = dom(p  v) p∈s↔t (D.33)

Propriétés des parties entières Condition Ident.


 n2  +  n2  = n n∈Z (D.34)
x ≤ y x, y ∈ R∗+ × R∗+ ∧ x ≤ y (D.35)
Annexe E

Opérateurs et priorités

Le tableau ci-dessous récapitule les différents opérateurs, en précisant leurs


priorités. Plus la priorité est élevée plus l’opérateur est prioritaire. À priorités
égales, les opérateurs s’associent à gauche, à l’exception de xy (élévation à la
puissance) et de · (séparateur de quantificateur) qui s’associent à droite.

Op. Description Pri. Op. Description Pri.


∀ Quantificateur universel 25 ∃ Quantificateur existentiel 25
λ Lambda expression 25 r−1 Relation inverse 23
· Séparateur de quantificateur 22 − Moins unaire 21
˜ Inversion de liste 21 ¬ Négation 21
xy Puissance 20 · Multiplication 19
÷, / Divisions 19 × Produit cartésien 19
mod Modulo 19 + Addition 18
− Soustraction 18 .. Intervalle 17
= Inégalité 16 ∩ Intersection 16
 Intersection (sacs) 16 ∪ Union 16
 Union (sacs) 16 − Différence 16
−̇ Différence (sacs) 16  Addition (sacs) 16
< Inférieur strict 16 ≤ Inférieur ou égal 16
> Supérieur strict 16 ≥ Supérieur ou égal 16
 − Surcharge 16 −
 Soustraction de domaine 16

 Soustraction de codomaine 16  Restriction de domaine 16
 Restriction de codomaine 16  → Maplet 16
 Décalage de domaine 16   Translation 16
 Concaténation de listes 16 →  Fonction partielle 13
  Surjection partielle 13 → Fonction totale 13
 Surjection totale 13 ↔ Relation 13
  Injection partielle 13  Injection totale 13
  Bijection totale 13 ∈ Appartenance 12
∈ / Non appartenance 12 −  Appartenance (sacs) 12
−  Non appartenance (sacs) 12 ⊆ Non inclusion 11
⊂ Non inclusion stricte 11 ⊆ Inclusion 11
 Inclusion (sacs) 11 ⊂ Inclusion stricte 11
 Inclusion stricte (sacs) 11 ⇔ Équivalence 6
= Égalité 6 ∧ Et logique 4
416 Structures de données et méthodes formelles

Op. Description Pri. Op. Description Pri.


∨ Ou logique 4 ⇒ Implication 3
→ Conditionelle 2 : any 1
; Composition de relations 1 ◦ Composition de relations 1
| Séparateur 1 
= Définition 0
Bibliographie

[1] Groupe Algol de l’Afcet. Définition du langage algorithmique Algol 68.


Hermann, Paris, 1972.
[2] NIST. Dictionary of algorithms and data structures. Site du National
Institute for Standards and Technology. URL http://xw2k.nist.gov/
dads//. Consulté le 8 juillet 2008.
[3] Abrial J.-R. The B-Book: Assigning Programs to Meanings. Cambridge
University Press, 1996.
[4] Abrial J.-R. Introduction à la méthode B. iut de Nantes, 1997. Cassettes
vhs.
[5] Abrial Jean-Raymond. Modeling in Event-B. System and Software Ingi-
neering. Cambridge University Press, 2010.
[6] Abrial Jean-Raymond and Mussat Louis. On using conditional defi-
nitions in formal theories. In 2nd International Conference of B and Z
Users, France, January 23–25, volume 2272 of Lecture Notes in Computer
Science, pages 242–269. Springer, 2002.
[7] Aho A. and Hopcroft J. Structures de données et algorithmes. Inter-
Editions, 1987.
[8] Allen Brian and Munro Ian. Self-organizing binary search trees. Journal
of the ACM, 25(4): 526–535, 1978.
[9] Arnold André and Guessarian Irène. Mathématiques pour l’informa-
tique. Masson, 1997. 3e édition.
[10] Arsac J. Premières leçons de programmation. Cédic/F. Nathan, 1980.
[11] Arsac J. Les bases de la programmation. Dunod, 1983.
[12] Bayer R. and McCreight Edward M. Organization and maintenance of
large ordered indexes. Acta Informatica, 1(3): 173–189, 1972.
[13] Bentley Jon Louis. Multidimensional binary search trees used for asso-
ciative searching. Commun. ACM, 18(9): 509–517, 1975.
[14] Berlioux P. and Bizard Ph. Algorithmique. Construction preuve et éva-
luation des programmes. Dunod Bordas, 1983.
[15] Bird Richard S., Morgan Carroll, and Woodcock Jim, editors. volume
669 of Lecture Notes in Computer Science, 1993. Springer.
[16] Bitner James R. Heuristics that dynamically organize data structures.
418 Structures de données et méthodes formelles

SIAM J. Comput., 8(1): 82–110, 1979.


[17] Brassard Gilles and Bratley Paul. Fundamentals of algorithmics. Pear-
son – Prentice-Hall, Inc., 1996.
[18] Brett D. and Flegg A. Spatial indexing. URL http://www.flegg.
net/brett/pubs/spatial/index.html.
[19] Brown Mark R. Implementation and analysis of binomial queue algo-
rithms. SIAM J. Comput., 7(3): 298–319, 1978.
[20] Burton F. Warren. An efficient functional implementation of fifo queues.
Inf. Process. Lett., 14(5): 205–206, 1982.
[21] Challab D.J. Implementation of flexible arrays using balanced trees.
Comput. J., 34(5): 386–396, 1991.
[22] Chanzy P., Devroye L., and Zamora-cura C. Analysis of range search
for random k-d trees. Acta Informatica, 37(4–5): 355–383, 1999.
[23] Chuang Tyng-Ruey. Fully persistent arrays for efficient incremental up-
dates and voluminous reads. In 4th European Symposium on Programming,
pages 1–20, 1992.
[24] Cohen E. Programming in the 1990s, an Introduction to the Calculation
of Programs. Springer-Verlag, 1990.
[25] Cormen T., Leiserson Ch., Rivest R., and Stein C. Introduction to
Algorithms. Mit Press, McGraw-Hill Book Company, 2005. Sixth printing.
[26] Csürös M. Cours de structures de données, 2006. URL
http://www.iro.umontreal.ca/~csuros/IFT2010/materiel/arbres-
pp31_42.pdf. Consulté le 23 septembre 2009.
[27] Derniame J.-C. and Finance J.-P. Types abstraits de données : spéci-
fication, utilisation et réalisation. CRIN, Nancy, École d’été de l’Afcet,
Monastir, 79.E.57, 1979.
[28] Dielissen Victor J. and Kaldewaij Anne. A simple, efficient, and flexible
implementation of flexible arrays. In Mathematics of Program Construc-
tion, MPC’95, Proceedings, volume 947 of Lecture Notes in Computer
Science, pages 232–241, 1995.
[29] Dietz P.F. Fully persistent arrays (extended array). In WADS ’89: Pro-
ceedings of the Workshop on Algorithms and Data Structures, volume 382,
pages 67–74, London, UK, 1989. Springer-Verlag.
[30] Dijkstra E.W. A Discipline of Programming. Prentice-Hall, 1976.
[31] Dijkstra E.W. and Feijen W.H.J. A Method of Programming. Addison-
Wesley Longman Publishing Co., Inc., Boston, MA, USA, 1988.
[32] Ducrin A. Programmation. Du problème à l’algorithme, volume 1. Dunod,
1984.
[33] Ducrin A. Programmation. De l’algorithme au programme, volume 2.
Dunod, 1984.
[34] Enbody R. J. and Du H.C. Dynamic hashing schemes. ACM Computing
Surveys, 20(2): 85–113, 1988.
[35] Finkel Raphael A. and Bentley Jon Louis. Quad trees: A data structure
Bibliographie 419

for retrieval on composite keys. Acta Inf., 4(1): 1–9, 1974.


[36] Foster C.C. Information storage and retrieval using Avl trees. In ACM.
20th National Conference, Session 8, 1965.
[37] Fraenkel A.B. Systems of numeration. CH1892-9. IEEE, 1983.
[38] Fredkin Edward. Trie memory. Commun. ACM, 3(9): 490–499, 1960.
[39] Friedman J.H., Bentley J., and Finkel R. An algorithm for finding
best matches in logarithmic expected time. Technical Report CS-TR-75-
482, Stanford University, Department of Computer, July 1976.
[40] Gaudel M.-C., Soria M., and Froidevaux C. Types de données et
algorithmes. Informatique. McGraw-Hill, 1990.
[41] Gibbons J. Calculating functional programs. In Keiichi Nakata, editor,
Proceedings of ISRG/SERG Research Colloquium. School of Computing
and Mathematical Sciences. Oxford Brookes University, November 1997.
[42] Gibbons J. Calculating functional programs. In Backhouse R., Crole
R., and Gibbons J., editors, Algebraic and Coalgebraic Methods in the
Mathematics of Program Construction, volume 2297 of Lecture Notes in
Computer Science, pages 148–203. Springer-Verlag, 2002.
[43] Graham R.L., Knuth D.E., and Patashnik O. Mathématiques concrètes
– Fondations pour l’informatique. Édition Vuibert, 2003. Seconde édition.
[44] Gries D. and Schneider F.B. A logical approach to discrete math.
Springer-Verlag New York, Inc., New York, NY, USA, 1994.
[45] Gries David. The Science of Programming. Springer-Verlag, 1981. Second
printing.
[46] Guttag John V. and Horning James J. The algebraic specification of
abstract data types. Acta Inf., 10: 27–52, 1978.
[47] Guyomard M. EB : A constructive approach for the teaching of data
structures. In Formal Methods in Computer Science Education, Satellite
workshop of ETAPS, pages 25–36, Budapest, Hungary, March 29th 2008.
[48] Guyomard M. The teaching of data structures : A balanced presentation
of skew heaps. In The B method : from Research to Teaching, pages 45–64,
Nantes, June 7th 2010.
[49] Guyomard M., Alain P., Hadjali A., Jaudoin H., and Smits G. First
balance sheet of a formal approach in the teaching of data structures. In
The B method: from Research to Teaching, pages 66–91, Nantes, June 16th
2008.
[50] Habrias H. and Chauvet J.-Y. Introduction à la spécification formelle
avec B. Technical report, iut de Nantes, Département Informatique, Mai
2000. Version provisoire.
[51] Habrias H. and Frappier M. Software Specification Methods. interEdi-
tions. Wiley-ISTE, May 2006.
[52] Heinz Steffen, Zobel Justin, and Williams Hugh E. Burst tries : A fast,
efficient data structure for string keys. ACM Transactions on Information
Systems, 20(2): 192–223, April 2002.
420 Structures de données et méthodes formelles

[53] Hinze R. Functional pearls: Explaining binomial heaps. Journal of Func-


tional Programming, 9(1): 93–104, January 1999.
[54] Hinze Ralf. Generalizing generalized tries. J. Funct. Program., 10(4) :
327–351, July 2000.
[55] Hoare C.A.R. An axiomatic basis for computer programming. Comm.
ACM 22, 12(10): 576–580, October 1969.
[56] Hoare C.A.R. Proof of correctness of data representations. Acta Inf., 1:
271–281, 1972.
[57] Hoare C.A.R. An overview of some formal methods for program design.
IEEE Computer, pages 85–91, 1987.
[58] Hofstadter D. Gödel, Escher, Bach : les brins d’une guirlande éternelle.
interEditions, 1979.
[59] Hood Robert T. and Melville Robert C. Real time queue operations in
pure LISP. Information Processing Letters, 13(2): 50–54, November 1981.
[60] Hoogerwoord R. The Design of functional Programs: a Calculational
Approach. PhD thesis, Eindhoven University of Technology, Eindhoven,
The Netherlands, 1989.
[61] Hoogerwoord Rob R. A logarithmic implementation of flexible arrays.
In Bird Richard S. et al. [15], pages 191–207.
[62] Hoogerwoord Rob R. Functional pearls – a symmetric set of efficient
list operations. J. Funct. Program., 2(4): 505–513, 1992.
[63] Hoogerwoord Rob R. A derivation of Huffman’s algorithm. In Bird
Richard S. et al. [15], pages 375–378.
[64] Hoogerwoord Rob R. Programming by calculation. In Lucio Pa-
qui, Martelli Maurizio, and Navarro Marisa, editors, APPIA-GULP-
PRODE, pages 407–426, 1996.
[65] Huffman David A. A method for the construction of minimum-
redundancy codes. Proceedings of the Institute of Radio Engineers, 40(9):
1098–1101, September 1952.
[66] Kaldewaij A. Programming : the derivation of algorithms. Prentice-Hall,
Inc., Upper Saddle River, NJ, USA, 1990.
[67] Kaldewaij Anne. Programming : the derivation of algorithms, Teacher’s
Manual. Prentice-Hall, Inc., Upper Saddle River, NJ, USA, 1991.
[68] Kaldewaij Anne and Dielissen Victor J. Decomposable func-
tions and leaf trees : A systematic approach. In Proceedings of the
IFIP TC2/WG2.1/WG2.2/WG2.3 Working Conference on Programming
Concepts, Methods and Calculi, San Miniato, Italy, volume A-56 of IFIP
Transactions, pages 3–17. North-Holland, 1994.
[69] Kaldewaij Anne and Schoenmakers Berry. The derivation of a tighter
bound for top-down skew heaps. Information processing letters, 37(5) :
265–271, March 1991.
[70] Kaplan H., Okasaki C., and Tarjan R.E. Simple confluently persistent
catenable lists. SIAM J. Comput., 30(3): 965–977, 2000.
Bibliographie 421

[71] Kaplan Haim. Persistent data structures. In Mehta D. and Sahni S.,
editors, In Handbook on Data Structures and Applications, pages 1.1–1.27.
CRC Press, 2001.
[72] Kaplan Haim and Tarjan Robert E. Purely functional, real-time deques
with catenation. J. ACM, 46(5): 577–603, 1999.
[73] King D. Functional binomial queues. In Proceedings of the Glasgow Work-
shop on Functional Programming, pages 141–150, Ayr, Scotland, 1994.
Springer-Verlag.
[74] Kleinberg J. and Tardos É. Algorithm Design. Person – Addison-
Wesley, 2005.
[75] Knuth Donald E. The Art of Computer Programming, volume 3. Addison-
Wesley, 1998.
[76] Krivine J.-L. Théorie des ensembles. Nouvelle bibliothèque mathéma-
tique. Cassini, Paris, 1998.
[77] Lalement R. Logique réduction et résolution. Masson, Paris, 1990.
[78] Larson P.-Å. Dynamic hashing. BIT, 18: 184–201, 1978.
[79] Livercy C. Théorie des programmes, schémas, preuves, sémantique. Du-
nod Informatique, 1978.
[80] Manber U. Introduction to Algorithms: A Creative Approach. Addison-
Wesley, Boston, MA, USA, 1989.
[81] Martínez C. and Roura S. Randomized binary search trees. J. Assoc.
Comput. Mach., 45(2): 288–323, 1998.
[82] McCreight Edward M. Priority search trees. SIAM J. Comput., 14(2):
257–276, 1985.
[83] Monin J.-F. Introduction aux méthodes formelles. Hermes Science et
france telecom, 2000. 2e édition.
[84] Moore Andrew. A tutorial on kd-trees. PhD thesis, University of
Cambridge Computer Laboratory Technical, Report No 209, 1991. URL
http://www.cs.cmu.edu/$sim$awm/papers.html.
[85] Morris F.L. and Jones C.B. An early program proof by Alan Turing.
IEEE Annals of the History of Computing, 6: 139–143, April 1984.
[86] Morrison Donald R. PATRICIA - practical algorithm to retrieve infor-
mation coded in alphanumeric. J. ACM, 15(4): 514–534, October 1968.
[87] Myers Eugene W. An applicative random-access stack. Inf. Process.
Lett., 17(5): 241–248, 1983.
[88] Nguyen D., Duprie K., and Zografou P. A multidimensional binary
search tree for star catalog correlations. In Albrecht R., Hook R.N.,
and Bushouse H.A., editors, In Astronomical Data Analysis software and
Systems VII ASP Conference Series, volume 145, 1998.
[89] Nievergelt Jürg and Reingold Edward M. Binary search trees of boun-
ded balance. In Proceedings of the fourth annual ACM symposium on
Theory of computing, pages 33–43, 1972.
[90] Okasaki Chris. Purely functional random-access lists. Functional Pro-
422 Structures de données et méthodes formelles

gramming Languages and Computer Architecture, pages 86–95, June 1995.


[91] Okasaki Chris. Simple and efficient purely functional queues and deques.
J. Funct. Program., 5(4): 583–592, October 1995.
[92] Okasaki Chris. Functional pearls – three algorithms on Braun trees. J.
Funct. Program., 7(6): 661–666, 1997.
[93] Okasaki Chris. Purely functional data structures. Cambridge University
Press, Cambridge U.K. New York, 1998.
[94] Olivié Henk J. A new class of balanced search trees: Half balanced binary
search trees. RAIRO Informatique Théorique, 16(1): 51–71, 1982.
[95] O’Neill Melissa E. and Burton F. Warren. A new method for functional
arrays. J. Funct. Program., 7(5): 487–513, 1997.
[96] Ottmann Thomas and Wood Derick. 1-2 brother trees or avl trees
revisited. Comput. J., 23(3): 248–255, 1980.
[97] Polya G. Comment poser et résoudre un problème. Éditions Jacques
Gabay, 1965. Seconde édition, Nouveau tirage 2007.
[98] Ramachandran Sridhar, Rajasekar K., and Pandu Rangan C. Pro-
babilistic data structures for priority queues (extended abstract). In
SWAT, volume 1432 of Lecture Notes in Computer Science, pages 143–
154, 1998.
[99] Samet Hanan. Deletion in two-dimensional quad trees. Commun. ACM,
23(12): 703–710, 1980.
[100] Santoro Renaud. Vers des générateurs de nombres aléatoires uniformes
et gaussiens à très haut débit. PhD thesis, Enssat (Université de Rennes 1)
et Université Laval, Lannion France, Décembre 2009.
[101] Schoenmakers Berry. Data Structures and Amortized Complexity in a
Functional Setting. Doctoral dissertation, Eindhoven University of Tech-
nology, Eindhoven, September 1992.
[102] Sedgewick R. Algorithms. Addison-Wesley, 1988.
[103] Sedgewick R. and Flajolet P. Introduction à l’analyse d’algorithmes.
International Thomson publishing France, 1996.
[104] Sedgewick Robert. Algorithmes en Java. Pearson education, 2004. Troi-
sième édition.
[105] Seidel Raimund and Aragon Cecilia R. Randomized search trees. In
Proc. 30th Symp. Foundations of Computer Science (FOCS 1989), Wa-
shington, D.C.: IEEE Computer Society Press, pages 540–545, 1989.
[106] Seidel Raimund and Aragon Cecilia R. Randomized search trees. Al-
gorithmica, 16(4/5): 464–497, 1996.
[107] Sen S. and Tarjan R.E. Deletion without rebalancing in multiway search
trees. In Algorithms and Computation, 20th International Symposium, vo-
lume 5878 of Lecture Notes in Computer Science, pages 832–841. Springer,
2009.
[108] Sen S. and Tarjan Robert E. Deletion without rebalancing in balanced
binary trees. In Proceedings of the Twenty-First Annual ACM-SIAM Sym-
Bibliographie 423

posium on Discrete Algorithms, pages 1490–1499. SIAM, January 2010.


[109] Sleator Daniel Dominic and Tarjan Robert Endre. Self-adjusting bi-
nary search trees. J. ACM, 32(3): 652–686, 1985.
[110] Sleator Daniel Dominic and Tarjan Robert Endre. Self adjusting heaps.
SIAM J. Comput., 15(1): 52–69, 1986.
[111] Stephenson C.J. A method for constructing binary search trees by ma-
king insertions at the root. International Journal of Computer and Infor-
mation Sciences, 9(1): 15–29, February 1980.
[112] Tarjan R.E. Data structures and network algorithms. CBMS-NSF Re-
gional Conference Series in Applied Mathematics, SIAM, 1983.
[113] Tarjan R.E. Amortized computational complexity. SIAM J. Alg. Disc.
Meth., 6(2): 306–318, April 1985.
[114] Vuillemin Jean. A data structure for manipulating priority queues. Com-
mun. ACM, 21(4): 309–315, 1978.
[115] Vuillemin Jean. A unifying look at data structures. Commun. ACM,
23(4): 229–239, 1980.
[116] Vuillemin Jean. Structures de données. Inria, notes de cours, 1981.
[117] Wolper P. Introduction à la calculabilité. Dunod, 2006. Troisième édition.
[118] Woo M. Site qui comprend en particulier une généalogie des mises en
œuvre des files de priorité, consulté le 17 février 2010. URL http://www.
cs.cmu.edu/~maverick/Talks/2005-09-21%20A%20Tale.pdf.
[119] Wordsworth John. Software Engineering with B. Addison-Wesley, Sep-
tember 1996.
[120] Yu Cui. High-dimensional indexing: Transformational approaches to high-
dimensional range and similarity searches. Lecture Notes in Computer
Science, 2341, 2002.
[121] Wikipedia. Entrée wikipedia sur les caractéristiques du langage Java,
consulté le 15 septembre 2008. URL http://fr.wikipedia.org/wiki/
Langage_Java.
[122] Wikipedia. Entrée wikipedia concernant le nombre d’or, consulté le 24
septembre 2009. URL http://fr.wikipedia.org/wiki/Nombre_d’or.
[123] Wikipedia. Entrée wikipedia sur les kd-arbres, consulté le 31 mars 2010.
URL http://en.wikipedia.org/wiki/Kd-tree.
Index

Symboles Allen B., 258


Ω, 111 analyse
Φ, 125 amortie, voir complexité, amortie
Θ, 111 des algorithmes, voir complexité,
bCard, mult, bRan, 102 107–128
:=, 23 antécédent, 19
◦, ;, 33 Aragon C.R., 3, 269, 270
−̇, 102 arborescence, 84
, 101 arbre
⊥, 21 1d-arbre, 296
−, 
 −, 102 2-3, 191, 215, 392
, 21 2-3-4, 191, 215
card, 37 2d-arbre, 295–310
choice, 28 4-aire, 309
dom, ran, 31 aléatoire, 154, 155, 157, 161, 167,
, ,  −, −, 31 168, 182, 271, 263–271, 303
, , , , 36 ascendant, voir arbre, inverse
 , 100 binaire
→, 22 de recherche, voir abr
O, 110 binomial, 87, 90, 337
max, min, 37 cartésien, 33, 269, 270, 309, 398
N1 , 37 complet, 89, 96
−, 33
déployé, 3, 94, 148, 245–257, 270
→, →, ,  , ,  , 
, 34
de Braun, 8, 377, 378–392
P, 27
de Catalan, 154, 155
, !, , , , 102
de codage, 359
, , 38
de démonstration, 19

, 18
de Fibonacci, 360
enraciné, voir arbre, libre, 84, 132,
A 274
abr, 90, 150–168, 170, 177, 187, 190– équilibré, 154, 155, 157, 192–209,
213, 245, 247, 252, 258–264, 245, 295, 307–310, 374, 378
269–271, 273–310 étiqueté, 83–99, 329, 359, 381, 382
Abrial J.-R., 1, 16, 18, 23, 43, 56, 79, externe, 77, 96, 96–99, 132, 148,
104, 105, 199 164, 170, 178–191, 243, 309,
adressage dispersé, voir hachage 391, 392
Aho A., 113, 175 feuille, voir arbre, externe
algèbre, 11, 59, 60, 122 filiforme, 89, 93, 365, 395
426 Structures de données et méthodes formelles

généalogique, 360 strict, 217, 221–223, 232, 233, 237,


guidon, 132 239, 240, 242
h-équilibré, 92, 93, 192–217 Bayer R., 243
inverse, 10 Bentley J., 303, 309, 310
kd-arbre, 273, 293–310 Berlioux P., 73
libre, 84 bijection, voir fonction, bijective
multidimensionnel, voir arbre, kd- Bitner J.R., 258
arbre Bizard Ph., 73
multidirectionnel, voir arbre, kd- Brassard G., 45, 123, 270
arbre Bratley P., 45, 123, 270
n-aire, 77, 83, 88–95, 191, 217 Brett D., 309, 310
non étiqueté, voir arbre, étiqueté Brown M.R., 360
non ordonné, 77, 78, 83, 85–86, Burton F.W., 324, 391
87, 88, 274, 275
p-équilibré, 191, 192, 196, 307
parfait, 89, 330–331 C
à gauche, 89, 330 calcul
partiellement étiqueté, voir arbre, des prédicats, 22–26
étiqueté propositionnel, 21–22
planaire, 77, 78, 83, 87, 88 cardinal, 108, 169, 241
plein, 89, 93, 96, 132, 194, 307 Challab D.J., 392
quaternaire, 309 Chanzy P., 309
récursion, 123 Chauvet J.-Y., 56
randomisé, 148, 151, 270–271 Chuang T-R., 391
recouvrant, 328 Cohen E., 2, 10, 73
semi-équilibré, 215 collision, voir hachage
totalement étiqueté, voir arbre, complexité
étiqueté amortie, 3, 8, 108, 123–128, 129–
arbre-trie, voir trie 138, 245–257, 308, 313, 315,
Arnold A., 56, 105, 108, 117, 119 322–324, 328, 351–360, 362–
Arsac J., 71 374, 380, 398
asymptotique, 75, 99, 108–112, 113, asymptotique, voir asymptotique
114, 117, 123, 136, 137, 154, de Kolmogorov, 107
167, 168, 192, 215, 269, 341, spatiale, 214, 269, 286, 287, 308,
374 309
Avizienis A., 132 temporelle, 107, 117, 120, 154,
Avl, 2, 6, 71, 75, 90, 94, 148, 151, 170, 169, 188, 192, 243, 286, 292,
177, 191, 192, 215–217, 241, 308, 315, 346
273, 285, 292, 398 comportement asymptotique, voir
axiome, 19, 28 asymptotique
composition, 33, 384, 385
compréhension, 27, 30, 41, 59, 61, 148
B concaténation, 78, 81, 82, 317–319, 321,
B+ -arbre, 99, 191, 238, 243, 286 377, 394–396, 398
B-arbre, 2, 71, 90, 99, 116, 148, 151, conditionnelle, 25, 26, 27, 48, 52–54,
170, 177, 191, 192, 215, 216– 56, 154, 341, 346, 354, 355,
243, 245, 286, 392 357
régulier, 217, 226 conjonction, 17, 19, 21, 50
Index 427

conséquent, 19 de priorité, 3, 4, 8, 83, 87, 100, 132,


Cormen T., 117, 119, 122, 123, 128, 245, 257, 327, 327–374
138, 175 gloutonne, 132, 398
couple, 22, 273, 293 simple, 4, 8, 10, 12, 78, 313, 313–
coupure, 20, 158, 295, 306 324
coût amorti, voir complexité, amortie Finance J.-P., 105
Csürös M., 194 Finkel R., 309, 310
Flajolet P., 123
Flegg A., 309, 310
D
flexibilité, voir tableau, flexibilité
décalage, 38, 39, 391, 394, 395
fonction
démonstration, 18–20
bijective, 17, 34, 60, 64
décomposition, 74, 187, 188, 213, 358
d’abstraction, 10, 47, 64, 130, 140,
Derniame J.-C., 105
151, 172, 179, 196, 227, 259,
Devroye L., 309
298, 363
diamètre, 92
de hachage, 170, 171, 172, 174,
Dielissen V.J., 10, 75, 188, 191, 192,
175, 269, 271
391, 393
Dietz P.F., 391 de potentiel, 125, 125–138, 252,
Dijkstra E.W., 2, 16, 37, 73, 199, 380 253, 257, 308, 322, 323, 351,
disjonction, 54 368, 369, 374
domaine, 31, 34, 35, 80, 108, 109, 116, injective, 17, 34, 39, 67, 140, 196,
314, 339, 377, 378, 380, 397 219, 258, 261, 268
Ducrin A., 71 partielle, 34, 49
DuPrie K., 309 surjective, 34, 49, 140, 149, 171,
176, 196, 335, 363
totale, 17, 34, 38, 39, 49
E forêt-trie, voir trie
égalité, 15, 26–27, 28, 29 formule, 22
Enbody R.J., 292 Foster C.C., 215
équation Fraenkel A.B., 132
caractéristique, 119 Frappier M., 56
évaluation, 35, 50, 73 Friedman J.H., 309
expression, 18, 22–40, 47–57 Froidevaux C., 105, 108, 117, 119,
gardée, 25, 25, 52–54 155, 175
préconditionnée, 48, 49, 50–53

F G
Feijen W.H.J., 2, 16, 37, 73 Gaudel M.-C., 105, 108, 117, 119, 155,
Fibonacci L., 132, 360 175
fichier, 79, 107, 238, 328 Gibbons J., 5
séquentiel, 79 Graham R.L., 108, 117, 156
séquentiel indexé, 99, 238 graphe, 7, 84, 92, 128, 328
file Gries D., 2, 73, 104, 105, 324
binomiale, 132, 328, 331, 331–368, Guessarian I., 56, 105, 108, 117, 119
392, 398 Guttag J.V., 105
d’attente, voir file, simple Guyomard M., 192
428 Structures de données et méthodes formelles

H lambda abstraction, 35
Habrias H., 56 langage de programmation, voir pro-
hachage, 2, 3, 7, 116, 148, 168, 169, grammation
169–177, 269, 271, 309 langage des prédicats, voir calcul, des
dynamique, 273, 287, 287–292 prédicats
externe, 169–170 langage propositionnel, voir calcul,
hashing, voir hachage propositionnel
hauteur, voir rayon, 85 Larson P.-Å., 176, 292
Hinze R., 292 Leiserson Ch., 117, 119, 122, 123,
Hoare C.A.R., 11, 16, 72, 75 128, 138, 175
Hofstadter D., 20 liste, 4–10, 43, 73, 74, 78, 77–91, 97–
homomorphisme, 59, 60, 64, 151 100, 129–143, 154, 170, 177,
Hood R., 324 215, 270, 273–275, 285, 313–
Hoogerwoord R., 5, 10, 188, 257, 324, 332–334, 341, 351, 355,
359, 392 392, 398
Horning J.J., 105 circulaire, 10
Huffman A., 359 doublement chaînée, 10
Livercy C., 73
logarithme, 60, 96, 193, 241, 252, 253,
I 256
image, 31, 33
implication, 21, 54
inclusion, 102 M
induction, 18, 43, 63, 71–75 méthode de hachage, voir hachage ;
structurelle, 64, 133 fonction, de hachage
injection, voir fonction, injective Manber U., 71, 108, 117, 119
intersection, 29, 30, 43, 102 Martínez C., 270
intervalle, 37, 39 maximier, voir minimier
inverse, 31, 34 McCreight E., 243, 309
Melville M., 324
Mersenne M., 132
J méthode du potentiel, voir complexité,
Jones C.B., 15 amortie
minimier, 89, 258–270, 329, 377
K binaire, 329, 330, 336
Kaldewaij A., 10, 73, 75, 138, 188, binomial, 328, 331, 332, 332, 333–
191, 192, 391, 393 339, 345
Kaplan H., 7, 398 de position, 191
King D.J., 360 filiforme, 366
Kleinberg J., 359 gauchiste, 374
Knuth D.E., ix, 7, 79, 108, 117, 155, oblique, 3, 245, 328, 362, 361–374
156, 168, 175, 215, 243, 270, parfait, 331
286, 292, 310, 374 parfait binaire, 328, 330
Krivine J.-L., 57 modélisation, 16, 40, 100, 123, 199
Monin J.-F., 56, 105
Morris F.L., 15
L Morrison D.R., 308, 309
Lalement R., 57, 105 Munro I., 258
Index 429

Mussat L., 56 quadrant, 309, 310


Myers E.W., 392 quantificateur, 18, 23, 28, 38, 81, 183
existentiel, 30
universel, 22, 23, 30
N
négation, 21, 27, 282
Nguyen D., 309 R
Nievergelt J., 215 règle d’inférence, 19, 20, 21, 23, 24, 26–
non déterminisme, 18, 25, 29, 268, 269, 28, 48
285 radix, 273
notation asymptotique, voir asympto- raffinement, 4, 6, 11, 61, 63, 75, 99, 140
tique raisonnement, 18–20
numération, 132 Rajasekar K., 374
rayon, 92, 92, 93, 94, 96, 154, 192–214,
217–241, 328, 331–360, 382
O
recherche dichotomique, 150
O’Neill M.E., 391
récurrence
Okasaki C., 7, 11, 124, 132, 257, 324,
complète, 44
375, 392, 398
simple, 44
Olivié H.J., 215
récursivité, 10, 73, 75
opération, 11, 47–56
Reingold E.M., 215
Ottmann T., 216
Reiser M., ix
relation
P binaire, 17, 31, 31–34, 108, 293–
paire ordonnée, voir couple 298, 309, 310
Pandu Rangan C., 374 d’ordre, 35, 147, 216, 327
Patashnik O., 108, 117, 156 Rivest R., 117, 119, 122, 123, 128, 138,
permutation, 109, 154, 155, 168, 182, 175
244, 269 rotation, 91–95, 151, 161–166, 192,
pile, 4, 315, 378 197–215, 221–225, 242, 245,
plpc, 273, 274 258, 262, 265–267, 295
poids, 85, 93, 94 Roura S., 270
point fixe, 43, 77, 79, 80, 91, 105
Polya G., 45
preuve, 18–20 S
produit cartésien, 27, 80, 91, 97, 101, Samet H., 310
149, 293, 294 Schoenmakers B., 10, 73, 124, 126,
programmation, 4, 8, 9, 11, 61, 75, 114, 138, 257, 369, 374
377, 380 Sedgewick R., 123, 155, 175
dynamique, 74, 188 Seidel R., 3, 269, 270
fonctionnelle, 5, 6, 9–11, 54, 177 Sen S., 215, 243
impérative, 2, 5, 6, 10, 11, 26, 71, séquent, 18, 19, 20
192, 214 skew heap, voir minimier, oblique
propriété caractéristique, 66, 68 Sleator D.D., 124, 128, 257, 373, 374
Soria M., 105, 108, 117, 119, 155, 175
sous-arbre, voir arbre
Q spécification, 1–11, 15–18, 25, 47, 59–
quad tree, 309–310 76, 107, 109
430 Structures de données et méthodes formelles

splay tree, voir arbre, déployé W


Sridhar R., 374 Wirth N., ix
Stein C., 117, 119, 122, 123, 128, 138, Wolper P., 114
175 Woo M., 360
Stephenson C.J., 168 Wood D., 216
substitution, 23, 25, 48 Wordsworth J.B., 56
surcharge, 33, 34, 383
surjection, voir fonction, surjective
système de numération, voir numéra- Y
tion Yu C., 309

T Z
table de hachage, voir hachage Zamora-Cura C., 309
tableau, 4, 6–9, 15, 17, 18, 38, 39, 53, Zografou P., 309
56, 74, 75, 83, 85, 88, 109,
114–117, 172, 177, 217, 219,
220, 226, 229–232, 285, 292,
295, 308, 309, 313, 331, 360
flexibilité, 4, 8, 83, 99, 377, 380
faible, 377, 378–382, 384–386,
391–393
forte, 188, 191, 270, 315, 377,
380, 391, 394, 395–399
taille, voir poids
Tardos É., 359
Tarjan R.E., 124, 128, 215, 243, 257,
373, 374, 398
tas, 328–331
technique de hachage, voir hachage
terminaison, 48, 176
théorie des ensembles, 2, 7, 9, 16, 18,
27–36, 43, 56, 57
tournoi, voir minimier
translation, 38, 39
treap, 168, 177, 258–271
randomisé, 268–271
trie, 86, 176, 275, 273–292, 359
Patricia, 286–287

U
union, 29, 102

V
vecteur caractéristique, 148
Vuillemin J., 161, 167, 168, 191, 269,
270, 309, 360, 392, 398
Dans la même collection
Hermes Science Publications
Projet et innovation, méthode HYBRID pour les projets innovants, par G. Poulain,
2000, 358 pages.
UMTS et partage de l’espace hertzien, par L. Genty, 2001, 309 pages.
Introduction aux méthodes formelles, par J.-F. Monin, 2001, 351 pages.
Objets communicants, sous la direction de C. Kintzig, G. Poulain, G. Privat,
P.-N. Favennec, 2002, 396 pages.
Les réseaux de télécommunications, par R. Parfait, 2002, 524 pages.
Trafic et performances des réseaux de télécoms, par G. Fiche, G. Hébuterne, 2003,
592 pages.
Le téléphone public, par F. Carmagnat, 2003, 310 pages.
Les cristaux photoniques, sous la direction de J.-M. Lourtioz, 2003, 310 pages.
Management des connaissances en entreprise, sous la direction de I. Boughzala
et J.-L. Ermine, 2004, 304 pages.
Les agents intelligents pour un nouveau commerce électronique, par C. Paraschiv,
2004, 235 pages.
De Bluetooth à Wi-Fi, par H. Labiod et H. Afifi, 2004, 368 pages.
Les techniques multi-antennes pour les réseaux sans fil, par P. Guguen et G. El Zein, 2004,
238 pages.
Les net-compagnies, sous la direction de T. Bouron, 2004, 200 pages.
Optique sans fil – propagation et communication, par O. Bouchet, H. Sizun,
C. Boisrobert, F. de Fornel et P.-N. Favennec, 2004, 227 pages.
Interactions humaines dans les réseaux, par L. Lancieri, 2005, 224 pages.
La gestion des fréquences, par J.-M. Chaduc, 2005, 368 pages.
La nanophotonique, par H. Rigneault et al., 2005, 352 pages.
Le travail et les technologies de l’information, sous la direction d’E. Kessous
et J.-L. Metzger, 2005, 320 pages.
Les innovations dans les télécoms mobiles, par E. Samuelides-Milesi, 2005, 264 pages.
CyberMonde, sous la direction de B. Choquet et D. Stern, 2005, 192 pages.
432 Structures de données et méthodes formelles

Communications et territoires, par APAST, coordination de P.-N Favennec, 2006,


398 pages.
Administration électronique, par S. Assar et I. Boughzala, 2007, 336 pages.
Management des connaissances en entreprise (2e édition), sous la direction de
I. Boughzala et J.-L. Ermine, 2007, 364 pages.
Communications Ultra Large Bande : le canal de propagation radioélectrique, par
P. Pagani, T. Talam, P. Pajusco et B. Uguen, 2007, 246 pages.
Mathématiques pour les télécoms, par G. Fiche et G. Hébuterne, 2007, 532 pages.
Compatibilité électromagnétique : des concepts de base aux applications, par
P. Degauque et A. Zeddam.
Volume 1. – 2007, 482 pages.
Volume 2. – 2007, 360 pages.
Ingénierie de la collaboration : théories, technologies et pratiques, par I. Boughzala,
2007, 310 pages.
Physique des matériaux pour l’électronique, par A. Moliton, 2007, 436 pages.
Electronique et photo-électronique des matériaux et composants, par A. Moliton
Volume 1.- 2008, 304 pages.
Volume 2.- 2009, 360 pages
Inégalités numériques : clivages sociaux et modes d’appropriation des TIC, par
F. Granjon, B. Lelong et J.-L. Metzger, 2009, 254 pages
Les réseaux domiciliaires et IPTV, par J.-G. Remy et Ch. Letamendia, 2009, 263 pages
Compression du signal – application aux signaux audio, par N. Moreau, 2009,
229 pages
Télécoms pour l’ingénierie du risque, par T. Tanzi et P. Perrot, 2009, 234 pages
Les antennes Ultra Large Bande, sous la direction de X. Begaud, 2010, 310 pages
De la radio logicielle à la radio intelligente, sous la direction de J. Palicot, 2010,
412 pages.
Les chambres réverbérantes en électromagnétisme, par B. Démoulin et P. Besnier, 2010,
422 pages.
Evolution des innovations dans les télécoms- histoire, techniques, acteurs et enjeux, par
C. Rigault, 2010, 226 pages.
Le laser et ses applications 50 ans après son invention, sous la direction de P. Besnard
et de P.-N. favennec, 2010, 377 pages.

Dunod
Electromagnétisme classique dans la matière, par Ch. Vassallo, 1980, 272 pages
(épuisé).
Télécommunications : Objectif 2000, sous la direction de A. Glowinski, 1981,
300 pages (épuisé).
Dans la même collection 433

Principes des communications numériques, par A.-J. Viterbi et J.-K. Omura. Traduit
de l’anglais par G. Batail, 1982, 232 pages (épuisé).
Propagation des ondes radioélectriques dans l’environnement terrestre, par L. Boithias,
1984, 328 pages (épuisé).
Systèmes de télécommunications : bases de transmission, par P.-G. Fontolliet, 1984,
528 pages (épuisé).
Eléments de communications numériques. Transmission sur fréquence porteuse, par
J.-C. Bic, D. Duponteil et J.C.Imbeaux (épuisé).
Tome 1. – 1986, 384 pages.
Tome 2. – 1986, 328 pages.
Téléinformatique. transport et traitement de l’information dans les réseaux et systèmes
téléinformatiques et télématiques, par C. Macchi, J.-F. Guilbert et al., 1987,
934 pages.
Les systèmes de télévision en ondes métriques et décimétriques, par L. Goussot, 1987,
376 pages (épuisé).
Programmation mathématique. théorie et algorithmes, par M. Minoux (épuisé).
Tome 1. – 1987, 328 pages.
Tome 2. – 1989, 272 pages.
Exploration informatique et statistique des données, par M. Jambu, 1989, 528 pages
(épuisé).
Télématique : techniques, normes, services, coordonné par B. Marti, 1990, 776 pages.
Compatibilité électromagnétique : bruits et perturbations radioélectriques, sous la
direction de P. Degauque et J. Hamelin, 1990, 688 pages.
Les faisceaux hertziens analogiques et numériques, par E. Fernandez et M. Mathieu,
1991, 648 pages.
Les télécommunications par fibres optiques, par I et M. Joindot et douze co-auteurs,
1996, 768 pages.

Eyrolles
De la logique câblée aux microprocesseurs, par J.-M. Bernard et J. Hugon (épuisé).
Tome 1. – Circuits combinatoires et séquentiels fondamentaux, avec la
collaboration de R. le Corvec, 1983, 232 pages.
Tome 2. – Applications directes des circuits fondamentaux, 1983, 135 pages.
Tome 3. – Méthodes de conception des systèmes, 1986, 164 pages.
Tome 4. – Application des méthodes de synthèse, 1987, 272 pages.
La commutation électronique, par Grinsec (épuisé).
Tome 1. – Structure des systèmes spatiaux et temporels, 1984, 456 pages.
Tome 2. – Logiciel. Mise en œuvre des systèmes, 1984, 512 pages.
Optique et télécommunications. transmission et traitement optiques de l’information,
par A. Cozannet, J. Fleuret, H. Maître et M. Rousseau, 1983, 512 pages (épuisé).
434 Structures de données et méthodes formelles

Radarmétéorologie : télédétection active de l’atmosphère, par H. Sauvageot, 1982,


312 pages.
Probabilités, signaux, bruits, par J. Dupraz, 1983, 384 pages (épuisé).
Méthodes structurelles pour la reconnaissance des formes, par L. Miclet, 1984,
208 pages.
Introduction aux réseaux de files d’attente, par E. Gelenbe et G. Pujolle, 1985,
208 pages (épuisé).
Applications des transistors à effet de champ en arséniure de gallium, coordonné par
R. Soares, J. Obregon et J. Graffeuil, 1984, 532 pages.
Pratique des circuits logiques, par J.-M. Bernard et J. Hugon, 1990, 480 pages (épuisé).
Théorie des guides d’ondes électromagnétiques, par Ch. Vassallo.
Tome 1. – 1985, 504 pages.
Tome 2. – 1985, 700 pages.
Conception des circuits intégrés MOS. Eléments de base, perspectives, par M. Cand,
E. Demoulin, J.-L. Lardy et P. Senn, 1986, 472 pages.
Théorie des réseaux et systèmes linéaires, par M. Feldmann, 1987, 424 pages (épuisé).
Conception structurée des systèmes logiques, par J.-M. Bernard, 1987, 2e tirage,
400 pages.
Prévision de la demande de télécommunications. Méthodes et modèles, par N. Curien et
M. Gensollen, 1989, 488 pages.
Systèmes de radiocommunications avec les mobiles, par J.-G. Rémy, J. Cueugniet et
C. Siben, 1992, 2e édition, 668 pages.
Innovation, déréglementation et concurrence dans les télécommunications, par
L. Benzoni et J. Hausman, 1993, 344 pages.
Les télécommunications : technologies, réseaux, services, par L.-J. Libois, 1994,
216 pages (épuisé).
Innovation et recherche en télécommunications. Progrès techniques et enjeux
économiques, par M. Feneyrol et A. Guérard, 1994, 328 pages (épuisé).
Les ondes évanescentes en optique et en optoélectronique, par F. de Fornel, 1997,
312 pages.
Codesign, conception conjointe logiciel-matériel, par C.T.I. Comete, 1998, 204 pages.
Introduction au Data Mining. Analyse intelligente des données, par M. Jambu, 1998,
114 pages.
Méthodes de base de l’analyse des données, par M. Jambu, 1999, 412 pages et un CD-
Rom.
Des télécoms à l’Internet : économie d’une mutation, par E. Turpin, 2000, 459 pages.
Ingénierie des connaissances – évolutions récentes et nouveaux défis, par J. Charlet,
M. Zacklad, G. Kassel et D. Bourigault, 2000, 610 pages (épuisé).
Emission photonique en milieu confiné, par A. Rahmani et F. de Fornel, 2000,
190 pages.
Dans la même collection 435

Apprentissage artificiel, concepts et algorithmes, par A. Cornuéjols et L. Miclet, 2002,


590 pages.

Masson
Stéréophonie. Cours de relief sonore théorique et appliqué, par R. Condamines, 1978,
320 pages.
Les Réseaux pensants. Télécommunications et société, sous la direction de A. Giraud,
J.-L. Missika et D. Wolton, 1978, 296 pages (épuisé).
Fonctions aléatoires, par A. Blanc-Lapierre et B. Picinbono, 1981, 440 pages.
Psychoacoustique. L’oreille recepteur d’information, par E. Zwicker et R. Feldtkeller.
Traduit de l’allemand par C. Sorin, 1981, 248 pages.
Décisions en traitement du signal, par P.-Y. Arquès, 1982, 288 pages (épuisé).
Télécommunications spatiales, par des ingénieurs du CNES et du CNET (épuisé).
Tome 1. – Bases théoriques, 1982, 432 pages.
Tome 2. – Secteur spatial, 1983, 400 pages.
Tome 3. – Secteur terrien. Systèmes de télécommunications par satellites, 1983,
468 pages.
Genèse et croissance des télécommunications, par L.-J. Libois, 1983, 432 pages (épuisé).
Le Vidéotex. Contribution aux débats sur la télématique, coordonné par Cl. Ancelin
et M. Marchand, 1984, 256 pages.
Ecoulement du trafic dans les autocommutateurs, par G. Hébuterne, 1985,
264 pages.
L’Europe des Postes et Télécommunications, par CI. Labarrère, 1985, 256 pages.
Traitement du signal par ondes élastiques de surface, par M. Feldmann et J. Hénaff,
1986, 400 pages (épuisé).
Théorie de l’information ou analyse diacritique des systèmes, par J. Oswald 1986,
488 pages.
Les vidéodisques, par G. Broussaud, 1986, 216 pages.
Les paradis informationnels : du minitel aux services de communication du futur, par
M. Marchand et le SPES, 1987, 256 pages.
Systèmes et réseaux de télécommunication en régime stochastique, par G. Doyon,
1989, 704 pages.
Principes de traitement des signaux radar et sonar, par R. Le Chevalier, 1989,
280 pages.
Circuits intégrés en arséniure de gallium. Physique, technologie et règles de conception,
par R. Castagné, J.-P. Duchemin, M. Gloanec et G. Rumelhard, 1989, 608 pages.
Analyse des signaux et filtrage numérique adaptatif, par M. Bellanger, 1989, 416 pages
(épuisé).
436 Structures de données et méthodes formelles

La parole et son traitement automatique, par Calliope, 1989, 736 pages.


Les filtres numériques. Analyse et synthèse des filtres unidimensionnels, par R. Boite et
H. Leich, 1990, 3e édition, 432 pages.
Les modems pour transmission de données, par M. Stein, 1991, 384 pages.
La mesure de la fréquence des oscillateurs, par Chronos, 1992, 368 pages.
Traitements des signaux pour les systèmes sonar, par M. Bouvet, 1992, 504 pages.
Codes correcteurs d’erreurs. Une introduction au codage algébrique, par G. Cohen,
Ph. Godlewski, J.-L. Dornstetter, 1992, 272 pages.
Complexité algorithmique et problèmes de communications, par J.-P. Barthélémy,
G. Cohen et A. Lobstein. Préface de M. Minoux, 1992, 256 pages.
Gestion de réseaux : concepts et outils, par Arpège, 1992, 272 pages.
Les normes de gestion de réseau à l’ISO, par C. Lecerf et D. Chomel, 1993, 272 pages.
L’implantation ionique pour la microélectronique et l’optique, par P-N. Favennec,
1993, 532 pages.
Technique de compression des signaux, par N. Moreau, 1994, 288 pages.
Le RNIS. Techniques et atouts, par G. Dicenet, 1995, 3e édition, 312 pages.
Théorie structurale de la communication et société, par A.-A. Moles, 1995, 3e tirage,
296 pages.
Traitement numérique du signal : théorie et pratique, par M. Bellanger, 1995, 5e
édition, 480 pages.
Télécommunication : réalités et virtualités : un avenir pour le XXe siècle, par
M. Feneyrol, 1996, 256 pages.
Comprendre les méthodes formelles. Panorama et outils logiques, par J.-F. Monin,
1996, 320 pages (épuisé).
La programmation réactive : application aux systèmes communicants, par F. Boussinot,
1996, 280 pages.
Ingénierie des systèmes à microprocesseurs : application au traitement du signal et de
l’image, par E. Martin et J.-L. Philippe, 1996, 320 pages.
Le régime juridique communautaire des services de télécommunications, par
A. Blandin-Obernesser, 1996, 216 pages.
Paysage des réseaux de télécommunications, par R. Parfait, 1997, 376 pages (épuisé).
Le complexe de Babel. Crise ou maîtrise de l’information ?, par Jean Voge, 1997,
192 pages.
La télévision haute définition (TVHD), par A. Boukelif, 1997, 233 pages.

Documentation française
Les télécommunications françaises. Quel statut pour quelle entreprise ?, par
G. Bonnetblanc, 1985, 240 pages.
Dans la même collection 437

La communication au quotidien. De la tradition et du changement à l’aube de


la vidéocommunication, par J. Jouët, avec la collaboration de N. Celle, 1985,
240 pages.
L’ordre communicationnel. Les nouvelles technologies de la communication : enjeux et
stratégies, par F. du Castel, P. Chambat et P. Musso, 1989, 352 pages.
Histoire d’enfance. Les réseaux câblés audiovisuels en France, par J.-M. Charon, J.-
P. Simon, avec la participation de B. Miège, 1989, 240 pages.
La communication plurielle : l’interaction dans les téléconférences, coordonné par
P. Périn et M. Gensollen, 1992, 304 pages.
Métaphore et multimédia : concepts et applications, par G. Poulain, 1996, 240 pages.
Histoire comparée de stratégies de développement des télécommunications, par A.-M.
Delaunay Macullan, 1997, 166 pages.

Presses polytechniques
et universitaires romandes
ADA avec le sourire, par J.-M. Bergé, L.-O. Donzelle, V. Olive et J. Rouillard, 1989,
400 pages.
Systèmes microprogrammés Une introduction au magiciel, par D. Mange, 1990,
384 pages.
Réseaux de neurones récursifs pour mémoires associatives, par Y. Kamp et M. Hasler,
1990, 244 pages.
VHDL, du langage à la modélisation, par R. Airiau, J.-M. Bergé, V. Olive et
J. Rouillard, 1990, 576 pages (épuisé).
Traitement de l’information, sous la direction de M. Kunt.
Volume 1. – Techniques modernes de traitement numérique des signaux, 1991,
440 pages.
Volume 2. – Traitement numérique des images, 1993, 584 pages.
Volume 3. – Reconnaissance des formes et analyse des scènes, 2000, 306 pages.
Effets non linéaires dans les filtres numériques, par R. Boite, M. Hasler et H. Dedieu,
1997, 226 pages.
VHDL, langage, modélisation, synthèse, par R. Airiau, J.-M. Bergé, V. Olive et
J. Rouillard, 1998, 568 pages.
Les objets réactifs en Java, par F. Boussinot, 2000, 188 pages.
Codage, cryptologie et applications, par B. Martin, 2004, 350 pages.
Processus stochastiques pour l’ingénieur, par Bassel Solaiman, 2006, 240 pages.
Paiements électroniques sécurisés, par M. H. Sherif, 2007, 600 pages.
438 Structures de données et méthodes formelles

Springer
ASN.1 – communication entre systèmes hétérogènes, par O. Dubuisson, 1999,
546 pages.
Droit et sécurité des télécommunications, par C. Guerrier et M.-C. Monget, 2000,
458 pages.
SDH, normes, réseaux et services, par T. Ben Meriem, 2000, 633 pages.
Traitement du signal aléatoire, par T. Chonavel, 2000, 296 pages.
Les fondements de la théorie des signaux numériques, par R. L. Oswald, 2000, 272 pages.
Le champ proche optique, par D. Courjon et C. Bainier, 2001, 344 pages.
Communications audiovisuelles, par E. Rivier, 2002.
La propagation des ondes radioélectriques, par H. Sizun, 2002, 360 pages.
Optoélectronique moléculaire et polymère : des concepts aux composants, par
A. Moliton, 2003, 426 pages.
Electronique et optoélectronique organiques, par A. Moliton, 2011, 568 pages.

Vous aimerez peut-être aussi