Ha pasado un tiempo desde esos años escolares. Conseguí un trabajo como especialista en informática en un hospital. Tratando de moverme para hacer una programación real ahora. Ahora estoy trabajando en árboles binarios y me preguntaba cuál sería la mejor manera de determinar si el árbol está equilibrado en altura.
Estaba pensando en algo a lo largo de esto:
public boolean isBalanced(Node root){
if(root==null){
return true; //tree is empty
}
else{
int lh = root.left.height();
int rh = root.right.height();
if(lh - rh > 1 || rh - lh > 1){
return false;
}
}
return true;
}
¿Es esta una buena implementación? ¿O me estoy perdiendo algo?
java
algorithm
data-structures
binary-tree
user69514
fuente
fuente
Respuestas:
Me encontré con esta vieja pregunta mientras buscaba algo más. Noto que nunca obtuvo una respuesta completa.
La forma de resolver este problema es comenzar escribiendo una especificación para la función que está intentando escribir.
Especificación: Se dice que un árbol binario bien formado tiene "altura equilibrada" si (1) está vacío, o (2) sus elementos secundarios izquierdo y derecho están equilibrados en altura y la altura del árbol de la izquierda está dentro de 1 del altura del árbol de la derecha.
Ahora que tiene la especificación, el código es trivial de escribir. Simplemente siga las especificaciones:
Traducir eso al lenguaje de programación de su elección debería ser trivial.
Ejercicio adicional : este boceto de código ingenuo atraviesa el árbol demasiadas veces al calcular las alturas. ¿Puedes hacerlo más eficiente?
Ejercicio de súper bonificación : supongamos que el árbol está enormemente desequilibrado. Como, un millón de nodos de profundidad en un lado y tres de profundidad en el otro. ¿Existe un escenario en el que este algoritmo arruine la pila? ¿Puede arreglar la implementación para que nunca arruine la pila, incluso cuando se le da un árbol enormemente desequilibrado?
ACTUALIZACIÓN : Donal Fellows señala en su respuesta que existen diferentes definiciones de "equilibrado" que se pueden elegir. Por ejemplo, se podría tomar una definición más estricta de "altura equilibrada", y requerir que la longitud del camino al niño vacío más cercano esté dentro de uno de los caminos al niño vacío más lejano . Mi definición es menos estricta que eso y, por lo tanto, admite más árboles.
También se puede ser menos estricto que mi definición; se podría decir que un árbol equilibrado es aquel en el que la longitud máxima del camino a un árbol vacío en cada rama no difiere en más de dos, tres o alguna otra constante. O que la longitud máxima de la ruta es una fracción de la longitud mínima de la ruta, como la mitad o un cuarto.
Realmente no importa por lo general. El objetivo de cualquier algoritmo de equilibrio de árboles es garantizar que no termines en la situación en la que tienes un millón de nodos en un lado y tres en el otro. La definición de Donal está bien en teoría, pero en la práctica es un fastidio crear un algoritmo de equilibrio de árboles que cumpla con ese nivel de rigor. Los ahorros de rendimiento generalmente no justifican el costo de implementación. Pasas mucho tiempo haciendo reordenamientos innecesarios de los árboles para lograr un nivel de equilibrio que en la práctica hace poca diferencia. ¿A quién le importa si a veces se necesitan cuarenta ramas para llegar a la hoja más lejana de un árbol con un equilibrio imperfecto de un millón de nodos cuando, en teoría, sólo podrían tomar veinte en un árbol perfectamente equilibrado? El caso es que nunca se necesita un millón. Pasar del peor de los casos de un millón al peor de los cuarenta suele ser suficiente; no tiene que ir hasta el caso óptimo.
fuente
El equilibrio es una propiedad verdaderamente sutil; cree que sabe lo que es, pero es muy fácil equivocarse. En particular, incluso la (buena) respuesta de Eric Lippert es incorrecta. Eso es porque la noción de altura no es suficiente. Necesitas tener el concepto de alturas mínimas y máximas de un árbol (donde la altura mínima es el menor número de pasos desde la raíz hasta una hoja, y el máximo es ... bueno, te haces una idea). Dado eso, podemos definir el equilibrio como:
(En realidad, esto implica que las ramas están equilibradas; puede elegir la misma rama tanto para el máximo como para el mínimo).
Todo lo que necesita hacer para verificar esta propiedad es un simple recorrido de árbol que realiza un seguimiento de la profundidad actual. La primera vez que retrocede, eso le da una profundidad de referencia. Cada vez que retrocede, compara la nueva profundidad con la línea de base
En codigo:
Supongo que podrías hacer esto sin usar el patrón Observer, pero me resulta más fácil razonar de esta manera.
[EDITAR]: Por qué no puedes simplemente tomar la altura de cada lado. Considere este árbol:
OK, un desordenado poco, pero cada lado de la raíz es equilibrado:
C
es la profundidad 2,A
,B
,D
,E
son la profundidad de 3, yF
,G
,H
,J
son la profundidad 4. La altura de la rama izquierda es 2 (recuerde la altura disminuye a medida que atraviesan la rama), la altura de la rama derecha es 3. Sin embargo, el árbol en general no está equilibrado ya que hay una diferencia de altura de 2 entreC
yF
. Necesita una especificación minimax (aunque el algoritmo real puede ser menos complejo, ya que solo debería haber dos alturas permitidas).fuente
Esto solo determina si el nivel superior del árbol está equilibrado. Es decir, podría tener un árbol con dos ramas largas en el extremo izquierdo y el extremo derecho, sin nada en el medio, y esto volvería a ser cierto. Debe comprobar de forma recursiva los
root.left
yroot.right
para ver si también están equilibrados internamente antes de volver a verdadero.fuente
Respuesta adicional al ejercicio. La solución simple. Obviamente, en una implementación real, uno podría ajustar esto o algo para evitar que el usuario incluya la altura en su respuesta.
fuente
out height
notación variable " ")Solución posterior al pedido, atraviesa el árbol solo una vez. La complejidad del tiempo es O (n), el espacio es O (1), es mejor que la solución de arriba hacia abajo. Te doy una implementación de la versión java.
fuente
left == -1
significa? ¿Cuándo sería ese el caso? ¿Asumimos que la llamada recursiva implica queleft == -1
es verdadera si todos los subárboles de los hijos de la izquierda están desequilibrados?left == 1
significa que el subárbol izquierdo está desequilibrado, entonces todo el árbol está desequilibrado. Ya no tenemos que comprobar el subárbol derecho y podemos volver-1
.La definición de un árbol binario de altura equilibrada es:
Entonces, un árbol binario vacío siempre está equilibrado en altura.
Un árbol binario no vacío está equilibrado en altura si:
Considere el árbol:
Como se ve, el subárbol izquierdo de
A
está equilibrado en altura (ya que está vacío) y también su subárbol derecho. Pero aún así, el árbol no está equilibrado en altura, ya que no se cumple la condición 3, ya que la altura del subárbol izquierdo es0
y la altura del subárbol derecho2
.Además, el siguiente árbol no está equilibrado en altura a pesar de que la altura del subárbol izquierdo y derecho son iguales. Su código existente devolverá verdadero para él.
Así que la palabra todos en la definición es muy importante.
Esto funcionará:
Enlace ideone
fuente
Si el árbol binario está equilibrado o no, se puede verificar mediante el cruce de orden de nivel:
fuente
Esto se está volviendo mucho más complicado de lo que realmente es.
El algoritmo es como sigue:
Sea B = profundidad del nodo de nivel más bajo
Si abs (AB) <= 1, entonces el árbol está equilibrado
fuente
Lo que significa equilibrado depende un poco de la estructura en cuestión. Por ejemplo, un árbol B no puede tener nodos a más de una cierta profundidad desde la raíz, o menos, todos los datos viven a una profundidad fija desde la raíz, pero puede estar desequilibrado si la distribución de hojas a hojas -pero uno de los nodos es desigual. Listas de omisión No tienen ninguna noción de equilibrio, confiando en cambio en la probabilidad para lograr un desempeño decente. Los árboles de Fibonacci se desequilibran a propósito, posponiendo el reequilibrio para lograr un rendimiento asintótico superior a cambio de actualizaciones ocasionalmente más largas. Los árboles AVL y Red-Black adjuntan metadatos a cada nodo para lograr un invariante de equilibrio de profundidad.
Todas estas estructuras y más están presentes en las bibliotecas estándar de los sistemas de programación más comunes (¡excepto python, RAGE!). Implementar uno o dos es una buena práctica de programación, pero probablemente no sea un buen uso del tiempo para lanzar el suyo propio para la producción, a menos que su problema tenga un rendimiento peculiar que no necesite ser satisfecho por ninguna colección estándar.
fuente
Nota 1: La altura de cualquier subárbol se calcula solo una vez.
Nota 2: Si el subárbol izquierdo no está equilibrado, se omite el cálculo del subárbol derecho, que potencialmente contiene millones de elementos.
fuente
El equilibrio generalmente depende de la longitud del camino más largo en cada dirección. El algoritmo anterior no lo hará por usted.
¿Qué estás intentando implementar? Hay árboles que se equilibran a sí mismos alrededor (AVL / Rojo-negro). De hecho, los árboles de Java están equilibrados.
fuente
Si esto es por su trabajo , sugiero:
fuente
fuente
1
(lo mismo para minDepth). Sin embargo, la profundidad correcta debería ser0
.La raíz de un árbol siempre tiene0
profundidadAquí hay una solución completa probada resuelta en C # (lo siento, no soy un desarrollador de Java) (solo copie y pegue en la aplicación de consola). Sé que la definición de equilibrado varía, por lo que no a todos les pueden gustar los resultados de mi prueba, pero solo mire el enfoque ligeramente diferente de verificar la profundidad / altura en un bucle recursivo y salir en la primera discrepancia sin guardar la altura / nivel / profundidad del nodo en cada nodo (solo manteniéndolo en una llamada de función).
fuente
fuente
RE: La solución de @ lucky usa un BFS para hacer un recorrido de orden de nivel.
Atravesamos el árbol y mantenemos una referencia a vars min / max-level que describen el nivel mínimo en el que un nodo es una hoja.
Creo que la solución @lucky requiere una modificación. Como lo sugiere @codaddict, en lugar de verificar si un nodo es una hoja, debemos verificar si el hijo izquierdo o derecho es nulo (no ambos). De lo contrario, el algoritmo consideraría esto como un árbol equilibrado válido:
En Python:
Esta solución debe satisfacer todas las estipulaciones provistas en la pregunta inicial, operando en O (n) tiempo y O (n) espacio. El desbordamiento de memoria se dirigiría al montón en lugar de soplar una pila de llamadas recursiva.
Alternativamente, podríamos atravesar inicialmente el árbol para calcular + las alturas máximas de caché para cada subárbol raíz de forma iterativa. Luego, en otra ejecución iterativa, verifique si las alturas en caché de los subárboles izquierdo y derecho para cada raíz nunca difieren en más de uno. Esto también se ejecutaría en el tiempo O (n) y el espacio O (n), pero de forma iterativa para no causar un desbordamiento de la pila.
fuente
Bueno, necesita una forma de determinar las alturas de la izquierda y la derecha, y si la izquierda y la derecha están equilibradas.
Y yo solo
return height(node->left) == height(node->right);
En cuanto a escribir una
height
función, lea: Comprendiendo la recursividadfuente
¿De qué tipo de árbol estás hablando? Hay árboles que se equilibran por sí mismos . Verifique sus algoritmos donde determinan si necesitan reordenar el árbol para mantener el equilibrio.
fuente
Aquí hay una versión basada en un recorrido genérico de profundidad primero. Debe ser más rápido que la otra respuesta correcta y manejar todos los "desafíos" mencionados. Disculpas por el estilo, realmente no conozco Java.
Aún puede hacerlo mucho más rápido si regresa temprano si el máximo y el mínimo están configurados y tienen una diferencia> 1.
fuente
fuente
fuente
Esto es lo que he probado para el ejercicio extra de Eric. Intento deshacerme de mis bucles recursivos y regresar tan pronto como encuentro que un subárbol no está equilibrado.
fuente
fuente
Un árbol vacío tiene una altura equilibrada. Un árbol binario T no vacío se equilibra si:
1) El subárbol izquierdo de T está equilibrado
2) El subárbol derecho de T está equilibrado
3) La diferencia entre las alturas del subárbol izquierdo y el subárbol derecho no es más de 1.
Complejidad del tiempo: O (n)
fuente
Para tener un mejor rendimiento, especialmente en árboles enormes, puede guardar la altura en cada nodo, por lo que es una compensación entre el espacio y el rendimiento:
Ejemplo de implementación de la adición y lo mismo para la eliminación
fuente
fuente
¿No funcionaría esto?
Cualquier árbol desequilibrado siempre fallaría en esto.
fuente