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?
InformationsquelleAutor MgSam | 2012-04-20
Schreibe einen Kommentar