【问题标题】:Find Path to the First Occurrence in a Nested Data Structure在嵌套数据结构中查找第一次出现的路径
【发布时间】:2016-04-23 08:16:40
【问题描述】:

考虑以下由向量嵌套在树形结构中的数字

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

我的目标是通过遍历树找到第一个偶数的路径:在上面的示例中,这将是 4,从根到该节点的路径将是 [2 3 0]

(def result [2 3 0])

我在编写一个函数来归档它时遇到了一些困难。但是,下面的函数会找到第一个偶数,而不是它的路径:

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

(find-even tree) ;; 4   

我需要做什么才能获得结果的路径?


编辑

我想出了一个办法。这是一个 - 至少 - 有效的函数:

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

但是,我仍然很想知道这里可以优化什么。它在我看来仍然很复杂,而且还不是尾递归的。

【问题讨论】:

    标签: clojure functional-programming clojurescript tail-recursion


    【解决方案1】:

    这是一个选项。这个想法是在遍历列表时保留索引的“堆栈跟踪”(r 参数)。每次我们找到满足p 谓词的项目时,我们都会返回那个“stacktrace”。如果没有找到,我们简单地返回 nil。 mapcat 将所有非空(非零)列表连接到一个结果列表中:

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

    它仍然不是尾递归,但它可以找到所有路径(懒惰,由于使用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))
    

    我们可以通过以下方式对其进行测试:

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

    【讨论】:

    • 感谢您的回答。我很快就会更仔细地研究它。我刚刚发现我提出的解决方案有一个错误。它不适用于 [[1 1 1] 2]。必须先解决这个问题。
    • 好吧,现在对我有意义。我想你的函数是懒惰的——因为我只需要第一次出现。这可以用'first'来完成。如果您考虑到这一点可以更新您的答案,那就太好了!
    【解决方案2】:

    这是一种尾递归的方法:

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

    该函数在path 每次下降到树的下方时都会向其添加一个索引。这里要注意的主要技巧是使用“失败延续”,由变量fk 表示。 fk 是传递给 loop 以在子树搜索失败后继续搜索的下一组参数,如果搜索位于顶层,则为 nil。这可以在不违反尾递归的情况下进行回溯。换句话说,在非尾递归版本中,完成递归调用后剩余工作所需的信息在尾递归版本中累积在 fk 中。


    对我的 2009 MacBook Air(1.86 GHz Core 2 Duo)的快速测试表明,尾递归版本是迄今为止发布的答案中最快的:

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

    【讨论】:

    • 太棒了!只有一件事:findt 懒惰地搜索树中的所有结果。为了让你的测试公平,你应该用(first ..)
    • @AntonHarald 谢谢!好点子。我刚刚修好了。令人惊讶的是,实现惰性向量的第一个元素需要额外的 300 毫秒。 (通过在其最后一个参数中传递一个空列表而不是一个空向量,可以减少大约 10–20 毫秒,因为 cons 在向量上很慢。)
    • 有趣。这怎么解释?我想到的唯一一件事是:“Clojure 中的许多惰性序列函数通过一次生成 N 个元素的“块”来摊销元素的实现”(insideclojure.org/2015/01/02/sequences)这意味着惰性只显示在大于 N 的“序列”上。如果我们的测试树上的惰性操作小于 N,那么无论如何都必须评估整个事情,然后在结果上调用 first 会导致额外的毫秒。但这只是猜测。
    • @AntonHarald 我的理解是懒惰总是会牺牲性能。原因是处理懒惰需要额外的簿记和复杂性。懒惰似乎只有当你不看延迟的价值时才会得到回报。即便如此,懒惰地评估你从未看过的东西比根本不评估它们要慢。懒惰的唯一好处是,程序员懒惰地表达事物有时非常方便;对 CPU 来说总是更糟(除非编译器非常聪明)。
    • @AntonHarald 我想我刚刚明白为什么从findt 实现第一个项目如此缓慢。如果将mapcat 更改为mapfindt 将返回(nil nil (nil nil nil ([(2 3 0)] [(2 3 1)] nil) nil nil) nil ([(4 0)] nil nil nil))。因此,要获得第一项,它必须生成、检查和拒绝五个 nil。非惰性版本不会产生任何额外的东西。
    【解决方案3】:

    这不是尾递归,但很简单:

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

    我称之为depth-first-path,因为在搜索树时还有其他合理的方式来定义“第一”。

    注意:我是 Clojure 的新手,我什至没有看过 clojure.walkSpecter。可能有更简单的方法。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-17
      • 1970-01-01
      • 2016-02-12
      • 2017-08-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多