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版本。