如何在Clojure中实现递归DFS(不使用向量/堆栈)

时间:2017-12-12 23:43:19

标签: python clojure depth-first-search

我如何在clojure中编写以下python的等效代码,但严格使用递归进行堆栈管理(例如,不使用带有向量作为前沿的循环/重复)?我意识到它很简单,有一个矢量保持你的路径,只是偷看/弹出,但我这样做是为了一个思考练习。

Python版

def dfs(start,successors,goal,visited=set()): 
    if start not in visited:
        visited.add(start)
        for s in successors.get(start):
            if goal(s): 
                return s
            else:
                res = dfs(s,successors)
                if res: return res #bail early when found
    return False

Clojure版本

(defn dfs [start goal? successors visited]
  (if (goal? start) 
       start
      (when (not (contains? visited start))
             (mapcat #(dfs % goal?  successors (conj visited start)) 
                      (successors start)))))

由于迭代是通过Clojure版本中的map调用来控制的,所以你不能像Python那样尽早保释,例如if goal(s): return s
由于您正在使用map收集列表内的递归调用,因此即使找到目标,您也必须评估每个可能的节点。只有在探索完所有节点后才能得到结果。

<小时/> 现在,我知道我可以做这样的事情(我知道这不是很好......只是想提供一个快速的例子,随时提出改进建议!)但我主要是想知道是否有办法避免显式使用堆栈,让调用堆栈像python版本一样工作。

(defn dfs-non-rec [frontier goal? successors visited]
  (loop [f frontier g? goal? s successors v visited]
        (let [node (peek f)]
             (cond ; case 1
                   (goal? node) 
                    node
                   ;case 2
                   (not (contains? v node))
                   (recur (vec (concat (pop f) (successors node))) g? s (conj v node))
                   ;case 3
                   :else 
                   (recur (pop f) g? s (conj v node)))))) 

我该如何处理?

修改

<小时/> 关于一些提供的答案是否实际上是深度优先的,我有些困惑。混淆源于我对输入的假设,我本来应该在这篇文章中提供。我将输入视为邻接列表,它代表图形,而不是

(def graph  {"a"  ["b","c","d"],
             "b"  ["a","e","f"],
             "c"  ["x","y"],
             "d"  [],
             "e"  [],
             "f"  [],
             "x"  ["c"],
             "y"  ["e"]})

然后当转换为seq时,所遵循的顺序实际上是深度优先,用于通过调用图上的seq创建的结果树,但是不遵循邻接列表所隐含的顺序,因为图形结构在转换中丢失了。

因此,如果您从x开始寻找节点a,我希望遍历顺序为adcyex,而不是abcdbaefcxy

2 个答案:

答案 0 :(得分:2)

首先,你并不需要检查树的循环,因为clojure的数据结构没有循环引用(除非你不使用{{{ 1}}引用另一个原子,这是一个明显的代码气味)。简单的遍历方式可能看起来像这样(这种方式被大量的lisp(和整体编程)书籍引用):

atom

另外,在clojure中有更简洁(因而也就是惯用)的方法。最简单的方法是使用tree-seq

user> (defn dfs [goal? data]
        (if (goal? data)
          data
          (loop [data data]
            (when-let [[x & xs] (seq data)]
              (cond (goal? x) x
                    (coll? x) (recur (concat x xs))
                    :else (recur xs))))))

user> (dfs #{10} [1 [3 5 [7 9] [10] 11 12]])
10

user> (dfs #{100} [1 [3 5 [7 9] [10] 11 12]])
nil

user> (defn dfs [goal? tree] (first (filter goal? (tree-seq coll? seq tree)))) #'user/dfs user> (dfs #{10} [1 [3 5 [7 9] [10] 11 12]]) 10 user> (dfs #{100} [1 [3 5 [7 9] [10] 11 12]]) nil user> (dfs (every-pred number? even?) [1 [3 5 [7 9] [10] 11 12]]) 10 是懒惰的,所以它只会遍历树,直到找到所需的值。

另一种方法是使用clojure&#39; zippers

tree-seq

答案 1 :(得分:0)

我会像下面那样使用the Tupelo library进行测试:

(ns tst.demo.core
  (:use tupelo.test)
  (:require [tupelo.core :as t] ))

(def data [[1 2 [3]]
           [[4 5] 6]
           [7]])

(def search-result (atom nil))
(defn search-impl
  [goal? data]
  (when (= ::not-found @search-result)
    (if (goal? data)
      (reset! search-result data)
      (when (coll? data)
        (doseq [it data]
          (search-impl goal? it))))))

(defn search [goal? data]
  (reset! search-result ::not-found)
  (search-impl goal? data)
  @search-result)

(dotest
  (println "1 => " (search #(= 5 %) data))
  (println "2 => " (search #(and (integer? %) (even? %)) data))
  (println "3 => " (search #(= [4 5] %) data))
  (println "4 => " (search #(= 99 %) data)) )

结果:

1 =>  5
2 =>  2
3 =>  [4 5]
4 =>  :tst.demo.core/not-found

当它让你的程序更清晰和/或更简单时,不要害怕使用一些可变状态(在这种情况下是一个原子)。

如果你真的想隐藏原子全局可见,请执行以下操作:

(defn search2-impl
  [search2-result goal? data]
  (when (= ::not-found @search2-result)
    (if (goal? data)
      (reset! search2-result data)
      (when (coll? data)
        (doseq [it data]
          (search2-impl search2-result goal? it))))))

(defn search2 [goal? data]
  (let [search2-result (atom ::not-found)]
    (search2-impl search2-result goal? data)
    @search2-result))

(dotest
  (println "21 => " (search2 #(= 5 %) data))
  (println "22 => " (search2 #(and (integer? %) (even? %)) data))
  (println "23 => " (search2 #(= [4 5] %) data))
  (println "24 => " (search2 #(= 99 %) data)))
21 =>  5
22 =>  2
23 =>  [4 5]
24 =>  :tst.demo.core/not-found