我试图自学clojure,并且我使用Prime因素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))
我最终遇到堆栈溢出错误。我知道我需要将其转换为适当的重复循环,但我看到的所有示例似乎都过于简单,仅指向计数器或数值变量作为焦点。如何进行递归?
谢谢!
答案 0 :(得分:12)
这是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
调用是可选的,只要您不关心这些因子是否以相反的顺序列出。
答案 1 :(得分:5)
您的第二次递归调用已经在尾部位置,您只需将其替换为recur
。
(primefactors n (inc candidate))
变为
(recur n (inc candidate))
任何函数重载都会打开隐式loop
块,因此您无需手动插入。这应该已经在某种程度上改善了堆栈的情况,因为这个分支将更常用。
第一次递归
(primefactors (/ n candidate))
不在尾部位置,因为其结果传递给conj
。要将它放在尾部位置,您需要在另一个累加器参数中收集素因子,在conj
当前递归级别的结果上,然后在每次调用时传递给recur
。您需要调整终止条件以返回累加器。
答案 2 :(得分:5)
典型的方法是将累加器包含为函数参数之一。在函数定义中添加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-arity版本。
(<= n 1)
案例需要返回acc
而不是空列表。
如果你不能为自己找出解决方案,我可以详细说明,但我想我应该给你一个机会尝试先解决它。
答案 3 :(得分:4)
这个函数实际上不应该是尾递归的:它应该构建一个惰性序列。毕竟,知道4611686018427387902
是非素数(它可被2整除)不是很好,而不必处理数字并发现它的其他素因子是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)))))
以上是您发布的算法的相当缺乏想象力的翻译;当然存在更好的算法,但是这会让你产生正确性和懒惰,并修复堆栈溢出。
答案 4 :(得分:2)
尾递归,无累加器,懒惰序列解决方案:
(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
中嵌入的递归调用进行求值,从而消除堆栈溢出的风险,而无需求助于累加器。