【问题标题】:How to determine the depth of a C# Expression Tree Iterativly?如何迭代地确定 C# 表达式树的深度?
【发布时间】:2013-03-20 11:55:32
【问题描述】:

我正在尝试找出是否有一种使用迭代方法来确定特定 C# 表达式树深度的好方法。我们将表达式用于一些动态评估,并且在极少数(错误)情况下,系统可以尝试处理一个太大而导致堆栈溢出的表达式树。我正在尝试找出一种在允许评估树之前检查树深度的方法。

【问题讨论】:

  • 你确定那里没有无限递归吗?堆栈是相当大的东西。这篇文章建议您最多可以进行 18.000 次递归调用 (stackoverflow.com/questions/4513438/…)
  • @alex - 积极的。我们计算出我们看到问题的确切深度,并且能够证明如果我们将表达式树简化到该深度它会运行正常,但是将其增加 1 会导致问题。对于我们的情况,表达式深度为 517,表达式树解析中每次递归 3 个堆栈帧。
  • @alex 您可以进行的递归调用的数量是堆栈大小的乘积(可以更改默认值,并且在任何给定点您拥有多少取决于之前的代码执行)以及您放入堆栈的数据的大小,这直接受函数调用中参数的数量和大小的影响。因此,对于一个非常具体的案例,需要 18,000 个。
  • 您在使用哪种树?只是想出个主意。到目前为止,您尝试过什么?我没有看到任何不包括递归的解决方案。
  • @LarsUdengaard 这是一个 C# ExpressionTree。我认为这个问题可能没有答案,但我想我应该检查一下是否有人知道迭代遍历它们的方法。对于普通树,只需将需要处理的节点添加到列表中,打开每个节点,将其节点添加到需要处理的集合中,然后将其从列表中删除。此外,有必要跟踪列表中每个节点的深度。不过,我没有看到用 ExpressionTree 做到这一点的方法。

标签: c# lambda expression


【解决方案1】:

与其尝试专门解决表达式树的问题,不如让我为您描述一些处理不良行为树的通用技术。

您可能想先阅读我关于解决您提出的问题的系列文章:如何在不使用递归的情况下确定树的深度

http://blogs.msdn.com/b/ericlippert/archive/2005/07/27/recursion-part-one-recursive-data-structures-and-functions.aspx

那些文章是我在做 JScript 的时候写的,所以例子都是用 JScript 写的。不过,如何将这些概念应用到 C# 中并不难。

让我给你一个 C# 中的小玩具示例,说明如何在不进行完全递归的情况下对递归数据结构进行操作。假设我们有以下二叉树:(假设 WOLOG 的二叉树节点是零个或两个子节点,从不完全是一个。)

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

我们希望将 p4 打印为带括号的表达式

   (((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)),根据需要。

所以你在这里看到我可以从两个递归减少到一个。我们可以使用类似的技术从一个递归到没有递归。让这个算法完全迭代——这样它既不会在左边也不会在右边递归——留作练习。

(顺便说一句:我问这个问题的一个变体作为一个面试问题,所以如果你被我面试过,你现在有一个不公平的优势!)

【讨论】:

  • 感谢您详细撰写有关迭代搜索树与递归搜索的文章。我认为这对于其他提出问题的人来说非常有价值,但我的问题实际上不是如何简单地迭代地遍历一棵树(只需将每个节点的子节点和该节点的深度添加到队列中,从队列,并继续处理队列中的下一个节点。跟踪存储的最大深度,它将迭代地扫描一棵树。)我的问题更多是到目前为止我无法追踪如何具体地遍历 ExpressionTree 结构。
  • 我找到了 ExpressionVisitor,但如果我的理解是正确的,它似乎是递归访问,因此不适合。是否有其他结构可以干净地公开节点以便我可以迭代它们?
  • @AJHenderson:我的建议是您编写自己的表达式树访问者版本,它在可能深度递归的节点上进行迭代。
【解决方案2】:

.Net 中包含的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;
    }
}

【讨论】:

  • 这似乎正是我想要的,因为它提供了一种迭代方式来获取每个元素中的节点。我不清楚的一件事是,这次访问显然不像我预期的那样工作。访客之间的私人队列如何保持一致?是否重复使用了同一个访客?
  • @AJHenderson 没有其他访客。如果您创建一位访问者,那么将只有一位访问者。 base.Visit() 不会创建新的访问者,我认为这没有任何意义。
  • 子表达式是如何入队的呢?我没有看到任何会迭代地将它们添加到队列中的调用。我只能通过手动进入更具体类型的 Expression 对象来找到它们。
  • @AJHenderson 每个表达式的排队由m_queue.Enqueue() 行完成。处理表达式的每个子表达式是通过调用base.Visit() 来完成的,它为每个子表达式调用Visit()
  • 是的,但是由于它正在下降一个递归级别,这不会是一个不同的访问者吗?
【解决方案3】:

您总是可以使用显式内存结构来代替使用递归来迭代树。如果您想密切模仿递归行为,您甚至可以使用明确的Stack。由于这是在堆中存储尚未处理的节点上的所有信息,因此需要更多的时间才能用完它。

这是一种通用方法,它遍历基于树的结构(迭代地,而不是递归地)并返回所有项目的扁平序列以及该项目的深度。

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));
        }
    }
}

现在要使用它,我们需要做的就是传入根节点,这是一个将每个元素映射到其直接子节点的函数,然后我们可以取深度的最大值。由于延迟执行,由 traverse 函数产生的每个项目不会被 Max 保留在内存中,因此内存中唯一保留的项目是尚未处理的节点,但有一个已处理的父节点。

public static int GetDepth(Expression t)
{
    return TraverseWithDepth(new[] { t }, GetDirectChildren)
        .Max(pair => pair.Item2);
}

【讨论】:

  • 一个不错的解决方案。一个小的缺点是,如果子选择器从“左到右”迭代子代,则此代码将按从“右到左”的顺序枚举它们。如果这很重要,您可以随时说foreach(var child in childSelector(next.Item1).Reverse())
猜你喜欢
  • 2012-11-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-10
  • 2014-02-03
相关资源
最近更新 更多