功能编程的效率

时间:2014-02-21 05:09:00

标签: scala map functional-programming

给定一个整数和值的数组我试图找出如果它存在于数组中,那么来自任一端的值的距离是多少。从而创建一张地图。我尝试使用不可变数据映射并以函数方式解决它,但是从计算角度来看,与我以命令式方式(java方式)编写时相比,效率非常低。 我认为这是由于我对编码功能风格的不完全理解,而不是风格之间的内在差异。

val typeSum = 8
val data = List(2,3,4,5,2,3)
val dogTimes:scala.collection.mutable.Map[Int,Int] = scala.collection.mutable.Map() withDefaultValue(-1);
for ( x <- 1 to (data.length)/2 ){
    if (dogTimes(data(x-1)) > x || dogTimes(data(x-1)) < 0) dogTimes(data(x-1)) = x;
}
for( x <- (data.length/2 + 1) to data.length ){
    if (dogTimes(data(x-1)) > (data.length - x)|| dogTimes(data(x-1)) < 0)
        dogTimes(data(x-1)) = data.length - x+1;
}
if (typeSum%2 ==0) dogTimes(typeSum/2) = -1

这是我可以用函数式编写的代码,并且比上面的代码慢。如何改进以下代码以提高效率?

val tempDogTimes = data.zipWithIndex groupBy(_._1) mapValues(w =>
    List(w.head._2+1,data.length - w.last._2).min) withDefaultValue(-1)
val dogTimes = collection.mutable.Map[Int,Double]() ++= tempDogTimes
if (typeSum%2 ==0) dogTimes(typeSum/2) = -1

注意:这是我为竞赛提交的问题的一部分,并且命令性代码被接受,而下一个则超出了时间错误。

3 个答案:

答案 0 :(得分:3)

让我从我的眼睛里揉搓睡眠。你想从任何一端走到列表,第一次看到每个元素时记录,对吗?

scala> val data = List(2,3,4,5,2,3)
data: List[Int] = List(2, 3, 4, 5, 2, 3)

scala> val is = ((data take (data.size / 2)), (data drop (data.size / 2)).reverse).zipped
is: scala.runtime.Tuple2Zipped[Int,List[Int],Int,List[Int]] = scala.runtime.Tuple2Zipped@72cf531c

scala> .toList
res0: List[(Int, Int)] = List((2,3), (3,2), (4,5))

scala> ((Map.empty[Int,Int], data.to[Set], 0) /: is) { case ((m, n, i), (x, y)) =>
     | if (n.isEmpty) (m, n, i+1)
     | else (
     | m ++ List(if (m contains x) None else Some(x -> i), if (m contains y) None else Some(y -> i)).flatten,
     | n -- Set(x,y),
     | i + 1
     | )}
res1: (scala.collection.immutable.Map[Int,Int], Set[Int], Int) = (Map(2 -> 0, 3 -> 0, 4 -> 2, 5 -> 2),Set(),3)

scala> ._1
res2: scala.collection.immutable.Map[Int,Int] = Map(2 -> 0, 3 -> 0, 4 -> 2, 5 -> 2)

使用Vector和视图来索引两半会更好。构建元素集是无关紧要的,但如果你已经知道了域,那将会很方便。

另一个摇摆:

scala> val data = List(2,3,4,5,2,3).to[Seq]
data: Seq[Int] = Vector(2, 3, 4, 5, 2, 3)

scala> val half = data.size / 2
half: Int = 3

scala> val vs = (data.view take half, (data.view drop half).reverse).zipped
vs: scala.runtime.Tuple2Zipped[Int,scala.collection.SeqView[Int,Seq[Int]],Int,scala.collection.SeqView[Int,Seq[Int]]] = scala.runtime.Tuple2Zipped@72cf531c

scala> import collection.mutable
import collection.mutable

scala> val x = 4 // some key to exclude
x: Int = 4

scala> ((mutable.Map.empty[Int,Int].withDefaultValue(Int.MaxValue), 0) /: vs) {
     | case ((m, i), (x, y)) => m(x) = m(x) min i; m(y) = m(y) min i; (m, i+1) }
res4: (scala.collection.mutable.Map[Int,Int], Int) = (Map(2 -> 0, 5 -> 2, 4 -> 2, 3 -> 0),3)

scala> ._1.filter { case (k, v) => k != x }.toMap
res5: scala.collection.immutable.Map[Int,Int] = Map(2 -> 0, 5 -> 2, 3 -> 0)

我还不确定视图是否被折叠强制,因此带索引的循环可能会更好。并没有那么水平而不是包裹?这样的代码是不可读的。

答案 1 :(得分:2)

首先,请允许我说,在可变版本中使用List的方式非常糟糕。 List对索引访问有很糟糕的表现,你经常使用它。对于索引访问,请改用Vector。或Array,因为它无论如何都是可变的。

