Prélude
Cet article est dĂ©diĂ© aux arbres binaires de recherche. RĂ©cemment, j'ai Ă©crit un article sur Ă l'Ă©poque, je n'ai pas beaucoup prĂȘtĂ© attention aux arbres binaires, car les mĂ©thodes de recherche, d'insertion et de suppression n'Ă©taient pas pertinentes. Maintenant, j'ai dĂ©cidĂ© d'Ă©crire un article spĂ©cifiquement sur les arbres. Commençons.
Un arbre est une structure de donnĂ©es composĂ©e de nĆuds reliĂ©s par des arĂȘtes. On peut dire qu'un arbre est un cas particulier d'un graphe. Voici un exemple d'arbre :

Ce n'est pas un arbre binaire de recherche ! Tout est sous le cat !
Terminologie
Racine
La racine de l'arbre est le nĆud le plus Ă©levĂ©. Dans cet exemple, il s'agit du nĆud A. Dans un arbre, il ne peut y avoir qu'un seul chemin menant de la racine Ă n'importe quel autre nĆud ! En rĂ©alitĂ©, tout nĆud peut ĂȘtre considĂ©rĂ© comme la racine de son sous-arbre correspondant.
Parents/enfants
Tous les nĆuds, Ă l'exception de la racine, ont exactement une arĂȘte reliant vers un autre nĆud. Le nĆud situĂ© au-dessus du nĆud actuel s'appelle le parent de ce nĆud. Le nĆud situĂ© en dessous du nĆud actuel et connectĂ© Ă lui s'appelle l'enfant de ce nĆud. Prenons un exemple. Si nous prenons le nĆud B, alors son parent sera le nĆud A, et ses enfants seront les nĆuds D, E et F.
Feuille
Un nĆud qui n'a pas d'enfants sera appelĂ© feuille de l'arbre. Dans l'exemple, les feuilles seront les nĆuds D, E, F, G, I, J, K.
C'est la terminologie de base. D'autres concepts seront abordĂ©s plus loin. Donc, un arbre binaire est un arbre dans lequel chaque nĆud aura au maximum deux enfants. Comme vous l'avez devinĂ©, l'arbre de l'exemple ne sera pas binaire, car les nĆuds B et H ont plus de deux enfants. Voici un exemple d'arbre binaire :

Les nĆuds de l'arbre peuvent contenir n'importe quelle information. Un arbre binaire de recherche est un arbre binaire dont les propriĂ©tĂ©s suivantes sont caractĂ©ristiques :
- Les deux sous-arbres - gauche et droit - sont des arbres binaires de recherche.
- Pour tous les nĆuds du sous-arbre gauche d'un nĆud X donnĂ©, les valeurs des clĂ©s de donnĂ©es sont infĂ©rieures Ă la valeur de la clĂ© de donnĂ©es du nĆud X lui-mĂȘme.
- Pour tous les nĆuds du sous-arbre droit d'un nĆud X donnĂ©, les valeurs des clĂ©s de donnĂ©es sont supĂ©rieures ou Ă©gales Ă la valeur de la clĂ© de donnĂ©es du nĆud X lui-mĂȘme.
ClĂ© une sorte de caractĂ©ristique du nĆud (par exemple, un nombre). La clĂ© est nĂ©cessaire pour pouvoir trouver l'Ă©lĂ©ment de l'arbre correspondant Ă cette clĂ©. Exemple d'arbre binaire de recherche :

