INF431

publicité
INF431
Arbres Binaires Équilibrés
Sujet proposé par Antoine Rauzy
Version: 1989:2075M
L’objectif de cette série d’exercices est de vous entrainer à manipuler des structures de données
récursives, à écrire des procédures sur ces structures et à les prouver.
1
Arbres de Braun
Nous allons commencer par nous intéresser à la mise en œuvre d’une file de priorité à l’aide d’arbres
de Braun.
On considère des arbres constitués d’une part de nœuds binaires, qui portent chacun un élément, et
d’autre part de feuilles, qui ne portent rien. Les éléments sont munis d’un ordre total. Un arbre est dit
« de Braun » s’il vérifie les trois propriétés suivantes :
1. Si la racine est un nœud binaire, alors l’élément qu’elle porte est inférieur ou égal à tous les
éléments situés dans les sous-arbres gauche et droit (propriété du tas) ;
2. Si la racine est un nœud binaire, si s désigne la taille du sous-arbre gauche et t la taille du sousarbre droit, alors on a t ≤ s ≤ t + 1 (propriété d’équilibrage) ;
3. Si la racine est un nœud binaire, alors ses sous-arbres gauche et droit sont eux-mêmes des arbres
de Braun.
Pour choisir de bonnes structures de données pour mettre en œuvre les arbres de Braun, il faut
prendre en compte les deux remarques suivantes :
– Les arbres de Braun sont un containeur (d’objets). Un principe général de bonne programmation
des containeurs est de ne pas donner accès directement aux structures permettant de stocker les
objets. On interpose toujours une classe entre ces structures et l’utilisateur. Toutes les interactions
se font via cette classe (et si besoin des itérateurs). Par exemple, si l’on veut mettre en œuvre une
liste, on créera une classe List qui encapsulera les structures ListCell qui porteront effectivement
les éléments de la liste.
– Les feuilles de l’arbre ne portent pas d’éléments. Elles ne servent en fait qu’à représenter l’arbre
vide. Il est donc inutile de les dupliquer. Une seule feuille suffira.
Dans le cadre de ce cours, nous allons utiliser des structures persistentes. Ce n’est pas forcément le
choix que l’on ferait dans un » vrai« programme (pour des raisons d’efficacité).
Question 1 Proposer une structure de données pour mettre en œuvre les arbres de Braun. On supposera
que les éléments stockés dans l’arbre sont des objets de type KeyedObject possédant une méthode
getKey qui retourne un entier. Les objets seront comparés via cette valeur.
Question 2 Écrire une méthode permettant de vérifier qu’un arbre stocké est équilibré.
Pour insérer un objet dans un arbre de Braun, on applique l’idée suivante :
– Si l’objet a inséré est plus grand que l’objet b stocké à la racine de l’arbre < T1 , b, T2 >, on
l’insère a dans le sous-arbre droit (T2 ). On retourne un nœud portant b, dont le sous-arbre gauche
est l’arbre ainsi obtenu et le sous-arbre droit est T1 .
– Sinon, on opère de même mais en inversant le rôle de a et b.
1
Question 3 Écrire une méthode permettant d’insérer un objet dans un arbre. Quelle est la complexité
de cette méthode ?
Question 4 Que se passe-t-il si on insère les entiers 3, 4, 1, 2, 5, 6.
Extraire le plus petit objet d’un arbre de Braun est plus délicat qu’il peut y paraître au premier abord.
Nous allons procéder en plusieurs étapes.
Question 5 Écrire une méthode efficace permettant d’extraire un objet d’un arbre (n’importe lequel).
L’idée est :
– d’extraire l’objet porté par la racine si l’arbre est constitué d’un nœud unique.
– d’extraire un objet du sous-arbre gauche et de permuter les sous-arbres dans le cas contraire.
Donner la complexité de cette méthode.
Question 6 Écrire une méthode efficace permettant de remplacer le plus petit objet d’un arbre par un
objet donné. Donner la complexité de cette méthode.
Question 7 Écrire une méthode efficace permettant de fusionner deux arbres A et B tels que la taille
de A est égale à la taille de B, ou à la taille de B plus 1. On supposera que ces deux arbres sont des
sous-arbres d’un même arbre et qu’on peut donc les modifier. On utilisera de plus les deux méthodes
précédentes. Donner la complexité de cette méthode.
On a maintenant toutes les briques algorithmiques nécéssaires pour extraire le plus petit objet de
notre file de priorité.
Question 8 Écrire une méthode efficace permettant d’extraire le plus petit élément d’un arbre. Donner
la complexité de cette méthode.
2
Skew Heaps
On a essentiellement la même structure de données d’arbres binaires que précédemment. Un arbre
est soit une feuille <>, soit un nœud < T1 , a, T2 > ou T1 et T2 sont des arbres et a un entier. La notion
de tas (être partiellement ordonné) reste la même. On va utiliser une notion d’équilibrage plus lâche
mais efficace. Prouver son efficacité demandera d’utiliser une complexité amortie.
Le but de cette exercice est de voir encore des invariants récursifs astucieux, et aussi la complexité
amortie.
Les algorithmes d’insertion d’un élément et d’extraction du plus petit élément du tas sont décrits par
les équations récursives suivantes :
extract(< T1 , a, T2 >)
insert(a, T )
merge(T, <>)
merge(<>, T )
merge(T, U )
merge(T, U )
join(< T1 , a, T2 >, U )
=
=
(a, merge(T1 , T2 ))
merge(T, <<>, a, <>>)
= T
= T
=
=
join(T, U )
join(U, T )
si la racine de T est plus petite que la racine de U
sinon
= < T2 , a, merge(T1 , U ) >
Question 9 Montrer que ces algorithmes sont corrects.
Question 10 On insère successivement dans l’arbre vide les entiers 1, 2, 3, 4. Quel est l’arbre obtenu ?
Est-il équilibré ? Que se passe-t-il quand on insère successivement 4, 3, 2, 1 ?
2
On va maintenant analyser la complexité amortie d’une suite quelconque d’insertions et d’extractions dans l’arbre. L’idée générale est qu’une insertion ou une extraction individuelle peut prendre plus
de O(log n) opérations (où n est la taille de l’arbre), mais que la suite elle ne prendra jamais plus que
O(n log n) opérations.
Pour cela, on va définir une notion de budget : chaque fois que l’on effectue une opération, on
s’alloue un budget de O(log n) opérations élémentaires (comparaison de deux nœuds ou création d’un
nœud). Si on ne consomme pas ce budget, on met de côté ce qu’on n’a pas consommé pour plus tard.
On va prouver qu’en agissant de la sorte, on n’est jamais à découvert.
On dit qu’un nœud est bon si son sous-arbre droit est au moins aussi gros que son sous-arbre gauche
et qu’il est mauvais sinon.
Question 11 Donner les équations récursives permettant de compter le nombre de mauvais nœuds d’un
arbre.
On prend la convention que la construction d’un nouveau nœud et une comparaison d’entiers prend
un temps 1 et on note |T | la taille de l’arbre T et t(op) le nombre d’opérations élémentaires nécessaires
pour effectuer l’opération op.
Question 12 Montrez que t(merge(T, U )) est borné par :
2 · (log(|T |) + log(|U |)) + Pt(T ) + Pt(U ) − Pt(merge(T, U ))
Question 13 En déduire que la complexité d’une suite de n d’insertions est en O(n log n).
Discussion : On trouve des arguments de complexité amortie dans les structures de données que
l’on redimensionne en fonction du nombre d’objets stockés, typiquement les tables de hachage ou les
modules de gestion de la mémoire. L’opération de redimensionnement est faite de temps en temps et elle
est relativement coûteuse (typiquement linéraire), mais ce coût est amorti sur le nombre d’opérations. Un
algorithme dont la complexité amortie est satisfaisante peut malgré tout ne pas l’être dans un contexte
temps réel.
3
Téléchargement