Académique Documents
Professionnel Documents
Culture Documents
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
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.
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. . .
Henri Habrias
Professeur émérite à l’Université de Nantes
Table des matières
Préface ix
Avant-propos 1
I Les bases 13
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
Bibliographie 417
Index 425
Avant-propos
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
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
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
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
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.
l • 2 • 4 • 7 • 9 /
6 •
l • 2 • 4 • 7 • 9 /
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
Plan de l’ouvrage
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
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.
8
0Z0L0Z0Z
7
Z0Z0Z0L0
6
0ZQZ0Z0Z
5
Z0Z0Z0ZQ
4
0L0Z0Z0Z
3
Z0Z0L0Z0
2
QZ0Z0Z0Z
1
Z0Z0ZQZ0
a b c d e f g h
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.
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
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)
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
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 |
prédicat : := ⊥ |
prédicat ∧ prédicat |
prédicat ⇒ prédicat |
¬prédicat
(prédicat)
prédicat : := ... |
|
prédicat ∨ prédicat |
prédicat ⇔ prédicat
Notation Définition
¬⊥
P ∨Q ¬P ⇒ Q
P ⇔Q (P ⇒ Q) ∧ (Q ⇒ P )
expression : := variable |
expression → expression |
(expression, expression) |
(expression)
variable : := identificateur
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 :
[x := y]x + 5
= Définition de la substitution
y+5
ou encore :
Les deux nouvelles règles d’inférence nécessaires au raisonnement sur les pré-
dicats quantifiés universellement sont :
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
expression : := ... |
if listeExpGardées fi
listeExpGardées : := expressionGardée |
expressionGardée | listeExpGardées
expressionGardée : := prédicat → expression
⎛ → 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 ⎠
fi
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
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
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
prédicat : := ... |
expression ∈ expression
expression : := ... |
expression × expression |
P(expression) |
{listeVar · prédicat | expression}
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}
s ∈ P(s)
∀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
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.
expression : := ... |
choice(expression)
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)
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 )
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 :
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) |
expression : := ... |
any listeVar where prédicat then expression end
any e where
e ⊆ 1 .. 100 ∧
(7 ∈ e ∨ 23 ∈ e)
then
e − {49}
end
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
Notation Définition
any E where P end any E where P then E end
expression : := ... |
expression ↔ expression |
dom(expression) |
ran(expression)
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)
×
×
×
×
×
×
×
×
×
expression : := ... |
expression−1 |
expression expression |
expression expression |
expression
− expression |
expression
− expression |
expression[expression]
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)
S T S T
U U r rV V
× ×
× ×
× ×
× ×
× ×
× ×
× ×
× ×
× ×
S T S T
U U
−r r
−V V
× ×
× ×
× ×
× ×
× ×
× ×
× ×
× ×
× ×
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
S T U S U
r t r; t
× × × ×
×
× × × ×
×
× × × ×
×
× × × ×
×
× × × ×
S r T S r − t T
t
× ×
× ×
× ×
× ×
× ×
× ×
× ×
× ×
× ×
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
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
λi ·(i ∈ 1 .. 10 | 2 · i)
expression : := ... |
λlisteVar ·(prédicat | expression)
Évaluation de fonctions
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 :
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
Exercices
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.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.
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
prédicat : := ... |
expression ≤ expression |
expression < expression |
expression ≥ expression |
expression > expression
expression : := ... |
expression .. expression |
card(expression) |
max(expression) |
min(expression)
expression : := ... |
listeVar ·(prédicat | expression) |
listeVar ·(prédicat | expression)
Nous pouvonsainsi é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 |
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)
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.
t1
− t2 card(t1)
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.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.
indiv ∈ N ∧
1. Mathématiques pour la spécification et les structures de données 41
Si nous souhaitons modéliser le fait que les parents sont plus âgés que leurs
enfants, nous pouvons écrire :
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 :
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 :
Autrement dit : un parent qui peut être tuteur est un tuteur de ses enfants.
Le point 3 peut se formuler par :
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 :
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.
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 :
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
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) ·(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.
expressionPréc : := pre
prédicat
then
expression
end
Quant au tableau suivant, il définit les règles d’inférence pour les expressions
préconditionnées.
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.
Exemple
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 :
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) :
⎧
⎨ [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)
divis(4, 2) = 2
function f (v) ∈ N → N =
if v = 0 →
1
|v>0 →
v · f (v − 1)
fi
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
Exemple
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.
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
⏐ ⏐ .
⏐ ⏐ ..
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
fi
| v2 ≤ v1 →
if v3 ≤ v2 →
v2
| v3 ≥ v1 →
v1
| v2 ≤ v3 ∧ v 3 ≤ v1 →
v3
fi
fi
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
fi
end
function f (v) ∈ N → N =
if v ≥ 5 →
4
|v≤5 →
9
fi
56 Structures de données et méthodes formelles
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 →
..
.
fi
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é.
Spécifications + Fonction
d’abstraction + Calcul =
Programme
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 ))
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
– 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
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
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.
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 =
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.
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)
A(plus_n(x, y))
= Propriété caractéristique
A(x) + A(y) (2.3.1)
A(x) + A(y)
= Hypothèse (x = zero)
A(zero) + A(y)
= Définition de A
0 + A(y)
= Arithmétique
A(y)
x = zero →
plus_n(x, y) = y
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.
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
Nous devons à nouveau procéder à une induction sur x. Débutons par le cas
x = zero.
y = zero →
x = zero →
inf _n(x, y) = true
y = suiv(y ) →
x = suiv(x ) →
inf _n(x, y) = inf _n(x , y )
Exercices
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)}
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)
fi
• 7 • 12 • 4 /
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
• 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]).
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.
arbres arbres
binaires
listes et
planaires arbres-
feuilles
ensembles
arbres
non sacs
ordonnés
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 | [ ]]]]
{1 → 3, 2 → 7, 3 → 7, 4 → 12, 5 → 4}
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 )
Exercices
Exercice 3.1.1 Ci-dessus nous avons fourni une construction inductive des listes. Proposer à
présent une construction directe.
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, [ ]]]].
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
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 .
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
É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 :
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).
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.
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 :
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
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
• • • •
(1) (2) (3) (4)
• • • • • • •
• • • • • • • • • • •
• • • • • • •• • • • • • •
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
}
à 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.
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
7. Notion définie ici par analogie avec la notion de diamètre dans les graphes.
3. Étude de quelques structures outils 93
• +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é
⇒ 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.
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
vg d vg v
rgd
gg v gd gg g dg ddg d
g dg ddg
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
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
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
{ [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 :
(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 :
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
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 ?
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).
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) ∧ ⎪
⎬
st 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)}) ⎭
Exercice
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
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.
f (n)
f2 f (n) f2
f1
(11) f1 (12)
n n
n0
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
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 Θ.
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
Exercices
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.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.
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 ).
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é.
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 :
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
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
[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
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
∀i ·(i ∈ 0 .. n − 1 ⇒ T (n − i + 1) = T (n − i) + 1)
[i := n − 1](i ∈ 0 .. n − 1 ⇒ T (n − i + 1) = T (n − i) + 1)
⇔ Calculs non développés
T (n) = n
20 · T (m) = 20 ·(2 · T (m − 1) + 1)
+
21 · T (m − 1) = 21 ·(2 · T (m − 2) + 1)
+
..
.
+
2m · T (0) = 2m · 1
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.
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 :
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
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.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).
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 dechacune 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.
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
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.
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
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 :
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+
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
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
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).
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
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].
function raz_l() ∈ lb =
l : (l ∈ lb ∧ A(l) = raz())
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 =
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
function raz_l() ∈ lb =
[]
L’opération raz_l() délivre simplement une liste vide. Sa complexité est trivia-
lement en O(1).
A(suc_l(l))
= Propriété caractéristique de l’opération suc_l
1 + A(l) (5.1.1)
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 | [ ]])
l=[]→
suc_l(l) = [1 | [ ]]
1 + A(l)
= Hypothèse
1 + A([v | q])
= Définition de A
1 + v + 2 · A(q) (5.1.2)
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])
l = [v | q] →
v=0→
suc_l(l) = [1 | q]
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)])
l = [v | q] →
v=1→
suc_l(l) = [0 | suc_l(q)]
function suc_l(l) ∈ lb → lb =
if l = [ ] →
[1 | [ ]]
| l = [v | q] →
if v = 0 →
[1 | q]
|v=1 →
[0 | suc_l(q)]
fi
fi
T (suc_l(l)) ∈ O(#(l))
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
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)
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
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
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))
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.
1) [ ] ∈ lt
2) v ∈ N ⇒ [v | [ ]] ∈ lt
3) v ∈ N ∧ [w | q] ∈ lt ∧ v < w ⇒ [v | [w | q]] ∈ lt
function A(l) ∈ lt
entN at =
if l = [ ] →
∅
| l = [v | q] →
{v} ∪ A(q)
fi
A(eAjout_l(v, l))
= Propriété caractéristique de l’opération eAjout_l
{v} ∪ A(l) (5.2.1)
5. Exemples 141
l = [w | q] →
v=w →
eAjout_l(v, l) = l
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]]
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 :
l = [w | q] →
v>w →
eAjout_l(v, l) = [w | eAjout_l(v, q)]
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)]
fi
fi
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.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
Structures de données
fondamentales :
spécification et mises en
œuvre
Chapitre 6
Exercices
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
1) ∈ eAbr
2) n ∈ N ∧ g ∈ eAbr ∧ d ∈ eAbr ∧ max(A(g)) < n ∧ min(A(d)) > n
⇒
g, n, d ∈ eAbr
, 4, , 7, , 10, , 15, , 20, , 24, , 27, , 30, , 33,
20
7 33
4 15 27
10 24 30
6. Ensembles de clés scalaires 151
A(eAjout_a(v, a))
= Propriété caractéristique de l’opération eAjout_a
A(a) ∪ {v} (6.3.1)
= Application de A
A(, v, )
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)
a = g, n, d →
v=n →
eAjout_a(v, a) = g, n, d
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 :
• • • • •
• • • • • •
• • 2, 1, 3 • •
3, 2, 1 3, 1, 2 2, 3, 1 1, 3, 2 1, 2, 3
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 formenclose 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
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)
2·
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
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)
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
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
···
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
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)
v∈
/ A(a) →
let (r, l) := part(v, a) in
eAjout_a(v, a) = l, v, r
end
A (part(v, a))
= Propriété caractéristique
A(a) (6.3.7)
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)
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
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
a = →
eSupp_a(v, a) =
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)
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)
v=n→
g = →
eSupp_a(v, g, n, d) = d
6. Ensembles de clés scalaires 163
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)
v=n→
g = ∧ d = →
eSupp_a(v, g, n, d) = eSupp_a(max(A(g)), g), max(A(g)), d
if g = →
d
| d = →
g
| g = ∧ d = →
eSupp_a(max(A(g)), g), max(A(g)), d
fi
|v<n →
eSupp_a(v, g), n, d
|v>n →
g, n, eSupp_a(v, d)
fi
fi
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
···
concreteType eabr =
..
.
auxiliaryOperationSpecifications
function maxRac(a) ∈ eAbr → eAbr =
pre
A(a) = ∅
then
a : (a ∈ eAbr ∧ a = g , m, ∧ A(a ) = A(a))
end
end
v=n→
g = ∧ d = →
let g , m, := maxRac(g) in
eSupp_a(v, g, n, d) = g , m, d
end
A(maxRac(a))
= Spécification de maxRac
A(a)
= Hypothèse
A(l, w, r) (6.3.12)
A(l, w, r)
= Hypothèse
A(l, w, )
let l, w, r := a in
r = →
maxRac(a) = a
end
A(l, w, r)
= Définition de A
A(l) ∪ {w} ∪ A(r)
= Spécification de maxRac
A(l) ∪ {w} ∪ A(maxRac(r))
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
Exercices
Exercice 6.3.1 L’opération de rotation droite (rd) d’un arbre binaire de recherche est définie
par :
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
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.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.
17 •
28 •
2
54 •
0
127 • 1
221 •
241 •
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 •
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
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}
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).
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.
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).
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.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
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 :
•
• •
• • 27 •
4 7 10 • 30 35
• 24
15 20
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.
a = →
eAjout_ae(v, a) = v
A(a) ∪ {v}
= Hypothèse
A(n) ∪ {v}
= Définition de A
A(n) ∪ A(v)
A(n) ∪ A(v)
= Définition de A
{n} ∪ {v}
= Hypothèse et propriété A.9
{n}
= Définition de A
A(n)
a = n →
n=v→
eAjout_ae(v, a) = a
A(n) ∪ A(v)
= Définition de A
A(n, v)
a = n →
v>n→
eAjout_ae(v, a) = n, v
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, d →
v ≤ max(A(g)) →
eAjout_ae(v, a) = eAjout_ae(v, g), 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
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
n−2
(n − 2) · C(n − 1) = (n − 2)2 + 2 · C(i) (6.5.6)
i=1
(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)
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
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.
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) =
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()
a = n →
n=v →
eSupp_ae(v, a) =
186 Structures de données et méthodes formelles
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).
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
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
35
24 35
7 24 27 35
4 7 10 24 30 35
20 24
15 20
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
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.
24
7 27
4 10 27 30
4 7 10 20 30 35
15 24
15 20
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.
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).
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
120 340
Un Avl Un Avl
190
10+1 27−2
70 290 320
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.
[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+
[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é.
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.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.
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
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
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.
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
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
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
A(eAjout_v(v, a))
= Propriété caractéristique de eAjout_v
A(a) ∪ {v} (6.6.4)
a = →
eAjout_v(v, a) = , v,
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)
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
(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
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
r(d) ∈ −1 .. 1 ne serait pas satisfaite. La propriété Aug(a) est donc établie pour
cette équation gardée.
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
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
a = →
eSupp_v(v, a) =
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)
A(g) ∪ A(d)
= Hypothèse
∅
= Définition de A
A()
a = g, u, d →
v=u →
g = ∧ d = →
eSupp_v(v, 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
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)
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
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.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.
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
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
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).
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
0
| x = te, tl, lg →
r(tl(0)) + 1
fi
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
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
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
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 :
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
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)))).
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
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
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)
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 :
(1 .. i − 1 te)[1 .. lg]
= Propriétés B.52 et A.26
(1 .. i − 1 te)[1 .. lg + 1] (6.7.12)
(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)
{v}
= Propriété B.60
{i → v}[1 .. lg + 1] (6.7.14)
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 :
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)))
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
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))
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).
1 i lg 1 n+1 2·n+1
geG geD peG m peD
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
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 :
À 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
1 n+1 2·n+1
te teG m teD (1)
1
m (2)
1 n 1 n
teG teD
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.
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
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 :
É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
à 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
a a
a n zig-zag ( m , n )
l m r g r
d d
g d
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.
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
a = →
part(v, a) = (, )
···
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
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 :
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)
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 ) :
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
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
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.
∈ 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 :
⎧
⎪
⎪ 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
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
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)
dont la partie droite vient se substituer à T (part(v, g)) dans la formule précé-
dente :
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 second cas de base, a = , n, r, le calcul se développe comme suit :
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)
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
Les cas zag-zag et zag-zig sont analogues aux cas zig-zig et zig-zag. Nous
avons donc démontré que
M(eAjout_ad(v, a))
= Partitionnement + enracinement
M(part(v, a)) + 1 + Φ(l, v, r) − Φ(l) − Φ(r)
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 ?
function prio(v) ∈ N N =
...
227
1210 348
715 1813 2712 3919
418 1021 1417 2020 2424 3022
929 1328 1627
function P rio(a) ∈ eT rp → N =
...
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.
a = →
eAjout_tp(v, a) = , v,
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 = 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 :
= 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
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
fi
end
|v>n → /*insertion à droite*/
..
.
fi
fi
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.
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)
{n} − {v}
= Hypothèse et propriété A.18
∅
= Définition de A
A()
a = g, n, d →
g = ∧ d = →
v=n →
eSupp_tp(v, a) =
{n} − {v}
= Hypothèse et propriété A.16
n
= Propriété A.9
(∅ ∪ {n} ∪ ∅) − {v}
= Définition de A
A(, n, )
a = g, n, d →
g = ∧ d = →
v = n →
eSupp_tp(v, a) = , n,
a = g, n, d →
g = ∨ d = →
v = n →
v<n →
eSupp_tp(v, a) = eSupp_tp(v, g), n, d
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
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
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
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
fi
end
|v>n →
..
.
fi
fi
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.
Exercices
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
{ 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
c ∈ char ∧ b ∈ bool ∧ f ∈ f t ∧
(b = true ∨ f = ∅)
⇔
c, b, f ∈ at
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 A (c, b, f )
⎛∈ at → ensAbst(chaine)
⎞
=
if b = true →
⎜ {[c]} ⎟
⎜ ⎟
pref ixer(c, A(f )) ∪ ⎜ | b = false → ⎟
⎜
⎟
⎝ ∅ ⎠
fi
···
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
fi
end
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 analyse par cas sur q, selon que q = [ ] ou non. Débutons par
q = [ ].
f =∅ →
let [c | q] := l in
q=[] →
eAjout_f t(l, f ) = {c, true, ∅}
end
f =∅→
let [c | q] := l in
q = [ ] →
eAjout_f t(l, f ) = {c, false, eAjout_f t(q, ∅)}
end
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.
Nous devons effectuer une analyse par cas selon que q = [ ] ou que q = [ ].
Débutons par le cas q = [ ] :
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
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 devons à nouveau réaliser une analyse par cas, en distinguant le cas q = [ ]
de sa négation. Débutons par q = [ ] :
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
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éfinition de A, de [c | q] et de d, b, g
A({i} ∪ eAjout_f t(l, h))
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
fi
| c = d →
{i} ∪ eAjout_f t(l, h)
fi
end
end
fi
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.
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).
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
{ 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.
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
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.
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
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
{ m , n }
i a i
e m s
{ m , n }
e i a i
e m s
{ m , n }
e i
e m s
{ m , n }
i
e m s
{ m , n }
mien nais
mima nies
mise nias
miss nina
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.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 :
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.
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
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
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
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
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
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 :
Il nous faut alors procéder à une analyse par cas selon que (x, y) = (x , y ) ou
non. Dans le premier cas, nous avons :
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 :
a = g, (x , y ), d →
(x, y) = (x , y ) →
x ≤ x →
ecAjout_x((x, y), a) = ecAjout_y((x, y), g), (x , y ), d
ecAjout_2d((x, y), a)
= Spécification de ecAjout_2d
ecAjout_x((x, y), a)
ecCoupeX_x(x, a)
= Spécification de ecCoupeX_x
Ax (a)[{x}] (7.2.3)
Ax (a)[{x}]
= Hypothèse
Ax ()[{x}]
= Définition de Ax
∅[{x}]
= Propriété B.62
∅
a = →
ecCoupeX_x(x, a) = ∅
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}]
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 }
{(x , y )}[{x}]
= Propriété B.62
∅
a = g, (x , y ), d →
ecCoupeX_x(x, a) =
ecCoupeX_y(x, g) ∪ if x = x → {y } | x = x → ∅ fi
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
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).
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 :
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
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
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.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.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.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.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.
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
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.
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.
···
abstractType fileabst(T ) =
..
.
auxiliaryOperationRepresentations
function ens(f ) ∈ f ileAbst(T ) F(T )
if f = [ ] →
∅
| f = [h | q] →
ens(q) ∪ {h}
fi
;
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]
fi
end
;
function l ˜ ∈ f ileAbst(T )
f ileAbst(T )
if l = [ ] →
[]
| l = [h | q]
q ˜[h | [ ]]
fi
end
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.
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
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
A((a, b)) = [ ]
= Définition de A
a b˜ = [ ]
8. Files simples 321
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))
a = [h | q] →
f Supp_dl((a, b)) = (q, b)
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˜, [ ])))
a=[] →
f Supp_dl((a, b)) = f Supp_dl((b˜, [ ]))
322 Structures de données et méthodes formelles
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.
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] :
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).
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
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é
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.
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 :
• •
• • • •
• • • • • • • •
• • • •• • • • • • •
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
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
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.
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.
Bi
Bi
! "# $
Bi+1
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
1 2 3 2 7 1
2 3 8 4 7 5 6 5
4 (13) 8 9 5 (14) 9
9
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.
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.
j ∈ N ∧ f ∈ f B(j) ∧ #(f ) = j
⇔
f ∈ f BC(j)
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.
niveau
3
0 1=
0
3
1 3=
1
3
2 3=
2
3
3 1=
3
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
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)
fi
; function r(f ) ∈ f B(i) → N =
if f = [ ] →
0
| f = [t | q] →
r(t)
fi
end
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
A (f pF us_mb(t, t ))
= Propriété caractéristique de f pF us_mb
A (t) A (t ) (9.4.8)
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])
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.
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
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)
P(l) = 3 · l − 2
⇒ Propriété 9.4.5, page 334
P(l) ≤ 3 · log(n + 1) − 2
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
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 :
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 :
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 .
Soit encore :
F(0) = 2
F(l) = F(l − 1) + 6 si l > 0
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))
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
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 :
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
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 | [ ]])
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)
f = [t | q] →
r(m) = r(t) →
f aa(m, f ) = [f pF us_mb(m, t) | q]
f = [t | q] →
r(m) > r(t) →
f aa(m, f ) = [m | f ]
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) :
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
L’équation qui fournit la complexité amortie de l’opération f aa est alors (cf. for-
mule 4.3.2, page 125) :
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 = [ ] :
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 :
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 :
Posons t = v, g :
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]).
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
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 :
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)).
···
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
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 :
let v, g := t in
v=w→
f pSuppAux_f b([t | q], w) = f pF us_f b(g, q)
end
let v, g := t in
v = w →
f pSuppAux_f b([t | q], w) = [t | f pSuppAux_f b(q, w)]
end
S (l) = 12 · l − 9 (9.4.22)
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 :
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).
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
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
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.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.
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).
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 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.
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
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 )
f = →
f us(f, f ) = f
9. Files de priorité 365
À 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.
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).
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
fi
end
fi
fi
9. Files de priorité 367
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, ))
function f pAjout_mo(v, f ) ∈ N × mo → mo =
f us(f, , v, )
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
function f pSupp_mo(f ) ∈ mo
mo =
pre
f =
then
let g, r, d := f in
f us(g, d)
end
end
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 :
ϕ(a, b) ∈ mo
× mo → R+
1 a < b
si
ϕ(a, b) = (9.5.3)
0 a ≥ b
si
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
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é.
= 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)
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 :
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
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) :
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 :
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
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 )).
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 :
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.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
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
1) ∈ twBr(E)
2) r ∈ e ∧ g, d ∈ twBr(E) × twBr(E) ∧ w(g) − w(d) ∈ {0, 1}
⇒
g, r, d ∈ twBr(E)
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
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
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).
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.
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
({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
= 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
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 :
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)
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, )
a = →
twAll_br(v, a) = , v,
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}
⎛ ⎞
{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))
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
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.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
1) ∈ tsM b
2) r ∈ N ∧ g, d ∈ tsM b × tsM b ∧ a = g, r, d ∧ r = min(ens(a))
⇒
a ∈ tsM b
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
7 7
9 7 7
17 10 10 19 8
14 15 17 19
23
10. Tableaux flexibles 397
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.
relatifs sont équiprobables, on montre (cf. [115, 116]) que la complexité moyenne
des opérations est en O(log n).
Exercices
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
Opérateurs et priorités
[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
, 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
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
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
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
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
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
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.