3

From this Q: How to split an array to 2 arrays with odd and even indices respectively?

I have used this method to split an array into 2 arrays with odd and even indices respectively:

int[] a = new int[] { 1, 3, 7, 8 };

int[] aEven = a.Where((x, i) => i % 2 == 0).ToArray();
int[] aOdd = a.Where((x, i) => i % 2 != 0).ToArray();

This results 2 arrays:

aEven : {1, 7}
aOdd  : {3, 8}

How can I merge the aEven/aOdd back to be the same like the original a array in an elegant way?

NOTE: I'm not changing the aEven/aOdd arrays, and it is guaranteed both arrays have the same length.

The output I need from aEven/aOdd inputs is:

{ 1, 3, 7, 8 };
zig
  • 4,524
  • 1
  • 24
  • 68
  • it is not C. It is C# - completely different language. – 0___________ May 13 '18 at 07:53
  • It was just a mistake. Sorry – zig May 13 '18 at 08:18
  • Any solution to this is going to involve a loop, whether directly (ie `for`) or indirectly (ie LINQ's `zip`) – pinkfloydx33 May 13 '18 at 08:29
  • Do you want to update the original array? It seems you never modify it so it would already be correct. Or do you mean you want to merge the two arrays to create a new array that is identical to the original (such that perhaps you don't have the original at all and you're trying to reconstruct it)? – pinkfloydx33 May 13 '18 at 08:41
  • @zig I updated my answer , `Zip` is not suitable for this problem without a bunch of workarounds as appeared in my first answer, you can have a look at it! – mshwf May 13 '18 at 11:28
  • @MohamedElshawaf, The answer provided by pinkfloydx33 works great and uses `Zip`. is there anything wrong with it? – zig May 13 '18 at 11:34
  • I didn't say it's wrong, I said it need some workaround to meet your requirements, it's not designed for such problem where both lists have different lengths, also have you tried my solution ? – mshwf May 13 '18 at 11:39
  • @MohamedElshawaf, Yes, I tried it and it works. I'll up-vote. thank you. – zig May 13 '18 at 11:48
  • 1
    Hopefully after my edit everyone who voted to close as `unclear what you're asking` will know what I'm asking :) – zig May 13 '18 at 12:24

6 Answers6

5

One way to do this is by combining Enumerable.Zip and Enumerable.Aggregate along with a little finessing. Note that this still uses loops under the hood.

var aList = aEven.Zip(aOdd, (even, odd) => new {even, odd})
                 .Aggregate(new List<int>(aEven.Length + aOdd.Length),
                            (list, z) =>
                            {
                                list.Add(z.even);
                                list.Add(z.odd);
                                return list;
                            });
if (aEven.Length != aOdd.Length)
    aList.Add(aEven[aEven.Length-1]);

var aOutput = aList.ToArray();

for (var i = 0; i < aOutput.Length; ++i)
    Console.WriteLine($"aOutput[{i}] ==> {aOutput[i]} == {a[i]} <== a[{i}]");

This only works in your scenario however (splitting out and then restoring an array by odd/even indices, assuming that the order of the 'sub-arrays' has been maintained).

The resultant arrays will either have the same size (the original array had an even number of items) or the even array will have one extra item (the original array had an odd number of items). In the latter case, the extra item will be dropped by Zip and needs to be manually accounted for. This won't work for other scenarios where your two sub-arrays were calculated via other means.

You can also do it without the intermediate list, using a pre-allocated array but you'd have to keep track of the index outside of the LINQ calls (which I don't like as much):

var index = 0;
var aOutput = aEven.Zip(aOdd, (even, odd) => new {even, odd})
                   .Aggregate(new int[aEven.Length + aOdd.Length],
                             (arr, z) =>
                             {
                                 arr[index++] = z.even;
                                 arr[index++] = z.odd;
                                 return arr;
                             });
if (aEven.Length != aOdd.Length)
    aOutput[index] = aEven[aEven.Length-1];

Another way to do it would be to use a combination of Zip, SelectMany and Concat (to account for the last item):

var aOutput = aEven.Zip(aOdd, (even, odd) => new[]{ even, odd })
    .SelectMany(z => z)
    .Concat(aEven.Length == aOdd.Length ? new int[0] : new []{ aEven[aEven.Length - 1] })
    .ToArray();

A straightforward for loop would still probably be the simplest solution.

pinkfloydx33
  • 11,863
  • 3
  • 46
  • 63
2

Just use for loop instead. It won't create all these intermediate arrays of tuples, while is comparably readable and requires the same amount of code.

