最大公約數(Greatest Common Divisor)【算法及實現】

概述

最大公約數(Greatest Common Divisor),也稱最大公因數、最大公因子。是指兩個或多個整數公有因數中最大的一種。編程時常用 gcd(a, b) 表示兩個數的最大公約數。

相反的,將兩個或多個整數公有的倍數叫做它們的公倍數,其中除0以外最小的一個公倍數就叫做這幾個整數的最小公倍數(Least Common Multiple)。編程時候用 lcm(a, b) 表示兩個數的最大公約數。

其中 lcm(a, b) = a * b / gcd(a, b);

所以在計算 gcd 和 lcm 時,通常只需要計算出 gcd 即可。本文中也主要闡述最大公約數的算法。

 

算法實現

在算法學習的過程中,對於最大公約數求解的算法有很多,常見的有窮舉法,分解質因數法,短除法,更相減損法,歐幾里得算法(輾轉相除法)等。這裏筆者對於最大公約數求解的常見算法及實現進行簡單是闡述。

窮舉法

窮舉法實現最大公約數的思路較爲簡單。其算法步驟如下

gcd(m ,n)

1.找出參數m,n中較小的一個。並以此作爲循環的起點i;

2.檢查 i 是否。若 i 爲m,n的公因數,那麼 i 即爲m,n的最大公因數;

3.若 i 不爲m,n的公因數,那麼就對 i 自減,直至滿足步驟2的條件或者 i == 1。

該算法建立在兩個數m,n的最大公因數一定不大於min(m, n)這一合理推論之上。只要從min(m, n)向下自減,遇到的第一個公因數就一定是最大公因數。該算法實現如下

//窮舉法
public static long gcd(long m, long n) {

    for (long i = Math.min(m, n); i > 1; i--) {
        if (m % i == 0 && n % i == 0) {
            return i;
        }
    }

    return 1;
}

 

分解質因數法

利用分解質因數法求解兩個數的最大公約數。把每個數分別分解質因數,再把各數中的全部公有質因數提取出來連乘,所得的積就是這幾個數的最大公約數。

該算法實現要點在於如何找出兩個數的公有質因數。筆者的實現中採用類似線性素數篩的方式,保證能求得最終所有公共質因數的積。下面給出算法步驟

1.設置保存結果的變量result,且初始化爲1。

2.置循環起點 i 爲2,最大不超過min(m, n)。當 m 或 n 等於1時,退出循環。

3.若 i 爲m,n的公約數,那麼 m /= i,n /= i,result *= i。否則 i++。直至不滿足循環條件後退出。

該算法的實現代碼如下

// 分解質因數法
public static long gcd(long m, long n) {
    long result = 1;

    for (int i = 2; i <= Math.min(m, n) && m != 1 && n != 1;) {
        if (m % i == 0 && n % i == 0) {
            m /= i;
	    n /= i;
            result *= i;
        } else {
            i++;
        }
    }

    return result;
}

需要注意的是,在分解質因數時,常用的一種方法是短除法。它同樣可以用來求解最大公約數,但其實質上只是分解質因數法的一種運用形式,所以本文中不將其作爲一種算法單獨列出。

 

更相減損法

更相減損法是出自《九章算術》的一種求最大公約數的算法,它原本是爲約分而設計的,但它適用於任何需要求最大公約數的場合。又名“更相減損術”,輾轉相減法,等值算法,尼考曼徹斯法。

《九章算術》中對這種方法的說明如下:

"可半者半之,不可半者,副置分母、子之數,以少減多,更相減損,求其等也。以等數約之。"

將其轉化爲算法步驟如下:

1.對於兩個參數m,n。若兩者都是偶數,那麼用2進行約分。否則繼續執行步驟2;

2.以較大的數減較小的數,接着把所得的差與較小的數比較,並以大數減小數。繼續這個操作,直到所得的減數和差相等爲止;

