码迷,mamicode.com
首页 > 其他好文 > 详细

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

时间:2014-12-30 17:15:25      阅读:182      评论:0      收藏:0      [点我收藏+]

标签:f#   函数编程   实用函数编程   尾递归   列表   

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# 库包含了处理列表的尾递归函数,所以,并不真的要自己写像在这里实现的映射和筛选。在第六、七和八章,我们已经看到,设计自己的数据结构,写函数来处理它们,是函数编程的关键。

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

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

标签:f#   函数编程   实用函数编程   尾递归   列表   

原文地址:http://blog.csdn.net/hadstj/article/details/42267011

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!