Respuestas:
Discutir sobre el rendimiento de los árboles binarios no tiene sentido: no son una estructura de datos, sino una familia de estructuras de datos, todas con diferentes características de rendimiento. Si bien es cierto que los árboles binarios desequilibrados tienen un rendimiento mucho peor que los árboles binarios autoequilibrados para la búsqueda, existen muchos árboles binarios (como los intentos binarios) para los cuales "equilibrar" no tiene sentido.
map
y set
en las bibliotecas de muchos idiomas.La razón por la que los árboles binarios se usan con más frecuencia que los árboles n-arios para la búsqueda es que los árboles n-arios son más complejos, pero generalmente no ofrecen una ventaja de velocidad real.
En un árbol binario (equilibrado) con m
nodos, pasar de un nivel al siguiente requiere una comparación, y hay log_2(m)
niveles, para un total de log_2(m)
comparaciones.
Por el contrario, un árbol n-ary requerirá log_2(n)
comparaciones (usando una búsqueda binaria) para pasar al siguiente nivel. Como hay log_n(m)
niveles totales, la búsqueda requerirá log_2(n)*log_n(m)
= log_2(m)
comparaciones totales. Entonces, aunque los árboles n-arios son más complejos, no proporcionan ninguna ventaja en términos de comparaciones totales necesarias.
(Sin embargo, los árboles n-arios siguen siendo útiles en situaciones de nicho. Los ejemplos que vienen a la mente de inmediato son los árboles cuádruples y otros árboles de partición del espacio, donde la división del espacio usando solo dos nodos por nivel haría que la lógica sea innecesariamente compleja; y Árboles B utilizados en muchas bases de datos, donde el factor limitante no es cuántas comparaciones se realizan en cada nivel, sino cuántos nodos se pueden cargar desde el disco duro a la vez)
Cuando la mayoría de las personas hablan de árboles binarios, la mayoría de las veces piensan en árboles de búsqueda binarios , así que lo cubriré primero.
Un árbol de búsqueda binario no equilibrado es realmente útil para poco más que educar a los estudiantes sobre las estructuras de datos. Esto se debe a que, a menos que los datos lleguen en un orden relativamente aleatorio, el árbol puede degenerar fácilmente en su peor forma, que es una lista vinculada, ya que los árboles binarios simples no están equilibrados.
Un buen ejemplo: una vez tuve que arreglar algún software que cargaba sus datos en un árbol binario para su manipulación y búsqueda. Escribió los datos en forma ordenada:
Alice
Bob
Chloe
David
Edwina
Frank
para que, al volver a leerlo, terminara con el siguiente árbol:
Alice
/ \
= Bob
/ \
= Chloe
/ \
= David
/ \
= Edwina
/ \
= Frank
/ \
= =
cual es la forma degenerada Si buscas a Frank en ese árbol, tendrás que buscar los seis nodos antes de encontrarlo.
Los árboles binarios se vuelven realmente útiles para buscar cuando los equilibras. Esto implica rotar subárboles a través de su nodo raíz para que la diferencia de altura entre dos subárboles sea menor o igual que 1. Agregar esos nombres arriba de uno a la vez en un árbol equilibrado le daría la siguiente secuencia:
1. Alice
/ \
= =
2. Alice
/ \
= Bob
/ \
= =
3. Bob
_/ \_
Alice Chloe
/ \ / \
= = = =
4. Bob
_/ \_
Alice Chloe
/ \ / \
= = = David
/ \
= =
5. Bob
____/ \____
Alice David
/ \ / \
= = Chloe Edwina
/ \ / \
= = = =
6. Chloe
___/ \___
Bob Edwina
/ \ / \
Alice = David Frank
/ \ / \ / \
= = = = = =
En realidad, puede ver subárboles enteros girando hacia la izquierda (en los pasos 3 y 6) a medida que se agregan las entradas y esto le da un árbol binario equilibrado en el que la búsqueda del peor de los casos es en O(log N)
lugar de la O(N
que da la forma degenerada. En ningún momento el NULL más alto ( =
) difiere del más bajo en más de un nivel. Y, en el árbol final anterior, se puede encontrar Frank con sólo mirar tres nodos ( Chloe
, Edwina
y, por último, Frank
).
Por supuesto, pueden volverse aún más útiles cuando los haces árboles multidireccionales equilibrados en lugar de árboles binarios. Eso significa que cada nodo contiene más de un elemento (técnicamente, contienen N elementos y N + 1 punteros, un árbol binario es un caso especial de un árbol multidireccional de 1 vía con 1 elemento y 2 punteros).
Con un árbol de tres vías, terminas con:
Alice Bob Chloe
/ | | \
= = = David Edwina Frank
/ | | \
= = = =
Esto normalmente se usa para mantener las claves de un índice de elementos. He escrito un software de base de datos optimizado para el hardware en el que un nodo es exactamente del tamaño de un bloque de disco (por ejemplo, 512 bytes) y coloca todas las claves que puede en un solo nodo. Los punteros en este caso eran en realidad números de registro en un archivo de acceso directo de registro de longitud fija separado del archivo de índice (por lo que se X
podía encontrar el número de registro simplemente buscando X * record_length
).
Por ejemplo, si los punteros son 4 bytes y el tamaño de la clave es 10, el número de claves en un nodo de 512 bytes es 36. Eso es 36 claves (360 bytes) y 37 punteros (148 bytes) para un total de 508 bytes con 4 bytes desperdiciados por nodo.
El uso de claves de múltiples vías introduce la complejidad de una búsqueda de dos fases (búsqueda de múltiples vías para encontrar el nodo correcto combinado con una pequeña búsqueda secuencial (o binaria lineal) para encontrar la clave correcta en el nodo) pero la ventaja en haciendo menos E / S de disco más de lo que compensa esto.
No veo ninguna razón para hacer esto por una estructura en memoria, sería mejor quedarse con un árbol binario equilibrado y mantener su código simple.
También tenga en cuenta que las ventajas de O(log N)
over O(N)
realmente no aparecen cuando sus conjuntos de datos son pequeños. Si está utilizando un árbol multidireccional para almacenar las quince personas en su libreta de direcciones, probablemente sea exagerado. Las ventajas se obtienen cuando almacena algo como cada pedido de sus cien mil clientes en los últimos diez años.
El objetivo de la notación big-O es indicar lo que sucede a medida que se N
acerca al infinito. Algunas personas pueden estar en desacuerdo, pero incluso está bien usar el método de burbuja si está seguro de que los conjuntos de datos se mantendrán por debajo de un cierto tamaño, siempre que no haya nada más disponible :-)
En cuanto a otros usos para los árboles binarios, hay muchos, como:
Dada la cantidad de explicación que generé para los árboles de búsqueda, soy reticente a entrar en muchos detalles sobre los demás, pero eso debería ser suficiente para investigarlos, si lo desea.
La organización del código Morse es un árbol binario.
Un árbol binario es una estructura de datos en árbol en la que cada nodo tiene como máximo dos nodos secundarios, generalmente distinguidos como "izquierda" y "derecha". Los nodos con hijos son nodos principales, y los nodos secundarios pueden contener referencias a sus padres. Fuera del árbol, a menudo hay una referencia al nodo "raíz" (el antepasado de todos los nodos), si existe. Se puede llegar a cualquier nodo en la estructura de datos comenzando en el nodo raíz y siguiendo repetidamente las referencias al hijo izquierdo o derecho. En un árbol binario, un grado de cada nodo es máximo dos.
Los árboles binarios son útiles porque, como puede ver en la imagen, si desea encontrar algún nodo en el árbol, solo tiene que buscar un máximo de 6 veces. Si quisiera buscar el nodo 24, por ejemplo, comenzaría en la raíz.
Esta búsqueda se ilustra a continuación:
Puede ver que puede excluir la mitad de los nodos de todo el árbol en la primera pasada. y la mitad del subárbol izquierdo en el segundo. Esto hace búsquedas muy efectivas. Si esto se hiciera en 4 mil millones de elementos, solo tendría que buscar un máximo de 32 veces. Por lo tanto, cuantos más elementos contenga el árbol, más eficiente puede ser su búsqueda.
Las eliminaciones pueden volverse complejas. Si el nodo tiene 0 o 1 hijo, simplemente se trata de mover algunos punteros para excluir el que se va a eliminar. Sin embargo, no puede eliminar fácilmente un nodo con 2 hijos. Entonces tomamos un atajo. Digamos que queríamos eliminar el nodo 19.
Dado que tratar de determinar dónde mover los punteros izquierdo y derecho no es fácil, encontramos uno para sustituirlo. Vamos al subárbol izquierdo y vamos tan a la derecha como podamos. Esto nos da el siguiente valor más grande del nodo que queremos eliminar.
Ahora copiamos todo el contenido de 18, excepto los punteros izquierdo y derecho, y eliminamos el nodo 18 original.
Para crear estas imágenes, implementé un árbol AVL, un árbol de equilibrio automático, de modo que en cualquier momento, el árbol tiene como máximo un nivel de diferencia entre los nodos de la hoja (nodos sin hijos). Esto evita que el árbol se sesgue y mantiene el O(log n)
tiempo máximo de búsqueda, con el costo de un poco más de tiempo requerido para las inserciones y eliminaciones.
Aquí hay una muestra que muestra cómo mi árbol AVL se ha mantenido lo más compacto y equilibrado posible.
En una matriz ordenada, las búsquedas aún tomarían O(log(n))
, al igual que un árbol, pero la inserción y eliminación aleatorias tomarían O (n) en lugar de las del árbol O(log(n))
. Algunos contenedores STL usan estas características de rendimiento para su ventaja, por lo que los tiempos de inserción y extracción toman un máximo de O(log n)
, lo cual es muy rápido. Algunos de estos recipientes son map
, multimap
, set
, y multiset
.
El código de ejemplo para un árbol AVL se puede encontrar en http://ideone.com/MheW8
La aplicación principal es árboles de búsqueda binarios . Se trata de una estructura de datos en la que la búsqueda, inserción y eliminación son muy rápidas (sobre log(n)
operaciones)
Un ejemplo interesante de un árbol binario que no se ha mencionado es el de una expresión matemática evaluada de forma recursiva. Es básicamente inútil desde un punto de vista práctico, pero es una forma interesante de pensar en tales expresiones.
Básicamente, cada nodo del árbol tiene un valor que es inherente a sí mismo o se evalúa de forma recursiva operando en los valores de sus hijos.
Por ejemplo, la expresión (1+3)*2
se puede expresar como:
*
/ \
+ 2
/ \
1 3
Para evaluar la expresión, solicitamos el valor del padre. Este nodo a su vez obtiene sus valores de sus hijos, un operador más y un nodo que simplemente contiene '2'. El operador más a su vez obtiene sus valores de los hijos con los valores '1' y '3' y los agrega, devolviendo 4 al nodo de multiplicación que devuelve 8.
Este uso de un árbol binario es similar a la notación polaca inversa en cierto sentido, en el sentido de que el orden en que se realizan las operaciones es idéntico. También una cosa a tener en cuenta es que no necesariamente tiene que ser un árbol binario, es solo que los operadores más utilizados son binarios. En su nivel más básico, el árbol binario aquí es, de hecho, un lenguaje de programación puramente funcional muy simple.
Aplicaciones del árbol binario:
No creo que haya ningún uso para los árboles binarios "puros". (excepto para fines educativos) Árboles binarios equilibrados, como árboles Rojo-Negros o árboles AVL son mucho más útiles, ya que garantizan operaciones O (log). Los árboles binarios normales pueden terminar siendo una lista (o casi una lista) y no son realmente útiles en aplicaciones que utilizan muchos datos.
Los árboles equilibrados se usan a menudo para implementar mapas o conjuntos. También se pueden usar para ordenar en O (nlogn), incluso aunque existan mejores formas de hacerlo.
También para buscar / insertar / eliminar se pueden usar tablas Hash , que generalmente tienen un mejor rendimiento que los árboles de búsqueda binarios (balanceados o no).
Una aplicación en la que los árboles de búsqueda binarios (equilibrados) serían útiles sería si se necesitara buscar / insertar / eliminar y ordenar. La ordenación podría estar en su lugar (casi, ignorando el espacio de pila necesario para la recursión), dado un árbol equilibrado de compilación listo. Todavía sería O (nlogn) pero con un factor constante más pequeño y no se necesita espacio adicional (excepto para la nueva matriz, suponiendo que los datos deben colocarse en una matriz). Las tablas de hash, por otro lado, no se pueden ordenar (al menos no directamente).
Quizás también son útiles en algunos algoritmos sofisticados para hacer algo, pero no me viene a la mente nada. Si encuentro más, editaré mi publicación.
Otros árboles como los árboles fe B + son ampliamente utilizados en bases de datos
Una de las aplicaciones más comunes es almacenar eficientemente los datos en forma ordenada para acceder y buscar elementos almacenados rápidamente. Por ejemplo, std::map
o std::set
en la biblioteca estándar de C ++.
El árbol binario como estructura de datos es útil para diversas implementaciones de analizadores de expresiones y solucionadores de expresiones.
También se puede usar para resolver algunos de los problemas de la base de datos, por ejemplo, la indexación.
En general, el árbol binario es un concepto general de una estructura de datos basada en un árbol particular y se pueden construir varios tipos específicos de árboles binarios con diferentes propiedades.
En C ++ STL, y muchas otras bibliotecas estándar en otros lenguajes, como Java y C #. Los árboles de búsqueda binarios se utilizan para implementar conjuntos y mapas.
Una de las aplicaciones más importantes de los árboles binarios son los árboles de búsqueda binarios balanceados como:
Este tipo de árboles tiene la propiedad de que la diferencia en las alturas del subárbol izquierdo y el subárbol derecho se mantiene pequeña haciendo operaciones como rotaciones cada vez que se inserta o elimina un nodo.
Debido a esto, la altura total del árbol permanece del orden de log n y las operaciones como la búsqueda, inserción y eliminación de los nodos se realizan en tiempo O (log n). El STL de C ++ también implementa estos árboles en forma de conjuntos y mapas.
la sintaxis de sus programas, o de hecho muchas otras cosas, como los lenguajes naturales, se pueden analizar utilizando el árbol binario (aunque no necesariamente).
En el hardware moderno, un árbol binario casi siempre es subóptimo debido al mal comportamiento de la memoria caché y el espacio. Esto también se aplica a las variantes (semi) equilibradas. Si los encuentra, es donde el rendimiento no cuenta (o está dominado por la función de comparación), o es más probable por razones históricas o de ignorancia.
Un compilador que usa un árbol binario para una representación de un AST, puede usar algoritmos conocidos para analizar el árbol como postorder, inorder. El programador no necesita crear su propio algoritmo. Debido a que un árbol binario para un archivo fuente es más alto que el árbol n-ario, su construcción lleva más tiempo. Tome esta producción: selstmnt: = "if" "(" expr ")" stmnt "ELSE" stmnt En un árbol binario tendrá 3 niveles de nodos, pero el árbol n-ary tendrá 1 nivel (de chids)
Es por eso que los sistemas operativos basados en Unix son lentos.