3

I'd like to sort multiple lists (variable number of them) into single list, but keeping the specific order. For example:

List A: { 1,2,3,4,5 }
List B: { 6,7,8 }
List C: { 9,10,11,12 }

Result List: { 1,6,9,2,7,10,3,8,11,4,12,5 }

The only idea I got was to remove the first element from each list and put it into resulting set (and repeat until all lists are empty), but maybe there is a better way that doesn't require to create copy of each list and doesn't affect the original lists as well?

9
  • Loop through each list from 0 to length and put each corresponding element in the new list? By that I mean A[0], B[0], C[0], A[1], (...). Commented Nov 23, 2016 at 15:30
  • And how do I know when to stop looping? Some kind of flag "hasElements", set to false in the beginning of loop and modified to true if any list still has greater count? Commented Nov 23, 2016 at 15:32
  • 3
    There is Enumerable.Zip<Tfirst, Tsecond>. Commented Nov 23, 2016 at 15:33
  • I like the Zip idea! Will give it a try. Commented Nov 23, 2016 at 15:34
  • 1
    Zip wants all lists to have equal Lengths Commented Nov 23, 2016 at 15:43

7 Answers 7

5

I suggest using IEnumerator<T> to enumerate lists while they have items:

private static IEnumerable<T> Merge<T>(params IEnumerable<T>[] sources) {
  List<IEnumerator<T>> enums = sources
    .Select(source => source.GetEnumerator())
    .ToList();

  try {
    while (enums.Any()) {
      for (int i = 0; i < enums.Count;)
        if (enums[i].MoveNext()) {
          yield return enums[i].Current;

          i += 1;
        }
        else {
          // exhausted, let's remove enumerator
          enums[i].Dispose();
          enums.RemoveAt(i);
        }
    }
  }
  finally {
    foreach (var en in enums)
      en.Dispose();
  }
}

Test

List<int> A = new List<int>() { 1, 2, 3, 4, 5 };
List<int> B = new List<int>() { 6, 7, 8 };
List<int> C = new List<int>() { 9, 10, 11, 12 };

var result = Merge(A, B, C)
  .ToList();

Console.Write(string.Join(", ", result));

The outcome is

1, 6, 9, 2, 7, 10, 3, 8, 11, 4, 12, 5
Sign up to request clarification or add additional context in comments.

Comments

2

For more flexible use

 public static string MergeArrays(params IList<int>[] items)
    {

        var result = new List<int>();
        for (var i = 0; i < items.Max(x => x.Count); i++)
            result.AddRange(from rowList in items where rowList.Count > i select rowList[i]);

        return string.Join(",", result);
    }

.

        var a = new List<int>() { 1, 2, 3, 4, 5 };
        var b = new List<int>() { 6, 7, 8 };
        var c = new List<int>() { 9, 10, 11, 12, 0, 2, 1 };

        var r = MergeArrays(a, b, c);

4 Comments

can you pls change name of your Foo2 function to something meaningful?
Find you're looking for hairy:)
Yeach, MergeArray looks almost ideal. (but MergeArrays will be better :P)
Among others, this solution looks simple enough to be understandable for others, even if it isn't the fastest. Thank you!
2

There is no sense in over complicating this in my opinion, why not use a simple for loop to accomplish what you need?

List<int> list1 = new List<int> { 1, 2, 3, 4, 5 };
List<int> list2 = new List<int> { 6, 7, 8 };
List<int> list3 = new List<int> { 9, 10, 11, 12 };
List<int> resultList = new List<int>();

for (int i = 0; i < list1.Count || i < list2.Count || i < list3.Count; i++)
{
    if (i < list1.Count) resultList.Add(list1[i]);
    if (i < list2.Count) resultList.Add(list2[i]);
    if (i < list3.Count) resultList.Add(list3[i]);
}

Result: 1,6,9,2,7,10,3,8,11,4,12,5

Comments

1

Here's a fairly simple way. It was fun to write up anyway.
No, it isn't the best, but it works and you could expand it to suit your needs of using a List<List<int>> very easily.

//Using arrays for simplicity, you get the idea.
int[] A = { 1, 2, 3, 4, 5 };
int[] B = { 6, 7, 8 };
int[] C = { 9, 10, 11, 12 };

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

//Determine this somehow. I'm doing this for simplicity.
int longest = 5; 

for (int i = 0; i < longest; i++)
{
    if (i < A.Length)
        ResultSet.Add(A[i]);
    if (i < B.Length)
        ResultSet.Add(B[i]);
    if (i < C.Length)
        ResultSet.Add(C[i]);
}

//ResultSet contains: { 1, 6, 9, 2, 7, 10, 3, 8, 11, 4, 12, 5 }

As you can see, just pop this out into a method and loop through your lists of lists, properly determining the max length of all lists.

3 Comments

