Ваш подход и ответ Гэри являются совершенно разумными способами переформулировать абстрактное понятие рекурсивного обхода графа объектов. Тем не менее, я вижу четыре потенциальных вопроса. Не зная вашего точного сценария, возможно, это не проблема для вас, или, возможно, вы должны рассмотреть их:
Во-первых, предположим, что пройденный вами граф имеет чрезвычайно длинные пути. Вы неявно выполняете обход сначала на глубину, и ваш метод не может быть легко рекурсивным, даже на архитектурах, которые поддерживают хвостовую рекурсию, поэтому вы рискуете исчерпать стек вызовов.
Во-вторых, вы предполагаете, что граф ацикличен; если график циклический, то вам, безусловно, не хватит места в стеке.
В-третьих, я не понимаю, почему алгоритм обхода возвращает объект. Почему этот метод не является недействительным? Или, если вы используете возврат в качестве накопителя для накопления значения, вычисленного путем обхода, то почему рекурсивный шаг не делает что-то с возвращенной сущностью?
В-четвертых, у вас, похоже, плохое разделение интересов. Вызывающий отвечает за определение (1) корня графа, (2) что делать с каждым узлом. Но вызываемый абонент отвечает за (3) выяснение, на какие объекты возиться. Это кажется странным для меня. Звонящий обеспечивает отправную точку; разве звонящий не должен иметь права голоса?
Обычно я решаю эту проблему следующим образом:
- Использовать явный стек, выделенный в куче, вместо использования стека вызовов для потока управления
- Отслеживать узлы, которые я посетил ранее, и не посещать их повторно
- Пусть вызывающий абонент определит, когда у объекта есть перемещаемые дети. Если вызывающий объект желает, чтобы обход «снизу вверх» в базовом случае, он может вернуть пустой набор дочерних элементов.
Если бы я хотел аккумулятор, я мог бы реализовать что-то вроде этого наброска:
static R DepthFirstGraphAccumulate<T, R>(
T root,
Func<T, IEnumerable<T>> children,
Func<T, R, R> accumulate)
{
var accumulator = default(R);
var visited = new HashSet<T>();
var stack = new Stack<T>;
stack.Push(root);
while(stack.Count != 0)
{
var current = stack.Pop();
if (!visited.Contains(current))
{
visited.Add(current);
foreach(var child in children(current))
stack.Push(child);
accumulator = accumulate(current, accumulator);
}
}
return accumulator;
}
Так, например, если бы у меня был график целых чисел, и я хотел бы суммировать узлы, достижимые из определенного начального узла, я бы сказал:
int total = DepthFirstGraphAccumulate<Node, int>(
startNode,
node=>node.NeighbouringNodes,
(node, sum)=>node.Value + sum);
Однако я бы соблазнился пойти еще дальше на пути «давайте разделим наши проблемы» и скажу: эй, давайте просто напишем абстрактный traverser:
static IEnumerable<T> DepthFirstGraphTraversal<T>(
T root,
Func<T, IEnumerable<T>> children)
{
var visited = new HashSet<T>();
var stack = new Stack<T>;
stack.Push(root);
while(stack.Count != 0)
{
var current = stack.Pop();
if (!visited.Contains(current))
{
visited.Add(current);
foreach(var child in children(current))
stack.Push(child);
yield return current;
}
}
}
и теперь, если я хочу выполнить какое-либо действие с каждым узлом графа, я просто говорю:
foreach(var node in DepthFirstGraphTraversal<Node>(startNode, n=>n.NeighbouringNodes))
DoSomething(node);
Если бы я хотел выразить понятие «сделать что-то для каждого узла, соответствующего условию», я бы написал:
var nodes = from node in DepthFirstGraphTraversal<Node>(startNode, n=>n.NeighbouringNodes)
where condition(node)
select node;
foreach(var matchingNode in nodes) DoSomething(matchingNode);