漫畫演算法:輾轉相除法是什麼鬼?

玻璃貓發表於2016-09-30

大四畢業前夕,計算機學院的小灰又一次頂著炎炎烈日,

去某IT公司面試研發工程師崗位……

%e5%b0%8f%e4%bb%93%e9%bc%a0%e8%b5%b6%e8%b7%af

半小時後,公司會議室,面試開始……

%e5%b0%8f%e4%bb%93%e9%bc%a0%e9%9d%a2%e8%af%95%e5%ae%981

漫畫演算法:輾轉相除法是什麼鬼?

%e5%b0%8f%e4%bb%93%e9%bc%a0%e9%9d%a2%e8%af%95%e5%ae%987

%e5%b0%8f%e4%bb%93%e9%bc%a0%e5%9b%9e%e7%ad%944

%e5%b0%8f%e4%bb%93%e9%bc%a0%e9%9d%a2%e8%af%95%e5%ae%985

%e9%9d%a2%e8%af%95%e5%ae%98%e5%82%b2%e6%85%a22

%e5%b0%8f%e4%bb%93%e9%bc%a0%e8%8b%a6%e6%80%9d

小灰奮筆疾書,五分鐘後……

%e5%b0%8f%e4%bb%93%e9%bc%a0%e5%9b%9e%e7%ad%945

%e4%bb%a3%e7%a0%811

小灰的思路十分簡單。他使用暴力列舉的方法,試圖尋找到一個合適的整數 i,看看這個整數能否被兩個整型引數numberA和numberB同時整除。

這個整數 i 從2開始迴圈累加,一直累加到 numberA 和 numberB 中較小引數的一半為止。迴圈結束後,上一次尋找到的能夠被兩數整除的最大 i 值,就是兩數的最大公約數。

%e5%b0%8f%e4%bb%93%e9%bc%a0%e9%9d%a2%e8%af%95%e5%ae%988

%e9%9d%a2%e8%af%95%e5%ae%98%e5%82%b2%e6%85%a2

%e5%b0%8f%e4%bb%93%e9%bc%a0%e8%8b%a6%e6%80%9d

%e5%b0%8f%e4%bb%93%e9%bc%a0%e6%97%a0%e5%a5%88

%e5%b0%8f%e4%bb%93%e9%bc%a0%e9%9d%a2%e8%af%95%e5%ae%980

%e5%b0%8f%e4%bb%93%e9%bc%a0%e5%a4%b1%e6%9c%9b

事後,垂頭喪氣的小灰去請教同系的學霸大黃……

%e5%b0%8f%e4%bb%93%e9%bc%a0%e8%ae%b2%e8%a7%a33

%e5%b0%8f%e4%bb%93%e9%bc%a0%e6%97%a0%e5%a5%882

%e5%b0%8f%e4%bb%93%e9%bc%a0%e8%ae%b2%e8%a7%a34

輾轉相除法, 又名歐幾里得演算法(Euclidean algorithm),目的是求出兩個正整數的最大公約數。它是已知最古老的演算法, 其可追溯至公元前300年前。

這條演算法基於一個定理:兩個正整數a和b(a>b),它們的最大公約數等於a除以b的餘數c和b之間的最大公約數。比如10和25,25除以10商2餘5,那麼10和25的最大公約數,等同於10和5的最大公約數。

%e6%ac%a7%e5%87%a0%e9%87%8c%e5%be%97

有了這條定理,求出最大公約數就簡單了。我們可以使用遞迴的方法來把問題逐步簡化。

首先,我們先計算出a除以b的餘數c,把問題轉化成求出b和c的最大公約數;然後計算出b除以c的餘數d,把問題轉化成求出c和d的最大公約數;再然後計算出c除以d的餘數e,把問題轉化成求出d和e的最大公約數……

以此類推,逐漸把兩個較大整數之間的運算簡化成兩個較小整數之間的運算,直到兩個數可以整除,或者其中一個數減小到1為止。

%e5%b0%8f%e4%bb%93%e9%bc%a0%e8%ae%b2%e8%a7%a35b

%e5%b0%8f%e4%bb%93%e9%bc%a0%e5%9b%9e%e7%ad%946

 

五分鐘後,小灰改好了程式碼……

%e4%bb%a3%e7%a0%812

%e5%b0%8f%e4%bb%93%e9%bc%a0%e8%ae%b2%e8%a7%a37

%e5%b0%8f%e4%bb%93%e9%bc%a0%e8%8b%a6%e6%80%9d3

%e5%b0%8f%e4%bb%93%e9%bc%a0%e8%ae%b2%e8%a7%a38

更相減損術, 出自於中國古代的《九章算術》,也是一種求最大公約數的演算法。

