14

I am looking for an approach to join multiple Lists in the following manner:

ListA a b c
ListB 1 2 3 4
ListC + # * § %
..
..
..

Resulting List: a 1 + b 2 # c 3 * 4 § %

In Words: The elements in sequential order, starting at first list combined into the resulting list. An arbitrary amount of input lists could be there varying in length.

I used multiple approaches with variants of zip, sliding iterators but none worked and especially took care of varying list lengths. There has to be an elegant way in scala ;)

Th 00 mÄ s
  • 3,776
  • 1
  • 27
  • 46
  • 2
    Zip is a natural for this. What did you try that didn´t work? – itsbruce Sep 30 '13 at 14:46
  • Zip allows me to combine 2 lists. having multiple lists does at least complicate its usage for me (begginer in scala) also it does not allow to combine lists of none matching length. Once one lists ends further elements from the other lists are not zipped. – Th 00 mÄ s Sep 30 '13 at 14:54
  • 5
    @itsbruce: This is not at all trivial with `zip` and even with `zipAll`, `zipWith`, etc. it would be a little tricky. – Travis Brown Sep 30 '13 at 14:56
  • 1
    http://stackoverflow.com/questions/1664439/can-i-zip-more-than-two-lists-together-in-scala – red1ynx Sep 30 '13 at 14:58

7 Answers7

20
val lists = List(ListA, ListB, ListC)

lists.flatMap(_.zipWithIndex).sortBy(_._2).map(_._1)

It's pretty self-explanatory. It just zips each value with its position on its respective list, sorts by index, then pulls the values back out.

Luigi Plinge
  • 50,650
  • 20
  • 113
  • 180
  • 1
    Is sortBy guaranteed to be stable? – Miles Sabin Sep 30 '13 at 20:15
  • 2
    @Miles yes, it defers to `java.util.Arrays.sort`, and to quote from the javadocs: "This sort is guaranteed to be stable: equal elements will not be reordered as a result of the sort." – Luigi Plinge Sep 30 '13 at 20:43
  • 1
    Just to note this down somewhere: This is `O(n log n)` where n is the total number of elements. However, you would expect this to perform in `O(n)`. – gzm0 Oct 03 '13 at 07:51
5

Here's how I would do it:

class ListTests extends FunSuite {
  test("The three lists from his example") {
    val l1 = List("a", "b", "c")
    val l2 = List(1, 2, 3, 4)
    val l3 = List("+", "#", "*", "§", "%")

    // All lists together
    val l = List(l1, l2, l3)

    // Max length of a list (to pad the shorter ones)
    val maxLen = l.map(_.size).max

    // Wrap the elements in Option and pad with None
    val padded = l.map { list => list.map(Some(_)) ++ Stream.continually(None).take(maxLen - list.size) }

    // Transpose 
    val trans = padded.transpose

    // Flatten the lists then flatten the options
    val result = trans.flatten.flatten

    // Viola 
    assert(List("a", 1, "+", "b", 2, "#", "c", 3, "*", 4, "§", "%") === result)
  }
}
joescii
  • 6,495
  • 1
  • 27
  • 32
  • 1
    You may be interested in the built-in `padTo` method which allows you to simplify: `val padded = l.map(_.map(Option(_)).padTo(maxLen, None))` – Luigi Plinge Oct 01 '13 at 00:07
1

Here's a small recursive solution.

def flatList(lists: List[List[Any]]) = {
  def loop(output: List[Any], xss: List[List[Any]]): List[Any] = (xss collect { case x :: xs => x }) match {
    case Nil => output
    case heads => loop(output ::: heads, xss.collect({ case x :: xs => xs })) 
  }
  loop(List[Any](), lists)
}

And here is a simple streams approach which can cope with an arbitrary sequence of sequences, each of potentially infinite length.

def flatSeqs[A](ssa: Seq[Seq[A]]): Stream[A] = {
  def seqs(xss: Seq[Seq[A]]): Stream[Seq[A]] = xss collect { case xs if !xs.isEmpty => xs } match {
    case Nil => Stream.empty
    case heads => heads #:: seqs(xss collect { case xs if !xs.isEmpty => xs.tail })
  }
  seqs(ssa).flatten
}
Michael Mior
  • 28,107
  • 9
  • 89
  • 113
itsbruce
  • 4,825
  • 26
  • 35
1

Here's an imperative solution if efficiency is paramount:

def combine[T](xss: List[List[T]]): List[T] = {
  val b = List.newBuilder[T]
  var its = xss.map(_.iterator)
  while (!its.isEmpty) {
    its = its.filter(_.hasNext)
    its.foreach(b += _.next)
  }
  b.result
}
Luigi Plinge
  • 50,650
  • 20
  • 113
  • 180
1

You can use padTo, transpose, and flatten to good effect here:

lists.map(_.map(Some(_)).padTo(lists.map(_.length).max, None)).transpose.flatten.flatten
Ben Reich
  • 16,222
  • 2
  • 38
  • 59
0

Here's something short but not exceedingly efficient:

def heads[A](xss: List[List[A]]) = xss.map(_.splitAt(1)).unzip
def interleave[A](xss: List[List[A]]) = Iterator.
  iterate(heads(xss)){ case (_, tails) => heads(tails) }.
  map(_._1.flatten).
  takeWhile(! _.isEmpty).
  flatten.toList
Rex Kerr
  • 166,841
  • 26
  • 322
  • 407
0

Here's a recursive solution that's O(n). The accepted solution (using sort) is O(nlog(n)). Some testing I've done suggests the second solution using transpose is also O(nlog(n)) due to the implementation of transpose. The use of reverse below looks suspicious (since it's an O(n) operation itself) but convince yourself that it either can't be called too often or on too-large lists.

def intercalate[T](lists: List[List[T]]) : List[T] = {
    def intercalateHelper(newLists: List[List[T]], oldLists: List[List[T]], merged: List[T]): List[T] = {
      (newLists, oldLists) match {
        case (Nil, Nil) => merged
        case (Nil, zss) => intercalateHelper(zss.reverse, Nil, merged)
        case (Nil::xss, zss) => intercalateHelper(xss, zss, merged)
        case ( (y::ys)::xss, zss) => intercalateHelper(xss, ys::zss, y::merged)
      }
    }
    intercalateHelper(lists, List.empty, List.empty).reverse
  }