Les arbres binaires de recherche, ou bien ABR, sont des structures de données qui nous permettent d'optimiser certaines opérations, qui seraient trop coûteuses si appliquées sur d'autres structures de données, comme les listes chaînées.
Pour cela, ce projet à pour but de confirmer cette hypothèse. Ainsi nous analyserons les complexité en temps des algorithmes de type CRUD sur les arbres binaires, nous nous intéresserons plus particulièrement aux opérations d'insertion, de recherche et de suppression.
.
├── arbre.dot
├── Element.java
├── Main.java
├── report.md
└── Tree.java
├── LinkedList.java
└── Node.java
7 files
./Element.java
C'est la classe élément qui représente la valeur/contenu des nœuds de nos arbres binaires.
Chaque élément à une clé entière supposé strictement positive, et inspirée des clé primaire entière des SGBD relationnel.
De même, chaque élément à une valeur à lui, qui est dans ce cas, un nombre réel, modélisé par le typedouble
.
./Node.java
C'est la classe nœud qui représente les nœuds de nos listes chaînées.
Chaque nœud à une valeur entière, qui contiendra les valeurs des clés de nos éléments lors des tests.
Chaque nœud à un attribut suivant (next) de même nature, qui contiendra la référence vers son prochain dans la liste.
./Tree.java
C'est la classe arbre (tree) qui représente les nœuds de notre arbre binaire de recherche.
Chaque nœud à un fils gauche et un fils droit, qui respectent la loi d'ordre d'un ABR.
Aussi, chaque nœud à une instance de la classeElement
qui représente la valeur de ce nœud.
Note: Cette classe représente aussi bien un nœud qu'un arbre, étant donné que chaque nœud peut être potentiellement un sous-arbre à l'arbre auquel il appartient.
./LinkedList.java
C'est la classe liste chainée (LinkedList) qui représentera les listes utilisées lors des tests.
Chaque élément de la liste est une instance de la classe Node.
On a choisi cette classe, même si elle utilise pas la classeElement
, car elle contenait déjà toutes les méthodes d'ajout, suppression, et de recherche. De plus, lors de ces opérations, il y a que la clé qui nous intéresse et non le contenu, donc la classElement
n'est pas nécessaire. (pour la phase de tests)
./Main.java
C'est la classe
Main
qui sera notre contrôleur, où on testera toutes nos méthodes.
./arbre.dot
C'est le fichier
arbre.dot
qui contiendra notre arbre binaire sous forme d'un pseudo code, afin de générer une visualisation utilisant l'outil xdot.
Exemple d'un arbre binaire de recherche affiché par l'outil xdot.
# cela peut être obtenu avec la commande
xdot arbre.dot
Soit A
, un arbre binaire de recherche complet.
Soit n
, le nombre d'élément de notre arbre.
Soit el
, un élément.
Soit k
, le nombre d'opérations élémentaires nécessaires à notre algorithme.
Soit h
, la hauteur à laquelle el
se trouve dans A
.
Soit nbNoeuds(hauteur)
, une fonction qui nous renvoie le nombres de nœuds entre la racine et une certaine hauteur passé en paramètre.
Note: on prendra en considération que les tests conditionnels comme opérations élémentaires.
Note: Par la suite, log(n)
représentent le logarithm en base 2 de n
.
Pour insérer el
dans l'arbre A
, on doit faire un parcours sur l'arbre afin de trouver le bon
emplacement pour insérer le nouvel élément.
Pour comprendre la complexité de l'insertion, il suffit de comprendre la nature de ce parcours.
Lors d'un parcours précédant une insertion, on se retrouve en train d'exploiter la loi d'ordre de notre arbre,
afin de décider si on doit continuer vers la droite ou vers la gauche. Cela, afin d'arriver à ajouter une feuille dans notre arbre qui contiendra el
.
Ayant un arbre binaire, et étant donné que cette décision est prise à chaque fois qu'on visite un nœud qui n'est pas une feuille, on conclut que le nombre d'éléments à visiter diminue de moitié (50%) à chaque étape. (une étape est quand on visite un nœud qui n'est pas une feuille.)
Par conséquent, notre parcours d'insertion doit visiter log(n)
d'éléments avant d'arriver au bon emplacement.
D'autre part, à chaque étape de prise de décision, on effectue un nombre k = 3
de tests conditionnels, donc, le nombre d'opérations élémentaires durant un parcours d'insertion est k * log(n)
.
On déduit que la complexité en temps d'une insertion dans un arbre binaires et de l'ordre de O(log(n))
, ce qui confirme la complexité théorique de ce type de donnée abstrait.
Lors d'une recherche, tout comme une insertion, on doit faire un parcours dans A
afin de trouver
l'emplacement de el
.
Il se trouve que la nature de ce parcours est similaire à celle du parcours effectué lors d'une insertion. Autrement dit, dans une recherche à chaque étape on va aussi diviser de moitié le nombre d'éléments dans lesquels la recherche continuera.
Toutefois, dans la recherche, on ne s'intéresse pas qu' aux feuilles, mais aussi aux nœuds qui se trouvent entre la racine et les feuilles. Par conséquent, un nouveau paramètre doit être pris en compte, et c'est la hauteur h
ou se situe el
.
Du coup, le nombre d'élément à parcourir est égale à log(nbNoeuds(h))
, et cela car le parcour à une nature dichotomique et l'élément recherché est à la hauteur h
.
Pour la suite, il suffit de calculer la valeur retourné par l'appel à la fonction nbNoeuds(h)
.
Ayant un ABR, et si on suppose que le dénombrement de la hauteur commence à la valeur 0, on sait qu' à une certaine hauteur, on retrouve un nombre de noeuds égale à 2^h
, ces nœuds partagent tous la même hauteur.
Exemple, si on suppose que A
est un arbre binaire complet, alors à la hauteur h = 3
, on retrouvera 2³ = 8
noeuds qui partagent la hauteur h
.
Par conséquent, le nombre de noeuds entre la hauteur h
et la racine est égale à somme(2⁰ + 2¹ + ... + 2^h)
. Cela est une somme de termes consécutifs d'une suite géométrique de raison 2. Donc, nbNoeuds(h) = 2^(h+1) - 1
.
D'autre part, le nombre d'opérations conditionnelles effectuées par parcours est égal à k = 3
.
Partant de cela, on déduit que la complexité temporelle d'un parcour de recherche suit la fonction k * (log(2^(h+1) - 1))
.
On conclut que la complexité temporelle est de l'ordre de O(log(n))
, ce qui confirme la complexité théorique d'une recherche dans ce type de donnée abstrait.
Pour la suppression dans un ABR, c'est un peu plus complexe à décrire et à étudier, étant donné qu'il y a plusieurs algorithmes qu'on peut mettre en place.
Note: J'ai moi même proposé à ma professeur, Madame Véronique Jay, une autre approche pour ce type d'opération, qui consiste à convertir notre ABR à une liste de noeuds énumérés suivant un parcour prefix, puis à supprimer l'élément voulu de la liste, et enfin, recréer l'arbre en défilant du début.
Par ailleurs, étant donné que cette méthode est très coûteuse, on a décidé d'implémenter un algorithme de suppression in-place
, qui permet de supprimer l'élément dans l'arbre directement sans structure de données annexe.
Pour la suite, on définit d'abord les différents cas à traiter lors d'une suppression dans un ABR.
les cas sont les suivants:
- Si
el
est une feuille, on le supprime directement. - Si
el
a qu'un seul fils, on le supprime et on le remplace avec le nœud de son fils. - Si
el
a deux fils, on suit les étapes suivantes:- On cherche le prochain élément qui vient après
el
lors d'une énumération ordonnée des nœuds de l'ABR. Forcément ce successeur est une feuille, ou un parent à un fil droit uniquement. - Si le successeur est parent, on le remplace par son fils droit, si non on le supprime de son emplacement.
- Enfin, on remplace la valeur de
el
par celle de son successeur, qu'on devait garder en mémoire.
- On cherche le prochain élément qui vient après
Pour les deux premiers scénarios, l'ordre de grandeur des complexités temporelles est le même avec celles d'un parcour de recherche. Autrement dit, la complexité est de l'ordre de O(log(n))
.
Pour le dernier cas, il faut additionner à cela la complexité de la recherche du successeur, or qu'on sait que la recherche est de l'ordre de grandeur de O(log(n))
.
Par conséquent, on peut déduire, même si n
représente une valeurs différente, que l'ordre de grandeur de la suppression dans ce cas est de C * O(log(n))
, avec C
étant une constante.
En conclusion, la complexité temporelle est de l'ordre de O(log(n))
, ce qui confirme la complexité théorique d'une suppression dans ce type de donné abstrait.
Note: ces complexité ont étaient calculées dans le cas ou A
est un ABR complet, or qu'un arbre binaire peut facilement se transformer en une liste, ce qui aggraverait nos complexités, étant donné qu'on aura un ordre de grandeur de O(n)
, dans la pire des situations.
On crée notre arbre binaire de recherche avec un nombre d'éléments égal à 10 000.
Les valeurs des clés sont tirées aléatoirement d'un interval d'entiers entre 0 et 10 000 000, afin d'avoir un minimum de duplications.
D'autre part, on choisit parmi ces valeurs 1000 d'entre elles à supprimer plus tard.
Enfin, on rassemble 1000 valeurs aléatoires qui ne devrait pas être dans l'arbre, afin de tester l'insertion.
Note:
- L'insertion dans les listes chaînées se fait en tête. Par conséquent, l'ordre n'est pas une priorité.
➜ binarySearchTrees git:(master) ✗ java Main
BinarySearchTree Insertions Done in 1762micro seconds
LinkedList Insertions Done in 80micro seconds
BinarySearchTree Deletion Done in 1402micro seconds
LinkedLists Deletion Done in 72600micro seconds
Soit N
le nombre d'éléments.
- Pour l'insertion:
Les résultats sont conformes aux attentes, étant donné que l'insertion est plus rapide dans les listes que dans les arbres. Cela à pour
cause le fait que dans les listes l'insertion se fait en tête, qui donne une complexité de l'ordre de O(1)
, alors que dans l'arbre
une insertion est basée sur un parcour dichotomique de la structure, ce qui nous fait une complexité de O(log(N))
.
- Pour la suppression:
Les résultats sont conformes aux attentes, étant donné que la suppression est plus rapide dans les arbres binaires. Cela à pour cause le fait que dans les listes la suppression est toujours précédée par un parcour linéaire qui donne une complexité de l'ordre de O(N)
, alors que dans l'arbre, une suppression est basée sur un parcour dichotomique de la structure, potentiellement deux, ce qui nous fait une complexité de O(log(N))
.