【问题标题】:Stumped with functional breadth-first tree traversal in Clojure?Clojure 中的功能性广度优先树遍历难倒了?
【发布时间】:2012-07-09 16:23:42
【问题描述】:

假设我根据this post 中的建议定义了一棵树,尽管在我的情况下它是一个向量,但希望这无关紧要(它们是 Programming Clojure 书中的向量):

(def tree [1 [[2 [4] [5]] [3 [6]]]])

应该是这样的:

      1
     / \
    2   3
   / \  |
  4   5 6

现在,我想在不使用任何传统方法(例如队列)的情况下对树进行广度优先遍历,而是专门使用堆栈来传递信息。我知道这不是最简单的路线,但我主要是作为锻炼来做的。同样在这一点上,我不打算返回一个集合(我会在之后作为练习来解决这个问题),而是在我遍历它们时打印出节点。

我目前的解决方案(刚开始使用 Clojure,请客气):

(defn breadth-recur
  [queue]
  (if (empty? queue)
    (println "Done!")
    (let [collections (first (filter coll? queue))]
      (do
        ; print out nodes on the current level, they will not be wrapped'
        ; in a [] vector and thus coll? will return false
        (doseq [node queue] (if (not (coll? node)) (println node)))
        (recur (reduce conj (first collections) (rest collections)))))))

最后一行没有按预期工作,我不知道如何修复它。我确切地知道我想要什么:我需要剥离每一层向量,然后将结果连接起来传递给 recur。

我看到的问题主要是:

IllegalArgumentException Don't know how to create ISeq from: java.lang.Long 

基本上 conj 不喜欢将向量附加到 long 上,如果我将 conj 换成 concat,那么当我是两个项目之一时我会失败连接不是向量。 conj 和 concat 在面对时都失败:

[2 [4] [5] [3 [6]]]

我觉得我在这里遗漏了一个非常基本的操作,它可以在两个位置上对向量和基元都起作用。

有什么建议吗?

编辑 1:

这棵树实际上应该是(感谢 Joost!):

(def tree [1 [2 [4] [5]] [3 [6]]])

但是我们还没有找到广度优先的解决方案。

【问题讨论】:

  • 你看过clojure.walk或tree-seq中的函数的来源吗?他们实现了树遍历,你可能会感兴趣。
  • @Gert:我的印象是所有这些都是深度优先的,对吗?我也考虑过拉链,但后来我的大脑开始融化。
  • 啊,是的,clojure.walk 进行深度优先遍历;我错过了您帖子中的广度优先要求,哎呀!

标签: algorithm clojure functional-programming


【解决方案1】:

这可能会有所帮助,我正在创建一个算法来评估树是否对称并使用广度优先遍历:

(defn node-values [nodes]
    (map first nodes))

(defn node-children [nodes]
  (mapcat next nodes))

(defn depth-traversal [nodes]
    (if (not (empty? nodes))
        (cons (node-values nodes) (depth-traversal (node-children nodes)))))

(defn tree-symmetric? [tree]
    (every?
        (fn [depth] (= depth (reverse depth)))
        (depth-traversal (list tree))))

(def tree '(1 (2 (3) (4)) (2 (4) (3))))
(node-values (list tree)) ; (1)
(node-children (list tree)) ; ((2 (3) (4)) (2 (4) (3)))
(depth-traversal (list tree)) ; ((1) (2 2) (3 4 4 3))
(tree-symmetric? tree) ; true

【讨论】:

    【解决方案2】:

    您的树数据不正确。应该是[1 [2 [4] [5]] [3 [6]]]

    此外,您将树遍历与打印混合并构建结果。如果您专注于单独完成困难的部分,事情会变得更简单:

    (def tree [1 [2 [4] [5]] [3 [6]]])
    

    请注意这是深度优先。见下文

    (defn bf "return elements in tree, breath-first"
       [[el left right]] ;; a tree is a seq of one element,
                         ;; followed by left and right child trees
       (if el
         (concat [el] (bf left) (bf right))))
    
    (bf tree)
    => (1 2 4 5 3 6)
    

    正确版本

    (defn bf [& roots] 
       (if (seq roots) 
           (concat (map first roots) ;; values in roots
                   (apply bf (mapcat rest roots))))) ;; recursively for children
    
    (bf tree)
    => (1 2 3 4 5 6)
    

    【讨论】:

    • 行得通,谢谢!我忘了提到我正在尝试解决没有最大子节点数量的一般情况下的问题,因此我尝试减少/进入。另外,有没有办法通过尾调用来做到这一点?
    • (defn bf [[el & children]] (if el (apply concat [el] (map bf children))))
    • 如果您真的愿意,可以将其转换为尾调用。但我不明白这一点,因为它本质上是基于堆栈的并且已经很懒了。
    • @Joost Diepenmaat:我和 kotarak 有同样的问题......不是 1 2 4 5 3 6 DFS 而 BFS 应该给出 1 2 3 4 5 6 吗?我的意思是,看起来 OP 精确地构建了他的树,以便 BFS 回馈 1 2 3 4 5 6。关于 BFS 的维基百科文章对“a b c d e f g h”也是如此。在此处查看动画 gif:en.wikipedia.org/wiki/Breadth-first_search
    • 注意这里使用concat意味着你会得到一个大树(无论是否深)的堆栈溢出,例如(reduce (fn [tree node] [node tree]) [10000] (range 9999 0 -1))。
    【解决方案3】:

    由于显然还没有发布广度优先的解决方案,这里有一个简单的算法,首先急切地实现,然后转化为惰性:

    (defn bfs-eager [tree]
      (loop [ret [], queue (conj clojure.lang.PersistentQueue/EMPTY tree)]
        (if (seq queue)
          (let [[node & children] (peek queue)]
            (recur (conj ret node) (into (pop queue) children)))
          ret)))
    
    (defn bfs-lazy [tree]
      ((fn step [queue]
         (lazy-seq
          (when (seq queue)
            (let [[node & children] (peek queue)]
              (cons node
                    (step (into (pop queue) children)))))))
       (conj clojure.lang.PersistentQueue/EMPTY tree)))
    

    【讨论】:

    • 我很好奇,你使用 PersistentQueue 仅仅是因为它比列表快吗?
    • 我们需要从前面取并添加到末尾。我们可以使用带有 concat 的惰性序列,但是如果树非常大,这将失败。所以从某种意义上说:是的,我们可以在这里使用一个列表。但除了慢得多,它也没有表达意图:这是一个队列,那么为什么不使用实际的队列结构呢?
    • 因此,如果我理解正确,如果您使用 conj/pop,向量和列表都可以作为堆栈(从不同方面)工作,但是使用 PersistentQueue 您可以从前面弹出并在结束了吗?
    【解决方案4】:

    reduce和conj的许多组合可以替换为单个into调用 在上面使用 reduce 的情况下,您可能需要传递一个初始的空向量来减少到 得到使结合快乐。

    【讨论】:

      猜你喜欢
      • 2016-02-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-11-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-08-10
      相关资源
      最近更新 更多