2
\$\begingroup\$

I wrote a few simple extension methods that allow to traverse a hierachically data structure depth-first or breath-first using LINQ:

Usage:

public class Item { public Item(string name, params Item[] children) { Name = name; Items = (children ?? new Item[0]).ToList(); } public string Name { get; private set; } public List<Item> Items { get; private set; } } void Main() { var hierachicalItems = new[] { new Item("A1", new Item("B1", new Item("C1"), new Item("C2")), new Item("B2")) }; var query = hierachicalItems.DepthFirst(i => i.Items).Select(i => i.Name); Console.WriteLine("DepthFirst:\t\t" + string.Join(", ", query)); query = hierachicalItems.BreadthFirst(i => i.Items).Select(i => i.Name); Console.WriteLine("BreadthFirst:\t" + string.Join(", ", query)); } 

Output:

DepthFirst: A1, B1, C1, C2, B2

BreadthFirst: A1, B1, B2, C1, C2

The TreeEnumerable class:

public static class TreeEnumerable { // BreathFirst // ========================================================== public static IEnumerable<TNode> BreadthFirst<TNode>(this IEnumerable<TNode> nodes, Func<TNode, IEnumerable<TNode>> getSubNodes) { Queue<TNode> queue = new Queue<TNode>(256); foreach (TNode node in nodes) { queue.Enqueue(node); } return BreadthFirst(queue, getSubNodes); } private static IEnumerable<TNode> BreadthFirst<TNode>(Queue<TNode> queue, Func<TNode, IEnumerable<TNode>> getSubNodes) { TNode node; while (queue.Count > 0) { node = queue.Dequeue(); if (node == null) { yield break; } foreach (TNode subNode in getSubNodes(node)) { queue.Enqueue(subNode); } yield return node; } } public static IEnumerable<TNode> BreadthFirst<TNode>(this IEnumerable<TNode> nodes, Func<TNode, IEnumerable<TNode>> getSubNodes, Boolean repeatNodes) { HashSet<TNode> passedNodes = new HashSet<TNode>(); Queue<TNode> queue = new Queue<TNode>(256); if (repeatNodes) { return nodes.BreadthFirst(getSubNodes); } foreach (TNode node in nodes) { if (passedNodes.Contains(node)) { continue; } passedNodes.Add(node); queue.Enqueue(node); } return BreadthFirst(queue, getSubNodes, passedNodes); } private static IEnumerable<TNode> BreadthFirst<TNode>(Queue<TNode> queue, Func<TNode, IEnumerable<TNode>> getSubNodes, HashSet<TNode> passedNodes) { TNode node; while (queue.Count > 0) { node = queue.Dequeue(); if (node == null) { yield break; } foreach (TNode subNode in getSubNodes(node)) { if (passedNodes.Contains(subNode)) { continue; } passedNodes.Add(subNode); queue.Enqueue(subNode); } yield return node; } } // DepthFirst // ========================================================== public static IEnumerable<TNode> DepthFirst<TNode>(this IEnumerable<TNode> nodes, Func<TNode, IEnumerable<TNode>> getSubNodes) { foreach (TNode node in nodes) { yield return node; foreach (TNode subNode in getSubNodes(node).DepthFirst(getSubNodes)) { yield return subNode; } } } public static IEnumerable<TNode> DepthFirst<TNode>(this IEnumerable<TNode> nodes, Func<TNode, IEnumerable<TNode>> getSubNodes, Boolean repeatNodes) { if (repeatNodes) { return nodes.DepthFirst(getSubNodes); } return nodes.DepthFirst(getSubNodes, new HashSet<TNode>()); } private static IEnumerable<TNode> DepthFirst<TNode>(this IEnumerable<TNode> nodes, Func<TNode, IEnumerable<TNode>> getSubNodes, HashSet<TNode> passedNodes) { foreach (TNode node in nodes) { if (passedNodes.Contains(node)) { continue; } passedNodes.Add(node); yield return node; foreach (TNode subNode in getSubNodes(node).DepthFirst(getSubNodes, passedNodes)) { yield return subNode; } } } } 

Any improvments or usefull enhancement?

\$\endgroup\$
2
  • \$\begingroup\$ Yep, I have suggestion. It would be great timesaver if it could run not only over lists of children but also enumerate object properties and their values and subobjects... something like linqpad's dump \$\endgroup\$ Commented Jun 15, 2016 at 19:01
  • \$\begingroup\$ Ok, interesting idea. However the use case seems to be a little bit different. A "linqpad dump iterator" have to return a more general object that can not be used directly... a list of KeyValue<string, object> pairs where each value may be another list of KeyValue<string, object> pairs or something like that... \$\endgroup\$ Commented Jun 15, 2016 at 19:19

1 Answer 1

2
\$\begingroup\$
Items = (children ?? new Item[0]).ToList(); 

In C# 6.0, you can write it like this:

Items = children?.ToList() ?? new List<Item>(); 

// BreathFirst // ========================================================== 

Consider using #region instead?

Also, typo: BreathFirst.


TNode node; 

No need to declare the variable here, outside the loop.


if (node == null) { yield break; } 

Why does null node mean "stop iterating"?


HashSet<TNode> passedNodes = new HashSet<TNode>(); Queue<TNode> queue = new Queue<TNode>(256); if (repeatNodes) { return nodes.BreadthFirst(getSubNodes); } 

Why are you creating the two collections before you know whether you will need them?


if (passedNodes.Contains(node)) { continue; } passedNodes.Add(node); queue.Enqueue(node); 

You can take advantage of the fact that HashSet<T>.Add() returns a bool indicating whether the item was added and simplify this to:

if (passedNodes.Add(node)) { queue.Enqueue(node); } 

foreach (TNode subNode in getSubNodes(node).DepthFirst(getSubNodes)) { yield return subNode; } 

Keep in mind that this is going to have pretty bad time complexity (quadratic) if the depth of the tree becomes large enough. If performance matters, consider using approach similar to BreadthFirst, only with Stack instead of Queue.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the great review! I agree with all of your points. However, could you please explain the last one? I dont understand why the "depth first" approach has bad time complexity compared to the "breadth first" approach. \$\endgroup\$ Commented Jul 10, 2016 at 19:17
  • \$\begingroup\$ @JanDotNet See for example this article. \$\endgroup\$ Commented Jul 11, 2016 at 0:32

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.