Oggi stavo per implementare un metodo per attraversare un grafo arbitrariamente profondo e appiattirlo in una singola enumerabile. Invece, ho fatto un po 'di ricerca prima e trovato questo:Attraversamento grafico efficiente con LINQ - eliminazione della ricorsione
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 teoria, questo sembra buono, ma in pratica ho trovato esegue significativamente più scarsa rispetto all'utilizzo di codice scritto a mano equivalente (come la situazione si verifica) per passare attraverso un grafico e fare tutto ciò che deve essere fatto. Sospetto che ciò avvenga perché in questo metodo, per ogni oggetto restituito, lo stack deve svolgersi a un livello arbitrariamente profondo.
Ho anche il sospetto che questo metodo sarebbe molto più efficiente se la ricorsione fosse eliminata. Inoltre, non sono molto bravo nell'eliminare la ricorsione.
Qualcuno sa come riscrivere questo metodo per eliminare la ricorsione?
Grazie per qualsiasi aiuto.
MODIFICA: Grazie mille per tutte le risposte dettagliate. Ho provato il benchmarking della soluzione originale contro la soluzione di Eric vs non usando un metodo di enumerazione e invece ricorsivamente attraversando con un lambda e stranamente, la ricorsione lambda è significativamente più veloce di uno degli altri due metodi.
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;
}
}
Dove TraverseOld è il metodo originale, TraverseNew è il metodo di Eric, e, ovviamente, il lambda è la lambda.
Sulla mia macchina, TraverseOld richiede 10127 ms, TraverseNew richiede 3038 ms, la ricorsione lambda richiede 1181 ms.
È questo tipico che i metodi di enumerazione (con rendimento restituito) possono richiedere 3 volte più che l'esecuzione immediata? O qualcos'altro sta succedendo qui?
A prima vista, sembra che questo metodo è recursing moltiplicare ad ogni livello, cioè, come un ingenuo fibonacci 'f (x) = f (x - 1) + f (x - 2) '. – mellamokb
L'ultima versione non sta facendo quasi la stessa quantità di lavoro dei primi due; in particolare, non sta allineando liste, copia elementi da matrice a serie e così via. Immagina di essere un candidato al censimento; se decidi semplicemente di andare di casa in casa e di non disturbarti, sai, annota qualsiasi informazione, quindi il tuo lavoro andrebbe molto più veloce. –
Dove posso trovare TraverseOld e TraverseNew 3 anni? – user1756694