¿Se puede atravesar un árbol sin recursión, apilamiento o cola, y solo un puñado de punteros?

15

Hace media década, estaba sentado en una clase de estructuras de datos donde el profesor ofrecía crédito adicional si alguien podía atravesar un árbol sin usar recursividad, una pila, cola, etc. (o cualquier otra estructura de datos similar) y solo algunos indicadores. Se me ocurrió lo que pensé que era una respuesta obvia a esa pregunta que finalmente fue aceptada por el profesor. Estaba sentado en una clase de matemática discreta con otro profesor en el mismo departamento, y él afirmó que era imposible atravesar un árbol sin recursión, una pila, cola, etc., y que mi solución no era válida.

Entonces, ¿es posible o imposible? ¿Por qué o por qué no?

Editar: para agregar algunas aclaraciones, implementé esto en un árbol binario que tenía tres elementos: los datos almacenados en cada nodo y punteros a dos hijos. Mi solución podría extenderse a árboles n-ary con solo unos pocos cambios.

Mi maestro de estructuras de datos no impuso ninguna restricción contra la mutación del árbol, y de hecho descubrí más tarde que su propia solución era usar los punteros de los niños para señalar el árbol en su camino hacia abajo. Mi discreto profesor de matemáticas dijo que cualquier mutación de un árbol significa que ya no es un árbol de acuerdo con la definición matemática de un árbol, su definición también excluiría cualquier puntero a los padres, lo que coincidiría con el caso donde lo resolví anteriormente.

NL - Disculpate con Monica
fuente
3
Necesita especificar las restricciones. ¿Se me permite mutar el árbol? ¿Cómo se representa el árbol? (Por ejemplo, ¿cada nodo tiene un puntero padre a su padre?) La respuesta dependerá de las restricciones específicas; sin especificar esas restricciones, este no es un problema bien planteado.
DW
2
Supongo que la restricción que los profesores realmente querían expresar era "con espacio adicional". Pero, ¿cuál fue su solución, de todos modos? O(1)
Raphael

Respuestas:

17

Una gran cantidad de investigación en esta área ha sido domo, motivado por el método de atravesar árboles "a bajo costo" y estructuras de listas generales en el contexto de la recolección de basura.

Un árbol binario enhebrado es una representación adaptada de árboles binarios donde se utilizan algunos punteros nulos para enlazar a nodos sucesores en el árbol. Esta información adicional se puede utilizar para atravesar un árbol sin pila. Sin embargo, es necesario un bit adicional por nodo para distinguir los subprocesos de los punteros secundarios. Wikipedia: Tree_traversal

Hasta donde yo sé, los árboles binarios implementados utilizando punteros de la manera habitual (puntero izquierdo y derecho por nodo) se pueden recorrer utilizando el método de hilos, en un método atribuido a Morris . Los punteros NIL se reutilizan temporalmente para enhebrar un camino de regreso a la raíz. La parte inteligente es que durante el recorrido uno puede distinguir los bordes originales de los enlaces de hilo temporales, utilizando la forma en que forman ciclos en el árbol).

Buena parte: sin estructura de datos adicional. Parte mala: engañando un poco, la pila está dentro del árbol de una manera inteligente. Muy inteligente.

Una prueba de la pila oculta se muestra en P. Mateti y R. Manghirmalani: Algoritmo transversal del árbol de Morris Reconsiderado DOI: 10.1016 / 0167-6423 (88) 90063-9

JM Morris: atravesar árboles binarios de manera simple y económica. IPL 9 (1979) 197-200 DOI: 10.1016 / 0020-0190 (79) 90068-1

Luego también está el escaneo de Lindstrom . Este método "rota" los tres punteros involucrados en cada nodo (padre y dos hijos). Si desea realizar algún algoritmo decente de pedido previo o posterior, necesita bits adicionales por nodo. Si solo desea visitar todos los nodos (tres veces, pero nunca sabe qué visita realiza), puede hacerlo sin los bits.

G. Lindstrom: Escaneo de estructuras de listas sin pilas o bits de etiquetas. IPL 2 (1973) 47-51. DOI: 10.1016 / 0020-0190 (73) 90012-4

Quizás la forma más directa es un método de Robson . Aquí la pila necesaria para el algoritmo clásico se enhebra a través de las hojas.

JM Robson: un algoritmo mejorado para atravesar árboles binarios sin pila auxiliar IPL 1 (1973) 149-152. 10.1016 / 0020-0190 (73) 90018-5

