Intuitivement, vous pouvez imaginer une arborescence indexée binaire comme une représentation comprimée d'une arborescence binaire, qui est elle-même une optimisation d'une représentation de tableau standard. Cette réponse entre dans une dérivation possible.
Supposons, par exemple, que vous souhaitiez stocker des fréquences cumulées pour un total de 7 éléments différents. Vous pouvez commencer par écrire sept paniers dans lesquels les numéros seront distribués:
[ ] [ ] [ ] [ ] [ ] [ ] [ ]
1 2 3 4 5 6 7
Supposons maintenant que les fréquences cumulées ressemblent à ceci:
[ 5 ] [ 6 ] [14 ] [25 ] [77 ] [105] [105]
1 2 3 4 5 6 7
En utilisant cette version du tableau, vous pouvez incrémenter la fréquence cumulée de n’importe quel élément en augmentant la valeur du nombre stocké à cet endroit, puis en incrémentant les fréquences de tout ce qui vient après. Par exemple, pour augmenter la fréquence cumulée de 3 sur 7, nous pourrions ajouter 7 à chaque élément du tableau à ou après la position 3, comme indiqué ci-dessous:
[ 5 ] [ 6 ] [21 ] [32 ] [84 ] [112] [112]
1 2 3 4 5 6 7
Le problème avec ceci est que cela prend O (n) temps, ce qui est assez lent si n est grand.
Une façon de penser à l’amélioration de cette opération serait de changer ce que nous stockons dans les compartiments. Plutôt que de stocker la fréquence cumulée jusqu'à un point donné, vous pouvez plutôt penser à stocker uniquement le montant de l'augmentation de la fréquence actuelle par rapport au compartiment précédent. Par exemple, dans notre cas, nous réécririons les compartiments ci-dessus comme suit:
Before:
[ 5 ] [ 6 ] [21 ] [32 ] [84 ] [112] [112]
1 2 3 4 5 6 7
After:
[ +5] [ +1] [+15] [+11] [+52] [+28] [ +0]
1 2 3 4 5 6 7
Nous pouvons maintenant incrémenter la fréquence dans un intervalle dans le temps O (1) en ajoutant simplement la quantité appropriée à cet intervalle. Cependant, le coût total de la recherche devient maintenant O (n), car nous devons recalculer le total dans le compartiment en additionnant les valeurs de tous les compartiments plus petits.
La première idée importante que nous devons tirer d’ici un arbre indexé binaire est la suivante: au lieu de recalculer continuellement la somme des éléments du tableau qui précèdent un élément particulier, qu’en serait-il si nous devions pré-calculer la somme totale de tous les éléments avant points dans la séquence? Si nous pouvions le faire, nous pourrions alors calculer la somme cumulée à un moment donné en résumant simplement la bonne combinaison de ces sommes précalculées.
Une façon de procéder consiste à changer la représentation, qui passe d’un tableau de compartiments à un arbre binaire de nœuds. Chaque nœud sera annoté avec une valeur qui représente la somme cumulative de tous les nœuds situés à gauche de ce nœud donné. Par exemple, supposons que nous construisions l'arborescence binaire suivante à partir de ces nœuds:
4
/ \
2 6
/ \ / \
1 3 5 7
Maintenant, nous pouvons augmenter chaque noeud en stockant la somme cumulative de toutes les valeurs, y compris ce noeud et son sous-arbre de gauche. Par exemple, compte tenu de nos valeurs, nous stockons les éléments suivants:
Before:
[ +5] [ +1] [+15] [+11] [+52] [+28] [ +0]
1 2 3 4 5 6 7
After:
4
[+32]
/ \
2 6
[ +6] [+80]
/ \ / \
1 3 5 7
[ +5] [+15] [+52] [ +0]
Étant donné cette arborescence, il est facile de déterminer la somme cumulée jusqu’à un certain point. L'idée est la suivante: nous maintenons un compteur, initialement 0, puis effectuons une recherche binaire normale jusqu'à trouver le nœud en question. Ce faisant, nous avons également les éléments suivants: chaque fois que nous nous déplaçons à droite, nous ajoutons également la valeur actuelle au compteur.
Par exemple, supposons que nous recherchions la somme pour 3. Pour cela, nous procédons comme suit:
- Commencez à la racine (4). Le compteur est 0.
- Allez à gauche au noeud (2). Le compteur est 0.
- Allez à droite au noeud (3). Le compteur est 0 + 6 = 6.
- Trouver le noeud (3). Le compteur est 6 + 15 = 21.
Vous pouvez également imaginer exécuter ce processus à l’inverse: en commençant par un nœud donné, initialisez le compteur à la valeur de ce nœud, puis accédez à l’arborescence jusqu’à la racine. Chaque fois que vous suivez un lien enfant droit vers le haut, ajoutez la valeur sur le nœud auquel vous arrivez. Par exemple, pour trouver la fréquence pour 3, nous pourrions procéder comme suit:
- Commencez au noeud (3). Le compteur a 15 ans.
- Montez à noeud (2). Le compteur est 15 + 6 = 21.
- Montez au noeud (4). Le compteur a 21 ans.
Pour incrémenter la fréquence d'un nœud (et, implicitement, les fréquences de tous les nœuds qui le suivent), nous devons mettre à jour l'ensemble des nœuds de l'arborescence comprenant ce nœud dans son sous-arbre de gauche. Pour ce faire, procédez comme suit: incrémentez la fréquence de ce nœud, puis commencez à vous déplacer à la racine de l’arbre. Chaque fois que vous suivez un lien qui vous prend comme enfant de gauche, incrémentez la fréquence du nœud que vous rencontrez en ajoutant la valeur actuelle.
Par exemple, pour incrémenter la fréquence du nœud 1 de cinq, procédez comme suit:
4
[+32]
/ \
2 6
[ +6] [+80]
/ \ / \
> 1 3 5 7
[ +5] [+15] [+52] [ +0]
À partir du noeud 1, incrémentez sa fréquence de 5 pour obtenir
4
[+32]
/ \
2 6
[ +6] [+80]
/ \ / \
> 1 3 5 7
[+10] [+15] [+52] [ +0]
Maintenant, allez à son parent:
4
[+32]
/ \
> 2 6
[ +6] [+80]
/ \ / \
1 3 5 7
[+10] [+15] [+52] [ +0]
Nous avons suivi un lien enfant de gauche vers le haut, nous avons donc incrémenté la fréquence de ce nœud également:
4
[+32]
/ \
> 2 6
[+11] [+80]
/ \ / \
1 3 5 7
[+10] [+15] [+52] [ +0]
Nous allons maintenant à son parent:
> 4
[+32]
/ \
2 6
[+11] [+80]
/ \ / \
1 3 5 7
[+10] [+15] [+52] [ +0]
C'était un lien enfant de gauche, alors nous incrémentons également ce noeud:
4
[+37]
/ \
2 6
[+11] [+80]
/ \ / \
1 3 5 7
[+10] [+15] [+52] [ +0]
Et maintenant nous avons fini!
La dernière étape consiste à convertir cette arborescence en un arbre indexé binaire, et c’est là que nous pouvons faire des choses amusantes avec des nombres binaires. Réécrivons chaque index de compartiment de cette arborescence en binaire:
100
[+37]
/ \
010 110
[+11] [+80]
/ \ / \
001 011 101 111
[+10] [+15] [+52] [ +0]
Ici, nous pouvons faire une observation très, très cool. Prenez n'importe lequel de ces nombres binaires et recherchez le tout dernier 1 qui a été défini dans le nombre, puis supprimez ce bit, ainsi que tous les bits qui le suivent. Vous êtes maintenant avec ce qui suit:
(empty)
[+37]
/ \
0 1
[+11] [+80]
/ \ / \
00 01 10 11
[+10] [+15] [+52] [ +0]
Voici une observation vraiment très intéressante: si vous traitez 0 comme signifiant "à gauche" et 1 comme "correct", les bits restants de chaque nombre précisent exactement comment commencer à la racine et revenir ensuite à ce nombre. Par exemple, le noeud 5 a le motif binaire 101. Le dernier 1 est le dernier bit, nous allons donc laisser tomber pour obtenir 10. En effet, si vous commencez à la racine, allez à droite (1), puis à gauche (0), vous terminez au noeud 5!
La raison pour laquelle cela est important est que nos opérations de recherche et de mise à jour dépendent du chemin d'accès du nœud à la racine et du fait que nous suivons les liens enfants de gauche ou de droite. Par exemple, lors d'une recherche, nous nous soucions seulement des bons liens que nous suivons. Lors d'une mise à jour, nous nous soucions seulement des liens de gauche que nous suivons. Cet arbre indexé binaire fait tout cela très efficacement en utilisant simplement les bits de l'index.
L'astuce clé est la propriété suivante de cet arbre binaire parfait:
Étant donné le nœud n, le nœud suivant sur le chemin d'accès remontant à la racine dans laquelle nous allons à droite est donné en prenant la représentation binaire de n et en supprimant le dernier 1.
Par exemple, examinez le chemin d'accès du noeud 7, qui est le 111. Les noeuds du chemin d'accès à la racine que nous prenons et qui impliquent de suivre un pointeur droit se trouvent vers le haut.
- Noeud 7: 111
- Noeud 6: 110
- Noeud 4: 100
Ce sont tous des bons liens. Si nous prenons le chemin d'accès pour le nœud 3, qui est 011, et regardons les nœuds où nous allons à droite, nous obtenons
- Noeud 3: 011
- Noeud 2: 010
- (Nœud 4: 100, qui suit un lien à gauche)
Cela signifie que nous pouvons calculer très, très efficacement la somme cumulée d’un nœud comme suit:
- Ecrivez le noeud n en binaire.
- Placez le compteur à 0.
- Répétez la procédure suivante lorsque n n 0:
- Ajoutez la valeur au noeud n.
- Effacer le bit le plus à droite de n.
De même, réfléchissons à la manière dont nous ferions une étape de mise à jour. Pour ce faire, nous voudrions suivre le chemin d'accès jusqu'à la racine, en mettant à jour tous les nœuds où nous avons suivi un lien de gauche vers le haut. Nous pouvons le faire en faisant essentiellement l’algorithme ci-dessus, mais en commutant tous les 1 en 0 et les 0 en 1.
La dernière étape de l’arborescence indexée binaire consiste à noter qu’en raison de cette astuce au niveau des bits, il n’est même plus nécessaire de stocker l’arborescence explicitement. Nous pouvons simplement stocker tous les nœuds dans un tableau de longueur n, puis utiliser les techniques de rotation par bits pour naviguer de manière implicite dans l'arbre. En fait, c'est exactement ce que fait l'arbre indexé au niveau du bit - il stocke les nœuds dans un tableau, puis utilise ces astuces au niveau du bit pour simuler efficacement la progression vers le haut dans cet arbre.
J'espère que cela t'aides!