尾递归函数,用于在Ocaml中查找树的深度

ppa*_*l74 33 tree binary-tree ocaml functional-programming

我的tree定义类型如下

type 'a tree = Leaf of 'a | Node of 'a * 'a tree * 'a tree ;;
Run Code Online (Sandbox Code Playgroud)

我有一个函数来查找树的深度如下

let rec depth = function 
    | Leaf x -> 0
    | Node(_,left,right) -> 1 + (max (depth left) (depth right))
;;
Run Code Online (Sandbox Code Playgroud)

这个函数不是尾递归的.有没有办法让我以尾递归的方式编写这个函数?

gas*_*che 45

您可以通过将功能转换为CPS(延续传递风格)来轻松完成此操作.我们的想法是,不是调用depth left,而是根据这个结果计算事物,而是调用depth left (fun dleft -> ...),其中第二个参数是"一旦result(dleft)可用就计算什么".

let depth tree =
  let rec depth tree k = match tree with
    | Leaf x -> k 0
    | Node(_,left,right) ->
      depth left (fun dleft ->
        depth right (fun dright ->
          k (1 + (max dleft dright))))
  in depth tree (fun d -> d)
Run Code Online (Sandbox Code Playgroud)

这是一个众所周知的技巧,可以使任何函数尾递归.Voilà,这是尾巴.

袋中的下一个众所周知的技巧是"去功能化"CPS结果.(fun dleft -> ...)作为函数的continuation(部分)的表示是整洁的,但您可能希望看到它看起来像数据.因此,我们用数据类型的具体构造函数替换每个闭包,捕获其中使用的自由变量.

在这里,我们有三个封延续:(fun dleft -> depth right (fun dright -> k ...)),只重用的环境变量rightk,(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
Run Code Online (Sandbox Code Playgroud)

defunctorized函数如下所示:

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
;;
Run Code Online (Sandbox Code Playgroud)

我没有构建函数k并将其应用于leaves(k 0),而是构建了一个类型的数据('a, int) cont,需要稍后对其eval进行计算以计算结果.eval当它被传递时Kleft,执行闭包(fun dleft -> ...)正在做的事情,即它递归调用depth右子树.eval并且depth是相互递归的.

现在仔细看看('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 []
;;
Run Code Online (Sandbox Code Playgroud)

列表是一个堆栈.我们这里所拥有的实际上是前一个递归函数的调用堆栈的具体化(转换为数据),两种不同的情况对应于两种不同类型的非tailrec调用.

请注意,defunctionalization只是为了好玩.在实践中,CPS版本很简单,易于手工派生,相当容易阅读,我建议使用它.闭包必须在内存中分配,但是元素也是如此('a, 'b) cont- 虽然那些可能更紧凑地表示.我会坚持使用CPS版本,除非有很好的理由去做更复杂的事情.

  • @Steve,这个转换与原始的非tailrec版本具有相同的复杂性是正确的 - 事实上,有一个通用技术来减少任何递归函数的空间使用会有点太好了!然而,我要说,对于使用C/OS /硬件堆栈的实现,tailrec的一般动机是保存*stack*空间,因为它比其他内存严格受限.在您可以减少空间复杂性的快乐案例中,您实际上正在编写一种新的不同算法. (10认同)
  • 这一切都取决于OP是否正在尝试学习如何使*a*函数尾递归,或*this*函数. (5认同)
  • 使尾部递归的原因之一是节省空间.这不是唯一的原因,但它往往是驱动力.令我感到震惊的是,通过CPS使这个特定问题的尾部递归不会节省空间.它似乎将堆栈帧以1:1的比例转换为函数.如果我对此不正确,有人可以在这里纠正我吗? (3认同)
  • 也就是说,defunctionalized CPS版本有时有助于找到这种新的节省空间的算法:你有时可以通过CPS-defunctionalized代码上的等式推理得到更好的版本.如果你在`length:'list - > int`函数中尝试这种技术,你会注意到生成的`cont`类型与整数同构,而使用整数直接给你的是常量内存tailrec版本. (2认同)

Tho*_*mas 17

在这种情况下(深度计算),您可以累加成对(subtree depth*subtree content)以获得以下尾递归函数:

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)]
Run Code Online (Sandbox Code Playgroud)

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

  • 实际上,对于这种特定算法,这是一个更简洁的表示.实际上,您可以将此算法理解为两种技术的组合:列表的使用是深度优先遍历的常规尾部重构(一个使用下一个邻居的FIFO队列进行广度优先遍历,一个LIFO列表用于深度-first),并且线程参数`depth`是一个隐藏状态monad,用于累积有关结果的信息 - 引用也可以完成工作. (4认同)