2008-12-30 21 views
5

Estoy intentando descubrir cómo implementar una función en un nodo de árbol que devuelve todas sus hojas descendientes (ya sean directas o indirectas). Sin embargo, no quiero pasar un contenedor en el que los nodos hoja se colocarán de forma recursiva (el árbol podría ser enorme), en cambio me gustaría utilizar un generador para iterar a través del árbol. He intentado algunos enfoques, pero ninguno de ellos funcionó hasta ahora. Este es el más cercano que he llegado a una posible solución:¿Cómo iterar a través de una estructura de árbol usando un generador?

public interface ITreeNode 
    { 
     IEnumerable<ITreeNode> EnumerateLeaves();    
    } 

    class Leaf : ITreeNode 
    { 
     public IEnumerable<ITreeNode> EnumerateLeaves() 
     { 
      throw new NotImplementedException(); 
     } 
    } 

    class Branch : ITreeNode 
    { 
     private List<ITreeNode> m_treeNodes = new List<ITreeNode>(); 

     public IEnumerable<ITreeNode> EnumerateLeaves() 
     { 
      foreach(var node in m_treeNodes) 
      { 
       if(node is Leaf) 
        yield return node; 
       else 
        node.EnumerateLeaves(); 
      } 
     } 
    } 

Pero esto tampoco está funcionando. ¿Qué estoy haciendo mal? Parece que llamar .EnumerateLeaves recursivamente no funcionará si hay una declaración de rendimiento en la misma función.

Cualquier ayuda sería muy apreciada. Gracias por adelantado.

Editar: Olvidé mencionar que una rama puede tener hojas o ramas como niños, de ahí la recursión.

+0

No es ésta una pregunta .NET? –

+0

Sí - retenido. La etiqueta 'Programación' es redundante en un sitio de programación. =) –

Respuesta

7

Así es como se debe implementar Branch.EnumerateLeaves:

public IEnumerable<ITreeNode> EnumerateLeaves() 
{ 
    foreach(var node in m_treeNodes) 
    { 
     if(node is Leaf) 
      yield return node; 
     else 
     { 
      foreach (ITreeNode childNode in node.EnumerateLeaves()) 
       yield return childNode; 
     } 
    } 
} 
+0

Eso tampoco funcionará, ya que una sucursal podría tener otras parcelas cuando eran niños, y es por eso que utilicé la recursión. – Trap

+0

Déjame editar la respuesta. –

+0

Allí, eso debería ser el truco, ahora usará la recursión correctamente. –

3

lassevk es correcto - un problema potencial con este método, sin embargo, es que la llamada recursiva en enumerables puede rendir O (n^2) el rendimiento. Si esto es un problema, entonces debes factorizar la recursión y usar una pila interna.

public IEnumerable<ITreeNode> EnumerateLeaves() 
{ 
    Stack<ITreeNode> workStack = new Stack<ITreeNode>(m_treeNodes); 

    while(workStack.Count > 0) { 
     var current = workStack.Pop(); 
     if(current is Leaf) 
      yield return current; 
     else { 
      for(n = 0; n < current.m_treeNodes.Count; n++) { 
       workStack.Push(current.m_treeNodes[n]); 
      } 
     } 
    } 
} 

Esto debería realizar la misma función, sin recurrencia.

Editar: Daniel Plaisted publicado en un comentario sobre un problema mayor con los enumeradores de llamadas recursivas, resumidas en un post on the MSDN Blogs regarding iterators. Gracias Daniel. =)

Otra Edición: Recuerde siempre que la simplicidad del código puede ser más importante que el rendimiento, especialmente si espera que otros mantengan su código. Si no espera que su árbol crezca demasiado, utilice el método de recursión lassevk que se describe en su respuesta.

+0

El GC no es el mayor problema con la recursión, es el hecho de que en cada iteración tiene que recorrer el "árbol" de los enumeradores desde la parte superior. Esto puede hacer que el tiempo de ejecución aumente dramáticamente. Ver http://blogs.msdn.com/wesdyer/archive/2007/03/23/all-about-iterators.aspx –

+0

Eso es muy cierto, aunque no pude pensar en cómo expresarlo. Gracias por el enlace, muy útil. =) –

1

Las otras respuestas son correctas, pero el patrón de tener un retorno de rendimiento dentro de un bucle foreach con una llamada recursiva hará que el tiempo de ejecución itere a través del árbol algo así como O (número de nodos * profundidad promedio de un nodo) . Consulte this blog post para obtener más detalles sobre el problema.

Aquí es un intento de un generador que es eficiente, tanto en tiempo de ejecución y el uso de memoria:

class Node 
{ 
    List<Node> _children; 

    public bool IsLeaf { get { return _children.Count == 0; } } 

    public IEnumerable<Node> Children { get { return _children; } } 

    public IEnumerable<Node> EnumerateLeaves() 
    { 
     if (IsLeaf) 
     { 
      yield return this; 
      yield break; 
     } 

     var path = new Stack<IEnumerator<Node>>(); 

     path.Push(Children.GetEnumerator()); 

     while(!path.Empty) 
     { 
      var cur = path.Pop(); 
      if (cur.MoveNext()) 
      { 
       path.Push(cur); 
       if (cur.IsLeaf) 
       { 
        yield return cur; 
       } 
       else 
       { 
        path.Push(cur.Children.GetEnumerator()); 
       } 
      } 

     } 
    } 

} 
+0

¿Puedes explicar cómo es mejor que O (n^2)? No lo estoy viendo –

+0

Es O (n log n). En el enlace que proporcionó, la profundidad de recursión fue n. En un árbol, es log n en un caso promedio. – recursive

+0

Lo actualicé para indicar que el tiempo de ejecución del método recursivo es O (número de nodos * profundidad promedio de un nodo). Este debería tener un tiempo de ejecución de solo O (número de nodos), que es el mismo que el de Erik. Este teóricamente tiene menos uso de memoria. –

Cuestiones relacionadas