今天我要实现一种方法来遍历任意深度图并将其展平为单个可枚举。相反,我先做了一点搜索,发现了这个:
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;
}
}
}
从理论上讲,这看起来不错,但实际上我发现它比使用等效的手写代码(因为情况出现)表现更差,无法通过图表完成任何需要做的事情。我怀疑这是因为在这个方法中,对于它返回的每个项目,堆栈必须放松到某个任意深度的水平。
我还怀疑如果递归被消除,这种方法会更有效地运行。我也不太擅长消除递归。
有谁知道如何重写此方法以消除递归?
感谢您的帮助。
编辑: 非常感谢所有详细的回复。我已尝试对原始解决方案与Eric的解决方案进行基准测试,而不使用枚举器方法,而是使用lambda递归遍历,奇怪的是,lambda递归明显快于其他两种方法。
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;
}
}
如果TraverseOld是原始方法,TraverseNew是Eric的方法,显然lambda是lambda。
在我的机器上,TraverseOld需要10127 ms,TraverseNew需要3038 ms,lambda递归需要1181 ms。
这是典型的枚举器方法(带有yield return)可能需要3倍才能立即执行吗?或者是其他事情发生在这里?
答案 0 :(得分:20)
首先,你绝对正确;如果图形具有n个平均深度为d的节点,那么幼稚嵌套迭代器产生的解决方案是时间为O(n * d),堆栈中为O(d)。如果d是n的很大一部分,那么这可以成为O(n 2 )算法,如果d很大,那么你可以完全吹掉堆栈。
如果您对嵌套迭代器的性能分析感兴趣,请参阅前C#编译器开发人员Wes Dyer的博客文章:
http://blogs.msdn.com/b/wesdyer/archive/2007/03/23/all-about-iterators.aspx
dasblinkenlight的解决方案是标准方法的变体。我通常会写这样的程序:
public static IEnumerable<T> Traverse<T>(
T root,
Func<T, IEnumerable<T>> children)
{
var stack = new Stack<T>();
stack.Push(root);
while(stack.Count != 0)
{
T item = stack.Pop();
yield return item;
foreach(var child in children(item))
stack.Push(child);
}
}
然后,如果你有多个根源:
public static IEnumerable<T> Traverse<T>(
IEnumerable<T> roots,
Func<T, IEnumerable<T>> children)
{
return from root in roots
from item in Traverse(root, children)
select item ;
}
现在,请注意,如果您拥有高度互连的图形或循环图形,则遍历不您想要的内容!如果你有一个带有向下箭头的图表:
A
/ \
B-->C
\ /
D
然后遍历是A,B,D,C,D,C,D。如果你有一个循环或互连的图形,那么你想要的是传递闭包。
public static IEnumerable<T> Closure<T>(
T root,
Func<T, IEnumerable<T>> children)
{
var seen = new HashSet<T>();
var stack = new Stack<T>();
stack.Push(root);
while(stack.Count != 0)
{
T item = stack.Pop();
if (seen.Contains(item))
continue;
seen.Add(item);
yield return item;
foreach(var child in children(item))
stack.Push(child);
}
}
此变体仅产生之前未产生的项目。
我也不太擅长消除递归。
我写了很多关于消除递归的方法,以及一般的递归编程。如果您对此主题感兴趣,请参阅:
http://blogs.msdn.com/b/ericlippert/archive/tags/recursion/
特别是:
答案 1 :(得分:8)
你是对的,在代码中递归地运行树和图表yield return
是效率低下的重要原因。
通常,您使用堆栈重写递归代码 - 与通常在编译代码中实现的方式类似。
我没有机会试一试,但这应该有效:
public static IEnumerable<T> Traverse<T>(this IEnumerable<T> enumerable, Func<T, IEnumerable<T>> recursivePropertySelector) {
var stack = new Stack<IEnumerable<T>>();
stack.Push(enumerable);
while (stack.Count != 0) {
enumerable = stack.Pop();
foreach (T item in enumerable) {
yield return item;
var seqRecurse = recursivePropertySelector(item);
if (seqRecurse != null) {
stack.Push(seqRecurse);
}
}
}
}
答案 2 :(得分:2)
通过复制递归与堆栈一起工作的基础知识,您总是可以消除递归。
疯狂聪明的理论答案:https://stackoverflow.com/a/933979/29093
http://cs.saddleback.edu/rwatkins/CS2B/Lab%20Exercises/Stacks%20and%20Recursion%20Lab.pdf
答案 3 :(得分:0)
您可以在代码中使用队列。队列可以初始化为列表,其中一个元素等于顶层节点。然后,您必须从第一个元素开始遍历列表中的每个元素。如果第一个元素包含子节点,则将它们全部附加到队列的末尾。然后移动到下一个元素。当你到达队列末尾时,你的图表将完全展平。