4

Suppose I have list of list. I want to create new list from given list of list such that elements are in order of example given below.

Inputs:-

List<List<int>> l = new List<List<int>>();

List<int> a = new List<int>();
a.Add(1);
a.Add(2);
a.Add(3);
a.Add(4);
List<int> b = new List<int>();
b.Add(11);
b.Add(12);
b.Add(13);
b.Add(14);
b.Add(15);
b.Add(16);
b.Add(17);
b.Add(18);

l.Add(a);
l.Add(b);

Output(list):-

1
11
2
12
3
13
4
14
15
16 

And output list must not contain more than 10 elements.

I am currently doing this using foreach inside while but I want to know how can I do this using LINQ.

int loopCounter = 0,index=0;
List<int> o=new List<int>();
while(o.Count<10)
{
    foreach(List<int> x in l)
    {
        if(o.Count<10)
           o.Add(x[index]);
    }
    index++;
}

Thanks.

yajiv
  • 2,901
  • 2
  • 15
  • 25

3 Answers3

3

Use the SelectMany and Select overloads that receive the item's index. That will be used to apply the desired ordering. The use of the SelectMany is to flatten the nested collections level. Last, apply Take to retrieve only the desired number of items:

var result = l.SelectMany((nested, index) => 
                  nested.Select((item, nestedIndex) => (index, nestedIndex, item)))
              .OrderBy(i => i.nestedIndex)
              .ThenBy(i => i.index)
              .Select(i => i.item)
              .Take(10);

Or in query syntax:

var result = (from c in l.Select((nestedCollection, index) => (nestedCollection, index))
              from i in c.nestedCollection.Select((item, index) => (item, index))
              orderby i.index, c.index
              select i.item).Take(10);

If using a C# 6.0 and prior project an anonymous type instead:

var result = l.SelectMany((nested, index) => 
                  nested.Select((item, nestedIndex) => new {index, nestedIndex, item}))
              .OrderBy(i => i.nestedIndex)
              .ThenBy(i => i.index)
              .Select(i => i.item)
              .Take(10);

To explain why Zip alone is not enough: zip is equivalent to performing a join operation on the second collection to the first, where the attribute to join by is the index. Therefore Only items that exist in the first collection, if they have a match in the second, will appear in the result.

The next option is to think about left join which will return all items of the first collection with a match (if exists) in the second. In the case described OP is looking for the functionality of a full outer join - get all items of both collection and match when possible.

Gilad Green
  • 36,708
  • 7
  • 61
  • 95
2

I know you asked for LINQ, but I do often feel that LINQ is a hammer and as soon as a developer finds it, every problem is a nail. I wouldn't have done this one with LINQ, for a readability/maintainability point of view because I think something like this is simpler and easier to understand/more self documenting:

List<int> r = new List<int>(10);
for(int i = 0; i < 10; i++){
  if(i < a.Count)
    r.Add(a[i]);
  if(i < b.Count)
    r.Add(b[i]);
}

You don't need to stop the loop early if a and b collectively only have eg 8 items, but you could by extending the test of the for loop

I also think this case may be more performant than LINQ because it's doing a lot less

If your mandate to use LINQ is academic (this is a homework that must use LINQ) then go ahead, but if it's a normal everyday system that some other poor sucker will have to maintain one day, I implore you to consider whether this is a good application for LINQ

Caius Jard
  • 72,509
  • 5
  • 49
  • 80
  • You can leverage Enumerator instead of List in case input is NOT IList. – qxg Nov 23 '17 at 07:59
  • works only for 2 internal lists - not for lists of lists as asked for :) – Patrick Artner Nov 23 '17 at 08:34
  • Where was this asked for? It's trivial to convert to two for loops, but my answer was more to make a point about considering Lina appropriate than to solve problems possibly implied rather than stated. If you refer to the loops code in the question, you should be aware that that was not present when this answer was posted – Caius Jard Nov 23 '17 at 09:39
0

This will handle 2 or more internal List<List<int>>'s - it returns an IEnumerable<int> via yield so you have to call .ToList() on it to make it a list. Linq.Any is used for the break criteria.

Will throw on any list being null. Add checks to your liking.

static IEnumerable<int> FlattenZip (List<List<int>> ienum, int maxLength = int.MaxValue)
{
  int done = 0;
  int index = 0;
  int yielded = 0;

  while (yielded <= maxLength && ienum.Any (list => index < list.Count))
    foreach (var l in ienum)
    {
      done++;

      if (index < l.Count)
      {
        // this list is big enough, we will take one out
        yielded++;
        yield return l[index];
      }

      if (yielded > maxLength)
        break; // we are done

      if (done % (ienum.Count) == 0)
        index += 1; // checked all lists, advancing index
    }
}

public static void Main ()
{
  // other testcases to consider: 
  //   in total too few elememts
  //   one list empty (but not null)
  //   too many lists (11 for 10 elements) 

  var l1 = new List<int> { 1, 2, 3, 4 };
  var l2 = new List<int> { 11, 12, 13, 14, 15, 16 };
  var l3 = new List<int> { 21, 22, 23, 24, 25, 26 };

  var l = new List<List<int>> { l1, l2, l3 };

  var zipped = FlattenZip (l, 10);

  Console.WriteLine (string.Join (", ", zipped));
  Console.ReadLine ();
}
Patrick Artner
  • 50,409
  • 9
  • 43
  • 69