为什么即使使用延续传递样式,遍历大型二叉树也会导致堆栈溢出?

Wol*_*sch 6 mono f# binary-tree tail-recursion continuation-passing

专家F#3.0一书的第9章介绍了如何在遍历二叉树时使用延续传递样式来避免堆栈溢出.我编写的树遍历代码几乎与本书中的代码完全相同,但我仍然得到了堆栈溢出.我的代码如下:

type 'a Tree =
  | Leaf   of 'a
  | Branch of 'a Tree * 'a Tree

let rec mkLeftLeaningTree n tree =
  if n = 0 then
    tree
  else
    Branch (mkLeftLeaningTree (n - 1) tree, Leaf "right")

let leftLeaningTree1 = Leaf "left"
let leftLeaningTree2 = mkLeftLeaningTree 30000 leftLeaningTree1
let leftLeaningTree3 = mkLeftLeaningTree 30000 leftLeaningTree2
let leftLeaningTree4 = mkLeftLeaningTree 30000 leftLeaningTree3
let leftLeaningTree5 = mkLeftLeaningTree 30000 leftLeaningTree4
let leftLeaningTree6 = mkLeftLeaningTree 30000 leftLeaningTree5

let sizeContAcc tree =
  let rec worker acc tree cont =
    match tree with
      | Leaf _               -> cont (acc + 1)
      | Branch (left, right) -> worker acc left  (fun acc ->
                                worker acc right cont)
  worker 0 tree id
Run Code Online (Sandbox Code Playgroud)

将其加载到F#交互式环境中并评估表达式sizeContAcc leftLeaningTree6会使堆栈溢出.为什么是这样?

Nes*_*ure 1

我的第一个猜测是,您在 cont 参数中将函数堆叠在一起,我将其理解为可能溢出的堆栈(而正如 Wolfgang 在评论中解释的那样,它是一个堆),但我使用 cont 做了一些测试参数并且它没有导致任何堆栈溢出。相反,我的速度明显变慢,最终达到了内存溢出。

解决您的特定问题的方法是将您需要探索的树显式存储在列表中(您仍然会受到列表最大大小的限制):

let sizeContAcc tree =
  let rec worker acc tree contList =
    match tree with
      | Leaf _ -> 
        match contList with
        | [] -> acc+1
        | t::cl -> worker (acc+1) t cl
      | Branch (left, right) -> worker acc left (right::contList)
  worker 0 tree []
Run Code Online (Sandbox Code Playgroud)

它有效并立即为我提供了 leftLeaningTree6 的 150001 。

  • “堆栈溢出”中的术语“堆栈”并不指代 F# 程序中的任何堆栈结构。它指的是系统堆栈。F# 程序在内部使用堆栈和堆。当我在代码中构建某种函数堆栈时,此嵌套函数表达式是在堆上构建的(比堆栈大);所以它不应该导致系统堆栈溢出。您的示例中的“overflower”和“nonOverflower”本质上是不同的。前者经常以指数方式应用函数“f”,后者仅经常以线性方式应用。 (2认同)