我有一个对象图,其中每个子对象都包含一个引用回其父对象的属性。是否有任何好的策略忽略父引用以避免无限递归?我曾考虑为这些属性添加特殊的[Parent]属性或使用特殊的命名约定,但也许有更好的方法。
答案 0 :(得分:32)
多么巧合;这是本周一my blog的话题。有关更多详细信息,请参阅在此之前,这里有一些代码可以让您了解如何执行此操作:
static IEnumerable<T> Traversal<T>(
T item,
Func<T, IEnumerable<T>> children)
{
var seen = new HashSet<T>();
var stack = new Stack<T>();
seen.Add(item);
stack.Push(item);
yield return item;
while(stack.Count > 0)
{
T current = stack.Pop();
foreach(T newItem in children(current))
{
if (!seen.Contains(newItem))
{
seen.Add(newItem);
stack.Push(newItem);
yield return newItem;
}
}
}
}
该方法有两个方面:一个项目,以及一个产生与项目相邻的所有东西的集合的关系。它产生深度优先遍历项目上的邻接关系的传递和反身闭包。假设分支因子不是有界的,假设图中的项数是n,并且最大深度是1 <= d <= n。这个算法使用显式堆栈而不是递归,因为(1)递归在这种情况下将应该是O(n)算法变成O(nd),然后是O(n)和O(n ^ 2)之间的东西, (2)如果d超过几百个节点,则过多的递归会使堆栈爆炸。
请注意,此算法的峰值内存使用量当然是O(n + d)= O(n)。
所以,例如:
foreach(Node node in Traversal(myGraph.Root, n => n.Children))
Console.WriteLine(node.Name);
有意义吗?
答案 1 :(得分:31)
如果循环可以被推广(你可以有任意数量的元素组成循环),你可以跟踪你已经在HashSet
中看到过的对象,如果对象已经在当你访问它时设置。或者在您访问它时设置的对象上添加一个标志(但是当您完成时必须返回并取消设置所有标志,并且图形一次只能由一个线程遍历)。
或者,如果循环只返回父节点,则可以保留对父节点的引用,而不是循环引用它的属性。
为简单起见,如果您知道父引用将具有某个名称,则您可能无法循环该属性:)
答案 2 :(得分:3)
如果您正在进行图遍历,则每个节点上都可以有一个“已访问”标志。这可以确保您不会重新访问节点并可能陷入无限循环。我相信这是执行图遍历的标准方法。
答案 3 :(得分:2)
我不确定你在这里尝试做什么,但是当你进行深度优先搜索的广度优先搜索时,你可以维护一个包含所有先前访问过的节点的哈希表。
答案 4 :(得分:2)
这是一个常见问题,但最佳方法取决于方案。另一个问题是,在许多情况下,两次访问同一个对象并不是问题 - 这并不意味着递归 - 例如,考虑树:
A
=> B
=> C
=> D
=> C
这可能是有效的(想想XmlSerializer
,它只会简单地将C
实例写出两次),因此通常需要在堆栈上推送/弹出对象以检查真正的递归。我最后一次实现“访问者”时,我保留了一个“深度”计数器,并且只启用了超过某个阈值的堆栈检查 - 这意味着大多数树只是最终做了一些{{1} } / ++
,但没有更贵的。你可以看到我采用的方法here。
答案 5 :(得分:0)
我发布了一篇帖子,详细解释了代码示例如何通过递归反射进行对象遍历,还检测并避免递归引用以防止堆栈溢出异常:https://doguarslan.wordpress.com/2016/10/03/object-graph-traversal-by-recursive-reflection/
在该示例中,我使用递归反射进行了深度优先遍历,并且我为参考类型维护了受访节点的HashSet。有一点需要注意的是使用自定义相等比较器初始化HashSet,它使用对象引用进行哈希计算,基本上是由基础对象类本身实现的GetHashCode()方法,而不是任何重载版本的GetHashCode(),因为如果类型您遍历重载GetHashCode方法的属性,您可能会检测到错误的哈希冲突,并认为您检测到一个递归引用,实际上可能是GetHashCode的重载版本通过一些启发式方法生成相同的哈希值并混淆了HashSet,所有您需要的detect是检查对象树中任何位置指向内存中相同位置的父子。