在Racket

时间:2018-02-06 09:43:13

标签: performance recursion functional-programming racket primes

我正在学习Racket(使用HtDP课程),这是我第一次使用函数式语言编写程序。

我试图设计一个函数,在某个输入n下找到所有素数,使用(我认为)这个问题的函数方法,但程序可能变得非常慢(对于100.000来说是86秒,而我的Python ,C和C ++快速编写的解决方案只需几秒钟。

以下是代码:

;; Natural Natural -> Boolean
;; Helper function to avoid writing the handful (= 0 (modulo na nb))

(define (divisible na nb) (= 0 (modulo na nb)))


;; Natural ListOfNatural -> Boolean
;; n is the number to check, lop is ALL the prime numbers less than n

(define (is-prime? n lop)
  (cond [(empty? lop) true]
        [(divisible n (first lop)) false]
        [ else (is-prime? n (rest lop))]))


;; Natural -> ListOfNatural

(define (find-primes n)
  (if (= n 2)
      (list 2)
      (local [(define LOP (find-primes (sub1 n)))]
        (if (is-prime? n LOP)
            (append LOP (list n))
                  LOP))))

(time (find-primes 100000))

我正在使用可分割的函数,而不是仅仅使用其他功能,因为我真的希望在它们可以在程序的另一部分中使用时具有分离的函数。我也应该定义is-prime?在找到素数之内,因为没有人会称之为素数?在一个数字的同时也给出了少于该数字的所有素数。

有关如何改善这一点的任何指示?

2 个答案:

答案 0 :(得分:1)

以下是一些改善效果的建议,现在,n = 100000的过程会在两秒内返回。

(define (is-prime? n lop)
  (define sqrtn (sqrt n))
  (if (not (or (= (modulo n 6) 1) (= (modulo n 6) 5)))
      false
      (let loop ([lop lop])
        (cond [(or (empty? lop) (< sqrtn (first lop))) true]
              [(zero? (modulo n (first lop))) false]
              [else (loop (rest lop))]))))

(define (find-primes n)
  (cond [(<= n 1) '()]
        [(=  n 2) '(2)]
        [(=  n 3) '(2 3)]
        [else
         (let loop ([lop '(2 3)] [i 5])
           (cond [(> i n) lop]
                 [(is-prime? i lop) (loop (append lop (list i)) (+ i 2))]
                 [else (loop lop (+ i 2))]))]))

一些优化与语言相关,其他优化是算法:

  • 递归被转换为尾部位置。通过这种方式,递归调用是我们在每一步中做的最后一件事,除此之外别无其他事情 - 编译器可以将其优化为与其他编程语言中的循环一样高效。
  • find-primes中的循环被修改为仅迭代奇数。请注意,我们从3转到n,而不是从n转到2
  • divisible已内联,(sqrt n)仅计算一次。
  • is-prime?只检查sqrt(n),之后查找素数是没有意义的。这是最重要的优化,而不是O(n)算法现在是O(sqrt(n))
  • 遵循@五法律的建议,is-prime?现在在n与1或5模6不一致时跳过检查。

此外,通常我建议使用cons代替append来构建列表,但在这种情况下,我们需要按照最重要的升序构建素数列表is-prime?中的优化工作。

答案 1 :(得分:1)

此处Óscar López&#39; code,调整以自上而下的方式构建列表:

(define (is-prime? n lop)
  (define sqrtn (sqrt n))
  (let loop ([lop lop])
        (cond [(or (empty? lop) (< sqrtn (mcar lop))) true]
              [(zero? (modulo n (mcar lop))) false]
              [else (loop (mcdr lop))])))

(define (find-primes n)
  (let* ([a (mcons 3 '())]
         [b (mcons 2 a)])
    (let loop ([p a] [i 5] [d 2]     ; d = diff +2 +4 +2 ...
                           [c 2])    ; c = count of primes found
           (cond [(> i n) c]
                 [(is-prime? i (mcdr a))
                       (set-mcdr! p (mcons i '()))
                       (loop (mcdr p) (+ i d) (- 6 d) (+ c 1))]
                 [else (loop p        (+ i d) (- 6 d)    c   )]))))

在约 ~n 1.25..1.32 empirically运行;与原始的 ~n 1.8..1.9 相比,在测量范围内,DrRacket( append 内部是这种不良行为的罪魁祸首)。 &#34;在两秒钟内&#34; 100K变为 0.05秒;两秒钟让你远远高于1M(一百万):

; (time (length (find-primes 100000)))  ; with cons        times in milliseconds

; 10K 156 ; 20K 437  ; 40K 1607 ; 80K 5241 ; 100K  7753   .... n^1.8-1.9-1.7    OP's
; 10K  62 ; 20K 109  ; 40K  421 ; 80K 1217 ; 100K  2293   .... n^1.8-1.9     Óscar's

; mcons:
(time (find-primes 2000000))

; 100K 47 ; 200K 172 ; 1M 1186 ; 2M 2839 ; 3M 4851 ; 4M 7036 .... n^1.25-1.32    this
; 9592      17984      78498     148933    216816    283146

它仍然只是一个试验部门...... :) sieve of Eratosthenes会更快。

编辑:至于set-cdr!,很容易用它来模拟任何惰性算法...否则,我们可以使用可扩展数组(lists of...), 摊销 O(1) snoc / append1操作(这是很多很多编码);或维持分为两部分的素数列表(实际上是三部分;参见下面的代码),用cons反向构建第二部分,并且每隔一段时间将其反向追加到第一部分(具体来说,判断)下一个素数方块的需要:

; times:               ; 2M 1934 ; 3M 3260 ; 4M 4665 ; 6M 8081 .... n^1.30

;; find primes up to and including n, n > 2
(define (find-primes n)
  (let loop ( [k 5] [q 9]              ; next candidate; square of (car LOP2)
                    [LOP1 (list 2)]    ; primes to test by
                    [LOP2 (list 3)]    ; more primes
                    [LOP3 (list  )] )  ; even more primes, in reverse
    (cond [ (> k n)
            (append LOP1 LOP2 (reverse LOP3)) ]
          [ (= k q) 
            (if (null? (cdr LOP2))
              (loop    k     q  LOP1 (append LOP2 (reverse LOP3)) (list)) 
              (loop (+ k 2)   
                      (* (cadr LOP2) (cadr LOP2))          ; next prime's square
                        (append LOP1 (list (car LOP2)))
                                     (cdr LOP2)      LOP3 )) ]
          [ (is-prime? k (cdr LOP1))
              (loop (+ k 2)  q  LOP1  LOP2   (cons k LOP3))  ]
          [ else
              (loop (+ k 2)  q  LOP1  LOP2           LOP3 )  ])))

 ;; n is the number to check, lop is list of prime numbers to check it by
(define (is-prime? n lop)
  (cond [ (null? lop) #t ]
        [ (divisible n (car lop)) #f ]   
        [ else (is-prime? n (cdr lop)) ]))

edit2:尽可能最简单,最简单的解决方法是 解耦 结果列表的素数计算,以及用于检查可分性的列表。在你的

      (local [(define LOP (find-primes (sub1 n)))]
        (if (is-prime? n LOP)

LOP用作要检查的素数列表,并在

中作为结果列表的一部分重复使用
            (append LOP (list n))
                  LOP))))
之后立刻。打破这种纠缠使我们能够在sqrt的上限停止生成测试素数列表,从而为我们提供:

;times:                    ; 1M-1076  2M-2621  3M-4664   4M-6693
                           ;           n^1.28   ^1.33     n^1.32
(define (find-primes n)
  (cond
    ((<= n 4)  (list 2 3))
    (else
     (let* ([LOP (find-primes (inexact->exact (floor (sqrt n))))] 
            [lp  (last LOP)])
       (local ([define (primes k ps)
                  (if (<= k lp)  
                      (append LOP ps)
                      (primes (- k 2) (if (is-prime? k LOP)
                                          (cons k ps) 
                                          ps)))])
         (primes (if (> (modulo n 2) 0) n (- n 1)) '()))))))

它也使用与问题中相同的is-prime?代码,不变,与上面的第二个变体一样。

它比第二个变种慢。算法的原因很清楚 - 它通过相同的素数列表测试从sqrt(n)n的所有数字,所有素数都小于或等于sqrt(n) - 但在测试给定的素数{ {1}}仅使用不大于p < n而不是sqrt(p)的素数就足够了。但它最接近您的原始代码。

为了比较,在类似Haskell的语法中,在严格的评估下,

sqrt(n)

isPrime n lop = null [() | p <- lop, rem n p == 0] -- OP: findprimes 2 = [2] findprimes n = lop ++ [n | isPrime n lop] where lop = findprimes (n-1) = lop ++ [n | n <- [q+1..n], isPrime n lop] where lop = findprimes q ; q = (n-1) -- 3rd: findprimes n | n < 5 = [2,3] findprimes n = lop ++ [n | n <- [q+1..n], isPrime n lop] where lop = findprimes q ; q = floor $ sqrt $ fromIntegral n -- 2nd: findprimes n = g 5 9 [2] [3] [] where g k q a b c | k > n = a ++ b ++ reverse c | k == q, [h] <- b = g k q a (h:reverse c) [] | k == q, (h:p:ps) <- b = g (k+2) (p*p) (a++[h]) (p:ps) c | isPrime k a = g (k+2) q a b (k:c) | otherwise = g (k+2) q a b c b一起(也就是说,代码中的cLOP2)实际上构成了一个纯粹的功能性队列a-la Okasaki,来自在维护的素数前缀LOP3(即a)的末尾,在每个连续的素数的正方形上传递哪些连续素数并追加LOP1用于a的素性测试。

由于这种附加的罕见性,其计算效率低下对整体代码的时间复杂性没有影响。