C #: избегать бесконечной рекурсии при обходе графа объекта - PullRequest
22 голосов
/ 05 февраля 2010

У меня есть граф объектов, в котором каждый дочерний объект содержит свойство, которое ссылается на своего родителя. Есть ли хорошие стратегии для игнорирования родительских ссылок, чтобы избежать бесконечной рекурсии? Я рассмотрел возможность добавления специального атрибута [Parent] к этим свойствам или использования специального соглашения об именах, но, возможно, есть лучший способ.

Ответы [ 6 ]

32 голосов
/ 05 февраля 2010

Какое совпадение; это тема моего блога в этот понедельник. Смотрите это для более подробной информации. А пока вот код, который поможет вам понять, как это сделать:

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 превышает несколько сотен узлов. </p>

Обратите внимание, что пиковое использование памяти этим алгоритмом, конечно, O (n + d) = O (n).

Так, например:

foreach(Node node in Traversal(myGraph.Root, n => n.Children))
  Console.WriteLine(node.Name);

Имеет смысл?

31 голосов
/ 05 февраля 2010

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

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

Для простоты, если вы знаете, что родительская ссылка будет иметь определенное имя, вы можете просто не зацикливаться на этом свойстве:)

3 голосов
/ 05 февраля 2010

Если вы выполняете обход графика, у вас может быть флаг «посещен» на каждом узле. Это гарантирует, что вы не вернетесь к узлу и, возможно, застряли в бесконечном цикле. Я считаю, что это стандартный способ выполнения обхода графа.

2 голосов
/ 05 февраля 2010

Это распространенная проблема, но наилучший подход зависит от сценария. Дополнительная проблема заключается в том, что во многих случаях это не проблема посещение одного и того же объекта дважды - это не подразумевает рекурсию - например, рассмотрим дерево:

A
=> B
   => C
=> D
   => C

Это может быть допустимым (подумайте XmlSerializer, который просто выписал бы экземпляр C дважды), поэтому часто необходимо помещать / выталкивать объекты в стек, чтобы проверить истинную рекурсию. В прошлый раз, когда я реализовал «посетителя», я сохранил счетчик «глубины» и включил проверку стека только за определенным порогом - это означает, что большинство деревьев просто заканчивают тем, что выполняли некоторые ++ / --, но не дороже. Вы можете увидеть подход, который я выбрал здесь .

2 голосов
/ 05 февраля 2010

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

0 голосов
/ 04 октября 2016

Я опубликовал пост, подробно объясняющий примеры кода, как выполнять обход объекта с помощью рекурсивного отражения, а также обнаруживать и избегать рекурсивных ссылок для предотвращения исключения стека из-за потока: https://doguarslan.wordpress.com/2016/10/03/object-graph-traversal-by-recursive-reflection/

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

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