从Clojure中的向量构建树

时间:2016-09-09 20:00:48

标签: clojure tree

我正在制作我的第一个Clojure计划。我有一些问题想出如何基于如下输入构建树:

["A B" "A C" "C D" "D E" "A F" "F G"]

输出应该是这样的:

'(A (B) (C (D (E)) (F (G)))

我不确定如何开始这样做。例如,当涉及到命令式编程时,我使用嵌套循环来查找这种关系是否已经存在,当它不存在时,我会找到父元素并附加子元素它。但据我所知,函数式编程将使用另一种方法,我会递归地遍历向量中的所有元素,而不是更改现有的树,我建立了一个新的方法。

我不确定这是否有效,但我的功能如下:

(defn build-tree
  [func tree parent child]
  (map (fn [i] (if (seq? i)
                 build-tree
                 (func i parent child))))) 

我确定问题来自于我对Clojure和函数式编程的不熟悉,并且希望有人可以解释使用递归和构建此树的最佳方法。

3 个答案:

答案 0 :(得分:6)

你可能会收到比我下面更短的答案。我决定教你钓鱼 - 而不是展示一个简短的课程,我将带你通过系统的方式来解决这类问题。您将不得不填写一些空白,包括将您的数据转换为更可口的形式。这些都是小问题,只会分散注意力。

您需要做的第一件事是将输出分解为构建步骤。这些步骤应与输出所代表的抽象类型相匹配。在您的情况下,输出的具体类型是一个列表,但它实际上代表一棵树。这些是不同的抽象类型 - 列表有头部和尾部,但是树有节点,可能有子节点。您需要考虑构建不同类型节点的抽象构造函数,而不是您碰巧选择代表抽象类型的偶然特定结构 - 在您的情况下列表。

想象一下,你有两个看起来像

的构造函数
(defn ->branch
  [id kids]
  ...)

(defn ->leaf
  [id]
  ...)

当然假设每个kid都是一棵树 - 即分支或叶子。换句话说,每个kid都应该是->branch->leaf的结果。

因此,构建任何树的唯一方法是使用嵌套的构造函数调用链。这就是我的意思"将输出分解为构建步骤。"在您的情况下,它意味着以下链:

(->branch :A [(->leaf :B) (->branch :C [(->branch :D (->leaf :E))]) (->branch :F [(->leaf :G)])])

(我将使用关键字而不是节点ID的符号,否则我们将陷入绑定/引用细节中。)

在这里停下来感受功能和命令式风格之间的区别。您自己概述了命令式样式 - 您可以通过找到正确的位置并在其中添加新节点来更新树。在功能风格中,您可以考虑创建值,而不是更新其他值。 (当然,在技术上没有任何东西阻止你在命令式语言中做同样的事情 - 比如使用嵌套调用构造函数,或者以这种方式使用静态工厂,它不是自然产生的东西。)

要将您的特定树形表示作为列表,您只需要填写上面->branch->leaf的实现,这非常简单。 (留给你练习。)

现在回到构建树。编写构建树的函数的任务实际上是从输入数据创建一系列构造函数调用。所以你需要了解两件事:

  • 何时调用哪个构造函数(此处,何时调用->branch以及何时调用->leaf
  • 如何获取构造函数的参数(两个idkids的{​​{1}})

首先,我们如何知道要开始的节点?这取决于问题所在,但我们假设它是作为参数给出的。所以我们假设我们正在构建以下函数:

->branch

(defn ->tree [adj-list node] ...) 是您的输入 - 即使您尝试将其伪装成字符串列表,也是adjacency list,我们将以这种方式对待它(如@SamEstep建议的那样)评论。)留下作为练习,将您的输入形式转换为邻接列表。

因此我们应该清楚我们在构造函数中使用adj-list - id。但它是node还是->branch?那么,答案取决于->leaf中是否有node的直接后代,所以显然我们需要一个函数

adj-list

返回(defn descendants [adj-list node] ...) 的直接后代的id列表(可能为空)。 (作为练习留下。)因此我们可以决定是否拨打node->branch,具体取决于该列表是否为空,例如

->leaf

好的,那么我们需要将这些(if-let [kid-ids (descendants node)] (->branch node ...) (->leaf node)) 提供给...。他们是->branch吗?不,它们必须是树木,而不是ids,树枝或树叶本身。换句话说,我们需要先调用kid-ids。看到? 我们点击递归点 ,它自然而然地产生(或者我希望如此)。在Clojure中,在序列的每个元素上调用一个函数由->tree完成,它返回一系列结果,这正是我们所需要的:

map

除了(if-let [kid-ids (descendants adj-list node)] (->branch node (map ->tree kid-ids) (->leaf node)) 需要一个额外的参数->tree。我们可以使用匿名函数,也可以使用adj-list。我们将使用partial partial绑定,这是最干净的方法:

let

就是这样。让我们把它放在一起:

(let [->tree' (partial ->tree adj-list)]
   (if-let [kid-ids (descendants adj-list node)]
      (->branch node (map ->tree' kid-ids))
      (->leaf node)))

结果:

(defn ->tree
  [adj-list node]
  (let [->tree' (partial ->tree adj-list)]
     (if-let [kid-ids (descendants adj-list node)]
       (->branch node (map ->tree' kid-ids))
       (->leaf node))))

总结一下:

  • 以抽象的方式查看您的数据。创建构造函数,并使用它们来构建输出。
  • 构建输出意味着创建一系列构造函数调用。
  • 这意味着您需要将输入的形状映射到输出构造函数及其参数。
  • 在很多情况下,递归会在这一步自然出现。

完成后,您可以优化和缩短您心中的内容(如果仔细观察,可以取消(def adj-list [[:A :B] [:A :C] [:C :D] [:D :E] [:A :F] [:F :G]]) (->tree adj-list :A) ;; => (:A (:B) (:C (:D (:E))) (:F (:G))) 。)但不要做它过早了。

答案 1 :(得分:1)

以为user2946753的示例很有用。对其进行了调整以使其正常工作。

(defn example []
  (reduce (fn[g edge] 
            (let [[from to] (map keyword (str/split edge #" "))]
              (update g from #(conj % to))))
          {} 
          ["A B" "B C"]))


(example)
=> {:A (:B), :B (:C)}

答案 2 :(得分:0)

您的输出看起来像一个关联结构,是否有理由在此处使用列表而不是地图?

还是保证与您的示例中的树结构匹配的顺序?我不会假设。我认为在这里不使用递归更容易......

因此,让我们将输入解析为邻接列表,例如:

(reduce 
(fn[g edge] 
 (let [[from to]] (map keyword (str/split " " edge))
  (update g from #(conj % to))
 {} 
["A B" "B C"])

哪个应该输出:     {A [B C F] ,,,}

可用于根据需要将树创建为列表。

我无法对此进行测试,因为我现在正在移动设备,因此请原谅任何错误。 :)