編程題 求最大公約數

暴力枚舉法

暴力枚舉的方法從較小整數的一半開始,試圖找到一個合適的整數 i,看看這個整數能否被 a 和 b 同時整除。

public static int getGreatestCommonDivisor(int a, int b) {
    int big = a > b ? a : b;
    int small = a < b ? a : b;
    if (big % small == 0) {
        return small;
    }
    for (int i = small / 2; i > 1; i--) {
        if (small % i == 0 && big % i == 0) {
            return i;
        }
    }
    return 1;
}

效率不高。如果傳入的整數是 10000 和 10001,就需要循環 10000 / 2 - 1 = 4999 次。

輾轉相除法

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

這條算法基於一個定理:兩個正整數 a 和 b(a > b),它們的最大公約數等於 a 除以 b 的餘數 c 和 b 之間的最大公約數。

例如 10 和 25,25 除以 10 商 2 餘 5,那麼 10 和 25 的最大公約數,等同於 10 和 5 的最大公約數。

有了這條定理,求最大公約數就變得簡單了。可以使用遞歸的方法把問題逐步簡化。

首先,計算出a除以b的餘數c,把問題轉化成求b和c的最大公約數;然後計算出b除以c的餘數d,把問題轉化成求c和d的最大公約數;再計算出c除以d的餘數e,把問題轉化成求d和e的最大公約數……
以此類推,逐漸把兩個較大整數之間的運算簡化成兩個較小整數之間的運算,直到兩個數可以整除,或者其中一個數減小到1爲止。

public static int getGreatestCommonDivisor(int a, int b) {
    int big = a > b ? a : b;
    int small = a < b ? a : b;
    if (big % small == 0) {
        return small;
    }
    return getGreatestCommonDivisor(big % small, small);
}

該算法的缺點是當兩個整數較大時,做 a % b 取模運算的性能會比較差。

更相減損術

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

它的原理更加簡單:兩個正整數 a 和b(a > b),它們的最大公約數等於 a - b 的差值 c 和較小數 b 的最大公約數。

例如 10 和 25,25 減 10 的差是 15,那麼 10 和 25 的最大公約數,等同於 15 和 10 的最大公約數。

public static int getGreatestCommonDivisor(int a, int b) {
    if (a == b) {
        return a;
    }
    int big = a > b ? a : b;
    int small = a < b ? a : b;
    return getGreatestCommonDivisor(big - small, small);
}

該算法避免了大整數取模可能出現的性能問題。

但是更相減損術依靠兩數求差的方式來遞歸,運算次數遠大於輾轉相除法的取模方式。
更相減損術是不穩定的算法,當兩數相差懸殊時,如計算 10000 和 1 的最大公約數,就要遞歸 9999 次。

最優算法

該算法既能避免大整數取模,又能儘可能地減少運算次數

思想是把輾轉相除法和更相減損術的優點結合起來,在更相減損術的基礎上使用移位運算。

衆所周知,移位運算的性能非常好。對於給出的正整數 a 和 b,不難得到如下的結論:

  • 當 a 和 b 均爲偶數時,gcd(a,b) = gcd(a/2, b/2) * 2 = gcd(a>>1, b>>1) * 2

  • 當 a 爲偶數,b 爲奇數時,gcd(a,b) = gcd(a/2,b) = gcd(a>>1, b)

  • 當 a 爲奇數,b 爲偶數時,gcd(a,b) = gcd(a,b/2) = gcd(a, b>>1)

  • 當 a 和 b 均爲奇數時,先利用更相減損術運算一次,gcd(a,b) = gcd(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。

該算法在兩數都比較小時,可能看不出計算次數的優勢;當兩數越大時,計算次數的減少就會越明顯。

public static int gcd(int a, int b) {
    if (a == b) {
        return a;
    }
    if ((a & 1) == 0 && (b & 1) == 0) {
        return gcd(a >> 1, b >> 1) << 1;
    } else if ((a & 1) == 0 && (b & 1) != 0) {
        return gcd(a >> 1, b);
    } else if ((a & 1) != 0 && (b & 1) == 0) {
        return gcd(a, b >> 1);
    } else {
        int big = a > b ? a : b;
        int small = a < b ? a : b;
        return gcd(big - small, small);
    }
}

在上述代碼中,判斷整數奇偶性的方式是讓整數和 1 進行運算,如果 (a&1) == 0,則說明整數 a 是偶數;如果 (a&1) != 0,則說明整數 a 是奇數。

各算法的時間複雜度

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

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

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

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

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