可以重写此代码以避免stackoverflow?

时间:2018-08-02 19:04:32

标签: clojure linked-list tail-recursion

我知道这不是惯用的Clojure。在现实世界中,我会使用向量。但是,只是好奇,有没有一种方法可以使add-node在不引起堆栈溢出的情况下工作?例如,也许通过某种方式使列表中的最后一个节点发生变异?

(defrecord ListNode [value next])

(defn add-node [^ListNode curr v]
  (if curr
    (assoc curr :next (add-node (:next curr) v))
    (ListNode. v nil)))

//stackoverflow!
(def result
  (loop [l nil i 0]
    (if (< i 100000)
      (recur (add-node l i) (inc i))
      l)))

3 个答案:

答案 0 :(得分:1)

Clojure的实现方式是反向构造一个常规列表,将新元素添加到列表的开头。这样,您的所有操作都只会影响(新)头节点,并且您不需要消耗堆栈。然后,最后,您只需反转(单链列表):

(dotest
  (let [result-1 (loop [cum nil
                        val 0]
                   (if (< val 100000)
                     (recur
                       (cons val cum)
                       (inc val))
                     cum))
        result-2 (reverse result-1)]
    (is= (take 10 result-1)
      [99999 99998 99997 99996 99995 99994 99993 99992 99991 99990])
    (is= (take 10 result-2)
      [0 1 2 3 4 5 6 7 8 9])))

如果您确实是真的 想要,则可以根据需要使用ListNode结构重新进行广播(但是,为什么?)。


话虽如此,如果要按顺序构建列表,而又不反转任何内容,只需使用Java LinkedList:

(ns xxx
  (:import [java.util LinkedList]))

(dotest
  (let [linked-list  (LinkedList.) ]
    (dotimes [i 100000]
      (.add linked-list i) )
    (is= (take 10 linked-list)
      [0 1 2 3 4 5 6 7 8 9])
    (is= (take-last 10 linked-list)
      [ 99990 99991 99992 99993 99994 99995 99996 99997 99998 99999 ] ) ))

您也可以创建一个懒惰列表。我最喜欢的方式是by using lazy-cons

(ns tst.demo.core
  (:use demo.core tupelo.core tupelo.test))

(defn lazy-nodes
  [value limit]
  (when (< value limit )
    (lazy-cons value (lazy-nodes (inc value) limit)) ))

(dotest
  (let [result (lazy-nodes 0 100000)]
    (is= (take 10 result)
      [ 0 1 2 3 4 5 6 7 8 9] )
    (is= (take-last 10 result)
      [ 99990 99991 99992 99993 99994 99995 99996 99997 99998 99999 ] ) ))

答案 1 :(得分:1)

您似乎缺少将列表定义为代数数据类型list x = (x, list x) | nil

的关键功能

这个定义的重点是它的递归性,对吗?因此使用此定义,如果要将项目x添加到现有的xs列表中,只需执行new-list = (x, list x)

在代码中,您的add-node函数应定义为:

(defn add-node [^ListNode xs x] (ListNode x xs))

现在,如果要向此列表中添加大量的项目,则不会产生堆栈溢出,但是由于递归定义,您的列表将是LIFO样式列表。您插入的最后一项将是您看到的第一项。得到头是O(1),得到任何其他东西都是O(n)。该定义暗示了这一点。在您当前对add-node的定义中,每次插入都是O(n),这可能意味着您使用的数据结构错误。

这是函数编程中非常常见的习惯用法,例如lisps(缺点列表)和haskell。如果您想翻转列表,执行反向操作非常简单。但是,在这种情况下,您可能希望一起使用其他数据结构,例如clojure的vector或差异列表(请参见Bill Burdick的this wonderful answer

答案 2 :(得分:1)

策略是按照相反的顺序收集列表节点,然后从下至上重建列表。可能是这样的:

(defn add-node [^ListNode curr v]
  (loop [nodes () curr curr]
    (if curr
      (recur (cons curr nodes) (:next curr))
      (reduce (fn [acc n] (assoc n :next acc))
              (ListNode. v nil)
              nodes))))

user> (add-node (ListNode. 1 (ListNode. 2 nil)) 100)
;;=> #user.ListNode{:value 1, :next #user.ListNode{:value 2, :next #user.ListNode{:value 100, :next nil}}}

recur部分将列表“反向”,而将累积值(从新节点开始)减少“追加”到下一个,导致结构相同。

现在您的函数不会导致堆栈溢出(很好地尝试打印结果仍会导致堆栈溢出,但是由于另一个原因:地图只是要打印的很深)

但是对,这仅对教育有好处)