他的原理更加簡單:兩個正整數a和b(a>b),它們的最大公約數等於a-b的差值c和較小數b的最大公約數比如10和25,25減去10的差是15,那麼10和25的最大公約數,等同於10和15的最大公約數。

%e4%b9%9d%e7%ab%a0%e7%ae%97%e6%9c%af

由此,我們同樣可以通過遞迴來簡化問題。首先,我們先計算出a和b的差值c(假設a>b),把問題轉化成求出b和c的最大公約數;然後計算出c和b的差值d(假設c>b),把問題轉化成求出b和d的最大公約數;再然後計算出b和d的差值e(假設b>d),把問題轉化成求出d和e的最大公約數……

以此類推,逐漸把兩個較大整數之間的運算簡化成兩個較小整數之間的運算,直到兩個數可以相等為止,最大公約數就是最終相等的兩個數。

%e5%b0%8f%e4%bb%93%e9%bc%a0%e8%ae%b2%e8%a7%a39

%e5%b0%8f%e4%bb%93%e9%bc%a0%e5%9b%9e%e7%ad%946

五分鐘後,小灰重寫了程式碼……

%e4%bb%a3%e7%a0%813

%e5%b0%8f%e4%bb%93%e9%bc%a0%e8%ae%b2%e8%a7%a310

%e5%b0%8f%e4%bb%93%e9%bc%a0%e8%8b%a6%e6%80%9d4

%e5%b0%8f%e4%bb%93%e9%bc%a0%e8%ae%b2%e8%a7%a311

%e5%b0%8f%e4%bb%93%e9%bc%a0%e6%b2%89%e6%80%9d4

%e5%b0%8f%e4%bb%93%e9%bc%a0%e8%ae%b2%e8%a7%a312

眾所周知,移位運算的效能非常快。對於給定的正整數a和b,不難得到如下的結論。其中gcb(a,b)的意思是a,b的最大公約數函式:

當a和b均為偶數,gcb(a,b) = 2*gcb(a/2, b/2) = 2*gcb(a>>1, b>>1)

當a為偶數,b為奇數,gcb(a,b) = gcb(a/2, b) = gcb(a>>1, b)

當a為奇數,b為偶數,gcb(a,b) = gcb(a, b/2) = gcb(a, b>>1)

當a和b均為奇數,利用更相減損術運算一次,gcb(a,b) = gcb(b, a-b), 此時a-b必然是偶數,又可以繼續進行移位運算。

比如計算10和25的最大公約數的步驟如下:

  1. 整數10通過移位,可以轉換成求5和25的最大公約數
  2. 利用更相減損法,計算出25-5=20,轉換成求5和20的最大公約數
  3. 整數20通過移位,可以轉換成求5和10的最大公約數
  4. 整數10通過移位,可以轉換成求5和5的最大公約數
  5. 利用更相減損法,因為兩數相等,所以最大公約數是5

在兩數比較小的時候,暫時看不出計算次數的優勢,當兩數越大,計算次數的節省就越明顯。

%e5%b0%8f%e4%bb%93%e9%bc%a0%e8%ae%b2%e8%a7%a313

%e4%bb%a3%e7%a0%814

最後總結一下上述所有解法的時間複雜度:

1.暴力列舉法:時間複雜度是O(min(a, b)))

2.輾轉相除法:時間複雜度不太好計算,可以近似為O(log(min(a, b))),但是取模運算效能較差。

3.更相減損術:避免了取模運算,但是演算法效能不穩定,最壞時間複雜度為O(max(a, b)))

4.更相減損術與移位結合:不但避免了取模運算,而且演算法效能穩定,時間複雜度為O(log(max(a, b)))

%e5%b0%8f%e4%bb%93%e9%bc%a0%e5%9b%9e%e7%ad%947

%e5%b0%8f%e4%bb%93%e9%bc%a0%e8%ae%b2%e8%a7%a314

本文原本只寫到輾轉相除法就終告結束,後來網友們指出還有更優化的解法,看來自己還是才疏學淺,很感謝大家指出問題。另外,方法的引數預設必定是正整數,所以在程式碼中省去了合法性檢查。

文中描述的更相減損術是簡化了的方式。在九章算術原文中多了一步驗證:如果兩數都是偶數,計算差值之前會首先讓兩個數都折半,使得計算次數更少。這種方法做到了部分優化,但古人似乎沒想到一奇一偶的情況也是可以優化的。

由於篇幅所限,本文省略了關於輾轉相除法原和更相減損術的原理及證明。其實證明過程並不複雜,細心的同學們也可以自己嘗試研究一下。謝謝大家的捧場!

 

本人微訊號:bjweimengshu

歡迎朋友們一起交流討論,加好友請註明伯樂線上 :)

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

漫畫演算法:輾轉相除法是什麼鬼?

相關文章