我正在尝试找出一种好的方法来使用迭代方法确定特定C#表达式树的深度。我们使用表达式进行某些动态评估,在罕见(错误)情况下,系统可能会尝试处理一个Expression Tree,该树非常大,以至于会使堆栈溢出。我正在尝试找出在允许评估树之前检查树深度的方法。
我正在尝试找出一种好的方法来使用迭代方法确定特定C#表达式树的深度。我们使用表达式进行某些动态评估,在罕见(错误)情况下,系统可能会尝试处理一个Expression Tree,该树非常大,以至于会使堆栈溢出。我正在尝试找出在允许评估树之前检查树深度的方法。
不要试图专门解决表达式树问题,让我为您描述一些处理行为不佳的树的常规技巧。
您可能希望先阅读我的一系列文章,解决您提出的问题:如何在不使用递归的情况下确定树的深度?
这些文章是我在开发JScript时编写的,所以示例都是用JScript编写的。不过很容易看出如何将这些概念应用到C#中。class Node
{
public Node Left { get; private set; }
public Node Right { get; private set; }
public string Value { get; private set; }
public Node(string value) : this(null, null, value) {}
public Node(Node left, Node right, string value)
{
this.Left = left;
this.Right = right;
this.Value = value;
}
}
...
Node n1 = new Node("1");
Node n2 = new Node("2");
Node n3 = new Node("3");
Node n3 = new Node("4");
Node n5 = new Node("5");
Node p1 = new Node(n1, n2, "+");
Node p2 = new Node(p1, n3, "*");
Node p3 = new Node(n4, n5, "+");
Node p4 = new Node(p2, p3, "-");
所以我们有树p4:
-
/ \
* +
/ \ / \
+ 3 4 5
/ \
1 2
(((1+2)*3)-(4+5))
递归解法很直接:
static void RecursiveToString(Node node, StringBuilder sb)
{
// Again, assuming either zero or two children.
if (node.Left != null)
sb.Append(node.Value);
else
{
sb.Append("(");
RecursiveToString(node.Left, sb);
sb.Append(node.Value);
RecursiveToString(node.Right, sb);
sb.Append(")");
}
}
现在假设我们知道树的左侧很可能是“深”的,但右侧是“浅”的。我们能否消除左侧的递归?
static void RightRecursiveToString(Node node, StringBuilder sb)
{
// Again, assuming either zero or two children.
var stack = new Stack<Node>();
stack.Push(node);
while(stack.Peek().Left != null)
{
sb.Append("(");
stack.Push(stack.Peek().Left);
}
while(stack.Count != 0)
{
Node current = stack.Pop();
sb.Append(current.Value);
if (current.Right != null)
RightRecursiveToString(current.Right, sb);
sb.Append(")");
}
}
}
递归右侧版本显然更难阅读和理解,但它不会导致堆栈溢出。
让我们来看看我们的例子。
push p4
push p2
output (
push p1
output (
push n1
output (
loop condition is met
pop n1
output 1
go back to the top of the loop
pop p1
output +
recurse on n2 -- this outputs 2
output )
go back to the top of the loop
pop p2
output *
recurse on n3 -- this outputs 3
output )
go back to the top of the loop
pop p4
output -
recurse on p3
push p3
push n4
output (
loop condition is met
pop n4
output 4
go back to the top of the loop
pop p3
output +
recurse on n5 -- this outputs 5
output )
loop condition is not met; return.
output )
loop condition is not met, return.
我们输出什么?(((1+2)*3)-(4+5))
,如所需。
因此,您已经看到我可以从两个递归降至一个递归。我们可以使用类似的技术将一个递归降至零。将此算法完全迭代 - 使其既不在左侧也不在右侧递归 - 留作练习。
(顺便说一句:我会在面试中问这个问题的变体,因此如果你被我面试了,你现在就有了一个不公平的优势!)
ExpressionVisitor
是递归的,但是通过一个技巧,你可以将其转换为迭代式的。base.Visit()
访问其所有子节点,然后将这些子节点添加到队列中而不是立即进行递归处理。Expression
子类型的代码,但也可以解决 ExpressionVisitor
的递归特性。class DepthVisitor : ExpressionVisitor
{
private readonly Queue<Tuple<Expression, int>> m_queue =
new Queue<Tuple<Expression, int>>();
private bool m_canRecurse;
private int m_depth;
public int MeasureDepth(Expression expression)
{
m_queue.Enqueue(Tuple.Create(expression, 1));
int maxDepth = 0;
while (m_queue.Count > 0)
{
var tuple = m_queue.Dequeue();
m_depth = tuple.Item2;
if (m_depth > maxDepth)
maxDepth = m_depth;
m_canRecurse = true;
Visit(tuple.Item1);
}
return maxDepth;
}
public override Expression Visit(Expression node)
{
if (m_canRecurse)
{
m_canRecurse = false;
base.Visit(node);
}
else
m_queue.Enqueue(Tuple.Create(node, m_depth + 1));
return node;
}
}
base.Visit()
不会创建新的访客,我认为这没有任何意义。 - svickm_queue.Enqueue()
行完成的。处理表达式的每个子表达式是通过调用 base.Visit()
完成的,该方法会为每个子表达式调用 Visit()
。 - svickDepthVisitor
执行。 - svickStack
。由于这将在堆中存储所有尚未处理的节点信息,因此需要更多的空间才能运行。public static IEnumerable<Tuple<T, int>> TraverseWithDepth<T>(IEnumerable<T> items
, Func<T, IEnumerable<T>> childSelector)
{
var stack = new Stack<Tuple<T, int>>(
items.Select(item => Tuple.Create(item, 0)));
while (stack.Any())
{
var next = stack.Pop();
yield return next;
foreach (var child in childSelector(next.Item1))
{
stack.Push(Tuple.Create(child, next.Item2 + 1));
}
}
}
Max
保留在内存中,因此仅保存那些尚未被处理但已经有一个已处理的父节点的节点。public static int GetDepth(Expression t)
{
return TraverseWithDepth(new[] { t }, GetDirectChildren)
.Max(pair => pair.Item2);
}
foreach(var child in childSelector(next.Item1).Reverse())
来解决。 - Eric Lippert