Arbre B
En informatique, un arbre B (appelé aussi B-arbre par analogie au terme anglais « B-tree ») est une structure de données en arbre équilibré. Les arbres B sont principalement mis en œuvre dans les mécanismes de gestion de bases de données et de systèmes de fichiers. Ils stockent les données sous une forme triée et permettent une exécution des opérations d'insertion et de suppression en temps toujours logarithmique.
Découvreurs ou inventeurs | |
---|---|
Date de découverte | |
Problème lié | |
Structure des données |
Pire cas |
, , |
---|---|
Moyenne |
, , |
Pire cas | |
---|---|
Moyenne |
Le principe est de permettre aux nœuds parents de posséder plus de deux nœuds enfants : c'est une généralisation de l’arbre binaire de recherche. Ce principe minimise la taille de l'arbre et réduit le nombre d'opérations d'équilibrage. De plus un B-arbre grandit à partir de la racine, contrairement à un arbre binaire de recherche qui croît à partir des feuilles.
Le créateur des arbres B, Rudolf Bayer, n'a pas explicité la signification du « B ». L'explication la plus fréquente est que le B correspond au terme anglais « balanced » (en français : « équilibré »). Cependant, il pourrait aussi découler de « Bayer », du nom du créateur, ou de « Boeing », du nom de la firme pour laquelle le créateur travaillait (Boeing Scientific Research Labs).
Origine
Les arbres B ont été inventés en 1970 par Rudolf Bayer et Edward M.McCreight dans les laboratoires de recherche de Boeing. Le but était de pouvoir gérer les pages d'index de fichiers de données, en tenant compte du fait que le volume des index pouvait être si grand que seule une fraction des pages pouvait être chargée en mémoire vive. Le premier article[2] décrivant le mécanisme des arbres B a été écrit en juillet et publié en .
Structure
Structure générale
Un arbre étiqueté est un arbre (au sens informatique du terme) tel qu'à chaque nœud on associe une étiquette ou clé (ou bien plusieurs étiquettes ou clés dans le cas des arbres B) prise(s) dans un ensemble donné. Donc formellement un arbre étiqueté est un couple formé d'un graphe orienté, acyclique et connexe et d'une fonction d'étiquetage des arbres qui attribue à chaque nœud une étiquette ou une clé. Parmi les arbres étiquetés, un arbre B possède quelques propriétés spécifiques supplémentaires.
Soient L et U deux entiers naturels non nuls tels que L ≤ U. En toute généralité, on définit alors un L-U arbre B[3] de la manière suivante : chaque nœud, sauf la racine, possède un minimum de L−1 clés (appelées aussi éléments), un maximum de U−1 clés et au plus U fils[4]. Pour chaque nœud interne — nœud qui n’est pas une feuille —, le nombre de fils est toujours égal au nombre de clés augmenté d’une unité. Si n est le nombre de fils, alors on parle de n-nœud. Un L-U arbre ne contient que des n-nœuds avec L ≤ n ≤ U. Souvent on choisit la configuration L = t et U = 2 × t : t est appelé le degré minimal de l’arbre B.
De plus, la construction des arbres B garantit qu’un arbre B est toujours équilibré. Chaque clé d’un nœud interne est en fait une borne qui distingue les sous-arbres de ce nœud. Par exemple, si un nœud a 3 fils — lesquels constituent les racines respectives de trois sous-arbres : sous-arbre gauche, sous-arbre du milieu et sous-arbre droit —, alors il a 2 clés notées c1 et c2 qui délimitent les clés de chaque sous-arbre : les clés du sous-arbre gauche seront inférieures à c1 ; les clés du sous-arbre du milieu seront comprises entre c1 et c2 ; les clés du sous-arbre droit seront supérieures à c2.
Implémentation
Un arbre B est implémenté par un arbre enraciné[5]. Un nœud est étiqueté par :
- Un entier qui correspond au nombre de clefs contenues dans le nœud .
- clefs notées .
- Un booléen indiquant si est une feuille ou non.
- pointeurs notés associés aux fils de . Une feuille ne contient pas de pointeurs.
De plus, un arbre B vérifie ces propriétés :
- Toutes les feuilles ont la même profondeur, à savoir la hauteur de l’arbre.
- Si n’est pas une feuille :
- Pour , pour toute clef du fils : .
- pour toute clef du fils : .
- pour toute clef du fils : .
- Si n’est ni une feuille, ni la racine, est compris entre L-1 et U-1.
Exemple de nœud en C++
template<typename T, size_t L>
struct Noeud {
size_t n; // nombre de clés utilisées
bool feuille; // si ce nœud est une feuille
Noeud<T, L>* parent = nullptr; // connaître le parent peut être nécessaire dans certains algorithmes
T cles[L]; // tableau des clés
Noeud<T, L>* branches[L + 1]; // tableau des branches, si ce n'est pas une feuille
};
En pratique
La plupart du temps, la configuration est telle que U = 2 L. On parle alors d’arbre B d'ordre L.
Un arbre B d’ordre t est alors défini plus simplement par un arbre qui satisfait les propriétés suivantes :
- Chaque nœud a au plus clés.
- Chaque nœud qui n’est ni racine ni feuille possède au moins clés.
- Si l'arbre est non vide, la racine est aussi non vide.
- Un nœud qui possède k fils contient k − 1 clefs.
- Toutes les feuilles se situent à la même hauteur.
Opérations
Comme on le verra par la suite, la hauteur d'un B-arbre est logarithmique en le nombre d'éléments. Ainsi, les opérations de recherche, insertion et suppression sont implémentables en O(log n) dans le pire cas, où n est le nombre d'éléments.
Recherche
La recherche est effectuée de la même manière que dans un arbre binaire de recherche. Partant de la racine, on parcourt récursivement l’arbre ; à chaque nœud, on choisit le sous-arbre fils dont les clés sont comprises entre les mêmes bornes que celles de la clé recherchée grâce à une recherche dichotomique.
Pseudo-code
fonction Rechercher(noeud, x):
i = 0
tant que i < noeud.taille et x > noeud.cles[i]:
i += 1
si noeud.feuille:
retourner x == noeud.cles[i]
sinon:
si x == noeud.cles[i]:
retourner Vrai
sinon si i == noeud.taille et x > noeud.cles[noeud.taille - 1]:
retourner Rechercher(noeud.branches[noeud.taille], x)
sinon:
retourner Rechercher(noeud.branches[i], x)
Dans de nombreuses implémentations, l'égalité () entre éléments est remplacée par l'équivalence (). Il faut donc veiller à utiliser des types de données où deux éléments équivalents sont considérés comme égaux.
Insertion
L’insertion nécessite tout d’abord de chercher le nœud où la nouvelle clé devrait être insérée, et l’insérer. La suite se déroule récursivement, selon qu’un nœud ait ou non trop de clés : s’il possède un nombre acceptable de clés, on ne fait rien ; autrement on le transforme en deux nœuds, chacun possédant un nombre minimum de clés, puis on fait « remonter » la clé du milieu qui est alors insérée dans le nœud père. Ce dernier peut du coup se retrouver avec un nombre excessif de fils ; le procédé se poursuit ainsi jusqu’à ce que l’on atteigne la racine. Si celle-ci doit être divisée, on fait « remonter » la clé du milieu dans une nouvelle racine, laquelle génèrera comme nœuds fils les deux nœuds créés à partir de l’ancienne racine, à l’instar de l'étape précédente. Pour que l’opération soit possible, on remarque qu’il faut que U ≥ 2 L ; sinon les nouveaux nœuds ne possèderont pas suffisamment de clés.
Une variante consiste à éclater préventivement chaque nœud « plein » (possédant le nombre maximal de clés) rencontré lors de la recherche du nœud où se réalisera l'insertion. De cette manière on évite une remontée dans l'arbre, puisque l'on assure que le père d'un nœud à scinder en deux peut accueillir une clé supplémentaire. La contrepartie en est une légère augmentation de la hauteur moyenne de l'arbre.
Pseudo-code
fonction Inserer(x,c)
n = nombre de clefs du noeud x
Soit i tel que x.clef[i] > c ou i >= n
Si x est une feuille :
Inserer c dans x.clef a la i-ieme place
Sinon:
Si x.fils[i] est complet:
Scinder x.fils[i]
Si c > x.clef[i]:
i := i+1
FinSi
FinSi
Inserer(x.fils[i],c)
FinSi
FinFonction
Suppression
On doit d’abord chercher la clé à supprimer et la supprimer du nœud qui la contient.
- Si le nœud est interne, on procède de manière similaire aux arbres binaires de recherche en recherchant la clé k la plus à gauche dans le sous-arbre droit de la clé à supprimer ou la plus à droite dans le sous-arbre gauche. Cette clé k appartient à une feuille. On peut la permuter avec la clé à supprimer, que l’on supprime ensuite. Comme elle appartient à une feuille, on se ramène au cas suivant.
- Si le nœud est une feuille, soit il possède encore suffisamment de clés et l’algorithme termine, soit il dispose de moins de L−1 clés et on se trouve dans l’une des deux situations suivantes :
- soit un de ses frères à droite ou à gauche possède suffisamment de clés pour pouvoir en « passer » une à la feuille en question : dans ce cas cette clé remplace la clé qui sépare les deux sous-arbres dans l’arbre père, qui va elle-même dans la feuille en question ;
- soit aucun de ses frères n’a suffisamment de clés : dans ce cas, le père fait passer une de ses clés dans un des deux (ou le seul) frères pour permettre à la feuille de fusionner avec celui-ci. Ceci peut cependant conduire le père à ne plus avoir suffisamment de clés. On réitère alors l’algorithme : si le nœud a un frère avec suffisamment de clés, la clé la plus proche va être échangée avec la clé du père, puis la clé du père et ses nouveaux descendants sont ramenés dans le nœud qui a besoin d’une clé ; sinon on effectue une fusion à l’aide d’une clé du père et ainsi de suite. Si l’on arrive à la racine et qu’elle possède moins de L éléments, on fusionne ses deux fils pour donner une nouvelle racine.
Équilibrage
Notamment après une suppression, l'arbre peut être rééquilibré. Cette opération consiste à répartir équitablement les valeurs dans les différents nœuds de l'arbre et à rétablir les propriétés de remplissage minimum des nœuds.
Le rééquilibrage commence au niveau des feuilles et progresse vers la racine, jusqu'à celle-ci. La redistribution consiste à transférer un élément d'un nœud voisin qui possède suffisamment de valeurs vers le nœud qui en manque. Cette redistribution est appelée rotation. Si aucun voisin ne peut fournir de valeur sans être lui même sous la limite, le nœud déficient doit être fusionné avec un voisin. Cette opération provoque la perte d'un séparateur dans le nœud parent, celui-ci peut alors être en déficit et a besoin d'être équilibré. La fusion et redistribution se propage jusqu'à la racine, seul élément où la déficience en valeurs est tolérée.
Un algorithme d'équilibrage simple consiste à:
- Si le nœud voisin gauche existe et dispose de suffisamment de valeurs pour pouvoir en offrir une, réaliser une rotation gauche.
- Sinon, si le nœud voisin droite existe et dispose de suffisamment d'éléments, réaliser une rotation droite.
- Sinon, le nœud déficient doit être fusionné avec un de ses voisins tel que la somme du nombre de leurs clés plus 1 soit inférieure ou égale à la capacité maximale (). La valeur supplémentaire correspond au séparateur présent dans le parent. Cette opération est toujours possible si avec et ou le contraire, soit un nœud immédiatement sous la limite de clés et un nœud exactement à la limite.
- copier le séparateur à la fin du nœud de gauche
- ajouter tous les éléments du nœud de droite à la fin du nœud de gauche
- effacer le nœud de droite et effacer le séparateur du parent puis vérifier qu'il contient assez d'éléments. Si ce n'est pas le cas, rééquilibrer le parent avec ses voisins.
Rotation gauche
La rotation à gauche d'un cran entre deux nœuds voisins se fait en
- déplaçant le séparateur, présent dans le parent, à la fin du nœud de gauche
- déplaçant le premier élément du nœud de droite en tant que séparateur dans le parent
Ce genre d'opération peut être également utilisé pour compresser l'arbre: un arbre destiné à la lecture seule peut être vidé d'un maximum de cases mémoires inutilisées en remplissant au maximum un minimum de nœuds.
Hauteur dans le pire des cas
Soit le nombre de clefs que contient l’arbre B. La hauteur de l’arbre vérifie l’inégalité :
Remarques
- Les arbres 2-3-4 sont les structures de données d’arbre B les plus utilisées : ils correspondent en fait à des 2-4 arbres B ou arbres B d’ordre 2.
- Les B-arbres présentent l'avantage d'être équilibrés (toutes les feuilles sont à la même hauteur), ce qui permet d'obtenir une majoration de la hauteur et donc de meilleures complexités (en O(log n)) pour les opérations de base (recherche, insertion, suppression) que pour un arbre classique (où l'insertion est en O(h), avec h la hauteur de l'arbre, donc potentiellement en O(n) suivant l'implémentation choisie).
Variantes
L’arbre Arbre B+ (en) diffère légèrement de l’arbre B, en ceci que toutes les données sont stockées exclusivement dans des feuilles, et celles-ci sont reliées entre elles.
D’autres variantes existent également, telles que l’arbre B* (en).
Annexes
- (en) Rudolf Bayer, Binary B-Trees for Virtual Memory, ACM-SIGFIDET Workshop 1971, San Diego, California, Session 5B, pp. 219-235.
- (en) Rudolf Bayer et McCreight, E. M. Organization and Maintenance of Large Ordered Indexes. Acta Informatica 1, 173-189, 1972.
Références
- R. Bayer et E. McCreight, « Organization and maintenance of large ordered indices », SIGFIDET '70: Proceedings of the 1970 ACM SIGFIDET (now SIGMOD) Workshop on Data Description, Access and Control, ACM, , p. 107-141 (ISBN 978-1-4503-7941-0, DOI 10.1145/1734663.1734671)
- (en) R. Bayer et E. McCreight, « Organization and maintenance of large ordered indices », Proceedings of the 1970 ACM SIGFIDET (now SIGMOD) Workshop on Data Description, Access and Control - SIGFIDET '70, ACM Press, , p. 107 (DOI 10.1145/1734663.1734671, lire en ligne, consulté le )
- L-U arbre B doit se lire « L U arbre B », car L-U représente une expression composée, et non pas la soustraction de deux nombres.
- « L−1 » et « U−1 » figurent ici des expressions de soustraction.
- (en) Thomas H.Cormen, Introduction to algorithms, 1989, pp.485-504.
Voir aussi
Liens externes
- (en) cs.usfca.edu : animation permettant d'insérer et de supprimer visuellement des éléments dans un arbre B
- (it) (en) B-tree GUI : Spataro Fabrizio e Todaro Michelangelo – Emulatore Java BTree – BTree Java Emulator.
- (en) Slady.net : animation sous forme d’applet Java permettant de construire visuellement des arbres B.
- Portail de l'informatique théorique