以递归方式思考

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

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. spark安装(实战)

    sparksql+hive :http://lxw1234.com/archives/2015/06/294.htm 1,安装scala http://scala-lang.org/download/ ...

  2. 开放数据库互联ODBC配置(odbcconf)

    开放数据库互连(ODBC)是微软引进的一种早期数据库接口技术,通过ODBC驱动程序可访问数据库数据:使用ODBC管理器可以完成对数据库的链接操作.笔者利用ODBC接口,将WINDOWS计数器信息写入到 ...

  3. PHP数据采集curl常用的5个例子

    用php ,curl主要是抓取数据,当然我们可以用其他的方法来抓取,比如fsockopen,file_get_contents等.但是只能抓那些能直接访问的页面,如果要抓取有页面访问控制的页面,或者是 ...

  4. 关于Azure带宽的测试

    以前见客户经常会碰到一些客户问我们你们Azure的带宽是多少,每次回答这个问题我们只能含糊地告诉客户一个大概数值,这样就会留给客户一个认为我们很不专业的印象,其实站在客户的角度我们也能理解,连这样的一 ...

  5. ASP.NET Web API学习 (一)

    开发环境:win10,使用VS2015社区版和SQLSERVER2012开发 1.打开VS2015应用程序,点击左上角按钮:文件--新建--项目,弹出窗口中选择ASP.NET Web应用程序, 2.点 ...

  6. python3 爬虫

    保存当前cookie到本地 import urllib.request as ur import http.cookiejar as hc url='http://www.xxxx.com/admin ...

  7. jstl foreach标签

    forEach标签 forEach标签用来循环. 属性: * var :定义循环变量 * begin :从哪开始 * end :到哪结束 * step :递增 * items :遍历的内容 * var ...

  8. 命令行环境下简单实用的工具——重定向&管道

    如果你对管道和重定向应用自如了,无需继续往下看.本文虽然以windows上cmd命令行环境演示,但同样适用于Unix/Linux等平台. 引言 关于管道和重定向,最初是在刘汝佳的<算法竞赛入门经 ...

  9. 3.EasyUI学习总结(三)——easyloader源码分析

    easyloader模块是用来加载jquery easyui的js和css文件的,即easyloader可以在调用的时候自动加载当前页面所需的文件,不用再自己引用, 而且它可以分析模块的依赖关系,先加 ...

  10. ASM:《X86汇编语言-从实模式到保护模式》第9章:实模式下中断机制和实时时钟

    中断是处理器一个非常重要的工作机制.第9章是讲中断在实模式下如何工作,第17章是讲中断在保护模式下如何工作. ★PART1:外部硬件中断 外部硬件中断是通过两个信号线引入处理器内部的,这两条线分别叫N ...