Arbres binaires de recherche optimaux et quasi-optimaux

publicité
Université Libre de Bruxelles
Faculté des Sciences
Département d’Informatique
Arbres binaires de recherche
optimaux et quasi-optimaux
Mémoire présenté par Gabriel Kalyon
en vue de l’obtention du grade de
Licencié en Informatique.
Année académique 2005-2006.
Table des matières
1 Introduction
5
I
7
Fondements et définitions
2 Arbres binaires et complexité
2.1 Arbres binaires . . . . . . . . . . . . . . . . .
2.1.1 Définitions . . . . . . . . . . . . . . . .
2.1.2 Profondeur d’un nœud . . . . . . . . .
2.1.3 Hauteur d’un nœud . . . . . . . . . . .
2.1.4 Rotations . . . . . . . . . . . . . . . .
2.2 Arbres binaires de recherche . . . . . . . . . .
2.2.1 Modèle . . . . . . . . . . . . . . . . . .
2.2.2 Classes d’arbres binaires de recherche .
2.3 Performances asymptotiques . . . . . . . . . .
2.3.1 Notation O(.) . . . . . . . . . . . . . .
2.3.2 Notation Ω(.) . . . . . . . . . . . . . .
2.3.3 Notation Θ(.) . . . . . . . . . . . . . .
2.3.4 Lien entre les notations asymptotiques
2.4 Complexité . . . . . . . . . . . . . . . . . . .
2.4.1 Complexité au pire cas . . . . . . . . .
2.4.2 Complexité moyenne . . . . . . . . . .
2.4.3 Complexité amortie . . . . . . . . . . .
2.5 Conclusion . . . . . . . . . . . . . . . . . . . .
3 Propriétés des arbres binaires de
3.1 Relations entre les propriétés . .
3.2 Static finger . . . . . . . . . . .
3.3 Optimalité statique . . . . . . .
3.4 Working set . . . . . . . . . . .
3.5 Dynamic finger . . . . . . . . .
1
recherche
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
8
8
8
9
9
9
11
12
13
14
15
15
15
15
16
16
16
17
18
.
.
.
.
.
19
19
19
21
21
22
3.6 Unifiée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.7 Optimalité dynamique . . . . . . . . . . . . . . . . . . . . . . 24
3.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
II
Arbres binaires de recherche particuliers
4 Arbres Binaires de Recherche
4.1 Généralités . . . . . . . . .
4.2 Algorithme exhaustif . . . .
4.3 Algorithme de Knuth . . . .
4.4 Conclusion . . . . . . . . . .
Optimaux
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
5 Arbres binaires de recherche équilibrés
5.1 Définition . . . . . . . . . . . . . . . .
5.2 Arbres rouges-noirs . . . . . . . . . . .
5.2.1 Définition . . . . . . . . . . . .
5.2.2 Recherche . . . . . . . . . . . .
5.2.3 Insertion . . . . . . . . . . . . .
5.2.4 Successeur . . . . . . . . . . . .
5.2.5 Prédécesseur . . . . . . . . . . .
5.2.6 Analyse des performances . . .
5.3 Conclusion . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6 Arbres binaires de recherche auto-ajustables
6.1 Définition . . . . . . . . . . . . . . . . . . . .
6.2 Splay trees . . . . . . . . . . . . . . . . . . . .
6.2.1 Splaying . . . . . . . . . . . . . . . . .
6.2.2 Recherche . . . . . . . . . . . . . . . .
6.2.3 Insertion . . . . . . . . . . . . . . . . .
6.2.4 Suppression . . . . . . . . . . . . . . .
6.2.5 Analyse des performances . . . . . . .
6.3 Comparaisons . . . . . . . . . . . . . . . . . .
6.4 Conclusion . . . . . . . . . . . . . . . . . . . .
III
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
26
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
27
27
29
29
31
.
.
.
.
.
.
.
.
.
32
32
32
33
33
33
34
35
35
36
.
.
.
.
.
.
.
.
.
38
38
38
39
39
39
39
40
44
45
Optimalité dynamique
7 Bornes inférieures sur le coût des arbres binaires de recherche
7.1 Modèle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.2 La borne inférieure de couverture de rectangle . . . . . . . . .
7.3 Applications de la borne inférieure de couverture de rectangle
2
46
47
47
48
53
7.3.1 La borne inférieure d’entrelacement . . . . . . . . . . . 54
7.3.2 La seconde borne de Wilber . . . . . . . . . . . . . . . 56
7.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
8 Tango
8.1 Structures de données . . . . . .
8.2 Algorithme de l’arbre de référence
8.3 Arbres auxiliaires . . . . . . . . .
8.3.1 Définition . . . . . . . . .
8.3.2 Concaténation . . . . . . .
8.3.3 Éclatement . . . . . . . .
8.3.4 Cutting . . . . . . . . . .
8.3.5 Joining . . . . . . . . . . .
8.4 Algorithme Tango . . . . . . . . .
8.5 Analyse des performances . . . .
8.6 Conclusion . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
9 Multi-splay tree
9.1 Structures de données . . . . . . . . . .
9.2 Algorithmes . . . . . . . . . . . . . . . .
9.2.1 Algorithme de l’arbre de référence
9.2.2 Algorithme multi-splay tree . . .
9.3 Analyse des performances . . . . . . . .
9.3.1 Fonction potentiel . . . . . . . . .
9.3.2 Access Lemma Généralisé . . . .
9.3.3 Multi-Splay Access Lemma . . . .
9.3.4 Propriétés . . . . . . . . . . . . .
9.4 Conclusion . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
60
60
61
66
66
67
67
69
72
75
77
81
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
82
82
83
83
83
89
90
90
91
98
100
.
.
.
.
.
.
.
.
.
.
.
10 Optimalité de recherche dynamique
10.1 Définition . . . . . . . . . . . . . .
10.2 Intérêt . . . . . . . . . . . . . . . .
10.3 Structure . . . . . . . . . . . . . .
10.4 Conclusion . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
102
. 102
. 102
. 103
. 108
11 Expérimentations
11.1 Jeux de tests . . . . . .
11.2 Distribution aléatoire . .
11.3 Distribution working set
11.4 Conclusion . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3
.
.
.
.
.
.
.
.
.
.
.
.
109
109
110
111
112
12 Conclusion
13 Annexe
13.1 Implémentation . . . . . .
13.1.1 Multi-splay trees .
13.1.2 Splay trees . . . . .
13.1.3 Arbres rouges-noirs
13.2 Jeux de tests . . . . . . .
114
.
.
.
.
.
.
.
.
.
.
4
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
116
116
116
137
148
158
Chapitre 1
Introduction
Lorsque nous recherchons un élément dans un ensemble, nous pouvons
effectuer deux types de recherche : une recherche linéaire et une recherche dichotomique. La recherche linéaire consiste à examiner un à un les éléments de
l’ensemble, ce qui ne donne pas des performances intéressantes. La recherche
dichotomique consiste à éliminer à chaque étape la moitié des éléments de
l’ensemble. Cette méthode donne des résultats intéressants, car la recherche
se fait en temps logarithmique et non plus en temps linéaire.
Les arbres binaires de recherche sont une application de la recherche dichotomique. Ce type de structure organise les éléments sous forme d’arbre
pour tenter d’éliminer à chaque étape de la recherche le plus d’éléments
possibles (dans le meilleur des cas la moitié des éléments sont éliminés) de
manière à localiser le plus rapidement possible un élément.
Depuis que ce type de structure a été développé dans les années 1960, il
y a eu énormément de travaux réalisés sur le sujet. En général, le but de ces
travaux était d’essayer de développer une structure qui atteigne l’optimalité
dynamique, c’est-à-dire qui donne le meilleur coût de recherche et cela quelle
que soit la séquence d’éléments à rechercher.
Ce mémoire consiste en un travail de recherche bibliographique et le
but est de présenter les principaux travaux réalisés dans le domaine des
arbres binaires de recherche et de l’optimalité dynamique, ainsi que les principales étapes franchies dans ces domaines. Cela va en général consister à
présenter des structures et à analyser leurs performances. La présentation de
ces résultats se fera de manière plus détaillée et plus didactique que dans
les articles dans lesquels ils sont présentés. Le second objectif de ce mémoire
est d’implémenter quelques structures de données pour vérifier si en pratique
nous obtenons des résultats aussi intéressants qu’en théorie. En effet, il se
peut que pour des structures trop complexes les résultats obtenus en pratique
soient moins bons que ceux auxquels nous aurions pu nous attendre à avoir
5
par la théorie. Ceci s’explique notamment par le fait que dans les analyses
des coûts d’une structure, nous omettrons les constantes multiplicatives et
les termes d’ordre inférieur.
Dans les chapitres 2 et 3, nous allons définir un certain nombre de notions. En fait, dans le chapitre 2, nous allons définir des notions de base
dans le domaine des arbres binaires de recherche et dans le chapitre 3, nous
allons définir des propriétés permettant de caractériser le coût d’exécution
d’une séquence d’accès dans un arbre binaire de recherche. Parmi ces propriétés, nous pouvons notamment citer l’optimalité statique et l’optimalité
dynamique.
Dans le chapitre 4, nous allons étudier les arbres binaires de recherche
optimaux. Cette structure se base sur une connaissance préalable de la distribution des clés à accéder pour atteindre l’optimalité statique.
Dans les chapitres 5 et 6, nous allons présenter deux catégories d’arbres
binaires de recherche : les arbres binaires de recherche équilibrés et les arbres
binaires de recherche auto-ajustables. Chacune de ces catégories sera illustrée
par une structure de données : les arbres rouges-noirs pour la première et les
splay trees pour la seconde. Nous détaillerons les algorithmes de ces structures
et nous analyserons leurs performances.
Dans le chapitre 7, nous présenterons trois bornes inférieures sur le coût
d’exécution d’une séquence d’accès dans un arbre binaire de recherche. Une
de ces trois bornes est la borne inférieure d’entrelacement. Celle-ci est à la
base de la structure Tango étudiée dans le chapitre 8. Cette structure présente
un grand intérêt, parce qu’elle approche l’optimalité dynamique à un facteur
log(log n) près.
Dans le chapitre 9, nous étudierons la structure des multi-splay trees,
qui approche également l’optimalité dynamique à un facteur log(log n)
près. Cette structure présente comme principal intérêt d’être plus facile à
implémenter que Tango.
À l’heure actuelle, aucune structure de données n’atteint l’optimalité dynamique, mais nous présenterons dans le chapitre 10 un cas particulier où
cette propriété est atteinte. Nous montrerons dans ce chapitre, que nous pouvons atteindre l’optimalité dynamique en changeant le modèle de coût et en
permettant à la structure d’effectuer un nombre quelconque d’opérations de
réorganisation entre chaque accès sans que cela soit comptabilisé dans le coût
d’exécution.
Pour analyser les performances en pratique des structures étudiées, nous
en avons implémenté certaines pour y exécuter des jeux de tests. Dans le
chapitre 11, nous fournirons les résultats des jeux de tests exécutés, ainsi que
les conclusions que nous pouvons en tirer. L’implémentation des structures,
accompagnée d’explications, est fournie en annexe.
6
Première partie
Fondements et définitions
7
Chapitre 2
Arbres binaires et complexité
Dans ce chapitre, nous allons définir toute une série de notions de base
dans le domaine des arbres binaires de recherche et dans le domaine de la
complexité. Ces rappels seront utiles au lecteur de manière à poser ces notions, qui seront sans cesse utilisées.
2.1
Arbres binaires
Dans cette section, nous allons donner la définition d’un arbre binaire,
ainsi que de la terminologie associée à ce type de structure.
2.1.1
Définitions
Cette section est basée sur les notes d’un cours dispensé par Olivier Markowitch [Mar02].
Un arbre est une structure de données permettant de stocker un ensemble
d’éléments et il consiste en une collection de nœuds et d’arêtes (on utilisera
aussi le terme arc pour désigner une arête). Un nœud permet de stocker
un élément de cet ensemble et une arête (arc) est un lien entre deux nœuds.
L’organisation de l’arbre est telle que deux nœuds quelconques de l’arbre sont
reliés entre eux par un chemin unique. Un chemin est une suite de nœuds
distincts dans laquelle deux nœuds successifs sont reliés par une arête. Un
nœud peut posséder des fils. Un nœud y est le fils d’une nœud x si y se situe
en-dessous de x dans l’arbre et s’il est relié à ce nœud par une arête. Un
nœud x possède également un père ; il s’agit du nœud ayant x comme fils.
Les nœuds qui n’ont pas de fils sont appelés des feuilles. Le nœud au sommet
de l’arbre est appelé la racine et il ne possède pas de père.
8
Un arbre binaire est un arbre, où chaque nœud possède au plus deux fils
appelés fils gauche et fils droit.
Les ancêtres d’un nœud x sont les nœuds qui appartiennent au chemin
reliant la racine de l’arbre à x.
Les descendants d’un nœud x sont les nœuds qui ont x comme ancêtre.
Le sous-arbre d’un nœud x dans un arbre A est la partie de A contenant
x, ainsi que ses descendants. Le sous-arbre gauche d’un nœud x est le sousarbre du fils gauche de x. Le sous-arbre droit d’un nœud x est le sous-arbre
du fils droit de x. Un sous-arbre est vide s’il ne contient pas d’éléments ; par
exemple si x n’a pas de fils droit, alors le sous-arbre droit de x est vide.
2.1.2
Profondeur d’un nœud
Cette section est basée sur les notes d’un cours dispensé par Olivier Markowitch [Mar02].
La profondeur d’un nœud x, notée d(x), est le nombre d’arcs sur le chemin
allant de la racine de l’arbre au nœud x plus 1. De manière plus formelle :
1. d(x) = 1
si x est la racine de l’arbre
2. d(x) = 1 + d(parent)
sinon
où parent est le nœud père de x.
2.1.3
Hauteur d’un nœud
Cette section est basée sur les notes d’un cours dispensé par Olivier Markowitch [Mar02].
La hauteur d’un nœud x, notée h(x), est le nombre d’arcs sur le chemin
allant de x à la feuille la plus profonde du sous-arbre de x. De manière plus
formelle :
1. h(x) = 0
si x est feuille
2. h(x) = 1 + max{h(f g), h(f d)}
sinon
où fg et fd sont, respectivement, les fils gauche et droit de x.
2.1.4
Rotations
Il existe trois types de rotations possibles sur les arbres binaires :
1. une rotation simple
2. une rotation double
3. une rotation zig-zig
9
Fig. 2.1 – Rotation simple entre x et y .
2.1.4.1 Rotation simple
Cette opération a été décrite pour la première fois dans l’article
d’Adel’son-Vel’skii et Landis [AVL62].
Une rotation simple entre un nœud x et un nœud y (avec d(x) > d(y),
où d(x) est la profondeur de x et d(y) est la profondeur de y) consiste à
permuter la profondeur de x et de y comme décrit à la figure 2.1.
Une rotation simple est dite droite si x est le fils gauche de y et elle dite
gauche si x est le fils droit de y. La rotation à la figure 2.1 correspond à une
rotation droite. La rotation gauche est déduite de manière symétrique par
rapport à la rotation droite.
2.1.4.2 Rotation double
Une rotation double entre un nœud x, un nœud y et un nœud z (avec
d(x) > d(y) > d(z)) consiste à effectuer une rotation simple entre x et y,
puis une rotation simple entre x et z (cfr figure 2.2).
Pour pouvoir effectuer une rotation double, il faut que x soit un fils droit
(respectivement fils gauche) et y soit un fils gauche (respectivement fils droit).
10
Fig. 2.2 – Rotation double entre x, y et z.
2.1.4.3 Rotation zig-zig
Cette opération a été décrite pour la première fois dans l’article de Sleator
et Tarjan [ST85].
Une rotation zig-zig entre un nœud x, un nœud y et un nœud z (avec
d(x) > d(y) > d(z)) consiste à permuter la profondeur de x et de z comme
décrit à la figure 2.3.
Pour pouvoir effectuer une rotation zig-zig, il faut que x et y soient tous
deux des fils gauches ou des fils droits.
2.2
Arbres binaires de recherche
Le point principal dans cette section consiste à définir le modèle des
arbres binaires de recherche, qui sera étudié. Il faut noter, qu’il existe plusieurs modèles d’arbres binaires de recherche. En général, ces variantes sont
équivalentes (à un facteur constant près) en terme de performances au modèle
décrit ci-dessous.
11
Fig. 2.3 – Rotation zig-zig entre x, y et z.
2.2.1
Modèle
Nous considérons le modèle défini par Demaine, Harmon, Iacono et Patrascu [DHIP04]. Ils ont défini ce modèle en se basant sur celui de Wilber
[Wil89], qui correspond également à celui utilisé par Sleator et Tarjan [ST85].
Un ABR(Arbre Binaire de Recherche) est un arbre binaire qui maintient
un ensemble de clés selon une règle précise : pour chaque nœud x de l’arbre,
la clé de x est plus grande que toutes les clés contenues dans le sous-arbre
gauche de x et elle est plus petite que toutes les clés contenues dans le sousarbre droit de x.
Dans cette définition, nous considérons qu’un ABR ne supporte qu’une
seule opération : la recherche d’un élément. Cette recherche se fait sur un ensemble statique de n clés. Nous considérons seulement des recherches réussies,
appelées accès. Nous supposons également, sans nuire à la généralité, que
chaque clé d’un ABR prend une valeur unique dans l’ensemble {1, 2, . . . , n}.
Un ABR est utilisé pour servir une séquence d’accès, qui consiste en une
suite de clés X = x1 , x2 , . . . , xm . L’algorithme d’accès de l’ABR permet, en
fait, de servir chaque accès xi de cette séquence. Pour accéder à un nœud xi ,
l’algorithme d’accès ne peut utiliser qu’un seul pointeur P durant le parcours
de l’arbre. Au début de l’accès, ce pointeur est initialisé à la racine de l’arbre.
12
L’algorithme d’accès peut, ensuite, réaliser les opérations de coût unitaire
suivantes :
1. déplacer le pointeur P vers son fils gauche
2. déplacer le pointeur P vers son fils droit
3. déplacer le pointeur P vers son père
4. effectuer une rotation simple entre le pointeur P et son père
L’algorithme d’accès réalise une séquence des opérations ci-dessus jusqu’à
ce que le pointeur P atteigne le nœud xi .
Le coût d’accès au nœud xi est le nombre d’opérations de coût unitaire
réalisées pour l’atteindre. Dès lors, le coût
!m exigé par un ABR pour exécuter
une séquence X = x1 , x2 , . . . , xm vaut i=1 cout(xi ).
Nous désignerons ce modèle par le terme modèle dynamique.
Comme autre modèle pour les arbres binaires de recherche, nous pouvons
citer le modèle standard. Ce modèle est défini dans les articles [DSW05] et
[BCK03] et il sera abordé dans le chapitre 10.
2.2.2
Classes d’arbres binaires de recherche
Il existe deux grandes classes d’ABR, basées sur la manière de choisir
l’opération de coût unitaire suivante à réaliser lors d’un accès xi :
1. les ABR online
2. les ABR offline
2.2.2.1 ABR online
Un ABR online [DHIP04] est une structure de données ABR dans laquelle
chaque nœud peut contenir des données supplémentaires. Lors de chaque
opération de coût unitaire, les données contenues dans le nœud pointé par
le pointeur P peuvent être modifiées. Les données contenues dans les nœuds
permettent, en général, de garder de l’information sur les clés déjà accédées,
de manière à améliorer le coût d’accès aux clés devant encore être accédées.
Lors d’un accès, l’algorithme d’accès choisit l’opération de coût unitaire suivante à réaliser en fonction de la clé à accéder et des informations
supplémentaires contenues dans le nœud pointé par le pointeur P .
La quantité d’informations supplémentaires contenues dans un nœud doit
être la plus petite possible de manière à avoir une incidence négligeable sur le
temps d’exécution du programme. En effet, si cette quantité est trop grande,
la mise à jour de ces informations après une opération risque de coûter cher.
13
Comme exemples d’ABR online, nous pouvons citer : les arbres rougesnoirs (qui nécessitent un bit de couleur par nœud), les arbres AVL (qui
nécessitent un compteur par nœud) et les splay trees (qui ne nécessitent
pas d’informations supplémentaires).
2.2.2.2 ABR offline
Un ABR offline est une structure de données ABR, qui a une connaissance
préalable de la séquence X à exécuter. En fait, il s’agit d’une structure utopique, qui avant même de servir X, connaı̂t déjà les nœuds qui devront être
accédés dans cette séquence. De par cette connaissance, une telle structure
n’a pas besoin de garder des informations supplémentaires dans ses nœuds.
Lors d’un accès xi , le choix de l’opération de coût unitaire suivante à
réaliser est basé sur la connaissance de toute la séquence X. La différence
entre un ABR online et un ABR offline est que, pour choisir l’opération de
coût unitaire suivante à réaliser lors d’un accès xi , ce dernier peut se baser
sur les accès qui suivent xi dans la séquence X.
2.3
Performances asymptotiques
Cette section est basée sur l’ouvrage de Flajolet et Sedgewick [FS98].
Pour caractériser l’efficacité d’un algorithme, nous étudions ses performances asymptotiques. Ceci consiste à étudier la façon dont varie le temps
d’exécution d’un algorithme quand la taille de l’entrée tend vers l’infini.
Plusieurs raisons justifient l’usage des méthodes asymptotiques :
1. les performances asymptotiques d’un algorithme sont plus faciles à calculer que son temps d’exécution exact.
2. calculer le temps d’exécution exact d’un algorithme est en général inutile. En effet, pour des entrées suffisamment grandes, les effets des
constantes multiplicatives et des termes d’ordre inférieur dans le temps
d’exécution exact sont négligeables.
3. les méthodes asymptotiques fournissent de bons critères de comparaison entre les algorithmes. En effet, un algorithme, qui est asymptotiquement meilleur qu’un autre, est plus efficace que ce dernier pour des
entrées suffisamment longues.
Ci-dessous suivent trois notations asymptotiques permettant de décrire
le temps d’exécution asymptotique d’un algorithme.
14
2.3.1
Notation O(.)
Soit N la taille de l’entrée d’un algorithme. Nous disons que le temps
d’exécution T (N) d’un algorithme est en O(g(N)) (dit grand o de g de N )
s’il existe des constantes positives c1 et n0 telles que :
∀n ≥ n0 , 0 ≤ T (n) ≤ c1 .g(n)
(2.1)
Nous constatons, que pour n suffisamment grand, T (n) est borné
supérieurement par c1 · g(n). Intuitivement, la notation O(.) sert à majorer la fonction T (N).
2.3.2
Notation Ω(.)
Soit N la taille de l’entrée d’un algorithme. Nous disons que le temps
d’exécution T (N) d’un algorithme est en Ω(g(N)) (dit grand omega de g de
N ) s’il existe des constantes positives c1 et n0 telles que :
∀n ≥ n0 , 0 ≤ c1 .g(n) ≤ T (n)
(2.2)
Nous constatons, que pour n suffisamment grand, T (n) est borné
inférieurement par c1 · g(n). Intuitivement, la notation Ω(.) sert à minorer la
fonction T (N).
2.3.3
Notation Θ(.)
Soit N la taille de l’entrée d’un algorithme. Nous disons que le temps
d’exécution T (N) d’un algorithme est en Θ(g(N)) (dit grand theta de g de
N ) s’il existe des constantes positives c1 , c2 et n0 telles que :
∀n ≥ n0 , 0 ≤ c1 .g(n) ≤ T (n) ≤ c2 .g(n)
(2.3)
Nous constatons, que pour n suffisamment grand, T (n) est borné
inférieurement par c1 · g(n) et supérieurement par c2 · g(n). Intuitivement,
la notation Θ(.) fournit une valeur approchée de la fonction T (N).
2.3.4
Lien entre les notations asymptotiques
Une fois les trois définitions ci-dessus présentées, il est assez aisé d’établir
un lien entre elles. Ce lien est explicité par le théorème suivant.
Théorème 1. Pour deux fonctions quelconques g(n) et f(n), nous avons que
f(n) = Θ(g(n)) si et seulement si f(n) = O(g(n)) et f(n) = Ω(g(n)).
15
Ce théorème peut facilement être démontré en prouvant d’abord l’implication du théorème dans un sens, puis en prouvant son implication dans
l’autre sens et cela en se basant à chaque fois sur la définition de O(.), Θ(.)
et Ω(.)
2.4
Complexité
Cette section est basée sur l’ouvrage de Jean Cardinal [Car04] et l’article
de Tarjan [Tar85].
Il existe principalement trois manières d’analyser la complexité d’une
structure :
1. effectuer une analyse au pire cas
2. effectuer une analyse amortie
3. effectuer une analyse du cas moyen
Dans cette section, nous allons définir ces trois analyses et développer de
manière plus précise l’analyse amortie.
2.4.1
Complexité au pire cas
L’analyse au pire cas consiste à caractériser la complexité d’une opération
dans le pire des cas. Ce pire cas peut se produire de manière répétée ou non.
Si ce pire cas peut se produire de manière répétée, la complexité au pire cas
donne une bonne indication des performances en pratique de la structure.
Par contre, si ce pire cas ne peut pas se produire souvent, elle ne donne
pas une indication concrète sur les performances en pratique. Ceci peut être
illustré par l’exemple suivant : la complexité au pire cas de l’algorithme de
Knuth-Morris-Pratt (pour la recherche de motifs) est bien meilleure que la
complexité au pire cas de l’algorithme de Boyer-Moore, alors qu’en pratique
ce deuxième algorithme est plus efficace que le premier.
2.4.2
Complexité moyenne
L’analyse de la complexité moyenne est en général délicate. La complexité moyenne est calculée sur la base d’une distribution de probabilités
sur l’ensemble des entrées de l’algorithme, pour laquelle des hypothèses ont
été posées. La source de l’aléatoire vient alors des entrées. Cette complexité
donne des informations assez significatives concernant les performances en
pratique si les pire cas sont rares.
16
Il existe une seconde manière de calculer une complexité moyenne, dans
laquelle la source de l’aléatoire ne vient pas des entrées, mais de l’algorithme
qui fait des choix aléatoires. La complexité est calculée sur ces choix et on
parle alors de complexité moyenne randomisée.
2.4.3
Complexité amortie
2.4.3.1 Définition
La complexité amortie est la complexité au pire cas d’une séquence de
M opérations divisée par M. La complexité amortie donne des informations
significatives concernant les performances en pratique d’une structure, tout
en se basant sur les performances au pire cas. Cette analyse apparaı̂t, donc,
comme un bon intermédiaire entre la complexité moyenne et la complexité au
pire cas. Elle permet, mieux que les deux analyses précédentes, de concevoir
des structures efficaces en pratique.
Il existe plusieurs méthodes pour effectuer une analyse amortie d’une
structure.
La méthode qui sera développée dans le point suivant est la méthode du
potentiel.
2.4.3.2 Méthode du potentiel
La méthode du potentiel nécessite de définir une fonction potentiel. Cette
fonction peut être comparée à un compte bancaire. Celui-ci ne peut pas
descendre en dessous d’une certaine valeur (par exemple 0). Lorsqu’un achat
nécessite un coût moindre que prévu, l’argent restant est remis dans le compte
et celui-ci augmente. Par contre, lorsque le coût d’un achat est supérieur à
celui prévu, il faut aller puiser dans le compte pour payer le reste et celui-ci
diminue donc. Cette analogie permet de donner une première idée de ce qu’est
une fonction potentiel. Une définition plus précise est donnée ci-dessous.
La fonction potentiel est une valeur qui est bornée inférieurement par
une constante. Cette borne dépend de la structure, mais vaut en général 0.
La fonction potentiel va augmenter, lorsque le temps réel pour effectuer une
opération est inférieur à son temps amorti. Et elle va diminuer, si le temps réel
pour effectuer une opération est supérieur à son temps amorti. En d’autres
termes, lorsque le coût effectif d’une opération est inférieur à son coût amorti,
la fonction potentiel sauvegarde la différence entre ces deux coûts, et lorsque
le coût d’une opération est supérieur à son coût amorti, la fonction potentiel
puise dans sa réserve pour compenser ce surcoût.
17
Pour prouver une complexité amortie CA , il faut établir l’équation suivante, qui utilise une fonction potentiel P :
CA = CE + $P,
(2.4)
où CE est la complexité effective de l’opération et $P est la variation de
la fonction potentiel entre le début et la fin de l’opération.
Pour établir l’équation ci-dessus, il faut prouver, qu’à la ième (∀ 1 ≤ i ≤
M) opération d’une séquence de M opérations, l’égalité suivante est vérifiée :
CA = CE (i) + $P (i),
(2.5)
CA = CE (i) + P (i) − P (i − 1)
(2.6)
où CE (i) est le coût effectif de la ième opération et $P (i) est la variation
de potentiel à la ième étape, c’est-à-dire la différence entre le potentiel après
la ième étape (= P (i)) et le potentiel avant la ième étape (= P (i − 1)). Cette
équation peut être réécrite de la manière suivante :
2.5
Conclusion
Dans ce chapitre, nous avons d’abord défini la notion d’arbre binaire,
ainsi que la terminologie associée à ce type de structure. Ensuite, nous avons
défini le modèle dynamique des arbres binaires de recherche. Et finalement,
nous avons présenté plusieurs outils (les performances asymptotiques et les
différents types de complexité) permettant de caractériser les performances
d’une structure ABR et permettant de comparer les performances de telles
structures.
18
Chapitre 3
Propriétés des arbres binaires
de recherche
Dans ce chapitre, nous allons présenter plusieurs propriétés caractérisant
le coût d’exécution d’une séquence d’accès dans des ABR. Ces propriétés sont
également valables pour certaines structures qui ne sont pas des ABR.
3.1
Relations entre les propriétés
Nous allons décrire dans ce chapitre six propriétés :
1. Static Finger
2. Optimalité Statique
3. Working Set
4. Dynamic Finger
5. Unifiée
6. Optimalité Dynamique
La figure 3.1 illustre la relation d’implication entre ces propriétés. La
propriété static finger est la plus faible de toutes, car elle est une conséquence
de toutes les autres propriétés. De plus, les propriétés working set et dynamic
finger sont indépendantes.
Les définitions des propriétés ci-dessus sont basées sur deux articles de
Iacono [Iac01] et [Iac05].
3.2
Static finger
Considérons un ensemble de n clés {1, 2, . . . , n}, une séquence d’accès
X = x1 , x2 , . . . , xm et un nœud spécifique f , appelé le finger, appartenant à
19
Fig. 3.1 – Relation d’implication entre les propriétés.
l’ensemble {1, 2, . . . , n}. Une structure a la propriété static finger si le coût
total pour exécuter la séquence X vaut :
$
" m
#
log (1 + |xi − f |)
(3.1)
O
i=1
L’expression |xi −f | est la distance dans l’ordre symétrique entre le finger
f et le nœud xi . En d’autres termes, la distance |xi − f | est le nombre
d’éléments situés entre xi et f dans la séquence triée des nœuds plus 1. Une
fois fixé, le finger reste le même durant toute l’exécution de la séquence X.
Les Finger Search Trees [BLM+ 03] possèdent cette propriété, mais cette
structure ne répond pas à la définition du modèle dynamique des ABR (cfr
point 2.2.1), car son opération de recherche peut débuter à partir d’une feuille
quelconque de l’arbre. Les splay trees [ST85] possèdent également cette propriété.
De l’équation (3.1), nous constatons que plus la distance entre le finger f
et le nœud xi est petite, plus le coût sera petit et inversement.
Par conséquent, pour une structure ayant la propriété static finger, le
coût total pour exécuter une séquence X sera déterminé par la distance dans
l’ordre symétrique entre le finger f et chaque nœud de la séquence X à
exécuter.
20
3.3
Optimalité statique
Considérons un ensemble de n clés {1, 2, . . . , n} et une séquence d’accès
X = x1 , x2 , . . . , xm . Une structure a la propriété d’optimalité statique, si elle
exécute X avec un coût de :
" n
%
&$
#
m
f (i) · log
O
,
(3.2)
f
(i)
i=1
où f (i) est le nombre d’accès à i dans la séquence X.
Supposons que nous ayons une variable aléatoire discrète qui puisse
prendre n valeurs ( {1, 2, . . . , n} ), l’entropie de cette variable est la quantité :
H =−
n
#
i=1
pi · log (pi ) =
n
#
i=1
% &
1
pi · log
,
pi
où pi est la probabilité que la variable aléatoire soit égale à i.
Prenons l’équation (3.2) et réécrivons-la de la manière suivante :
"
%
&$
n
#
f (i)
m
O m·
· log
m
f (i)
i=1
En posant pi = f (i)/m , nous obtenons :
"
% &$
n
#
1
pi · log
= O(m · H)
O m·
p
i
i=1
(3.3)
(3.4)
(3.5)
Une structure, qui atteint l’optimalité statique, exécute donc la séquence
X avec un coût égal à O(m · H). De plus, elle exécute un accès avec un coût
amorti O(H).
Les Arbres Binaires de Recherche Optimaux [Knu73] et les splay trees
[ST85] sont deux structures ABR ayant cette propriété d’optimalité statique.
Ces deux structures feront l’objet d’explications plus détaillées, respectivement, dans les chapitres 4 et 6.
3.4
Working set
Considérons un ensemble de n clés {1, 2, . . . , n} et une séquence d’accès
X = x1 , x2 , . . . , xm .
Soit l(i, x) égal à j si xj est le dernier accès à x dans la séquence x1 , x2 , . . . ,
xi−1 . Si x n’est pas accédé dans cette séquence, alors l(i, x) vaut 1. Nous
définissons w(i, x) comme étant le nombre d’éléments distincts accédés dans
21
la séquence xl(i,x)+1 , xl(i,x)+2 , . . . , xi . Dès lors, w(i, xi ) est le nombre d’éléments
distincts accédés dans la séquence xl(i,xi )+1 , xl(i,xi )+2 , . . . , xi . Il s’agit donc du
nombre d’éléments distincts accédés entre l’accès xi et le précédent accès à
cet élément. Notons w(i, xi ) par w(xi ).
Une structure a la propriété working set, si elle exécute une séquence
d’accès X avec un coût :
$
" m
#
log (w (xi ))
(3.6)
O
i=1
De cette propriété, nous déduisons que plus il y a d’éléments différents
accédés entre deux accès successifs d’un nœud xi , plus le coût d’accès à ce
nœud xi sera élevé et inversement.
De plus, nous constatons que, si les accès d’une séquence X ne se font
que sur un sous-ensemble de k éléments parmi les n, la complexité d’un accès
à une clé xi de cette séquence sera d’au plus O(log k), car w(xi ) ne pourra
jamais être supérieur à k.
Par conséquent, pour une structure ayant la propriété working set, le coût
total pour exécuter une séquence X dépend de deux critères :
1. le nombre de clés différentes dans la séquence X.
2. le nombre d’éléments différents accédés entre deux accès successifs
d’une même clé dans la séquence X.
Plus la première quantité sera petite, plus elle aura d’importance dans le
coût d’exécution de la séquence X.
En fait, cette propriété est basée sur la localité temporelle : plus la localité
temporelle entre deux accès successifs d’une même clé est petite (c’est-à-dire
que le temps écoulé entre ces deux accès est petit), plus le coût d’accès à ce
nœud est petit et inversement.
La Working Set Data Structure proposée par Iacono [Iac01] atteint un
pire cas de O(log(w(xi ))) pour un accès xi . Cependant, cette structure ne
répond pas à la définition du modèle dynamique des ABR, car elle utilise des
pointeurs et des files additionnels. Les splay trees [ST85] possèdent également
cette propriété.
3.5
Dynamic finger
Considérons un ensemble de n clés {1, 2, . . . , n} et une séquence d’accès
X = x1 , x2 , . . . , xm . Une structure a la propriété dynamic finger si le coût
total pour exécuter la séquence X vaut :
22
O
"m−1
#
i=1
log (1 + |xi+1 − xi |)
$
(3.7)
Cette propriété est analogue à la propriété static finger. La différence
étant que pour cette dernière, le finger f est fixe, tandis que pour la propriété
dynamic finger, le finger est le dernier élément accédé.
Les Level-Linked Trees [BT80] possèdent cette propriété, mais cette structure ne répond pas à la définition du modèle dynamique des ABR, car elle
utilise des pointeurs additionnels. Les level-linked trees atteignent un pire cas
de O(log (1 + |xi+1 − xi |)) pour un accès. Les splay trees [ST85] possèdent
également la propriété dynamic finger.
De l’équation (3.7), nous déduisons que l’accès à un nœud est d’autant
plus rapide que la distance entre l’élément auquel on doit accéder et le dernier
élément auquel on a accédé est petite.
En fait, cette propriété est basée sur la localité spatiale : plus la distance
entre deux accès adjacents est petite, plus le coût d’accès au nœud est petit
et inversement.
Par conséquent, pour une structure ayant la propriété dynamic finger,
le coût total pour exécuter une séquence X sera déterminé par la distance
entre les accès adjacents de la séquence. Plus la distance entre les accès
adjacents de la séquence sera grande, plus le coût pour exécuter X sera
grand et inversement.
3.6
Unifiée
Considérons un ensemble de n clés {1, 2, . . . , n} et une séquence d’accès
X = x1 , x2 , . . . , xm . Une structure a la propriété unifiée si elle exécute X
avec un coût :
$
" m
#
min (log (w(i, j) + |xi − j| + 1))
(3.8)
O
i=1
j∈X
L’Unified Structure [Iac01] possède cette propriété, mais elle ne répond
pas à la définition du modèle dynamique des ABR. Les splay trees [ST85] sont
conjecturés avoir la propriété unifiée. À l’heure actuelle, il n’existe aucune
structure ABR qui atteigne cette borne.
Une structure ayant cette propriété exécutera une séquence avec un coût
faible si les accès sont proches en terme de distance à des éléments accédés
récemment et inversement. Cette propriété est donc basée sur la localité
temporelle et la localité spatiale des accès.
23
La propriété unifiée implique la propriété working set et la propriété dynamic finger. L’intérêt de la propriété unifiée est d’unifier les propriétés working
set et dynamic finger ; ces deux propriétés étant indépendantes, car aucune
des deux n’implique l’autre.
3.7
Optimalité dynamique
Considérons une séquence quelconque d’accès X = x1 , x2 , . . . , xm . Certains ABR peuvent l’exécuter de manière optimale. Notons par OP T (X) ce
coût minimal pour exécuter X. Ce coût OP T (X) est le coût de l’ABR offline
le plus rapide qui puisse exécuter X. En effet, dans le modèle ABR offline,
il n’y a aucune contrainte dans l’algorithme d’accès sur la manière de choisir
la prochaine opération de coût unitaire à réaliser. Et en particulier, ce choix
peut dépendre des futurs accès à servir dans la séquence.
Une structure de données ABR a la propriété d’optimalité dynamique, si
elle exécute une quelconque séquence d’accès X avec un coût O(OP T (X)).
On dit qu’une structure de données A est α − compétitive (ou a un taux
de compétitivité de α) si pour toute séquence X :
COUTA (X) ≤ α · OP T (X),
(3.9)
où COUTA (X) est le coût nécessaire à la structure A pour exécuter X.
La conjecture d’optimalité dynamique, énoncée par Sleator et Tarjan
[ST85], affirme que les splay trees sont O(1) − compétitif s. Cependant, aucune démonstration n’est venue étayer cette conjecture.
Jusqu’il y a peu, le meilleur taux de compétitivité était le taux trivial de
O(log(n)), atteint par exemple par les arbres rouges-noirs. Ce n’est qu’en
2004, que ce taux a pu être amélioré par une structure de données appelée Tango [DHIP04]. Cette structure atteint un taux de compétitivité de
O(log(log n)). Par après, une autre structure de données, les Multi-Splay
Trees ([SW04] et [DSW06]), a été développée sur base de Tango pour atteindre également un taux de compétitivité de O(log(log n)). Ces deux structures seront détaillées dans les chapitres 8 et 9.
La propriété d’optimalité dynamique a une signification profonde :
connaı̂tre le futur ne permet d’améliorer le temps d’exécution d’une séquence
d’accès que d’un facteur constant. En effet, si nous parvenons à développer
un ABR online, qui atteint l’optimalité dynamique, cela signifierait que nous
sommes parvenus à créer une structure, qui en se basant sur le passé de la
séquence d’accès X à exécuter, donne des résultats équivalents, à un facteur
constant près, à la meilleure structure de données offline, qui se base sur la
24
connaissance du futur pour exécuter de manière optimale la séquence d’accès
X.
3.8
Conclusion
Dans ce chapitre, nous avons défini toute une série de propriétés permettant de caractériser le coût d’exécution d’une séquence d’accès dans un ABR.
La propriété la plus forte étant bien évidemment l’optimalité dynamique.
De plus, nous avons présenté la relation d’implication entre ces propriétés
de manière à spécifier celles qui sont les plus fortes et celles qui sont les plus
faibles.
25
Deuxième partie
Arbres binaires de recherche
particuliers
26
Chapitre 4
Arbres Binaires de Recherche
Optimaux
La rédaction de ce chapitre est basée sur l’ouvrage de Knuth [Knu73].
La structure des ABRO (= Arbres Binaires de Recherche Optimaux) a
été développée par Knuth [Knu73]. Le but de cette structure est d’atteindre
l’optimalité statique et elle nécessite pour cela de connaı̂tre la distribution
des clés à accéder.
Pour rappel, une structure a la propriété d’optimalité statique (cfr point
3.3), si elle exécute une séquence d’accès X = x1 , x2 , . . . , xm sur un arbre de
n clés {1, 2, . . . , n} avec un coût de :
" n
%
&$
#
m
f (i) · log
O
,
(4.1)
f (i)
i=1
où f (i) est le nombre d’accès à i dans la séquence X.
4.1
Généralités
Pour construire l’ABRO d’un ensemble de nœuds P = {p1 , p2 , ... , pn },
nous disposons, en plus de ces nœuds, de leur fréquence relative d’accès. Pour
tenir compte des recherches infructueuses, nous avons également besoin d’un
ensemble de nœuds externes Q = {q0 , q1 , q2 , ... , qn }. Ces nœuds externes
représentent les pointeurs nuls des nœuds de l’arbre. Chaque nœud de Q
représente un intervalle de nœuds pour lesquels la recherche est infructueuse :
q0 représente l’intervalle (−∞, p1 ), qi représente l’intervalle (pi , pi+1 ) (∀ 0 <
i < n) et qn représente l’intervalle (pn , ∞). Les nœuds de Q sont accompagnés
de leur fréquence relative d’accès.
27
Fig. 4.1 – Arbre dont le coût de recherche est de 2.2
Le coût d’une recherche dans l’arbre T construit sur les clés de P est
donné par la quantité :
c(T ) =
"
#
1≤j≤n
f (pj ) · prof ondeur(pj )
$
+
"
#
0≤j≤n
f (qj ) · (prof ondeur(qj ) − 1) ,
où f (pj ) est la fréquence relative d’accès au nœud pj et où f (qj ) est la
fréquence relative d’accès au nœud externe qj , c’est-à-dire la probabilité que
la clé recherchée appartienne à l’intervalle (qj , qj+1 ). La notion de profondeur
est définie au point 2.1.2.
Considérons l’arbre T de la figure 4.1 et l’assignation de fréquences suivante : f (p1 ) = 0.15, f (p2 ) = 0.1, f (p3 ) = 0.25, f (q0 ) = 0.05, f (q1 ) = 0.2,
f (q2 ) = 0.15, f (q3 ) = 0.1. Le coût d’une recherche dans l’arbre T est le
suivant :
c(T ) = (0.15) · 3 + (0.1) · 2 + (0.25) · 1 + (0.05) · 3
+(0.2) · 3 + (0.15) · 2 + (0.1) · 1
= 2.2
Pour déterminer l’ABRO de P , il suffit de trouver l’arbre T , dont le coût
de recherche c(T ) est le minimum parmi les coûts de recherche de tous les
arbres possibles. À noter qu’il peut exister plusieurs arbres, dont le coût de
recherche est minimal.
28
$
4.2
Algorithme exhaustif
Pour déterminer l’ABRO de l’ensemble P de nœuds, il suffit de construire
tous les arbres possibles sur P , de calculer leur coût de recherche et de retenir
celui dont le coût est minimal. Cependant, la complexité d’un tel algorithme
est exponentielle. En effet, avec n nœuds il est possible de construire Cn+1
(= le (n + 1)ème nombre de Catalan) arbres binaires de recherche. Or,
'2n(
n
Cn+1 =
(n + 1)
4n
∼
π 1/2 · n3/2
Clairement un tel algorithme pour construire l’ABRO est à exclure.
4.3
Algorithme de Knuth
L’algorithme de Knuth est un algorithme de programmation dynamique.
Il se base sur la propriété suivante : tous les sous-arbres d’un ABRO sont
optimaux. En effet, une amélioration du coût de recherche d’un quelconque
sous-arbre d’un ABRO mène à une amélioration du coût de recherche de
l’ABRO.
L’idée est de considérer le problème de manière récursive (cfr figure 4.2).
Un ABRO T est constitué d’une racine pk , d’un sous-arbre gauche (qui est
un ABRO) et d’un sous-arbre droit (qui est un ABRO). On construit de
manière récursive le sous-arbre gauche et le sous-arbre droit de pk avant de
construire T .
Pour construire l’ABRO avec l’algorithme de programmation dynamique,
nous avons besoin de trois tables :
1. la table C, où C(i, j) (∀ 0 ≤ i ≤ j ≤ n) est le coût de l’ABRO sur les
nœuds {pi+1 , ... , pj , qi , ... , qj }.
2. la table r, où r(i, j) (∀ 0 ≤ i ≤ j ≤ n) est l’indice k de la racine pk de
l’ABRO sur les nœuds {pi+1 , ... , pj , qi , ... , qj }.
3. la table W , où W (i, j) = f (pi+1 ) + . . . + f (pj ) + f (qi ) + . . . + f (qj )
(∀ 0 ≤ i ≤ j ≤ n).
Le remplissage de la table C commence à la ligne 0 et se termine à la
ligne n. Il est basé sur la formule suivante :
29
Fig. 4.2 – ABRO d’un point de vue récursif
C(i, j) =
)
0
si i ≥ j
W (i, j) + min (C(i, k − 1) + C(k, j)) si i < j
(4.2)
i<k≤j
L’idée de cette formule est de déterminer pour chaque indice k le coût de
l’arbre construit avec pk comme racine, un ABRO de coût C(i, k − 1) comme
sous-arbre gauche et un ABRO de coût C(k, j) comme sous-arbre droit ; il
faut retenir celui qui minimise la quantité C(i, k − 1) + C(k, j). Une fois k
déterminé (si plusieurs indices donnent un coût minimal, il faut en choisir un
arbitrairement), il faut stocker cet indice dans r(i, j). Il faut noter que dans
le calcul de C(i, j), il faut rajouter le terme W (i, j). En effet, lorsque le sousarbre gauche de coût C(i, k − 1) et le sous-arbre droit de coût C(k, j) sont
rattachés à la racine pk , la profondeur de chacun des nœuds dans ces sousarbres augmente de 1. Ceci implique qu’il faut rajouter dans le coût C(i, j)
la fréquence de chacun de ces nœuds. Comme il faut également rajouter la
fréquence de pk , cela revient finalement à rajouter W (i, j) au coût C(i, j).
Il est possible d’optimiser la formule 4.2 en se basant sur l’inégalité suivante démontrée par Knuth dans [Knu73] :
r(i, j − 1) ≤ r(i, j) ≤ r(i + 1, j)
30
(4.3)
La formule devient alors :
C(i, j) =
)
0
W (i, j) +
si i ≥ j
(C(i, k − 1) + C(k, j)) si i < j
min
r(i,j−1)≤r(i,j)≤r(i+1,j)
(4.4)
La construction de l’ABRO en utilisant la formule 4.4 nécessite un coût en
O(n2 ) (si n est la taille de l’arbre), alors que la construction avec la formule
4.2 nécessite un coût en O(n3 ) [Knu73].
De plus, le coût d’une recherche dans un tel arbre se fait en O(H) [Knu73]
(où H est l’entropie).
4.4
Conclusion
Dans cette section, nous avons présenté un algorithme de programmation dynamique, qui construit avec un coût en O(n2) une structure qui atteint l’optimalité dynamique. Cette structure est statique et elle requière la
connaissance de la fréquence des nœuds.
En 1979, Bitner [Bit79] a présenté et analysé les Arbres Monotones Dynamiques. Cette structure est un ABR dynamique (un ABR qui permet les
insertions et les suppressions), qui essaie d’approximer l’ABRO.
31
Chapitre 5
Arbres binaires de recherche
équilibrés
Dans ce chapitre, nous allons d’abord définir la notion d’arbre binaire
de recherche équilibré. Ensuite, nous allons illustrer ce concept par la
présentation des arbres rouges-noirs. Le choix de cette structure plutôt qu’une
autre se justifie par le fait qu’elle fait partie des plus connues et des plus efficaces de sa catégorie.
5.1
Définition
Un arbre binaire de recherche équilibré est un ABR, qui maintient des
informations supplémentaires dans ses nœuds, de manière à ce qu’il reste
équilibré. Le but de cet équilibrage est qu’à tout moment, la hauteur de
l’arbre soit logarithmique, à un facteur constant près, en le nombre d’éléments
dans l’arbre. Ceci permet d’avoir des opérations sur la structure en temps
logarithmique en le nombre d’éléments dans l’arbre.
5.2
Arbres rouges-noirs
Dans la littérature, il existe deux manières de présenter les arbres rougesnoirs : soit de les décrire comme une implémentation des arbres 2-3-4 (une
description de cette structure-ci est présentée dans l’ouvrage [Sed04]), soit de
les décrire comme une structure vérifiant un certain nombre de conditions,
qui permettent d’équilibrer l’arbre. Les deux méthodes sont bien évidemment
équivalentes. La première méthode est plus intéressante, car elle permet de
comprendre le sens des propriétés imposées aux arbres rouges-noirs. Elle
permet, de plus, de comprendre aisément les algorithmes d’insertion et de
32
suppression des arbres rouges-noirs, ainsi que leur preuve de correction. La
deuxième méthode permet de présenter la structure de manière plus brève.
Nous utiliserons la deuxième méthode pour présenter la structure. Cette description est basée sur les ouvrages de Jean Cardinal [Car04] et de Sedgewick
[Sed04].
5.2.1
Définition
Les arbres rouges-noirs ont deux types d’arcs : les arcs rouges et les
arcs noirs. Un seul bit par nœud x est nécessaire comme information
supplémentaire. Ce bit indique si l’arc pointant vers ce nœud x est rouge
ou noir. Un arbre, pour répondre à la définition d’arbres rouges-noirs, doit
vérifier les propriétés suivantes :
1. dans un chemin de la racine à une feuille, il n’y a jamais deux arcs
rouges successifs
2. le nombre d’arcs noirs dans un chemin de la racine à un sous-arbre vide
est le même quel que soit le chemin
3. le bit de couleur de la racine est noir
Ces conditions permettent d’équilibrer l’arbre, comme il le sera démontré
plus loin dans ce chapitre, de manière à ce que les opérations sur les arbres
rouges-noirs se fassent en temps logarithmique au pire cas en le nombre
d’éléments dans l’arbre.
5.2.2
Recherche
L’algorithme de recherche pour les arbres rouges-noirs est identique à
l’algorithme d’accès des ABR défini pour le modèle dynamique (cfr point
2.2.1).
5.2.3
Insertion
L’algorithme d’insertion pour les arbres rouges-noirs est le suivant.
En commençant le traitement à la racine, il faut effectuer les opérations
suivantes :
1. Désignons par x le nœud courant. Si x a deux arcs sortants rouges, il
faut traiter ce nœud de la manière suivante :
(a) colorier les deux arcs sortants de x en noir
(b) colorier l’arc liant x à p (où p est le père de x) en rouge
33
(c) si l’arc liant p à g (où g est le père de p ) est rouge, il faut effectuer
une rotation. Cette rotation est simple (rotation entre p et g) si x
et p sont tous deux des fils gauches ou s’ils sont tous deux des fils
droits ; elle est double sinon.
Après cet éventuel traitement, il faut déplacer le pointeur courant sur
le fils gauche ou le fils droit de x suivant que la clé à insérer soit,
respectivement, plus petite ou plus grande que x.
Cette première étape est à itérer tant que le nœud courant ne pointe
pas sur un sous-arbre vide.
2. À ce niveau, le pointeur courant pointe sur un sous-arbre vide. Il ne
suffit, dès lors, qu’à insérer le nouveau nœud dans ce sous-arbre vide
en coloriant son bit de couleur en rouge. Après cela, si l’arc liant p (où
p est le père du nœud inséré) à g (où g est le père de p) est rouge, il
faut effectuer une rotation. Cette rotation est simple (rotation entre p
et g) si le nœud inséré et p sont tous deux des fils gauches ou s’ils sont
tous deux des fils droits ; elle est double sinon. Les notions de rotation
simple et de rotation double sont définies au point 2.1.4.
Certains lecteurs pourraient être sceptiques quant à la correction de cet
algorithme, mais si nous considérons les arbres rouges-noirs comme une
implémentation des arbres 2-3-4, alors cette preuve peut facilement être
établie. Les utilisateurs intéressés par cela peuvent consulter la référence
[Car04] ou [Sed04].
5.2.4
Successeur
L’opération successeur pour un nœud x donne l’élément, qui suit x dans
l’ordre symétrique. L’algorithme suivant permet de déterminer le successeur
de x.
Supposons que le pointeur courant pointe sur le nœud x (si ce n’est pas
le cas, il suffit de réaliser une recherche sur cet élément). Ensuite, deux cas
sont à envisager, selon que x ait un fils droit ou non :
1. Si x a un fils droit, noté y , il faut se positionner sur le fils droit de x
et suivre le chemin des fils gauches à partir de y . Le successeur de x
est alors le dernier nœud rencontré sur ce chemin.
2. Si x n’a pas de fils droit, il faut remonter dans l’arbre, tant que x
appartient au sous-arbre droit du nœud courant. Le successeur de x
est, alors, le premier nœud dans cette remontée, qui contient x dans
son sous-arbre gauche. S’il n’existe pas de tel nœud, alors x n’a pas de
successeur.
34
5.2.5
Prédécesseur
L’opération prédécesseur pour un nœud x donne l’élément, qui précède
x dans l’ordre symétrique. L’algorithme suivant permet de déterminer le
prédécesseur de x.
Supposons que le pointeur courant pointe sur le nœud x (si ce n’est pas
le cas, il suffit de réaliser une recherche sur cet élément). Ensuite, deux cas
sont à envisager, selon que x ait un fils gauche ou non :
1. Si x a un fils gauche, noté y , il faut se positionner sur le fils gauche de
x et suivre le chemin des fils droits à partir de y . Le prédécesseur de
x est alors le dernier nœud rencontré sur ce chemin.
2. Si x n’a pas de fils gauche, il faut remonter dans l’arbre, tant que x
appartient au sous-arbre gauche du nœud courant. Le prédécesseur de
x est, alors, le premier nœud dans cette remontée, qui contient x dans
son sous-arbre droit. S’il n’existe pas de tel nœud, alors x n’a pas de
prédécesseur.
5.2.6
Analyse des performances
Les deux démonstrations présentées dans cette partie sont basées sur
l’ouvrage de Sedgewick [Sed04]
Lemme 1. Soient un arbre rouge-noir et un nœud x appartenant à cet arbre.
Le sous-arbre A de x contient au moins 2bh(x) −1 nœuds (bh(x) est la hauteur
noire de x, c’est-à-dire le nombre de nœuds ayant un bit noir entre x (inclus)
et la feuille la plus profonde de x moins 1. On impose que bh(x) ≥ 0).
Démonstration : La démonstration de ce lemme se fait par récurrence sur
la hauteur de x :
1. Initialisation de la récurrence : Si la hauteur de x vaut 0, alors bh(x)
vaut 0. Dès lors, le sous-arbre A de x doit contenir au moins 2bh(x) −1 =
20 − 1 = 0 nœud. Or, si x a une hauteur de 0, cela signifie que le sousarbre A consiste en la seule feuille x. Le sous-arbre A contient, donc,
un élément et le lemme est, alors, vérifié.
2. Pas de récurrence : Supposons que le lemme soit vrai pour une hauteur
h − 1 et montrons qu’il l’est aussi pour une hauteur h.
Supposons que le nœud x ait une hauteur h et une hauteur noire bh(x).
Considérons le pire cas, où x a deux enfants ; en effet, on augmente
ainsi le nombre d’éléments dans le sous-arbre de x. Chaque enfant de
x a une hauteur noire qui vaut bh(x) ou bh(x) − 1, selon que son bit de
35
couleur soit respectivement rouge ou noir. Étant donné que la hauteur
de chacun de ces enfants vaut h − 1, on peut appliquer l’hypothèse de
récurrence sur ces nœuds, et ainsi déduire que le sous-arbre de chacun
de ces éléments contient au moins 2bh(x)−1 − 1 nœuds. Finalement, le
sous-arbre de x doit contenir au minimum (2bh(x)−1 − 1) + (2bh(x)−1 −
1) + 1 = 2bh(x) − 1 nœuds, ce qui prouve bien le lemme.
!
Théorème 2. Un arbre rouge-noir T ayant n nœuds a une hauteur au plus
égale à 2 · lg(n + 1) + 2.
Démonstration : Supposons que la hauteur de l’arbre rouge-noir T vaut
h. Étant donné que dans un chemin allant de la racine à un sous-arbre vide il
ne peut y avoir deux arcs rouges successifs et que le bit de la racine est noir,
il doit y avoir au moins h/2 nœuds ayant un bit noir sur ce chemin. Dès lors,
bh(y) (où y est la racine de T ) vaut au moins h/2 − 1. Par le lemme 1, nous
déduisons que T contient au moins 2bh(y) − 1 = 2h/2−1 − 1 nœuds. Sachant
que T contient exactement n nœuds, nous en déduisons l’inégalité suivante :
n ≥ 2h/2−1 − 1
⇔ n + 1 ≥ 2h/2−1
En prenant le logarithme des deux membres, nous obtenons :
h
−1
2
h
⇔ log(n + 1) + 1 ≥
2
⇔ 2 · log(n + 1) + 2 ≥ h
log(n + 1) ≥
!
Ainsi, par cette propriété, nous constatons que la hauteur d’un arbre
rouge-noir à n nœuds vaut O(log n), ce qui implique que les opérations de
recherche, d’insertion, de successeur et de prédécesseur se font en O(log n)
au pire cas.
5.3
Conclusion
Dans ce chapitre, nous avons détaillé la structure des arbres rouges-noirs
et analysé ses performances. Celle-ci fournit des performances intéressantes
36
pour une opération : un coût au pire cas de O(log n). Cependant, cette structure n’atteint pas l’optimalité dynamique, car elle est O(log n)−compétitive.
Un inconvénient de cette structure est qu’elle n’est pas capable de s’adapter à la séquence d’accès à traiter. Si par exemple une séquence d’accès
consiste à accéder plusieurs fois à un même nœud et que celui-ci se trouve
au bas de l’arbre, la structure effectuera chacun des accès à ce nœud avec un
coût au pire cas de O(log n), alors qu’en le remontant à la racine, elle aurait
pu améliorer les accès à ce nœud.
37
Chapitre 6
Arbres binaires de recherche
auto-ajustables
Dans ce chapitre, nous allons d’abord définir la notion d’arbre binaire
de recherche auto-ajustable. Ensuite, nous allons illustrer ce concept par la
présentation des splay trees. Et nous terminerons par une comparaison entre
les arbres rouges-noirs et les splay trees.
6.1
Définition
Un arbre binaire de recherche auto-ajustable est un ABR sur lequel nous
appliquons une règle après chaque opération dans le but d’équilibrer l’arbre.
En général, le but de cette règle n’est pas d’avoir, à tout moment, une hauteur
pour l’arbre qui soit logarithmique en le nombre d’éléments. Par contre, nous
nous attendons à ce que, pour une séquence suffisamment longue, la règle
équilibre l’arbre de manière à avoir une complexité amortie (cfr point 2.4.3)
logarithmique en la taille de l’arbre.
6.2
Splay trees
Cette partie contient une présentation des splay trees avec ses principales
opérations et l’analyse de ses performances. Le choix de cette structure autoajustable, plutôt qu’une autre, se justifie par le fait que cette structure est
sans conteste l’une des plus intéressantes et l’une des plus impressionnantes,
qui ait jamais été développée. Elle possède un éventail de propriétés très
large, bien qu’elle soit définie sur une idée assez simple.
La présentation de cette structure est basée sur l’ouvrage de Jean Cardinal
[Car04] et sur l’article de Sleator et Tarjan [ST85].
38
6.2.1
Splaying
L’opération de splaying sur un nœud x consiste à le remonter à la racine
par une série de rotations. Trois types de rotations peuvent être employés :
1. la rotation simple (cfr point 2.1.4) : nous l’appliquons si le nœud à
splayer n’a pas de grand-père.
2. la rotation zig-zag : nous l’appliquons si le nœud à splayer est un fils
gauche (respectivement fils droit) et que son père est un fils droit (respectivement fils gauche). Cette rotation est en fait une rotation double
(cfr point 2.1.4).
3. la rotation zig-zig (cfr point 2.1.4) : nous l’appliquons si le nœud à
splayer est un fils gauche (respectivement fils droit) et que son père est
un fils gauche (respectivement fils droit).
6.2.2
Recherche
L’algorithme de recherche dans un splay tree est identique à l’algorithme
d’accès des ABR défini pour le modèle dynamique (cfr point 2.2.1), hormis
qu’après avoir atteint le nœud désiré, il faut effectuer un splaying sur ce
nœud.
6.2.3
Insertion
L’algorithme d’insertion pour les splay trees est le suivant.
En commençant le traitement à la racine, il faut effectuer les opérations
suivantes :
1. Désignons par x le nœud courant. Il faut déplacer le pointeur courant
sur le fils gauche ou le fils droit de x suivant que la clé à insérer soit,
respectivement, plus petite ou plus grande que x.
Cette première étape est à itérer tant que le nœud courant ne pointe
pas sur un sous-arbre vide.
2. À ce niveau, le pointeur courant pointe sur un sous-arbre vide. Il ne
suffit, dès lors, qu’à insérer le nouveau nœud dans ce sous-arbre vide
et à effectuer un splaying sur ce nœud.
6.2.4
Suppression
L’algorithme de suppression pour les splay trees est le suivant :
1. Rechercher l’élément à supprimer en utilisant l’algorithme de recherche.
39
2. Lorsque l’élément est repéré, il faut le splayer. Par ce splaying, cet
élément devient la racine de l’arbre.
3. Enlever la racine de l’arbre. Cette suppression a pour conséquence de
créer deux sous-arbres Tg et Td (Tg est le sous-arbre gauche de la racine
enlevée et Td est son sous-arbre droit).
4. Rechercher le plus grand élément de Tg . Pour cela, il faut partir de la
racine de l’arbre et suivre le chemin des fils droits. Le dernier nœud
rencontré sur ce chemin est l’élément maximal de Tg .
5. Réaliser un splaying du plus grand élément de Tg . Cet élément devient
alors la racine de Tg et a un sous-arbre droit vide, car il est le plus
grand élément de Tg .
6. Fusionner Tg et Td en accrochant Td comme sous-arbre droit de la racine
de Tg .
6.2.5
Analyse des performances
L’analyse des performances des splay trees se fera par une analyse amortie. Cette analyse nécessite donc d’introduire une fonction potentiel (cfr point
2.4.3). Le choix de cette fonction est crucial, car de cette fonction dépendent
les résultats que nous obtiendrons. Si nous utilisons une mauvaise fonction
potentiel, nous ne parviendrons pas à démontrer les résultats escomptés.
La fonction potentiel utilisée dans cette analyse est celle introduite par
Sleator et Tarjan [ST85]. Elle est décrite dans le paragraphe ci-dessous.
Supposons que chaque nœud x d’un arbre T ait un poids w $ (x) ; il s’agit
d’une valeur arbitraire positive, mais fixée. La taille s(x) d’un nœud x est la
somme des poids de tous les nœuds appartenant au sous-arbre de x (w $ (x)
est inclus dans cette somme). Le rang r(x) d’un nœud x est le logarithme en
base 2 de sa taille. Finalement, le potentiel d’un arbre T est la somme des
rangs de tous ses nœuds. En résumé :
w $(x) ∈ R+ , ∀x ∈ T
!
s(x) = j∈Tx w $ (j) , où Tx est le sous-arbre de x.
r(x) = log(s(x)) , par log nous entendons logarithme en base 2.
!
P (T ) = x∈T r(x)
En se basant sur cette fonction potentiel, nous pouvons démontrer le
lemme suivant, qui est la propriété de base permettant de caractériser les
performances amorties des splay trees. Nous nous limiterons pour chaque
40
propriété à l’idée de la démonstration.
6.2.5.1 Access Lemma
Le coût amorti nécessaire pour effectuer un splaying sur un nœud x dans
un arbre de racine t est d’au plus 3(r(t) - r(x)) + 1.
La démonstration de ce lemme est décrite, notamment, dans l’ouvrage
de Weiss [Wei99]. L’idée de la démonstration, est de calculer le coût d’une
rotation simple, d’une rotation zig-zig et d’une rotation zig-zag en se basant
sur la fonction potentiel définie ci-dessus et utilisant l’équation fondamentale
pour définir une complexité amortie (CA = CE + $P , cfr point 2.4.3). Le
splaying étant une séquence de rotations simple, zig-zig et zig-zag, son coût
peut alors être calculé à partir du coût de ces rotations.
En partant de cette propriété, il est alors possible de démontrer toute
une série de propriétés sur les splay trees en assignant des poids aux nœuds
de l’arbre. Les propriétés, ainsi déduites, sont présentées ci-dessous.
6.2.5.2 Théorème d’équilibre
Dans un arbre de taille n, le coût total de m opérations vaut O(m · log n).
La complexité amortie d’une opération vaut alors O(log n).
Pour démontrer ce théorème, il suffit d’utiliser l’Access Lemma et d’assigner un poids de 1/n à chaque nœud de l’arbre. Le théorème d’équilibre est,
donc, atteint en imposant une distribution uniforme sur les clés.
Nous constatons, donc, que si la distribution sur les clés est uniforme,
les splay trees ont un coût de recherche amorti égal au coût de recherche au
pire cas d’arbres équilibrés comme les arbres AVL et les arbres rouges-noirs
et cela sans utiliser la moindre information de rééquilibrage.
6.2.5.3 Théorème d’optimalité statique
La notion d’optimalité statique est définie au point 3.3 et est rappelée
ci-dessous.
Soient un arbre de taille n et une séquence d’accès X = x1 , x2 , . . . , xm .
Le temps d’accès total de cette séquence de m opérations vaut :
" n
&$
%
#
m
O
f (i) · log
,
(6.1)
f
(i)
i=1
41
où f (i) est la fréquence d’accès du nœud i. Dès lors, f (i)/m est sa fréquence
relative.
Pour démontrer ce théorème, il suffit d’utiliser l’Access Lemma et d’assigner à chaque nœud sa fréquence relative. En d’autres termes, pour un nœud
xi , son poids w $(xi ) vaut f (xi )/m.
Les ABRO [Knu73] (cfr chapitre 4) atteignent la propriété d’optimalité
statique, mais ils requièrent pour cela de connaı̂tre préalablement la fréquence
de chaque nœud. Les splay trees atteignent l’optimalité statique sans cette
connaissance préalable de la fréquence de chaque nœud
.
6.2.5.4 Théorème du static finger
La notion de static finger est définie au point 3.2 et est rappelée ci-dessous.
Soient un arbre de taille n, un nœud spécifique f appelé le finger et une
séquence d’accès X = x1 , x2 , . . . , xm . Le coût pour traiter cette séquence de
m accès vaut :
$
" m
#
log(1 + |xi − f |) ,
(6.2)
O
i=1
où |xi − f | est la distance dans l’ordre symétrique entre le finger f et le
nœud xi . Donc, |xi − f | est le nombre d’éléments situés entre xi et f dans
la séquence triée des nœuds de l’arbre.
Pour démontrer ce théorème, il suffit d’utiliser l’Access Lemma et d’assigner à chaque nœud xi de l’arbre un poids w $(xi ) égal à (1+|x1i −f |)2 .
Il est important de noter que le finger f est un nœud quelconque de
l’arbre, mais une fois choisi, ce finger doit rester le même durant toute
l’exécution de la séquence d’accès. Ce qui est le plus remarquable est que
les splay trees atteignent la propriété static finger pour un quelconque finger
possible. En effet, il suffit d’utiliser ce finger dans l’assignation des poids
aux nœuds.
6.2.5.5 Théorème du working set
La notion de working set est définie au point 3.4 et est rappelée ci-dessous.
Soient un arbre de taille n et une séquence d’accès X = x1 , x2 , . . . , xm .
Le coût pour traiter cette séquence de m accès vaut :
" m
$
#
O
log(w(xi )) ,
(6.3)
i=1
42
où w(xi ) est le nombre d’éléments distincts entre l’accès xi et le précédent
accès à cet élément.
L’assignation des poids pour ce théorème est plus compliquée, car il faut
modifier le poids des nœuds durant l’exécution de la séquence.
6.2.5.6 Théorème du dynamic finger
La notion de dynamic finger est définie au point 3.5 et est rappelée cidessous.
Soient un arbre de taille n et une séquence X = x1 , x2 , . . . , xm . Le coût
pour traiter cette séquence de m accès vaut :
"m−1
$
#
O
log (|xj+1 − xj | + 1)
(6.4)
j=1
En 1985, époque de la publication de l’article de Sleator et Tarjan [ST85]
, ce théorème n’avait pas pu être démontré. Ce n’est qu’en 1995, que Cole
[Col00] y est parvenu, mais la démonstration de ce théorème reste très complexe.
Le splay tree est la seule structure ayant cette propriété et répondant
à la définition du modèle dynamique des ABR (cfr point 2.2.1), mais la
démonstration de cette propriété est complexe. Un problème intéressant
(proposé par Stefan Langerman) serait d’élaborer une structure relativement
simple et ayant la propriété dynamic finger avec une démonstration de cette
propriété plus simple que celle des splay trees.
6.2.5.7 Théorème du scanning
Soient un arbre de taille n et une séquence d’accès X = 1, 2, . . . , n. Le
coût pour traiter ces n accès vaut :
O(n)
(6.5)
Ce théorème est en fait un corollaire du théorème dynamic finger. En
effet, comme les nœuds sont accédés par ordre croissant sur les clés, nous
avons que |xj+1 − xj | = 1 (∀ j ∈ [1, n − 1]). Et par conséquent, la complexité
pour traiter X vaut O(n).
43
6.2.5.8 Conjecture d’unifiée
La notion d’unifiée est définie au point 3.6 et est rappelée ci-dessous.
Soient un arbre de taille n et une séquence d’accès X = x1 , x2 , . . . , xm .
Le coût pour traiter cette séquence de m accès vaut :
" m
$
#
O
min (log (w(i, j) + |xi − j| + 1))
(6.6)
i=1
j∈X
Cette propriété n’a pas encore pu être démontrée pour les splay trees et
elle reste donc une conjecture.
6.2.5.9 Conjecture d’optimalité dynamique
La notion d’optimalité dynamique est définie au point 3.7 et est rappelée
ci-dessous.
Soient un arbre de taille n et une séquence d’accès X. Le coût pour
exécuter cette séquence vaut :
O(OP T (X))
(6.7)
Si cette conjecture est démontrée, cela signifierait que les splay trees
peuvent exécuter n’importe quelle séquence d’accès avec un coût optimal
et cela, par une simple règle de réarrangement et sans garder la moindre
information supplémentaire. Ce serait comme si les splay trees étaient capables de prévoir la séquence à exécuter, de manière à pouvoir s’y adapter
et l’exécuter de manière optimale.
Cette propriété n’est malheureusement pas encore démontrée pour les
splay trees. Et à l’heure actuelle, cette structure est l’une des rares à encore
pouvoir prétendre atteindre l’optimalité dynamique.
6.3
Comparaisons
Nous allons comparer les arbres rouges-noirs et les splay trees. Ces comparaisons sont en général valables pour les ABR équilibrés et les ABR autoajustables.
Pour le temps d’exécution au pire cas, les arbres rouges-noirs présentent
des performances plus intéressantes que les splay trees. Les splay trees fournissent des performances intéressantes sur de longues séquences d’accès, alors
44
que les arbres rouges-noirs ont pour but de fournir des résultats intéressants
sur une seule opération. L’inconvénient des splay trees est que les opérations
de réorganisation peuvent être nombreuses, et ainsi coûter cher. Quant aux
arbres rouges-noirs, les opérations de rééquilibrage restent limitées, et ne
nuisent donc pas aux performances.
Au niveau de l’espace mémoire, les arbres rouges-noirs nécessitent de stocker un bit de couleur, alors que les splay trees n’ont pas besoin d’informations supplémentaires. Les arbres rouges-noirs nécessitent donc plus d’espace
mémoire, mais ce surplus reste limité (n bits pour un arbre de taille n).
En ce qui concerne les propriétés atteintes par les structures, nous avons
que les splay trees ont un éventail très large de propriétés, ce qui n’est pas
du tout le cas pour les arbres rouges-noirs. Nous remarquons, notamment
grâce à ces propriétés, que les splay trees ont la capacité de s’adapter à une
séquence d’accès. Ceci est dû au fait que les splay trees gardent près de
la racine les nœuds fréquemment accédés. Les arbres rouges-noirs n’ont pas
cette capacité de s’adapter à une séquence d’accès, ce qui peut influer sur le
temps d’exécution de la séquence. Supposons par exemple, qu’une séquence
d’accès consiste à accéder plusieurs fois à un même nœud x situé dans le bas
de l’arbre. Les arbres rouges-noirs effectueront chacun des accès à x avec un
coût O(log n), alors que pour les splay trees, le premier accès à x se fera avec
un coût égal à la profondeur de x et les accès suivants à x se feront en temps
constant.
6.4
Conclusion
Si la distribution sur les clés est uniforme (théorème d’équilibre), la recherche pour les splay trees se fait avec une complexité amortie en O(log n).
Mais pour une distribution qui n’est pas uniforme (théorème d’optimalité
statique, théorème du static finger et théorème du working set), les résultats
obtenus pour la recherche sont meilleurs que celui obtenu pour la distribution uniforme. On peut comprendre cela de manière intuitive. Si la distribution n’est pas uniforme, certains éléments seront accédés plus souvent que
d’autres. Les éléments souvent accédés auront tendance à se situer en haut de
l’arbre, alors que les éléments moins fréquemment accédés se situeront dans
le bas de l’arbre. Et comme les éléments du haut sont plus souvent accédés,
on aura plus d’accès à faible coût, ce qui aura pour conséquence d’améliorer
le coût total de l’exécution de la séquence d’accès.
45
Troisième partie
Optimalité dynamique
46
Chapitre 7
Bornes inférieures sur le coût
des arbres binaires de recherche
Ce chapitre est consacré au problème de borner inférieurement le coût
d’exécution d’une séquence d’accès par un ABR. L’intérêt d’avoir de telles
bornes est double. D’une part, il est possible de développer de nouvelles
structures à partir d’une borne inférieure (cela a par exemple été le cas pour la
structure Tango que nous étudierons dans le chapitre 8). D’autre part, il serait
possible de prouver l’optimalité dynamique grâce à une borne inférieure. En
effet, si nous parvenons à démontrer que, pour un ABR, le coût d’exécution
d’une quelconque séquence d’accès est égal à la borne inférieure, alors nous
en déduirons immédiatement que la structure est optimale dynamiquement.
Dans ce chapitre, nous allons présenter trois bornes inférieures sur le
temps d’exécution d’une séquence d’accès : La Borne Inférieure de Couverture de Rectangle, La Borne Inférieure d’Entrelacement et La Seconde Borne
de Wilber. Les deux dernières bornes seront en fait dérivées de la première.
Ce chapitre est basé sur l’article de Derryberry, Sleator et Wang [DSW05],
sur l’article de Wilber [Wil89] et sur l’article de Demaine, Harmon, Iacono
et Patrascu [DHIP04].
7.1
Modèle
Pour prouver la borne inférieure de couverture de rectangle, nous devons
passer au modèle standard. La différence entre ce modèle et le modèle dynamique (cfr point 2.2.1) est que le nœud à accéder doit d’abord être ramené
à la racine par une série de rotations. Ensuite, seulement, l’accès au nœud
se fait à la racine. Après l’accès au nœud, l’algorithme peut effectuer des
rotations pour améliorer le coût des futurs accès.
47
Le coût d’accès est défini comme le nombre de rotations plus 1. Comme
une rotation est inversible, le coût dans le modèle standard vaut au plus deux
fois celui dans le modèle dynamique.
Par conséquent, si nous parvenons à déterminer une borne inférieure sur
le coût d’exécution d’une séquence d’accès dans le modèle standard, alors il
suffit de prendre cette borne et de la diviser par deux pour obtenir une borne
dans le modèle dynamique.
7.2
La borne inférieure de couverture de rectangle
Considérons une séquence d’accès comme un ensemble de points dans
l’espace à deux dimensions, où la coordonnée x correspond au temps et
la coordonnée y correspond à l’espace des clés. Pour une séquence X =
x1 , x2 , . . . , xm , chaque accès xi correspond au point (i, xi ). Notons P l’ensemble des points générés à partir de X.
Une boı̂te est un rectangle, dont deux coins opposés sont des points de
P . Nous considérons deux types de boı̂tes : les boı̂tes hautes et les boı̂tes
basses. Une boı̂te est haute si ses coins inférieur gauche et supérieur droit
sont dans P . Et une boı̂te est basse si ses coins supérieur gauche et inférieur
droit sont dans P . Chaque boı̂te contient un diviseur, qui consiste en une
ligne horizontale traversant la boı̂te d’une extrémité à l’autre. L’ordonnée
du diviseur est choisie de manière arbitraire, mais elle doit cependant être
différente de l’ordonnée de tous les points de P et elle doit être comprise
entre l’ordonnée minimale et l’ordonnée maximale de la boı̂te.
Deux boı̂tes sont dites en conflit si elles sont toutes deux des boı̂tes hautes
ou des boı̂tes basses et si leur intersection contient une partie ou l’entièreté
des deux diviseurs.
Pour illustrer ces définitions, considérons le schéma de la figure 7.1 représentant les points correspondant à la séquence X =
3(x1 ), 5(x2 ), 1(x3 ), 7(x4 ), 10(x5 ), 2(x6 ). Trois boı̂tes sont illustrées sur ce
schéma (les diviseurs sont représentés en pointillés). Les boı̂tes A et B sont
hautes, tandis que la boı̂te C est basse. De plus, les boı̂tes A et B sont en
conflit, alors que les boı̂tes B et C ne le sont pas.
Avant d’énoncer et de démontrer la borne inférieure de couverture de rectangle, nous prouvons le lemme qui suit. Nous notons par LCA(x, y) l’ancêtre
commun le plus bas des nœuds x et y. Les démonstrations du lemme 2 et du
théorème qui suit ce lemme sont basées sur l’article [DSW05]. La contribution personnelle dans ces deux démonstrations a consisté à aller plus dans le
48
Fig. 7.1 – Représentation graphique de la séquence X = 3, 5, 1, 7, 10, 2.
49
détail en éclaircissant certains points de la démonstration.
Lemme 2. Soient a et b deux nœuds distincts dans un ABR T avec a < b.
Soient p et c deux nœuds de T avec p parent de c. Si une rotation est effectuée
sur les nœuds c et p et qu’elle implique que LCA(a, b) change, alors les quatre
propriétés suivantes sont vérifiées :
1. a ≤ c ≤ b
2. a ≤ p ≤ b
3. LCA(a, b) = p avant la rotation
4. LCA(a, b) = c après la rotation
Démonstration : Considérons une permutation Π des clés de T , où ces
clés sont triées par ordre croissant de profondeur. Les cas d’égalité pour des
nœuds ayant la même profondeur sont gérés arbitrairement. On impose une
contrainte : les nœuds p et c doivent être adjacents (p précède alors c, car
il a une profondeur plus petite que celle de c). Comme les nœuds sont triés
par ordre croissant de profondeur, l’arbre obtenu en insérant de manière
successive les clés de Π à partir d’un arbre vide est T . Notons que LCA(a, b)
est le nœud le moins profond ayant une valeur comprise entre a et b, car
tous les autres nœuds ayant une valeur comprise entre a et b appartiennent
au sous-arbre de LCA(a, b). Par conséquent, LCA(a, b) est le premier nœud
dans Π ayant une valeur comprise entre a et b.
Si nous permutons les nœuds p et c dans Π, l’arbre obtenu à partir de
cette séquence est celui que nous obtenons en effectuant une rotation sur c
et p dans T , et cela parce que la permutation de p et c dans Π implique que
le nœud c est inséré avant p.
La séquence Π est donc une autre manière de représenter une rotation
sur p et c, ainsi que LCA(a, b).
Supposons que nous réalisons une rotation sur p et c, et que cela ait
comme conséquence que LCA(a, b) change. Cela signifie, qu’au niveau de
Π, le fait de permuter p et c implique que le premier nœud dans Π ayant
une valeur entre a et b change. Nous en déduisons alors que les nœuds p
et c appartiennent à l’intervalle [a, b], car sinon la permutation de p et c
dans Π n’aurait pas modifié le premier élément de Π appartenant à [a, b]. De
plus, comme le premier élément de Π appartenant à [a, b] est modifié par la
permutation de p et c, nous en déduisons que LCA(a, b) était égal à p avant
la rotation et que LCA(a, b) vaut c après la rotation.
!
50
Théorème 3 (Théorème de la borne inférieure de couverture de
rectangle). Soient une séquence d’accès X = x1 , x2 , . . . , xm et un ensemble
P de points correspondant à X. Soit B un ensemble de boı̂tes pour P, qui prises
deux à deux, ne sont pas en conflit. Le nombre de rotations nécessaires à un
ABR (répondant à la définition du modèle standard) pour traiter X vaut au
moins |B|.
Démonstration : Nous allons associer à chaque boı̂te Bk de B une rotation
Rk réalisée par l’ABR pour traiter X. À deux boı̂tes ne sera pas associée
la même rotation, ce qui permettra de déduire que le nombre de rotations
effectuées par l’ABR pour traiter X vaut au moins le nombre de boı̂tes dans
B.
Considérons une boı̂te haute Bk . Disons que le point (i, xi ) est son coin
inférieur gauche et que le point (j, xj ) est son coin supérieur droit. Après
l’accès xi , nous avons que ce nœud devient la racine de l’arbre. De même,
après l’accès xj , nous avons que ce nœud-ci devient la racine de l’arbre. L’intervalle de temps couvert par la boı̂te vaut [i, j]. Au début de cet intervalle,
LCA(xi , xj ) vaut xi , car ce nœud est la racine de l’arbre et à la fin de l’intervalle, LCA(xi , xj ) vaut xj , car ce nœud-ci est la racine de l’arbre. Par
conséquent, durant l’intervalle [i, j], nous avons que LCA(xi , xj ) a dû passer
d’une valeur inférieure au diviseur de Bk à une valeur supérieure au diviseur de Bk . La rotation Rk associée à Bk est la première, qui implique que
LCA(xi , xj ) traverse le diviseur de Bk de bas en haut.
Par le lemme 2, si LCA(xi , xj ) change à cause d’une rotation, alors cette
rotation est appliquée sur deux éléments p et c appartenant à l’intervalle
[xi , xj ]. Les points correspondant à ces deux clés ont donc une ordonnée
comprise entre xi et xj . La rotation Rk est représentée par une ligne verticale
traversant le diviseur de la boı̂te Bk (cfr figure 7.2).
Une rotation Rk ne peut pas être associée à deux boı̂tes hautes différentes.
En effet, supposons que Rk soit associée à deux boı̂tes différentes Bl et Bk .
Dès lors, Rk doit traverser le diviseur de Bl et de Bk . L’intersection de Bl et
de Bk doit contenir Rk et par conséquent les diviseurs de Bl et de Bk . Or,
cela implique que Bl et Bk soient en conflit, ce qui est impossible car ces
deux boı̂tes appartiennent à B et ne sont par définition pas en conflit.
De manière analogue, nous pouvons montrer qu’une rotation Rk ne peut
pas être associée à deux boı̂tes basses différentes.
Une rotation Rk ne peut pas être associée à une boı̂te basse et à une boı̂te
haute. Nous prouvons ceci en montrant que la rotation associée à une boı̂te
haute est une rotation gauche et la rotation associée à une boı̂te basse est
une rotation droite. En effet, considérons la boı̂te haute Bk et la rotation Rk
51
Fig. 7.2 – Rotation Rk représentée par une flèche.
52
p
c
p
c
Fig. 7.3 – Avant la rotation on a que p est le parent de c et que p < c
qui y est associée et qui porte sur p et c (avec p parent de c). Comme cette
rotation entre p et c implique que LCA(xi , xj ) passe d’une valeur inférieure
au diviseur de Bk à une valeur supérieure au diviseur de Bk , alors nous avons
que p < c par le lemme 2, car LCA(xi , xj ) = p avant la rotation Rk et
LCA(xi , xj ) = c après cette rotation. Et comme p est le père de c, alors la
rotation entre p et c est une rotation gauche (cfr figure 7.3). Nous pouvons
montrer de manière analogue que la rotation associée à une boı̂te basse est
une rotation droite.
En conclusion, nous avons montré qu’une rotation ne peut pas être associée à deux boı̂tes différentes, ce qui prouve bien la borne inférieure de
couverture de rectangle.
!
7.3
Applications de la borne inférieure de
couverture de rectangle
Dans cette section, nous allons dériver deux bornes à partir de la borne
inférieure de couverture de rectangle :
1. la borne inférieure d’entrelacement. Cette borne a été développée par
Demaine, Harmon, Iacono et Patrascu [DHIP04].
2. la seconde borne de Wilber. Cette borne a été développée par Wilber
[Wil89]
Les démonstrations dans cette section sont basées sur l’article [DSW05].
La contribution personnelle a consisté à détailler les démonstrations en y
éclaircissant certains points.
53
7.3.1
La borne inférieure d’entrelacement
Soient un ensemble de n clés {1, 2, . . . , n} et une séquence d’accès X =
x1 , x2 , . . . , xm . La région gauche d’un nœud xi correspond à xi plus son sousarbre gauche et la région droite de xi correspond à son sous-arbre droit.
Nous définissons ci-dessous la borne inférieure d’entrelacement en expliquant la manière de la calculer.
Le calcul de la borne inférieure d’entrelacement nécessite de maintenir
un arbre parfait P sur les clés {1, 2, . . . , n}. Pour calculer l’entrelacement
IB(X, y) d’un nœud y , il faut étiqueter chaque accès xi de X par left si xi
appartient à la région gauche de y ou par right si xi appartient à la région
droite de y ; si xi n’appartient à aucune des deux régions, il faut l’exclure
de la séquence étiquetée. IB(X, y) correspond alors au nombre d’alternances
entre les labels left et right dans la séquence étiquetée créée.
La borne inférieure d’entrelacement IB(X) de la séquence X correspond
à la somme des entrelacements IB(X, y) de chaque nœud y dans P . Nous
avons donc :
#
IB(X) =
IB(X, y)
(7.1)
y∈P
Théorème 4 (Théorème de la borne inférieure d’entrelacement).
Soient un ensemble de n clés {1, 2, . . . , n} et une séquence d’accès X =
x1 , x2 , . . . , xm . Dans le modèle standard, nous avons la borne inférieure suivante sur le coût optimal OPT(X) de l’algorithme offline pour traiter X :
OP T (X) ≥ IB(X) + m
(7.2)
Démonstration : Pour prouver ce théorème, il faut montrer que nous
pouvons placer une boı̂te haute pour chaque alternance left-right et une boı̂te
basse pour chaque alternance right-left, de sorte que les boı̂tes prises deux à
deux ne sont pas en conflit. Comme les boı̂tes hautes et les boı̂tes basses ne
peuvent pas être en conflit, il suffit de démontrer que deux boı̂tes hautes ne
peuvent pas être en conflit et que deux boı̂tes basses ne peuvent pas être en
conflit.
Supposons que pour un nœud v, il y ait une alternance left-right dans la
séquence étiquetée au temps r. La boı̂te haute, correspondant à cette alternance, est construite de la manière suivante. Soit xl le dernier accès ayant
lieu dans le sous-arbre gauche de v strictement avant le temps r. Disons
que cet accès ait eu lieu au temps l (l < r). Et soit xr l’accès ayant provoqué l’alternance left-right au temps r. Nous créons la boı̂te correspondant
à cette alternance avec (l, xl ) comme coin inférieur gauche, (r, xr ) comme coin
54
supérieur droit et v − ε comme diviseur (avec ε très petit). Nous dénotons
cette boı̂te par le triplet ((l, xl ), (r, xr ), v − ε).
Considérons deux boı̂tes hautes : B = ((l, xl ), (r, xr ), v − ε) et B $ =
((l$ , x$l ), (r $, x$r ), v $ − ε). Quatre cas sont à envisager :
1. si v = v $ , alors les boı̂tes B et B $ ne peuvent pas se chevaucher, car les
intervalles [l, r] et [l$ , r $ ] sont soit disjoints, soit adjacents. Et donc, les
boı̂tes ne peuvent pas être en conflit.
2. si v n’est pas un ancêtre de v $ et si v $ n’est pas un ancêtre de v, alors l’espace des clés [xl , xr ] correspondant à l’espace des ordonnées de la boı̂te
B et l’espace de clés [x$l , x$r ] correspondant à l’espace des ordonnées de
la boı̂te B $ sont disjoints. Ces deux boı̂tes ne peuvent donc pas être en
conflit.
3. si v $ est un ancêtre de v et si v < v $ , alors v appartient au sous-arbre
gauche de v $ . Et donc, tous les éléments dans l’espace des clés [xl , xr ]
de B sont strictement plus petits que v $ . De plus, comme B est une
boı̂te haute, nous avons que xl < xr . Tout ceci nous permet de déduire
que xl < xr ≤ v $ − 1 < v $ − ε. Ainsi, B ne peut pas contenir le diviseur
de B $ et les boı̂tes B et B $ ne peuvent donc pas être en conflit.
4. si v $ est un ancêtre de v et si v > v $ , alors v appartient au sous-arbre
droit de v $ . Et donc, tous les éléments dans l’espace des clés [xl , xr ] de
B sont strictement plus grands que v $ . De plus, comme B est une boı̂te
haute, nous avons que xl < xr . Tout ceci nous permet de déduire que
xr > xl ≥ v $ + 1 > v $ − ε. Ainsi, B ne peut pas contenir le diviseur de
B $ et les boı̂tes B et B $ ne peuvent donc pas être en conflit.
Les cas où v est un ancêtre de v $ sont similaires au cas 3 et 4. Et les cas
où B et B $ sont des boı̂tes basses sont analogues aux quatre cas ci-dessus.
Nous avons donc prouvé que dans tous les cas possibles B et B $ ne peuvent
pas être en conflit.
Par conséquent, nous avons prouvé que IB(X) est une borne inférieure sur
le nombre de rotations nécessaires à un algorithme pour traiter une séquence
X = x1 , x2 , . . . , xm . Comme le coût d’un algorithme pour traiter la séquence
X vaut le nombre de rotations effectuées plus m (un coût de 1 est nécessaire
pour accéder au nœud ramené à la racine), alors nous pouvons rajouter m à
la borne inférieure IB(X). Finalement, nous avons que :
OP T (X) ≥ IB(X) + m
(7.3)
!
Corollaire 1. Soient un ensemble de n clés {1, 2, . . . , n} et une séquence
d’accès X = x1 , x2 , . . . , xm . Dans le modèle dynamique, nous avons la borne
55
inférieure suivante sur le coût optimal OPT(X) de l’algorithme offline pour
traiter X :
1
(7.4)
OP T (X) ≥ · (IB(X) + m)
2
7.3.2
La seconde borne de Wilber
Soient un ensemble de n clés {1, 2, . . . , n} et une séquence d’accès X =
x1 , x2 , . . . , xm .
De manière informelle, nous pouvons définir cette borne de la manière
suivante. Pour chaque accès xi de X, nous commençons la traversée de la
séquence à partir de xi−1 en reculant jusqu’à x1 . Nous gardons à chaque
étape une trace des deux accès les plus proches de xi ; l’un devant être plus
petit que xi et l’autre plus grand que xi . La seconde borne de Wilber pour
un accès xi est le nombre de fois qu’un record de proximité s’est produit
sur un côté différent du dernier record. La seconde borne de Wilber pour la
séquence X est égale à la somme des secondes bornes de Wilber de chaque
accès de X.
De manière formelle, pour déterminer la seconde borne de Wilber pour
l’accès xi , il faut trouver la sous-séquence de taille maximale constituée
d’accès traversants xc1 , . . . , xcK(i)+1 (K(i) est alors égal à la seconde borne
de Wilber pour l’accès xi ) (si nous prenons deux accès adjacents de cette
séquence, alors ils représentent une amélioration du record de proximité,
mais sur des côtés différents par rapport à xi ) et une sous-séquence d’accès
intérieurs xb1 , . . . , xbK(i) telles que :
1. Pour s’assurer que les accès traversants et les accès intérieurs reculent
dans le temps, il faut que :
cj+1 < bj ≤ cj ≤ c1 = i − 1
(7.5)
2. Pour s’assurer que deux accès traversants successifs se trouvent sur
deux côtés différents de xi , il faut que ∀j ∈ [1, K(i)] :
(xcj+1 − xi ) · (xcj − xi ) ≤ 0
(7.6)
3. bj est défini comme suit ∀j ∈ [1, K(i)] :
*
+
Sj = k | (cj+1 < k < i) ∧ ((xcj − xi ) · (xk − xi ) > 0)
bj = min |xk − xi |
k∈Sj
Si le minimum est atteint pour plusieurs k ∈ Sj , alors par convention,
nous prenons celui dont la valeur est la plus petite. Cette définition de
56
bj assure que xbj est l’élément de l’ensemble M (M est l’ensemble des
éléments de X qui ont un indice dans l’intervalle (cj+1 , i)) qui est le
plus proche de xi et qui se trouve du même côté que xcj .
4. Pour s’assurer que xcj+2 (∀j ∈ [1, K(i) − 1]) est un nouveau record, il
faut que :
|xcj+2 − xi | < |xbj − xi |
(7.7)
Tous les éléments qui appartiennent à l’ensemble M (M est l’ensemble
des éléments de X qui ont un indice dans l’intervalle (cj+1, i)) et qui
sont du même côté que xcj+2 (par rapport à xi ) sont plus éloignés de
xi que l’est xcj+2 .
La seconde borne de Wilber pour la séquence X est égale à :
m
#
K(i)
(7.8)
i=1
Pour illustrer cette définition, prenons la séquence X
=
7(x1 ), 15(x2 ), 1(x3 ), 2(x4 ), 5(x5 ), 20(x6 ), 23(x7 ), 10(x8 ). La seconde borne
de Wilber pour l’accès 10 vaut 3, car il s’agit du nombre de fois qu’un record
de proximité est amélioré sur un côté différent par rapport à 10 (cfr figure
7.4).
Calculons cette borne en utilisant la définition formelle. Nous calculons
donc la valeur de K(8). Pour cela, prenons les valeurs suivantes pour les
indices des accès traversants :
1. c1 = 7 (donc xc1 = 23)
2. c2 = 5 (donc xc2 = 5)
3. c3 = 2 (donc xc3 = 15)
4. c4 = 1 (donc xc4 = xcK(8)+1 = 7)
Nous pouvons alors calculer la valeur de Sj (pour j = 1, 2, 3 et 4) définie
à la propriété 3 pour en déduire que :
1. b1 = 6 (donc xb1 = 20), car S1 = {6, 7}
2. b2 = 5 (donc xb2 = 5), car S2 = {3, 4, 5}
3. b3 = 2 (donc xb3 = 15), car S3 = {2, 6, 7}
Les sous-séquences d’accès traversants 23, 5, 15, 7 et d’accès internes
20, 5, 15 vérifient les quatre propriétés et sont maximales. Nous avons donc
que la seconde borne de Wilber pour l’accès 10 vaut 3, car K(8) = 3.
57
Fig. 7.4 – La seconde borne de Wilber pour l’accès 10 vaut 3.
58
Théorème 5 (Théorème de la seconde borne de Wilber). Soient un
ensemble de n clés {1, 2, . . . , n} et une séquence d’accès X = x1 , x2 , . . . , xm .
Le nombre de rotations nécessaires
dans le modèle standard pour traiter la
!m
séquence X vaut au moins i=1 K(i). Et donc,
OP T (X) ≥ m +
m
#
K(i)
(7.9)
i=1
Ce théorème n’est pas démontré ici, mais une démonstration est disponible dans l’article [DSW05].
7.4
Conclusion
Dans ce chapitre, nous avons présenté trois bornes inférieures sur le temps
d’exécution d’une séquence d’accès. La première borne, la borne inférieure
de couverture de rectangle, nous a permis d’en dériver deux autres : la borne
inférieure d’entrelacement et la seconde borne de Wilber. La borne inférieure
d’entrelacement sera utilisée pour élaborer une structure ABR dans le chapitre suivant. Cette borne est en fait une simplification de la Première Borne
de Wilber [Wil89]. Et la seconde borne de Wilber est utilisée dans un concept
particulier appelé Key-Independent Optimality [Iac05].
Un problème [DSW05] concernant la borne inférieure de couverture de
rectangle est de savoir si cette borne est égale à l’optimalité dynamique (à
un facteur constant près).
Un autre problème [DSW05] concernant cette borne est de savoir si elle
peut être calculée en temps polynomial, car la valeur de cette borne est égale
à |B|, c’est-à-dire au nombre de boı̂tes qui prises deux à deux ne sont pas en
conflit, mais ce nombre dépend de la manière dont on place les diviseurs des
boı̂tes.
59
Chapitre 8
Tango
Tango est une structure de données, qui atteint un taux de compétitivité
de O(log(log n)). Il s’agit de la première structure de données, qui a amélioré
le taux de compétitivité trivial de O(log n). Elle a été élaborée en 2004 par
Demaine, Harmon, Iacono et Patrascu [DHIP04] et constitue une très grande
avancée dans le domaine de l’optimalité dynamique.
Dans ce chapitre, nous commencerons par présenter Tango selon une
première approche, qui ne vérifie pas la définition du modèle dynamique
des ABR (cfr point 2.2.1). Cette approche est nécessaire pour comprendre
les fondements du véritable algorithme. Ensuite, nous montrerons comment
réaliser cet algorithme avec une structure ABR. Et pour finir, nous analyserons les performances de Tango.
8.1
Structures de données
Les sections présentant l’algorithme Tango sont basées sur l’article
[DHIP04]. Cet article présente les grandes lignes de l’algorithme. La contribution personnelle a été de présenter cet algorithme en détaillant tous les
sous-algorithmes qui le constituent.
Supposons que nous ayons un ensemble {1, 2, . . . , n} de n clés et une
séquence X = x1 , x2 , . . . , xm .
Nous notons par Ti l’état de Tango après avoir effectué les accès
x1 , x2 , . . . , xi . À côté de cette structure, nous maintenons également un arbre
parfait P , appelé arbre de référence, construit sur les mêmes clés que celles
de T . Chaque nœud de l’arbre P contient une information supplémentaire :
son fils préféré. Le fils préféré d’un nœud x est son fils gauche si le dernier
accès dans le sous-arbre de x dans P a eu lieu dans le sous-arbre gauche de
x ou s’il consistait à atteindre x. Le fils préféré de x est son fils droit si le
60
dernier accès dans le sous-arbre de x dans P a eu lieu dans le sous-arbre droit
de x. Dans le cas où aucun accès n’a eu lieu dans le sous-arbre de x, alors x
n’a pas de fils préféré.
L’arbre P est fixe durant toute l’exécution de la séquence X ; seul le fils
préféré des nœuds peut changer. Ainsi, l’état Pi de l’arbre P au temps i (l’état
de P après avoir exécuté les accès x1 , x2 , . . . , xi ) est seulement déterminé par
la séquence d’accès.
8.2
Algorithme de l’arbre de référence
L’état Ti de Tango est déterminé à partir de l’état Pi en appliquant l’algorithme suivant :
1. Suivre le fils préféré de la racine, puis le fils préféré de ce fils, et ainsi
de suite jusqu’à atteindre une feuille. Nous obtenons, alors, un chemin
allant de la racine à une feuille, qu’on appelle le chemin préféré.
2. Compresser ce chemin en un arbre auxiliaire R. À ce stade-ci, nous
pouvons nous contenter d’admettre qu’un arbre auxiliaire est un arbre
rouge-noir. Des informations supplémentaires seront fournies plus loin
dans ce chapitre sur les arbres auxiliaires.
3. Supprimer ce chemin préféré de Pi . Cela a pour conséquence de partitionner cet arbre en plusieurs pièces.
4. Procéder de manière récursive sur chacune de ces pièces. Nous obtenons,
alors, un arbre auxiliaire pour chaque pièce. Il ne reste dès lors qu’à
accrocher chacun de ces arbres obtenus à l’arbre auxiliaire R, tout en
respectant l’ordre ABR sur les nœuds de l’arbre. Pour rappel, l’ordre
ABR signifie que, pour chaque nœud x de l’arbre, les éléments dans le
sous-arbre gauche de x sont plus petits que x et les éléments dans le
sous-arbre droit de x sont plus grands que x.
Nous déduisons de cette construction, que l’arbre Ti est un arbre d’arbres
auxiliaires, dans lequel l’ordre ABR est vérifié.
Nous allons illustrer l’algorithme présenté ci-dessus avec un exemple.
Supposons que nous ayons l’arbre Pi de la figure 8.1 et que nous désirons
construire l’arbre Ti .
Le chemin préféré de l’arbre de la figure 8.1 est constitué des nœuds
3, 1 et 2. Il faut, alors, compresser ce chemin en un arbre auxiliaire. L’article [DHIP04] ne précise pas comment compresser ce chemin. Nous allons ,
alors, choisir de réaliser cette compression en insérant, à l’aide de l’algorithme
d’insertion des arbres rouges-noirs (cfr point 5.2.3), de manière successive les
nœuds du chemin préféré en partant d’un arbre vide et en commençant les
61
3
= fils non
préféré
= fils préféré
1
0
5
2
6
4
Fig. 8.1 – Arbre Pi .
insertions avec le nœud racine. Ainsi, pour compresser le chemin préféré de
la figure 8.1, il suffit d’effectuer l’opération d’insertion sur les nœuds 3, 1 et
2. Le résultat de ces opérations donne l’arbre auxiliaire de la figure 8.2, que
nous désignons par Ri
Maintenant, que le chemin préféré de Pi est compressé, il faut le supprimer
de l’arbre Pi . Ceci partitionne Pi en deux pièces (Pi1 et Pi2 ) présentées à la
figure 8.3. Et il ne reste qu’à effectuer les mêmes opérations sur chaque pièce.
Nous traitons, d’abord, la pièce Pi1 constituée du nœud 0. Sa transformation en arbre auxiliaire est triviale et consiste en le nœud 0. Nous désignons
cet arbre auxiliaire par Ri1 .
Nous traitons, maintenant, la dernière pièce Pi2 constituée des nœuds 4,
5 et 6. Le chemin préféré de cet arbre est constitué des nœuds 5 et 6. La
compression de ce chemin, notée R3 , est donnée à la figure 8.4.
Après cette compression, il faut retirer le chemin préféré de Pi2 , ce qui
donne une unique pièce consistant en le nœud 4.
La transformation de cette pièce en arbre auxiliaire est triviale. Nous
désignons par R4 cet arbre auxiliaire.
Maintenant, il faut rassembler chaque arbre auxiliaire obtenu. On commence par placer R4 comme fils gauche de la racine de R3 , pour obtenir
l’arbre auxiliaire relatif à Pi2 . L’agencement de R3 et R4 est réalisé de la
sorte, de manière à respecter l’ordre ABR. Ce nouvel arbre auxiliaire est
représenté à la figure 8.5 et est noté Ri2 .
À présent, il ne reste plus qu’à attacher Ri1 et Ri2 à Ri pour obtenir l’arbre
auxiliaire Ti correspondant à Pi . L’arbre Ri1 est placé comme fils gauche du
62
= arc rouge
2
= arc noir
= l’isroot bit
est activé
1
3
= l’isroot bit
est désactivé
Fig. 8.2 – Arbre Ri correspondant à la compression du chemin préféré 3, 1,
2
5
4
0
6
Fig. 8.3 – À gauche, l’arbre Pi1 et à droite, l’arbre Pi2 .
63
5
6
Fig. 8.4 – Arbre R3 correspondant à la compression du chemin préféré 5, 6
5
4
6
Fig. 8.5 – Arbre Ri2 obtenu en attachant R4 à R3 .
64
2
1
3
5
0
4
6
Fig. 8.6 – Arbre Ti obtenu en attachant Ri1 et Ri2 à Ri .
nœud 1 de Ri et l’arbre Ri2 est placé comme fils droit du nœud 3 de Ri , de
manière à vérifier l’ordre ABR. L’arbre résultant Ti est présenté à la figure
8.6 et correspond à la transformation de Pi par l’algorithme Tango.
Il existe deux principaux inconvénients avec cette première approche de
Tango :
1. la transformation de Pi en Ti est fort coûteuse en temps d’exécution,
ce qui nuit aux performances de l’algorithme.
2. cette approche ne vérifie pas la définition du modèle ABR, car deux
arbres sont utilisés par l’algorithme.
Cependant, cette première approche permet d’avoir une idée globale des
principes de l’algorithme Tango et aidera à la compréhension du véritable
65
algorithme.
8.3
Arbres auxiliaires
Avant de présenter l’algorithme Tango, nous avons besoin de définir les
arbres auxiliaires, ainsi que quatre opérations supportées par cette structure :
1. la concaténation (cet algorithme provient de l’article [Sah05])
2. l’éclatement (cet algorithme provient de l’article [Sah05])
3. le cutting (cet algorithme provient de l’article [DHIP04])
4. le joining (cet algorithme provient de l’article [DHIP04])
Les deux premières opérations sont définies pour être utilisées par les
deux dernières.
8.3.1
Définition
Un arbre auxiliaire est une structure, qui contient un sous-chemin d’un
chemin allant de la racine à une feuille de P (les nœuds de ce sous-chemin
sont reliés par des arêtes fils préféré dans P ). Les nœuds d’un arbre auxiliaire
sont organisés en fonction de la valeur de leur clé. Chaque nœud x d’un arbre
auxiliaire contient des informations supplémentaires :
1. la profondeur du nœud x dans l’arbre parfait P . Cette donnée est fixe
durant toute l’exécution d’une séquence et est comprise dans l’intervalle
[0, log(n + 1)). Cette information sera désignée par le terme prof.
2. la profondeur maximale dans P des nœuds appartenant au sous-arbre
de x dans T . Cette information sera désignée par le terme profondeurMax.
3. la profondeur minimale dans P des nœuds appartenant au sous-arbre de
x dans T . Cette information sera désignée par le terme profondeurMin.
4. un bit, nommé isroot, pour indiquer si x est la racine de l’arbre auxiliaire, auquel il appartient. Tango étant un arbre d’arbres auxiliaires,
il arrivera, donc, qu’une feuille d’un arbre auxiliaire ait comme fils la
racine d’un autre arbre auxiliaire. L’isroot bit permet, donc, d’indiquer
s’il y a eu un passage d’un arbre auxiliaire à un autre.
Il est important de noter qu’un arbre auxiliaire est implémenté comme
un arbre rouge-noir.
66
8.3.2
Concaténation
Cette opération porte sur un arbre D tel que la racine est x, le sous-arbre
gauche A de x est un arbre rouge-noir et le sous-arbre droit B de x est un
arbre rouge-noir ; l’arbre D n’étant pas nécessairement un arbre rouge-noir.
Le but de la concaténation est de construire à partir de A, B et x un arbre
rouge-noir. Pour réaliser cette opération de manière efficace, il est nécessaire
que chaque nœud x de l’arbre stocke une information supplémentaire : sa
hauteur noire bh(x) (pour rappel bh(x) est le nombre de nœuds ayant un bit
noir entre x (inclus) et la feuille la plus profonde de x moins 1).
L’opération de concaténation se réalise par l’algorithme suivant :
1. Si la hauteur noire de la racine de A est égale à la hauteur noire de
la racine de B, nous construisons l’arbre rouge-noir C avec x comme
racine, A comme sous-arbre gauche de x et B comme sous-arbre droit
de x. On colorie les arcs liant x à A et x à B en noir. La hauteur noire
de x est alors définie comme la hauteur noire de la racine de A plus 1.
2. Si la hauteur noire de la racine de A est plus grande que la hauteur
noire de la racine de B, il faut suivre le chemin des fils droits de A
jusqu’au premier nœud y ayant une hauteur noire égale à celle de la
racine de B. Il faut remarquer que la hauteur noire du père p de y doit
être égale à bh(B) − 1 (où bh(B) correspond à la hauteur noire de la
racine de B), car sinon il aurait été le premier nœud sur le chemin des
fils droits à avoir une hauteur noire égale à bh(B). Désignons par Y
le sous-arbre de y. Nous retirons Y de l’arbre A pour placer le nœud
x à la place de Y . Pour que le nœud x ait une hauteur noire égale à
bh(B) − 1, nous colorions l’arc entre x et p en rouge. Si l’arc entre g (le
père de p) et p est rouge, alors il faut effectuer une rotation simple entre
g et p. Cette rotation laisse x sans fils, car il n’en avait pas avant la
rotation entre g et p (cfr figure 8.7). Ensuite, il faut attacher Y comme
fils gauche de x en coloriant l’arc en noir et l’arbre B comme fils droit
de x en coloriant l’arc en noir.
3. Si la hauteur noire de la racine de A est plus petite que la hauteur noire
de la racine de B, nous procédons de manière symétrique au cas 2 sur
l’arbre B.
8.3.3
Éclatement
L’opération d’éclatement porte sur un arbre auxiliaire C et sur un nœud
x de cet arbre. Elle réorganise C, de manière à ramener x à la racine de
67
Fig. 8.7 – Rotation simple sur les nœuds g et p.
l’arbre avec comme sous-arbre gauche de x un arbre rouge-noir, noté A, et
comme sous-arbre droit de x un arbre rouge-noir, noté B.
Pour réaliser l’éclatement, il faut d’abord construire les arbres A et B à
partir de C avec l’algorithme suivant. Au début les arbres A et B sont vides.
1. Rechercher le nœud x dans C. Lorsque x est localisé, il faut rajouter
dans A le sous-arbre gauche de x et dans B le sous-arbre droit de x.
2. Ensuite, il faut remonter le chemin de x à la racine et réaliser l’étape,
qui suit, jusqu’à ce que la racine soit atteinte.
3. Soit p le nœud courant. Deux cas doivent être envisagés, selon que x se
situe dans le sous-arbre gauche ou dans le sous-arbre droit de p :
(a) si x est dans le sous-arbre gauche de p, alors le sous-arbre droit
de p contient des nœuds ayant des valeurs plus grandes que celle
de x. Ces nœuds doivent, donc, se situer dans l’arbre B. Dès lors,
nous effectuons une concaténation avec B, p et le sous-arbre droit
de p. Cette concaténation donne un arbre rouge-noir, qui devient
B.
(b) si x est dans le sous-arbre droit de p, alors le sous-arbre gauche de
p contient des nœuds ayant des valeurs plus petites que celle de x.
Ces nœuds doivent, donc, se situer dans l’arbre A. Dès lors, nous
effectuons une concaténation avec A, p et le sous-arbre gauche de
p. Cette concaténation donne un arbre rouge-noir, qui devient A.
68
4. Lorsque tous les éléments sur le chemin de la remontée sont traités, nous
obtenons que A forme un arbre rouge-noir avec des éléments ayant des
valeurs plus petites que celle de x et que B forme un arbre rouge-noir
avec des éléments ayant des valeurs plus grandes que celle de x. Une
fois A et B créés, il suffit d’attacher A comme sous-arbre gauche de x
avec un arc noir et B comme sous-arbre droit de x avec un arc noir.
Bien que cela ne soit pas trivialement vérifiable, la complexité de cette
opération est logarithmique en le nombre d’éléments dans l’arbre [Sah05].
8.3.4
Cutting
Le cutting à une profondeur d sur un arbre auxiliaire coupe cet arbre en
deux arbres auxiliaires. L’un stockant les nœuds, dont l’information prof est
plus petite que d. L’autre stockant les nœuds, dont l’information prof est
plus grande que d.
Un arbre auxiliaire A stocke un sous-chemin C d’un chemin allant de
la racine à une feuille de P . Par conséquent, l’opération de cutting à une
profondeur d sur l’arbre A consiste, au niveau de l’arbre P , à scinder C en
deux parties. La première partie part du début du chemin C jusqu’au nœud
de profondeur d − 1 et la seconde partie part du nœud de profondeur d
jusqu’à la fin du chemin C. Ces deux parties correspondent aux deux arbres
auxiliaires créés par le cutting.
Les nœuds d’un arbre auxiliaire A, qui ont une information prof plus
grande que d, forment un intervalle I dans l’espace des clés de cet arbre.
Ceci est dû au fait que les nœuds de cet arbre auxiliaire appartiennent à
un même sous-chemin d’un chemin allant de la racine à une feuille dans P .
L’opération de cutting se base sur cette propriété.
Pour réaliser un cutting à une profondeur d, il faut d’abord déterminer les
éléments minimal et maximal de l’intervalle I. Il faut, donc, trouver l’élément
minimal de A ayant une profondeur plus grande que d (ou égale à d) et
l’élément maximal de A ayant une profondeur plus grande que d (ou égale à
d). Nous désignerons, respectivement, ces éléments par l et r.
Le nœud l est l’élément de l’arbre A le plus à gauche et ayant une profondeur plus grande que d. Déterminer l peut se faire par l’algorithme suivant :
1. Commencer le traitement à la racine de A.
2. Analyser l’information prof du nœud courant x :
(a) Si l’information prof est supérieure à d, alors il faut analyser l’information profondeurMax du fils gauche de x :
69
i. Si l’information profondeurMax du fils gauche de x est plus
grande que d, alors il existe dans le sous-arbre du fils gauche
de x un nœud plus petit que le nœud courant x et ayant
une information prof plus grande que d. Dès lors, il faut se
déplacer sur le fils gauche de x et recommencer l’étape 2.
ii. Si l’information profondeurMax du fils gauche de x est strictement plus petite que d, alors il n’existe pas de nœud plus
petit que x et ayant une information prof plus grande que d.
Le nœud x correspond, alors, à l et l’algorithme est arrêté.
(b) Si l’information prof est strictement plus petite que d, alors il faut
analyser l’information profondeurMax des fils de x :
i. Si l’information profondeurMax du fils gauche de x est plus
grande que d, alors il faut se déplacer sur le fils gauche de x,
car le nœud l se situe dans le sous-arbre gauche de x.
ii. Si l’information profondeurMax du fils droit de x est plus
grande que d, alors il faut se positionner sur le fils droit de x,
car le nœud l se situe dans le sous-arbre droit de x.
iii. Si l’information profondeurMax du fils gauche et du fils droit
de x est plus grande que d, alors il faut se positionner sur le
fils gauche de x, plutôt que le fils droit. En effet, comme le
sous-arbre gauche de x contient des clés plus petites que celles
du sous-arbre droit de x, on est certain que l se situe dans le
sous-arbre gauche de x, et non pas dans le sous-arbre droit de
x.
Cet algorithme n’est correct que s’il existe un nœud l dans l’arbre auxiliaire A. Pour traiter le cas, où il n’existe pas un tel nœud dans A, il suffit
d’examiner l’information profondeurMax de la racine de A. Si cette information est strictement plus petite que d, alors il n’existe pas de nœud l dans
A. Par contre, si elle est plus grande que d, le nœud l existe et l’algorithme
ci-dessus peut être appliqué.
Quant au nœud r, il s’agit de l’élément de A le plus à droite et ayant une
profondeur plus grande que d. Déterminer r se fait de manière symétrique à
l’algorithme pour déterminer l.
Une fois l et r déterminés, il faut déterminer le prédécesseur l$ de l et le
successeur r $ de r en appliquant, respectivement, les algorithmes prédécesseur
(cfr point 5.2.5) et successeur (cfr point 5.2.4) des arbres rouges-noirs.
Étant donné que les nœuds qui ont une information prof supérieure à
d forment un intervalle I dans l’espace des clés de A, nous allons utiliser
70
cette propriété pour effectuer le cutting. En effet, l’intervalle I correspond à
[l, r] ou de manière équivalente à (l$ , r $). Il ne reste, dès lors, qu’à isoler les
éléments de cet intervalle dans un sous-arbre, de manière à pouvoir, ensuite,
les séparer du reste de l’arbre. L’algorithme, qui suit, se base sur ce principe
pour effectuer le cutting (voir figure 8.8) :
1. Effectuer un éclatement sur A en l$ . De par cette opération, l$ devient
la racine de l’arbre, ce qui a pour conséquence que les éléments du
sous-arbre gauche de l$ ont des clés appartenant à l’intervalle (−∞, l$ )
et que les éléments du sous-arbre droit de l$ ont des clés appartenant
à l’intervalle (l$ , ∞). Notons par B et C, respectivement, le sous-arbre
gauche de l$ et le sous-arbre droit de l$ .
2. Effectuer sur C un éclatement en r $ . Cette opération fait de r $ la racine
du sous-arbre C. Dès lors, le sous-arbre gauche de r $ , noté D, contient
des clés appartenant à l’intervalle (l$ , r $) et le sous-arbre droit de r $ ,
noté E, contient des clés dans l’intervalle (r $ , ∞). Ainsi, par les deux
éclatements sur l$ et r $ , on est parvenu à isoler les nœuds, dont l’information prof est supérieure à d, dans un sous-arbre de A. Ce sous-arbre
étant D.
3. Activer l’isroot bit de la racine D. Cette opération sépare, de manière
effective, le sous-arbre D du reste de l’arbre A et fait de D un
arbre auxiliaire. La partie restante de A, obtenue après avoir enlevé
D, sera dorénavant désignée par A$ . Une opération d’éclatement ou
de concaténation sur A$ ne portera que sur les nœuds de cet arbre
auxiliaire ; elle n’affectera pas les nœuds de l’arbre auxiliaire D. À
cette étape-ci, D contient les nœuds de A, dont l’information prof
est supérieure à d et A$ contient les nœuds de A, dont l’information
prof est inférieure à d. Cependant, le fait d’avoir ainsi séparé de r $
son sous-arbre gauche D a pour conséquence, que le sous-arbre de r $
n’est plus forcément un arbre rouge-noir, ce qui implique que l’arbre
A$ , également, n’en est plus forcément un. Les deux opérations, qui
suivent, ont pour but de transformer A$ en un arbre rouge-noir, et par
la même occasion en un arbre auxiliaire.
4. Effectuer une concaténation sur r $ . Le nœud r $ n’ayant pas de sousarbre gauche, l’opération de concaténation forme un arbre rouge-noir à
partir du nœud r $ et des nœuds dans le sous-arbre droit de r $ . Notons
par C $ cet arbre rouge-noir, ainsi, créé.
5. Effectuer une concaténation sur l$ . Cette opération crée un arbre rougenoir à partir de tous les nœuds de A$ . Ainsi, par ces deux concaténations
successives, l’arbre A$ a été réordonné, de manière à devenir un arbre
71
répondant à la définition d’arbre auxiliaire. Il est important de noter
que deux concaténations sont nécessaires pour transformer A$ en un
arbre auxiliaire. En effet, on pourrait penser à effectuer qu’une seule
concaténation (portant sur l$ ), de manière à transformer A$ en un arbre
auxiliaire. Mais cette solution aurait été erronée, car la concaténation
sur un nœud nécessite que le sous-arbre gauche et le sous-arbre droit
de ce nœud soient des arbres rouges-noirs. Or, le sous-arbre droit de l$ ,
c’est-à-dire le sous-arbre de r $ , n’est plus forcément un arbre rouge-noir,
après que son sous-arbre gauche D lui ait été retiré. D’où la nécessité,
d’abord, de transformer le sous-arbre de r $ en arbre rouge-noir, avant
de réaliser la même opération sur le sous-arbre de l$ .
Deux cas particuliers sont à gérer :
1. le nœud l$ n’existe pas (l n’a pas de prédécesseur dans son arbre auxiliaire) : il ne faut alors réaliser des étapes ci-dessus que les étapes 2, 3
et 4.
2. le nœud r $ n’existe pas (r n’a pas de successeur dans son arbre auxiliaire) : il ne faut alors réaliser des étapes ci-dessus que les étapes 1, 3
et 5.
8.3.5
Joining
L’opération de joining porte sur deux arbres auxiliaires A et B, et consiste
à les réunir pour n’en former qu’un seul. Une précondition à cette opération
consiste, au niveau de l’arbre parfait P , à ce que les deux chemins, qui correspondent aux arbres A et B, forment un sous-chemin continu C1 d’un chemin
allant de la racine à une feuille de P s’ils sont réunis. Par conséquent, le
joining sur A et B consiste à réunir les chemins, qu’ils stockent, pour former
le chemin C1 , qui sera stocké dans un nouvel arbre auxiliaire C. Du fait, que
le chemin A1 , stocké par A, est adjacent au chemin B1 , stocké par B, nous
en déduisons deux propriétés :
1. un des deux arbres A et B est tel que l’information prof de chacun de
ses nœuds est plus grande que celle de tous les nœuds de l’autre arbre.
2. l’espace des clés de l’arbre, ayant les informations prof plus grandes,
forme un intervalle dans l’espace des clés de l’arbre ayant les informations prof plus petites.
Une seconde précondition est nécessaire pour cette opération : l’arbre,
ayant les informations prof plus grandes, est attaché à un nœud du second
arbre.
L’algorithme du joining se base sur tout ceci et est décrit ci-dessous.
72
Fig. 8.8 – Réalisation d’un cutting avec des éclatements et des
concaténations.
73
D’abord, il faut déterminer lequel des arbres A et B stocke les nœuds
ayant les informations prof plus grandes. Pour cela, il suffit de comparer
l’information prof de la racine de A et de la racine de B, et de déterminer
laquelle est la plus grande. Supposons, quitte à renommer les arbres, que B
stocke les nœuds ayant les informations prof plus grandes.
Ensuite, il faut rechercher les nœuds l$ et r $ (posons l$ < r $ ) de A entre
lesquels l’espace de clés de B est compris. La recherche de ces éléments se
fait par l’algorithme suivant :
1. Rechercher dans A la clé de la racine de B. Étant donné que la racine
de B est le fils d’un nœud p de A, cette opération permet d’atteindre
ce nœud p.
2. Si la racine de B est le fils gauche de p, alors ce nœud-ci correspond à
r $ et comme l$ est le prédécesseur de r $ dans A, ce nœud l$ peut être
déterminé en appliquant l’algorithme prédécesseur (cfr point 5.2.5) sur
r $ . Ainsi, nous sommes parvenus à identifier l$ et r $ dans ce premier cas.
3. Par contre, si la racine de B est le fils droit de p, alors ce nœud-ci
correspond à l$ et comme r $ est le successeur de l$ dans A, ce nœud
r $ peut être déterminé en appliquant l’algorithme successeur (cfr point
5.2.4) sur l$ . Ainsi, nous sommes parvenus à identifier l$ et r $ dans ce
second cas.
Une fois l$ et r $ déterminés, il faut appliquer l’algorithme, qui suit, de
manière à réaliser le joining (voir figure 8.9) :
1. Effectuer sur A un éclatement en l$ . Ceci ramène l$ à la racine de
l’arbre. Notons par, respectivement, A$ et C le sous-arbre gauche et le
sous-arbre droit de l$ . Les clés de A$ appartiennent à l’intervalle (−∞, l$ )
et les clés de C appartiennent à l’intervalle (l$ , ∞)
2. Effectuer sur C un éclatement en r $. Par cette opération, r $ devient la
racine du sous-arbre C. Comme les nœuds l$ et r $ sont adjacents, alors
r $ est le plus petit élément de C. Dès lors, étant ramené à la racine
de C par le second éclatement, r $ contient dans son sous-arbre droit,
noté E, tous les éléments de C. Comme r $ ne contient aucun élément
de C dans son sous-arbre gauche, on est, alors, certain de n’avoir dans
le sous-arbre gauche de r $ que l’arbre auxiliaire B.
3. Désactiver l’isroot bit de la racine de B. Cette opération réunit de
manière effective l’arbre A et l’arbre B. L’arbre résultant de cette
réunification sera désigné par Z. Le fait de rajouter, ainsi, l’arbre B a
pour conséquence que le sous-arbre de r $ n’est plus forcément un arbre
rouge-noir, ce qui implique que Z, également, n’en est plus forcément
74
un. Les deux opérations, qui suivent, ont pour but de résoudre ce
problème.
4. Effectuer une concaténation sur r $ . Cette opération de concaténation
réarrange le sous-arbre de r $ , de manière à le transformer en un arbre
rouge-noir. Notons par C $ l’arbre résultant de cette concaténation.
5. Effectuer une concaténation sur l$ . Cette opération réarrange l’arbre
Z pour le transformer en un arbre rouge-noir. Ainsi, Z est un arbre
auxiliaire, qui contient les éléments de A et B. Il s’agit, donc, de l’arbre
résultant du joining de A et B.
8.4
Algorithme Tango
Dans la première approche, l’état Ti de l’arbre Tango T est construit à
partir de l’état Pi de l’arbre parfait P . Cependant, cette manière de procéder
posait plusieurs inconvénients. L’algorithme Tango se base sur les fondements
de la première approche, mais il construit différemment l’arbre Ti , de sorte à
résoudre les problèmes de la première approche. En fait, l’état Ti est construit
à partir de l’état Ti−1 et du prochain accès xi de la séquence X.
Il faut remarquer que l’accès au nœud xi , au niveau de l’arbre parfait P ,
change nécessairement les fils préférés, de manière à avoir un chemin préféré
allant de la racine de P à xi et ne change pas d’autres fils préférés. Par
convention, nous décidons que pour un nœud accédé son fils gauche doit
devenir son fils préféré. Par conséquent, les seuls nœuds, dont les fils préférés
peuvent changer à cause de l’accès à xi , sont ceux se trouvant sur le chemin
menant de la racine de P à xi (ainsi que le fils droit de xi ).
Au niveau de l’arbre Tango T , il est possible, lors de la recherche de xi ,
de déterminer les nœuds, dont les fils préférés changent, car un changement
de fils préféré (excepté pour le changement de fils préféré de xi ) correspond
au passage entre deux arbres auxiliaires (disons de A à B). En effet, comme
un arbre auxiliaire stocke un chemin préféré, alors le point de passage entre
deux arbres auxiliaires correspond à choisir le fils non préféré d’un des nœuds
de A, car si on avait choisi son fils préféré, on serait resté dans le même arbre
auxiliaire.
En se basant sur cette constatation, il est inutile de construire Ti à partir
de Pi . Il suffit de partir de Ti−1 , de rechercher le nœud xi dans cet arbre et de
changer durant cette recherche le fils préféré des nœuds, pour lesquels l’accès
à xi provoque un changement de fils préféré. L’algorithme suivant se base sur
cette idée pour construire l’état Ti à partir de Ti−1 et de xi :
1. Commencer la recherche de xi à la racine de Ti−1 .
75
Fig. 8.9 – Réalisation d’un joining avec des éclatements et des
concaténations.
76
2. Se déplacer vers le sous-arbre gauche ou le sous-arbre droit du nœud
courant x selon que xi soit, respectivement, plus petit ou plus grand
que x. Une fois le déplacement effectué, disons sur le nœud y , il faut
analyser l’isroot bit de ce nœud :
(a) Si l’isroot bit est désactivé, cela signifie que x et y appartiennent
au même arbre auxiliaire. Dès lors, l’accès à xi ne modifie pas à ce
niveau un fils préféré et aucun traitement additionnel n’est alors
nécessaire.
(b) Si l’isroot bit est activé, cela signifie qu’on est passé d’un arbre
auxiliaire à un autre (disons de A à B). Dès lors, l’accès à xi
modifie le fils préféré d’un nœud z de A. Il faut alors effectuer
le traitement qui suit pour changer le fils préféré de z . Notons
d’abord que le nouveau fils préféré de z est le nœud de B ayant
la plus petite information prof :
i. Effectuer sur l’arbre auxiliaire A contenant x un cutting à une
profondeur d égale à l’information profondeurMin de y , de
manière à faire du fils préféré de z son fils non préféré
ii. Effectuer un joining entre l’arbre auxiliaire A et l’arbre auxiliaire B, de manière à ce que z ait son nouveau fils préféré.
L’étape 2 doit être réitérée tant que le nœud xi n’est pas atteint.
3. Lorsque le nœud xi est atteint, il faut encore faire de son fils gauche son
fils préféré. Pour cela, il faut effectuer un cutting sur l’arbre auxiliaire
contenant xi à une profondeur d égale à la profondeur de xi plus 1. Ensuite, il faut rechercher le nœud p. Ce nœud est atteint en recherchant
le prédécesseur de xi , mais en s’arrêtant sur le premier nœud dont l’isroot bit est activé. Ce nœud p est en fait la racine de l’arbre auxiliaire
contenant le fils gauche de xi dans P . En effet, le prédécesseur de xi
appartient au sous-arbre du fils gauche de xi dans P , ce qui implique
qu’en recherchant dans T le prédécesseur de xi , nous devons obligatoirement passer par l’arbre auxiliaire contenant le fils gauche de xi dans
P . Et finalement, il faut effectuer un joining entre l’arbre auxiliaire
contenant xi et l’arbre auxiliaire contenant p .
8.5
Analyse des performances
Cette section est basée sur l’article [DHIP04]. La contribution personnelle
a été de détailler les démonstrations des lemmes et des théorèmes énoncés.
77
Soient un arbre Tango T construit sur les clés {1, 2, . . . , n} et une séquence
d’accès X = x1 , x2 , . . . , xm .
L’analyse des performances de Tango se base sur la borne inférieure d’entrelacement (cfr point 7.3.1) et nous avons besoin de définir la notion qui
suit.
La borne d’entrelacement IBi (X) d’un accès xi est la borne inférieure
d’entrelacement de la séquence x1 , x2 , . . . , xi , à laquelle on soustrait la borne
inférieure d’entrelacement de la séquence x1 , x2 , . . . , xi−1 . Par conséquent,
IBi (X) est le nombre d’alternances left-right et right-left introduites en plus
par l’accès xi .
Durant l’analyse des performances de Tango, nous ne tiendrons pas
compte des changements de fils préférés qui font du fils gauche de chaque
nœud accédé son fils préféré. Pour une séquence de m accès, nous omettrons donc m changements de fils préférés. Cependant, ceci n’est pas
problématique, car nous pouvons nous rendre compte que ces m changements
de fils préférés n’influent pas sur les performances de Tango.
Lemme 3. Supposons que durant un accès xi , il y ait k nœuds, dont le fils
préféré change (de gauche à droite ou de droite à gauche) dans P. Ce nombre
k est égal à la borne d’entrelacement IBi (X) de l’accès xi .
Démonstration : Considérons que les éléments x1 , x2 , . . . , xi−1 aient été
accédés et que le prochain élément à accéder soit xi .
Pour démontrer ce lemme, il suffit de prouver l’équivalence suivante :
l’accès xi change le fils préféré d’un nœud x si et seulement si l’accès xi
induit une alternance left-right ou right-left supplémentaire dans la séquence
étiquetée de x sur les clés x1 , x2 , . . . , xi par rapport à la séquence étiquetée
de x sur les clés x1 , x2 , . . . , xi−1 (entre d’autres termes IBi (X) augmente).
Ceci se fait en deux étapes :
1. Lors d’un accès xi , le fils préféré du nœud x passe de gauche à droite
si et seulement si le dernier accès dans le sous-arbre de x était dans sa
région gauche et que xi se situe dans la région droite de x. Ce dernier
événement correspond exactement au cas où une alternance left-right
supplémentaire est induite par l’accès xi dans la séquence étiquetée de
x sur les clés x1 , x2 , . . . , xi par rapport à la séquence étiquetée de x sur
les clés x1 , x2 , . . . , xi−1 .
2. Lors d’un accès xi , le fils préféré du nœud x passe de droite à gauche
si et seulement si le dernier accès dans le sous-arbre de x était dans sa
région droite et que xi se situe dans la région gauche de x. Ce dernier
événement correspond exactement au cas où une alternance right-left
78
supplémentaire est induite par l’accès xi dans la séquence étiquetée de
x sur les clés x1 , x2 , . . . , xi par rapport à la séquence étiquetée de x sur
les clés x1 , x2 , . . . , xi−1 .
Ces deux cas permettent de démontrer l’équivalence citée ci-dessus et par
conséquent le lemme.
!
Lemme 4. Soit k le nombre de nœuds, dont les fils préférés changent durant
un accès xi . Le coût d’accès à xi vaut O ((k + 1) · (1 + log(log n))).
Démonstration : Le coût d’accès au nœud xi consiste, d’une part, au coût
pour rechercher xi dans l’arbre, et d’autre part, au coût pour réarranger la
structure, de manière à la faire passer de l’état Ti−1 à l’état Ti .
Nous analysons d’abord le coût pour rechercher xi . Pour rappel, si durant
la recherche de xi , on passe d’un arbre auxiliaire à un autre(disons de A à
B), cela signifie que l’accès xi change le fils préféré d’un des nœuds de l’arbre
auxiliaire A. Étant donné que la recherche de xi induit k changements de fils
préférés, cela signifie que k + 1 arbres auxiliaires devront être visités pour la
recherche de xi . Un arbre auxiliaire contient au plus log(n) éléments, car il
stocke un sous-chemin d’un chemin allant de la racine à une feuille dans P .
Par conséquent, le coût d’une recherche dans un arbre auxiliaire nécessite un
coût au pire cas de O(log(log n)). Pour être plus précis, la complexité vaut
O (max {1, log(log n)}). Ceci permet de tenir compte du cas, où log(log n) est
négatif, car la recherche se fait, alors, en temps constant. Dans la notation
O(.), l’expression O (max {1, log(log n)}) est équivalente à O(1 + log(log n)).
En effet, dans la notation O(.), en présence d’une somme, on prend le terme
maximal de cette somme. Finalement, comme la recherche de xi visite k + 1
arbres auxiliaires, le coût pour la réaliser vaut O ((k + 1) · (1 + log(log n))).
Maintenant que le coût de la recherche est analysé, nous nous occupons
du coût nécessaire pour transformer Ti−1 en Ti . Quand nous passons d’un
arbre auxiliaire à un autre, il faut effectuer un cutting et un joining. Comme
ces opérations portent chacune sur au plus log(n) éléments, elles nécessitent
chacune un coût de O(1 + log(log n)).
Comme nous visitons k + 1 arbres auxiliaires, nous avons alors k passages
entre des arbres auxiliaires, ce qui implique d’effectuer k cuttings et k joinings
pour lesquels nous payons un coût de O(k · (1 + log(log n))).
En conclusion, le coût de l’accès à xi (= recherche de xi + réorganisation
de l’arbre) vaut O (max {(k + 1) · (1 + log(log n)) , k · (1 + log(log n))}).
En
simplifiant
ce
résultat,
nous
obtenons
finalement
O ((k + 1) · (1 + log(log n))).
!
79
Corollaire 2. Soient un arbre constitué des clés {1, 2, . . . , n} et
une séquence X = x1 , x2 , . . . , xm . Le coût pour servir X vaut
O ((K + m) · (1 + log(log n))), où K est le nombre de changements de fils
préférés durant l’exécution de la séquence X.
Théorème 6. Soient un arbre constitué des clés {1, 2, . . . , n} et une séquence
X = x1 , x2 , . . . , xm . Le coût pour servir X avec l’algorithme Tango vaut
O ((OP T (X) + n) · (1 + log(log n))).
Démonstration : Pour démontrer ce résultat, nous allons utiliser le corollaire 2. En fait, nous allons compter le nombre de changements de fils
préférés durant l’exécution de la séquence X pour remplacer le terme K
dans le corollaire 2 par ce nombre.
!
Par le lemme 3, nous avons que IB(X) ( car m
i=1 IBi (X) = IB(X)) est
le nombre de fois qu’un fils préféré change de gauche à droite ou de droite
à gauche lors de l’exécution de la séquence X. Cependant, il y a également
au plus n initialisations de fils préférés (une initialisation d’un fils préféré
consiste à choisir pour un nœud son fils gauche ou son fils droit comme
fils préféré, alors qu’il n’avait pas encore de fils préféré). Par conséquent, le
nombre total de changements de fils préférés durant l’exécution de la séquence
X vaut IB(X) + n. En combinant ceci avec le corollaire 2, nous obtenons
que le coût pour servir X vaut :
O ((IB(X) + n + m) · (1 + log(log n)))
(8.1)
Dans le chapitre 7, nous avons démontré le résultat, qui suit :
1
· (IB(X) + m)
(8.2)
2
En utilisant ce résultat, nous déduisons que le coût pour servir X vaut
O((2·OP T (X)−m+n+m)·(1+log(log n))). En simplifiant ce résultat, nous
obtenons finalement, que le coût pour servir la séquence X avec l’algorithme
Tango vaut :
OP T (X) ≥
O ((OP T (X) + n) · (1 + log(log n)))
(8.3)
!
Corollaire 3. Lorsque m = Ω(n), le temps d’exécution de Tango pour servir
la séquence X vaut O(OP T (X) · (1 + log(log n))).
80
Démonstration : Si la séquence d’accès X est constituée de plus de n clés,
on a que OP T (X) = Ω(n). Par conséquent, en se basant sur le théorème
précédent (théorème 6), on a que le coût pour servir X avec l’algorithme
Tango vaut :
O (OP T (X) · (1 + log(log n)))
(8.4)
!
Si nous avions tenu compte des changements de fils préférés, qui font
du fils gauche de chaque nœud accédé son fils préféré, alors il aurait fallu
rajouter le terme m· (1 + log(log n)) au résultat de l’équation 8.4. Cependant,
ce terme peut être négligé, car OP T (X) ≤ m. Et donc le fait d’omettre ces
changements de fils préférés n’est pas problématique.
Théorème 7. Le coût au pire cas pour la recherche d’un nœud xi vaut
O ((log n) · (1 + log(log n))).
Démonstration : Comme l’arbre P est un arbre parfait, sa hauteur est
égale à log n. Par conséquent, le nombre maximal de nœuds, dont les fils
préférés peuvent changer à cause de l’accès xi vaut log n. Dès lors, par le
lemme 4, le coût au pire cas de l’accès xi vaut O ((log n) · (1 + log(log n))).
!
8.6
Conclusion
Dans ce chapitre, nous avons présenté une structure, qui atteint un taux
de compétitivité de O(log(log n)). Ce taux est atteint au prix d’un algorithme
assez compliqué et lourd à implémenter. De plus, les nombreuses opérations
nécessaires pour réaliser l’algorithme pourraient porter préjudice à ses performances en pratique.
Notons qu’en utilisant la borne inférieure d’entrelacement, nous ne pouvons pas améliorer le taux de O(log(log n)). En effet, chaque arbre auxiliaire
contient au pire cas O(log n) éléments. Et donc quelle que soit la manière
dont sont organisés les arbres auxiliaires, chaque arbre doit avoir une profondeur de Ω(log(log n)), ce qui se traduit par un coût d’accès de Ω(log(log n))
dans chaque arbre auxiliaire.
81
Chapitre 9
Multi-splay tree
Le multi-splay tree est une structure de données qui est basée sur Tango
et qui atteint un taux de compétitivité de O(log(log n)). Elle a été élaborée en
2004 par Sleator et Wang [SW04]. Le principal avantage des multi-splay trees
par rapport à Tango est que l’algorithme des multi-splay trees est moins compliqué et plus simple à implémenter, ce qui peut favoriser les performances
en pratique de la structure, car l’implémentation peut être plus facilement
optimisée.
Dans ce chapitre, nous allons d’abord présenter l’algorithme des multisplay trees et ensuite, nous allons procéder à l’analyse amortie des performances de cette structure.
9.1
Structures de données
Les sections présentant l’algorithme des multi-splay trees sont basées sur
les articles [SW04] et [DSW06]. La contribution personnelle a été de détailler
le plus possible l’algorithme et d’essayer de le présenter de la manière la plus
claire possible.
Nous disposons comme pour Tango d’un arbre de référence P (cfr point
8.1), auquel est lié un arbre T , qui est ici appelé un multi-splay tree. La
différence est que le multi-splay tree est un arbre de splay trees et non pas
un arbre d’arbres rouges-noirs.
Chaque nœud x dans T possède plusieurs informations :
1. sa profondeur dans P , dénotée par prof. Cette valeur est constante.
2. la profondeur minimale dans P de tous les nœuds appartenant au splay
subtree de x. Le splay subtree de x est l’ensemble des nœuds appartenant au même splay tree que x et qui ont x comme ancêtre (à noter
que x y est inclus). Cette information est dénotée par mindepth.
82
3. l’isroot bit qui indique si l’arête reliant x à son père est solide ou discontinue. Deux nœuds reliés par une arête solide appartiennent au même
splay tree et deux nœuds reliés par une arête discontinue appartiennent
à des splay trees différents
9.2
Algorithmes
Nous allons d’abord expliquer l’algorithme du point de vue de l’arbre de
référence P et, ensuite, nous allons expliquer comment le réaliser au niveau
de T .
9.2.1
Algorithme de l’arbre de référence
L’algorithme d’accès à un nœud xi est le suivant :
1. localiser le nœud xi , en commençant la recherche à la racine de P .
2. suivre le chemin allant de xi à la racine, en changeant les fils préférés
adéquats, de manière à ce que xi fasse partie du même chemin préféré
que celui de la racine de P .
Cet algorithme est identique à celui de Tango (cfr point 8.2), hormis que
les changements de fils préférés se font en remontant de xi à la racine. Ceci
facilite, en fait, l’algorithme dans T .
9.2.2
Algorithme multi-splay tree
Pour réaliser l’algorithme ci-dessus sans utiliser l’arbre de référence, nous
allons nous baser sur l’accès xi et sur l’arbre Ti−1 (l’arbre T avant l’accès xi )
pour le transformer de manière à tenir compte des modifications causées par
l’accès xi .
L’idée de l’algorithme est donc la même que celle de Tango. Cependant,
les changements de fils préférés devront se faire différemment puisque un
multi-splay tree est constitué de splay trees et non pas d’arbres rouges-noirs.
Les changements de fils préférés peuvent être réalisés dans un multi-splay
tree de manière plus souple par rapport à Tango, car un splay tree n’impose
pas de conditions sur sa structure, contrairement aux arbres rouges-noirs (cfr
point 5.2.1).
L’algorithme d’accès au nœud xi est le suivant :
1. localiser le nœud xi , en commençant la recherche à la racine de T .
83
2. suivre le chemin menant de xi à la racine de T . Pour chaque nœud x,
rencontré durant la remontée, il faut analyser son isroot bit. Si ce bit
est activé, cela signifie que le passage de x à son père p correspond au
passage d’un splay tree à un autre. Il faut alors effectuer un switch (un
changement de fils préféré). Ce switch doit porter sur le nœud y du
splay tree, auquel p appartient, et dont l’information prof est égale à
l’information mindepth de x moins 1. Si nous nous basons sur l’arbre
parfait P , nous constatons que dans P ce nœud y est l’ancêtre des
nœuds appartenant au splay tree de x. De ce fait, le splay tree, auquel
x appartient, doit être un descendant de y dans T (voir figure 9.1). Et
donc, le nœud y doit se situer sur le chemin menant de p à la racine
du splay tree de p. Le nœud y peut donc être déterminé en suivant
ce chemin. Une fois y trouvé, il faut effectuer un switch sur ce nœud,
c’est-à-dire changer son fils préféré. Ce changement de fils préféré de y ,
au niveau de l’arbre T , consiste à réunir le splay tree, auquel appartient
y et le splay tree auquel appartient x, pour n’en faire plus qu’un seul
splay tree.
3. une fois les switches effectués et la racine atteinte, il faut splayer le
nœud xi pour le ramener à la racine de T . Pour cela, on peut utiliser l’algorithme usuel de splaying (cfr point 6.2.1), car après tous les
switches, xi appartient au même splay tree que celui de la racine de T .
Nous allons, maintenant, expliquer comment réaliser l’opération de
switch. En fait, il y a deux switches à considérer : le switch gauche-droite
et le switch droite-gauche, selon qu’il faille, respectivement, changer le fils
préféré de gauche à droite ou de droite à gauche. Nous allons détailler l’algorithme du switch gauche-droite. L’autre switch peut être déduit de manière
analogue.
Considérons un switch sur y . Nous définissons alors quatre ensembles :
1. L, qui est l’ensemble des nœuds dans le sous-arbre gauche de y dans P
appartenant au même chemin préféré que celui du fils gauche de y.
2. R, qui est l’ensemble des nœuds dans le sous-arbre droit de y dans P
appartenant au même chemin préféré que celui du fils droit de y.
3. U, qui est l’ensemble des nœuds au-dessus de y dans P appartenant au
même chemin préféré que celui de y.
4. S = L ∪ R ∪ {y} ∪ U.
Si nous considérons les éléments de S par ordre croissant, nous avons que
les clés de L forment un sous-intervalle dans l’intervalle des clés de S. Et
idem pour R. De plus, il n’y a que le nœud y qui se situe entre l’intervalle
de L et l’intervalle de R.
84
Fig. 9.1 – Le splay tree auquel x appartient doit être un descendant de y.
Dans ce cas, le nœud x est plus petit que y.
85
Le splay tree A dans T , qui contient y est constitué des nœuds L∪U ∪{y}.
Après le switch, il doit être constitué des nœuds R ∪ U ∪ {y}. Il faut donc
retirer L de A et y rajouter R. Cela peut se faire par l’algorithme suivant
(voir figure 9.2) :
1. splayer le nœud y pour qu’il devienne la racine de A.
2. déterminer z . Le nœud z est le plus grand nœud, qui a une information
prof plus petite que celle de y et qui est plus petit que y.
3. splayer le nœud z , jusqu’à ce qu’il devienne le fils gauche de y . Dès lors,
le sous-arbre droit de z est constitué de nœuds ayant une valeur dans
l’intervalle (z, y) et ayant une information prof plus grande que celle
de y. En effet, comme z est le plus grand nœud ayant une information
prof plus petite que celle de y , nous en déduisons que les nœuds dans
le sous-arbre droit de z ont une information prof plus grande que celle
de y . Comme l’ensemble L est constitué de nœuds ayant une valeur
plus petite que celle de y et ayant une information prof plus grande que
celle de y , nous en déduisons que le sous-arbre droit de z correspond
à L.
4. activer l’isroot bit de la racine du sous-arbre droit de z pour le séparer
de A. Cette opération retire de manière effective L de A. De cette
manière, le fils gauche de y dans P devient son fils non préféré.
5. Déterminer x. Le nœud x est le plus petit nœud, qui a une information
prof plus petite que celle de y et qui est plus grand que y.
6. splayer le nœud x, jusqu’à ce qu’il devienne le fils droit de y . Dès lors,
le sous-arbre gauche de x est constitué de nœuds ayant une valeur dans
l’intervalle (y, x) et ayant une information prof plus grande que celle de
y. En effet, comme x est le plus petit nœud ayant une information prof
plus petite que celle de y , nous en déduisons que les nœuds dans le
sous-arbre gauche de x ont une information prof plus grande que celle
de y . Comme l’ensemble R est constitué de nœuds ayant une valeur
plus grande que celle de y et ayant une profondeur plus grande que
celle de y , nous en déduisons que le sous-arbre gauche de x correspond
à R.
7. désactiver l’isroot bit de la racine du sous-arbre gauche de x, de manière
à l’incorporer dans A. De cette manière, le fils droit de y dans P devient
son fils préféré.
La souplesse des multi-splay trees par rapport à Tango est illustrée dans
les étapes 3 et 7 de l’algorithme ci-dessus. Nous pouvons activer ou désactiver
un isroot bit, c’est-à-dire retirer un sous-arbre d’un splay tree ou attacher un
86
Fig. 9.2 – Les étapes d’un switch gauche-droite sur le nœud y.
87
sous-arbre à un splay tree sans devoir réorganiser le splay tree, car après de
telles opérations l’arbre reste toujours un splay tree. Pour Tango, après de
telles opérations il faut réorganiser les arbres rouges-noirs (cfr points 8.3.4 et
8.3.5) pour qu’ils répondent à nouveau à la définition d’arbres rouges-noirs.
Deux cas particuliers doivent être considérés :
1. le nœud z n’existe pas. Dans ce cas, il n’existe pas de nœuds, qui soient
plus petits que y et qui aient une information prof plus petite que
celle de y . Le sous-arbre gauche de y est alors constitué exclusivement
de nœuds ayant une information prof plus grande que celle de y . Il
correspond donc à L.
2. le nœud x n’existe pas. Dans ce cas, il n’existe pas de nœuds, qui soient
plus grands que y et qui aient une information prof plus petite que
celle de y . Le sous-arbre droit de y est alors constitué exclusivement
de nœuds ayant une information prof plus grande que celle de y . Il
correspond donc à R.
Il ne reste plus qu’à détailler les étapes 2 et 5 de l’algorithme précédent,
c’est-à-dire comment déterminer z et x. Nous allons présenter l’algorithme
pour déterminer z ; celui pour x peut être déduit par symétrie.
Pour déterminer z , il faut trouver le plus grand nœud, qui a une information prof plus petite que celle de y et qui est plus petit que y , sachant
que y a déjà été ramené à la racine de A (étape 1 de l’algorithme précédent).
L’algorithme est alors le suivant :
1. commencer le traitement sur le fils gauche de y , pour ne considérer
que des nœuds plus petits que y.
2. analyser l’information prof du nœud courant x :
(a) si l’information prof de x est plus petite que l’information prof de
y , alors il faut analyser l’information mindepth du fils droit de x :
i. si l’information mindepth du fils droit de x est plus petite
que l’information prof de y , alors il existe dans le sous-arbre
droit de x un nœud qui est plus grand que x et qui a une
information prof plus petite que celle de y . Dès lors, x n’est
pas z et il faut, par conséquent, se déplacer sur le fils droit de
x et recommencer l’étape 2.
ii. si l’information mindepth du fils droit de x est plus grande que
l’information prof de y , alors il n’existe aucun nœud dans le
sous-arbre droit de x, qui ait une information prof plus petite
que celle de y . Par conséquent, x est le nœud z.
88
(b) si l’information prof de x est plus grande que l’information prof
de y , alors il faut analyser l’information mindepth des fils de x :
i. si l’information mindepth du fils droit de x est plus petite que
l’information prof de y , alors il faut se déplacer sur le fils
droit de x et recommencer l’étape 2.
ii. si l’information mindepth du fils gauche de x est plus petite
que l’information prof de y , alors il faut se déplacer sur le
fils gauche de x et recommencer l’étape 2.
iii. si l’information mindepth du fils gauche et du fils droit de x
est plus petite que l’information prof de y , alors il faut se
déplacer sur le fils droit de x plutôt que le fils gauche, car le
sous-arbre du fils droit de x a des clés plus grandes que celles
du sous-arbre du fils gauche de x. Nous sommes donc certains
que z se situera dans le sous-arbre droit de x et non pas dans le
sous-arbre gauche de x. Ensuite, il faut recommencer l’étape
2.
Cet algorithme n’est correct que si le nœud z existe dans A. Pour traiter
le cas où z n’existe pas, il suffit d’examiner l’information mindepth du fils
gauche de y . Si cette information est plus grande que l’information prof de
y , alors il n’existe aucun nœud plus petit que y et ayant une information
prof plus petite que celle de y . Dans ce cas, le nœud z n’existe donc pas.
Par contre, si l’information mindepth du fils gauche de y est plus petite que
l’information prof de y , alors il existe un nœud plus petit que y et ayant une
information prof plus petite que celle de y . Dans ce cas, le nœud z existe et
l’algorithme ci-dessus peut être appliqué.
Le dernier point à éclaircir est la manière de déterminer le type de switch
(gauche-droite ou droite-gauche) qu’il faut appliquer à un nœud pour lequel
cette opération doit être réalisée. Supposons que nous passons du splay tree
A au splay tree B et qu’il faut effectuer un switch sur le nœud y de B. Si la
racine de A a une valeur qui est plus grande que celle de y , alors les éléments
de A appartiennent au sous-arbre droit de y dans P (car ils sont plus grands
que y). Le switch à effectuer est alors un switch gauche-droite. Par contre, si
la valeur de la racine de A est plus petite que celle de y , alors il faut effectuer
un switch droite-gauche.
9.3
Analyse des performances
Dans cette section, nous allons démontrer que les multi-splay trees atteignent un taux de compétitivité de O(log(log n)), ainsi qu’une complexité
89
amortie de O(log n) pour l’opération de recherche. Nous allons également
montrer que la complexité au pire cas d’une recherche vaut O((log n)2 ).
Cette section est basée sur les articles [SW04] et [DSW06]. La contribution personnelle a été de détailler les démonstrations et de justifier dans les
démonstrations des points qui ne l’avaient pas été.
9.3.1
Fonction potentiel
La fonction potentiel utilisée pour analyser les multi-splay trees est la
suivante.
Soit T le multi-splay tree. Nous assignons à chaque nœud x de T un poids
arbitraire positif, noté w(x). Pour un nœud x, nous définissons sa taille s(x)
comme la somme des poids de tous les nœuds appartenant au splay subtree
de x (tous les nœuds appartenant au même splay tree que celui de x et ayant
x comme ancêtre). Le rang r(x) d’un nœud x vaut log(s(x)). Finalement,
nous définissons le potentiel d’un arbre T comme la somme des rangs de tous
ses nœuds.
En résume :
w(x) ∈ R+ , ∀x ∈ T
!
s(x) = a∈Tx w(a) , où Tx est le splay subtree de x
r(x) = log(s(x))
!
P (T ) = a∈T r(a)
Il est possible de donner une autre définition équivalente pour le potentiel d’un multi-splay tree. Pour cela, il faut considérer la fonction potentiel
définie par Sleator et Tarjan [ST85] pour un simple splay tree (cfr point
6.2.5). Comme un multi-splay tree est une collection de splay trees, il suffit
de calculer le potentiel de chacun de ces splay trees et de sommer ces potentiels pour obtenir le potentiel de T . En comparant ces deux définitions de
potentiel pour T , nous pouvons constater qu’elles sont équivalentes.
9.3.2
Access Lemma Généralisé
Soit un nœud x, la complexité pour splayer ce nœud jusqu’à un ancêtre a appartenant au même splay tree est d’au plus
3(r(a) − r(x)) + 1 = O(log(s(a)/s(x))).
90
La démonstration de l’Access Lemma Généralisé découle immédiatement
de la démonstration de l’Access Lemma pour les splay trees (cfr point 6.2.5),
parce que cette démonstration-ci ne nécessite pas que le nœud à splayer soit
remonté à la racine.
La différence entre l’Access Lemma et l’Access Lemma Généralisé est que,
pour ce dernier, le splaying peut s’arrêter à un quelconque ancêtre a de x.
9.3.3
Multi-Splay Access Lemma
Soit P un arbre de référence de racine t constitué de n nœuds et f ≥
2 un multiplicateur. Considérons une assignation de poids, qui attribue à
chaque nœud x de P un poids positif w(x) et qui satisfait les deux conditions
suivantes :
1) w(x) ≥ max w(v)
(9.1)
v∈d(x,P )
2) f · w(x) ≥ max
t∈p(x,P )
#
w(v)
(9.2)
v∈t
où p(x, P ) est l’ensemble des chemins dans P allant de x à un descendant
de x sans enfant et d(x, P ) est l’ensemble des descendants de x dans P . La
valeur du multiplicateur f n’est pas constante. Elle peut dépendre de n (le
nombre d’éléments dans l’arbre). Le fait que la valeur de f puisse être choisie
arbitrairement (mais en vérifiant les conditions ci-dessus) est intéressant, car
cela nous laisse une marge de manœuvre plus grande pour déterminer des
propriétés à partir de l’équation 9.3 et d’une assignation de poids aux nœuds.
Alors le temps d’exécution amorti d’une séquence X = x1 , x2 , . . . , xm
vaut :
"" m
$
%
&$
#
w(t)
log
O
+ (log f ) · (IB(X) + m)
(9.3)
w(xi )
i=1
Démonstration : La démonstration est composée de trois étapes :
1. calculer le coût d’un switch
2. calculer le coût d’accès à un élément
3. calculer le temps d’exécution d’une séquence
Supposons que pour l’accès xj , il y ait k switches qui soient réalisés sur les
nœuds Y = y1 , y2 , . . . , yk . L’ordre dans lequel les switches sont effectués est
yk , . . . , y2 , y1 . Notons par ti la racine du splay tree contenant yi avant l’accès
xj et par tk+1 la racine du splay tree contenant xj .
91
yi
z
x
cx
cz
Fig. 9.3 – Relation entre x, yi , z , cx et cz dans l’arbre T . Il s’agit de la
représentation de T après 3 splayings, mais avant l’activation de l’isroot bit
de cz et la désactivation de l’isroot bit de cx .
Pour calculer le coût d’un switch yi , il faut d’abord calculer la variation
de potentiel dû à ce switch. Le switch yi est composé de deux changements
d’isroot bit et d’au plus trois splayings (sur yi, x et z, où x et z sont les
nœuds comme définis pour l’algorithme des multi-splay trees au point 9.2.2).
Les changements d’isroot bit porteront sur le fils droit de z , noté cz , et
le fils gauche de x, noté cx . Ces changements d’isroot bit n’affecteront que
le potentiel des nœuds yi , x et z . Si nous notons par r(v) et s(v) le rang
et la taille d’un nœud v avant les changements d’isroot bit et par r $ (v) et
s$ (v) le rang et la taille du nœud v après les changements d’isroot bit, nous
constatons les propriétés suivantes (voir figure 9.3) pour les nœuds yi, x et
z:
1. s$ (z) = s(z) − s(cz), car l’activation de l’isroot bit de cz sépare son
sous-arbre du reste du splay tree, ce qui diminue la taille de z de s(cz).
Cette égalité sera désignée par (A).
2. s$ (x) = s(x) + s(cx), car la désactivation de l’isroot bit de cx rajoute
le sous-arbre de cx au splay tree de yi, ce qui augmente la taille de x
de s(cx). Cette égalité sera désignée par (B).
3. s$ (yi ) = s(yi ) + s(cx) − s(cz), car la taille de z diminue de s(cz) et la
taille de x augmente de s(cx). Cette égalité sera désignée par (C).
92
Pour calculer la variation de potentiel due au switch yi , il est nécessaire
de déduire encore deux résultats :
1. s(x) ≥ w(yi)
2. f ≥ s(cx)/w(yi)
Le premier résultat est déduit de la manière suivante. Au niveau de P ,
nous avons que x et yi appartiennent au même chemin préféré, car ils font
partie du même splay tree. De plus, nous avons que x a une profondeur plus
petite que celle de yi ; ceci est dû à la définition de x dans l’algorithme des
multi-splay trees (cfr point 9.2.2). De ceci, nous concluons que x est l’ancêtre
de yi dans P . Comme x est l’ancêtre de yi , nous obtenons de la condition
9.1, que w(x) ≥ w(yi). Et comme s(x) ≥ w(x), nous obtenons finalement
que s(x) ≥ w(yi).
Le deuxième résultat est déduit de la manière suivante. Par l’algorithme
des multi-splay trees, nous avons que les nœuds (en particulier cx ) dans le
sous-arbre gauche de x au niveau de T appartiennent au sous-arbre du fils
non préféré de yi dans P . Nous avons donc que yi est l’ancêtre de cx dans
P . Considérons l’ensemble C des chemins dans P , qui commencent en yi, qui
passent par cx et qui se terminent en une feuille. Nous pouvons utiliser la
condition 9.2 pour déduire que, pour un chemin quelconque appartenant à
C, la somme des poids des nœuds de ce chemin est plus petite que f · w(yi).
Or, dans T le sous-arbre gauche de x, c’est-à-dire le splay subtree de cx ,
contient des nœuds plus grands que yi et ayant des informations prof plus
grandes que celle de yi . Ces nœuds forment donc dans P un sous-chemin d’un
des chemins de l’ensemble C. Par conséquent, la somme des poids des nœuds
dans le splay subtree de cx , qui vaut s(cx), est plus petite que f · w(yi).
Nous avons donc déduit que s(cx) ≤ f · w(yi), d’où f ≥ s(cx)/w(yi).
En se basant sur ces constatations, nous pouvons calculer la variation de
potentiel dû au switch yi :
93
$P = (r $ (x) − r(x)) + (r $ (yi) − r(yi)) + (r $ (z) − r(z)) ,
car un switch modifie le potentiel des nœuds yi , x et z
< (r $ (x) − r(x)) + (r $ (yi) − r(yi)) , car r $ (z) − r(z) < 0
% $
&
% $ &
s (yi)
s (x)
+ log
, car log(A) − log(B) = log(A/B)
= log
s(x)
s(yi)
%
&
%
&
s(x) + s(cx)
s(yi) + s(cx) − s(cz)
= log
+ log
,
s(x)
s(yi )
par les égalités (A) et (B)
&
%
&
%
s(yi) + s(cx)
s(x) + s(cx)
+ log
< log
s(x)
s(yi )
&
%
&
%
s(cx)
s(cx)
+ log 1 +
= log 1 +
s(x)
s(yi )
%
&
%
&
s(cx)
s(cx)
≤ log 1 +
+ log 1 +
, car s(x) ≥ w(yi)
w(yi)
s(yi)
%
&
%
&
s(cx)
s(cx)
≤ log 1 +
+ log 1 +
,
w(yi)
w(yi)
car s(yi ) ≥ w(yi) par définition de s(yi )
%
&
s(cx)
= 2 · log 1 +
w(yi)
s(cx)
≤ 2 · log (1 + f ) , car f ≥
w(yi)
= O(log f )
Nous avons donc déduit que $P < O(log f ).
Le calcul de la complexité amortie pour le switch de yi se base sur
l’équation suivante : CAswitch = CEswitch + $P (cfr point 2.4.3), c’est-à-dire
que la complexité amortie du switch est égale à la complexité effective du
switch plus la variation de potentiel due à l’opération. La complexité effective
du switch est égale au coût pour splayer yi, x et z . Comme les changements
d’isroot bit se font en temps constant, leur coût peut être négligé . Avant de
calculer la complexité amortie du switch de yi, il est utile de prouver les trois
résultats suivants :
1. w(x) ≥ w(yi) (ce résultat sera désigné par (D))
2. w(z) ≥ w(yi) (ce résultat sera désigné par (E))
3. w(yi) ≥ s(ti+1 )/f (ce résultat sera désigné par (F ))
94
Le premier résultat a déjà été prouvé pour le calcul de $P .
Le deuxième résultat peut être prouvé de manière similaire.
Le troisième résultat est prouvé de la manière suivante. Pour rappel, lors
d’un accès xj , il y a k switches qui sont effectués sur les nœuds y1 , y2, . . . , yk
dans l’ordre yk , . . . , y2 , y1 . Et tj représente la racine du splay tree contenant
yj . Comme nous effectuons un switch sur yi+1 suivi d’un switch sur yi, cela
signifie que nous sommes passés du splay tree contenant yi+1 au splay tree
contenant yi par une arête discontinue. Au niveau de P , l’ensemble des nœuds
dans le splay tree de yi+1 forme un chemin t allant du fils non préféré de yi à
une feuille. Or, t est un sous-chemin de d(yi , P ). En se basant sur!
cela, nous
pouvons utiliser la condition 9.2 pour déduire que f · w(yi) ≥! v∈t w(v).
Comme ti+1 est la racine du splay tree de yi+1 , nous avons que v∈t w(v) =
s(ti+1 ). Finalement, nous obtenons que f · w(yi ) ≥ s(ti+1 ), d’où w(yi) ≥
s(ti+1 )/f . En se basant sur ce résultat, nous pouvons calculer la complexité
amortie d’un switch :
Cout(switch(yi )) = Cout(splay(yi)) + Cout(splay(x)) + Cout(splay(z)) + $P
% %
&&
% %
&&
% %
&&
s(ti )
s(ti )
s(ti )
< O log
+ O log
+ O log
s(yi )
s(x)
s(z)
+ O(log f ) , où s(ti ) est la taille du splay tree de yi
% %
&&
% %
&&
% %
&&
s(ti )
s(ti )
s(ti )
< O log
+ O log
+ O log
w(yi)
w(x)
w(z)
+ O(log f )
&&
% %
&&
% %
&&
% %
s(ti )
s(ti )
s(ti )
+ O log
+ O log
≤ O log
w(yi)
w(yi)
w(yi)
+ O(log f ) , par les résultats (D) et (E)
&&
% %
s(ti )
+ O(log f )
= O log
w(yi)
&&
% %
s(ti )
≤ O log
+ O(log f ) , par le résultat (F )
s(ti+1 )/f
&&
% %
s(ti )
·f
+ O(log f )
= O log
s(ti+1 )
% %
&&
s(ti )
= O log
+ O(log f ) + O(log f )
s(ti+1 )
&&
% %
s(ti )
+ O(log f )
= O log
s(ti+1 )
95
Pour calculer le coût d’accès à un nœud xj , il est nécessaire de prouver
les deux résultats suivants :
1. s(tk+1 ) ≥ w(xj ) (tk+1 est la racine du splay tree contenant xj ) (ce
résultat sera désigné par (G))
2. f · w(t) ≥ s(t1 ) (t est la racine de P ) (ce résultat sera désigné par (H))
La preuve du premier résultat découle immédiatement du fait que tk+1
est la racine du splay tree contenant xj .
Le deuxième résultat est prouvé de la manière suivante. Le nœud t1 ,
qui est la racine du splay tree contenant y1 , est également la racine de T .
Donc, le splay tree de t1 correspond au chemin C dans P allant de t à
une feuille en
9.2, nous avons que
!suivant les fils préférés. Par la condition
!
f · w(t) ≥
v∈C w(v) (car C ∈ p(t, P )). Or,
v∈C w(v) = s(t1 ), ce qui
implique que f · w(t) ≥ s(t1 ).
L’accès au nœud xj consiste à effectuer k switches et à splayer xj . Le coût
d’accès à xj est alors calculé comme suit :
96
Cout(Acces(xj )) =
k
#
Cout(switch(yi)) + Cout(splay(xj ))
i=1
k
#
% %
% %
&& #
&&
k
s(ti )
s(t1 )
=
O log
O(log f ) + O log
+
s(t
)
s(xj )
i+1
i=1
i=1
% %
% %
&& #
&&
k
s(t1 )
s(t1 )
O(log f ) + O log
= O log
+
,
s(tk+1)
s(x
)
j
i=1
=
≤
≤
=
=
=
la simplification du premier terme découle du fait que
log(A) + log(B) = log(A · B)
&&
% %
&&
% %
s(t1 )
s(t1 )
+ O(k · log f ) + O log
O log
s(tk+1)
s(xj )
% %
&&
% %
&&
s(t1 )
s(t1 )
O log
+ O(k · log f ) + O log
,
w(xj )
w(xj )
par le résultat (G)
% %
&&
% %
&&
f · w(t)
f · w(t)
O log
+ O(k · log f ) + O log
,
w(xj )
s(xj )
par le résultat (H)
% %
&&
f · w(t)
O log
+ O(k · log f ),
w(xj )
&&
% %
w(t)
+ O(k · log f ),
O(log f ) + O log
w(xj )
&&
% %
w(t)
+ O((k + 1) · (log f )),
O log
w(xj )
Le nombre k de switches effectués lors de l’accès xj correspond au nombre
de nœuds dans P , dont les fils préférés changent à cause de cet accès. Durant
l’analyse des performances de Tango, il a été démontré que ce nombre k est
égal à la borne d’entrelacement IBj (X) de l’accès xj (cfr point 8.5).
Le coût nécessaire à un multi-splay tree pour exécuter une séquence X =
x1 , x2 , . . . , xm vaut :
97
Cout(executer(X)) =
=
m
#
i=1
m
#
i=1
=
m
#
i=1
=
m
#
i=1
=
m
#
i=1
car
= O
Cout(acces(xi ))
%
O log
%
O log
%
O log
%
O log
m
#
%
%
%
%
w(t)
w(xi )
w(t)
w(xi )
w(t)
w(xi )
w(t)
w(xi )
&&
&&
&&
&&
+
m
#
i=1
+O
"
O((IBi(X) + 1) · (log f ))
m
#
" i=1
(IBi (X) + 1) · (log f )
+ O (log f ) ·
m
#
(IBi (X) + 1)
i=1
+ O ((log f ) · (IB(X) + m)) ,
IBi (X) = IB(X)
""i=1m
#
log
i=1
%
w(t)
w(xi )
&$
+ (log f ) · (IB(X) + m)
$
!
9.3.4
Propriétés
En se basant sur le multi-splay tree access lemma, il est possible de définir
trois propriétés sur les multi-splay trees.
Théorème 8. Le multi-splay tree est O(log(log n))-compétitif.
Démonstration : Soit P un arbre de référence à n nœuds. La profondeur
de P vaut (log n) + 1. On pose w(v) = 1 pour tout nœud v de P et on choisit
f = 2 · log n.
La condition 9.1 est vérifiée, car le poids maximal d’un ensemble quelconque de nœuds vaut 1.
La condition 9.2 est vérifiée, car la longueur maximale d’un chemin vaut
(log n) + 1, ce qui est bien inférieur à f .
Avec l’assignation de poids choisie, le multi-splay tree access lemma vaut :
"" m
$
% &$
#
1
log
O
+ (log(2 · log n)) · (IB(X) + m)
(9.4)
1
i=1
98
$
$
Comme O(log(2 · log n) = O((log 2) + (log(log n))) = O(log(log n)), nous
obtenons finalement :
O((log(log n)) · OP T (X))
(9.5)
!
Théorème 9. Un multi-splay tree de n nœuds a une complexité amortie de
O(log n) pour un accès.
Démonstration : Soient P un arbre de référence à n nœuds et une
séquence d’accès X = x1 , x2 , . . . , xm . La profondeur de P vaut (log n) + 1.
Soit h(v), la hauteur d’un nœud v (la hauteur d’une feuille vaut 0 ; cfr point
2.1.3). On pose w(v) = 2h(v) pour tout nœud v de P et on choisit f = 2 .
Notons que le poids de la racine t de P vaut :
w(t) = 2h(t) = 2(log n)+1 = 2log n · 2 = n · 2 = O(n)
(9.6)
La condition 9.1 est vérifiée, car un descendant a une hauteur plus petite
que celle de son ancêtre.
!
j
La condition 9.2 est vérifiée, car 2i ≥ i−1
j=0 2 .
Avec l’assignation de poids choisie, le multi-splay tree access lemma vaut :
Cout(executer(X)) ≤ O
≤ O
""
m
#
"" i=1
m
#
log
%
n
w(xi )
$
log (n)
i=1
&$
+ (log 2) · (IB(X) + m)
+ (IB(X) + m)
$
$
,
car ∀ 1 ≤ i ≤ m : w(xi ) ≥ 1
= O(m · log n + OP T (X))
= O(m · log n) , car OP T (X) ≤ m · log n
!
Théorème 10. Pour une séquence d’accès X, le coût nécessaire à un multisplay tree pour traiter X vaut :
O(min (log(log n) · OP T (X) , m · log n))
99
(9.7)
Démonstration : Ce résultat découle des deux théorèmes précédents (les
théorèmes 8 et 9).
!
Nous avons encore deux propriétés concernant les multi-splay trees.
Théorème 11. Un multi-splay tree de n nœuds a une complexité au pire cas
de O((log n)2 ) pour un accès xi .
Démonstration : Comme la hauteur de l’arbre de référence vaut log n,
nous avons que le nombre de switches pour un accès xi vaut au pire cas
log n. De plus, comme chaque splay tree contient au plus log n éléments,
nous avons que la complexité au pire cas pour une opération sur un splay
tree vaut log n (car la complexité au pire cas d’une opération sur un splay
tree est linéaire sur la taille de l’arbre). Ainsi, le coût pour effectuer un switch
sur un splay tree vaut O(log n) au pire cas, car on effectue 3 splayings pour
réaliser cette opération. Par conséquent, le coût pour accéder au nœud xi
vaut O((log n) · (log n)) = O((log n)2 ).
!
Théorème 12. Soient un arbre à n nœuds et une séquence X consistant à
accéder à ces nœuds dans l’ordre symétrique. Le coût pour traiter X vaut :
O(n)
(9.8)
Ce théorème ne sera pas démontré ici, mais une preuve est disponible
dans l’article [DSW06].
9.4
Conclusion
Nous venons de présenter une structure qui atteint un taux de
compétitivité de O(log(log n)), mais qui est plus simple que l’algorithme
Tango. Nous avons également montré qu’un accès se fait avec un coût au
pire cas de O((log n)2 ) et avec un cout amorti de O(log n).
Nous avons également constaté que cette structure présente des similarités
avec Tango. En fait, le principe fondamental est le même, à savoir qu’il
faut effectuer des switches après chaque accès pour changer le fils préféré
de certains nœuds. Seul diffère la manière d’effectuer les switches, car un
multi-splay tree est constitué de splay trees et Tango est constitué d’arbres
rouges-noirs. Nous avons également constaté que cette différence fournit une
plus grande souplesse aux multi-splay trees.
100
Grâce à cette souplesse, il est possible d’étendre cette structure pour
qu’elle puisse supporter les opérations d’insertion et de suppression tout en
gardant au taux de compétitivité de O(log(log n)).
Comme les multi-splay trees présentent des similitudes avec les splay
trees, nous pouvons nous demander si les multi-splay trees possèdent
également les propriétés d’optimalité statique, de static finger, de working set et de dynamic finger [DSW06]. De par cette similitude, un autre
problème, que nous pourrions nous poser, serait de savoir si les splay trees
sont O(log(log n)) − compétitif s [DSW06].
Nous avons montré que les multi-splay trees et Tango ont un taux de
compétitivité de O(log(log n)), mais il se peut que ces deux structures atteignent l’optimalité dynamique [DSW06]. Pour prouver cela, il faudrait un
borne inférieure sur OP T (X) qui soit meilleure que la borne inférieure d’entrelacement, mais le grand problème est que de telles bornes sont rares. Ce
manque de bornes inférieures est l’un des principaux obstacles pour avoir une
structure optimale dynamiquement.
101
Chapitre 10
Optimalité de recherche
dynamique
La rédaction de ce chapitre est basée sur l’article de Blum, Chawla et
Kalai [BCK03].
Dans cette section, nous allons montrer qu’il existe une structure, qui
atteint l’optimalité dynamique, si nous ne tenons pas compte du coût des
rotations qu’elle effectue après chaque accès.
10.1
Définition
Dans le modèle dynamique (cfr point 2.2.1) des ABR que nous
considérons, le coût d’accès à un élément xi est égal au coût de localisation de xi plus le coût des rotations effectuées après avoir atteint xi . Une
structure a la propriété d’optimalité de recherche dynamique si, pour une
quelconque séquence X, le coût nécessaire à la localisation des éléments de
X est égal à O(OP T (X)), où OP T (X) est le coût d’accès aux éléments de X
pour l’algorithme offline optimal. En d’autres termes, une structure a l’optimalité de recherche dynamique si son coût pour traiter une séquence (sans
compter les rotations) est égal au coût de l’algorithme offline optimal.
10.2
Intérêt
Un argument en défaveur de l’existence d’un algorithme online optimal
serait le suivant. Pour qu’un algorithme soit optimal, il faut qu’il puisse
garder près de la racine les nœuds qui doivent encore être accédés. Mais le
problème d’un algorithme online est qu’il ne peut pas deviner les accès à
venir et que par conséquent il a trop de nœuds à maintenir près de la racine.
102
L’intérêt de la propriété d’optimalité de recherche dynamique est de rejeter
cet argument, car elle montre qu’il est possible pour une structure online de
garder les nœuds à accéder suffisamment proches de la racine pour avoir un
coût optimal.
10.3
Structure
Ici, le but est de prouver qu’il existe une structure ABR online qui atteint
l’optimalité de recherche dynamique. L’élaboration de la structure se fait à
l’aide de distributions de probabilités sur les séquences d’accès. Nous verrons
qu’il est possible de transformer une distribution de probabilités en un arbre
binaire de recherche et un arbre binaire de recherche en une distribution
de probabilités. Ces transformations sont telles que les nœuds proches de
la racine sont ceux dont la probabilité d’accès est la plus grande. Avant de
présenter la structure, il est nécessaire de présenter quelques résultats.
Toutes les démonstrations présentées ci-dessous sont basées sur l’article [BCK03]. La contribution personnelle a consisté à détailler ces
démonstrations et à justifier dans ces démonstrations des points qui ne
l’avaient pas été.
Théorème 13. Le nombre de séquences d’accès pouvant être exécutées avec
un coût optimal k est de tout au plus 212k , pour tout k ≥ 0.
Ce théorème n’est pas démontré ici, mais une preuve peut être consultée
dans [BCK03].
Théorème 14. Il existe une distribution de probabilités sur les séquences
d’accès, qui assigne une probabilité d’au plus 2−13k à une séquence d’accès
dont le coût optimal est k.
Démonstration : On choisit le nombre k dans l’intervalle [1, ∞] selon la
distribution 1/2k . Ensuite, on choisit une séquence d’accès X parmi toutes les
séquences dont le coût optimal est k. Comme il existe au plus 212k séquences
de coût optimal k, la probabilité de choisir X parmi toutes les séquences de
coût k est de 2−12k . Finalement, la probabilité de choisir X vaut au plus :
1
1
1
·
=
2k 212k
213k
(10.1)
!
Théorème 15. Un arbre binaire de recherche T peut être transformé en une
distribution de probabilités p telle que pour tout nœud j appartenant à T :
103
p(j) ≥ 3−prof ondeur(j)
(10.2)
Démonstration : À chaque nœud j de T , on associe une probabilité p(j)
qui est égale à la probabilité d’atteindre j si on commence la recherche à la
racine. Cette probabilité est déterminée de la manière suivante :
1. Commencer à la racine de l’arbre.
2. Soit x le nœud courant :
(a) Si x a deux fils, alors la probabilité d’aller à gauche vaut 1/3, celle
d’aller à droite vaut 1/3 et celle de s’arrêter vaut également 1/3.
(b) Si x a un fils (disons le fils gauche), alors la probabilité d’aller à
gauche vaut 1/2 et celle de s’arrêter vaut également 1/2. Le cas
du fils droit est similaire.
(c) Si x n’a pas de fils, alors la probabilité de s’arrêter vaut 1.
Ce traitement est à itérer jusqu’au nœud j.
La probabilité p(j) correspond donc à la probabilité d’atteindre le nœud
j et de s’y arrêter.
De cette construction, nous déduisons immédiatement que p(j) ≥
−prof ondeur(j)
3
.
!
Il ne reste plus qu’à démontrer que j∈T p(j) = 1, de manière à vérifier
que p soit bien une distribution de probabilités. Ceci peut se faire par
récurrence sur la hauteur h(T ) de T :
1. Cas de base : la hauteur de T vaut 0. Dans ce cas, l’arbre est constitué
d’un seul nœud dont la probabilité associée vaut 1.
!
2. Cas d’induction : considérons que i∈T p(i) = 1 pour un arbre T de
hauteur i et montrons que c’est également le cas pour un arbre de
hauteur i + 1.
Soit T un arbre de hauteur i+1 et de racine t. Deux cas sont possibles :
soit t a deux fils, soit il n’en a qu’un seul. Traitons le premier cas (le
deuxième cas peut être résolu de manière similaire).
La probabilité de s’arrêter en t vaut 1/3, celle d’aller dans le sous-arbre
gauche de t vaut 1/3 et celle d’aller dans le sous-arbre droit de t vaut
1/3. Comme le sous-arbre gauche de t a une hauteur i, nous pouvons
y appliquer l’hypothèse d’induction. Mais comme ce sous-arbre est attaché à la racine de t comme fils gauche, nous avons que la probabilité
de chaque nœud du sous-arbre doit être multipliée par 1/3. On obtient
alors que :
104
#
i∈SAG(t)
p(i) = 1 ·
1
1
=
3
3
(10.3)
Finalement, nous avons que :
#
i∈T

#
p(i) = 
i∈SAG(t)


p(i) + 
1 1 1
+ +
3 3 3
= 1
=
#
i∈SAD(t)

p(i) + p(t)
!
Théorème 16. Pour une distribution de probabilités sur n accès, nous pouvons créer un arbre binaire de recherche T tel que pour tout nœud a de T :
prof ondeur(a) ≤ 1 − log(p(a))
(10.4)
Démonstration : La construction de T se fait de!manière récursive.
i−1
Pour
j=1 p(j) ≤ 1/2 et
!n la racine de T , on choisit le premier i tel que
j=i+1 p(j) ≤ 1/2. Ensuite, on travaille de manière récursive sur les éléments
strictement plus petits que i pour construire le sous-arbre
gauche de i. Pour
!i−1
cela, il faut normaliser p, de manière à ce que j=1 p(j) = 1. Après cela,
il faut effectuer un travail similaire sur les éléments strictement plus grands
que i pour construire le sous-arbre droit de i.
De la manière dont on construit T , nous constatons que pour tout nœud
i de T , la somme des probabilités des nœuds appartenant au sous-arbre de i
est d’au plus 1/2d−1 , où d est la profondeur de i.
Grâce à cette observation, nous pouvons montrer que la profondeur d’un
nœud i de probabilité p(i) ne peut pas être plus grande que − log(p(i)). En
effet, supposons que le nœud i de profondeur d soit tel que d−1 > − log(p(i))
(dans ce cas d est plus grand que − log(p(i))). Nous en déduisons que :
1
2d−1
<
1
2− log(p(i))
= 2log(p(i))
= p(i)
105
Et donc, 1/2d−1 < p(i). Nous savons également que p(Ti ) ≤ 1/2d−1, où Ti
est le sous-arbre de i. En combinant ces deux résultats, nous obtenons que :
p(Ti ) ≤
1
2d−1
< p(i)
(10.5)
Or, ceci est impossible, car p(Ti ) ≥ p(i). En faisant l’hypothèse que d−1 >
− log(p(i)), nous sommes arrivés à une contradiction. On en déduit donc que :
d − 1 ≤ − log(p(i))
d ≤ 1 − log(p(i))
!
Théorème 17. Pour une quelconque distribution de probabilités p sur une
séquence d’accès, nous pouvons créer un algorithme online dont le coût pour
traiter la séquence vaut tout au plus m−log(p(x1 x2 . . . xm )) et cela quelle que
soit la séquence x1 , x2 , . . . , xm . Nous notons par p(x1 x2 . . . xm ) la probabilité
d’avoir x1 , x2 , . . . , xm comme premier, deuxième, . . . , mème accès.
Démonstration : Considérons les distributions de probabilités
p1 , p2 , . . . , pm . Nous définissons la probabilité pi (xj ) comme la probabilité p d’avoir xj comme ième accès sachant que x1 était le premier accès, x2
était le deuxième accès, et ainsi de suite jusque xi−1 le (i − 1)ème accès. La
probabilité pi (xj ) est définie en termes de probabilités conditionnelles de la
manière suivante :
pi (xj ) = p(xj |x1 x2 . . . xi−1 )
(10.6)
où p(xj |x1 x2 . . . xi−1 ) est la probabilité d’avoir xj comme ième accès sachant que x1 , x2 , . . . , xi−1 étaient respectivement le premier, deuxième, . . . ,
(i − 1)ème accès.
Par définition des probabilités conditionnelles, nous avons que :
p(A ∩ B)
p(A)
En se basant sur ce résultat, nous obtenons que :
p(B |A) =
p(xj ∩ (x1 x2 . . . xi−1 )
p(x1 x2 . . . xi−1 )
p(x1 x2 . . . xi−1 xj )
=
p(x1 x2 . . . xi−1 )
p(xj |x1 x2 . . . xi−1 ) =
106
(10.7)
Pour la probabilité p(x1 x2 . . . xi−1 xj ), nous n’avons aucune restriction
quant à la valeur que peut prendre le (i + 1)ème , (i + 2)ème , . . . , mème accès.
Nous avons donc que :
p(xj |x1 x2 . . . xi−1 ) =
!xm
!xm
bi+1 =x1 . . .
bm =x1 p(x1 . . . xi−1 xj bi+1 . . . bm )
! xm
!
xm
bi =x1 . . .
bm =x1 p(x1 . . . xi−1 bi . . . bm )
(10.8)
La valeur de pi (xj ) peut alors être calculée algorithmiquement grâce au
résultat ci-dessus.
Soit le lemme suivant :
p(A1 ∩ A2 ∩ . . . ∩ An ) = p(A1 ) · p(A2 |A1 ) . . . p(An |A1 ∩ . . . ∩ An−1 ) (10.9)
En se basant sur ce lemme, nous avons que :
p(x1 x2 . . . xm ) = p(x1 ) · p(x2 |x1 ) . . . p(xm |x1 . . . xm−1 )
= p1 (x1 ) · p2 (x2 ) . . . pm (xm )
m
0
pi (xi )
=
i=1
L’algorithme online procède de la manière suivante. Pour le ième accès,
on calcule la distribution de probabilités pi grâce à la formule 10.8. Ensuite,
on convertit la distribution de probabilités pi en un ABR en utilisant le
théorème 16. De par cette construction, la profondeur d’un accès xi est d’au
plus 1 − log(pi (xi )). Le coût des m accès (en ne tenant compte que du coût
pour localiser les éléments) est d’au plus :
m
#
i=1
(1 − log(pi (xi ))) = m − log(p(x1 . . . xm ))
(10.10)
!
Théorème 18. Il existe un algorithme online, qui a la propriété d’optimalité
de recherche dynamique.
Démonstration : Considérons la distribution de probabilités du théorème
14, qui assigne une probabilité de 2−13k à une séquence d’accès dont le coût
optimal est k. Combinons cette distribution de probabilités avec le théorème
17 pour obtenir un algorithme online, dont le coût d’accès à une séquence
107
X = x1 , . . . , xm (en ne tenant compte que du coût pour localiser les éléments)
est d’au plus :
(
'
m − log 2−13·OP T (x1...xm ) = m + 13 · OP T (x1 . . . xm )
≤ 14 · OP T (x1 . . . xm ),
car m ≤ OP T (x1 . . . xm )
L’algorithme online nécessite un coût d’au plus 14 fois le coût optimal
pour localiser les éléments d’une séquence d’accès. Cet algorithme a donc la
propriété d’optimalité de recherche dynamique.
!
10.4
Conclusion
Dans cette section, nous avons présenté une structure qui atteint l’optimalité de recherche dynamique. L’existence d’une telle structure a un grand
intérêt, parce qu’il s’agit d’un argument en faveur de l’existence d’une structure atteignant l’optimalité dynamique.
Un problème ouvert concernant l’optimalité de recherche dynamique
[BCK03] serait d’élaborer une structure efficace, qui puisse atteindre cette
propriété.
108
Chapitre 11
Expérimentations
Dans ce chapitre, nous allons exécuter un certain nombre de jeux de tests
pour analyser et comparer les performances en pratique des arbres rougesnoirs, des splay trees et des multi-splay trees. Le but est de surtout constater
si les multi-splay trees fournissent des résultats intéressants en pratique et si
l’algorithme des multi-splay trees, qui est plus complexe que celui des arbres
rouges-noirs et des splay trees, ne porte pas préjudice aux performances en
pratique des multi-splay trees.
Nous allons d’abord expliquer le type de jeux de tests qui seront exécutés
et ensuite, nous allons présenter les résultats de ces jeux de tests pour essayer
d’en tirer des constatations.
11.1
Jeux de tests
Nous avons effectué plusieurs tests en considérant des tailles différentes
pour la structure. Nous faisons varier la taille de la structure de 28 à 216
en multipliant à chaque pas la taille de la structure par 2. Pour chaque
taille n, nous faisons varier la taille de la séquence de n à 10 · n par pas de
log n. Pour chaque séquence, nous calculons le coût nécessaire à la structure
pour l’exécuter en prenant comme modèle de coût celui défini par le modèle
dynamique (cfr point 2.2.1). Nous considérons deux types de distribution
pour générer une séquence d’accès :
1. distribution aléatoire
2. distribution working set
109
80000
MST8
ST8
RB8
70000
60000
50000
40000
30000
20000
10000
0
0
500
1000
1500
2000
2500
Fig. 11.1 – Coûts d’un arbre de taille 28 avec une distribution aléatoire
11.2
Distribution aléatoire
Supposons que la taille de l’arbre soit égale à n. Une séquence d’accès
est générée en choisissant de manière aléatoire les éléments qui la composent
parmi les n éléments possibles.
Les résultats de ces tests sont illustrés par les graphes des figures 11.1 et
11.2 (d’autres graphes sont fournis en annexe). Ces résultats nous permettent
de constater que le coût des arbres rouges-noirs est plus petit que celui des
splay trees ; lui-même plus petit que celui des multi-splay trees. Ceci est
vérifié sur chacun des graphiques cités ci-dessus. De plus , nous constatons sur
chaque graphique que la différence de coût entre les trois structures s’amplifie
à mesure que la taille de la séquence augmente.
Dès lors, au vue de ces résultats, on peut penser que les arbres rouges-noirs
ont un meilleur comportement que les splay trees et que cette structure a un
meilleur comportement que les multi-splay trees pour le type de distribution
que nous considérons dans cette section. Le fait que les multi-splay trees aient
de moins bonnes performances s’expliquent par la complexité de l’algorithme
de cette structure et par le grand nombre d’opérations de réorganisation
qu’elle nécessite.
110
3000
5e+07
MST16
ST16
RB16
4.5e+07
4e+07
3.5e+07
3e+07
2.5e+07
2e+07
1.5e+07
1e+07
5e+06
0
0
100000
200000
300000
400000
500000
600000
Fig. 11.2 – Coûts d’un arbre de taille 216 avec une distribution aléatoire
11.3
Distribution working set
Supposons que la taille de la structure soit égale à n. Une séquence d’accès
est générée en choisissant de manière aléatoire les éléments qui la composent
parmi un sous-ensemble de l’ensemble {1, . . . , n}. Ce sous-ensemble a une
taille de n/5 et correspond à l’ensemble {n/2 − n/10, . . . , n/2 + n/10}. Le
but est de tirer profit de la propriété working set des splay trees pour analyser
son comportement dans ce cas.
Les résultats de ces tests sont illustrés par les graphes des figures 11.3 et
11.4 (d’autres graphes sont fournis en annexe). Si nous analysons le comportement des splay trees, nous remarquons qu’au début il est similaire à leur
comportement pour le premier type de distribution analysé. Ensuite, le comportement diffère , car nous assistons à une diminution du coût d’exécution
des séquences d’accès. Cette diminution s’explique par le fait que les splay
trees ont la propriété working set. En effet, comme nous n’accédons qu’à
un sous-ensemble des n éléments possibles, nous avons que de plus en plus
d’éléments de ce sous-ensemble ont tendance à se retrouver près de la racine,
ce qui implique que nous avons de plus en plus d’accès à faible coût. Et cela
se traduit par une diminution du coût total de la séquence d’accès. Après la
phase de décroissance, nous avons une phase où le coût augmente, mais de
111
700000
façon légère. Cette augmentation s’explique par le fait que tous les éléments
du sous-ensemble des éléments accédés ont été ramenés près de la racine,
ce qui implique que nous ne pouvons plus espérer une amélioration du coût
d’exécution des séquences d’accès. Dans cette phase, l’augmentation du coût
est légère, car les éléments accédés sont proches de la racine, ce qui implique
que leur coût d’accès est faible.
Le comportement des arbres rouges-noirs diffère peu de leur comportement pour le premier type de distribution analysé, parce que les arbres
rouges-noirs n’ont pas la propriété working set et qu’ils ne sont pas capables
de s’adapter à une séquence d’accès.
En ce qui concerne les multi-splay trees, nous constatons que leur comportement présente des analogies avec celui des splay trees, ce qui pourrait laisser
suggérer que cette structure a la propriété working set. Ceci est à prendre
avec précaution, car nous tirons une constatation sur quelques résultats, mais
il n’existe aucune preuve qui le démontre. Dans la dernière phase de croissance, nous constatons que le coût des multi-splay trees a tendance à croı̂tre
plus vite que celui des splay trees. Ceci s’explique par le fait que l’algorithme
des multi-splay trees exige plus d’opérations de réorganisation que celui des
splay trees. Nous pouvons également constater que le coût des multi-splay
trees est plus petit que celui des arbres rouges-noirs.
Au début de chaque graphique les arbres rouges-noirs ont un coût qui
est meilleur que celui des splay trees et cette structure a un coût qui est
meilleur que celui des multi-splay trees. Mais de par le type de distribution
sur laquelle nous travaillons, la tendance s’inverse rapidement. Les splay trees
ont alors un coût meilleur que celui des multi-splay trees et ceux-ci ont un
coût meilleur que celui des arbres rouges-noirs.
11.4
Conclusion
Les différents jeux de tests exécutés nous ont permis de constater que les
multi-splay trees souffrent de la complexité de leur algorithme et du grand
nombre d’opérations de réorganisation qu’il nécessite.
Nous avons également obtenu des résultats qui pourraient laisser suggérer
que les multi-splay trees ont la propriété working set.
Les tests effectués sur le deuxième type de distribution illustrent bien la
capacité des splay trees à s’adapter à une séquence d’accès et à l’incapacité
des arbres rouges-noirs à faire de même.
112
40000
MST8
ST8
RB8
35000
30000
25000
20000
15000
10000
5000
0
0
500
1000
1500
2000
2500
3000
Fig. 11.3 – Coûts d’un arbre de taille 28 avec une distribution working set
2.2e+07
MST16
ST16
RB16
2e+07
1.8e+07
1.6e+07
1.4e+07
1.2e+07
1e+07
8e+06
6e+06
4e+06
2e+06
0
0
100000
200000
300000
400000
500000
600000
Fig. 11.4 – Coûts d’un arbre de taille 216 avec une distribution working set
113
700000
Chapitre 12
Conclusion
Nous avons étudié comme première structure les ABRO et nous avons
détaillé l’algorithme qui permet à cette structure d’atteindre l’optimalité statique en se basant sur la connaissance préalable de la distribution des clés à
accéder.
Puis, nous avons étudié les arbres rouges-noirs et les splay trees. En analysant les performances de ces structures, nous avons constaté que les arbres
rouges-noirs fournissent des résultats intéressants sur un accès, alors que les
splay trees ont besoin de séquences d’accès plus grandes pour fournir de
bonnes performances. Nous avons également constaté que les arbres rougesnoirs ne sont pas capables de s’adapter aux particularités d’une séquence
d’accès, alors que les splay trees y parviennent. C’est cette qualité qui fait la
force des splay trees. Et enfin, nous avons constaté que les arbres rouges-noirs
sont O(log n) − compétitif s, alors que les splay trees sont conjecturés être
O(1) − compétitif s.
Ensuite, nous avons énoncé et démontré trois bornes inférieures sur le
coût d’exécution d’une séquence d’accès dans des ABR. L’intérêt de telles
bornes est grand, car elles permettent de développer de nouvelles structures.
À l’heure actuelle, le principal obstacle pour développer une structure qui
puisse atteindre l’optimalité dynamique, est le manque de bornes inférieures
de qualité.
La structure Tango a été développée sur base de l’une des trois bornes
étudiées : la borne inférieure d’entrelacement. Cette structure est la première
qui ait amélioré le taux trivial de compétitivité de O(log n). Cette structure
atteint un taux de O(log(log n)). Le problème avec cette structure est qu’elle
est complexe, ce qui fait que son implémentation est assez lourde.
Les multi-splay trees sont une structure de données ABR. Cette structure est basée sur les mêmes principes que Tango et elle atteint également
un taux de compétitivité de O(log(log n)). Cette structure est plus simple
114
à implémenter que Tango. De plus, elle peut être étendue pour supporter les opérations d’insertion et de suppression tout en gardant le taux de
compétitivité de O(log(log n)).
Ensuite, nous avons étudié une structure, qui atteint l’optimalité de recherche dynamique. Nous avons également montré que l’existence d’une telle
structure est importante, car il s’agit d’un argument en faveur de l’existence
d’une structure optimale dynamiquement.
Et enfin, nous avons exécuté des jeux de tests sur les arbres rouges-noirs,
les splay trees et les multi-splay trees. Le but de ces tests était d’analyser les
performances en pratique de ces structures. Cela nous a permis de constater
que les arbres rouges-noirs sont performants sur des séquences d’accès, où
les nœuds à accéder sont choisis de manière aléatoire parmi l’ensemble des
nœuds de l’arbre. Nous avons également remarqué que les splay trees sont
performants sur des séquences d’accès, où les nœuds à accéder sont choisis
de manière aléatoire parmi un sous-ensemble de l’ensemble des nœuds de
l’arbre. Ceci est dû au fait que les splay trees ont la propriété working set.
L’analyse des performances des multi-splay trees sur ce type de séquences
peut nous laisser suggérer que cette structure a également la propriété working set. Concernant les multi-splay trees, nous avons également constaté que
l’algorithme complexe de cette structure a une incidence sur ses performances
en pratique.
Ce mémoire a consisté en un travail de recherche bibliographique et il
a consisté à synthétiser les grandes avancées dans le domaine des arbres
binaires de recherche et de l’optimalité dynamique. En général, les articles
étudiés présentent les algorithmes et les démonstrations de manière assez
succincte. Un objectif de ce mémoire a donc été de présenter ces algorithmes
et ces démonstrations de manière didactique en les décortiquant et en les
détaillant le plus possible.
115
Chapitre 13
Annexe
13.1
Implémentation
De manière à analyser et comparer les performances en pratique des
multi-splay trees, des splay trees et des arbres rouges-noirs, nous avons
implémenté l’algorithme des multi-splay trees et des splay trees. Pour l’algorithme des arbres rouges-noirs, nous avons récupéré une implémentation sur
le net [Nie05]. Ces trois implémentations sont fournies en annexe.
Le but n’est pas d’avoir une implémentation de ces structures qui puissent
faire partie d’une librairie en implémentant par exemple un type générique
pour les clés. Le but est de simplement avoir une implémentation permettant
d’exécuter des jeux de tests.
13.1.1
Multi-splay trees
Les clés sont des éléments de type entier et il faut que la taille de l’arbre
soit égale à n = 2k −1 (∀ k > 1). Les valeurs des clés correspond à l’ensemble
{1, . . . , n}.
Pour l’implémentation de la structure, nous utilisons deux classes :
1. elem : cette classe représente un nœud de l’arbre, ainsi que ses différents
champs.
2. splaytree : cette classe représente le multi-splay tree.
Nous détaillons ci-dessous les points principaux de l’implémentation. Des
explications plus détaillées sont fournies en tant que commentaires dans le
code.
L’initialisation de la structure se fait à l’aide de la méthode construireP,
qui est appelée par le constructeur paramétré de la classe splaytree. Dans cette
méthode, nous construisons un arbre parfait de n nœuds et nous initialisons
116
le fils préféré de chaque nœud à son fils gauche. Durant cette construction
l’isroot bit du fils gauche et du fils droit de chaque nœud est initialisé, respectivement, à false et true (car pour chaque nœud, son fils droit est son fils
non préféré). De plus, le champ mindepth de chaque nœud x est initialisé à
sa profondeur dans l’arbre parfait (car tous les nœuds dans le splay subtree
de x ont une profondeur plus grande que celle de x).
L’arbre parfait ainsi construit est, en fait, également un multi-splay tree
et il est utilisé comme état initial de la structure. L’avantage d’une telle
construction est qu’elle permet d’initialiser la structure avec un coût O(n).
La méthode recherche localise d’abord le nœud i passé en paramètre et
réorganise ensuite la structure de manière à avoir un chemin préféré allant de
la racine du multi-splay tree au nœud passé en paramètre. La réorganisation
de la structure est effectuée dans la boucle principale de la méthode. Le
traitement de cette boucle est le suivant. Si le nœud courant est la racine
d’un splay tree, alors il faut rechercher dans le splay tree de son père le nœud
sur lequel effectuer un switch. Un fois ce nœud déterminé, on applique soit
un switch droite-gauche (méthode switchDG), soit un switch gauche-droite
(méthode switchGD). Et on recommence la boucle jusqu’à ce que la racine
de l’arbre soit atteinte. Et lorsqu’elle est atteinte, on splaye le nœud i pour
le ramener à la racine de l’arbre (méthode switchracine).
Les méthodes switchGD et switchDG ne sont qu’une implémentation des
algorithmes décrits dans le chapitre 9 pour effectuer un switch.
L’implémentation est fournie ci-dessous.
# include <iostream>
# include <limits.h>
# include <time.h>
# include <math.h>
# include <iomanip>
# include <stdlib.h>
# include <unistd.h>
# include <sys/types.h>
# include <sys/stat.h>
# include <fcntl.h>
using namespace std ;
class splaytree
{
private :
class elem
{
117
friend class splaytree ;
int info ;// clé du noeud
int profondeur ;// profondeur du noeud dans
//l’arbre de référence
int mindepth ;// profondeur minimum dans le
//splay subtree du noeud
bool isroot ;
elem ∗ fg ;
elem ∗ fd ;
elem ∗ pere ;
elem(){}
elem(int i, int prof=0 , int mind=1, bool
isr=false, elem ∗ g = NULL, elem ∗ d = NULL, elem ∗ p =
NULL) :info(i),profondeur(prof), mindepth(mind), isroot(isr), fg(g), fd(d),
pere(p) {}
};
elem ∗ rac ;// racine du multi-splay tree
void rotationsimple(elem ∗,bool &) ;
void zigzag(elem ∗,bool &) ;
void zigzig(elem ∗,bool &) ;
void splayingracine(elem ∗) ;
void splayingto(elem ∗, elem ∗) ;
elem∗ construireP(int,int,double,int, bool,elem∗) ;
elem ∗ localisation(int) ;
elem∗ determinerZ(elem ∗) ;
elem∗ determinerX(elem ∗) ;
void switchGD(elem ∗) ;
void switchDG(elem ∗) ;
return(b) ;}
void infixe(elem ∗) ;
void destructeur(elem ∗) ;
int min(int a, int b) {if(a < b) return(a) ; else
public :
splaytree(){rac=NULL ;}
splaytree(int) ;
elem ∗ recherche(int) ; ;
118
};
∼splaytree() ;
splaytree : :splaytree(int n)
//Constructeur paramétré du multi-splay tree
{
elem ∗ p = construireP((n/2)+1,1,(log(n+1.0)/log(2.0)),0, true,
NULL) ;
rac=p ;
}
splaytree : :elem∗ splaytree : :construireP(int taille, int prof, double stop,
int biais, bool isroot, elem ∗ parent)
// Construit l’état initial du multi-splay tree
{
if(prof <= stop)
{
elem ∗ p = new elem(taille+biais, prof, prof, isroot,
NULL, NULL, parent) ;
// la variable biais contient la valeur de la clé
// du père de p, ce qui permet d’initialiser la clé
// de p à la bonne valeur
p→fg = construireP(taille/2, prof+1,stop,biais, false, p) ;
p→fd = construireP(taille/2, prof+1, stop, taille+biais,
true, p) ;
// Construction récursive pour le sous-arbre gauche
// et le sous-arbre droit de p
return(p) ;
}
else
return(NULL) ;
}
void splaytree : :rotationsimple(elem ∗ p, bool & stop)
// Cette méthode effectue une rotation simple. La variable ”stop”
//indique à l’appelant si le noeud p est devenu la racine du
//splay tree auquel il appartient (stop = true) ou non (stop = false)
{
elem ∗ q = p→pere ;
int a=INT MAX, b=INT MAX ;
119
if(q→isroot)
// Dans ce cas, il faut changer l’isroot bit de p et q,
//car p devient la racine du splay tree auquel il
//appartient et q ne l’est plus
{
q→isroot=false ;
p→isroot=true ;
stop=true ;
}
if(q→pere)
{
// Mise à jour des pointeurs du père de q pour
// qu’il devienne le père de p
if(q == q→pere→fd)
q→pere→fd = p ;
else
q→pere→fg = p ;
}
// Les opérations ci-dessous sont des mises à jour de
//pointeurs pour effectuer la rotation simple
p→pere = q→pere ;
q→pere = p ;
if(p == q→fg)
{
q→fg = p→fd ;
if(q→fg)
q→fg→pere = q ;
p→fd = q ;
}
else
{
q→fd = p→fg ;
if(q→fd)
q→fd→pere = q ;
p→fg = q ;
}
120
// Les opérations ci-dessous mettent à jour les champs
//”mindepth”, qui changent à cause de la rotation
p→mindepth = q→mindepth ;
if(q→fg)
a = q→fg→mindepth ;
if(q→fd)
b = q→fd→mindepth ;
q→mindepth = min(q→profondeur, min(a,b)) ;
}
void splaytree : :zigzag(elem ∗ p, bool & stop)
// Cette méthode effectue une rotation zig-zag. La variable ”stop”
//indique à l’appelant si le noeud p est devenu la racine du splay
//tree auquel il appartient (stop = true) ou non (stop = false)
{
elem ∗ q = p→pere ;
elem ∗ z = q→pere ;
int a=INT MAX, b=INT MAX ;
if(z→isroot)
// Dans ce cas, il faut changer l’isroot bit de p et z,
//car p devient la racine du splay tree auquel il
//appartient et z ne l’est plus
{
// Mise à jour des pointeurs du père de z pour
//qu’il devienne le père de p
z→isroot=false ;
p→isroot=true ;
stop=true ;
}
// Les opérations ci-dessous sont des mises à jour de
//pointeurs pour effectuer la rotation zig-zag
p→pere = z→pere ;
z→pere = p ;
q→pere = p ;
if(p == q→fd)
121
{
}
else
{
q→fd = p→fg ;
if(q→fd)
q→fd→pere = q ;
z→fg = p→fd ;
if(z→fg)
z→fg→pere = z ;
p→fg = q ;
p→fd = z ;
q→fg = p→fd ;
if(q→fg)
q→fg→pere = q ;
z→fd = p→fg ;
if(z→fd)
z→fd→pere = z ;
p→fg = z ;
p→fd = q ;
}
if(p→pere)
{
if(p→pere→fg == z)
p→pere→fg = p ;
else
p→pere→fd = p ;
}
// Les opérations ci-dessous mettent à jour les champs
//”mindepth”, qui changent à cause de la rotation
p→mindepth = z→mindepth ;
if(z→fg)
a = z→fg→mindepth ;
if(z→fd)
b = z→fd→mindepth ;
122
z→mindepth = min(z→profondeur, min(a,b)) ;
a = INT MAX ;
b = INT MAX ;
if(q→fg)
a = q→fg→mindepth ;
if(q→fd)
b = q→fd→mindepth ;
q→mindepth = min(q→profondeur, min(a,b)) ;
}
void splaytree : :zigzig(elem ∗ p, bool & stop)
// Cette méthode effectue une rotation zig-zig. La variable
//”stop” indique à l’appelant si le noeud p est devenu la
//racine du splay tree auquel il appartient (stop = true)
//ou non (stop = false)
{
elem ∗ q = p→pere ;
elem ∗ z = q→pere ;
int a = INT MAX, b = INT MAX ;
if(z→isroot)
// Dans ce cas, il faut changer l’isroot bit de p et z,
//car p devient la racine du splay tree auquel il
//appartient et z ne l’est plus
{
// Mise à jour des pointeurs du père de z pour
//qu’il devienne le père de p
z→isroot=false ;
p→isroot=true ;
stop=true ;
}
// Les opérations ci-dessous sont des mises à jour de
//pointeurs pour effectuer la rotation zig-zig
p→pere = z→pere ;
q→pere = p ;
z→pere = q ;
123
if(p == q→fg)
{
q→fg = p→fd ;
if(q→fg)
q→fg→pere = q ;
z→fg = q→fd ;
if(z→fg)
z→fg→pere = z ;
p→fd = q ;
q→fd = z ;
p→mindepth = z→mindepth ;
if(z→fg)
a = z→fg→mindepth ;
if(z→fd)
b = z→fd→mindepth ;
z→mindepth = min(z→profondeur, min(a,b)) ;
a = INT MAX ;
if(q→fg)
a = q→fg→mindepth ;
q→mindepth = min(q→profondeur,
min(a,z→mindepth)) ;
}
else
{
q→fd = p→fg ;
if(q→fd)
q→fd→pere = q ;
z→fd = q→fg ;
if(z→fd)
z→fd→pere = z ;
p→fg = q ;
q→fg = z ;
124
p→mindepth = z→mindepth ;
if(z→fg)
a = z→fg→mindepth ;
if(z→fd)
b = z→fd→mindepth ;
z→mindepth = min(z→profondeur, min(a,b)) ;
a = INT MAX ;
if(q→fd)
a = q→fd→mindepth ;
q→mindepth = min(q→profondeur,
min(a,z→mindepth)) ;
}
if(p→pere)
{
if(p→pere→fg == z)
p→pere→fg = p ;
else
p→pere→fd = p ;
}
}
void splaytree : :splayingracine(elem ∗ p)
// Cette méthode splaye le noeud p jusqu’à ce qu’il devienne
//la racine du multi-splay tree auquel il appartient
{
elem ∗ q =p→pere ;
bool stop = false ;
short int tester=0 ;
// On vérifie que le noeud à splayer ne soit pas la
//racine du multi-splay tree
if( !p→isroot)
{
while( !stop && q)
125
{
if(q→isroot)
{
rotationsimple(p,stop) ;
break ;
}
else
{
if(p == q→fg)
//si p est un fils gauche, on
//incrémente test de 1
++tester ;
else
//sinon test est décrémenté de 1
– –tester ;
if(q == q→pere→fg)
//si q est un fils gauche, on
//incrémente test de 1
++tester ;
else
//sinon test est décrémenté de 1
– –tester ;
if(tester)
//si test est différent de 0, cela
//signifie qu’on est remonté par deux
//sens différents pour aller du
//noeud x à son grand-père. Il faut
//donc effectuer une rotation zig-zig
{
zigzig(p,stop) ;
tester=0 ;
}
else
//sinon on est remonté deux fois par le
//même sens pour aller du
//noeud x à son grand-père. Il faut
//donc effectuer une rotation zig-zag
{ zigzag(p,stop) ;}
126
q = p→pere ;
}
}
}
}
if( !p→pere)//si p n’a pas de père, alors il
// est la racine du multi-splay tree
{
rac = p ;
// on met à jour la racine du multi-splay tree
}
void splaytree : :splayingto(elem ∗ p, elem ∗ arret)
// Cette méthode splaye le noeud p jusqu’à ce qu’il devienne
//le fils du noeud arret
{
elem ∗ q =p→pere ;
short int test=0 ;
bool stop=false ;
while(q != arret)
{
if(q→pere == arret)
{
rotationsimple(p,stop) ;
break ;
}
else
{
if(p == q→fg)
//si p est un fils gauche, on
//incrémente test de 1
++test ;
else
//sinon test est décrémenté de 1
– –test ;
127
if(q == q→pere→fg)
//si q est un fils gauche, on
//incrémente test de 1
++test ;
else
//sinon test est décrémenté de 1
– –test ;
if(test)
//si test est different de 0, cela
//signifie qu’on est remonté par deux
//sens différents pour aller du
//noeud x à son grand-père. Il faut
//donc effectuer une rotation zig-zig
{
zigzig(p,stop) ;
test=0 ;
}
else
//sinon on est remonté deux fois par le
//même sens pour aller du
//noeud x à son grand-père. Il faut
//donc effectuer une rotation zig-zag
zigzag(p,stop) ;
q = p→pere ;
}
}
}
splaytree : :elem∗ splaytree : :localisation(int i)
// Cette méthode localise le noeud i dans le multi-splay tree
{
elem ∗ p = rac ;
elem ∗ q=NULL ;
if(p)
// On vérifie que l’arbre ne soit pas vide
{
do
128
{
q = p;
if(i == p→info)
{
return(p) ;
}
else
{
if(i < p→info)
p = p→fg ;
else
p = p→fd ;
}
}
while(p) ;
//si on sort du while, c’est que l’élément i
//n’est pas présent dans l’arbre.
}
return(NULL) ;
}
splaytree : :elem∗ splaytree : :recherche(int i)
//Cette fonction recherche l’élément i et réorganise la structure
//pour tenir compte des changements causés par l’accès i.
{
elem ∗ p = localisation(i) ;//localisation de i
elem ∗ save = p ;
//On vérifie que le noeud i ne soit pas la racine du
//multi-splay tree
if(p)
{
while(p != rac)
{
if(p→isroot)
// Dans ce cas, un changement de fils
//préféré doit être effectué
129
{
//recherche du noeud a switcher,
//qui sera stocké dans p à la fin
//de la boucle while
elem ∗ rac splay bas = p ;
p=p→pere ;
while(p→profondeur !=
(rac splay bas→mindepth)-1)
{
p=p→pere ;
}
//après que le noeud à switcher
//ait été localisé, il faut déterminer
//le type de switch à effectuer
if(rac splay bas→info > p→info)
switchGD(p) ;
else
switchDG(p) ;
}
else
{
p=p→pere ;
}
}
splayingracine(save) ;// le noeud i est remonté
//jusqu’à la racine
}
else
{
}
return(NULL) ;
}
void splaytree : :switchGD(elem ∗ y)
// Cette méthode effectue un switch gauche-droite sur le noeud y
{
elem ∗ z = NULL ;
elem ∗ x = NULL ;
int mingauche = INT MAX, mindroite = INT MAX ;
130
splayingracine(y) ;// on ramène y à la racine du splay tree
//auquel il appartient
z = determinerZ(y) ;
// on localise le noeud z. Il s’agit du plus grand noeud,
//qui a une information prof plus petite que celle
//de y et qui est plus petit que y
if(z)
{
splayingto(z, y) ;
// on splaye z jusqu’à ce qu’il devienne le fils
//gauche de y
if(z→fd)
{
// on fait du fils gauche de y dans P
//son fils non préféré
z→fd→isroot = true ;
int a = INT MAX ;
if(z→fg)
a = z→fg→mindepth ;
z→mindepth = min(z→profondeur, a) ;
mingauche = z→mindepth ;
}
}
else // z n’existe pas
{
// on fait du fils gauche de y dans P son fils
//non préféré
if(y→fg)
{
y→fg→isroot = true ;
}
}
x = determinerX(y) ;
// on localise le noeud x. Il s’agit du plus petit noeud,
//qui a une information prof plus petite que celle
131
//de y et qui est plus grand que y
if(x)
{
splayingto(x, y) ;
// on splaye x jusqu’à ce qu’il devienne le fils
//droit de y
if(x→fg)
{
// on fait du fils droit de y dans P son
//fils préféré
x→fg→isroot = false ;
if(x→fg→mindepth < x→mindepth)
x→mindepth = x→fg→mindepth ;
mindroite = x→mindepth ;
}
}
else // x n’existe pas
{
// on fait du fils droit de y dans P son fils préféré
if(y→fd)
{
y→fd→isroot = false ;
}
}
y→mindepth = min(y→profondeur, min(mingauche,mindroite)) ;
}
void splaytree : :switchDG(elem ∗ y)
// Cette méthode effectue un switch gauche-droite sur le noeud y
{
elem ∗ z = NULL ;
elem ∗ x = NULL ;
int mingauche = INT MAX, mindroite = INT MAX ;
splayingracine(y) ;// on ramène y à la racine du splay tree
//auquel il appartient
z = determinerZ(y) ;
132
// on localise le noeud z. Il s’agit du plus grand noeud,
//qui a une information prof plus petite que celle
//de y et qui est plus petit que y
if(z)
{
splayingto(z, y) ;
// on splaye z jusqu’à ce qu’il devienne le fils
//gauche de y
if(z→fd)
{
// on fait du fils gauche de y dans P son
//fils préféré
z→fd→isroot = false ;
if(z→fd→mindepth < z→mindepth)
z→mindepth = z→fd→mindepth ;
mingauche = z→mindepth ;
}
}
else // z n’existe pas
{
// on fait du fils gauche de y dans P son fils
//préféré
if(y→fg)
{
y→fg→isroot = false ;
}
}
x = determinerX(y) ;
// on localise le noeud x. Il s’agit du plus petit noeud,
//qui a une information prof plus petite que celle
//de y et qui est plus grand que y
if(x)
{
splayingto(x, y) ;
// on splaye x jusqu’à ce qu’il devienne le fils
//droit de y
133
if(x→fg)
{
// on fait du fils droit de y dans P son
// fils non préféré
x→fg→isroot = true ;
int a = INT MAX ;
if(x→fd)
a = x→fd→mindepth ;
x→mindepth = min(x→profondeur, a) ;
mindroite = x→mindepth ;
}
}
else // x n’existe pas
{
// on fait du fils droit de y dans P son fils non
//préféré
if(y→fd)
{
y→fd→isroot = true ;
}
}
y→mindepth = min(y→profondeur, min(mingauche,mindroite)) ;
}
splaytree : :elem∗ splaytree : :determinerZ(elem ∗ y)
// Cette méthode renvoie le plus grand noeud, qui a une information
//prof plus petite que celle de y et qui est plus petit que y.
{
elem ∗ x = y →fg ;
if(x && x→mindepth < y→profondeur)
{
while(true)
{
if(x→profondeur < y→profondeur)
{
if(x→fd && x→fd→mindepth <
y→profondeur)
134
x = x→fd ;
else
return(x) ;
}
else
{
if(x→fd && x→fd→mindepth <
y→profondeur)
x = x→fd ;
else
x = x→fg ;
}
}
else
{
}
}
return(NULL) ;
}
splaytree : :elem∗ splaytree : :determinerX(elem ∗ y)
// Cette méthode renvoie le plus petit noeud, qui a une information
//prof plus petite que celle de y et qui est plus grand que y
{
elem ∗ x = y →fd ;
if(x && x→mindepth < y→profondeur)
{
while(true)
{
if(x→profondeur < y→profondeur)
{
if(x→fg && x→fg→mindepth <
y→profondeur)
x = x→fg ;
else
return(x) ;
}
else
135
{
if(x→fg && x→fg→mindepth <
y→profondeur)
x = x→fg ;
else
x = x→fd ;
}
else
{
}
}
}
return(NULL) ;
}
splaytree : :∼splaytree()
{
destructeur(rac) ;
}
void splaytree : :destructeur(elem ∗ p)
{
if(p)
{
destructeur(p→fg) ;
destructeur(p→fd) ;
delete p ;
}
}
int main()
{
return(0) ;
}
136
13.1.2
Splay trees
Les clés sont des éléments de type entier. Les valeurs des n clés de l’arbre
correspondent à l’ensemble {1, . . . , n}.
Pour l’implémentation de la structure, nous utilisons deux classes :
1. elem : cette classe représente un nœud de l’arbre.
2. splaytree : cette classe représente le splay tree.
Nous détaillons ci-dessous les points principaux de l’implémentation. Des
explications plus détaillées sont fournies en tant que commentaires dans le
code.
L’initialisation de la structure se fait dans le constructeur paramétré de la
classe splaytree. Elle consiste à insérer de manière successive les n éléments de
l’arbre dans l’ordre croissant. Elle fait appel pour cela à la méthode insertion.
La méthode insertion consiste à insérer l’élément passé en paramètre
dans l’arbre. Cette méthode n’est qu’une implémentation de l’algorithme
d’insertion de splay trees décrit dans le chapitre 6.
La méthode recherche consiste à localiser l’élément passé en paramètre.
Après cela, elle invoque la méthode splaying pour remonter l’élément recherché à la racine.
La méthode splaying consiste à remonter l’élément passé en paramètre à
la racine. La remontée est effectuée dans la boucle principale. Le traitement
de cette boucle consiste à déterminer le type de rotation qu’il faut effectuer
(soit une rotation simple, soit une rotation zig-zig, soit une rotation zig-zag)
et à invoquer, ensuite, la méthode adéquate pour l’effectuer. La boucle est
recommencée jusqu’à ce que la racine soit atteinte.
L’implémentation est fournie ci-dessous.
# include <iostream>
# include <limits.h>
# include <time.h>
# include <math.h>
# include <iomanip>
# include <stdlib.h>
# include <unistd.h>
# include <sys/types.h>
# include <sys/stat.h>
# include <fcntl.h>
using namespace std ;
class splaytree
137
{
private :
class elem
{
friend class splaytree ;
int info ;
elem ∗ fg ;
elem ∗ fd ;
elem ∗ pere ;
elem(){}
elem(int i, elem ∗ g = NULL, elem ∗ d =
NULL, elem ∗ p = NULL) :info(i),
fg(g), fd(d), pere(p) {}
};
elem ∗ rac ;
void rotationsimple(elem ∗) ;
void zigzag(elem ∗ ) ;
void zigzig(elem ∗ ) ;
void splaying(elem ∗ ) ;
void destructeur(elem ∗) ;
public :
splaytree(){rac=NULL ;}
splaytree(int) ;
elem ∗ recherche(int) ;
void insertion(int) ;
void suppression(int) ;
∼splaytree() ;
};
splaytree : :splaytree(int i)
//constructeur du splay tree
{
rac=new elem(1) ;
for(int j=2 ; j <= i ; j++)
{
insertion(j) ;
}
138
}
void splaytree : :rotationsimple(elem ∗ p)
// si l’élément à splayer n’a pas de grand-père on appelle
// cette fonction pour réaliser la rotation
{
elem ∗ q = p→pere ;
p→pere = NULL ;
q→pere = p ;
if(p == q→fg)
{
q→fg = p→fd ;
if(q→fg)
q→fg→pere = q ;
p→fd = q ;
}
else
{
q→fd = p→fg ;
if(q→fd)
q→fd→pere = q ;
p→fg = q ;
}
}
void splaytree : :zigzag(elem ∗ p)
//si le noeud à splayer est le fils droit(gauche) d’un fils
//gauche(droit), on appelle cette fonction pour
//effectuer la rotation
{
elem ∗ q = p→pere ;
elem ∗ z = q→pere ;
p→pere = z→pere ;
z→pere = p ;
139
q→pere = p ;
if(p == q→fd)
{
q→fd = p→fg ;
if(q→fd)
q→fd→pere = q ;
z→fg = p→fd ;
if(z→fg)
z→fg→pere = z ;
p→fg = q ;
p→fd = z ;
}
else
{
q→fg = p→fd ;
if(q→fg)
q→fg→pere = q ;
z→fd = p→fg ;
if(z→fd)
z→fd→pere = z ;
p→fg = z ;
p→fd = q ;
}
if(p→pere)
{
if(p→pere→fg == z)
p→pere→fg = p ;
else
p→pere→fd = p ;
}
}
void splaytree : :zigzig(elem ∗ p)
// si le noeud à splayer est le fils gauche(droit) d’un
//fils gauche(droit), on appelle cette fonction pour
//effectuer la rotation
{
140
elem ∗ q = p→pere ;
elem ∗ z = q→pere ;
p→pere = z→pere ;
q→pere = p ;
z→pere = q ;
if(p == q→fg)
{
q→fg = p→fd ;
if(q→fg)
q→fg→pere = q ;
z→fg = q→fd ;
if(z→fg)
z→fg→pere = z ;
p→fd = q ;
q→fd = z ;
}
else
{
q→fd = p→fg ;
if(q→fd)
q→fd→pere = q ;
z→fd = q→fg ;
if(z→fd)
z→fd→pere = z ;
p→fg = q ;
q→fg = z ;
}
if(p→pere)
{
if(p→pere→fg == z)
p→pere→fg = p ;
else
p→pere→fd = p ;
}
}
141
void splaytree : :splaying(elem ∗ p)
//fonction qui réalise un splaying sur un noeud. Ce splaying
//remontera ce noeud jusqu’à la racine de l’arbre par
//une série de rotations. Cette fonction déterminera
//le type de rotation à réaliser sur le noeud
//à chaque étape du splaying
{
elem ∗ q =p→pere ;
short int test=0 ;
while(q)
//si p est la racine, alors son pointeur père q est nul
{
if( !q→pere)
{
rotationsimple(p) ;
break ;
}
else
{
if(p == q→fg)
//si p est un fils gauche, on
//incrémente test de 1
++test ;
else
//sinon test est décrémenté de 1
– –test ;
if(q == q→pere→fg)
//si q est un fils gauche, on
//incrémente test de 1
++test ;
else
//sinon test est décrémenté de 1
– –test ;
if(test)
//si test est différent de 0, cela
//signifie qu’on est remonté par deux
//sens différents pour aller du
142
//noeud x à son grand-père. Il faut
//donc effectuer une rotation zig-zig
{
zigzig(p) ;
test=0 ;
}
else
//sinon on est remonté deux fois par le
//même sens pour aller du
//noeud x à son grand-père. Il faut
//donc effectuer une rotation zig-zag
zigzag(p) ;
q = p→pere ;
}
}
rac = p ;
}
splaytree : :elem∗ splaytree : :recherche(int i)
//Cette fonction recherche un élément dans un splay tree. Si
//l’élément est présent dans l’arbre, un pointeur vers le noeud
//recherché est renvoyé. Sinon le pointeur NULL est renvoyé pour
//signaler que l’élément n’est pas présent dans l’arbre
{
elem ∗ p = rac ;
elem ∗ q=NULL ;
if(p)
{
do
{
q = p;
if(i == p→info)
{
splaying(p) ;
143
return(p) ;
}
else
{
if(i < p→info)
p = p→fg ;
else
p = p→fd ;
}
}
while(p) ;
//si on sort du while, c’est que l’élément n’est
//pas dans l’arbre. On réalise donc un splaying
//sur le dernier élément rencontré pendant
//la recherche.
splaying(q) ;
}
return(NULL) ;
}
void splaytree : :insertion(int i)
//Cette fonction insère un élément dans l’arbre, puis y applique
//un splaying
{
elem ∗ p = rac ;
if(p)
{
elem ∗ q = NULL ;
do
{
q = p;
if( i <= p→info)
p = p→fg ;
else
p = p→fd ;
}
144
while(p) ;
if( i <= q→info)
{
q→fg = new elem(i) ;
q→fg→pere = q ;
splaying(q→fg) ;
}
else
{
q→fd = new elem(i) ;
q→fd→pere = q ;
splaying(q→fd) ;
}
}
else
rac = new elem(i) ;
}
void splaytree : :suppression(int i)
//Pour supprimer un élément, on doit d’abord appeler la
//fonction rechercher en passant comme paramètre l’élément
//à supprimer. Cet élément devient alors la racine
//de l’arbre. Puis, on supprime cette racine et on obtient
//deux sous-arbres que l’on doit fusionner. Pour ce faire,
//il suffit de prendre le plus grand élément dans le sous-arbre
//gauche(le fils droit le plus à droite dans le sous-arbre
//gauche) et de le remonter jusqu’à la racine (par la fonction
//de splaying). La racine du sous-arbre gauche ne possédant plus
//de fils droit, il suffit de rattacher la racine du sous-arbre
//droit comme fils droit de la racine du sous-arbre gauche.
{
elem ∗ p ;
if((p = recherche(i)))
{
splaying(p) ;
// l’élément à effacer est devenu la racine
//de l’arbre
145
elem∗ ssarbreG = p→fg ;
if (p→fg)
{
rac = p→fg ;
p→fg→pere= 0 ;
}
elem ∗ ssarbreD = p→fd ;
delete p ;
// suppression de la racine de l’arbre
if( ssarbreD && ssarbreG)
{
ssarbreD→pere= 0 ;
elem ∗ q = 0 ;
for( p = rac ; p ; p= p→fd)
// recherche du noeud le plus à droite
// du sous-arbre gauche
{
q = p;
}
if(q)
{
splaying(q) ;
q→fd = ssarbreD ;
ssarbreD→pere = q ;
}
}
else
{
if(ssarbreD)
{
rac = ssarbreD ;
ssarbreD→pere= 0 ;
146
}
else
{
if( !ssarbreG)
{
rac = 0 ;
}
}
}
}
}
splaytree : :∼splaytree()
{
destructeur(rac) ;
}
void splaytree : :destructeur(elem ∗ p)
{
if(p)
{
destructeur(p→fg) ;
destructeur(p→fd) ;
delete p ;
}
}
int main()
{
return(0) ;
}
147
13.1.3
Arbres rouges-noirs
L’implémentation est fournie ci-dessous.
#
#
#
#
#
#
#
#
#
#
include
include
include
include
include
include
include
include
include
include
<stdio.h>
<stdlib.h>
<string.h>
<stdarg.h>
<time.h>
<math.h>
<unistd.h>
<sys/types.h>
<sys/stat.h>
<fcntl.h>
/∗ implementation independent declarations ∗/
/∗ Red-Black tree description ∗/
typedef enum { BLACK, RED } nodeColor ;
typedef struct nodeTag {
struct nodeTag ∗left ; /∗ left child ∗/
struct nodeTag ∗right ; /∗ right child ∗/
struct nodeTag ∗parent ; /∗ parent ∗/
nodeColor color ; /∗ node color (BLACK, RED) ∗/
int key ; /∗ key used for searching ∗/
/∗ user data ∗/
} nodeType ;
#define NIL &sentinel /∗ all leafs are sentinels ∗/
static nodeType sentinel = { NIL, NIL, 0, BLACK, 0} ;
/∗ last node found, optimizes find/delete operations ∗/
static nodeType ∗lastFind ;
static nodeType ∗root = NIL ; /∗ root of Red-Black tree ∗/
static void rotateLeft(nodeType ∗x) {
/∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
∗ rotate node x to left ∗
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗/
148
nodeType ∗y = x→right ;
/∗ establish x->right link ∗/
x→right = y→left ;
if (y→left != NIL) y→left→parent = x ;
/∗ establish y->parent link ∗/
if (y != NIL) y→parent = x→parent ;
if (x→parent) {
if (x == x→parent→left)
x→parent→left = y ;
else
x→parent→right = y ;
} else {
root = y ;
}
/∗ link x and y ∗/
y→left = x ;
if (x != NIL) x→parent = y ;
}
static void rotateRight(nodeType ∗x) {
/∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
∗ rotate node x to right ∗
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗/
nodeType ∗y = x→left ;
/∗ establish x->left link ∗/
x→left = y→right ;
if (y→right != NIL) y→right→parent = x ;
/∗ establish y->parent link ∗/
if (y != NIL) y→parent = x→parent ;
if (x→parent) {
149
if (x == x→parent→right)
x→parent→right = y ;
else
x→parent→left = y ;
} else {
root = y ;
}
/∗ link x and y ∗/
y→right = x ;
if (x != NIL) x→parent = y ;
}
static void insertFixup(nodeType ∗x) {
/∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
∗ maintain Red-Black tree balance ∗
∗ after inserting node x ∗
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗/
/∗ check Red-Black properties ∗/
while (x != root && x→parent→color == RED)
{
/∗ we have a violation ∗/
if (x→parent == x→parent→parent→left)
{
nodeType ∗y = x→parent→parent→right ;
if (y→color == RED)
{
/∗ uncle is RED ∗/
x→parent→color = BLACK ;
y→color = BLACK ;
x→parent→parent→color = RED ;
x = x→parent→parent ;
}
150
else
{
/∗ uncle is BLACK ∗/
if (x == x→parent→right)
{
/∗ make x a left child ∗/
x = x→parent ;
rotateLeft(x) ;
}
/∗ recolor and rotate ∗/
x→parent→color = BLACK ;
x→parent→parent→color = RED ;
rotateRight(x→parent→parent) ;
}
}
else
{
/∗ mirror image of above code ∗/
nodeType ∗y = x→parent→parent→left ;
if (y→color == RED)
{
/∗ uncle is RED ∗/
x→parent→color = BLACK ;
y→color = BLACK ;
x→parent→parent→color = RED ;
x = x→parent→parent ;
}
else
{
/∗ uncle is BLACK ∗/
if (x == x→parent→left)
151
{
x = x→parent ;
rotateRight(x) ;
}
x→parent→color = BLACK ;
x→parent→parent→color = RED ;
rotateLeft(x→parent→parent) ;
}
}
}
root→color = BLACK ;
}
void insert(int key) {
nodeType ∗current, ∗parent, ∗x ;
/∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
∗ allocate node for data and insert in tree ∗
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗/
/∗ find future parent ∗/
current = root ;
parent = 0 ;
while (current != NIL)
{
/∗if (compEQ(key, current->key))
return STATUS DUPLICATE KEY ;∗/
parent = current ;
current = (key < current→key) ?
current→left : current→right ;
}
/∗ setup new node ∗/
152
x =(nodeType∗) malloc (sizeof(∗x)) ;
x→parent = parent ;
x→left = NIL ;
x→right = NIL ;
x→color = RED ;
x→key = key ;
/∗ insert node in tree ∗/
if(parent)
{
if((key < parent→key))
parent→left = x ;
else
parent→right = x ;
}
else
{
root = x ;
}
insertFixup(x) ;
lastFind = NULL ;
}
static void deleteFixup(nodeType ∗x) {
/∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
∗ maintain Red-Black tree balance ∗
∗ after deleting node x ∗
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗/
while (x != root && x→color == BLACK)
{
if (x == x→parent→left)
{
nodeType ∗w = x→parent→right ;
if (w→color == RED)
153
{
w→color = BLACK ;
x→parent→color = RED ;
rotateLeft (x→parent) ;
w = x→parent→right ;
}
if (w→left→color == BLACK && w→right→color ==
BLACK)
{
}
else
{
w→color = RED ;
x = x→parent ;
if (w→right→color == BLACK)
{
w→left→color = BLACK ;
w→color = RED ;
rotateRight (w) ;
w = x→parent→right ;
}
w→color = x→parent→color ;
x→parent→color = BLACK ;
w→right→color = BLACK ;
rotateLeft (x→parent) ;
x = root ;
}
else
{
}
nodeType ∗w = x→parent→left ;
if (w→color == RED)
{
w→color = BLACK ;
x→parent→color = RED ;
rotateRight (x→parent) ;
w = x→parent→left ;
}
if (w→right→color == BLACK && w→left→color ==
154
BLACK)
{
}
else
{
w→color = RED ;
x = x→parent ;
if (w→left→color == BLACK)
{
w→right→color = BLACK ;
w→color = RED ;
rotateLeft (w) ;
w = x→parent→left ;
}
w→color = x→parent→color ;
x→parent→color = BLACK ;
w→left→color = BLACK ;
rotateRight (x→parent) ;
x = root ;
}
}
}
x→color = BLACK ;
}
void deleter(int key) {
nodeType ∗x, ∗y, ∗z ;
/∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
∗ delete node z from tree ∗
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗/
z = root ;
while(z != NIL)
{
if((key == z→key))
155
break ;
else
z = (key < z→key) ? z→left : z→right ;
}
/∗if (z == NIL) return STATUS KEY NOT FOUND ;∗/
if (z→left == NIL || z→right == NIL)
{
/∗ y has a NIL node as a child ∗/
y = z;
}
else
{
/∗ find tree successor with a NIL node as a child ∗/
y = z→right ;
while (y→left != NIL)
{
y = y→left ;
}
}
/∗ x is y’s only child ∗/
if (y→left != NIL)
x = y→left ;
else
x = y→right ;
/∗ remove y from the parent chain ∗/
x→parent = y→parent ;
if (y→parent)
{
if (y == y→parent→left)
y→parent→left = x ;
else
156
y→parent→right = x ;
}
else
root = x ;
if (y != z)
{
z→key = y→key ;
}
if (y→color == BLACK)
deleteFixup (x) ;
free (y) ;
lastFind = NULL ;
}
int find(int key) {
/∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
∗ find node containing data ∗
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗/
nodeType ∗current = root ;
while(current != NIL)
{
if((key == current→key))
{
lastFind = current ;
return 1 ;
}
else
{
current = (key < current→key) ?
current→left : current→right ;
}
157
}
return 0 ;
}
void destructeur(nodeType∗ p)
{
if(p != NIL)
{
destructeur(p→left) ;
destructeur(p→right) ;
free(p) ;
}
}
int main()
{
return(0) ;
}
13.2
Jeux de tests
Nous fournissons ci-dessous le reste des diagrammes des jeux de tests pour
montrer que les résultats obtenus sont bien consistants.
158
200000
MST9
ST9
RB9
180000
160000
140000
120000
100000
80000
60000
40000
20000
0
500
1000
1500
2000
2500
3000
3500
4000
4500
5000
5500
Fig. 13.1 – Coûts d’un arbre de taille 29 avec une distribution aléatoire
450000
MST10
ST10
RB10
400000
350000
300000
250000
200000
150000
100000
50000
0
1000
2000
3000
4000
5000
6000
7000
8000
9000
10000
Fig. 13.2 – Coûts d’un arbre de taille 210 avec une distribution aléatoire
159
11000
1e+06
MST11
ST11
RB11
900000
800000
700000
600000
500000
400000
300000
200000
100000
0
2000
4000
6000
8000
10000
12000
14000
16000
18000
20000
22000
Fig. 13.3 – Coûts d’un arbre de taille 211 avec une distribution aléatoire
2.5e+06
MST12
ST12
RB12
2e+06
1.5e+06
1e+06
500000
0
0
5000
10000
15000
20000
25000
30000
35000
40000
Fig. 13.4 – Coûts d’un arbre de taille 212 avec une distribution aléatoire
160
45000
5e+06
MST13
ST13
RB13
4.5e+06
4e+06
3.5e+06
3e+06
2.5e+06
2e+06
1.5e+06
1e+06
500000
0
0
10000
20000
30000
40000
50000
60000
70000
80000
90000
Fig. 13.5 – Coûts d’un arbre de taille 213 avec une distribution aléatoire
1.1e+07
MST14
ST14
RB14
1e+07
9e+06
8e+06
7e+06
6e+06
5e+06
4e+06
3e+06
2e+06
1e+06
0
0
20000
40000
60000
80000
100000
120000
140000
160000
Fig. 13.6 – Coûts d’un arbre de taille 214 avec une distribution aléatoire
161
180000
2.5e+07
MST15
ST15
RB15
2e+07
1.5e+07
1e+07
5e+06
0
0
50000
100000
150000
200000
250000
300000
350000
Fig. 13.7 – Coûts d’un arbre de taille 215 avec une distribution aléatoire
90000
MST9
ST9
RB9
80000
70000
60000
50000
40000
30000
20000
10000
0
500
1000
1500
2000
2500
3000
3500
4000
4500
5000
Fig. 13.8 – Coûts d’un arbre de taille 29 avec une distribution working set
162
5500
200000
MST10
ST10
RB10
180000
160000
140000
120000
100000
80000
60000
40000
20000
0
1000
2000
3000
4000
5000
6000
7000
8000
9000
10000
11000
Fig. 13.9 – Coûts d’un arbre de taille 210 avec une distribution working set
450000
MST11
ST11
RB11
400000
350000
300000
250000
200000
150000
100000
50000
0
2000
4000
6000
8000
10000
12000
14000
16000
18000
20000
Fig. 13.10 – Coûts d’un arbre de taille 211 avec une distribution working set
163
22000
1e+06
MST12
ST12
RB12
900000
800000
700000
600000
500000
400000
300000
200000
100000
0
0
5000
10000
15000
20000
25000
30000
35000
40000
45000
Fig. 13.11 – Coûts d’un arbre de taille 212 avec une distribution working set
2.5e+06
MST13
ST13
RB13
2e+06
1.5e+06
1e+06
500000
0
0
10000
20000
30000
40000
50000
60000
70000
80000
Fig. 13.12 – Coûts d’un arbre de taille 213 avec une distribution working set
164
90000
4.5e+06
MST14
ST14
RB14
4e+06
3.5e+06
3e+06
2.5e+06
2e+06
1.5e+06
1e+06
500000
0
0
20000
40000
60000
80000
100000
120000
140000
160000
180000
Fig. 13.13 – Coûts d’un arbre de taille 214 avec une distribution working set
1e+07
MST15
ST15
RB15
9e+06
8e+06
7e+06
6e+06
5e+06
4e+06
3e+06
2e+06
1e+06
0
0
50000
100000
150000
200000
250000
300000
Fig. 13.14 – Coûts d’un arbre de taille 215 avec une distribution working set
165
350000
Bibliographie
[AVL62]
George M. Adel’son-Vel’skii and Evgenii M. Landis. An algorithm
for the organisation of information. Soviet Mathematics Doklay,
3 :1259–1262, 1962.
[BCK03]
Avrim Blum, Shuchi Chawla, and Adam Kalai. Static optimality
and dynamic search optimality in lists and trees. Algorithmica,
36(3) :249–260, 2003.
[Bit79]
James R. Bitner. Heuristics that dynamically organize data structures. SIAM Journal of Computing, 8 :82–110, 1979.
[BLM+ 03] Gerth S. Brodal, George Lagogiannis, Christos Makris, Athanasios Tsakalidis, and Kostas Tsichlas. Optimal finger search
trees in the pointer machine. Journal of Computer and System
Sciences, 67(2) :381–418, 2003.
[BT80]
Mark R. Brown and Robert E. Tarjan. Design and analysis of a
data structure for representing sorted lists. SIAM Journal Computer, 9 :594–614, 1980.
[Car04]
Jean Cardinal. Structures de l’information. Presses Universitaire
de Bruxelles, 3ème edition, 2004.
[Col00]
Richard Cole. On the dynamic finger conjecture for splay trees.
SIAM Journal of Computing, 30 :44–85, 2000.
[DHIP04] Erik D. Demaine, Dion Harmon, John Iacono, and Mihai Patrascu. Dynamic optimality-almost. In Proceedings of the 45th
Annual IEEE Symposium on Fonudations of Computer Science,
pages 484–490, 2004.
[DSW05] Jonathan Derryberry, Daniel D. Sleator, and Chengwen C. Wang.
A lower bound framework for binary search trees with rotations.
Technical report, Carnegie Mellon University, 2005.
[DSW06] Jonathan Derryberry, Daniel D. Sleator, and Chengwen C. Wang.
(log log n)-competitive dynamic binary search trees. pages 374–
383, 2006.
166
[FS98]
Philippe Flajolet and Robert Sedgewick. Introduction l’analyse
des algorithmes. International Thomson publishing, 1998.
[Iac01]
John Iacono. Alternatives to splay trees with o(log n) worst-case
access times. Symposium on Discrete Algorithms, pages 516–522,
2001.
[Iac05]
John Iacono. Key-independent optimality. Algorithmica, 42(1) :3–
10, 2005.
[Knu73]
Donald E. Knuth. Computing Programming, Sorting and Searching, volume 3. Addison-Wesley, 2ème edition, 1973.
[Mar02]
Olivier Markowitch. Université libre de bruxelles, INFO 090 :
Algorithmique général 1, 2002. http ://www.markowitch.net.
[Nie05]
Tom Niemann, 2005. http ://epaperpress.com/sortsearch/txt/
rbt.txt.
[Sah05]
Sartaj Sahni.
Efficient binary search trees, University
of Florida, COP 5536 : Advanced Data Structures, 2005.
http ://www.cise.ufl.edu/∼sahni/cop5536.
[Sed04]
Robert Sedgewick. Algorithmes en C++. Pearson Education,
2004.
[ST85]
Daniel D. Sleator and Robert E. Tarjan. Self-adjusting binary
search trees. Journal of the ACM, 32 :652–686, 1985.
[SW04]
Daniel D. Sleator and Chengwen C. Wang. Dynamic optimaltity
and multi-splay trees. Technical report, Carnegie Mellon University, 2004.
[Tar85]
Robert E. Tarjan. Amortized computational complexity. SIAM
Journal on Algebraic and Discrete Methods, 6 :306–318, 1985.
[Wei99]
Mark A. Weiss. Data Structures and Algorithm Analysis in Java.
Addison-Wesley, 1999.
[Wil89]
Robert Wilber. Lower bounds for accessing binary search trees
with rotations. SIAM Journal of Computing, 18 :56–67, 1989.
167
Téléchargement