Représentation de l'arbre
Au fur et à mesure de l'avancement, je fournirai quelques (possiblement incomplets) extraits de code pour améliorer votre compréhension. Le code complet sera à la fin de l'article.
L'arbre est constituĂ© de nĆuds. La structure d'un nĆud :
public class Node<T> {
private T data;
private int key;
private Node<T> leftChild;
private Node<T> rightChild;
public Node(T data, int key) {
this.data = data;
this.key = key;
}
public Node<T> getLeftChild() {
return leftChild;
}
public Node<T> getRightChild() {
return rightChild;
}
//...autres mĂ©thodes du nĆud
}
Chaque nĆud a deux descendants (il est tout Ă fait possible que les descendants leftChild et/ou rightChild contiennent la valeur null). Vous avez probablement compris que dans ce cas, le nombre data reprĂ©sente les donnĂ©es stockĂ©es dans le nĆud ; key est la clĂ© du nĆud.
Une fois le nĆud compris, parlons des problĂšmes pertinents concernant les arbres. Ici et ci-aprĂšs, par le terme « arbre », je dĂ©signerai un arbre binaire de recherche. La structure de l'arbre binaire :
public class BinaryTree<T> {
private Node<T> root;
//méthodes de l'arbre
}
Comme champ de la classe, nous n'aurons besoin que de la racine de l'arbre, car depuis la racine, en utilisant les mĂ©thodes getLeftChild() et getRightChild(), on peut atteindre n'importe quel nĆud de l'arbre.
Algorithmes dans l'arbre
La recherche
Supposons que vous ayez un arbre construit. Comment trouver un Ă©lĂ©ment avec la clĂ© key ? Il faut se dĂ©placer progressivement de la racine vers le bas de l'arbre et comparer la valeur key avec la clĂ© de chaque nĆud : si key est infĂ©rieur Ă la clĂ© du nĆud courant, passez au descendant gauche du nĆud ; si supĂ©rieur, allez au droit ; si les clĂ©s sont Ă©gales, le nĆud recherchĂ© est trouvĂ© ! Le code correspondant :
public Node<T> find(int key) {
Node<T> current = root;
while (current.getKey() != key) {
if (key < current.getKey())
current = current.getLeftChild();
else
current = current.getRightChild();
if (current == null)
return null;
}
return current;
}
Si current devient Ă©gal Ă null, cela signifie que la recherche a atteint la fin de l'arbre (au niveau conceptuel, vous ĂȘtes dans un endroit inexistant de l'arbre â un descendant d'une feuille).
ConsidĂ©rons l'efficacitĂ© de l'algorithme de recherche dans un arbre Ă©quilibrĂ© (un arbre dans lequel les nĆuds sont rĂ©partis de maniĂšre plus ou moins uniforme). Dans ce cas, l'efficacitĂ© de la recherche sera O(log(n)), avec un logarithme en base 2. Regardez : si l'arbre Ă©quilibrĂ© a n Ă©lĂ©ments, cela signifie qu'il y aura log(n) niveaux dans l'arbre. Et dans la recherche, Ă chaque Ă©tape de la boucle, vous descendez d'un niveau.
Insertion
Si vous avez compris l'essence de la recherche, il vous sera facile de comprendre l'insertion. Il suffit de descendre jusqu'Ă la feuille de l'arbre (en suivant les rĂšgles de descente dĂ©crites dans la recherche) et de devenir son descendant â Ă gauche ou Ă droite, selon la clĂ©. Mise en Ćuvre :
public void insert(T insertData, int key) {
Node current = root;
Node parent;
Node newNode = new Node(insertData, key);
if (root == null)
root = newNode;
else {
while (true) {
parent = current;
if (key < current.getKey()) {
current = current.getLeftChild();
if (current == null) {
parent.setLeftChild(newNode);
return;
}
}
else {
current = current.getRightChild();
if (current == null) {
parent.setRightChild(newNode);
return;
}
}
}
}
}
Dans ce cas, il faut, en plus du nĆud actuel, conserver des informations sur le parent du nĆud actuel. Lorsque current deviendra Ă©gal Ă null, la variable parent contiendra la feuille dont nous avons besoin.
L'efficacité de l'insertion sera, comme pour la recherche, de O(log(n)).
Désinstallation
La suppression est l'opĂ©ration la plus complexe Ă effectuer sur l'arbre. Ăvidemment, il faut d'abord trouver l'Ă©lĂ©ment que nous allons supprimer. Mais ensuite ? Si l'on se contente d'assigner la valeur null Ă sa rĂ©fĂ©rence, nous perdrons l'information sur le sous-arbre dont ce nĆud est la racine. Les mĂ©thodes de suppression d'un arbre sont divisĂ©es en trois cas.
Premier cas. Le nĆud Ă supprimer n'a pas de descendants.
Si le nĆud Ă supprimer n'a pas de descendants, cela signifie qu'il s'agit d'une feuille. On peut donc simplement assigner la valeur null aux champs leftChild ou rightChild de son parent.
DeuxiĂšme cas. Le nĆud Ă supprimer a un descendant.
Ce cas n'est pas trĂšs compliquĂ© non plus. Revenons Ă notre exemple. Supposons que nous devions supprimer l'Ă©lĂ©ment avec la clĂ© 14. Convenez que puisqu'il est le descendant droit du nĆud avec la clĂ© 10, tout descendant de ce nĆud (dans ce cas, le droit) aura une clĂ© supĂ©rieure Ă 10, il est donc facile de "l'Ă©monder" de l'arbre et de relier directement le parent au descendant du nĆud supprimĂ©, c'est-Ă -dire de relier le nĆud avec la clĂ© 10 au nĆud 13. Il en serait de mĂȘme si nous devions supprimer un nĆud qui est le descendant gauche de son parent. RĂ©flĂ©chissez bien Ă cela â c'est une analogie exacte.
TroisiĂšme cas. Le nĆud a deux descendants.
Le cas le plus complexe. Analysons par un nouvel exemple.

