简体   繁体   中英

How to implement a recursive DFS in Clojure (without using a vector / stack)

How would I code the equivalent of the following python in clojure, but strictly using recursion for the stack management (eg not using a loop/recur with a vector acting as the frontier)?
I realize it's fairly simple with a vector maintaining your paths and just peeking / popping, but I'm doing this as a thought exercise.

Python Version


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 Version


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

Since iteration is controlled by a call to map in the Clojure version, you can't really bail early the way you can in Python eg if goal(s): return s .
Since you are collecting the recursive calls inside of a list with map, you are forced to evaluate every possible node even after the target is found. Only then after all nodes have been explored do you get the result.


Now, I know I can do something like this (I know this isn't pretty... just trying to provide a quick example, feel free to suggest improvements!) but I'm primarily interested in seeing if there is a way to avoid explicitly using a stack, and letting the call stack do the work like in the python version.

 (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)))))) 

How should I approach this?

EDIT


There was some confusion on my part as to whether some of the provided answers were in fact depth-first. The confusion stemmed from an assumption I made about the input, which I should have provided in this post originally. I had assumed the input as an adjacency list, which represents a graph , rather than a tree

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

then when converted to a seq, the order followed is in-fact depth-first for that resulting tree created by calling seq on the graph, however the order implied by the adjacency list is not followed, because the graph structure is lost in the conversion.

So if you are looking for the node x starting at the a , I would expect the traversal order to be adcyex , rather than abcdbaefcxy

first of all you don't really need to check tree for loops, since clojure's data structures don't have circular references (unless you don't use mutable state with atom s referencing to another atoms, which is an obvious code smell). The simple way of traversal could look like this (this way is referenced by lots of lisp (and overall programming) books):

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

in addition there are more concise (and thus idiomatic i guess) ways to do this in clojure. The simplest one is to use tree-seq :

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

tree-seq is lazy, so it only traverses tree until you find the needed value.

another way is to use clojure's zippers :

user> (require '[clojure.zip :as z])
nil

user> (defn dfs [goal? tree]
        (loop [curr (z/zipper coll? seq identity tree)]
          (cond (z/end? curr) nil
                (goal? (z/node curr)) (z/node curr)
                :else (recur (z/next curr)))))
#'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

I would do it like the following, which uses the Tupelo library for testing:

(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)) )

with results:

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

Don't be afraid to use a bit of mutable state (in this case an atom) when it makes your program clearer and/or simpler.

If you really want to hide the atom from being globally visible, just do this:

(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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM