【问题标题】:Clojure Tail Recursion with Prime FactorsClojure 尾递归与素因子
【发布时间】:2012-03-22 07:40:12
【问题描述】:

我正在尝试自学 clojure,并且我正在使用素因数 Kata 和 TDD 的原理来做到这一点。

通过一系列像这样的 Midje 测试:

(fact (primefactors 1) => (list))

(fact (primefactors 2) => (list 2))

(fact (primefactors 3) => (list 3))

(fact (primefactors 4) => (list 2 2))

我能够创建以下函数:

(defn primefactors 
    ([n] (primefactors n 2))
    ([n candidate] 
        (cond (<= n 1) (list)
              (= 0 (rem n candidate)) (conj (primefactors (/ n candidate)) candidate)
              :else (primefactors n (inc candidate))
        )
    )
)

这很好用,直到我对其进行以下边缘案例测试:

(fact (primefactors 1000001) => (list 101 9901))

我最终遇到了堆栈溢出错误。我知道我需要将其转换为适当的递归循环,但我看到的所有示例似乎都过于简单化,只指向一个计数器或数值变量作为焦点。如何使这个递归?

谢谢!

【问题讨论】:

  • 哇。这是我第一次看到有人写 Lisp 并且实际上给出了自己的台词:P

标签: recursion clojure tail-recursion prime-factoring


【解决方案1】:

这是primefactors 过程的尾递归实现,它应该可以在不引发堆栈溢出错误的情况下工作:

(defn primefactors 
  ([n] 
    (primefactors n 2 '()))
  ([n candidate acc]
    (cond (<= n 1) (reverse acc)
          (zero? (rem n candidate)) (recur (/ n candidate) candidate (cons candidate acc))
          :else (recur n (inc candidate) acc))))

诀窍是使用累加器参数来存储结果。请注意,递归结束时的 reverse 调用是可选的,只要您不关心因子是否以它们被发现的相反顺序列出。

【讨论】:

  • 谢谢,这太棒了,这是我需要的解释。
  • 在 Clojure 中,“先递归再反向”是一种反模式:我们有向量,它们在右边附加成本很低,所以最好以正确的顺序开始构建列表(如果你需要最后是一个列表而不是一个向量,只是 seq 它,这比反向便宜得多)。但实际上,惰性解决方案比尾递归解决方案更可取:请参阅我的答案以获取一个简单的示例。
【解决方案2】:

您的第二个递归调用已经在尾部位置,您可以将其替换为recur

(primefactors n (inc candidate))

变成

(recur n (inc candidate))

任何函数重载都会打开一个隐含的loop 块,因此您无需手动插入它。这应该已经在一定程度上改善了堆栈情况,因为这个分支将更常见。

第一次递归

(primefactors (/ n candidate))

不在尾部位置,因为它的结果被传递给conj。要将其置于尾部位置,您需要在一个附加的累加器参数中收集主要因素,conj 将当前递归级别的结果传递给该参数,然后在每次调用时传递给 recur。您需要调整终止条件以返回该累加器。

【讨论】:

    【解决方案3】:

    典型的方法是包含一个累加器作为函数参数之一。在你的函数定义中添加一个 3-arity 版本:

    (defn primefactors
      ([n] (primefactors n 2 '()))
      ([n candidate acc]
        ...)
    

    然后修改(conj ...)的形式,调用(recur ...),并将(conj acc candidate)作为第三个参数传入。确保将 三个 参数传递给 recur,即 (recur (/ n candidate) 2 (conj acc candidate)),以便调用 primefactors 的 3 元版本。

    (&lt;= n 1) 的情况需要返回acc 而不是一个空列表。

    如果您无法自己找出解决方案,我可以更详细地说明,但我想我应该给您一个机会先尝试解决。

    【讨论】:

      【解决方案4】:

      这个函数真的不应该是尾递归的:它应该构建一个惰性序列。毕竟,如果知道4611686018427387902 是非质数(它可以被二整除),而不需要处理数字并发现它的另一个质数是2305843009213693951,这不是很好吗?

      (defn prime-factors
        ([n] (prime-factors n 2))
        ([n candidate]
           (cond (<= n 1) ()
                 (zero? (rem n candidate)) (cons candidate (lazy-seq (prime-factors (/ n candidate)
                                                                                    candidate)))
                 :else (recur n (inc candidate)))))
      

      以上是您发布的算法的一个相当缺乏想象力的翻译;当然存在更好的算法,但这会让你获得正确性和惰性,并修复堆栈溢出。

      【讨论】:

        【解决方案5】:

        尾递归、无累加器、惰性序列解决方案:

        (defn prime-factors [n]
          (letfn [(step [n div]
                    (when (< 1 n)
                      (let [q (quot n div)]
                        (cond
                          (< q div)           (cons n nil)
                          (zero? (rem n div)) (cons div (lazy-step q div))
                          :else               (recur n (inc div))))))
                  (lazy-step [n div]
                    (lazy-seq
                      (step n div)))]
            (lazy-step n 2)))
        

        lazy-seq 中嵌入的递归调用不会在对序列进行迭代之前进行评估,从而消除了堆栈溢出的风险,而无需借助累加器。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2013-05-15
          • 2011-02-28
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-03-03
          • 2019-10-30
          相关资源
          最近更新 更多