Aller au contenu

Arbre B

Un article de Wikipédia, l'encyclopédie libre.
Arbre B
Exemple d'un 3-5 B-arbre
Découvreurs ou inventeurs
Date de découverte
Problème lié
Structure des données
Complexité en temps
Pire cas
, , Voir et modifier les données sur Wikidata
Moyenne
, , Voir et modifier les données sur Wikidata
Complexité en espace
Pire cas
Voir et modifier les données sur Wikidata
Moyenne
Voir et modifier les données sur Wikidata

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.

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 arbre B 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).

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 générale

[modifier | modifier le code]

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 faiblement 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

[modifier | modifier le code]

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++

[modifier | modifier le code]
template<typename T, size_t U>
struct Noeud {
    size_t n;           // nombre de clés utilisées
    bool feuille;       // si ce nœud est une feuille
    Noeud<T, U>* parent = nullptr;  // connaître le parent peut être nécessaire dans certains algorithmes
    T cles[U-1];          // tableau des clés
    Noeud<T, U>* branches[U];   // tableau des branches, si ce n'est pas une feuille
};

En pratique

[modifier | modifier le code]

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 :

  1. Chaque nœud a au plus clés.
  2. Chaque nœud qui n’est ni racine ni feuille possède au moins clés.
  3. Si l'arbre est non vide, la racine est aussi non vide.
  4. Un nœud qui possède k fils contient k − 1 clefs.
  5. Toutes les feuilles se situent à la même hauteur.

Opérations

[modifier | modifier le code]

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.

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

[modifier | modifier le code]
fonction Rechercher(noeud, x):
    i = 0
    tant que i < noeud.taille et x > noeud.cles[i]:
        i += 1
        
    si noeud.feuille:
        retourner i < noeud.taille et x == noeud.cles[i]
    sinon:
        si i < noeud.taille et x == noeud.cles[i]:
            retourner Vrai
        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.

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

[modifier | modifier le 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

[modifier | modifier le code]

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

[modifier | modifier le code]

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.
    1. copier le séparateur à la fin du nœud de gauche
    2. ajouter tous les éléments du nœud de droite à la fin du nœud de gauche
    3. 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

[modifier | modifier le code]

La rotation à gauche d'un cran entre deux nœuds voisins se fait en

  1. déplaçant le séparateur, présent dans le parent, à la fin du nœud de gauche
  2. 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

[modifier | modifier le code]

Soit le nombre de clefs que contient l’arbre B. La hauteur de l’arbre vérifie l’inégalité :

  • 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).

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) — pour lequel on exige que le taux de remplissage minimal des nœuds internes soit de 2/3 — ou l’arbre B*+ (en) (qui combine les arbres B+ et B*).

Références

[modifier | modifier le code]
  1. 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).Voir et modifier les données sur Wikidata
  2. (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 )
  3. 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.
  4. « L−1 » et « U−1 » figurent ici des expressions de soustraction.
  5. (en) Thomas H.Cormen, Introduction to algorithms, 1989, pp.485-504.

Sur les autres projets Wikimedia :

Liens externes

[modifier | modifier le code]
  • (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 BTreeBTree Java Emulator.
  • (en) Slady.net : animation sous forme d’applet Java permettant de construire visuellement des arbres B.