两个(或更多个)整数的最大公约数(Greatest Common Divisor, GCD)是指能同时整除它们的最大正整数,求解最大公约数最常用的算法是辗转相除法(又称欧几里得算法)及Stein算法,至于直接穷举的暴力算法,因为过于低效,实际中是不会使用的。下面来总结下这两种方法。
问题描述
给定两个正整数M和N,求其最大公约数。函数原型:
1 | unsigned int GCD(unsigned int M, unsigned int N); |
调用此函数时保证M≥N>0。
辗转相除法
辗转相除法的介绍可见维基百科,其理论基础就是下面这个公式:
GCD(M, N) = GCD(N, M%N)
此公式的证明在此不做介绍,直接给出算法实现:
1 | unsigned int GCD(unsigned int M, unsigned int N) { |
上述算法可以进一步优化,算法中使用了一个中间变量Rem
,它用于交换两数,其实这是没有必要的。注意到:
GCD(M, N) = GCD(N, M % N) = GCD(M % N, N % (M % N))
在第一步计算出M % N
后,保留N
的值其实是没有必要的,可以直接跳过这一步,使用最后一个式子进行计算即可:
1 | M = M % N; // 这一步后,新的M的值即为M%N |
因为这相当于计算了2次,截止条件变为M
和N
中的任意一个为0,否则就会出现%0
这样的错误。返回结果是M
和N
中不为0那个,不过由于另一个数为0,可以用M + N
替代。这样就可以写出代码:
1 | unsigned int GCD(unsigned int M, unsigned int N) { |
如此简单而优雅!
Stein算法
此算法又被称为Binary GCD algorithm,它是针对欧几里得算法的一些缺陷提出的。
欧几里德算法是计算两个数最大公约数的传统算法,无论从理论还是从实际效率上都是很好的。但是却有一个致命的缺陷,这个缺陷在素数比较小的时候一般是感觉不到的,只有在大素数时才会显现出来。
一般实际应用中的整数很少会超过64位(当然现在已经允许128位了),对于这样的整数,计算两个数之间的模是很简单的。对于字长为32位的平台,计算两个不超过32位的整数的模,只需要一个指令周期,而计算64位以下的整数模,也不过几个周期而已。但是对于更大的素数,这样的计算过程就不得不由用户来设计,为了计算两个超过64位的整数的模,用户也许不得不采用类似于多位数除法手算过程中的试商法,这个过程不但复杂,而且消耗了很多CPU时间。对于现代密码算法,要求计算128位以上的素数的情况比比皆是,设计这样的程序迫切希望能够抛弃除法和取模。
由J. Stein 1961年提出的Stein算法很好的解决了欧几里德算法中的这个缺陷,Stein算法只有整数的移位和加减法。
算法的理论和实现方法在此从略,此处仅摘录一下实现代码以供参考,引用自WiKi。
递归实现:
1 | unsigned int gcd(unsigned int u, unsigned int v) |
非递归实现:
1 | unsigned int gcd(unsigned int u, unsigned int v) |
扩展问题
如果需要求多个数的最大公约数,可以先求出任意两个数的最大公约数,然后再求此数与其它数间的最大公约数,即有以下等式成立:
GCD(a, b, c) = GCD(a, GCD(b, c)) = GCD(c, GCD(a,b))
由此,易写出求N个数最大公约数的算法:
1 |
|
求出最大公约数的同时也就得到了最小公倍数(Lowest Common Multiple, LCM),二者存在以下关系:
GCD(M, N) * LCM(M, N) = M * N
故易得:
1 | unsigned int LCM(unsigned int M, unsigned int N) { |