10.2.1 用尾递归避免栈溢出(续!)

[

na?ve

不像是英语,不知道什么意思。

]

第六章中的列表处理函数并不是尾递归。如果我们传递很大的列表,就会因栈溢出而失败。我们将用尾递归重写两个函数(map 和 filter),将改正这个问题。为了对照,在清单 10.8 中包括了原来的实现。为了避免名字冲突,已经改名为 mapN 和 filterN。

清单10.8 na?ve 列表处理函数 (F#)

// Na?ve ‘map‘ implementation

let rec mapN f list =

match list with

| [] –> []

| x::xs -> let xs = (mapN fxs)     [1]

f(x) :: xs                   [2]

// Na?ve ‘filter‘ implementation

let rec filterN f list =

match list with

| [] –> []

| x::xs -> let xs = (filterN fxs)    [1]

if f(x) then x::xs else xs       [2]

两个函数都包含一个递归调用[2],但不是尾递归;递归调用每个分支都跟关一个额外的操作[2]。一般的模式是,函数首先把列表分解成头和尾;然后,递归处理尾,用头执行一些动作。更确切地说,mapN 应用 f 函数到头中的值,filterN 决定头中的值是否应包括在结果列表中。最后的操作是把新的头中的值(或者筛选分支中没有值)追加到递归处理的尾中,且在递归调用后必须处理。

要把这些函数变成尾递归,使用前面曾看到的、同样的累加器参数方法。在遍历列表时,收集元素(筛选或映射),并把它们存放在累加器中;一旦到达终点,就可以返回已经收集到的元素。清单 10.9 显示了映射和筛选的尾递归实现。

清单10.9 尾递归列表处理函数 (F#)

// Tail-recursive ‘map‘implementation

let map f list =

let rec map‘ f list acc =

match list with

| [] ->List.rev(acc)          [1]

| x::xs -> let acc =f(x)::acc       [2]

map‘f xs acc        [3]

map‘ f list []

// Tail-recursive ‘filter‘implementation

let filter f list =

let rec filter‘ f list acc=

match list with

| [] ->List.rev(acc)      [1]

| x::xs -> let acc =if f(x) then x::acc else acc    [2]

filter‘f xs acc    [3]

filter‘ f list []

像通常实现尾递归函数一样,这两个函数都包含本地的工具函数,有一个额外的累加器参数。这一次,我们在函数名上增加了一个单引号(‘),起初看起来有点怪。F# 把单引号当作标准的字符,可以在名称中使用,因此,也没有什么神奇的事情。

我们首先看一下终止递归的分支[1],我们说是仅返回收集到的元素,但实际上先是调用 List.rev,倒转元素的顺序。这是因为我们收集到的元素顺序是“错误的”。添加到累加器列表中的元素总是在前面,成为新的头,所以,最终我们处理的第一个元素,是累加器中的最后一个元素。调用 List.rev 函数倒转列表,这样,最终返回结果的顺序就正确了。这种方法比我们将在 10.2.2 节中见到的,追加元素到尾部,更有效率。

现在,处理 cons cell 的分支是尾递归。第一步处理头中的元素,并更新累加器[2],生成递归调用[3],立即返回结果。F# 编译器可以知道递归调用是最后一步,可以采用尾递归优化。

如果我们将它们粘贴到 F# Interactive  中,尝试处理大列表,很容易发现两个版本之间的区别。对于这些函数,递归深度与列表的长度相同,所以,如果我们用na?ve 版本,就会遇到问题:

> let large = [ 1 .. 100000 ]

val large : int list = [ 1; 2; 3; 4; 5;...]

> large |> map (fun n ->n*n);;

val it : int list = [1; 4; 9; 16; 25; ...]

> large |> mapN (fun n ->n*n);;

Process is terminated due toStackOverflowException.

可以发现,对于递归处理函数来说,尾递归是一项重要技术。当然,F# 库包含了处理列表的尾递归函数,所以,并不真的要自己写像在这里实现的映射和筛选。在第六、七和八章,我们已经看到,设计自己的数据结构,写函数来处理它们,是函数编程的关键。

将要创建的许多数据结构,都相当小,但是,而处理的数据量相当大,尾递归是一项重要技术。使用尾递归,可以写出能够正常处理大型数据集的代码。当然,正因为函数不会栈溢出,不能保证函数在合理的时间内完成任务,这就是为什么还需要考虑更有效地处理列表的原因。

时间: 2024-12-13 05:04:48

10.2.1 用尾递归避免栈溢出(续!)的相关文章

通过尾递归避免栈溢出

JavaScript中的递归即函数内调用函数自身,但递归是非常耗内存的,每一次调用都会分配一定的栈空间,达到一定的数量(具体看浏览器)便会溢出报错. function recursion (num) { if (num === 1) { return 1; } return num + recursion(--num); } console.log(recursion(5)); // 15 console.log(recursion(1000)); // 500500 console.log(r

有用函数编程

<序> 感谢 关于本书 关于封面 第一部分 学习函数式思维 第一章 不同的思维 1.1 什么是函数式编程? 1.2 通往有用函数编程之路 1.3 用函数式编程提高生产力 1.3.1 函数范式 1.3.2 声明式编程风格 1.3.3 了解程序的执行 1.3.4 设计并发友好的应用程序 1.3.5 函数风格怎样形成代码 1.4 函数式编程演示样例 1.4.1 用声明式风格表达意图 1.4.1.1 用 LINQ 处理数据 1.4.1.2 用 XAML 描写叙述用户界面 1.4.1.3 声明式函数动画

初学Python(九)——函数

初学Python(九)--函数 初学Python,主要整理一些学习到的知识点,这次是函数. 函数定义: # -*- coding:utf-8 -*- #函数的定义 def my_function(x): if x>0: return x elif x<0: return -x else: pass #函数的调用 a = my_function(-1) b = my_function(2) c = my_function(0) print a,b,c #空函数,pass为占位符 def empt

python学习之路(10)--难点

递归函数 在函数内部,可以调用其他函数.如果一个函数在内部调用自身本身,这个函数就是递归函数. 举个例子,我们来计算阶乘n! = 1 x 2 x 3 x ... x n,用函数fact(n)表示,可以看出: fact(n) = n! = 1 x 2 x 3 x ... x (n-1) x n = (n-1)! x n = fact(n-1) x n 所以,fact(n)可以表示为n x fact(n-1),只有n=1时需要特殊处理. 于是,fact(n)用递归的方式写出来就是: def fact

栈溢出笔记1.2 覆盖EIP

1.1节中我们说到可以利用栈溢出来破坏栈中原有的内容,这一节中,我们就来看看如何争夺到返回地址(EIP),使得我们可以随意控制它的值,这样我们就可以控制程序.来看一个经典的程序: 这个程序的get_print函数中定义了一个大小为11个字节的数组,正常情况下我们的输入应该最多为10个字符(还有一个\0结束符),而gets函数没有明确定义输入的大小,因此,我们可以输入超过10个字符,从而造成栈溢出.如下,输入10个'A',一切正常: 图8 当我输入11个'A'时,虽然顺利打印出来11个'A',但是

Java 递归、尾递归、非递归、栈 处理 三角数问题

import java.io.BufferedReader; import java.io.InputStreamReader; //1,3,6,10,15...n 三角数 /* * # 1 * ## 1+2 * ### 1+2+3 * #### 1+2+3+4 * ##### 1+2+3+4+5 * ...第1层为1, 第n层等于 n + (f(n-1)) */ public class TriangleNumber { static int triangle(int n) { if (n <

10分钟学会 Python 函数基础知识

一.函数基础 简单地说,一个函数就是一组Python语句的组合,它们可以在程序中运行一次或多次运行.Python中的函数在其他语言中也叫做过程或子例程,那么这些被包装起来的语句通过一个函数名称来调用. 有了函数,我们可以在很大程度上减少复制及粘贴代码的次数了(相信很多人在刚开始时都有这样的体验).我们可以把相同的代码可以提炼出来做成一个函数,在需要的地方只需要调用即可.那么,这样就提高了代码的复用率了,整体代码看起来比较简练,没有那么臃肿了. 函数在Python中是最基本的程序结构,用来最大化地

10分钟学会Python函数基础知识

看完本文大概需要8分钟,看完后,仔细看下代码,认真回一下,函数基本知识就OK了.最好还是把代码敲一下. 一.函数基础 简单地说,一个函数就是一组Python语句的组合,它们可以在程序中运行一次或多次运行.Python中的函数在其他语言中也叫做过程或子例程,那么这些被包装起来的语句通过一个函数名称来调用. 有了函数,我们可以在很大程度上减少复制及粘贴代码的次数了(相信很多人在刚开始时都有这样的体验).我们可以把相同的代码可以提炼出来做成一个函数,在需要的地方只需要调用即可.那么,这样就提高了代码的

python基础之函数

python 函数 函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段. 函数能提高应用的模块性,和代码的重复利用率.你已经知道Python提供了许多内建函数,比如print().但你也可以自己创建函数,这被叫做用户自定义函数. 定义一个函数 在Python中,定义一个函数要使用def语句,依次写出函数名.括号.括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回. 函数式编程和面向过程编程的区别: 函数式:将某功能代码封装到函数中,日后便无需重复编