Avantages de l'arbre binaire
Quel est l’avantage de stocker les données sous forme d’arbre de recherche binaire ? Imaginez que nous ayons un ouvrage de référence de 100 pages et que nous devions trouver une page spécifique qui contiendra les données dont nous avons besoin. Grâce au contenu, nous savons également de quelle page spécifique nous avons besoin. Si nous suivons le chemin habituel, nous devrons tourner une page à la fois jusqu'à arriver à celle dont nous avons besoin. Autrement dit, nous parcourrons de 1 à 100 pages jusqu'à ce que nous nous trouvions au bon endroit. Par exemple, si nous avons besoin de la page 77, nous aurons alors 77 recherches. Si nous parlons de complexité temporelle, alors ce sera O(N) . Mais cela peut être fait plus efficacement, n’est-ce pas ? Essayons de faire la même chose, mais en utilisant la recherche binaire :- Nous divisons le répertoire en deux parties, la première - de 1 à 50, la seconde - 51-100. Nous savons exactement dans quelle partie de ces parties se trouve notre page : si nous reprenons la page 77, elle se trouve dans la deuxième partie du livre.
- Ensuite, nous considérons la deuxième partie et la divisons en deux - de 51 à 75, de 76 à 100. Dans ce cas, notre page sera à nouveau dans la seconde moitié, entre 76 et 100.
- Ensuite, nous divisons cet intervalle par 2 et obtenons 76-88 et 89-100. La page s'insère dans le premier espace, nous supprimons donc le second.
- Viennent ensuite les intervalles : 76-81 et 82-88, prenez le premier.
- 76-78 et 79-81, prenez le premier.
- 76 et 77-78, prenez le deuxième.
- 77 et 78, prenez le premier et récupérez notre page - 77.
Règles de construction d'un arbre de recherche binaire
L'arbre de recherche binaire est construit selon certaines règles :- chaque nœud a au plus deux enfants ;
- chaque valeur inférieure à la valeur du nœud devient l'enfant de gauche ou l'enfant de l'enfant de gauche ;
- chaque valeur supérieure ou égale à la valeur du nœud devient le bon enfant ou l'enfant du bon enfant.
- tous les nœuds n'ont pas plus de deux héritiers, la première condition est remplie ;
- si l'on considère, par exemple, un nœud avec la valeur 7 (ou toute autre), nous verrons que toutes les valeurs des éléments dans le sous-arbre de gauche seront inférieures et dans celui de droite - plus. Cela signifie que les conditions 2 et 3 sont remplies.
Rechercher un élément
Lors de la recherche d'un élément avec une valeur donnée, on part de l'élément racine :- S'il est égal à la valeur recherchée, l'élément racine est celui recherché ; sinon, on compare les valeurs de la racine et de l'élément recherché.
- Si l’élément recherché est plus grand, on se déplace vers l’enfant de droite ; sinon, on se déplace vers l’enfant de gauche.
- Si l'élément n'est pas trouvé, appliquez les étapes 1 et 2, mais à l'enfant (droite ou gauche) jusqu'à ce que l'élément soit trouvé.
- on le compare avec l'élément racine, on voit que l'élément racine est plus grand, donc on se déplace vers l'enfant de gauche, qui a une valeur de 3 ;
- on compare celui que l'on recherche et l'élément de valeur 3, on voit que celui que l'on recherche est plus grand, il faut donc le bon descendant de l'élément en question, à savoir l'élément de valeur 5 ;
- Nous comparons ce descendant avec celui que nous recherchons et voyons que les valeurs sont égales - l'élément est trouvé.
Insérer un élément
L'algorithme d'insertion est également très, très simple :-
On compare le nouvel élément avec celui racine (s'il n'existe pas, le nouvel élément est celui racine).
-
Si un nouvel élément :
- est inférieur, alors on passe à l'héritier gauche, s'il n'y en a pas, le nouvel élément prend la place de l'héritier gauche, et l'algorithme est complété ;
- est supérieur ou égal à la racine, alors on passe à l'héritier droit. Et de même, si cet élément n’existe pas, alors un nouvel élément prendra la place du bon élément et l’algorithme sera complété.
-
Pour le nouvel élément en question, qui se trouvait à droite ou à gauche de l'étape précédente, répétez les étapes 1 et 2 jusqu'à ce que l'élément inséré soit en place.
A titre d'exemple, nous voudrons insérer dans l'arbre considéré ci-dessus, un élément de valeur 11 :
- nous comparons avec l'élément racine 7 et voyons que l'élément racine est plus petit, nous passons donc à l'héritier droit ;
- l'élément suivant considéré a une valeur de 9, ce qui est inférieur au nouveau 11, nous passons donc à l'héritier de droite ;
- l'héritier droit a une valeur de 10, ce qui est encore moins, on passe donc au premier élément, et comme il n'est pas là, un nouvel élément de valeur 11 est placé à cet endroit.
↓
Supprimer un élément
Peut-être que de toutes les opérations avec des arbres de recherche binaires, la suppression est la plus difficile. Tout d'abord, il y a une recherche de l'élément à supprimer, après quoi suit une étape qui peut avoir trois variantes :-
Le nœud à supprimer est un nœud feuille (n'a pas d'enfant).
Peut-être le plus simple. Tout se résume au fait que nous le coupons simplement de l'arbre, sans manipulation inutile. Par exemple, de notre arbre nous supprimons le nœud avec la valeur 8 :
↓ -
Le nœud en cours de suppression a un enfant.
Dans ce cas, nous supprimons le nœud sélectionné et mettons son descendant à sa place (en substance, nous coupons simplement le nœud sélectionné de la chaîne). À titre d'exemple, supprimons un nœud de valeur 9 de l'arborescence :
↓ -
Le nœud en cours de suppression a deux enfants.
La partie la plus intéressante. Après tout, si le nœud supprimé a deux enfants à la fois, vous ne pouvez pas simplement le remplacer par l'un de ces enfants (surtout si l'enfant a ses propres enfants).
Exemple : dans l'arborescence ci-dessus, quel nœud doit être l'enfant gauche du nœud 3 ?
Si vous y réfléchissez un peu, il devient évident qu'il doit s'agir d'un nœud de valeur 4. Mais que se passe-t-il si l'arbre n'est pas si simple ? S’il détient des centaines de valeurs, sera-t-il aussi simple de déterminer qui sera le successeur ?
Il est clair que non. Par conséquent, nous avons besoin de notre propre algorithme de recherche de petits récepteurs :
- Il faut d’abord aller au bon enfant du nœud sélectionné, dont la valeur doit être supérieure à la valeur du nœud.
- Ensuite, nous nous déplaçons vers l'enfant gauche de l'enfant droit (s'il en existe un), puis vers l'enfant gauche de l'enfant gauche, etc., en suivant la chaîne des enfants gauche.
- En conséquence, le dernier enfant restant sur ce chemin sera le successeur du nœud d'origine.
Généralisons un peu ce petit algorithme : dans le sous-arbre du fils droit du nœud source, tous les nœuds sont plus grands que celui en cours de suppression, ce qui peut être compris à partir des règles de base de l'arbre de recherche binaire. Dans ce sous-arbre, nous recherchons la plus petite valeur.
En d’autres termes, nous recherchons le plus petit nœud dans un ensemble de nœuds plus grand que le nœud d’origine. Ce plus petit nœud parmi les plus grands sera le successeur le plus approprié.
À quoi ressemblera l'arborescence après la suppression du nœud de valeur 3 :
↓
class Node {
private int value; // ключ узла
private Node leftChild; // Левый узел потомок
private Node rightChild; // Правый узел потомок
public void printNode() { // Вывод значения узла в консоль
System.out.println(" Выбранный узел имеет meaning :" + value);
}
public int getValue() {
return this.value;
}
public void setValue(final int value) {
this.value = value;
}
public Node getLeftChild() {
return this.leftChild;
}
public void setLeftChild(final Node leftChild) {
this.leftChild = leftChild;
}
public Node getRightChild() {
return this.rightChild;
}
public void setRightChild(final Node rightChild) {
this.rightChild = rightChild;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
", leftChild=" + leftChild +
", rightChild=" + rightChild +
'}';
}
}
Rien de bien compliqué : chaque élément a un lien vers ses enfants gauche et droit. Et maintenant, la classe la plus importante est peut-être la classe tree :
class Tree {
private Node rootNode; // корневой узел
public Tree() { // Пустое дерево
rootNode = null;
}
public Node findNodeByValue(int value) { // поиск узла по значению
Node currentNode = rootNode; // начинаем поиск с корневого узла
while (currentNode.getValue() != value) { // поиск покуда не будет найден элемент or не будут перебраны все
if (value < currentNode.getValue()) { // движение влево?
currentNode = currentNode.getLeftChild();
} else { //движение вправо
currentNode = currentNode.getRightChild();
}
if (currentNode == null) { // если потомка нет,
return null; // возвращаем null
}
}
return currentNode; // возвращаем найденный элемент
}
public void insertNode(int value) { // метод вставки нового element
Node newNode = new Node(); // создание нового узла
newNode.setValue(value); // вставка данных
if (rootNode == null) { // если корневой узел не существует
rootNode = newNode;// то новый элемент и есть корневой узел
}
else { // корневой узел занят
Node currentNode = rootNode; // начинаем с корневого узла
Node parentNode;
while (true) // мы имеем внутренний выход из цикла
{
parentNode = currentNode;
if(value == currentNode.getValue()) { // если такой элемент в дереве уже есть, не сохраняем его
return; // просто выходим из метода
}
else if (value < currentNode.getValue()) { // движение влево?
currentNode = currentNode.getLeftChild();
if (currentNode == null){ // если был достигнут конец цепочки,
parentNode.setLeftChild(newNode); // то вставить слева и выйти из методы
return;
}
}
else { // Или направо?
currentNode = currentNode.getRightChild();
if (currentNode == null) { // если был достигнут конец цепочки,
parentNode.setRightChild(newNode); //то вставить справа
return; // и выйти
}
}
}
}
}
public boolean deleteNode(int value) // Удаление узла с заданным ключом
{
Node currentNode = rootNode;
Node parentNode = rootNode;
boolean isLeftChild = true;
while (currentNode.getValue() != value) { // начинаем поиск узла
parentNode = currentNode;
if (value < currentNode.getValue()) { // Определяем, нужно ли движение влево?
isLeftChild = true;
currentNode = currentNode.getLeftChild();
}
else { // or движение вправо?
isLeftChild = false;
currentNode = currentNode.getRightChild();
}
if (currentNode == null)
return false; // yзел не найден
}
if (currentNode.getLeftChild() == null && currentNode.getRightChild() == null) { // узел просто удаляется, если не имеет потомков
if (currentNode == rootNode) // если узел - корень, то дерево очищается
rootNode = null;
else if (isLeftChild)
parentNode.setLeftChild(null); // если нет - узел отсоединяется, от родителя
else
parentNode.setRightChild(null);
}
else if (currentNode.getRightChild() == null) { // узел заменяется левым поддеревом, если правого потомка нет
if (currentNode == rootNode)
rootNode = currentNode.getLeftChild();
else if (isLeftChild)
parentNode.setLeftChild(currentNode.getLeftChild());
else
parentNode.setRightChild(currentNode.getLeftChild());
}
else if (currentNode.getLeftChild() == null) { // узел заменяется правым поддеревом, если левого потомка нет
if (currentNode == rootNode)
rootNode = currentNode.getRightChild();
else if (isLeftChild)
parentNode.setLeftChild(currentNode.getRightChild());
else
parentNode.setRightChild(currentNode.getRightChild());
}
else { // если есть два потомка, узел заменяется преемником
Node heir = receiveHeir(currentNode);// поиск преемника для удаляемого узла
if (currentNode == rootNode)
rootNode = heir;
else if (isLeftChild)
parentNode.setLeftChild(heir);
else
parentNode.setRightChild(heir);
}
return true; // элемент успешно удалён
}
// метод возвращает узел со следующим meaningм после передаваемого аргументом.
// для этого он сначала переходим к правому потомку, а затем
// отслеживаем цепочку левых потомков этого узла.
private Node receiveHeir(Node node) {
Node parentNode = node;
Node heirNode = node;
Node currentNode = node.getRightChild(); // Переход к правому потомку
while (currentNode != null) // Пока остаются левые потомки
{
parentNode = heirNode;// потомка задаём How текущий узел
heirNode = currentNode;
currentNode = currentNode.getLeftChild(); // переход к левому потомку
}
// Если преемник не является
if (heirNode != node.getRightChild()) // правым потомком,
{ // создать связи между узлами
parentNode.setLeftChild(heirNode.getRightChild());
heirNode.setRightChild(node.getRightChild());
}
return heirNode;// возвращаем приемника
}
public void printTree() { // метод для вывода дерева в консоль
Stack globalStack = new Stack(); // общий стек для значений дерева
globalStack.push(rootNode);
int gaps = 32; // начальное meaning расстояния между elementми
boolean isRowEmpty = false;
String separator = "-----------------------------------------------------------------";
System.out.println(separator);// черта для указания начала нового дерева
while (isRowEmpty == false) {
Stack localStack = new Stack(); // локальный стек для задания потомков element
isRowEmpty = true;
for (int j = 0; j < gaps; j++)
System.out.print(' ');
while (globalStack.isEmpty() == false) { // покуда в общем стеке есть элементы
Node temp = (Node) globalStack.pop(); // берем следующий, при этом удаляя его из стека
if (temp != null) {
System.out.print(temp.getValue()); // выводим его meaning в консоли
localStack.push(temp.getLeftChild()); // соохраняем в локальный стек, наследники текущего element
localStack.push(temp.getRightChild());
if (temp.getLeftChild() != null ||
temp.getRightChild() != null)
isRowEmpty = false;
}
else {
System.out.print("__");// - если элемент пустой
localStack.push(null);
localStack.push(null);
}
for (int j = 0; j < gaps * 2 - 2; j++)
System.out.print(' ');
}
System.out.println();
gaps /= 2;// при переходе на следующий уровень расстояние между elementми каждый раз уменьшается
while (localStack.isEmpty() == false)
globalStack.push(localStack.pop()); // перемещаем все элементы из локального стека в глобальный
}
System.out.println(separator);// подводим черту
}
}
Encore une fois, rien de bien compliqué. Les opérations d'arbre de recherche binaire décrites précédemment sont présentes, ainsi qu'une méthode d'affichage de l'arborescence dans la console. Eh bien, regardons maintenant l'arbre en action :
public class Application {
public static void main(String[] args) {
Tree tree = new Tree();
// вставляем узлы в дерево:
tree.insertNode(6);
tree.insertNode(8);
tree.insertNode(5);
tree.insertNode(8);
tree.insertNode(2);
tree.insertNode(9);
tree.insertNode(7);
tree.insertNode(4);
tree.insertNode(10);
tree.insertNode(3);
tree.insertNode(1);
// отображение дерева:
tree.printTree();
// удаляем один узел и выводим оставшееся дерево в консоли
tree.deleteNode(5);
tree.printTree();
// находим узел по значению и выводим его в консоли
Node foundNode = tree.findNodeByValue(7);
foundNode.printNode();
}
}
Les opérations de recherche/insertion/suppression dans un arbre de recherche binaire ont une complexité temporelle de O(log(N)) . Mais c’est le meilleur des cas. En général, la complexité temporelle des opérations varie de O(log(N)) à O(N) . Cela dépend du degré de dégénérescence de l'arbre.
GO TO FULL VERSION