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

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


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

 

Listing 10.8 Na?ve list processing functions (F#)

 

// Na?ve 'map' implementation
let rec mapN f list =
  match list with
  | [] �C> []
  | x::xs -> let xs = (mapN f xs)
                 f(x) :: xs

// Na?ve 'filter' implementation 
let rec filterN f list =
  match list with
  | [] �C> []
  | x::xs -> let xs = (filterN f xs)
                 if f(x) then x::xs else xs

 

    这两个函数都包含一个单独的递归调用,并不是尾递归。在每个递归调用都跟关一个额外的操作。一般情况,该函数首先把列表分解成一个头和一个尾。然后,递归处理尾,用头执行一些动作。更确切地说,mapN 应用 f 函数到头中的值,filterN 决定头中的值是否应包括在结果列表中。最后的操作是追加新的头中的值(筛选的情况下可能没有价值)到递归处理的尾中,在递归调用后必须处理。

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

 

Listing 10.9 Tail recursive list processing functions (F#)

 

// Tail-recursive 'map' implementation
let map f list =
  let rec map' f list acc =
    match list with
    | [] -> List.rev(acc)
    | x::xs -> let acc = f(x)::acc 
                   map' f xs acc
    map' f list []

// Tail-recursive 'filter' implementation
let filter f list = 
  let rec filter' f list acc =
    match list with
    | [] -> List.rev(acc)
    | x::xs -> let acc = if f(x) then x::acc else acc 
                   filter' f xs acc
    filter' f list []

 

    像往常实现尾递归函数时一样,这两个函数都包含一个本地工具函数,有一个额外的累加器参数。这一次,我们给函数增加了一个单引号('),这起初看起来可能会奇怪。 F# 处理这种单引号作为一个标准的字符,可以在名称中使用,所以,没有什么神奇的事情。

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

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

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

 

> 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 to StackOverflowException.

 

    正如你看到的,对于递归处理函数来说,尾递归是一项重要技术。当然,在 F# 库中,包含了处理列表的尾递归函数,所以,你不必真正要写自己的映射和筛选,像我们在这里实现的。在第六、七和八章中,我们看到,设计自己的数据结构,写函数来处理它们,是函数编程的关键。 
    你将要创建的许多数据结构,都相当小,但是,当处理数据时,尾递归是一项重要技术。使用尾递归,我们就可以写出能够正常处理大型数据集的代码。当然,仅仅因为一个函数不会堆栈溢出,并不意味着它就能在一个合理的时间内完成任务,这就是为什么我们还需要考虑如何更有效地处理列表。

你可能感兴趣的:(职场,堆栈,尾递归,休闲,堆栈溢出)