zoukankan      html  css  js  c++  java
  • 【Scala】尾递归优化

    以递归方式思考

    递归通过灵巧的函数定义,告诉计算机做什么。在函数式编程中,随处可见递归思想的运用。
    下面给出几个递归函数的例子:

    object RecursiveExample extends App{
      // 数列求和例子
      def sum(xs: List[Int]): Int =
        if (xs.isEmpty)
          1
        else
          xs.head + sum(xs.tail)
    
    
      // 求最大值例子
      def max(xs: List[Int]): Int =
        if (xs.isEmpty)
          throw new NoSuchElementException
        else if (xs.size == 1)// 递归的边界条件
          xs.head
        else
          if (xs.head > max(xs.tail)) xs.head else max(xs.tail)
    
    
      // 翻转字符串
      def str_reverse(xs: String): String =
        if (xs.length == 1)
          xs
        else
          str_reverse(xs.tail) + xs.head
    
      // 快速排序例子
      def quicksort(ls: List[Int]): List[Int] = {
        if (ls.isEmpty)
          ls
        else
          quicksort(ls.filter(_ < ls.head)) ::: ls.head :: quicksort(ls.filter(_ > ls.head))
          //quicksort(ls.filter(x =>  x < ls.head)) ::: ls.head :: quicksort(ls.filter(x => x > ls.head))
      }
    }

    我们以上面代码最后一个快速排序函数为例,使用递归的方式,其代码实现非常的简洁和通俗易懂。递归函数的核心是设计好递归表达式,并且确定算法的边界条件。上面的快速排序中,认为空列表就是排好序的列表,这就是递归的边界条件,这个条件是递归终止的标志。

    尾递归

    递归算法需要保持调用堆栈,效率较低,如果调用次数较多,会耗尽内存或栈溢出。然而,尾递归可以克服这一缺点。
    尾递归是指递归调用是函数的最后一个语句,而且其结果被直接返回,这是一类特殊的递归调用。由于递归结果总是直接返回,尾递归比较方便转换为循环,因此编译器容易对它进行优化。

    递归求阶乘的经典例子

    普通递归求解的代码如下:

    def factorial(n: BigInt): BigInt = {
      if (n <= 1)
        1
      else
        n * factorial(n-1)
    }

    上面的代码,由于每次递归调用n-1的阶乘时,都有一次额外的乘法计算,这使得堆栈中的数据都需要保留。在新的递归中要分配新的函数栈。
    运行过程就像这样:

    factorial(4)
    --------------
    4 * factorial(3)
    4 * (3 * factorial(2))
    4 * (3 * (2 * factorial(1)))
    4 * (3 * (2 * 1))

    而下面是一个尾递归版本,在效率上,和循环是等价的:

    import scala.annotation.tailrec
    
    def factorialTailRecursive(n: BigInt): BigInt = {
      @tailrec
      def _loop(acc: BigInt, n: BigInt): BigInt =
        if(n <= 1) acc else _loop(acc*n, n-1)
    
      _loop(1, n)
    }

    这里的运行过程如下:

    factorialTailRecursive(4)
    --------------------------
    _loop(1, 4)
    _loop(4, 3)
    _loop(12, 2)
    _loop(24, 1)

    该函数中的_loop在最后一步,要么返回递归边界条件的值,要么调用递归函数本身。
    改写成尾递归版本的关键:
    尾递归版本最重要的就是找到合适的累加器,该累加器可以保留最后一次递归调用留在堆栈中的数据,积累之前调用的结果,这样堆栈数据就可以被丢弃,当前的函数栈可以被重复利用。
    在这个例子中,变量acc就是累加器,每次递归调用都会更新该变量,直到递归边界条件满足时返回该值。
    对于尾递归,Scala语言特别增加了一个注释@tailrec,该注释可以确保程序员写出的程序是正确的尾递归程序,如果由于疏忽大意,写出的不是一个尾递归程序,则编译器会报告一个编译错误,提醒程序员修改自己的代码。

    菲波那切数列的例子

    原始的代码很简单:

    def fibonacci(n: Int): Int =
      if (n <= 2)
        1
      else
        fibonacci(n-1) + fibonacci(n-2)

    尾递归版本用了两个累加器,一个保存较小的项acc1,另一个保存较大项acc2:

    def fibonacciTailRecursive(n: Int): Int = {
      @tailrec
      def _loop(n: Int, acc1: Int, acc2: Int): Int =
        if(n <= 2)
          acc2
        else
          _loop(n-1, acc2, acc1+acc2)
    
      _loop(n, 1, 1)
    }

    几个列表操作中使用尾递归的例子

    求列表的长度

    def lengthTailRecursive[A](ls: List[A]): Int = {
      @tailrec
      def lengthR(result: Int, curList: List[A]): Int = curList match {
        case Nil => result
        case _ :: tail => lengthR(result+1, tail)
      }
      lengthR(0, ls)
    }

    翻转列表

    def reverseTailRecursive[A](ls: List[A]): List[A] = {
      @tailrec
      def reverseR(result: List[A], curList: List[A]): List[A] = curList match {
        case Nil        => result
        case h :: tail  => reverseR(h :: result, tail)
      }
      reverseR(Nil, ls)
    }

    去除列表中多个重复的元素

    这里要求去除列表中多个连续的字符,只保留其中的一个。

    // If a list contains repeated elements they should be replaced with
    // a single copy of the element.
    // The order of the elements should not be changed.
    // Example:
    // >> compress(List('a, 'a, 'a, 'a, 'b, 'c, 'c, 'a, 'a, 'd, 'e, 'e, 'e, 'e))
    // >> List('a, 'b, 'c, 'a, 'd, 'e)
    
    def compressTailRecursive[A](ls: List[A]): List[A] = {
      @tailrec
      def compressR(result: List[A], curList: List[A]): List[A] = curList match {
        case h :: tail  => compressR(h :: result, tail.dropWhile(_ == h))
        case Nil        => result.reverse
      }
      compressR(Nil, ls)
    }

    转载请注明作者Jason Ding及其出处
    Github博客主页(http://jasonding1354.github.io/)
    GitCafe博客主页(http://jasonding1354.gitcafe.io/)
    CSDN博客(http://blog.csdn.net/jasonding1354)
    简书主页(http://www.jianshu.com/users/2bd9b48f6ea8/latest_articles)
    **Google搜索jasonding1354进入我的博客主页



    文/JasonDing(简书作者)
    原文链接:http://www.jianshu.com/p/d177c30f59de
    著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
  • 相关阅读:
    关于软件生命周期以及软件过程
    尴尬!Jsp内置对象
    软件工程导论的课后作业
    Bing Maps进阶系列五:通过DeepEarth的MiniMap控件为Bing Maps扩展迷你小地图
    Bing Maps进阶系列四:路由功能服务(RouteService)
    Bing Maps进阶系列三:使用地图图像服务(ImageryService)
    Bing Maps进阶系列二:使用GeocodeService进行地理位置检索
    Bing Maps进阶系列一:初识Bing Maps地图服务
    在C#中实现listbox的项上下移动(winform) 标准
    C# LIstbox 解决WinForm下ListBox控件“设置DataSource属性后无法修改项集合”的问题
  • 原文地址:https://www.cnblogs.com/suanec/p/5941318.html
Copyright © 2011-2022 走看看