编程题 求最大公约数

暴力枚举法

暴力枚举的方法从较小整数的一半开始,试图找到一个合适的整数 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)))。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值