最大公约数是在很小的时候就学过的东西,似乎日常生活的应用也不广泛,大多数只应用在材料使用率最大化等问题上,但是算法却不见得高效。

那么求最大公约数有哪些算法呢?

Q:要求方法传两个正整型参数,返回值就是他们的最大公约数,尽可能保证性能

暴力枚举法

public static int getGreatestCommonDivisor(int numberA, int numberB) {
    int smallNumber = numberA < numberB ? numberA : numberB;
    int bigNumber = numberA >= numberB ? numberA : numberB;
    if (bigNumber % smallNumber == 0) {
        return smallNumber;
    }
    int greatestCommonDivisor = 1;
    for (int i = 2; i <= smallNumber / 2; i++) {
        if (numberA % i == 0 && numberB % i == 0) {
            greatestCommonDivisor = i;
        }
    }
    return greatestCommonDivisor;
}

思路十分简单,使用暴力枚举的方法,试图寻找到一个合适的整数 i,看看这个整数能否被两个整型参数 numberAnumberB 同时整除。

这个整数 i2 开始循环累加,一直累加到 numberAnumberB 中较小的参数的一半为止。循环结束后,上一次寻找到的能够被两数整除的最大 i 值,就是两数的最大公约数。

这样虽然可以得到正确结果,但效率却比较低。比如传入两个参数 1000010001,用此方法就需要循环 10000/2-1=4999 次!

辗转相除法

辗转相除法,又名欧几里得算法(Euclidean algorithm),是古希腊数学家欧几里得发明的,目的是求出两个正整数的最大公约数。它是已知最古老的算法,可追溯至公元前 300 年前。

这条算法基于一个定理:两个正整数 aba>b),它们的最大公约数等于 a 除以 b 的余数 cb 之间的最大公约数。

比如:102525 除以 1025,那么 1025 的最大公约数,等同于 105 的最大公约数。

有了这条定理,求最大公约数就简单了。我们可以使用递归的方法来把问题逐步简化。

首先,我们先计算出 a 除以 b 的余数 c,把问题转化成求出 bc 的最大公约数;然后计算出 b 除以 c 的余数 d,把问题转化成求出 cd 的最大公约数;再然后计算出 c 除以 d 的余数 e,把问题转化成求出 de 的最大公约数……

以此类推,逐渐把两个较大整数之间的运算简化成两个较小的整数之间的运算,直到两个数可以整除,或者其中一个数减少到 1 为止。

public static int getGreatestCommonDivisor(int numberA, int numberB) {
    int result = 1;
    if (numberA > numberB) {
        result = gcd(numberA, numberB);
    } else {
        result = gcd(numberB, numberA);
    }
    return result;
}
// 递归计算最大公约数
private static int gcd(int a, int b) {
    if (a % b == 0) {
        return b;
    } else {
        return gcd(b, a % b);
    }
}

不过一个新问题产生了,当两个整型数较大时,做 a%b 取模运算的性能会比较低。

更相减损术

更相减损术,出自中国古代的《九章算术》,也是一种求最大公约数的算法。

原理更加简单:两个正整数 aba>b),它们的最大公约数等于 a-b 的差值 c 和较小数 b 的最大公约数。

比如:102525 减去 10 的差是 15,那么 1025 的最大公约数,等同于 1015 的最大公约数。

由此,我们同样可以通过递归来简化问题。首先,我们先计算出 ab 的差值 c(假设 a>b),把问题转化成求出 bc 的最大公约数;然后计算出 cb 的差值 d(假设 c>d),把问题转化成求出 bd 的最大公约数;再然后计算出 bd 的差值 e(假设 b>d),把问题转化成求出 de 的最大公约数……

以此类推,逐渐把两个较大整数之间的运算简化成两个较小整数之间的运算,直到两数可以相等为止,最大公约数就是最终相等的两个数。

public static int gcd(int numberA, int numberB) {
    if (numberA == numberB) {
        return numberA;
    } else if (numberA < numberB) {
        return gcd(numberB - numberA, numberA);
    } else {
        return gcd(numberA - numberB, numberB);
    }
}

更相减损术避免了大整数取模的性能问题,已经越来越接近最优了,但是,依靠两数求差的方式来递归,运算的次数肯定远大于辗转相除法的取模方式。因此,更相减损术是不稳定的算法,当两数相差悬殊时,比如计算 100001,就要递归 9999 次。

更相减损术与移位结合

有什么方法,可以既避免大整数取模,又能尽可能减少运算次数呢?

最优方法——把辗转相除法和更相减损术的优势结合起来,在更相减损术的基础上使用移位运算。

众所周知,移位运算的性能非常快。对于给定的正整数a和b,不难得到如下的结论(其中 gcb(a, b) 的意思是 ab 的最大公约数函数):

ab 均为偶数,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);

ab 均为奇数,利用更相减损术运算一次,gcb(a, b) = gcb(b, a-b),此时 a-b 必然是偶数,又可以继续进行移位运算。

比如计算 1025 的最大公约数的步骤如下:

  1. 整数 10 通过移位,可以转换成求 525 的最大公约数
  2. 利用更相减损术,计算出 25-5=20,转换成求 520 的最大公约数
  3. 整数 20 通过移位,可以转换成求 510 的最大公约数
  4. 整数 10 通过移位,可以转换成求 55 的最大公约数
  5. 利用更相减损术,因为两数相等,所以最大公约数是 5

在两数比较小的时候,暂时看不出计算次数的优势,当两数越大,计算次数的节省就越明显。

public static int gcd(int numberA, int numberB) {
    if (numberA == numberB) {
        return numberA;
    } else if (numberA < numberB) {
        return gcd(numberB, numberA);   // 保证参数 numberA 永远大于等于参数 numberB,减少代码量
    } else {
        // 和 1 做按位与运算,判断奇偶
        if (!numberA&1 && !numberB&1) {
            return gcd(numberA >> 1, numberB >> 1) << 1;
        } else if (!numberA&1 && numberB&1) {
            return gcd(numberA >> 1, numberB);
        } else if (numberA&1 && !numberB&1) {
            return gcd(numberA, numberB >> 1);
        } else {
            return gcd(numberA, numberA - numberB);
        }
    }
}

总结上述所有算法的时间复杂度

  • 暴力枚举法: 时间复杂度是 O(min(a, b)))
  • 辗转相除法: 时间复杂度不太好计算,可以近似为 O(log(max(a, b))),但是取模运算性能较差。
  • 更相减损术: 避免了取模运算,但是算法性能不稳定,最坏时间复杂度为 O(max(a, b)))
  • 更相减损术与移位结合: 不但避免了取模运算,而且算法性能稳定,时间复杂度为 O(log(max(a, b)))

写在后面

  • 方法的参数默认必定是正整数,所以在代码中省去了合法性检查。
  • 文中描述的更相减损术是简化了的方式。在九章算术原文中多了一步验证:如果两数都是偶数,计算差值之前会首先让两个数都折半,使得计算次数更少。这种方法做到了部分优化,但古人似乎没想到一奇一偶的情况也是可以优化的。