Recursion แบบเร็วขึ้นแต่ออกแรงนิดเดียว

สมัยที่เรียนหนังสือเขามักจะเอา Fibonacci number มาเป็นตัวอย่างกัน function ก็เป็นงี้ครับ

fib(0) = 0 fib(1) = 1 fib(n) = fib(n-1) + fib(n-2) สำหรับ n มากกว่า 1

ซึ่งเอามาแปลงเป็น Clojure ได้แบบตรงไปตรงมาเลย

(def fib
  (fn [n]
    (if (<= n 1) n
        (+ (fib (- n 1)) (fib (- n 2))))))

ผมลองรันบนเครื่องผมใช้เวลา 4.324 วินาที ซึ่งมันช้าเพราะไปเรียก fib ซ้ำไปซ้ำมา ดูง่าย ๆ fib(4) ก็ไปเรียก fib(3), fib(2) แล้ว fib(3) ก็ไปเรียก fib(2) อีกซ้ำ พอเลขมากก็ซ้ำไปเรื่อย ๆ

จริง ๆ มันมีวิธีหล่อ ๆ เยอะแยะที่จะแก้ปัญหานี้ให้เร็วขึ้น แต่ถ้าเราไม่ต้องหล่อมาแก้แบบดอกเดียวเสร็จก็ใช้ memoize เขียนแบบนี้

(def fib
  (memoize
   (fn [n]
     (if (<= n 1) n
         (+ (fib (- n 1))
            (fib (- n 2)))))))
(println (time (fib 42)))

แก้นิดเดียวเอา memoize มาครอบ function ไว้ มันจะจำ function ที่เคยเรียกไว้แล้วส่งผลกลับมาเลยโดยไม่ต้องไล่ยาว ผลคือโปรแกรมใช้เวลารันเหลือ 0.820 วินาที

จริง ๆ มันจบแล้วล่ะ แต่เขียนให้ดูอีก function แบบใช้ loop

(defn fib [n]
  (loop [i 1 acc0 0 acc1 1]
    (if (< i n)
      (recur (inc i) acc1 (+ acc0 acc1))
      acc1)))      
(println (time (fib 42)))

อันนี้ใช้เวลาแค่ 0.0976 วินาที แต่ก็จะเห็นได้ว่าโปรแกรมเปลี่ยนไปเยอะ เปลี่ยนวิธีคิดเยอะ memoize นี่มันแทบไม่ต้องคิดต้องเขียนอะไรมากเลย ใส่ไปแล้วได้ผลเลย

ขอบคุณ visibletrap@twitter.com ที่แนะนำ function time มาครับ