El árbol binario aquí puede no ser necesariamente un árbol de búsqueda binaria.
La estructura podría tomarse como:
struct node {
int data;
struct node *left;
struct node *right;
};
La solución máxima que pude resolver con un amigo fue algo así:
considere este árbol binario :
El rendimiento transversal del pedido: 8, 4, 9, 2, 5, 1, 6, 3, 7
Y los rendimientos transversales del postorder - 8, 9, 4, 5, 2, 6, 7, 3, 1
Entonces, por ejemplo, si queremos encontrar el ancestro común de los nodos 8 y 5, entonces hacemos una lista de todos los nodos que están entre 8 y 5 en el recorrido del árbol de pedidos, que en este caso es [4, 9 2]. Luego verificamos qué nodo en esta lista aparece último en el recorrido del postorder, que es 2. Por lo tanto, el ancestro común para 8 y 5 es 2.
La complejidad de este algoritmo, creo que es O (n) (O (n) para recorridos de orden / postorder, el resto de los pasos nuevamente son O (n) ya que no son más que simples iteraciones en matrices). Pero existe una gran posibilidad de que esto esté mal. :-)
Pero este es un enfoque muy burdo, y no estoy seguro de si se rompe en algún caso. ¿Hay alguna otra solución (posiblemente más óptima) para este problema?
Respuestas:
Nick Johnson tiene razón en que un algoritmo de complejidad de tiempo O (n) es lo mejor que puede hacer si no tiene punteros principales.) Para una versión recursiva simple de ese algoritmo, vea el código en la publicación de Kinding que se ejecuta en tiempo O (n) .
Pero tenga en cuenta que si sus nodos tienen punteros principales, es posible un algoritmo mejorado. Para ambos nodos en cuestión, construya una lista que contenga la ruta desde la raíz hasta el nodo comenzando en el nodo e insertando el padre por delante.
Entonces, para 8 en su ejemplo, obtiene (mostrando pasos): {4}, {2, 4}, {1, 2, 4}
Haga lo mismo para su otro nodo en cuestión, resultando en (pasos no mostrados): {1, 2}
Ahora compare las dos listas que hizo buscando el primer elemento donde la lista difiere, o el último elemento de una de las listas, lo que ocurra primero.
Este algoritmo requiere un tiempo O (h) donde h es la altura del árbol. En el peor de los casos, O (h) es equivalente a O (n), pero si el árbol está equilibrado, eso es solo O (log (n)). También requiere espacio O (h). Es posible una versión mejorada que use solo espacio constante, con el código mostrado en la publicación de CEGRD
Independientemente de cómo se construya el árbol, si esta será una operación que realizará muchas veces en el árbol sin cambiarla, hay otros algoritmos que puede usar que requieren una preparación de tiempo O (n) [lineal], pero luego encontrar cualquier el par solo toma O (1) [constante] tiempo. Para obtener referencias a estos algoritmos, consulte la página con el problema de antepasado común más bajo en Wikipedia . (Crédito a Jason por publicar originalmente este enlace)
fuente
O(h)
es soloO(log(n))
si el árbol está equilibrado. Para cualquier árbol, ya sea binario o no, si tiene punteros principales, puede determinar la ruta de una hoja a la raíz aO(h)
tiempo, simplemente siguiendo el puntero principal hasta elh
momento. Eso te da el camino desde la hoja hasta la raíz. Si las rutas se almacenan como una pila, al iterar la pila se obtiene la ruta desde la raíz hasta la hoja. Si carece de punteros principales y no tiene una estructura especial para el árbol, encontrar el camino desde la raíz hasta la hoja llevaO(n)
tiempo.Comenzando desde el
root
nodo y moviéndose hacia abajo si encuentra algún nodo que tenga unop
oq
como su hijo directo, entonces es el LCA. (edit - esto debería ser sip
oq
es el valor del nodo, devuélvalo De lo contrario, se producirá un error cuando uno de.p
oq
es un hijo directo de la otra.)De lo contrario, si encuentra un nodo con
p
su subárbol derecho (o izquierdo) yq
su subárbol izquierdo (o derecho), entonces es el LCA.El código fijo se ve así:
El siguiente código falla cuando cualquiera es hijo directo de otro.
Código en acción
fuente
Aquí está el código de trabajo en JAVA
fuente
Las respuestas dadas hasta ahora usan recursividad o almacena, por ejemplo, una ruta en la memoria.
Ambos enfoques pueden fallar si tiene un árbol muy profundo.
Aquí está mi opinión sobre esta pregunta. Cuando verificamos la profundidad (distancia desde la raíz) de ambos nodos, si son iguales, entonces podemos movernos con seguridad hacia arriba desde ambos nodos hacia el ancestro común. Si una de las profundidades es más grande, entonces debemos movernos hacia arriba desde el nodo más profundo mientras permanecemos en el otro.
Aquí está el código:
La complejidad temporal de este algoritmo es: O (n). La complejidad espacial de este algoritmo es: O (1).
Con respecto al cálculo de la profundidad, primero podemos recordar la definición: Si v es raíz, profundidad (v) = 0; De lo contrario, profundidad (v) = profundidad (padre (v)) + 1. Podemos calcular la profundidad de la siguiente manera:
fuente
Bueno, este tipo de depende de cómo está estructurado su árbol binario. Presumiblemente, tiene alguna forma de encontrar el nodo de hoja deseado dada la raíz del árbol: simplemente aplique eso a ambos valores hasta que las ramas que elija diverjan.
Si no tiene una manera de encontrar la hoja deseada dada la raíz, entonces su única solución, tanto en operación normal como para encontrar el último nodo común, es una búsqueda de fuerza bruta del árbol.
fuente
Esto se puede encontrar en: - http://goursaha.freeoda.com/DataStructure/LowestCommonAncestor.html
fuente
El algoritmo de antepasados menos comunes fuera de línea de Tarjan es lo suficientemente bueno (véase también Wikipedia ). Hay más información sobre el problema (el problema ancestral más bajo) en Wikipedia .
fuente
Para descubrir el ancestro común de dos nodos: -
Esto funcionaría para el árbol de búsqueda binario.
fuente
Intenté con imágenes ilustrativas y código de trabajo en Java,
http://tech.bragboy.com/2010/02/least-common-ancestor-without-using.html
fuente
El siguiente algoritmo recursivo se ejecutará en O (log N) para un árbol binario equilibrado. Si cualquiera de los nodos pasados a la función getLCA () es el mismo que la raíz, entonces la raíz será el LCA y no habrá necesidad de realizar ninguna recusación.
Casos de prueba. [1] Ambos nodos n1 y n2 están en el árbol y residen a ambos lados de su nodo padre. [2] O bien el nodo n1 o n2 es la raíz, el LCA es la raíz. [3] Solo n1 o n2 están en el árbol, LCA será el nodo raíz del subárbol izquierdo de la raíz del árbol, o LCA será el nodo raíz del subárbol derecho de la raíz del árbol.
[4] Ni n1 ni n2 están en el árbol, no hay LCA. [5] Tanto n1 como n2 están en línea recta uno al lado del otro, LCA será de n1 o n2 que alguna vez esté cerca de la raíz del árbol.
fuente
Simplemente baje de todo el árbol
root
siempre que ambos nodos dados, digamosp
yq
, para los cuales se debe encontrar Ancestor, se encuentren en el mismo subárbol (lo que significa que sus valores son más pequeños o más grandes que los de la raíz).Esto camina directamente desde la raíz hasta el Ancestro Menos Común, sin mirar el resto del árbol, por lo que es casi tan rápido como puede. Algunas formas de hacerlo.
en caso de desbordamiento, haría (root.val - (long) p.val) * (root.val - (long) q.val)
fuente
fuente
Considera este árbol
Si hacemos un recorrido de postorder y preorder y encontramos el primer predecesor y sucesor común que ocurre, obtenemos el ancestro común.
postorder => 0,2,1,5,4,6,3,8,10,11,9,14,15,13,12,7 preorder => 7,3,1,0,2,6,4 , 5,12,9,8,11,10,13,15,14
Antepasado mínimo común de 8,11
en postorder tenemos => 9,14,15,13,12,7 después de 8 y 11 en preorden tenemos => 7,3,1,0,2,6,4,5,12,9 antes de 8 y 11
9 es el primer número común que ocurre después de 8 y 11 en postorder y antes de 8 y 11 en preorder, por lo tanto 9 es la respuesta
Antepasado mínimo común de 5,10
11,9,14,15,13,12,7 en postorder 7,3,1,0,2,6,4 en preorden
7 es el primer número que ocurre después de 5,10 en postorder y antes de 5,10 en preorder, por lo tanto, 7 es la respuesta
fuente
Si es un árbol binario completo con hijos del nodo x como 2 * x y 2 * x + 1, entonces hay una forma más rápida de hacerlo
Como funciona
Esto funciona porque básicamente divide el número más grande entre dos recursivamente hasta que ambos números sean iguales. Ese número es el ancestro común. Dividir es efectivamente la operación de turno correcta. Entonces necesitamos encontrar el prefijo común de dos números para encontrar el antepasado más cercano
fuente
En scala, puedes:
fuente
fuente
Aquí está la forma C ++ de hacerlo. Intenté mantener el algoritmo lo más fácil posible de entender:
Cómo usarlo:
fuente
La forma más fácil de encontrar el Ancestro común más bajo es usar el siguiente algoritmo:
fuente
Encontre una solucion
Dependiendo de 3 recorridos, puede decidir quién es el LCA. Desde LCA encuentre la distancia de ambos nodos. Agregue estas dos distancias, que es la respuesta.
fuente
Esto es lo que pienso.
Complejidad: paso 1: O (n), paso 2 = ~ O (n), total = ~ O (n).
fuente
Aquí hay dos enfoques en c # (.net) (ambos discutidos anteriormente) como referencia:
Versión recursiva de encontrar LCA en árbol binario (O (N), ya que como máximo se visita cada nodo) (los puntos principales de la solución es que LCA es (a) único nodo en árbol binario donde ambos elementos residen a ambos lados de los subárboles (izquierda y derecha) es LCA. (b) Y tampoco importa qué nodo esté presente en ambos lados: inicialmente intenté mantener esa información, y obviamente la función recursiva se volvió tan confusa. Una vez que me di cuenta, se volvió muy elegante.
Buscar ambos nodos (O (N)) y realizar un seguimiento de las rutas (usa espacio adicional, por lo tanto, el n. ° 1 es probablemente superior, incluso aunque el espacio sea insignificante si el árbol binario está bien equilibrado, ya que el consumo de memoria adicional será solo en O (log (N)).
para que las rutas se comparen (esencialmente similar a la respuesta aceptada, pero las rutas se calculan asumiendo que el nodo del puntero no está presente en el nodo del árbol binario)
Solo para completar ( no relacionado con la pregunta ), LCA en BST (O (log (N))
Pruebas
Recursivo:
donde la versión recursiva privada anterior se invoca mediante el siguiente método público:
Solución haciendo un seguimiento de las rutas de ambos nodos:
donde FindNodeAndPath se define como
BST (LCA): no relacionado (solo para completar como referencia)
Pruebas unitarias
fuente
Si alguien interesado en pseudocódigo (para trabajos en el hogar universitario) aquí hay uno.
fuente
Aunque esto ya ha sido respondido, este es mi enfoque para este problema usando el lenguaje de programación C. Aunque el código muestra un árbol de búsqueda binario (en lo que respecta a insert ()), el algoritmo también funciona para un árbol binario. La idea es repasar todos los nodos que se encuentran desde el nodo A al nodo B en el recorrido transversal, buscar los índices para estos en el recorrido del orden posterior. El nodo con el índice máximo en el recorrido de orden posterior es el ancestro común más bajo.
Este es un código C que funciona para implementar una función para encontrar el ancestro común más bajo en un árbol binario. También proporciono todas las funciones de utilidad, etc., pero salte a CommonAncestor () para una comprensión rápida.
fuente
Puede haber un enfoque más. Sin embargo, no es tan eficiente como el que ya se sugirió en las respuestas.
Cree un vector de ruta para el nodo n1.
Cree un segundo vector de ruta para el nodo n2.
El vector de ruta implica que los nodos establecidos desde ese atravesarían para alcanzar el nodo en cuestión.
Compara ambos vectores de ruta. El índice donde no coinciden, devuelve el nodo en ese índice - 1. Esto daría el LCA.
Contras para este enfoque:
Necesita atravesar el árbol dos veces para calcular los vectores de ruta. Necesita espacio adicional O (h) para almacenar vectores de ruta.
Sin embargo, esto también es fácil de implementar y comprender.
Código para calcular el vector de ruta:
fuente
Intenta así
fuente
Forma cruda:
El problema con el método anterior es que haremos el "hallazgo" varias veces, es decir, existe la posibilidad de que cada nodo sea atravesado varias veces. Podemos superar este problema si podemos registrar la información para no procesarla nuevamente (piense en la programación dinámica).
Entonces, en lugar de buscar cada nodo, mantenemos un registro de lo que ya se ha encontrado.
Mejor manera:
Código:
fuente
Código para A Breadth First Search para asegurarse de que ambos nodos estén en el árbol. Solo entonces avance con la búsqueda de LCA. Comente si tiene alguna sugerencia para mejorar. Creo que probablemente podamos marcarlos como visitados y reiniciar la búsqueda en un punto determinado donde lo dejamos para mejorar para el segundo nodo (si no se encuentra VISITADO)
fuente
Tiene razón en que sin un nodo principal, la solución con recorrido le dará O (n) complejidad de tiempo.
Enfoque transversal Suponga que está encontrando LCA para los nodos A y B, el enfoque más directo es obtener primero la ruta de la raíz a A y luego obtener la ruta de la raíz a B. Una vez que tenga estas dos rutas, puede iterar fácilmente sobre ellas. y encuentre el último nodo común, que es el ancestro común más bajo de A y B.
Solución recursiva Otro enfoque es utilizar la recursividad. Primero, podemos obtener LCA tanto del árbol izquierdo como del árbol derecho (si existe). Si A o B es el nodo raíz, entonces la raíz es el LCA y simplemente devolvemos la raíz, que es el punto final de la recursión. A medida que sigamos dividiendo el árbol en subárboles, eventualmente golpearemos A y B.
Para combinar soluciones de subproblemas, si LCA (árbol izquierdo) devuelve un nodo, sabemos que tanto A como B se ubican en el árbol izquierdo y el nodo devuelto es el resultado final. Si tanto LCA (izquierda) como LCA (derecha) devuelven nodos no vacíos, significa que A y B están en el árbol izquierdo y derecho respectivamente. En este caso, el nodo raíz es el nodo común más bajo.
Verifique el Ancestro común más bajo para obtener un análisis detallado y una solución.
fuente
Algunas de las soluciones aquí asumen que hay referencia al nodo raíz, algunas asumen que el árbol es un BST. Compartir mi solución usando hashmap, sin referencia al
root
nodo y al árbol puede ser BST o no BST:fuente
Solución 1: Recursivo - Más rápido
Solución 2: Iterativo - Uso de punteros principales - Más lento
fuente