使用foldRight反转列表的优雅方式?

时间:2010-06-27 15:23:21

标签: scala

我正在阅读Programming in Scala书中关于折叠技巧的文章,并且遇到了这个片段:

def reverseLeft[T](xs:List[T]) = (List[T]() /: xs) {
    (y,ys) => ys :: y
}

如您所见,它是使用foldLeft/:运算符完成的。好奇如果我使用:\做到这一点,我想出了这个:

def reverseRight[T](xs:List[T]) = (xs :\ List[T]()) {
    (y,ys) => ys ::: List(y)
}

据我了解,:::似乎没有::那么快,并且具有线性成本,具体取决于操作数列表的大小。不可否认,我没有CS的背景,也没有先前的FP经验。所以我的问题是:

  • 如何识别/区分问题方法中的foldLeft / foldRight?
  • 有没有更好的方法可以在不使用:::的情况下执行此操作?

2 个答案:

答案 0 :(得分:12)

由于标准库中foldRight上的List是严格的并且使用线性递归实现,因此您应该避免使用它作为规则。 foldRight的迭代实现如下:

def foldRight[A,B](f: (A, B) => B, z: B, xs: List[A]) =
  xs.reverse.foldLeft(z)((x, y) => f(y, x))

foldLeft的递归实现可能是这样的:

def foldLeft[A,B](f: (B, A) => B, z: B, xs: List[A]) =
  xs.reverse.foldRight(z)((x, y) => f(y, x))

所以你看,如果两者都是 strict ,那么foldRight和foldLeft中的一个或另一个将用reverse实现(概念上无论如何)。由于列表的构建方式是::与右侧相关联,因此直接的迭代折叠将为foldLeft,而foldRight只是“反向然后折叠左侧”。

直观地说,您可能认为这将是foldRight的缓慢实现,因为它会将列表折叠两次。但是:

  1. “两次”无论如何都是一个常数因子,因此它渐近等同于折叠一次。
  2. 无论如何,你必须两次检查清单。一旦将计算推入堆栈并再次将它们从堆栈中弹出。
  3. 上述foldRight的实施速度比标准库中的实施速度快。

答案 1 :(得分:1)

列表上的操作有意不对称。 List数据结构是单链表,其中每个节点(数据和指针)都是不可变的。这种数据结构背后的想法是,您通过引用内部节点并添加指向它们的新节点来对列表的前面执行修改 - 列表的不同版本将共享列表末尾的相同节点。

将新元素附加到列表末尾的:::运算符必须创建整个列表的新副本,否则它将修改与您要附加的列表共享节点的其他列表至。这就是:::占用线性时间的原因。 Scala有一个名为ListBuffer的数据结构,您可以使用它来代替:::运算符,以便更快地追加到列表的末尾。基本上,您创建一个新的ListBuffer,它以一个空列表开头。 ListBuffer维护一个完全独立于程序知道的任何其他列表的列表,因此可以通过添加到最后来修改它。当您完成添加到最后时,您调用ListBuffer.toList,将列表发布到世界中,此时您无法再将其添加到最终而不复制它。

foldLeftfoldRight也有类似的不对称性。 foldRight要求您遍历整个列表以到达列表的末尾,并跟踪您在途中访问过的所有位置,以便您以相反的顺序访问它们。这通常是递归完成的,它可能导致foldRight导致大型列表上的堆栈溢出。另一方面,foldLeft按照它们在列表中出现的顺序处理节点,因此它可以忘记已经访问过的节点,并且一次只需要知道一个节点。虽然foldLeft通常也是递归实现的,但它可以利用名为尾递归消除的优化,其中编译器将递归调用转换为循环,因为函数不执行任何操作从递归调用返回后。因此,即使在很长的列表中,foldLeft也不会溢出堆栈。 编辑:Scala 2.8中的foldRight实际上是通过反转列表并在反向列表上运行foldLeft来实现的 - 因此尾递归问题不是问题 - 两个数据结构正确地优化尾递归,你可以选择其中任何一个(你现在就reverse定义reverse时遇到了问题 - 你不需要担心,如果你'为了它的乐趣,重新定义你的自己的反向方法,但如果你定义了Scala的反向方法,根本就没有foldRight选项。)

因此,您应该更喜欢foldLeft::而不是foldRight:::

(在将foldLeft:::foldRight::合并的算法中,您需要自己决定哪个更重要:堆栈空间或运行时间。或者您应该foldLeft使用ListBuffer。)