Beneficios del árbol binario
¿Cuál es la ventaja de almacenar datos como un árbol de búsqueda binario? Imaginemos que tenemos un libro de referencia de 100 páginas y necesitamos encontrar una página específica que contenga los datos que necesitamos. También sabemos por el contenido qué página específica necesitamos. Si seguimos el camino habitual, tendremos que pasar página a página hasta llegar a la que necesitamos. Es decir, pasaremos de 1 a 100 páginas hasta encontrarnos en el lugar indicado. Por ejemplo, si necesitamos la página 77, entonces tendremos 77 búsquedas. Si hablamos de complejidad temporal, entonces será O(N) . Pero esto se puede hacer de manera más eficiente, ¿verdad? Intentemos hacer lo mismo, pero usando búsqueda binaria :- Dividimos el directorio en dos partes, la primera, del 1 al 50, la segunda, del 51 al 100. Sabemos exactamente en cuál de estas partes está nuestra página: si volvemos a tomar la página 77, está en la segunda parte del libro.
- A continuación, consideramos la segunda parte y la dividimos en dos: de 51 a 75, de 76 a 100. En este caso, nuestra página volverá a estar en la segunda mitad, en el rango 76-100.
- Luego dividimos este intervalo por 2 y obtenemos 76-88 y 89-100. La página encaja en el primer espacio, por lo que descartamos el segundo.
- Los siguientes son los intervalos: 76-81 y 82-88, toma el primero.
- 76-78 y 79-81, toma el primero.
- 76 y 77-78, toma el segundo.
- 77 y 78, tome el primero y obtenga nuestra página: 77.
Reglas para construir un árbol de búsqueda binario
El árbol de búsqueda binaria se construye según ciertas reglas:- cada nodo tiene como máximo dos hijos;
- cada valor menor que el valor del nodo se convierte en el hijo izquierdo o en el hijo del hijo izquierdo;
- cada valor mayor o igual al valor del nodo se convierte en el hijo correcto o en el hijo del hijo correcto.
- todos los nodos no tienen más de dos herederos, se cumple la primera condición;
- Si consideramos, por ejemplo, un nodo con el valor 7 (o cualquier otro), veremos que todos los valores de los elementos en el subárbol izquierdo serán menores y en el derecho, más. Esto significa que se cumplen las condiciones 2 y 3.
Buscar un elemento
Al buscar un elemento con un valor determinado, comenzamos en el elemento raíz:- Si es igual al valor buscado, el elemento raíz es el buscado; si no, comparamos los valores de la raíz y el buscado.
- Si el elemento que buscamos es más grande, pasamos al hijo derecho; si no, pasamos al hijo izquierdo.
- Si no se encuentra el elemento, aplique los pasos 1 y 2, pero al hijo (derecha o izquierda) hasta que encuentre el elemento.
- lo comparamos con el elemento raíz, vemos que el elemento raíz es más grande, entonces pasamos al hijo izquierdo, que tiene un valor de 3;
- comparamos el que buscamos y el elemento con valor 3, vemos que el que buscamos es más grande, por lo que necesitamos el descendiente derecho del elemento en cuestión, es decir, el elemento con valor 5;
- Comparamos este descendiente con el que estamos buscando y vemos que los valores son iguales: se encuentra el elemento.
Insertando un elemento
El algoritmo de inserción también es muy, muy sencillo:-
Comparamos el nuevo con el raíz (si no existe, el nuevo elemento es el raíz).
-
Si un nuevo elemento:
- es menor, entonces vamos al heredero izquierdo, si no hay ninguno, el nuevo elemento toma el lugar del heredero izquierdo y se completa el algoritmo;
- es mayor o igual que la raíz, entonces pasamos al heredero derecho. Y de manera similar, si este elemento no existe, entonces un nuevo elemento ocupará el lugar del elemento correcto y se completará el algoritmo.
-
Para el nuevo elemento en cuestión, que estaba a la derecha o a la izquierda del paso anterior, repita los pasos 1 y 2 hasta que el elemento insertado esté en su lugar.
Como ejemplo, querremos insertar en el árbol considerado anteriormente, un elemento con el valor 11:
- comparamos con el elemento raíz 7 y vemos que el elemento raíz es más pequeño, por lo que pasamos al heredero derecho;
- el siguiente elemento considerado tiene un valor de 9, que es menor que el nuevo 11, por lo que pasamos al heredero derecho;
- el heredero derecho tiene un valor de 10, que nuevamente es menor, entonces vamos al primer elemento, y como no está, se coloca en este lugar un nuevo elemento con un valor de 11.
↓
Eliminando un elemento
Quizás, de todas las operaciones con árboles de búsqueda binarios, la eliminación sea la más difícil. En primer lugar se busca el elemento a eliminar, tras lo cual sigue una etapa que puede tener tres variaciones:-
El nodo que se va a eliminar es un nodo hoja (no tiene hijos).
Quizás el más sencillo. Todo se reduce al hecho de que simplemente lo cortamos del árbol, sin manipulación innecesaria. Por ejemplo, de nuestro árbol eliminamos el nodo con el valor 8:
↓ -
El nodo que se está eliminando tiene un hijo.
En este caso, eliminamos el nodo seleccionado y colocamos su descendiente en su lugar (en esencia, simplemente cortamos el nodo seleccionado de la cadena). Como ejemplo, eliminemos un nodo con valor 9 del árbol:
↓ -
El nodo que se está eliminando tiene dos hijos.
La parte más interesante. Después de todo, si el nodo que se está eliminando tiene dos hijos a la vez, no se puede simplemente reemplazarlo con uno de estos hijos (especialmente si el hijo tiene sus propios hijos).
Ejemplo: en el árbol de arriba, ¿qué nodo debería ser el hijo izquierdo del nodo 3?
Si lo piensas un poco, resulta obvio que este debería ser un nodo con un valor de 4. Pero, ¿y si el árbol no es tan simple? Si tiene cientos de valores, ¿será tan fácil determinar quién será el sucesor?
Está claro que no. Por lo tanto, necesitamos nuestro propio algoritmo de búsqueda de receptores pequeños:
- Primero debemos ir al hijo derecho del nodo seleccionado, cuyo valor debe ser mayor que el valor del nodo.
- Luego vamos al hijo izquierdo del hijo derecho (si existe), luego al hijo izquierdo del hijo izquierdo, etc., siguiendo la cadena de hijos izquierdos.
- En consecuencia, el último hijo que quede en este camino será el sucesor del nodo original.
Generalicemos un poco este pequeño algoritmo: en el subárbol del hijo derecho del nodo de origen, todos los nodos son más grandes que el que se elimina, lo que se puede entender a partir de las reglas básicas del árbol de búsqueda binaria. En este subárbol buscamos el valor más pequeño.
En otras palabras, buscamos el nodo más pequeño en un conjunto de nodos más grandes que el nodo original. Este nodo más pequeño entre los más grandes será el sucesor más adecuado.
Cómo se verá el árbol después de eliminar el nodo con valor 3:
↓
class Node {
private int value; // ключ узла
private Node leftChild; // Левый узел потомок
private Node rightChild; // Правый узел потомок
public void printNode() { // Вывод значения узла в консоль
System.out.println(" Выбранный узел имеет significado :" + 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 +
'}';
}
}
Nada demasiado complicado: cada elemento tiene un vínculo con su hijo izquierdo y derecho. Y ahora, quizás, la clase más importante sea la clase de árbol:
class Tree {
private Node rootNode; // корневой узел
public Tree() { // Пустое дерево
rootNode = null;
}
public Node findNodeByValue(int value) { // поиск узла по значению
Node currentNode = rootNode; // начинаем поиск с корневого узла
while (currentNode.getValue() != value) { // поиск покуда не будет найден элемент o не будут перебраны все
if (value < currentNode.getValue()) { // движение влево?
currentNode = currentNode.getLeftChild();
} else { //движение вправо
currentNode = currentNode.getRightChild();
}
if (currentNode == null) { // если потомка нет,
return null; // возвращаем null
}
}
return currentNode; // возвращаем найденный элемент
}
public void insertNode(int value) { // метод вставки нового elemento
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 { // o движение вправо?
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; // элемент успешно удалён
}
// метод возвращает узел со следующим significadoм после передаваемого аргументом.
// для этого он сначала переходим к правому потомку, а затем
// отслеживаем цепочку левых потомков этого узла.
private Node receiveHeir(Node node) {
Node parentNode = node;
Node heirNode = node;
Node currentNode = node.getRightChild(); // Переход к правому потомку
while (currentNode != null) // Пока остаются левые потомки
{
parentNode = heirNode;// потомка задаём Cómo текущий узел
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; // начальное significado расстояния между elementoми
boolean isRowEmpty = false;
String separator = "-----------------------------------------------------------------";
System.out.println(separator);// черта для указания начала нового дерева
while (isRowEmpty == false) {
Stack localStack = new Stack(); // локальный стек для задания потомков elemento
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()); // выводим его significado в консоли
localStack.push(temp.getLeftChild()); // соохраняем в локальный стек, наследники текущего elemento
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;// при переходе на следующий уровень расстояние между elementoми каждый раз уменьшается
while (localStack.isEmpty() == false)
globalStack.push(localStack.pop()); // перемещаем все элементы из локального стека в глобальный
}
System.out.println(separator);// подводим черту
}
}
De nuevo, nada demasiado complicado. Las operaciones del árbol de búsqueda binaria descritas anteriormente están presentes, además de un método para mostrar el árbol en la consola. Bueno, ahora echemos un vistazo al árbol en acción:
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();
}
}
Las operaciones de búsqueda/inserción/eliminación en un árbol de búsqueda binario tienen una complejidad temporal de O(log(N)) . Pero ese es el mejor de los casos. En general, la complejidad temporal de las operaciones varía de O(log(N)) a O(N) . Depende del grado de degeneración del árbol.
GO TO FULL VERSION