暴力枚举法
暴力枚举的方法从较小整数的一半开始,试图找到一个合适的整数 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 的最大公约数的步骤如下。
- 整数 10 通过移位,可以转换成求 5 和 25 的最大公约数。
- 利用更相减损术,计算出25-5=20,转换成求5和20的最大公约数。
- 整数20通过移位,可以转换成求5和10的最大公约数。
- 整数10通过移位,可以转换成求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)))。