以纽克格式懒惰地打印树

时间:2013-10-17 10:33:57

标签: clojure tree-traversal lazy-sequences zipper

我希望在Newick format中打印二叉树,显示每个节点与其父节点的距离。目前我还没有遇到以下代码的问题,它使用常规递归,但树太深可能会产生堆栈溢出。

(defn tree->newick
  [tree]
  (let [{:keys [id children to-parent]} tree
        dist (double to-parent)] ; to-parent may be a rational
    (if children
      (str "(" (tree->newick (first children)) 
           "," (tree->newick (second children)) 
           "):" dist)
      (str (name id) ":" dist))))

(def example {:id nil :to-parent 0.0 
              :children [{:id nil :to-parent 0.5 
                          :children [{:id "A" :to-parent 0.3 :children nil}
                                     {:id "B" :to-parent 0.2 :children nil}]}
                         {:id "C" :to-parent 0.8 :children nil}]})

(tree->newick example)
;=> "((A:0.3,B:0.2):0.5,C:0.8):0.0"

(def linear-tree (->> {:id "bottom" :to-parent 0.1 :children nil}
                   (iterate #(hash-map :id nil :to-parent 0.1 
                                       :children [% {:id "side" :to-parent 0.1 :children nil}]))
                   (take 10000)
                   last))

(tree->newick linear-tree)
;=> StackOverflowError

我使用当前实用程序找到的问题,例如tree-seqclojure.walk,我必须多次访问内部节点,插入逗号并关闭括号。我已经使用clojure.zip,但没有设法编写一个惰性/尾递归实现,因为我需要为每个内部节点存储它们已被访问过多少次。

1 个答案:

答案 0 :(得分:4)

这是适用于linear-tree示例的版本。它是对您的实现的直接转换,有两个变化:它使用延续传递样式和蹦床。

(defn tree->newick
  ([tree]
     (trampoline tree->newick tree identity))
  ([tree cont]
     (let [{:keys [id children to-parent]} tree
           dist (double to-parent)]     ; to-parent may be a rational
       (if children
         (fn []
           (tree->newick
            (first children)
            (fn [s1] (fn []
                       (tree->newick
                        (second children)
                        (fn [s2] (cont (str "(" s1 "," s2 "):" dist))))))))
         (cont (str (name id) ":" dist))))))

编辑:添加了模式匹配,以便以简单的方式调用该函数。

编辑2 :我注意到我犯了错误。问题是我确实认为Clojure不会仅部分地考虑尾部调用。

我的解决方案的核心思想是转换为延续传递样式,因此递归调用可以移动到尾部位置(即,不是返回结果,递归调用将其作为参数传递给continuation)。

然后我通过让它们使用蹦床来手动优化递归调用。我忘了考虑的是,继续的调用 - 不是递归调用而是尾部位置 - 也需要优化,因为尾调用可以是一个很长的闭包链,所以当函数最终评估它们,它成为一个长链调用。

此问题未通过测试数据linear-tree实现,因为第一个子节点的延续返回到trampoline以处理第二个子节点的递归调用。但是如果更改linear-tree以便它使用每个节点的第二个子节点来构建线性树而不是第一个子节点,则会再次导致堆栈溢出。

所以延续的召唤也需要回到蹦床。 (实际上,无子基本情况下的调用没有,因为它在返回到trampoline之前最多会发生一次,然后对于第二次递归调用也是如此。)所以这是一个确实考虑到这一点的实现并且应该只在所有输入上使用常量堆栈空间:

(defn tree->newick
  ([tree]
     (trampoline tree->newick tree identity))
  ([tree cont]
     (let [{:keys [id children to-parent]} tree
           dist (double to-parent)]     ; to-parent may be a rational
       (if children
         (fn [] (tree->newick
                 (first children)
                 (fn [s1] (tree->newick
                           (second children)
                           (fn [s2] #(cont (str "(" s1 "," s2 "):" dist)))))))
         (cont (str (name id) ":" dist))))))