简体   繁体   English

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

[英]Tail recursive function to find depth of a tree in Ocaml

I have a type tree defined as follows 我有一个类型tree定义如下

type 'a tree = Leaf of 'a | Node of 'a * 'a tree * 'a tree ;;

I have a function to find the depth of the tree as follows 我有一个函数来查找树的深度如下

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

This function is not tail recursive. 这个函数不是尾递归的。 Is there a way for me to write this function in tail recursive way? 有没有办法让我以尾递归的方式编写这个函数?

You can trivially do this by turning the function into CPS (Continuation Passing Style). 您可以通过将功能转换为CPS(延续传递风格)来轻松完成此操作。 The idea is that instead of calling depth left , and then computing things based on this result, you call depth left (fun dleft -> ...) , where the second argument is "what to compute once the result ( dleft ) is available". 这个想法是,不是调用depth left ,然后根据这个结果计算东西,而是调用depth left (fun dleft -> ...) ,其中第二个参数是“一旦结果( 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)

This is a well-known trick that can make any function tail-recursive. 这是一个众所周知的技巧,可以使任何函数尾递归。 Voilà, it's tail-rec. Voilà,这是尾巴。

The next well-known trick in the bag is to "defunctionalize" the CPS result. 袋中的下一个众所周知的技巧是“去功能化”CPS结果。 The representation of continuations (the (fun dleft -> ...) parts) as functions is neat, but you may want to see what it looks like as data. 作为函数的continuation( (fun dleft -> ...)部分)的表示是整洁的,但您可能希望看到它看起来像数据。 So we replace each of these closures by a concrete constructor of a datatype, that captures the free variables used in it. 因此,我们用数据类型的具体构造函数替换每个闭包,捕获其中使用的自由变量。

Here we have three continuation closures: (fun dleft -> depth right (fun dright -> k ...)) , which only reuses the environment variables right and k , (fun dright -> ...) , which reuses k and the now-available left result dleft , and (fun d -> d) , the initial computation, that doesn't capture anything. 在这里,我们有三个封延续: (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

The defunctorized function looks like this: 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
;;

Instead of building a function k and applying it on the leaves ( k 0 ), I build a data of type ('a, int) cont , which needs to be later eval uated to compute a result. 我没有构建函数k并将其应用于叶子( k 0 ),而是构建了一个类型为('a, int) cont ,需要稍后对其进行eval以计算结果。 eval , when it gets passed a Kleft , does what the closure (fun dleft -> ...) was doing, that is it recursively call depth on the right subtree. eval ,当它传递给Kleft ,会执行闭包(fun dleft -> ...)正在做的事情,即它在右子(fun dleft -> ...)递归调用depth eval and depth are mutually recursive. evaldepth是相互递归的。

Now look hard at ('a, 'b) cont , what is this datatype? 现在仔细看看('a, 'b) cont ,这个数据类型是什么? It's a list! 这是一个清单!

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 []
;;

And a list is a stack. 列表是一个堆栈。 What we have here is actually a reification (transformation into data) of the call stack of the previous recursive function, with two different cases corresponding to the two different kinds of non-tailrec calls. 我们这里所拥有的实际上是前一个递归函数的调用堆栈的具体化(转换为数据),两种不同的情况对应于两种不同类型的非tailrec调用。

Note that the defunctionalization is only there for fun. 请注意,defunctionalization只是为了好玩。 In pratice the CPS version is short, easy to derive by hand, rather easy to read, and I would recommend using it. 在实践中,CPS版本很简单,易于手工派生,相当容易阅读,我建议使用它。 Closures must be allocated in memory, but so are elements of ('a, 'b) cont -- albeit those might be represented more compactly`. 闭包必须在内存中分配,但是('a, 'b) cont元素也是如此 - 尽管那些可能更紧凑地表示。 I would stick to the CPS version unless there are very good reasons to do something more complicated. 我会坚持使用CPS版本,除非有很好的理由去做更复杂的事情。

In this case (depth computation), you can accumulate over pairs ( subtree depth * subtree content ) to obtain the following tail-recursive function: 在这种情况下(深度计算),您可以累加对( 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)]

For more general cases, you will indeed need to use the CPS transformation described by Gabriel. 对于更一般的情况,您确实需要使用Gabriel描述的CPS转换。

There's a neat and generic solution using fold_tree and CPS - continuous passing style: 使用fold_tree和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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM