Tree 在Ocaml中查找树深度的尾部递归函数

Tree 在Ocaml中查找树深度的尾部递归函数,tree,functional-programming,ocaml,binary-tree,Tree,Functional Programming,Ocaml,Binary Tree,我有一个类型树,定义如下 type 'a tree = Leaf of 'a | Node of 'a * 'a tree * 'a tree ;; let rec depth = function | Leaf x -> 0 | Node(_,left,right) -> 1 + (max (depth left) (depth right)) ;; 我有一个函数可以找到树的深度,如下所示 type 'a tree = Leaf of 'a | Node of

我有一个类型
,定义如下

type 'a tree = Leaf of 'a | Node of 'a * 'a tree * 'a tree ;;
let rec depth = function 
    | Leaf x -> 0
    | Node(_,left,right) -> 1 + (max (depth left) (depth right))
;;
我有一个函数可以找到树的深度,如下所示

type 'a tree = Leaf of 'a | Node of 'a * 'a tree * 'a tree ;;
let rec depth = function 
    | Leaf x -> 0
    | Node(_,left,right) -> 1 + (max (depth left) (depth right))
;;

此函数不是尾部递归函数。有没有一种方法可以让我用尾部递归的方式编写这个函数

您可以通过将函数转换为CPS(延续传递样式)来实现这一点。其思想是,不要调用
depth left
,然后根据这个结果计算东西,而是调用
depth left(fun dleft->…)
,其中第二个参数是“一旦结果(
dleft
)可用,计算什么”

这是一个众所周知的技巧,可以使任何函数尾部递归。瞧,这是tail-rec

下一个众所周知的诀窍是使CPS结果“失效”。continuations(continuations)(
(fun-dleft->…)
部分)作为函数的表示形式很简洁,但您可能希望看到它作为数据的样子。因此,我们用一个数据类型的具体构造函数替换每个闭包,该构造函数捕获其中使用的自由变量

这里我们有三个连续闭包:
(fun-dleft->depth-right(fun-dright->k…)
,它只重用环境变量
right
k
(fun-dright->…)
,它重用
k
和现在可用的左结果
dleft
,以及
(fun-d->d
,最初的计算没有捕获任何东西

type ('a, 'b) cont =
  | Kleft of 'a tree * ('a, 'b) cont (* right and k *)
  | Kright of 'b * ('a, 'b) cont     (* dleft and k *)
  | Kid
失效函数如下所示:

let depth tree =
  let rec depth tree k = match tree with
    | Leaf x -> eval k 0
    | Node(_,left,right) ->
      depth left (Kleft(right, k))
  and eval k d = match k with
    | Kleft(right, k) ->
      depth right (Kright(d, k))
    | Kright(dleft, k) ->
      eval k (1 + max d dleft)
    | Kid -> d
  in depth tree Kid
;;
我没有构建一个函数
k
并将其应用于叶子(
k0
),而是构建了一个类型为
('a,int)cont
)的数据,需要稍后对其进行
评估以计算结果
eval
,当它传递一个
Kleft
时,执行闭包
(fun dleft->…)
所做的操作,即它递归地调用右侧子树上的
深度<代码>评估
和深度
是相互递归的

现在仔细看看
('a,'b)cont
,这个数据类型是什么?这是一张单子

type ('a, 'b) next_item =
  | Kleft of 'a tree
  | Kright of 'b

type ('a, 'b) cont = ('a, 'b) next_item list

let depth tree =
  let rec depth tree k = match tree with
    | Leaf x -> eval k 0
    | Node(_,left,right) ->
      depth left (Kleft(right) :: k)
  and eval k d = match k with
    | Kleft(right) :: k ->
      depth right (Kright(d) :: k)
    | Kright(dleft) :: k ->
      eval k (1 + max d dleft)
    | [] -> d
  in depth tree []
;;
列表就是一个堆栈。我们这里实际上是前一个递归函数调用堆栈的具体化(转换为数据),两种不同的情况对应于两种不同类型的非tailrec调用

请注意,解除功能只是为了好玩。在实践中,CPS版本很短,很容易手工推导,很容易阅读,我建议使用它。闭包必须在内存中分配,但是
('a,'b)cont
)的元素也必须在内存中分配,尽管这些元素可能表示得更紧凑。除非有很好的理由去做更复杂的事情,否则我会坚持使用CPS版本。

在这种情况下(深度计算),您可以对(
子树深度
*
子树内容
)进行累加,以获得以下尾部递归函数:

let depth tree =
  let rec aux depth = function
    | [] -> depth
    | (d, Leaf _) :: t -> aux (max d depth) t
    | (d, Node (_,left,right)) :: t ->
      let accu = (d+1, left) :: (d+1, right) :: t in
      aux depth accu in
aux 0 [(0, tree)]

对于更一般的情况,您确实需要使用Gabriel所描述的CPS转换。

有一个简洁而通用的解决方案,使用了
折叠树和CPS-连续传球风格:

let fold_tree tree f acc =
  let loop t cont =
    match tree with
    | Leaf -> cont acc
    | Node (x, left, right) ->
      loop left (fun lacc ->
        loop right (fun racc ->
          cont @@ f x lacc racc))
  in loop tree (fun x -> x)

let depth tree = fold_tree tree (fun x dl dr -> 1 + (max dl dr)) 0

我相信你可以,如果你转换成连续传球的方式。事实上,对于这个特殊的算法,这是一个更整洁的演示。实际上,您可以将此算法理解为两种技术的组合:列表的使用通常是深度优先遍历的一种简化(一种使用下一个邻居的FIFO队列进行广度优先遍历,一种使用后进先出列表进行深度优先遍历),线程参数
depth
是一个隐藏状态monad,用于积累有关结果的信息——引用也可以完成这项工作。这完全取决于OP是否试图学习如何使函数尾部递归,或者这个函数。CPS转换代码的Reynolds去功能化的好处在于,它或多或少机械地恢复了常规(即,只有一种递归调用)非尾部递归函数的著名尾部递归累积版本,具体化的延续类型总是与列表类型同构。使尾部递归的原因之一是为了节省空间。这不是唯一的原因,但往往是驱动力。让我吃惊的是,通过CPS递归这个特定的问题并不能节省空间。它似乎以1:1的比例将堆栈帧转换为函数。如果我在这一点上不正确,有人能在这里纠正我吗?@Steve,这个转换与原始的非tailrec版本具有相同的复杂性是正确的——事实上,如果有一个通用的技术来减少任何递归函数的空间使用,那就太好了!但是我想说,tailrec的总体动机是为使用C/OS/硬件堆栈的实现节省堆栈空间,因为它比内存的其余部分受到更严格的限制。在可以降低空间复杂度的令人高兴的情况下,实际上您正在编写一种新的、不同的算法。也就是说,失效的CPS版本有时有助于找到这种新的节省空间的算法:您有时可以通过对失效的CPS代码进行等式推理来推导出更好的版本。例如,如果您在
length:'a list->int
函数上尝试此技术,您会注意到生成的
cont
类型与整数同构,而使用整数直接获得恒定内存tailrec版本。