Recherche du successeur.
Supposons que nous devons supprimer un nĆud avec la clĂ© 25. Qui allons-nous mettre Ă sa place ? Quelqu'un de ses successeurs (descendants ou descendants des descendants) doit devenir le successeur(celui qui prendra la place du nĆud supprimĂ©).
Comment dĂ©terminer qui doit devenir le successeur ? Il est intuitivement clair que câest le nĆud dans lâarbre dont la clĂ© est la suivante par ordre croissant aprĂšs celle du nĆud supprimĂ©. Lâalgorithme est le suivant. Il faut se rendre Ă son successeur droit (toujours Ă droite, car il a dĂ©jĂ Ă©tĂ© dit que la clĂ© du successeur est supĂ©rieure Ă celle du nĆud supprimĂ©), puis parcourir la chaĂźne des successeurs gauches de ce successeur droit. Dans notre exemple, nous devons passer au nĆud avec la clĂ© 35, puis descendre jusqu'Ă la feuille en suivant la chaĂźne de ses successeurs gauches â dans ce cas, cette chaĂźne ne contient que le nĆud avec la clĂ© 30. Strictement parlant, nous cherchons le nĆud le plus petit dans l'ensemble des nĆuds supĂ©rieurs au nĆud recherchĂ©.

Code de la méthode de recherche du successeur :
public Node<T> getSuccessor(Node<T> deleteNode) {
Node<T> parentSuccessor = deleteNode; // parent du successeur
Node<T> successor = deleteNode; // successeur
Node<T> current = successor.getRightChild(); // simplement un nĆud "courant"
while (current != null) {
parentSuccessor = successor;
successor = current;
current = current.getLeftChild();
}
// Ă la sortie de la boucle, nous avons le successeur et le parent du successeur
if (successor != deleteNode.getRightChild()) { // si le successeur ne coĂŻncide pas avec le successeur droit du nĆud supprimĂ©
parentSuccessor.setLeftChild(successor.getRightChild()); // alors son parent prend le descendant du successeur pour ne pas le perdre
successor.setRightChild(deleteNode.getRightChild()); // relions le successeur au successeur droit du nĆud supprimĂ©
}
return successor;
}
Code complet de la méthode delete :
public boolean delete(int deleteKey) {
Node current = root;
Node parent = current;
boolean isLeftChild = false; // En fonction de savoir si le nĆud Ă supprimer est un enfant gauche ou droit de son parent, la variable boolĂ©enne isLeftChild prendra la valeur true ou false respectivement.
while (current.getKey() != deleteKey) {
parent = current;
if (deleteKey < current.getKey()) {
current = current.getLeftChild();
isLeftChild = true;
} else {
isLeftChild = false;
current = current.getRightChild();
}
if (current == null)
return false;
}
if (current.getLeftChild() == null && current.getRightChild() == null) { // premier cas
if (current == root)
current = null;
else if (isLeftChild)
parent.setLeftChild(null);
else
parent.setRightChild(null);
}
else if (current.getRightChild() == null) { // deuxiĂšme cas
if (current == root)
root = current.getLeftChild();
else if (isLeftChild)
parent.setLeftChild(current.getLeftChild());
else
current.setRightChild(current.getLeftChild());
} else if (current.getLeftChild() == null) {
if (current == root)
root = current.getRightChild();
else if (isLeftChild)
parent.setLeftChild(current.getRightChild());
else
parent.setRightChild(current.getRightChild());
}
else { // troisiĂšme cas
Node successor = getSuccessor(current);
if (current == root)
root = successor;
else if (isLeftChild)
parent.setLeftChild(successor);
else
parent.setRightChild(successor);
}
return true;
}
La complexitĂ© peut ĂȘtre approximĂ©e Ă O(log(n)).
Recherche du maximum/minimum dans l'arbre
Il est évident que pour trouver la valeur minimale/maximale dans l'arbre, il faut parcourir successivement la chaßne des éléments gauche/droit de l'arbre respectivement ; lorsque vous atteindrez une feuille, elle sera l'élément minimal/maximal.
public Node getMinimum(Node startPoint) {
Node current = startPoint;
Node parent = current;
while (current != null) {
parent = current;
current = current.getLeftChild();
}
return parent;
}
public Node getMaximum(Node startPoint) {
Node current = startPoint;
Node parent = current;
while (current != null) {
parent = current;
current = current.getRightChild();
}
return parent;
}
ComplexitĂ© â O(log(n))
Parcours symétrique
Le parcours consiste Ă visiter chaque nĆud de l'arbre dans le but d'effectuer une certaine action avec lui.
L'algorithme de parcours symétrique récursif :
- Effectuer une action avec l'enfant gauche
- Effectuer une action avec soi-mĂȘme
- Effectuer une action avec l'enfant droit
Code :
public void inOrder(Node current) {
if (current != null) {
inOrder(current.getLeftChild());
System.out.println(current.getData() + " ");//Vous pouvez mettre tout ce que vous voulez ici
inOrder(current.getRightChild());
}
}
Conclusion
Enfin ! Si j'ai manqué d'expliquer quelque chose ou si vous avez des remarques, n'hésitez pas à les laisser en commentaire. Comme promis, voici le code complet.
Node.java:
public class Node {
private T data;
private int key;
private Node leftChild;
private Node rightChild;
public Node(T data, int key) {
this.data = data;
this.key = key;
}
public void setLeftChild(Node newNode) {
leftChild = newNode;
}
public void setRightChild(Node newNode) {
rightChild = newNode;
}
public Node getLeftChild() {
return leftChild;
}
public Node getRightChild() {
return rightChild;
}
public T getData() {
return data;
}
public int getKey() {
return key;
}
}
BinaryTree.java:
public class BinaryTree<T> {
private Node<T> root;
public Node<T> find(int key) {
Node<T> current = root;
while (current.getKey() != key) {
if (key < current.getKey())
current = current.getLeftChild();
else
current = current.getRightChild();
if (current == null)
return null;
}
return current;
}
public void insert(T insertData, int key) {
Node<T> current = root;
Node<T> parent;
Node<T> newNode = new Node<>(insertData, key);
if (root == null)
root = newNode;
else {
while (true) {
parent = current;
if (key < current.getKey()) {
current = current.getLeftChild();
if (current == null) {
parent.setLeftChild(newNode);
return;
}
}
else {
current = current.getRightChild();
if (current == null) {
parent.setRightChild(newNode);
return;
}
}
}
}
}
public Node<T> getMinimum(Node<T> startPoint) {
Node<T> current = startPoint;
Node<T> parent = current;
while (current != null) {
parent = current;
current = current.getLeftChild();
}
return parent;
}
public Node<T> getMaximum(Node<T> startPoint) {
Node<T> current = startPoint;
Node<T> parent = current;
while (current != null) {
parent = current;
current = current.getRightChild();
}
return parent;
}
public Node<T> getSuccessor(Node<T> deleteNode) {
Node<T> parentSuccessor = deleteNode;
Node<T> successor = deleteNode;
Node<T> current = successor.getRightChild();
while (current != null) {
parentSuccessor = successor;
successor = current;
current = current.getLeftChild();
}
if (successor != deleteNode.getRightChild()) {
parentSuccessor.setLeftChild(successor.getRightChild());
successor.setRightChild(deleteNode.getRightChild());
}
return successor;
}
public boolean delete(int deleteKey) {
Node<T> current = root;
Node<T> parent = current;
boolean isLeftChild = false;
while (current.getKey() != deleteKey) {
parent = current;
if (deleteKey < current.getKey()) {
current = current.getLeftChild();
isLeftChild = true;
} else {
isLeftChild = false;
current = current.getRightChild();
}
if (current == null)
return false;
}
if (current.getLeftChild() == null && current.getRightChild() == null) {
if (current == root)
current = null;
else if (isLeftChild)
parent.setLeftChild(null);
else
parent.setRightChild(null);
}
else if (current.getRightChild() == null) {
if (current == root)
root = current.getLeftChild();
else if (isLeftChild)
parent.setLeftChild(current.getLeftChild());
else
current.setRightChild(current.getLeftChild());
} else if (current.getLeftChild() == null) {
if (current == root)
root = current.getRightChild();
else if (isLeftChild)
parent.setLeftChild(current.getRightChild());
else
parent.setRightChild(current.getRightChild());
}
else {
Node<T> successor = getSuccessor(current);
if (current == root)
root = successor;
else if (isLeftChild)
parent.setLeftChild(successor);
else
parent.setRightChild(successor);
}
return true;
}
public void inOrder(Node<T> current) {
if (current != null) {
inOrder(current.getLeftChild());
System.out.println(current.getData() + " ");
inOrder(current.getRightChild());
}
}
}
P.S.
Dégénération en O(n)
Beaucoup d'entre vous ont peut-ĂȘtre remarquĂ© : et si nous faisions en sorte que l'arbre devienne dĂ©sĂ©quilibrĂ© ? Par exemple, en ajoutant des nĆuds avec des clĂ©s croissantes : 1,2,3,4,5,6⊠à ce moment-lĂ , l'arbre ressemblera Ă quelque chose de semblable Ă une liste chaĂźnĂ©e. Et oui, l'arbre perdra sa structure arborescente, et par consĂ©quent, son efficacitĂ© d'accĂšs aux donnĂ©es. La complexitĂ© des opĂ©rations de recherche, d'insertion et de suppression deviendra similaire Ă celle d'une liste chaĂźnĂ©e : O(n). C'est lĂ qu'apparaĂźt, selon moi, l'un des principaux inconvĂ©nients des arbres binaires.
Seuls les utilisateurs enregistrés peuvent participer au sondage. , s'il vous plaßt.
Je ne suis pas sur Habr depuis longtemps, et j'aimerais savoir quels sujets d'articles aimeriez-vous voir plus souvent ?
Structures de données
Algorithmes (DP, récursion, compression de données, etc.)
Application des structures de données et des algorithmes dans la vie réelle
Programmation d'applications Android en Java
Programmation d'applications web en Java
2 utilisateurs ont voté. 1 utilisateur s'est abstenu.
Source : habr.com