3.最後將步驟一中約掉的若干個2與步驟二中的"等數"相乘就是所求的最大公約數。

其實現代碼如下

// 更相減損法——遞歸實現
public static long gcd(long m, long n) {

    if (m == n)
        return m;
    if ((m & 1) == 0 && (n & 1) == 0)
        return gcd(m >> 1, n >> 1) * 2;
    return gcd(Math.abs(m - n), Math.min(m, n));
}

遞歸實現相對來說代碼更爲簡潔,但因爲存在方法調用,所以開銷相對更大。因此這裏給出更相減損法的循環實現。

// 更相減損法——循環實現
public static long gcd(long m, long n) {
    long mul = 1;

    while ((m & 1) == 0 && (n & 1) == 0) {
        m >>= 1;
        n >>= 1;
        mul <<= 1;
    }

    while (m != n) {
        if (m > n) {
            m = m - n;
        } else {
            n = n - m;
        }
    }
    return mul * n;
}

 

歐幾里得算法

歐幾里德算法又稱輾轉相除法,是指用於計算兩個正整數a,b的最大公約數。應用領域有數學和計算機兩個方面。計算公式gcd(a,b) = gcd(b,a mod b)。

歐幾里得算法的公式十分簡單,實現算法的代碼量也極少,是求解最大公約數的一種較好的算法。對於該算法,僅僅會使用是不夠的,所以在這裏筆者對於歐幾里得算法進行一下簡單的證明。

對於一個正整數m,它可以表示成m = kn + r(其中m,k,n,r均爲正整數,且m > n,n > r)

故 r = m mod n

假設 g 爲 m,n 的一個公約數,記作 g|m,g|n。即 m mod g == 0,n mod g == 0。

同時 r = m - kn,兩邊同除以g。得

r/g = m/g - kn/g,不難發現 m/g - kn/g 結果爲整數(因爲g|m,g|n,且k爲整數)

故 g 是 m,n,r(也即m mod n)的公約數

本着歸約的思想,得出以下公式

gcd(m, n) = gcd(n, m mod n)

假設 g 是 n,m mod n的公約數,則 g|n,g|(m - kn)(k∈N*)

進而 g|m,因此g也是m,n的公約數

故(m, n)和(n, m mod n)的公約數一致,其最大公約數也必然相等。得證

從上述的證明過程中,也不難看出輾轉相減法的影子。實際上輾轉相除法和輾轉相減法的理論基礎是相同的,只是實現的方式不同罷了。同時由於輾轉相除法能通過一次取模完成多次減法的操作,所以其運算效率較輾轉相減法更高。

下面給出算法實現

// 歐幾里得算法——循環實現
public static long gcd(long m, long n) {

    while (n != 0) {
        long rem = m;
        m = n;
        n = rem % n;
    }

    return m;
}

對於歐幾里得算法的公式而言,使用遞歸實現可以使得代碼更爲簡潔,所以這裏也給出遞歸實現

// 歐幾里得算法——遞歸實現
public static long gcd(long m, long n) {

    if (n == 0)
        return m;

    return gcd(n, m % n);
	
}

實際上這裏的遞歸實現還可以通過引入三目運算符進行進一步的縮減,代碼如下

// 歐幾里得算法——單行遞歸實現
public static long gcd(long m, long n) {

    return n == 0 ? m : gcd(n, m % n);
}

 

Stein算法

Stein算法是一種計算兩個數最大公約數的算法,是針對歐幾里德算法在對大整數進行運算時,需要試商導致增加運算時間的缺陷而提出的改進算法。

在歐幾里德算法中,有個核心就是進行取模操作。對於32位或者64位的整數而言,取模操作或者除法操作耗費的時間或許還可以接受。但是對於更大的素數,這樣的計算過程就不得不由用戶來設計,爲了計算兩個超過64位的整數的模,用戶也許不得不採用類似於多位數除法手算過程中的試商法(可以百度“高位試商法”),這個過程不但複雜,而且消耗了很多CPU時間。對於現代密碼學來說,長度大於128位的素數比比皆是(例如RSA的非對稱密鑰1024位),所以設計這樣的程序迫切希望能夠拋棄除法和取模。

