1

I have an extension that explodes List into groups, which makes displaying content easy. Here is the extension code

public static List<List<T>> GroupItems<T>(this List<T> items, int totalGroups)
{
    List<List<T>> groups = null;
    if (items != null)
    {
        groups = new List<List<T>>();
        int itemsPerGroup = (int)Math.Ceiling((double)items.Count / totalGroups);
        if (itemsPerGroup > items.Count)
        {
            List<T> group = new List<T>(items);
            groups.Add(group);
        }
        else
        {

            for (int i = 0; i < totalGroups; i++)
            {
                List<T> group = items.Skip(i * itemsPerGroup).Take(itemsPerGroup).ToList();
                groups.Add(group);
            }
        }
    }

    return groups;
}

Here is the scenario:

I have a List and it has 13 items (items may vary).

In this particular instance, since i am exploding the list into 4 groups, i am getting

  • group 1: 4 items
  • group 2: 4 items

  • group 3: 4 items

  • group 4: 1 item

The 4 items per group is coming from

int itemsPerGroup = (int)Math.Ceiling((double)items.Count / totalGroups);

Now this is wrong, ideally i should have

  • group 1: 4 items
  • group 2: 3 items
  • group 3: 3 items
  • group 4: 3 items

Same way, when i try to explode this into 6 groups, i get

  • group 1: 3 items
  • group 2: 3 items
  • group 3: 3 items
  • group 4: 3 items
  • group 5: 1 item
  • group 6: 0 items

you can see this in the attached pic here as well

enter image description here

Ideally this should have

  • group 1: 3 items
  • group 2: 2 items
  • group 3: 2 items
  • group 4: 2 items
  • group 5: 2 items
  • group 6: 2 items

What am i missing here? How can i fix this issue?

Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
learning...
  • 3,104
  • 10
  • 58
  • 96
  • The problem is that you fill each bucket to capacity before moving to the next bucket. It seems like you want to evenly distribute across all buckets. – Josh C. Sep 24 '15 at 18:33
  • `int itemsPerGroup = (int)Math.Ceiling((double)items.Count / totalGroups);` Basically my approach here is wrong.. Any suggestions how to make this better? – learning... Sep 24 '15 at 18:37
  • check this answer with comments http://stackoverflow.com/a/420534/1506454 for http://stackoverflow.com/questions/419019/split-list-into-sublists-with-linq – ASh Sep 24 '15 at 18:45
  • This is little interesting algorithmic problem that have simple solution, I would suggest you to do it yourself (even if it will take more time). As your name suggest you are learning programming, asking for help isn't bad but in this case I suggest you to try a bit harder and solve it yourself... – csharpfolk Sep 24 '15 at 18:51
  • Does the order of the items in the groups matter? That is does the first x items have to be in group one, or can you put the first x items into different groups? – juharr Sep 24 '15 at 18:52
  • We need to keep the order intact – learning... Sep 24 '15 at 19:05

7 Answers7

1

Because you want the average in each group and the earliest rounded up for as long as it's needed I can suggest this algorithm.

public static List<List<T>> GroupItems<T>(this List<T> items, int totalGroups)
    {
        if (items == null || totalGroups == 0) return null;
        var ret = new List<List<T>>();

        var avg = items.Count / totalGroups; //int  rounds towards 0
        var extras = items.Count - (avg * totalGroups);
        for (var i = 0; i < totalGroups; ++i)
            ret.Add(items.Skip((avg * i) + (i < extras ? i : extras)).Take(avg + (i >= extras ? 0 : 1)).ToList());
        return ret;
    }

The advantage is, it's short and conside and scales with O(n^2) just like any two-dimensional algorithm should. The design ideas are: Find the average, find N=Missing elements after average and let the first N elements get one extra element to fill up the gap.

Joachim Olsson
  • 316
  • 1
  • 8
  • Out of all the solution here, this worked as far as keeping items intact. Now, even though i am exploding into 4 groups, i am getting a fifth group which has 0 items. – learning... Sep 24 '15 at 19:15
  • I am going to pick your solution as an answer here . I have removed the empty item being inserted here. I have tested it with other scenarios and all have passed. – learning... Sep 24 '15 at 19:27
  • Sorry :) The empty group comes from the <= totalgroups. That's a misfortune I forgot. Switching that to < totalgroups fixes the issue. I have edited the answer. – Joachim Olsson Sep 24 '15 at 19:28
  • Thanks for updating the code, i got lazy with my testing. – learning... Sep 24 '15 at 19:33
0

i would calculate items per group each time.

 int itemsPerGroupCount = 0;
 for (int i = 0; i < totalGroups; i++)
        {
            int itemsPerGroup = (int)Math.Ceiling(
                 ((double)items.Count - itemsPerGroupCount) / (totalGroups - i));

            itemsPerGroupCount += itemsPerGroup;

            List<T> group = items.Skip(itemsPerGroupCount).Take(itemsPerGroup).ToList();
            groups.Add(group);
        }