int longest = Math.Max(A.Length, Math.Max(B.Length, C.Length)); OR int longest = new[] {A.Length, B.Length, C.Length}.Max();
You could certainly implement @tym32167's max length code! The second one would probably be easier since you mention a List<List<int>>.
Can you pls just update your answer with any of solutions?
0

I'd go with:

static void Main(string[] args)
{
    var a = new List<int>() { 1, 2, 3, 4, 5 };
    var b = new List<int>() { 6, 7, 8 };
    var c = new List<int>() { 9, 10, 11, 12 };

    var abc = XYZ<int>(new[] { a, b, c }).ToList();
}

static IEnumerable<T> XYZ<T>(IEnumerable<IList<T>> lists)
{
    if (lists == null)
        throw new ArgumentNullException();
    var finished = false;
    for (int index = 0; !finished; index++)
    {
        finished = true;
        foreach (var list in lists)
            if (list.Count > index) // list != null (prior checking for count)
            {
                finished = false;
                yield return list[index];
            }
    }
}

I had to use use IList to have indexer and Count. It doesn't creates anything (no enumerators, no lists, etc.), purely yield return.

4 Comments

Man, that's soo complicated. It hard to read this code at least for me.
@tym32167, the idea was to complement linq deferred execution. XYZ<T> doesn't produces anything until you actually start using results (e.g. call ToList()). It's bulletproof (note comment) and can be used with any type (aka generic). The logic, I agree, somewhat unclear, but pretty simple: outer foreach to enumerate all lists you passed and inner for to return all elements by current index; finished is used to determine when to .. finish.
Yep, I already understand the code, but I believe that you can make it more readable. That for cycle with flag finished .. dont know, I just personally dont like it. But ofcource your solution should work great.
I updated my answer, where I refactored your code. Pls take a look, what do you think about refactored code? Its almost same, but from my point it looks better just because It easier to read.
0

For your problem I create static method, which can merge any collections as you want:

public static class CollectionsHandling
{
    /// <summary>
    /// Merge collections to one by index
    /// </summary>
    /// <typeparam name="T">Type of collection elements</typeparam>
    /// <param name="collections">Merging Collections</param>
    /// <returns>New collection {firsts items, second items...}</returns>
    public static IEnumerable<T> Merge<T>(params IEnumerable<T>[] collections)
    {
        // Max length of sent collections
        var maxLength = 0;

        // Enumerators of all collections
        var enumerators = new List<IEnumerator<T>>();

        foreach (var item in collections)
        {
            maxLength = Math.Max(item.Count(), maxLength);
            if(collections.Any())
                enumerators.Add(item.GetEnumerator());
        }
        // Set enumerators to first item
        enumerators.ForEach(e => e.MoveNext());

        var result = new List<T>();
        for (int i = 0; i < maxLength; i++)
        {
            // Add elements to result collection
            enumerators.ForEach(e => result.Add(e.Current));

            // Remobve enumerators, in which no longer have elements
            enumerators = enumerators.Where(e => e.MoveNext()).ToList();
        }

        return result;
    }
}

Example of using:

static void Main(string[] args)
{
    var a = new List<int> { 1, 2, 3, 4, 5 };
    var b = new List<int> { 6, 7, 8 };
    var c = new List<int> { 9, 10, 11, 12 };

    var result= CollectionsHandling.Merge(a, b, c);
}

When you understand how it works, it will be possible to reduce the method of smaller.

6 Comments

Oh, my.. this solution even worse that mine. Enumerators, for cycles and Linq inside one method. Soo uneffective. Can you count how much collections you are creating during this method?
Firstly, this method is faster than you blink.
Secondly, read the last string "When you understand how it works, it will be possible to reduce the method of smaller.".
Why do you think that its faster? And yeach, I red last string.
Use stopwatch and note the time for work. And please, do not waste my time on silly questions...
|
0

Shortest and probably slowest solution

int[] A = { 1, 2, 3, 4, 5 };
int[] B = { 6, 7, 8 };
int[] C = { 9, 10, 11, 12 };


var arrs = new[] { A, B, C };
var merged = Enumerable.Range(0, arrs.Max(a => a.Length))
    .Select(x => arrs.Where(a=>a.Length>x).Select(a=>a[x]))
    .SelectMany(x=>x)
    .ToArray();

upd.

Another way to solve - I just refactored @Sinatr answer.

static IEnumerable<T> XYZ<T>(IEnumerable<IList<T>> lists)
{
    if (lists == null)  
        throw new ArgumentNullException();
    var index = 0;

    while (lists.Any(l => l.Count > index))
    {
        foreach (var list in lists)     
            if (list.Count > index)         
                yield return list[index];       
        index++;
    }
}

2 Comments

Can be further shortened for (int index = 0; lists.Any(l => l.Count > index); index++) {...} instead of var index = 0; while(...) {... index++}
@DmitryBychenko aa, thats almost same, just 3 lines can be combined to one, not a big difference. But thank you!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.