我正在制作我的第一个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和函数式编程的不熟悉,并且希望有人可以解释使用递归和构建此树的最佳方法。
答案 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
)id
和kids
的{{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] ,,,}
可用于根据需要将树创建为列表。
我无法对此进行测试,因为我现在正在移动设备,因此请原谅任何错误。 :)