int[] Merge(int[] evenItems, int[] oddItems)
{
    var itemsCount = evenItems.Length + oddItems.Length;
    var result = new int[itemsCount];

    for (var i = 0; i < itemsCount; i++)
    {
        var sourceIndex = Math.DivRem(i, 2, out var remainder);
        var source = remainder == 0 ? evenItems : oddItems;
        result[i] = source[sourceIndex];
    }

    return result;
}
Uladzislaŭ
  • 1,680
  • 10
  • 13
  • it will only work for this case, try for example running this :`Merge( new int[] { 1, 2, 5, 10 }, new int[] { 8, 11 });` – mshwf May 13 '18 at 11:50
  • 2
    @MohamedElshawaf you can't get such input when initial array is split into two with elements staying on even and odd indices. – Uladzislaŭ May 13 '18 at 11:53
2
public static IEnumerable<T> MergeByZipAndRemainder<T>(T[] this firsts, T[] seconds)
{
  int maxLength = Math.Max(firsts.Length, seconds.Length);
  foreach(int i in Enumerable.Range(0, maxLength))
  {
     if (i < firsts.Length) { yield return firsts[i]; }
     if (i < seconds.Length) { yield return seconds[i]; }
  }
}

Then:

var result = evens.MergeByZipAndRemainder(odds).ToArray();

This approach meets several of the criticisms of the answerers.

  • It is elegant.
  • It addresses the general case of merging arrays which may be very different in length.
  • It only enumerates once O(N+M).
  • It does not allocate intermediate array objects which will require garbage collection.
  • It should be faster on paper, but I have not measured the performance.
Amy B
  • 108,202
  • 21
  • 135
  • 185
1

Create InterlockWith extension method, and yes, there's no escape from loops:

public static IEnumerable<T> InterlockWith<T>(this IEnumerable<T> seq1, IEnumerable<T> seq2)
    {
        Tuple<T[], int>[] metaSequences = new Tuple<T[], int>[2];
        metaSequences[0] = Tuple.Create(seq1.ToArray(), seq1.Count());
        metaSequences[1] = Tuple.Create(seq2.ToArray(), seq2.Count());
        var orderedMetas = metaSequences.OrderBy(x => x.Item2).ToArray();

        for (int i = 0; i < orderedMetas[0].Item2; i++)
        {
            yield return metaSequences[0].Item1[i];
            yield return metaSequences[1].Item1[i];
        }

        var remainingItems = orderedMetas[1].Item1.Skip(orderedMetas[0].Item2);
        foreach (var item in remainingItems)
        {
            yield return item;
        }
    }

You can get your result set by calling it like this:

a = aEven.InterlockWith(aOdd).ToArray();
mshwf
  • 7,009
  • 12
  • 59
  • 133
1
var merged = evens
  .Zip(odds, (even, odd) => new [] {even, odd})
  .SelectMany(pair => pair)
  .Concat(evens.Skip(odds.Length))
  .ToArray();
Amy B
  • 108,202
  • 21
  • 135
  • 185
  • this code is the other way around (`evens` should start first.), but after I made the modifications, it works great. very nice. – zig May 13 '18 at 11:56
  • Oops, hehe. Ok - made that change. Thanks! – Amy B May 13 '18 at 11:58
  • As a side note, it will skip one item from `odds` if it has more count than `evens`, but works well for the OP problem, I like how you used these LINQ methods combinations though – mshwf May 13 '18 at 12:17
  • 2
    @MohamedElshawaf In the case of the general problem, just call Concat twice. – Amy B May 13 '18 at 12:30
  • `Skip` isn't "specialized" for arrays or lists (like `ElementAt` is, for example with `List` or several others) meaning that the `Skip` call will have to iterate *all items* of the array (possibly to yield no items at all) and then potentially called *twice*?? Not so great in the general case if you ask me. – pinkfloydx33 May 13 '18 at 13:47
  • @pinkfloydx33 OP asked for elegance. This code is expressive. In general, if you can afford to enumerate an array once, you can afford to do it a constant number of times, such as three. If the code needs to be further optimized, it is easy to do so. This is different from ElementAt in a loop, which is O(N). – Amy B May 13 '18 at 14:24
  • @pinkfloydx33 I also would like to point out that I upvoted your answer as it is helpful, even though I would not use those solutions for my own reasons. Downvotes should be reserved for unhelpful answers, not to express mere disagreement. – Amy B May 13 '18 at 14:32
-1

First you can merge them by using Concat, and then if you want the reslt to be sorted, you can use OrderBy :

aEven.Concat(aOdd).OrderBy(b => b).ToArray();
  • 2
    It will not maintains the same order of `a` – mshwf May 13 '18 at 08:17
  • This problem cannot be solved unless the order of `a` is known (it appears to be ascending in the example). Without that, merging the two arrays is not guaranteed to restore the original order because there is no guarantee it alternated between even and odd... and in fact it doesn't in the example. – John Wu May 13 '18 at 08:47
  • 1
    You have to assign the code you posted to a variable like `var result = aEven.Concat(aOdd).OrderBy(b => b).ToArray();` because it doesn't change any of the declared variables in the OP's question – Slaven Tojić May 13 '18 at 09:08
  • @SlavenTojić: Thanks, you are right, I assumed it was clear, but as you did it's better to mention it. – Mahsa Aghajani May 13 '18 at 09:17