Functional way to find a pair of integers, which s

2019-07-07 20:07发布

This is a follow-up to my previous question. Suppose I want to find a pair of integers, which sum to a given number x, in a given sorted array. The well-known "one pass" solution looks like that:

def pair(a: Array[Int], target: Int): Option[(Int, Int)] = {

  var l = 0
  var r = a.length - 1
  var result: Option[(Int, Int)] = None
  while (l < r && result.isEmpty) {
    (a(l), a(r)) match {
      case (x, y) if x + y == target => result = Some(x, y)
      case (x, y) if x + y < target => l = l + 1
      case (x, y) if x + y > target => r = r - 1
    }
  }
  result
}

How would you suggest write functionally without any mutable state ?
I guess I can write a recursive version with Stream (lazy list in Scala) Could you suggest a non-recursive version ?

4条回答
相关推荐>>
2楼-- · 2019-07-07 20:41

This is a one liner

[ (x, y) | x <- array, y <- array, x+y == n ]

It even works on unsorted lists.

But if you want to take advantage of the sorting, just do a binary search for (n-x) for every x in the array instead of going through the array.

查看更多
一纸荒年 Trace。
3楼-- · 2019-07-07 20:47

Here's a fairly straightforward version. It creates a Stream of Vectors that removes the first or last element on each iteration. Then we limit the size of the otherwise infinite Stream (-1 so you can't add a number with itself), then map it into the output format and check for the target condition.

def findSum(a: Vector[Int], target: Int): Option[(Int, Int)] = {
  def stream = Stream.iterate(a){
    xs => if (xs.head + xs.last > target) xs.init else xs.tail
  }

  stream.take (a.size - 1)
        .map {xs => (xs.head, xs.last)}
        .find {case (x,y) => x + y == target}
}

There are a lot of gems hidden in the companion objects of Scala's collections, like Stream.iterate. I highly recommend checking them out. Knowing about them can greatly simplify a problem like this.

查看更多
做自己的国王
4楼-- · 2019-07-07 20:47

Without recursion and without a mutable state it can get pretty ugly. Here's my attempt:

def f(l: List[Int], x: Int): Option[(Int, Int)] = {
  l.foldLeft(l.reverse) {
    (list, first) =>
      list.headOption.map {
        last =>
          first + last match {
            case `x` => return Some(first, last)
            case sum if sum < x => list
            case sum if sum > x =>
              val innerList = list.dropWhile(_ + first > x)
              innerList.headOption.collect {
                case r if r + first == x => return Some(first, r)
              }.getOrElse {
                innerList
              }
          }
      }.getOrElse {
        return None
      }
  }
  None
}

Examples:

scala> f(List(1, 2, 3, 4, 5), 3)
res33: Option[(Int, Int)] = Some((1,2))

scala> f(List(1, 2, 3, 4, 5), 9)
res34: Option[(Int, Int)] = Some((4,5))

scala> f(List(1, 2, 3, 4, 5), 12)
res36: Option[(Int, Int)] = None

The .reverse in the beginning plus the foldLeft with a return when the result is found makes this O(2n).

查看更多
放我归山
5楼-- · 2019-07-07 20:49

Here's a version that doesn't use indices (which I try to avoid unless there's an important computation with the value of them):

def findPair2(x: Int, a: Array[Int]): Option[(Int, Int)] = {   
    def findPairLoop(x: Int, l: Array[Int], r: Array[Int]): Option[(Int, Int)] = {
        if (l.head >= r.last) None
        else if (l.head + r.last == x) Some((l.head, r.last))
        else if (l.head + r.last > x) findPairLoop(x, l, r.init)
        else findPairLoop(x, l.tail, r)
    }
    findPairLoop(x, a, a)
} 

It's recursive, but doesn't need Stream. tail and init are O(N) for Array but if we use Lists and reverse the r collection to avoid init and last an O(N) version can be done

  def findPairInOrderN(x: Int, a: Array[Int]): Option[(Int, Int)] = {
    def findPairLoop(x: Int, l: List[Int], r: List[Int]): Option[(Int, Int)] = {
        if (l.head >= r.head) None
        else if (l.head + r.head == x) Some((l.head, r.head))
        else if (l.head + r.head > x) findPairLoop(x, l, r.tail)
        else findPairLoop(x, l.tail, r)
    }
    val l = a.toList
    findPairLoop(x, l, l.reverse)
}  

If we don't care about one-pass (or efficiency generally :)) it's a one liner

(for (m <-a ; n <- a if m + n == x) yield (m,n)).headOption

unwrapping that into flatmap/map and then using collectFirst gives us this, which is fairly neat and more optimal (but still not O(n)) - it stops at the first correct pair but does more work than necessary to get there.

 a.collectFirst{case m => a.collectFirst{case n if n+m == x => (m,n)}}.get
查看更多
登录 后发表回答