我试着用这种方法找到一个完全数列表:
(defn classify [num]
(let [factors (->> (range 1 (inc num))
(filter #(zero? (rem num %))))
sum (reduce + factors)
aliquot-sum (- sum num)]
(cond
(= aliquot-sum num) :perfect
(> aliquot-sum num) :abundant
(< aliquot-sum num) :deficient)))
(defn is-p [n]
(= :perfect (classify n)))
(defn list-perfect [n]
(filter is-p (range 1 (inc n))))
问题:
如何构建完全数的惰性序列,以便我可以使用
(take n ...)
轻松获得列表这个代码是习惯的和有效的吗?任何改善吗?
你的算法效率很低,是O(n)
。
为了快速获胜,您可以立即将范围缩小一半,因为您永远不会有大于您正在测试的数字除以2的因子。
所以改成:
(defn classify [num]
(let [factors (->> (range 1 (max 2 (inc (quot num 2))))
;; ...
然而…您可以将其更改为O(sqrt n)
,这是数量级更快。请看下面我的计时。
真正的效率是注意到因子是成对的[x (quot num x)]
,然后只检查第一个(sqrt num)
(或略超过):
(defn perfect? [n]
(let [r (range 2 (+ 2 (int (Math/sqrt n))))
s (set (mapcat #(if (zero? (rem n %))
[% (quot n %)])
r))
t (reduce + 1 s)]
(= n t)))
我已经把它分成了单独的计算,所以你可以验证每一个阶段。
范围可以从2..((sqrt n) + 2),并初始化约简为1(它总是一个因子)。
这将问题从O(n)变为O(根号n),因此如果您要检查大数,则会产生巨大的差异。
作为一个例子,这里有一些时间在我的MBP上n的较大值:
n "n/2" "sqrt n"
33550336 1,172.5ms 2.85ms
8589869056 274,346.6ms 16.76ms
137438691328 didn't time 44.27ms
所以使用根版本的速度是第6个完全数的16369倍。详见http://en.wikipedia.org/wiki/List_of_perfect_numbers
编辑:为什么(int (root n)) + 2?为什么是"x (n x)"?
当你计算一个数字n的因数时,如果你找到一个(比如a
),那么n/a
也是一个因数(比如b
),因为n = a * b
。看看28,第一个相关因子是2,显然28/2 = 14也是一个因子。所以你不需要检查14,你已经知道它是2的因数。
当我们从2开始递增检查数字时,我们偶然发现较大的数字往下:
2 is a factor, 28 / 2 = 14 -> [a, b] = [2, 14]
4 is a factor, 28 / 4 = 7 -> [a, b] = [4, 7]
7 is a factor, 28 / 7 = 4 -> [a, b] = [7, 4] - wait a minute...
这里的[a,b]
对是mapcat函数中的[% (quot n %)]
,例如,当range当前迭代值2
时,则%
在函数中是2
,因此(quot n %)
是(quot 28 2)
,即14,因此[% (quot n %)]
只是向量[2 14]
,然后将其平化为2
和14
作为值后添加到集合中。之后,当range值为4
时,则[% (quot n %)]
为[4 (quot 28 4)]
,即[4 7]
,再次被mapcat平展为数字4
和7
。
因此,我们将每对数字(通过mapcat平坦化)添加到我们的集合中,包括数字1,并最终得到#{1 2 14 4 7}
,这是28的因数。(实际上,我没有把1放在集合中,因为我不需要,而是从1开始求和,这是相同的效果)。
但是他们在什么时候转身呢?也就是说,我们什么时候知道[7,4]
已经作为[4,7]
包含在集合中?
显然是当a
> b
时,因为在寻找最低的数字时,我们总是找到最高的数字,所以我们可以在这里完成检查。
但是这个点是什么?很简单,如果一个完全数是平方数,那么a和b就会相等,即a*a = n,所以a = sqrt(n)。
因此,我们需要检查的a
的最大值是大于n
的根的整数。
。对于28,sqrt(28) = 5.292,所以我们必须检查6,以确保我们包含了可能是有成对因子的因子的最小数。
我们需要(int (sqrt n)) + 1
我总是在根计算是1.9999999999的情况下这样做…它的四舍五入方式是错误的,所以再加1可以确保消除任何四舍五入错误。
但是在一个范围内,如果你想包含那个数字,你必须再加1(范围降低高的数字,(范围6)=(0 1 2 3 4 5)),因此它要加2的值:1为范围,1确保它在四舍五入的根之上。
虽然,在说了这句话之后,我已经测试了到2305843008139952128的完全数,它可以用+1而不是+2工作,并不是说这是一个巨大的节省。可能是因为在我检查的数字中,没有一个完全数接近完全平方,所以(int (sqrt n))
中没有舍入误差。
如果你对完全数感兴趣,我建议你阅读http://britton.disted.camosun.bc.ca/perfect_number/lesson1.html
list-perfect
已经懒惰了,因为您使用了过滤器:
(filter pred coll)
返回coll中项的惰性序列(pred item)返回true。药物必须无副作用。
代码是否习惯用语可能是一个意见问题(因此偏离主题),但从我的角度来看,它看起来已经足够好了。