【问题标题】:Functional way to find a pair of integers, which sum to X, in a sorted array在排序数组中找到一对整数的函数式方法,总和为 X
【发布时间】:2017-07-07 12:14:14
【问题描述】:

这是我之前的question 的后续。 假设我想在给定的排序数组中找到一对整数,它们的总和为给定数字x。著名的“一次性”解决方案如下所示:

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
}

您如何建议在没有任何可变状态的情况下进行函数式编写?
我想我可以用Stream(Scala 中的惰性列表)编写一个递归版本 你能推荐一个非递归版本吗?

【问题讨论】:

  • 这段代码不能用 Scala 编译。
  • 谢谢。将尝试编译并修复它。
  • 嗨@迈克尔。为你解决了这个问题。关于答案,我看到已经有很多了:)
  • 为什么是非递归的?循环的纯等价物是递归。

标签: arrays algorithm scala functional-programming


【解决方案1】:

这是一个相当简单的版本。它创建VectorsStream,在每次迭代中删除第一个或最后一个元素。然后我们限制了原本无限的Stream 的大小(-1,所以你不能自己加上一个数字),然后map 将其转换为输出格式并检查目标条件。

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}
}

Scala 集合的伴生对象中隐藏着很多宝石,例如Stream.iterate。我强烈建议检查一下。了解它们可以大大简化这样的问题。

【讨论】:

  • 谢谢!不知道Stream.iterate
  • 非常好。我喜欢它将产生候选人流与测试我们是否匹配的方式分开。
【解决方案2】:

这是一个不使用索引的版本(我尽量避免使用,除非对它们的值进行重要计算):

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)
} 

它是递归的,但不需要 Stream。 tailinit 对于 Array 来说是 O(N),但如果我们使用 Lists 并反转 r 集合以避免 initlast ,则可以完成 O(N) 版本

  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)
}  

如果我们不关心单程(或通常的效率:)),那就是单程

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

将其解包到 flatmap/map 中,然后使用 collectFirst 为我们提供了这个,它相当简洁且更优化(但仍然不是 O(n)) - 它在第一个正确的对处停止,但完成的工作比到达那里所需的要多.

 a.collectFirst{case m => a.collectFirst{case n if n+m == x => (m,n)}}.get

【讨论】:

  • Array.tail 在 Scala 中实际上是 O(n)。所以...这个答案也是O(n^2)
  • Array.tail 实际上继承自IndexedSeqOptimized。并在github.com/scala/scala/blob/v2.12.1/src/library/scala/… 定义,它使用slice 定义在github.com/scala/scala/blob/v2.12.1/src/library/scala/…
  • 所以,使用Vector
  • 这并不复杂,只是不同而已。鉴于 OP 已经说过这是对先前问题的跟进,如果您愿意看一下,这正是避免使用索引,原因既不奇怪也不未知。除了转换为 List 之外,效率与您的索引版本几乎相同。
  • 不,OP 要求两件事:在功能上,没有可变状态,以及非递归。就个人而言,我认为惯用的函数式代码不使用显式索引,除非确实有必要。但是我已经离开这里了,已经很晚了。
【解决方案3】:

如果没有递归和可变状态,它会变得非常丑陋。这是我的尝试:

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
}

例子:

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

开头的.reverse 加上找到结果时返回的foldLeft 构成O(2n)

【讨论】:

【解决方案4】:

这是一条线

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

它甚至适用于未排序的列表。

但如果您想利用排序,只需对数组中的每个x 进行二分搜索(n-x),而不是遍历数组。

【讨论】:

  • 这正是我想得到的答案。解释幕后发生的事情以及为什么这是一个理想的解决方案(与其他提出的解决方案相比)对每个人都有好处。给读者的提示:不,不是因为它在语法上更短就更好了。
  • 这应该在 Scala 中
  • 每个人都知道如何在 O(n^2) 中做到这一点。您错过了问题的重点,即制定 O(n) 算法。此外,不允许添加元素本身,因此 array = [1..1000000]n = 2 应该很快就不会返回任何结果。
  • @KarlBielefeldt 我没有错过这一点,请参阅我的最后一段。 OT 的问题是如何在没有变异的情况下做这些事情。我展示了一个可以改进的初始纯函数解决方案。例如,我们可以将x &lt;- array 细化为x &lt;- dropWhile (&lt;n) array 等等。尽管原始问题中没有说明 x 不能与自身配对的附加要求,但也可以轻松满足。
  • @KarlBielefeldt 当然必须是takeWhile 而不是dropWhile。很抱歉这个错误。
猜你喜欢
  • 2011-10-07
  • 1970-01-01
  • 2017-10-04
  • 1970-01-01
  • 2020-02-22
  • 2013-01-15
  • 2018-09-10
  • 2011-09-06
  • 2018-05-16
相关资源
最近更新 更多