在不可变版本上,您还会在每次迭代时使用length,即List的O(n)。只需在循环外调用length一次并保存它将有助于提高性能。 你也这样做:

List(w.head._2+1,data.length - w.last._2).min

与简单的

相比有点慢
(w.head._2+1) min (data.length - w.last._2)

当然,您应该将数据结构更改为Vector或将data.length替换为仅指定一次的内容。

现在,我可以看到两种方法。一种是以两种方式行走地图,并像往常一样得到最小值,另一种是像som snytt那样只走一次。首先,您确实需要将类型更改为Vector。第二种方法可以正常使用List

让我们从第一个开始,这更接近你所做的。我在这里一直在努力争取,就像练习一样。在实践中,我可能使用var不可变Map而不是递归。

def dogTimes(data: IndexedSeq[Int], typeSum: Int): Map[Int, Int] = {
  import scala.annotation.tailrec

  val unwantedKey = typeSum / 2
  val end = data.length
  val halfway = end / 2

  @tailrec
  def forward(result: Map[Int, Int], i: Int): Map[Int, Int] = {
    if (i > halfway) result
    else if (data(i) == unwantedKey) forward(result, i + 1)
    else if (result contains data(i)) forward(result, i + 1)
    else forward(result updated (data(i), i + 1), i + 1)
  }

  @tailrec
  def backward(result: Map[Int, Int], i: Int): Map[Int, Int] = {
    println(s"$i ${data(i)} $result")
    if (i < halfway) result
    else if (data(i) == unwantedKey) backward(result, i - 1)
    else if (result contains data(i)) backward(result updated (data(i), result(data(i)) min (end - i)), i - 1)
    else backward(result updated (data(i), end - i), i - 1)
  }

  // forward has to be computed first
  val fwd = forward(Map.empty[Int, Int], 0)
  val bwd = backward(fwd, end - 1)

  bwd
}

这几乎是你的可变代码的功能版本 - 它很冗长,并没有真正使用任何收集方法来帮助工作。它也可以简化一点 - 例如,data.length % 2是不必要的,因为它内部的代码将始终有效,无论data.length是偶数还是奇数。而且,contains测试也可以通过在更新中使用getOrElse来删除。

它还会返回标准地图,而不是默认地图。您可以在之后添加默认值。

另一种方式是或多或少som snytt的解决方案,但我宁愿让它更简单,因为在该解决方案中min不是必需的。在此,我接受适用于Seq的{​​{1}}。

List

我保持了snytt的def dogTimes(data: Seq[Int], typeSum: Int): Map[Int, Int] = { import scala.annotation.tailrec val unwantedKey = typeSum / 2 val half = data.length / 2 + 1 val vs = (data.view take half zip data.view.reverse).zipWithIndex val result = vs.foldLeft(Map.empty[Int, Int]) { case (map, ((x, y), i)) => val m1 = if (map.contains(x) || x == unwantedKey) map else map.updated(x, i + 1) if (m1.contains(y) || y == unwantedKey) m1 else m1.updated(y, i + 1) } result } ,但我怀疑它的反向表现对view来说非常糟糕。它应该适用于List,但我认为删除第二个Vector调用应该会使view更快。

请注意,我在这段代码中没有使用List,原因很简单:因为我同时向前和向后从最低索引到最高索引,每当一个键在map,我知道它必须有一个低于或等于当前索引的索引。

另请注意,我正在挑选min - 这确保我会处理奇数大小的列表中的中间元素。我不会在反转之前丢弃元素,因为zip总是选择最小的尺寸。

如果我们决定要求索引序列,则以下内容可能更快:

half + 1

另请注意,我更喜欢在两个示例中防止不需要的密钥进入地图而不是之后将其删除。这可能是一个糟糕的决定,但最后更改代码以删除它是微不足道的,所以我决定向您提供替代方案。

答案 2 :(得分:1)

Scala有一种优雅的方式可以将列表项与其索引配对:zipWithIndex。当您将列表分成两半时,您可以创建两个匹配大小写,第一个条件为:

val typeSum = 8
val data = List(2, 3, 4, 5, 2, 3)
val dogTimes: scala.collection.mutable.Map[Int, Int] = scala.collection.mutable.Map() withDefaultValue (-1)
data.zipWithIndex foreach {
  case (value, index) if (index < data.length / 2) => {
    if (dogTimes(value) > index + 1 || dogTimes(value) < 0) {
      dogTimes(value) = index + 1
    }
  }
  case (value, index) => {
    if (dogTimes(value) > (data.length - index) || dogTimes(value) < 0) {
      dogTimes(value) = data.length - index
    }
  }
}
if (typeSum % 2 == 0) dogTimes(typeSum / 2) = -1