I have list of objects that have properties Id and NextObjectId. NextObjectId is referring to Id of another object telling to that object it is supposed to be after the object it's referring to. Object Ids are random and not in any particular order.
Can you show the most efficient way how to organize list of objects from random order to ascending according to NextObjectId?
If I understand you in the correct way, you have the following relation:
A.Id = B.NextObjectId && B.Id != A.Id.
So you need to sort by Id with Linq.
IEnumerable<AClass> orderedObjects = objects.OrderBy(e => e.Id);
Or if you want to sort by NextObjectId
IEnumerable<AClass> orderedObjects = objects.OrderBy(e => e.NextObjectId);
I made this algorithm. It should do what you wanted to:
class Program
{
private static Random random = new Random();
static void Main(string[] args)
{
//GENERATE A RANDOM LIST FOR THE EXAMPLE
List<Item> items = new List<Item>();
int? lastID = null;
for(int i = 0; i < 100; i++)
{
int id = random.Next();
items.Add(new Item() { ID = id, NextItemID = lastID });
lastID = id;
}
//GENERATE A RANDOM LIST FOR THE EXAMPLE
items = items.OrderBy(x => random.Next()).ToList(); //SHUFFLE THE LIST FOR THE EXAMPLE
items.OrderByNextItemID(); //SORT THE LIST BY NextItemID
}
}
public static class Extensions
{
public static void OrderByNextItemID(this List<Item> items)
{
List<Item> results = new List<Item>();
Item item = items.Where(x => x.NextItemID == null).First();
results.Add(item);
items.Remove(item);
int length = items.Count;
for(int i = 0; i < length; i++)
{
item = items.Where(x => x.NextItemID == results.Last().ID).FirstOrDefault();
if(item != null)
{
results.Add(item);
items.Remove(item);
}
}
results.Reverse();
items.AddRange(results);
}
}
public class Item
{
public int ID { get; set; }
public int? NextItemID { get; set; }
}
Here it is.
If I understood you right, you have a class like this:
public class MyObject
{
public int Id { get; set; }
public int? NextObjectId { get; set; }
}
and List<MyObject> like this one:
List<MyObject> myObjects = new List<MyObject>
{
new MyObject {Id = 5, NextObjectId = 4},//4
new MyObject {Id = 4, NextObjectId = 1},//5
new MyObject {Id = 2, NextObjectId = 87},//1
new MyObject {Id = 70, NextObjectId = 5},//3
new MyObject {Id = 1, NextObjectId = null},//6
new MyObject {Id = 87, NextObjectId = 70}//2
};
Since the NextObjectId refers to the next object's Id, then there must be a NextObjectId with null (or zero), that is the last element, and the item before it is the before last element which its NextObjectId's value equals to the last element's Id. We can build the method OrderChain on this algorithm:
private static IEnumerable<MyObject> OrderChain(List<MyObject> myObjects)
{
var starter = myObjects.FirstOrDefault(x => x.NextObjectId == null);
yield return starter;
var count = myObjects.Count;
while (count > 1)
{
yield return GetCurrent(myObjects, ref starter);
count--;
}
}
private static MyObject GetCurrent(List<MyObject> lst, ref MyObject current)
{
var id = current.Id;
current = lst.SingleOrDefault(x => x.NextObjectId == id);
return current;
}
Then you can sort it like this:
var myOrderedObjects = OrderChain(myObjects).Reverse().ToList();
this gives me this list:
{ Id = 2, NextObjectId = 87 },//1
{ Id = 87, NextObjectId = 70 },//2
{ Id = 70, NextObjectId = 5 },//3
{ Id = 5, NextObjectId = 4 },//4
{ Id = 4, NextObjectId = 1 },//5
{ Id = 1, NextObjectId = null }//6
I'd simply create a dictionary from its object ID to the object. From there, assuming you have a starting object and some way of finishing (e.g. a NextObjectId of null) you can just build the list up. It's not particularly LINQ-based, but it is simple:
YourType item = ...; // Whatever should come first.
var map = objects.ToDictionary(obj => obj.ObjectId);
var list = new List<YourType>();
list.Add(item);
while (item.NextObjectId != null)
{
// TODO: Handle missing objects? (Use TryGetValue instead of the indexer)
item = map[item.NextObjectId];
list.Add(item);
}
Related
I have to make method called CalculateBill with given parameters, but I cannot define how to connect those two parameters inside of code.
public class Bill {
public int CalculateBill (List<Item> itemList, params int[] items) {
// Need to write code here
//itemList : available items in the shop
// int[] items : array of ItemId's purchased by customer
// A customer can buy same item many times , so itemId can be repeated in items array
// return sum of al items prices
}
}
The item class is like
public class Item {
public int ItemId {get; set;}
public int ItemPrice { get; set; }
}
The Program Class is like
List<Item> itemList = new List<Item> {
new Item() {ItemId = 100 , ItemPrice = 50},
new Item() {ItemId = 101 , ItemPrice = 60},
new Item() {ItemId = 102 , ItemPrice = 20},
};
Bill bObj = new Bill();
int a = bObj.CalculateBill(itemList, 101, 102, 101);
int b = bObj.CalculateBill(itemList);
It sounds like you need help implementing the CalculateBill function using your existing architecture. I would implement it like below.
I renamed your 'itemList' parameter to 'itemsInShop' and I also renamed your 'items' parameter to 'itemsPurchasedIDs'. Always try to make your variable names as descriptive as possible to make it easy to understand and read.
public int CalculateBill(List<Item> itemsInShop, params int[] itemsPurchasedIDs)
{
int totalSpent = 0;
for (int i = 0; i < itemsPurchasedIDs.Length; i++)
{
int itemId = itemsPurchasedIDs[i];
for (int j = 0; j < itemsInShop.Count; j++)
{
var shopItem = itemsInShop[j];
if (shopItem.ItemId == itemId)
{
totalSpent += shopItem.ItemPrice;
break;
}
}
}
return totalSpent;
}
You are almost there. Create a class for the shop that contains the items available so you don't need to pass it as an argument.
Also, since you want to retrieve Items with id values, you need a Dictionary<int,Item> which would allow you to do this quickly.
Here is the Shop class with a dictionary
public class Shop
{
private readonly Dictionary<int, Item> _items;
public Shop()
{
_items = new Dictionary<int, Item>();
}
public IReadOnlyList<Item> Items { get => _items.Values.ToList(); }
public void Add(Item item) => _items.Add(item.ItemId, item);
public Item[] GetItems(params int[] idList)
{
List<Item> list = new List<Item>();
foreach (var id in idList)
{
list.Add(_items[id]);
}
return list.ToArray();
}
public decimal CalculateBill(params int[] idList)
{
var items = GetItems(idList);
decimal sum = 0m;
foreach (var item in items)
{
sum += item.ItemPrice;
}
return sum;
}
}
Otherwise, you would need to lookup the items one by one. Here is a solution that uses theList.Find() method to match the Item from the id.
public class Shop
{
private readonly List<Item> _items;
public Shop()
{
_items = new List<Item>();
}
public IReadOnlyList<Item> Items { get => _items.ToList(); }
public void Add(Item item) => _items.Add(item);
public Item[] GetItems(params int[] idList)
{
List<Item> list = new List<Item>();
foreach (var id in idList)
{
list.Add(_items.Find((item)=>item.ItemId==id));
}
return list.ToArray();
}
public decimal CalculateBill(params int[] idList)
{
var items = GetItems(idList);
decimal sum = 0m;
foreach (var item in items)
{
sum += item.ItemPrice;
}
return sum;
}
}
Here are three alternatives for you to try:
public int CalculateBill(List<Item> itemsInShop, params int[] itemsPurchasedIDs) =>
itemsPurchasedIDs
.Join(itemsInShop, id => id, item => item.ItemId, (_, item) => item.ItemPrice)
.Sum();
public int CalculateBill2(List<Item> itemsInShop, params int[] itemsPurchasedIDs) =>
(
from id in itemsPurchasedIDs
join item in itemsInShop on id equals item.ItemId
select item.ItemPrice
).Sum();
public int CalculateBill3(List<Item> itemsInShop, params int[] itemsPurchasedIDs)
{
var map = itemsInShop.ToDictionary(x => x.ItemId, x => x.ItemPrice);
return itemsPurchasedIDs.Sum(x => map[x]);
}
I am wondering if/how I can do the following thing using LINQ: I have a list of objects with some properties and another list of different distinct values corresponding to a certain property.
Example:
A = [{id=1, property=1}, {id=2, property=1}, {id=3, property=2}]
B = [1, 2]
Is there a way of achieving the following thing (obtaining the counts list) using only LINQ?
var counts = new List<int>();
foreach (var b in B)
{
counts.Add(A.Where(a => a.property == b).Count();
}
Sample code:
public class MyObject
{
public MyObject(int id, int prop)
{
Id = id;
Property = prop;
}
public int Id { get; set; }
public int Property { get; set; }
public void test()
{
var A = new List<MyObject>
{
new MyObject(1, 1), new MyObject(2, 1), new MyObject(3, 2)
};
var B = new List<int>{1, 2};
// LINQ magic
// 2 objects with property 1
// 1 object with property 2
}
}
Yes, use select operators to only select the specific properties you want to compare, and then use intersect and count to get the count. Example:
var listOfObjects = new List<PocoClass>()
{
new PocoClass(){Id=1,Property=3},
new PocoClass(){Id=2,Property=2}
};
var intArray = new int[] { 1, 2, 3 };
var count = listOfObjects.Select(o => o.Property).Intersect(intArray).Count();
Sure, you can just loop through the values and, for each one, get the count of items that have Property == value.
In the sample below, I'm selecting an anonymous type that contains the Value and the Count of each item that has Property == value:
public class Data
{
public int Id { get; set; }
public int Property { get; set; }
}
public class Program
{
private static void Main()
{
var allData = new List<Data>
{
new Data {Id = 1, Property = 1},
new Data {Id = 2, Property = 1},
new Data {Id = 3, Property = 2},
};
var values = new[] {1, 2};
var results = values.Select(value =>
new {Value = value, Count = allData.Count(item => item.Property == value)});
foreach (var result in results)
{
Console.WriteLine($"{result.Count} objects with Property {result.Value}");
}
}
}
Output
You can use Count method with a predicate:
var A = new[] {new {id = 1, property = 1}, new {id = 2, property = 1}, new {id = 3, property = 2}};
var B = new[] {1, 2};
var count = B.Count(b => A.Any(a => a.property == b));
Code above will check every member in B and if at least one member in A have a property with that value it will be counted
convert B to List and run ForEach on it
B.OfType<int>().ToList().ForEach(m=>{
counts.Add(A.Where(a => a.property == m).Count();
})
This is the gist of what you need. I'm typing this without a c# compiler, so hopefully this doesn't have errors.
var results =
from a in A
join b in B on a.property equals b
group a by a.property into g
select new { Property = g.Key, Count = g.Count() }
Let's say, we have an object definition like this,
class MyObject {
int id;
string name;
}
and we have a list of MyObjects.
List<MyObject> objectList
Now, I need to partition this objectList into 2 sub-lists based on which objects have a non-null name field.
So, after the operation, I need to have 2 lists, objectsWithName where name field is non-null and objectsWithoutName where name field is null (this is the immediate criteria, but I'm looking more towards partitioning into 2 groups using a predicate).
What is the simplest way to achieve this? Can I do this in a single operation? Using LINQ is permitted.
Two LINQ statements would do:
var nameNotNull = objectList.Where(o => !string.IsNullOrEmpty(o.Name));
var nameNull = objectList.Where(o => string.IsNullOrEmpty(o.Name));
Of course, you could use GroupBy, or a more efficient foreach statement.
To show the foreach option:
List<MyObject> nameNotNull = new List<MyObject>();
List<MyObject> nameNull = new List<MyObject>();
foreach (MyObject o in objectList)
{
if (!string.IsNullOrEmpty(o.Name))
{
nameNotNull.Add(o);
}
else
{
nameNull.Add(o);
}
}
public class MyObject
{
public int Id { get; set; }
public string Name { get; set; }
}
public class MyObjectLists
{
private readonly List<MyObject> _objects;
public List<MyObject> NullNameObjects
{
get
{
return _objects.Where(x => x.Name == null).ToList();
}
}
public List<MyObject> NonNullNameObjects
{
get
{
return _objects.Where(x => x.Name != null).ToList();
}
}
public MyObjectLists(List<MyObject> objects)
{
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
}
}
Using the code:
var list = new List<MyObject>
{
new MyObject
{
Id = 1,
Name = "John"
},
new MyObject
{
Id = 2
},
new MyObject
{
Id = 3,
Name = "Mary"
},
new MyObject
{
Id = 4
}
};
var objects = new MyObjectLists(list);
foreach (MyObject myObject in objects.NonNullNameObjects)
{
Console.WriteLine($"Object with Id {myObject.Id} has a non-null name");
}
foreach (MyObject myObject in objects.NullNameObjects)
{
Console.WriteLine($"Object with Id {myObject.Id} has a null name");
}
I think you're looking for something like this example:
class MyObject
{
int id;
string name;
}
var objectList = new List<MyObject>();
objectList.Add(new MyObject { name = "item 1" });
objectList.Add(new MyObject { name = string.Empty });
objectList.Add(new MyObject { name = "item 3" });
var objectsWithName = objectList.Where(x => !string.IsNullOrEmpty(x.name));
var objectsWithoutName = objectList.Except(objectsWithName);
I've created 2 lists that the first list contains the MyObject items with the name property is NOT null, otherwise to the second's.
I have input that could look like this:
A 1 2 C,D
A 2 3 C,E
B 4 5 F
A 6 7
A 7 8 D
A 9 10 E
I store this in my model class:
public class Item {
public String Name {get;set;}
public int Start {get;set;}
public int End {get;set;}
public List<string> Orders {get;set;}
}
I tried to use Linq to merge all subsequent items if the items have the same name and generate a new item that has the start value of the first item in the group, the end value of the last item in the group and a union of all order lists. It should then look like this:
A 1 3 C,D,E
B 4 5 F
A 6 10 D, E
I tried the following Linq statement, however, it groups all As and Bs together, independent of whether there are any other items in between. What do I need to change? The union of the order list is also missing.
var groups = items.GroupBy(i => i.Name).ToList();
foreach (var group in groups)
{
result.Add(new Item {
Start = group.First().Start,
End = group.Last().End,
Name = group.First().Name });
}
Use a classic loop for this:
var List<List<Item>> groups = new List<List<Item>>()
var currentGroup = new List<Item> { items.First() };
int i = 0;
foreach(var item in items.Skip(1))
{
if(currentGroup.First().Name != item.Name)
{
groups.Add(currentGroup);
currentGroup = new List<Item> { item };
}
else
{
currentGroup.Add(item);
if(i == items.Count - 2)
groups.Add(currentGroup);
}
i++;
}
Now you can continue with your code by iterating the groups-list.
Maybe not the best or fastest way but I got bored:
int groupID = -1;
var result = items.Select((item, index) =>
{
if (index == 0 || items[index - 1].Name != item.Name)
++groupID;
return new { group = groupID, item = item };
}).GroupBy(item => item.group).Select(group =>
{
Item item = new Item();
var first = group.First().item;
var last = group.Last().item;
item.Name = first.Name;
item.Start = first.Start;
item.End = last.End;
item.Orders = group.SelectMany(g => g.item.Orders).Distinct().ToList();
return item;
});
The variable items should be your input collection like a List<Item>. The result will be stored in result. This is an IEnumerable<Item> but you may add .ToList() or .ToArray() as you like to convert it to List<Item> or Item[].
The result will contain new created items. I did this on purpose to not mess up the input data.
The trick here is to use a local variable as a group id. It is increased if it is the first item or the last item had a different name. Then we group by the group id and the rest of the code will just create the item. The SelectMany method will join all Orders-values from the entire group and Distinct will then remove all duplicates.
This is not done by Linq. I just played a bit using simpler methods. But it gives same result which you wanted.
using System;
using System.Collections.Generic;
public class Item
{
public static List<Item> Database;
static Item()
{
Database = new List<Item>();
}
public Item(string name, int start, int end, params string[] orders)
{
Name = name;
Start = start;
End = end;
Orders = new List<string>();
foreach (string s in orders)
Orders.Add(s);
//putting newly created Item to database
Database.Add(this);
}
//overload for creating tmp Items in GroupThem(), could be done using optinional parameter
public Item(bool AddToDatabase, string name, int start, int end, params string[] orders)
{
Name = name;
Start = start;
End = end;
Orders = new List<string>();
foreach (string s in orders)
Orders.Add(s);
if (AddToDatabase) Database.Add(this);
}
public string Name { get; set; }
public int Start { get; set; }
public int End { get; set; }
public List<string> Orders { get; set; }
public List<Item> GroupedItems()
{
List<Item> groupedItems = new List<Item>();
Item previous = Database[0];
Stack<Item> sameItems = new Stack<Item>();
foreach (Item item in Database)
{
if (previous.Name == item.Name)
{
sameItems.Push(item);
}
else
{
groupedItems.Add(GroupThem(sameItems));
previous = item;
sameItems.Push(item);
}
}
groupedItems.Add(GroupThem(sameItems));
return groupedItems;
}
private Item GroupThem(Stack<Item> sameItems)
{
string newName = "";
int newEnd = 0;
int newStart = int.MaxValue;
List<string> newOrders = new List<string>();
Item tmp = null;
while (sameItems.Count > 0)
{
tmp = sameItems.Pop();
if (tmp.Start < newStart)
newStart = tmp.Start;
if (tmp.End > newEnd)
newEnd = tmp.End;
foreach (string s in tmp.Orders)
if (!newOrders.Contains(s))
newOrders.Add(s);
newName = tmp.Name;
}
return new Item(false, newName, newStart, newEnd, newOrders.ToArray());
}
public override string ToString()
{
string tmp = "";
foreach (string s in Orders)
tmp += " " + s;
return "Name = " + Name + ", Start = " + Start + ", End = " + End +", Orders = "+ tmp;
}
}
class Program
{
static void Main(string[] args)
{
Item item1 = new Item("A", 1, 2, "C", "D");
Item item2 = new Item("A", 2, 3, "C", "E");
Item item3 = new Item("B", 4, 5, "F");
Item item4 = new Item("A", 6, 7);
Item item5 = new Item("A", 7, 8, "D");
Item item6 = new Item("A", 9, 10, "E");
foreach (Item item in item1.GroupedItems())
{
Console.WriteLine(item);
}
}
}
A bit late, I know, but I think the following solution will still help someone.
It includes the original Item class, enhanced with:
A ToString method to simplify inspection.
A CreateSamples method to generate the sample items.
A bonus nested class ComparerByStartAndEnd to sort items based on Start and End properties.
The solution resides in the EXTENSIONS.GroupWhenChanging method and the Item.FromGroup method.
The TEST class provides code to verify everything works as expected.
The actual grouping logic (EXTENSIONS.GroupWhenChanging) simply implements an enumerator that does not invoke Linq methods and allocates only a List object for each group, thus saving both in performance and memory resources.
The method is generic and accepts a comparison predicate, so it is not restricted to the sample Item class.
The creation of the result items, representing the groups with merged orders, is kept in the separate method Item.FromGroup. It uses some Linq to ease the task.
The TEST.Test method does the following:
Creates the list of samples.
Ensures the samples are ordered based on Start and End.
Enumerates the groups (by means of GroupWhenChanging) and creates the corresponing items (through Item.FromGroup).
The Item class:
public static class MODEL
{
public class Item
{
public String Name { get; set; }
public int Start { get; set; }
public int End { get; set; }
public List<string> Orders { get; set; }
/// <inheritdoc/>
public override string ToString()
{
return string.Format("{0} {1} .. {2} {3}", this.Name, this.Start, this.End, string.Join(",", this.Orders));
}
public static Item? FromGroup(IEnumerable<Item> group)
{
var array = group as Item[] ?? group.ToArray();
if (array.Length > 0)
{
var newName = array[0].Name;
var newStart = array.Min(item => item.Start);
var newEnd = array.Max(item => item.End);
var newOrders = array.SelectMany(item => item.Orders).Distinct().OrderBy(orderID => orderID).ToList();
var newItem = new Item()
{
Name = newName,
Start = newStart,
End = newEnd,
Orders = newOrders
};
return newItem;
}
return null;
}
public static IEnumerable<Item> CreateSamples()
{
yield return new Item() { Name = "A", Start = 1, End = 2, Orders = new List<string>() { "C", "D" } };
yield return new Item() { Name = "A", Start = 2, End = 3, Orders = new List<string>() { "C", "E" } };
yield return new Item() { Name = "B", Start = 4, End = 5, Orders = new List<string>() { "F" } };
yield return new Item() { Name = "A", Start = 6, End = 7, Orders = new List<string>() };
yield return new Item() { Name = "A", Start = 7, End = 8, Orders = new List<string>() { "D" } };
yield return new Item() { Name = "A", Start = 9, End = 10, Orders = new List<string>() { "E" } };
}
public sealed class ComparerByStartAndEnd : Comparer<Item>
{
/// <inheritdoc/>
public override int Compare(Item x, Item y)
{
if (x == y)
return 0;
return x.End.CompareTo(y.Start);
}
}
}
}
The EXTENSIONS class:
public static class EXTENSIONS
{
public static IEnumerable<IEnumerable<T>> GroupWhenChanging<T>(this IEnumerable<T> items, Func<T, T, bool> predicate)
{
List<T> group = null;
foreach (var item in items)
{
if (group is null)
group = new List<T>() { item };
else if (predicate(group[group.Count - 1], item))
group.Add(item);
else
{
yield return group;
group = new List<T>() { item };
}
}
if (group is not null)
yield return group;
}
}
The TEST class:
public static class TEST
{
public static void Test()
{
var items = MODEL.Item.CreateSamples().ToList();
items.Sort(new MODEL.Item.ComparerByStartAndEnd());
var groups = items
.GroupWhenChanging((prev, next) => prev.Name == next.Name)
.Select(MODEL.Item.FromGroup)
.ToList();
}
}
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.
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();
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();
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>();
}
}
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);
}
}
}