【Scala笔记——道】Scala List 遍历 foldLeft / foldRight详解

HOF foldLeft / foldRight

foldLeft 和 foldRight 都是对于 List 遍历的 高阶函数。是对列表遍历过程中进行函数操作的高阶函数抽象。

List 遍历

假设有两个方法如下

    // 求和
    def sum(ints: List[Int]): Int = ints match  {
      case Nil => 0
      case Cons(x, xs) => x + sum(xs)
    }

    //阶乘
    def product(ds: List[Double]): Double = ds match {
      case Nil => 1.0
      case Cons(x, xs) => x *  product(xs)
    }

可以看到这两个方法体很相似,基本可以概括为

case Nil => #ReturnBack#
case Cons(head, tail) => #递归遍历 + function 操作#

foldRight

定义输入 参数如下

li : List [A]
b: Nil返回结果
f : ( a : A, b: B) => B  函数操作

将这种方式进行抽象为如下流程
foldLeft简单的 流程可以理解为如下

  1. 从列表末尾开始执行函数操作 f, 具体 为 f0 = f ( li [li.lenth -1] , b), 此时 f0 为改末尾节点执行函数结果
  2. 在列表上一节点执行函数, 此时 的b 为一节点的函数执行结果,具体为 f1 = f ( li [li.lenth -2] , b = f0)
  3. 一直递归到列表头

具体实现如下

def foldRight[A, B](li : List[A], b : B)(f: (A, B)=> B) : B = li match {
    case Nil => b
    case Cons(head, tail) => f(head, foldRight(tail, b)(f))
  }

foladRight 实际上是先对列表进行递归,并将每个节点执行的方法先压入栈中,到最后拿到 b 后再从栈中一步恢复执行。

foldRight示意图

foldLeft

foladRight中我们的递归操作实际上是依靠栈实现的,但这就会造成一个问题:在列表过大时,会造成栈溢出

如何解决 栈溢出 的问题?我们比较容易想到的一个办法是 尾递归进行优化

foldRight 相当于从尾部进行递归操作,而栈的引入主要是保存 首次递归到尾部时,之前节点执行 函数操作的状态。

这就是foldLeft 的主要思想
foldLeft示意图

具体实现如下

def foldLeft[A, B](li : List[A], b: B)(f: (B, A)=> B) : B = li match {
    case Nil => b
    case Cons(head, tail) => foldLeft(tail, f(b, head))(f)
  }

实际上foldLeft其实是可以通过尾递归进行优化,因此foldLeft在大List情况下,并不会产生栈溢出情况

再看foldRight和foldLeft

再回到最初的程序,具体的foladRight和foldLeft实现如下

    def sumFoldRight(ns: List[Int]) : Int ={
      foldRight(ns, 0)((x, y) => x + y)
    }

    def producetFoldRight(ns : List[Double]): Double = {
      foldRight(ns, 1.0)(_*_)
    }

    def sumFoldLeft(ns: List[Int]): Int = {
      foldLeft(ns, 0)(_+_)
    }

    def produceFoldLeft(ns: List[Double]): Double = {
      foldLeft(ns, 1.0)(_*_)
    }

看起来 foldRight 和 foldLeft 对于这两种方法实现都是没有问题,但是实际上 foldRight 和 foldLeft 实际上是不同的。

foldRight 的从头遍历实际上我们理解的从末尾开始执行函数,但foldLeft实际上并不是,我们在foldLeft计算时实际上是把List的头当作尾部计算。

这里会带来一个问题相同的 执行函数 在foldLeft和 foldRight可能执行结果并不同。这两者相同的条件相当于我们从头开始函数计算或者从未开始函数计算对结果并没有影响。由于 加法和乘法都满足交换率,因此在这里是没有问题的。换句话说,foldLeft并不能直接替换foldRight,除非操作函数是幂等的

这里我们写一个 int2String方法

  def int2StringFoldRight[A](as: List[A]): String = {
    foldRight(as, "-")(_+_)
  }

  def int2StringFoldLeft[A](as: List[A]): String = {
    foldLeft(as, "-")(_+_)
  }

 def main(args: Array[String]): Unit = {

    val list = Cons(1, Cons(2, Cons(3, Cons(4, Cons(5, Cons(6, Nil))))))
    println(int2StringFoldRight(list))
    println("dropLastFoldRight
")

    println(int2StringFoldLeft(list))
    println("int2StringFoldLeft
")
  }

执行结果如下

123456-
dropLastFoldRight

-123456
int2StringFoldLeft

这样很明显就可以看出才非幂等函数里边,foldRight 和 foldLeft 可以进行替换,并且建议使用 foldLeft 通过尾递归进行优化,但对于非幂等函数,需要慎重使用foldLeft,更建议使用foldRight。

foldLeft适用场景

  • 大表
  • 幂等

foldRight适用场景

  • 小表
  • 非幂等
原文地址:https://www.cnblogs.com/cunchen/p/9464097.html