由J. Stein 1961年提出的Stein算法很好的解決了歐幾里得算法中的這個缺陷,Stein算法只有整數的移位和加減法。

同樣,本文在這裏簡單的對其正確性進行簡單說明

爲了說明Stein算法的正確性,首先必須注意到以下結論:

gcd(m, m) = m,也就是一個數和其自身的公約數仍是其自身。

gcd(km, kn) = k gcd(m, n),也就是最大公約數運算和倍乘運算可以交換。特殊地,當k=2時,說明兩個偶數的最大公約數必然能被2整除。

當k與b互質,gcd(km, n)=gcd(m, n),也就是約掉兩個數中只有其中一個含有的因子不影響最大公約數。特殊地,當k=2時,說明計算一個偶數和一個奇數的最大公約數時,可以先將偶數除以2。

下面給出算法步驟

1.如果 m = n ,那麼 m(或n)*k 是最大公約數,算法結束

2.如果 m = 0 ,n 是最大公約數,算法結束

3.如果 n = 0 ,m 是最大公約數,算法結束

4.如果 m 和 n 都是偶數,則 m /= 2,n /= 2,k *= 2

5.如果 m 是偶數,n 不是偶數,則 m /= 2

6.如果 n 是偶數,m 不是偶數,則 n /= 2

7.如果 m 和 n 都不是偶數,則 m =|m - n|,n = min(m, n)

下面給出Stein算法的代碼實現

// Stein算法——遞歸實現
public static long gcd(long m, long n) {

    if (m == 0)
        return n;
    if (n == 0)
        return m;
    if ((m & 1) == 0 && (n & 1) == 0)
        return 2 * gcd(m >> 1, n >> 1);
    else if ((m & 1) == 0)
	return gcd(m >> 1, n);
    else if ((n & 1) == 0)
	return gcd(m, n >> 1);
    else
	return gcd(Math.abs(m - n), Math.min(m, n));
}

當然,相對來說使用循環實現,開銷會更小一些。所以這裏也給出Stein算法的循環實現

// Stein算法——循環實現
public static long gcd(long m, long n) {
    long k = 0;
    while (m != n) {
        if (m == 0)
            return n;
        if (n == 0)
            return m;
        if ((m & 1) == 0 && (n & 1) == 0) {
            m >>= 1;
            n >>= 1;
            k += 1;
        } else if ((m & 1) == 0){
            m >>= 1;
        } else if ((n & 1) == 0){
            n >>= 1;
        } else {
            long tmp = Math.abs(m - n);
            n = Math.min(m, n);
             m = tmp;
        }
    }
    return m << k;
}

 

lcm最小公倍數

根據最小公倍數的計算公式lcm(a, b) = a * b / gcd(a, b);給出lcm的實現代碼

public static long lcm(long m, long n) {
	
    return m * n / gcd(m, n);
}


算法比較

輾轉相除法(歐幾里得算法)和輾轉相減法(更相減損法)相比,本質是相同的,但是由於輾轉相除法一次取模操作相當於多次減法,所以對於規模較大的兩個數而言,使用輾轉相除法效率更高。

歐幾里得算法與Stein算法相比,思路簡單且實現代碼量少,相對來說更加方便。但是對於數據長度較大的特殊情況時,使用Setin算法能夠規避進行除法和取模操作,而通過位運算來替代。算法代碼量更大,但是效率相對來說更高。

在實際引用中,不推薦使用窮舉法和分解質因數法以及更相減損法,雖然簡單易理解,但開銷相對較大。

以上內容,掛一漏萬。如有缺漏,歡迎指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章