How can I interleave two arrays?
Asked Answered
A

5

17

If I have two arrays e.g

let one = [1,3,5]
let two = [2,4,6]

I would like to merge/interleave the arrays in the following pattern [one[0], two[0], one[1], two[1] etc....]

//prints [1,2,3,4,5,6]
let comibned = mergeFunction(one, two)
print(combined)

What would be a good way to implement the combining function?

func mergeFunction(one: [T], _ two: [T]) -> [T] {
    var mergedArray = [T]()
    //What goes here
    return mergedArray
}
Anu answered 22/1, 2016 at 16:46 Comment(2)
I hope you don't mind, I changed the title to more closely represent your needs. +1 btwOminous
Thanks thats much clearer!Anu
T
34

If both arrays have the same length then this is a possible solution:

let one = [1,3,5]
let two = [2,4,6]

let merged = zip(one, two).flatMap { [$0, $1] }

print(merged) // [1, 2, 3, 4, 5, 6]

Here zip() enumerates the arrays in parallel and returns a sequence of pairs (2-element tuples) with one element from each array. flatMap() creates a 2-element array from each pair and concatenates the result.

If the arrays can have different length then you append the extra elements of the longer array to the result:

func mergeFunction<T>(one: [T], _ two: [T]) -> [T] {
    let commonLength = min(one.count, two.count)
    return zip(one, two).flatMap { [$0, $1] } 
           + one.suffixFrom(commonLength)
           + two.suffixFrom(commonLength)
}

Update for Swift 3:

