Как определить глубину на 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;
}
}
вместо того, чтобы пытаться решить вашу проблему для деревьев выражений конкретно, позвольте мне описать вам некоторые общие методы борьбы с плохо себя ведут деревья.
вы можете начать с чтения моей серии статей о решении проблемы, которую вы ставите:как определить глубину дерева без использования рекурсии?
эти статьи были написаны, когда я работал над 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);
}