Find Path to the First Occurrence in a Nested Data Structure

久未见 提交于 2019-12-13 02:46:23

问题


Consider the following numbers nested by vectors in a tree-structure

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

My goal is to find the path to the first even number that can be found by traversing the tree: In the upper example this would be 4, the path from the root to this node would be [2 3 0]

(def result [2 3 0])

I got some difficulties writing a function tho archive this. However, the following function finds the first even number, not its path:

(defn find-even [x]
  (if (vector? x)
    (some #(when-let [subresult (find-even %)]
             (when (even? subresult)
               subresult)) x)
    x))

(find-even tree) ;; 4   

What would I have to do in order to receive the path to the result?


EDIT

I figured out a way to do it. Here is a function that - at least - works:

(defn find-even-path [x]
  (letfn [(traverse [x]
            (if (vector? x)
              (some (fn [[i v]] (when-let [subresult (traverse v)]
                                  (when (even? (:value subresult))
                                    (update subresult :path conj i))))
                    (map-indexed vector x))
              {:path '()
               :value x}))]
    (when-let [result (traverse x)]
      (vec (:path result)))))

(find-even-path tree) ;; [2 3 0]

However, I'd still be curious to hear what could be optimized here. It still looks quite complex to me and is not tail recursive yet.


回答1:


Here's a way to do it tail-recursively:

(defn tailrec-depth-first-path [is? tree]
  (loop [[tree i path fk] [tree 0 [] nil]]
    (cond
      (>= i (count tree))
        (if (nil? fk) nil (recur fk))
      (vector? (tree i))
        (recur [(tree i) 0 (conj path i) [tree (inc i) path fk]])
      (is? (tree i))
        (conj path i)
      :else
        (recur [tree (inc i) path fk]))))

(tailrec-depth-first-path even? [7 9 [7 5 3 [4 6 9] 9 3] 1 [2 7 9 9]])
=> [2 3 0]

The function adds one more index to path each time it descends further down the tree. The main trick of note here is the use of a "failure continuation", represented by the variable fk. fk is the next set of arguments to pass to loop to continue the search after a failed search of a subtree, or nil if the search is at the top level. This enables backtracking without violating tail-recursion. In other words, the information that, in a non-tail-recursive version, would be needed to do the work remaining after the recursive call, is accumulated in fk in the tail-recursive version.


A quick test on my 2009 MacBook Air (1.86 GHz Core 2 Duo) suggests that the tail-recursive version is the fastest of the answers posted so far:

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

(time (dotimes [_ 100000] (find-even-path tree)))
"Elapsed time: 1153.137475 msecs"

(time (dotimes [_ 100000] (first (findt tree even? []))))
"Elapsed time: 1413.502082 msecs"

(time (dotimes [_ 100000] (depth-first-path even? tree)))
"Elapsed time: 690.56115 msecs"

(time (dotimes [_ 100000] (tailrec-depth-first-path even? tree)))
"Elapsed time: 524.332278 msecs"



回答2:


Here is an option. The idea is to keep a "stacktrace" of indices while traversing the list (the r argument). Every time we find an item that satisfies the p predicate, we return that "stacktrace". If none was found, we simply return nil. mapcat concatenates all the non-empty (non-nil) lists into one resulting list:

(defn findt [t p r]
  (mapcat (fn[i c]
            (if (coll? c)
              (findt c p (cons i r))
              (when (p c) [(reverse (cons i r))]))) (range) t))

Its still not tail recursive, but it can find all paths (lazily, due to the use of mapcat):

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

(findt tree even? [])
=> ((2 3 0) (2 3 1) (4 0))

And we can test it with:

(->> (findt tree odd? [])
 (map #(get-in tree %))
 (every? odd?))



回答3:


This isn't tail-recursive, but it's straightforward:

(defn depth-first-path 
 ([is? tree]
  (depth-first-path is? tree []))
 ([is? tree path-so-far]
  (cond
    (vector? tree)
      (some identity (map #(depth-first-path is? %1 (conj path-so-far %2))
                          tree
                          (range)))
    (is? tree)
      path-so-far
    :else
      nil)))

(depth-first-path even? [7 9 [7 5 3 [4 6 9] 9 3] 1 [2 7 9 9]])
=> [2 3 0]

I called it depth-first-path because there are other reasonable ways to define "first" when searching a tree.

Note: I'm new to Clojure, and I haven't even looked at clojure.walk or Specter. There is probably an easier way.



来源:https://stackoverflow.com/questions/36808383/find-path-to-the-first-occurrence-in-a-nested-data-structure

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!