TheJediCowboy
TheJediCowboy

Reputation: 9222

Build tree type list by recursively checking parent-child relationship C#

I have One class that has a list of itself so it can be represented in a tree structure.

I am pulling a flat list of these classes and want to unflatten it.

public class Group
{
     public int ID {get;set;}

     public int? ParentID {get;set;}

     public List<Group> Children {get;set;}

}

I want to be able to do the following

List<Group> flatList = GetFlatList() //I CAN ALREADY DO THIS
List<Group> tree = BuildTree(flatList);

The ParentID related to the ID property on its parent group if that wasnt obvious.

EDIT

There is some confusion as to why I am returning a list and not a single object.

I am building a UI element that has a list of items, each of why has a child. So the initial list DOES NOT have a root node. It seems all of the solutions so far do not work.

What this means is I essentially need a list of tree type structures using Group class.

Upvotes: 27

Views: 72288

Answers (5)

John Meyer
John Meyer

Reputation: 2356

I had a need to do this as well as I am storing hierarchical data in a single SQL table. I had a handful of different types/tables that do this, so building on the other answers here I created a generic version.

It's pretty slow due to the nested loops and reflection, but my data sets are small so far.

Hopefully this helps someone.

DataModels:

namespace DataModels
{
  public class Tag
  {
    public int TagId { get; set; }
    public int? TagIdParent { get; set; }
    public string TagName { get; set; }
    public IEnumerable<Tag> Items { get; set; }
  } // end class

  public class Equipment
  {
    public int EquipmentId { get; set; }
    public int? EquipmentIdParent { get; set; }
    public string EquipmentName { get; set; }
    public string? EquipmentDescription { get; set; }
    public string? EquipmentPhotoUrl { get; set; }
    public IEnumerable<Equipment> Items { get; set; }
  } // end class
} // end namespace

LogicBase (implementation):

using DataModels

namespace Logic
{
  public class LogicBase
  {
    public IEnumerable<T> BuildObjectTree<T>(List<T> data)
    {
      var id = $"{typeof(T).Name}Id";
      var idParent = $"{typeof(T).Name}IdParent";
            
      foreach (var dataItem1 in data)
      {
        var children = new List<T>();
                
        var dataItem1Id = dataItem1.GetType().GetProperty(id).GetValue(dataItem1);

        foreach (var dataItem2 in data)
        {
          var dataItem2Id = dataItem2.GetType().GetProperty(idParent).GetValue(dataItem2);

          if (dataItem1Id.Equals(dataItem2Id)) // == doesn't work here
          {
            children.Add(dataItem2);
          }
        }

        dataItem1.GetType().GetProperty("Items").SetValue(dataItem1, children);
      }

      var results = new List<T>();

      foreach (var dataItem3 in data)
      {
        var dataItem3IdParent = dataItem3.GetType().GetProperty(idParent).GetValue(dataItem3);
                
        if (dataItem3IdParent == null)
        {
          results.Add(dataItem3);
        }
      }
        
      return results;
    } // end
  } // end class
} // end namespace

LogicTag (Usage):

using Data;
using DataModels;

namespace Logic
{
  public interface ILogicTag
  {
     IEnumerable<Tag> RetrieveTags();
  }
 
  public class LogicTag(IDataTag dataTag) : LogicBase, ILogicTag
  {
    public IEnumerable<Tag> RetrieveTags()
    {
      var tags = dataTag.RetrieveTags().Result;
                
      return BuildObjectTree(tags.ToList());
    } // end
  } // end class
} // end namespace

Upvotes: 0

Zoyeb Shaikh
Zoyeb Shaikh

Reputation: 334

      public class Item {
      public readonly int Id;
      public readonly int ? ParentId;

      public Item(int id, int ? parent = null) {
        Id = id;
        ParentId = parent;
      }
      public readonly List < Item > Children = new List < Item > ();
    }

    public class BuildTree {

      public static List < Item > BuildTreeAndReturnRootNodes(List < Item > flatItems) {
        var byIdLookup = flatItems.ToLookup(i => i.Id);
        foreach(var item in flatItems) {
          if (item.ParentId != null) {
            var parent = byIdLookup[item.ParentId.Value].First();
            parent.Children.Add(item);
          }
        }
        return flatItems.Where(i => i.ParentId == null).ToList();
      }
    }
    public class TreeToFlatternBack {
      public static IEnumerable < Item > GetNodes(Item node) {
        if (node == null) {
          yield
          break;
        }

        yield
        return node;
        foreach(var n in node.Children) {
          foreach(var innerN in GetNodes(n)) {
            yield
            return innerN;
          }
        }
      }
    }

    class Program {
      static void Main(string[] args) {

        var flatItems = new List < Item > () {
          new Item(1),
            new Item(2),
            new Item(3, 1),
            new Item(4, 2),
            new Item(5, 4),
            new Item(6, 3),
            new Item(7, 5),
            new Item(8, 2),
            new Item(9, 3),
            new Item(10, 9),
        };

        Console.WriteLine();
        Console.WriteLine("--------------------Build a Tree--------------------");
        Console.WriteLine();

        var treeNodes = BuildTree.BuildTreeAndReturnRootNodes(flatItems);
        foreach(var n in treeNodes) {
          Console.WriteLine(n.Id + " number of children: " + n.Children.Count);
        }

        Console.WriteLine();
        Console.WriteLine("--------------------Tree Back to Flattern--------------------");
        Console.WriteLine();

        List < Item > BackToflatItems = new List < Item > ();
        foreach(var item in treeNodes) {
          BackToflatItems.AddRange(TreeToFlatternBack.GetNodes(item));
        }

        foreach(var n in BackToflatItems.OrderBy(x => x.Id)) {
          Console.WriteLine(n.Id + " number of children: " + n.Children.Count);
        }

      }
    }

Upvotes: 0

Dmitry Andrievsky
Dmitry Andrievsky

Reputation: 1873

I tried solutions suggested and figured out that they give us about O(n^2) complexity.

In my case (I have about 50k items to be built into tree) it was completely unacceptable.

I came to the following solution (assuming that each item has only one parent and all parents exist in the list) with complexity O(n*log(n)) [n times getById, getById has O(log(n)) complexity]:

static List<Item> BuildTreeAndReturnRootNodes(List<Item> flatItems)
{
    var byIdLookup = flatItems.ToLookup(i => i.Id);
    foreach (var item in flatItems)
    {
        if (item.ParentId != null)
        {
            var parent = byIdLookup[item.ParentId.Value].First();
            parent.Children.Add(item);
        }
    }
    return flatItems.Where(i => i.ParentId == null).ToList();
}

Full code snippet:

class Program
{
    static void Main(string[] args)
    {
        var flatItems = new List<Item>()
        {
            new Item(1),
            new Item(2),
            new Item(3, 1),
            new Item(4, 2),
            new Item(5, 4),
            new Item(6, 3),
            new Item(7, 5),
            new Item(8, 2),
            new Item(9, 3),
            new Item(10, 9),
        };
        var treeNodes = BuildTreeAndReturnRootNodes(flatItems);
        foreach (var n in treeNodes)
        {
            Console.WriteLine(n.Id + " number of children: " + n.Children.Count);
        }
    }
    // Here is the method
    static List<Item> BuildTreeAndReturnRootNodes(List<Item> flatItems)
    {
        var byIdLookup = flatItems.ToLookup(i => i.Id);
        foreach (var item in flatItems)
        {
            if (item.ParentId != null)
            {
                var parent = byIdLookup[item.ParentId.Value].First();
                parent.Children.Add(item);
            }
        }
        return flatItems.Where(i => i.ParentId == null).ToList();
    }
    class Item
    {
        public readonly int Id;
        public readonly int? ParentId;

        public Item(int id, int? parent = null)
        {
            Id = id;
            ParentId = parent;
        }
        public readonly List<Item> Children = new List<Item>();
    }
}

Upvotes: 11

JLRishe
JLRishe

Reputation: 101672

Here's how you can do this in one line:

static void BuildTree(List<Group> items)
{
    items.ForEach(i => i.Children = items.Where(ch => ch.ParentID == i.ID).ToList());
}

You can just call it like this:

BuildTree(flatList);

If at the end you want to get the nodes whose parent is null (i.e. the top-level nodes), you can simply do this:

static List<Group> BuildTree(List<Group> items)
{
    items.ForEach(i => i.Children = items.Where(ch => ch.ParentID == i.ID).ToList());
    return items.Where(i => i.ParentID == null).ToList();
}

And if you want to make it an extension method, you can just add this in the method signature:

static List<Group> BuildTree(this List<Group> items)

Then you can call it like this:

var roots = flatList.BuildTree();

Upvotes: 29

MarcinJuraszek
MarcinJuraszek

Reputation: 125620

I have no idea why you want your BuildTree method return List<Group> - tree needs to have root node, so you should expect it to return single Group element, not a list.

I would create an extension method on IEnumerable<Group>:

public static class GroupEnumerable
{
    public static IList<Group> BuildTree(this IEnumerable<Group> source)
    {
        var groups = source.GroupBy(i => i.ParentID);

        var roots = groups.FirstOrDefault(g => g.Key.HasValue == false).ToList();

        if (roots.Count > 0)
        {
            var dict = groups.Where(g => g.Key.HasValue).ToDictionary(g => g.Key.Value, g => g.ToList());
            for (int i = 0; i < roots.Count; i++)
                AddChildren(roots[i], dict);
        }

        return roots;
    }

    private static void AddChildren(Group node, IDictionary<int, List<Group>> source)
    {
        if (source.ContainsKey(node.ID))
        {
            node.Children = source[node.ID];
            for (int i = 0; i < node.Children.Count; i++)
                AddChildren(node.Children[i], source);
        }
        else
        {
            node.Children = new List<Group>();
        }
    }
}

Usage

var flatList = new List<Group>() {
    new Group() { ID = 1, ParentID = null },    // root node
    new Group() { ID = 2, ParentID = 1 },
    new Group() { ID = 3, ParentID = 1 },
    new Group() { ID = 4, ParentID = 3 },
    new Group() { ID = 5, ParentID = 4 },
    new Group() { ID = 6, ParentID = 4 }
};


var tree = flatList.BuildTree();

Upvotes: 51

Related Questions