Как определить глубину на C# дерево выражения Iterativly?

Я пытаюсь выяснить, есть ли хороший способ выяснить глубину конкретного дерева выражений C#, используя итерационный подход. Мы используем выражения для некоторой динамической оценки, и в редких (ошибочных) условиях система может попытаться обработать дерево выражений, которое настолько велико, что оно выдувает стек. Я пытаюсь выяснить способ проверить глубину дерева, прежде чем позволить дереву быть оцененным.

3 ответов


на ExpressionVisitor который включен в .Net является рекурсивным,но с помощью трюка вы можете превратить его в итеративный.

в основном, вы обрабатываете очередь узлов. Для каждого узла в очереди, используйте 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;
    }
}

вместо того, чтобы пытаться решить вашу проблему для деревьев выражений конкретно, позвольте мне описать вам некоторые общие методы борьбы с плохо себя ведут деревья.

вы можете начать с чтения моей серии статей о решении проблемы, которую вы ставите:как определить глубину дерева без использования рекурсии?

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

эти статьи были написаны, когда я работал над JScript, поэтому примеры находятся в JScript. Однако не слишком сложно понять, как применить эти понятия к C#.

позвольте мне дать вам небольшой пример игрушки на 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

и мы хотим распечатать 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)), как хотелось бы.

Итак, вы видели здесь, что я могу перейти от двух рекурсий к одной. Мы можем использовать аналогичные методы, чтобы перейти от одной рекурсии к none. Сделать этот алгоритм полностью итеративным - чтобы он рекурсы ни слева, ни справа-это левое упражнение.

(и кстати: я задаю вариацию этой проблемы как вопрос интервью, поэтому, если вы когда-нибудь получите интервью со мной, у вас теперь есть несправедливое преимущество!)


вместо использования рекурсии для итерации дерева вы всегда можете использовать явное в структуре памяти. Если вы хотите близко имитировать рекурсивное поведение, вы даже можете использовать явное 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));
        }
    }
}

теперь, чтобы использовать это все, что нам нужно сделать, это передать корневой узел(ы), функцию, которая сопоставляет каждый элемент с его прямыми дочерними элементами, а затем мы можем взять максимум глубины. Из-за отложенного выполнения каждый элемент, полученный функцией траверса, не будет сохранен в памяти Max, поэтому единственными элементами, хранящимися в памяти, являются узлы, которые не были обработаны, но у которых был обработанный родитель.

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