Hoy iba a implementar un método para atravesar un gráfico arbitrariamente profundo y aplanarlo en un solo enumerable. En su lugar, hice un poco de búsqueda primero y encontré esto:Recorrido eficiente de gráficos con LINQ - eliminando la recursión
public static IEnumerable<T> Traverse<T>(this IEnumerable<T> enumerable, Func<T, IEnumerable<T>> recursivePropertySelector)
{
foreach (T item in enumerable)
{
yield return item;
IEnumerable<T> seqRecurse = recursivePropertySelector(item);
if (seqRecurse == null) continue;
foreach (T itemRecurse in Traverse(seqRecurse, recursivePropertySelector))
{
yield return itemRecurse;
}
}
}
En teoría esto se ve bien, pero en la práctica lo he encontrado funciona claramente peor que el uso de código escrito a mano equivalente (según sea la situación) pasar por un gráfico y hacer lo que sea necesario hacer. Sospecho que esto se debe a que en este método, por cada elemento que devuelve, la pila debe relajarse a un nivel arbitrariamente profundo.
También sospecho que este método funcionaría mucho más eficientemente si se eliminara la recursión. Además, no soy muy bueno para eliminar la recursión.
¿Alguien sabe cómo reescribir este método para eliminar la recursión?
Gracias por cualquier ayuda.
EDIT: Muchas gracias por todas las respuestas detalladas. Intenté comparar la solución original con la de Eric versus la de no usar un método de enumerador y, en lugar de eso, atravesar recursivamente con a lambda y, curiosamente, la recursión lambda es significativamente más rápida que cualquiera de los otros dos métodos.
class Node
{
public List<Node> ChildNodes { get; set; }
public Node()
{
ChildNodes = new List<Node>();
}
}
class Foo
{
public static void Main(String[] args)
{
var nodes = new List<Node>();
for(int i = 0; i < 100; i++)
{
var nodeA = new Node();
nodes.Add(nodeA);
for (int j = 0; j < 100; j++)
{
var nodeB = new Node();
nodeA.ChildNodes.Add(nodeB);
for (int k = 0; k < 100; k++)
{
var nodeC = new Node();
nodeB.ChildNodes.Add(nodeC);
for(int l = 0; l < 12; l++)
{
var nodeD = new Node();
nodeC.ChildNodes.Add(nodeD);
}
}
}
}
nodes.TraverseOld(node => node.ChildNodes).ToList();
nodes.TraverseNew(node => node.ChildNodes).ToList();
var watch = Stopwatch.StartNew();
nodes.TraverseOld(node => node.ChildNodes).ToList();
watch.Stop();
var recursiveTraversalTime = watch.ElapsedMilliseconds;
watch.Restart();
nodes.TraverseNew(node => node.ChildNodes).ToList();
watch.Stop();
var noRecursionTraversalTime = watch.ElapsedMilliseconds;
Action<Node> visitNode = null;
visitNode = node =>
{
foreach (var child in node.ChildNodes)
visitNode(child);
};
watch.Restart();
foreach(var node in nodes)
visitNode(node);
watch.Stop();
var lambdaRecursionTime = watch.ElapsedMilliseconds;
}
}
Dónde TraverseOld es el método original, TraverseNew es el método de Eric, y obviamente el lambda es la lambda.
En mi máquina, TraverseOld lleva 10127 ms, TraverseNew tarda 3038 ms, la recursión lambda tarda 1181 ms.
¿Es típico que los métodos del enumerador (con retorno de rendimiento) pueden tomar 3 veces más tiempo que la ejecución inmediata? ¿O algo más está pasando aquí?
A primera vista, parece que este método es recursiva se multiplican en todos los niveles, es decir, como un ingenuo fibonacci 'f (x) = f (x - 1) + f (x - 2) '. – mellamokb
La última versión no está cerca de la misma cantidad de trabajo que las dos primeras; en particular, no asigna ninguna lista, copia elementos de una matriz a otra, etc. Imagina que eres censista; si decides caminar simplemente de casa en casa y no te molestes, ya sabes, anotar cualquier información, entonces tu trabajo sería mucho más rápido. –
¿Dónde puedo encontrar TraverseOld y TraverseNew 3 años después? – user1756694