func mergeFunction<T>(_ one: [T], _ two: [T]) -> [T] {
    let commonLength = min(one.count, two.count)
    return zip(one, two).flatMap { [$0, $1] } 
           + one.suffix(from: commonLength)
           + two.suffix(from: commonLength)
}
Trapezius answered 22/1, 2016 at 17:27 Comment(4)
flatMap does not "create a tuple". zip creates the tuples; in effect, it yields an array of tuples. flatMap flattens! Simple map would give [[1, 2], [3, 4], [5, 6]]; flatMap removes the extra level of array.Thomas
You are still not doing yourself justice. flatMap "concatenates the result" and at the same time removes the very array that it created, leaving just the two elements. This is the ingenuity of your solution. Of course I thought of zip immediately, and hoped you would use it, but what you are doing here that is brilliant is making a little array only to destroy it again. You are passing through the little array as a way of removing the tuple wrapper, leaving just the two elements.Thomas
:( Pfft, suddenly Swift needs the args to be called without destructuring the tuple. Eg: let merged = zip(one, two).flatMap { [$0.0, $0.1] }Mazman
@Charlesism: Yes, but that may change again. Is was discussed on the Swift evolution mailing list (e.g. here lists.swift.org/pipermail/swift-evolution-announce/2017-June/…), I don't know what the current status is.Trapezius
A
6

If you're just looking to interleave two arrays, you could just do something like:

let maxIndex = max(one.count, two.count)
var mergedArray = Array<T>()
for index in 0..<maxIndex {
    if index < one.count { mergedArray.append(one[index]) }
    if index < two.count { mergedArray.append(two[index]) }
}

return mergedArray
Andesite answered 22/1, 2016 at 16:49 Comment(11)
Ill update the question to be more specific, I was using ints as an easy example, this is more about merging two arrays based on the index of the array element (one after another where possible)Anu
You could still use the approach above, just write a custom sort function instead of using <Andesite
Im looking to insert based on position in the array not by the value of the element i.e [one[0], two[0], one[1], two[1], etc....]Anu
So you just want to interleave two arrays? I'll update my answer.Andesite
I thought so I was praying for a clever functional method :( I'll mark yours as correctAnu
Well, flatmap wouldn't interleave the items. You could potentially use map across a tuple of your two arrays, but I don't think that would be any clearer.Andesite
I don't agree. I think Martin R's answer is much clearer. Swift likes functional programming better than imperative programming.Thomas
@Thomas I like his answer better as well, but it only works with arrays of the same length. The OPs trivial example would work with that answer, but it's not a general answer.Andesite
True. That's a pity. I can see how to work around that with some preparation but it's not worth it for this example.Thomas
@Thomas Seems like you'd be more or less stuck implementing a version of zip that worked across different length arrays, which would probably look a lot like my answer. That or a subarray of the longer, zip them, flatMap them, and append the remainder of the longer.Andesite
Actually my trick was to pad the shorter array with nils and call flatMap twice. As I say, not worth it.Thomas
E
2

With Swift 5, you can use one of the following Playground sample codes in order to solve your problem.


#1. Using zip(_:_:) function and Collection's flatMap(_:) method

let one = [1, 3, 5, 7]
let two = [2, 4, 6]

let array = zip(one, two).flatMap({ [$0, $1] })
print(array) // print: [1, 2, 3, 4, 5, 6]

Apple states:

If the two sequences passed to zip(_:_:) are different lengths, the resulting sequence is the same length as the shorter sequence.


#2. Using an object that conforms to Sequence and IteratorProtocol protocols

struct InterleavedSequence<T>: Sequence, IteratorProtocol {

    private let firstArray: [T]
    private let secondArray: [T]
    private let thresholdIndex: Int
    private var index = 0
    private var toggle = false

    init(firstArray: [T], secondArray: [T]) {
        self.firstArray = firstArray
        self.secondArray = secondArray
        self.thresholdIndex = Swift.min(firstArray.endIndex, secondArray.endIndex)
    }

    mutating func next() -> T? {
        guard index < thresholdIndex else { return nil }
        defer {
            if toggle {
                index += 1
            }
            toggle.toggle()
        }
        return !toggle ? firstArray[index] : secondArray[index]
    }

}

let one = [1, 3, 5, 7]
let two = [2, 4, 6]

let sequence = InterleavedSequence(firstArray: one, secondArray: two)
let array = Array(sequence)
print(array) // print: [1, 2, 3, 4, 5, 6]
Eckhart answered 19/12, 2018 at 0:1 Comment(2)
If one is longer than two, by any amount, your #2 and #3 will take one more element from one, than from two. Check my answer for an option of how to deal with that.Slick
@Jessy Thanks for your comment. I've updated the examples to reflect this.Eckhart
O
1

This can be done also using sequence(state:next:) (https://developer.apple.com/documentation/swift/sequence(state:next:)

For example:

let seq1 = [1,2,3,4]
let seq2 = [10,20,30,40,50, 60]

// Interleave two sequences that yield the same element type
let result = sequence(state: (false, seq1.makeIterator(), seq2.makeIterator()), next: { iters in
  iters.0 = !iters.0
  return iters.0 ? iters.1.next() : iters.2.next()
})

print(Array(result)) // Prints: [1, 10, 2, 20, 3, 30, 4, 40]
Old answered 11/3, 2023 at 16:3 Comment(0)
S
0
  /// Alternates between the elements of two sequences.
  /// - Parameter keepSuffix:
  /// When `true`, and the sequences have different lengths,
  /// the suffix of `interleaved`  will be the suffix of the longer sequence.
  func interleaved<Sequence: Swift.Sequence>(
    with sequence: Sequence,
    keepingLongerSuffix keepSuffix: Bool = false
  ) -> AnySequence<Element>
  where Sequence.Element == Element {
    keepSuffix
    ? .init { () -> AnyIterator<Element> in
      var iterators = (
        AnyIterator( self.makeIterator() ),
        AnyIterator( sequence.makeIterator() )
      )
      return .init {
        defer { iterators = (iterators.1, iterators.0) }
        return iterators.0.next() ?? iterators.1.next()
      }
    }
    : .init(
      zip(self, sequence).lazy.flatMap { [$0, $1] }
    )
  }
let oddsTo7 = stride(from: 1, to: 7, by: 2)
let evensThrough10 = stride(from: 2, through: 10, by: 2)
let oneThrough6 = Array(1...6)

XCTAssertEqual(
  Array( oddsTo7.interleaved(with: evensThrough10) ),
  oneThrough6
)

XCTAssertEqual(
  Array(
    oddsTo7.interleaved(with: evensThrough10, keepingLongerSuffix: true)
  ),
  oneThrough6 + [8, 10]
)
Slick answered 11/4, 2020 at 13:11 Comment(3)
Very similar to existing answer https://mcmap.net/q/690373/-how-can-i-interleave-two-arrays.Thomas
No, those stop at the same time zip does. (Or after one more, which is probably never a desired result.)Slick
I see, that's an important distinction. You might want to supplement your code with some words of explanation! Code alone is almost never all that useful.Thomas

© 2022 - 2024 — McMap. All rights reserved.