g williams
  • 184
  • 1
  • 7
  • I have tested your suggestion, it i giving me 4 group with 4+3+3+3 items. however the 1st group 4th item and 2nd group 1st item are the same. – learning... Sep 24 '15 at 19:09
0

I would calculate the items per group as such (floor instead of ceiling):

int itemsPerGroup = (int)Math.Floor((double)items.Count / totalGroups);

Then I would figure out the number of remaining items:

int remainingItems = items.Count % totalGroups

And then in your loop:

int itemsToSkip = 0;
for (int i = 0; i < totalGroups; i++)
{
    int itemsPerGroupTemp = itemsPerGroup;
    if (remainingItems != 0) {
        itemsPerGroupTemp++;
        remainingitems--;
    }
    List<T> group = items.Skip(itemsToSkip ).Take(itemsPerGroupTemp).ToList();
    groups.Add(group);
    itemsToSkip += itemsPerGroupTemp;
}
Olivier De Meulder
  • 2,493
  • 3
  • 25
  • 30
0

How about going about it the other way, say you have 13 items you want 6 groups so take (as int) (13/6) and floor it, this will give you 2. Now with the same number of groups (6) multiply that by group size (2) gives you 12. you had 13 items so you have one left, add that to the first group. Should give you a nizer set of groups.

schultz
  • 316
  • 2
  • 14
0

If you adjust your for loop to something like this

 for (int i = 0; i < totalGroups; i++)
                {
                    List<T> group = items.Skip(i * itemsPerGroup).Take(itemsPerGroup).ToList();
                    groups.Add(group);
                    if(i==0)
                        itemsPerGroup = (int)Math.Ceiling((double)(items.Count - itemsPerGroup  )/ (totalGroups - 1) );
                }
Subhash Dike
  • 1,836
  • 1
  • 22
  • 37
0

If the order of the items across the groups is not relevant then you can simply do this with Linq by grouping by the index modulo the number of groups.

var result = list.Select((v,i)=>new{Value=v,Index=i})
                 .GroupBy(x => x.Index % numberOfGroups)
                 .Select(grp => grp.Select(x => x.Value).ToList())
                 .ToList();

So the following

List<int> list = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 };
var result =
    list.Select((v, i) => new { Value = v, Index = i })
        .GroupBy(x => x.Index % 6)
        .Select(grp => grp.Select(x => x.Value).ToList())
        .ToList();

int groupNumber = 1;
foreach (var l in result)
{
    Console.WriteLine("Group {0} : {1}", groupNumber++, string.Join(", ", l));
}

Will have these results

Group 1 : 1, 7, 13

Group 2 : 2, 8

Group 3 : 3, 9

Group 4 : 4, 10

Group 5 : 5, 11

Group 6 : 6, 12

Community
  • 1
  • 1
juharr
  • 31,741
  • 4
  • 58
  • 93
0

What I suppose you should do is calculate the average amount of items per bucket(aka List) and then insert that much into each bucket and then insert the remainder objects.

Note: Keeps order, and <int> can be replaced with <T>

for example:

public static List<List<int>> ExpandIntoGroups(this List<int> items, int totalGroups)
{
    List<List<int>> expandedList = null;
    if (items != null)
    {
        expandedList = new List<List<int>>();

        // Get amount of items to insert per list (for example: for 14 items, 4 groups, returns {4, 4, 3, 3}
        int[] itemsToInsertPerGroup = GetItemsToInsertPerGroup(items, totalGroups);

        // Go over each group
        for (int currGroup = 0, itemCount = 0;
             currGroup < itemsToInsertPerGroup.Length;
             currGroup++)
        {
            // add the list for the group
            expandedList.Add(new List<int>());

            // Add as much items as needed 
            for (int i = 0;
                 i < itemsToInsertPerGroup[currGroup] && 
                 itemCount < items.Count;
                 i++, itemCount++)
            {
                expandedList[currGroup].Add(items[itemCount]);
            }
        }
    }

    return expandedList;
}

private static int[] GetItemsToInsertPerGroup(List<int> items, int totalGroups)
{
    // Get average amount of items per list
    int avgItemsPerGroup = (int)(items.Count / totalGroups);

    int[] itemsToInsert = new int[totalGroups];

    // Set each cell to be the average
    for (int i = 0; i < itemsToInsert.Length; i++)
    {
        itemsToInsert[i] = avgItemsPerGroup;
    }

    // get how many items remain after inserting average amount
    int remainingItems = items.Count - avgItemsPerGroup * totalGroups;

    // Increase the amount needed per cell for each remaining item
    for (int i = 0, j = 0;
         i < remainingItems;
         ++i, ++j)
    {
        itemsToInsert[j]++;
    }

    return itemsToInsert;
}

Of course this is an ugly way of doing it, but it does the job.

Some test output:

Total items : 14
Total groups : 4

List 1 has 4 items
List 2 has 4 items
List 3 has 3 items
List 4 has 3 items

List 1 : 1 2 3 4 
List 2 : 5 6 7 8 
List 3 : 9 10 11 
List 4 : 12 13 14
Giora Guttsait
  • 1,279
  • 15
  • 29