讓我重新編寫一段代碼,將其分解成更容易討論的代碼。我使用的是相同的算法,我只是將一些內部表單分解成單獨的函數。
(declare primes) ;; declare this up front so we can refer to it below
(defn is-relatively-prime? [n candidates]
(if (= 0 (count candidates))
true
(if (zero? (rem n (first candidates)))
false
(is-relatively-prime? n (rest candidates)))))
(defn get-next-prime [largest-prime-so-far]
(let [primes-so-far (concat (take-while #(not= largest-prime-so-far %) primes) [largest-prime-so-far])]
(loop [n (+ (last primes-so-far) 2)]
(if
(is-relatively-prime? n (rest primes-so-far))
n
(recur (+ n 2))))))
(def primes
(lazy-cat '(2 3) (map get-next-prime (rest primes))))
(time (let [p (doall (take 200 primes))]))
最後一行只是爲了更容易在REPL中獲得一些非常粗糙的基準。通過將時序聲明作爲源文件的一部分,我可以不斷重新加載源代碼,並且每次都獲得新的基準。如果我只加載一次文件,並繼續嘗試執行(take 500 primes)
,則基準將會傾斜,因爲primes
將保留其已計算的素數。我還需要doall
,因爲我在let
聲明中提取素數,如果我不使用doall
,它只會將延遲序列存儲在p
中,而不是實際計算素數。
現在,讓我們得到一些基本值。在我的電腦,我得到這個:
Loading src/scratch_clojure/core.clj... done
"Elapsed time: 274.492597 msecs"
Loading src/scratch_clojure/core.clj... done
"Elapsed time: 293.673962 msecs"
Loading src/scratch_clojure/core.clj... done
"Elapsed time: 322.035034 msecs"
Loading src/scratch_clojure/core.clj... done
"Elapsed time: 285.29596 msecs"
Loading src/scratch_clojure/core.clj... done
"Elapsed time: 224.311828 msecs"
所以約275毫秒,或採取50.我的第一個懷疑是我們如何讓primes-so-far
在let
語句中get-next-prime
。我們正在通過完整的素數列表(直到我們擁有它),直到我們得到一個與迄今爲止最大的素數相等的數。然而,我們構建代碼的方式,所有素數都已排序,所以我們有效地遍歷除最後一個素數以外的所有素數,然後連接最後一個值。我們得到的結果與primes
序列中已經實現的完全相同,因此我們可以跳過整個步驟,只使用primes
。這應該可以拯救我們
我的下一個懷疑是呼叫(last primes-so-far)
在循環中。當我們在一個序列上使用last
函數時,它也從頭到尾拖曳列表(或者至少,這是我的理解 - 我不會讓它通過Clojure編譯器編寫者, case code來加快速度。)但是,我們不需要它。我們打電話get-next-prime
與largest-prime-so-far
,並且由於我們的素數是有序的,就已經意識到它們而言,這已經是素數的最後一個了,所以我們可以使用largest-prime-so-far
而不是(last primes)
。這會給我們這樣的:
(defn get-next-prime [largest-prime-so-far]
; deleted the let statement since we don't need it
(loop [n (+ largest-prime-so-far 2)]
(if
(is-relatively-prime? n (rest primes))
n
(recur (+ n 2)))))
這似乎像它應該加快速度,因爲我們已經消除了兩個完整的走過的素數序列。我們來試試吧。
Loading src/scratch_clojure/core.clj... done
"Elapsed time: 242.130691 msecs"
Loading src/scratch_clojure/core.clj... done
"Elapsed time: 223.200787 msecs"
Loading src/scratch_clojure/core.clj... done
"Elapsed time: 287.63579 msecs"
Loading src/scratch_clojure/core.clj... done
"Elapsed time: 244.927825 msecs"
Loading src/scratch_clojure/core.clj... done
"Elapsed time: 274.146199 msecs"
嗯,也許稍微好一些(?),但沒有接近我預期的改善。我們來看看is-relatively-prime?
的代碼(正如我重寫的那樣)。首先跳出來的是count
函數。 primes
序列是一個序列,而不是一個向量,這意味着count
函數也必須遍歷完整列表以添加其中的元素。更糟糕的是,如果我們從10個候選人名單開始,第一次走完整個循環,然後走下一個循環的剩餘九個候選人,然後剩下8個候選人,依此類推。隨着質數越來越大,我們將在計數函數中花費越來越多的時間,所以也許這就是我們的瓶頸。
我們想擺脫那count
,並且這表明我們可以使用if-let
做更循環的方式。就像這樣:
(defn is-relatively-prime? [n candidates]
(if-let [current (first candidates)]
(if (zero? (rem n current))
false
(recur n (rest candidates)))
true))
的(first candidates)
函數將返回nil
如果候選人列表是空的,如果出現這種情況,if-let
功能會發現,並自動跳轉到else子句,在這種情況下是我們的返回結果的「真」。否則,我們將執行「then」子句,並且可以測試n
是否可以被當前候選人整除。如果是的話,我們會回覆錯誤的,否則我們會和其他候選人一起回頭。我也利用了zero?
的功能,只是因爲我可以。讓我們看看這給我們帶來了什麼。
Loading src/scratch_clojure/core.clj... done
"Elapsed time: 9.981985 msecs"
Loading src/scratch_clojure/core.clj... done
"Elapsed time: 8.011646 msecs"
Loading src/scratch_clojure/core.clj... done
"Elapsed time: 8.154197 msecs"
Loading src/scratch_clojure/core.clj... done
"Elapsed time: 9.905292 msecs"
Loading src/scratch_clojure/core.clj... done
"Elapsed time: 8.215208 msecs"
很有戲劇性,呃?我是一箇中級Clojure編碼器,對內部構件有一個非常粗略的理解,所以我用一粒鹽來分析我的分析結果,但基於這些數字,我猜你會被count
咬傷。
有「快」的代碼是使用你的是不是一個其他的優化,而這想逃出來的is-relatively-prime?
測試時current
平方比n
更大---你可能會加速你的代碼多一些,如果你能把它扔進去。但我認爲count
是你要找的主要東西。
10000是兩個數量級比100慢。只是說'。 – m0skit0
https://en.wikibooks.org/wiki/Some_Basic_and_Inefficient_Prime_Number_Generating_Algorithms#PGsimple3 – leeor
問題是,https://zach.se/project-euler-solutions/7/上的解決方案瞬間獲得10000。我的功能做了相同數量的分區,需要將近一個小時。我的懶惰seq用法有什麼問題嗎? – macdeveloperprime