a ameoeba alba samba marimba...
這樣結束:
...megahertz gigahertz jazz buzz fuzz
有了這麼一個表,詩人會爽很多。
算法:得到單詞表後,先把單詞反序,用sort函數對所有反序單詞排序,排好後再反序。
我嘰裏呱啦地寫完了,用了630KB的2100行的文本來測試一下。time一下main函數,30多秒……現在用瞭如題的兩個工具,程序跑到了0.08s!
首先,我處理重複單詞的算法超級低效,先是在read-word函數裏,不管是否當前單詞已記錄,都把當前單詞記錄下來,然後:(setf words (delete-duplicates words :test #'equal :end n))
用delete-duplicates函數只保留相同單詞的最後一個。
該函數的工作原理是兩個兩個比較,然後把前者丟棄掉,如果:from-end是true的話,那把後者丟棄掉。顯然的是,它要比較很多,如果一個單詞重複很多次的話,那要經過很多輪的比較,元素移動很多。具體是如何經過很多輪的、爲什麼很慢的,我也不懂,望高手指點。
如果我在添加新單詞的時候:
(when (not (find word words :test #'string-equal))
(vector-push-extend word words 10000)
(incf n))
單詞不存在的時候才加入。這樣一來,程序就飛到了4s。我是怎麼知道delete-duplicates這裏慢了呢?高手可以靜態分析……當然,很容易猜錯,分析失誤。
(defun main ()
(defvar n)
(time (setf n (read-word "source.txt")))
(time (setf words (delete-duplicates words :test #'equal :end n)))
(time (reverse-words words))
(time (sort words #'string< #'nreverse))
(time (reverse-words words))
(time (write-word "rhymes.txt")))
這樣子給每個函數time一下。在c++中,即是clock()/CLOCKS_PER_SEC。有了它,就是爲啥“過早優化是一切罪惡的根源”。非常令我驚訝的是,一開始總共37s的時間,36s耗在 delete-duplicates上。按道理來說,我應該用36/37的優化時間都花在這部分。如果過早優化的話,一方面浪費時間在無關痛癢的優化上;另一方面,過早優化,代碼更復雜,使得後面越來越難以修改,也容易多錯誤。所以,面對一問題,應該儘快地搗鼓出簡單的第一版,之後再考慮優化。
接下來,用hash-table來記錄單詞,像這樣
(defparameter ht (make-hash-table :test #'equal :size 10000))
(when (not (gethash word ht))
(setf (gethash word ht) t)
(vector-push-extend word words 10000))
這樣就優化到了0.08s。
另外,在處理的過程中,不希望有那麼多中間數組,故用map-into,像這樣
(map-into seq fn seq)
函數fn對seq的每一個元素調用後結果保存到seq中。希望數組v每個數都加1可寫成:
(setf v (map-into v #'1+ v))
所以,最終我把反序、排序、再反序、輸出,寫成了這樣:
(defun xform (fn seq)
(map-into seq fn seq))
(defun write-word (to)
(with-open-file (str to :direction :output
:if-exists :supersede)
(map nil #'(lambda (x)
(fresh-line str)
(princ x str))
(xform #'nreverse
(sort (xform #'nreverse words)
#'string<)))))
事實上,這照搬了《Ansi Common Lisp》的代碼。
代碼見:https://github.com/lzwjava/rhymes