1.快速求冪演算法
在這篇文章我會展示怎樣通過求一個數的冪的基本思路,來引導我們發現一些抽象的東西比如半群和含么半群。
有一個很有名的對一個數求冪的演算法,也就是說,求一個數x的n次方或者這樣簡單表示:x^n。Donald Knuth在TAOCP的4.63節 求冪值中提出這個演算法。
這個演算法很簡單的實現就是x乘以自己n次,但是在這裡當然會提供一種比這種方式更快的演算法。正在談論的演算法通常被稱作二進位制法(binary method)、梯度求冪(the powering ladder)或者反覆平方法(repeated-squaring algorithm)
假設我們想計算2^23,在這裡x = 2,n = 23,這個演算法首先把23表示成二進位制的形式10111。掃描這個二進位制數(10111)每當遇到0或1,則相應的求x的平方或者乘以x。
這個方法有一個問題就是它掃描二進位制表示的數是從左到右進行的,但是對於計算機通常以相反的方向能夠更容易實現,因此Knuth提出一個替代的演算法。
一個出自TAOCP的4.63節的演算法A的簡單實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
; html-script: false ] function power1($x, $n) { $y = 1; while (true) { $t = $n % 2; $n = floor($n/2); if ($t == 1) { $y = $y * $x; } if ($n == 0) { break; } $x = $x * $x; } return $y; } |
這個函式需要兩個整數,$x和$n然後返回$x的$n次冪作為結果。
首先建立一個輔助變數$y並且初始化為1,把它作為乘法的主體。
然後函式在每次迴圈迭代的時候掃描$n
的二進位制表示的數。如果遇到1則$y乘上$x,然後賦值回$y。每次迴圈都會計算$x的平方,並且把它賦值回$x。
遇到1意味著當前$n的值不能被2整除,換句話說就是,$n % 2 == 1。
同樣的每次迴圈$n都會折半,然後向下取整得到結果。當$n等於0的時候,我們結束迴圈並且返回$y的值。
函式power能夠這樣被呼叫:
1 2 |
; html-script: false ] 1024 == power1(2, 10); => true |
我能想象你現在就像這個gif中的男孩。
儘管這個演算法看起來像一個 “呵呵,真有意思” (無語了,你別說了,我根本不關心)的故事,實際上當它用來計算非常的大數時時十分高效的。例如有很多的素數測試演算法都是依賴這個演算法的不同變式。
2.增加一些抽象
到目前為止還沒有什麼意想不到的事情發生,但是如果我們注意到求一個數的冪實際上和一個數自乘多次是等價的,我們也可以看到乘法實際上等價於自加多次。舉個例子2 * 5
能夠像這樣被計算2 + 2 + 2 + 2 + 2
。
我們能把這個演算法轉換成一種更普遍的形式使它能同樣應用在乘法還有加法上嗎?當然可以,我們僅僅需要改變幾樣東西。
在當前實現中,我們建立$y作為乘法的主體,並設定為1。如果我們想把演算法用在加法上,我們需要把$y設定為0。因此我們僅需要改變函式的單位元素的值。
第二步要提供一個函式給我們的演算法,它能夠作乘法或者加法。為了實現這個目的我們會傳遞一個擔當二元運算的函式。例如:一個需要兩個引數的函式。這個函式需要遵循以下的規則。必須滿足:a·( b · c ) = (a · b ) · c
。還要求返回結果的型別必須和兩個輸入引數的型別一致。
幸運的是加法和乘法都滿足結合律,因此我們能夠僅在一個函式中包含他們然後把它傳遞給我們的power演算法。
這裡是這個演算法新的實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
; html-script: false ] function power2($x, $n, $id, $f) { $y = $id; while (true) { $t = $n % 2; $n = floor($n/2); if ($t == 1) { $y = $f($y, $x); } if ($n == 0) { break; } $x = $f($x, $x); } return $y; } |
我們能夠像這樣呼叫它:
1 2 |
; html-script: false ] 1024 == power2(2, 10, 1, function ($a, $b) { return $a * $b; }); => true |
記住傳遞進我們演算法的運算必須是可結合的,舉個例子,減法不能被用在這裡由於10 - ( 5 - 3) = 8
但是(10 - 5 ) - 3 = 2
。
3.附加更抽象的概念
從數學的角度說這個演算法能夠在任何滿足結合律的代數結構中有效(在這個案例中就是整數的乘法和加法),換言之,它能夠用在半群中,引用一本關於群論的書。
1 2 |
一個半群的集合S含有一個可結合的運算 · ; 也就是說,x·(y · z) = (x · y) · z 對於所有的x, y, z ∈ S都成立。 |
同樣,這個集合必須有一個單位元素
使得它有一個獨異點:
1 |
一個獨異點是一個集合M含有一個可結合運算·;伴有一個單位元素e∈ M滿足e·x = x· e = x對於所有x∈ M都成立。 |
在這個預設條件下,有什麼我們經常用在程式設計上的結構能使用這個演算法的呢?如果你是一個web開發者,你不需要費大力氣去獲取strings。對於字串(strings),使用string append作為二元操作而且空字串(empty string)作為單位元素同樣會帶來類似的結果。如果一個字串想重複n次,我們建立下面的函式:
1 2 3 4 5 |
; html-script: false ] function repeat($s, $n) { return power2($s, $n, "", function ($a, $b) { return $a . $b; }); } |
測試:
1 2 |
; html-script: false ] "aaaaaaaaaa" == repeat("a", 10); => true |
現在考慮一下陣列(arrays)(或者其它語言稱為列表(lists))。我們想把一個陣列複製n次。在這裡空陣列是單位元素,對PHP來說array_merge會用來作為二元操作。
1 2 3 4 5 |
; html-script: false ] function repeat_el($el, $n) { return power2(array($el), $n, array(), function ($a, $b) { return array_merge($a, $b); }); } |
結果:
1 2 3 |
; html-script: false ] $arr = repeat_el("a", 10); 10 == count($arr); => true |
從上不難看出,像求一個數冪運算的這樣簡單事情給我們帶來一個優雅的演算法,它能被運用一些事情上,像重複的東西還有陣列裡的元素。
4.延伸閱讀
- 這裡的快速求冪演算法是基於TAOCP中,
卷二
的4.63節
。 - 所有的關於工作原理的解答都可以在TAOCP或者在這本書《A Computational Introduction to Number Theory and Algebra》上找到,這本書的PDF版本在作者的主頁上可以免費下載。瀏覽章節:
“Computing with large integers - The repeated squaring algorithm”
- 如果你想學習這個演算法的一些用法或者想知道更多這個演算法背後的理論,請查閱這本叫做《Elements of Programming》的書。這本書非常了不起,它定義了不同型別的函式和使用型別系統確定函式是否是可結合的,二元的等等。作者是
C++STL
的設計者,所以這本書的內容可能會比較理論化
,然後它能夠直接應用在物件導向程式設計(OOP)。 - 半群 和 含么半群的引用來自於《Handbook of Computational Group Theory.》。一本非常有趣的書,如果你對計算群論有興趣的話。
- 如果你想學習更多有關
么半群
還有它們的實現。《Learn You a Haskell》裡的有個章節非常有趣的介紹它:Functors, Applicative Functors and Monoids - 這是一個十分有趣的練習,通過實現這些概念使用PHP和OOP,對於不喜歡使用PHP無愛的人,也可以選擇其它你喜歡的語言。
5.你是想說Haskell?
既然我已經提及一本Haskell的書,這裡有一個Haskell實現的求冪演算法,使用的遞迴演算法來自於這本書《Prime Numbers: A Computational Perspective》
1 2 3 4 5 6 7 |
; html-script: false ] power :: (Eq a, Integral b) => (a -> a -> a) -> a -> b -> a power f a n | n == 1 = a | even n = square a (n `div` 2) | otherwise = f a (square a ((n-1) `div` 2)) where square a' n' = f (power f a' n') (power f a' n') |
幾個函式呼叫的結果:
1 2 3 4 5 6 7 8 9 |
; html-script: false ] *Main> :load pow.hs [1 of 1] Compiling Main ( pow.hs, interpreted ) Ok, modules loaded: Main. *Main> power (*) 2 10 1024 *Main> power (+) 2 10 20 *Main> power (++) "a" 10 "aaaaaaaaaa" |
正如你所看到的,這個函式呼叫一個function(a->a->a),例子中,對於integers使用*
或者+
,對於lists使用++
。
我希望你會覺得這邊文章有趣或者激起你學習與程式設計有關的數學的慾望。因為我認為我們掌握得越多數學方面的知識,我們就能更好的使用抽象的東西。