以递归方式思考

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

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
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

【Scala】尾递归优化的更多相关文章

  1. Scala进阶之路-尾递归优化

    Scala进阶之路-尾递归优化 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 递归调用有时候能被转换成循环,这样能节约栈空间.在函数式编程中,这是很重要的,我们通常会使用递归方法来 ...

  2. Scala尾递归

    递归函数应用 首先,我们来对比两个递归方法的求值步骤. 假设有方法gcd,用来计算两个数的最大公约数.下面是欧几里得算法的实现: def gcp(a: Int, b: Int): Int = if ( ...

  3. .NET 4.6的RyuJIT尾递归优化的Bug

    今天看到园子里有一篇新闻稿.NET 4.6的RyuJIT编译器中发现严重的Bug提到,在.Net 4.6的x64程序中默认启用新的JIT程序RyuJIT在处理尾递归指令的时候有一个Bug,导致无法得到 ...

  4. 对SNL语言的解释器实现尾递归优化

    对于SNL语言解释器的内容可以参考我的前一篇文章<使用antlr4及java实现snl语言的解释器>.此文只讲一下"尾递归优化"是如何实现的--"尾递归优化& ...

  5. Python尾递归优化

    Python开启尾递归优化 cpython本身不支持尾递归优化, 但是一个牛人想出的解决办法:实现一个 tail_call_optimized 装饰器 #!/usr/bin/env python2.4 ...

  6. kotlin递归&尾递归优化

    递归: 对于递归最经典的应用当然就是阶乘的计算啦,所以下面用kotlin来用递归实现阶乘的计算: 编译运行: 那如果想看100的阶乘是多少呢? 应该是结果数超出了Int的表述范围,那改成Long型再试 ...

  7. Py编程方法,尾递归优化,map函数,filter函数,reduce函数

    函数式编程 1.面向过程 把大的问题分解成流程,按照流程来编写过程 2.面向函数 面向函数编程=编程语言定义的函数+数学意义上的函数先弄出数学意义上的方程式,再用编程方法编写这个数学方程式注意面向函数 ...

  8. Scala Tail Recursion (尾递归)

    Scala对尾递归进行了优化,甚至提供了专门的标注告诉编译器需要进行尾递归优化.不过这种优化仅限于严格的尾递归,间接递归等情况,不会被优化. 尾递归的概念 递归,大家都不陌生,一个函数直接或间接的调用 ...

  9. 探索c#之尾递归编译器优化

    阅读目录: 递归运用 尾递归优化 编译器优化 递归运用 一个函数直接或间接的调用自身,这个函数即可叫做递归函数. 递归主要功能是把问题转换成较小规模的子问题,以子问题的解去逐渐逼近最终结果. 递归最重 ...

随机推荐

  1. Linux C 字符串函数 sprintf()、snprintf() 详解

    一.sprintf() 函数详解 在将各种类 型的数据构造成字符串时,sprintf 的强大功能很少会让你失望. 由于 sprintf 跟 printf 在用法上几乎一样,只是打印的目的地不同而已,前 ...

  2. Nginx反向代理+keepalived

    环境两台LB(nginx).两台web(nginx/apache都行) 安装httpd web01 [root@web01 /]# /etc/init.d/iptables stop iptables ...

  3. word20161215

    name / 名称 name mapping / 名称映射 name resolution / 名称解析 name server (NS) resource record / 名称服务器资源记录 na ...

  4. 【转】Spring mvc集成ZBUS--轻量级MQ、RPC、服务总线

    本文转自:http://www.cnblogs.com/top15from/p/4899954.html ZBUS = MQ + RPC + PROXY 支持消息队列, 发布订阅, RPC, 代理(T ...

  5. ASM:《X86汇编语言-从实模式到保护模式》第17章:保护模式下中断和异常的处理与抢占式多任务

    ★PART1:中断和异常概述 1. 中断(Interrupt) 中断包括硬件中断和软中断.硬件中断是由外围设备发出的中断信号引发的,以请求处理器提供服务.当I/O接口发出中断请求的时候,会被像8259 ...

  6. JavaScript高级程序设计学习笔记--高级技巧

    惰性载入函数 因为浏览器之间行为的差异,多数JavaScript代码包含了大量的if语句,将执行引导到正确的代码中,看看下面来自上一章的createXHR()函数. function createXH ...

  7. resultMap 映射

    1. sql的重用:定义一个sql片段,可在任何SQL语句中重用该片段. <sql id="personColumns"> name, sex, updateTime& ...

  8. MapReduce类型与格式(输入与输出)

    一.输入格式 (1)输入分片记录 ①JobClient通过指定的输入文件的格式来生成数据分片InputSplit: ②一个分片不是数据本身,而是可分片数据的引用: ③InputFormat接口负责生成 ...

  9. C#学习笔记---Dispose(),Finalize(),SuppressFinalize

    http://www.cnblogs.com/eddyshn/archive/2009/08/19/1549961.html 在.NET的对象中实际上有两个用于释放资源的函数:Dispose和Fina ...

  10. <转>[WinForm] VS2010发布、打包安装程序(超全超详细)

    1. 在vs2010 选择“新建项目”→“ 其他项目类型”→“ Visual Studio Installer→“安装项目”: 命名为:Setup1 . 这是在VS2010中将有三个文件夹, 1.“应 ...