Graphen-Traversierung mit LINQ - Beseitigung der Rekursion
Heute ich im Begriff war, eine Methode zu implementieren, um die traverse eine beliebig Tiefe Diagramm und glätten Sie es in eine einzige zählbare. Stattdessen habe ich ein wenig suchen, erste und fand dies:
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;
}
}
}
In der Theorie gut aussieht, aber in der Praxis habe ich gefunden, es führt wesentlich schlechter als gleichwertig mit von hand geschriebenen code (als die situation entsteht, zu gehen durch einen Graphen und tun, was getan werden muss. Ich vermute, dass dies ist, weil in dieser Methode wird für jedes Element, das es gibt, hat der Stapel zu entspannen, um einige beliebig tiefen Ebene.
Ich vermute auch, dass diese Methode viel effizienter, wenn die Rekursion beseitigt wurden. Ich habe nebenbei auch nicht sehr gut auf die Beseitigung von Rekursion.
Weiß jemand, wie man diese umschreiben Methode zur Beseitigung der Rekursion?
Vielen Dank für jede Hilfe.
BEARBEITEN:
Vielen Dank für all die ausführlichen Antworten. Ich habe versucht, benchmarking der ursprünglichen Lösung gegen Eric die Lösung von vs nicht mit einem enumerator-Methode und stattdessen rekursiv Durchlaufen mit einem lambda-und seltsam, die lambda-Rekursion ist deutlich schneller als die beiden anderen Methoden.
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;
}
}
Wo TraverseOld ist die ursprüngliche Methode, TraverseNew ist Eric ' s Methode, und natürlich die lambda ist lambda.
Auf meinem Rechner, TraverseOld nimmt 10127 ms, TraverseNew nimmt 3038 ms, die lambda-Rekursion dauert 1181 ms.
Ist das typisch, dass enumerator Methoden (mit Rendite) dauert 3X so lange, wie Sie gegen die sofortige Ausführung? Oder ist etwas anderes passiert hier?
- Auf den ersten Blick sieht es aus wie diese Methode ist recursing multiplizieren auf jeder Ebene, d.h., wie eine naive fibonacci
f(x) = f(x - 1) + f(x - 2)
. - Die Letzte version nicht irgendwo in der Nähe die gleiche Menge an Arbeit als die ersten beiden; insbesondere ist es nicht die Zuteilung irgendwelche Listen, die das kopieren von Elementen von array zu array, und so weiter. Stell dir vor, du bist ein Zensus-taker; wenn Sie sich entscheiden, gehen einfach von Haus zu Haus und nicht die Mühe, Sie wissen, notieren Sie sich die Informationen, dann ist Ihr job würde viel schneller gehen.
- Wo finde ich TraverseOld und TraverseNew 3 Jahre später?
Du musst angemeldet sein, um einen Kommentar abzugeben.
First off, Sie sind absolut richtig; wenn der graph hat n Knoten Durchschnittliche Tiefe d, dann der naive geschachtelte Iteratoren Ertrag einer Lösung, die O(n*d) Zeit und O(d) in den stack. Falls d ein großer Teil von n, dann kann sich ein O(n2) - Algorithmus, und wenn d groß ist, dann können Sie Blasen den stack vollständig.
Wenn Sie interessiert sind, in einer performance-Analyse von geschachtelten Iteratoren, siehe ehemaliger C# - compiler-Entwickler Wes Dyer ' s blog-post:
http://blogs.msdn.com/b/wesdyer/archive/2007/03/23/all-about-iterators.aspx
dasblinkenlight die Lösung ist eine variation der standard-Ansatz. Ich würde in der Regel schreiben Sie das Programm wie dieses:
Und dann, wenn Sie haben mehrere Wurzeln:
Nun, beachten Sie, dass eine Traversierung ist nicht, was Sie wollen, wenn Sie haben eine stark vernetzte Graphen oder eine zyklische Graphen! Wenn Sie ein Diagramm mit den nach unten weisenden Pfeile:
dann der Traversierung ist A, B, D, C, D, C, D. Wenn Sie eine zyklische oder miteinander verbundener graph dann, was Sie wollen, ist die transitive Abschluss.
Dieser Variante werden nur die Erträge Objekte, die nicht dabei vor.
Ich habe geschrieben, eine Reihe von Artikeln über die Möglichkeiten zur Beseitigung der Rekursion, und über rekursive Programmierung im Allgemeinen. Wenn das Thema interessiert, finden Sie unter:
http://blogs.msdn.com/b/ericlippert/archive/tags/recursion/
Insbesondere:
http://blogs.msdn.com/b/ericlippert/archive/2005/08/01/recursion-part-two-unrolling-a-recursive-function-with-an-explicit-stack.aspx
http://blogs.msdn.com/b/ericlippert/archive/2005/08/04/recursion-part-three-building-a-dispatch-engine.aspx
http://blogs.msdn.com/b/ericlippert/archive/2005/08/08/recursion-part-four-continuation-passing-style.aspx
ISet<T>.Add
gibt einen booleschen Wert, der angibt, ob das Element wurde Hinzugefügt-wenn nicht, war es schon da. So können Sie beseitigen den AufrufContains
so:T item = stack.Pop(); if (!seen.Add(item)) continue; ...
Du hast Recht, Wandern Bäume und Graphen rekursiv in code
yield return
ist eine große Quelle der Ineffizienz.In der Regel, Sie umschreiben rekursiven code mit stack - in ähnlicher Weise, wie es ist in der Regel umgesetzt in kompilierter code.
Habe ich nicht bekommen eine chance, es zu versuchen, aber dies sollte funktionieren:
Kann man immer beseitigen Rekursion durch die Replikation, die Grundlagen, wie die Rekursion arbeitet mit einem stack.
Crazy smart theoretische Antwort: https://stackoverflow.com/a/933979/29093
http://cs.saddleback.edu/rwatkins/CS2B/Lab%20Exercises/Stacks%20and%20Recursion%20Lab.pdf
Können Sie eine queue in Ihrem code. Die Warteschlange kann initialisiert werden, wie eine Liste mit einem element gleich dem obersten Knoten. Dann müssen Sie Durchlaufen und jedes element der Liste, beginnend von der ersten. Wenn das erste element enthält untergeordneten Knoten, die Sie anfügen, Sie alle auf das Ende der Warteschlange. Dann gehen Sie zu dem nächsten element. Ihr graph wird vollständig reduzieren, wenn Sie erreichen das Ende der Warteschlange.