IPL = Cartas de procesamiento de información

Hendrik Jan
fuente
También me gusta esta solución, aunque no es nada con lo que hubiera surgido durante mi primer año de clases de informática. Sí, probablemente haciendo trampa de acuerdo con las reglas de mi profesor.
NL - Pídele disculpas a Monica el
2
¿Puedes dar enlaces / referencias para las estrategias?
Raphael
1
La parte realmente mala de ese método es que no puede tener más de un recorrido al mismo tiempo.
Gilles 'SO- deja de ser malvado'
6

v

Yuval Filmus
fuente
Esto es similar a la solución que el profesor de estructuras de datos que propuso el problema usó para resolverlo. El profesor de matemática discreto objetó que "esto se ha convertido en un gráfico en lugar de un árbol" si hay sugerencias para los padres.
NL - Pídele disculpas a Monica el
@NathanLiddle: Eso dependería de la definición de árbol utilizada (que no proporcionó). En el "mundo real", la representación de árbol de Yuval es razonable incluso si la teoría de grafos dijera que las cosas que define no son árboles, por supuesto.
Raphael
@Raphael Sí, y cumple con los requisitos del profesor original, por lo que es una respuesta aceptable para mí.
NL - Pídele disculpas a Monica el
0

Mi solución fue un primer recorrido de cría utilizando bucles for anidados para forzar el árbol con fuerza bruta. Esto no es eficiente de ninguna manera, y de hecho una estructura de datos recursiva como un árbol está pidiendo un recorrido recursivo, pero la pregunta no era si un árbol podría atravesarse de manera eficiente, sino si era posible.

Pseudocode:
root = pointer root 
depth = integer 0
finished = bool false
//If we n-ary tree also track how many children have been found 
//on the node with the most children for the purposes of this psuedocode 
//we'll assume a binary tree and insert a magic number of 2 so that we 
//can use bitwise operators instead of integer division 
while(!finished)
    ++depth
    treePosition = pointer root
    finished = true;
    for i := 0..2**depth
        for j := 0..depth
            if (i & j) //bitwise operator explained below
                // if right child doesn't exist break the loop
                treePosition = treePosition.rightChild
            else
                // if left child doesn't exist break the loop
                treePosition = treePosition.leftChild
        if j has any children
            finished = false
            do anything else you want when visiting the node

Para los primeros niveles se vería así, como puede ver, el operador bit a bit en el pseudocódigo simplemente decide un giro a la izquierda o derecha en un árbol binario:

2**1       0               1
2**2   00      01      10      11
2**3 000 001 010 011 100 101 110 111

Para n-ary, tomaría i% (maxChildren ** j) / j para determinar qué ruta tomar entre 0 y maxChildren.

En cada nodo en n-ary también necesitaría verificar si el número de hijos es mayor que maxChildren y actualizarlo adecuadamente.

NL - Disculpate con Monica
fuente
Si quisiera usar más que binario, necesitaría reemplazar el número mágico 2 con una variable que se incrementaría para coincidir con el máximo de hijos que vio, y en lugar de operadores bit a bit, necesitaría dividir por la misma variable elevada a El poder de la profundidad del árbol donde estabas.
NL - Pídele disculpas a Monica el
O(1)O(lgnorte)O(1)O(norte)O(1)espacio adicional "). Por ejemplo, ¿qué hacer si depthexcede el ancho de int?
DW
DW, el profesor que planteó el problema no impuso esa restricción al problema, y ​​lo que me molestó tanto de mi discusión con el discreto profesor de matemáticas es que NUNCA reconoció que incluso era posible atravesar un árbol sin recurrencia, apilar, o hacer cola, sea cual sea el costo. Lo único que demuestra mi solución es que es posible hacer cualquier cosa de forma iterativa que se pueda hacer de forma recursiva, incluso si elimina las opciones para apilar,
poner en
Una cosa es decir que no se puede resolver sin O (1) espacio adicional, y otra muy distinta es declarar que el problema no se puede resolver sin recurrencia, apilamiento o cola. Y en realidad, después de ver mi código, el profesor discreto de matemáticas aún no admitió el punto porque dijo que "i" en el primer bucle estaba tomando el lugar de una cola. ¿Cómo es eso de cabeza dura?
NL - Pídele disculpas a Monica el
1
idepthdepthΘ(norte)Θ(norte)iO(1)