最大公约数(Greatest Common Divisor),也称最大公因数、最大公因子。是指两个或多个整数公有因数中最大的一种。编程时常用 gcd(a, b) 表示两个数的最大公约数。
相反的,将两个或多个整数公有的倍数叫做它们的公倍数,其中除0以外最小的一个公倍数就叫做这几个整数的最小公倍数(Least Common Multiple)。编程时候用 lcm(a, b) 表示两个数的最大公约数。
其中 lcm(a, b) = a * b / gcd(a, b);
所以在计算 gcd 和 lcm 时,通常只需要计算出 gcd 即可。本文中也主要阐述最大公约数的算法。
在算法学习的过程中,对于最大公约数求解的算法有很多,常见的有穷举法,分解质因数法,短除法,更相减损法,欧几里得算法(辗转相除法)等。这里笔者对于最大公约数求解的常见算法及实现进行简单是阐述。
穷举法实现最大公约数的思路较为简单。其算法步骤如下
gcd(m ,n)
1.找出参数m,n中较小的一个。并以此作为循环的起点i;
2.检查 i 是否。若 i 为m,n的公因数,那么 i 即为m,n的最大公因数;
3.若 i 不为m,n的公因数,那么就对 i 自减,直至满足步骤2的条件或者 i == 1。
该算法建立在两个数m,n的最大公因数一定不大于min(m, n)这一合理推论之上。只要从min(m, n)向下自减,遇到的第一个公因数就一定是最大公因数。该算法实现如下
//穷举法
public static long gcd(long m, long n) {
for (long i = Math.min(m, n); i > 1; i--) {
if (m % i == 0 && n % i == 0) {
return i;
}
}
return 1;
}
利用分解质因数法求解两个数的最大公约数。把每个数分别分解质因数,再把各数中的全部公有质因数提取出来连乘,所得的积就是这几个数的最大公约数。
该算法实现要点在于如何找出两个数的公有质因数。笔者的实现中采用类似线性素数筛的方式,保证能求得最终所有公共质因数的积。下面给出算法步骤
1.设置保存结果的变量result,且初始化为1。
2.置循环起点 i 为2,最大不超过min(m, n)。当 m 或 n 等于1时,退出循环。
3.若 i 为m,n的公约数,那么 m /= i,n /= i,result *= i。否则 i++。直至不满足循环条件后退出。
该算法的实现代码如下
// 分解质因数法
public static long gcd(long m, long n) {
long result = 1;
for (int i = 2; i <= Math.min(m, n) && m != 1 && n != 1;) {
if (m % i == 0 && n % i == 0) {
m /= i;
n /= i;
result *= i;
} else {
i++;
}
}
return result;
}
需要注意的是,在分解质因数时,常用的一种方法是短除法。它同样可以用来求解最大公约数,但其实质上只是分解质因数法的一种运用形式,所以本文中不将其作为一种算法单独列出。
更相减损法是出自《九章算术》的一种求最大公约数的算法,它原本是为约分而设计的,但它适用于任何需要求最大公约数的场合。又名“更相减损术”,辗转相减法,等值算法,尼考曼彻斯法。
《九章算术》中对这种方法的说明如下:
"可半者半之,不可半者,副置分母、子之数,以少减多,更相减损,求其等也。以等数约之。"
将其转化为算法步骤如下:
1.对于两个参数m,n。若两者都是偶数,那么用2进行约分。否则继续执行步骤2;
2.以较大的数减较小的数,接着把所得的差与较小的数比较,并以大数减小数。继续这个操作,直到所得的减数和差相等为止;
3.最后将步骤一中约掉的若干个2与步骤二中的"等数"相乘就是所求的最大公约数。
其实现代码如下
// 更相减损法——递归实现
public static long gcd(long m, long n) {
if (m == n)
return m;
if ((m & 1) == 0 && (n & 1) == 0)
return gcd(m >> 1, n >> 1) * 2;
return gcd(Math.abs(m - n), Math.min(m, n));
}
递归实现相对来说代码更为简洁,但因为存在方法调用,所以开销相对更大。因此这里给出更相减损法的循环实现。
// 更相减损法——循环实现
public static long gcd(long m, long n) {
long mul = 1;
while ((m & 1) == 0 && (n & 1) == 0) {
m >>= 1;
n >>= 1;
mul <<= 1;
}
while (m != n) {
if (m > n) {
m = m - n;
} else {
n = n - m;
}
}
return mul * n;
}
欧几里德算法又称辗转相除法,是指用于计算两个正整数a,b的最大公约数。应用领域有数学和计算机两个方面。计算公式gcd(a,b) = gcd(b,a mod b)。
欧几里得算法的公式十分简单,实现算法的代码量也极少,是求解最大公约数的一种较好的算法。对于该算法,仅仅会使用是不够的,所以在这里笔者对于欧几里得算法进行一下简单的证明。
对于一个正整数m,它可以表示成m = kn + r(其中m,k,n,r均为正整数,且m > n,n > r)
故 r = m mod n
假设 g 为 m,n 的一个公约数,记作 g|m,g|n。即 m mod g == 0,n mod g == 0。
同时 r = m - kn,两边同除以g。得
r/g = m/g - kn/g,不难发现 m/g - kn/g 结果为整数(因为g|m,g|n,且k为整数)
故 g 是 m,n,r(也即m mod n)的公约数
本着归约的思想,得出以下公式
gcd(m, n) = gcd(n, m mod n)
假设 g 是 n,m mod n的公约数,则 g|n,g|(m - kn)(k∈N*)
进而 g|m,因此g也是m,n的公约数
故(m, n)和(n, m mod n)的公约数一致,其最大公约数也必然相等。得证
从上述的证明过程中,也不难看出辗转相减法的影子。实际上辗转相除法和辗转相减法的理论基础是相同的,只是实现的方式不同罢了。同时由于辗转相除法能通过一次取模完成多次减法的操作,所以其运算效率较辗转相减法更高。
下面给出算法实现
// 欧几里得算法——循环实现
public static long gcd(long m, long n) {
while (n != 0) {
long rem = m;
m = n;
n = rem % n;
}
return m;
}
对于欧几里得算法的公式而言,使用递归实现可以使得代码更为简洁,所以这里也给出递归实现
// 欧几里得算法——递归实现
public static long gcd(long m, long n) {
if (n == 0)
return m;
return gcd(n, m % n);
}
实际上这里的递归实现还可以通过引入三目运算符进行进一步的缩减,代码如下
// 欧几里得算法——单行递归实现
public static long gcd(long m, long n) {
return n == 0 ? m : gcd(n, m % n);
}
Stein算法是一种计算两个数最大公约数的算法,是针对欧几里德算法在对大整数进行运算时,需要试商导致增加运算时间的缺陷而提出的改进算法。
在欧几里德算法中,有个核心就是进行取模操作。对于32位或者64位的整数而言,取模操作或者除法操作耗费的时间或许还可以接受。但是对于更大的素数,这样的计算过程就不得不由用户来设计,为了计算两个超过64位的整数的模,用户也许不得不采用类似于多位数除法手算过程中的试商法(可以百度“高位试商法”),这个过程不但复杂,而且消耗了很多CPU时间。对于现代密码学来说,长度大于128位的素数比比皆是(例如RSA的非对称密钥1024位),所以设计这样的程序迫切希望能够抛弃除法和取模。
由J. Stein 1961年提出的Stein算法很好的解决了欧几里得算法中的这个缺陷,Stein算法只有整数的移位和加减法。
同样,本文在这里简单的对其正确性进行简单说明
为了说明Stein算法的正确性,首先必须注意到以下结论:
gcd(m, m) = m,也就是一个数和其自身的公约数仍是其自身。
gcd(km, kn) = k gcd(m, n),也就是最大公约数运算和倍乘运算可以交换。特殊地,当k=2时,说明两个偶数的最大公约数必然能被2整除。
当k与b互质,gcd(km, n)=gcd(m, n),也就是约掉两个数中只有其中一个含有的因子不影响最大公约数。特殊地,当k=2时,说明计算一个偶数和一个奇数的最大公约数时,可以先将偶数除以2。
下面给出算法步骤
1.如果 m = n ,那么 m(或n)*k 是最大公约数,算法结束
2.如果 m = 0 ,n 是最大公约数,算法结束
3.如果 n = 0 ,m 是最大公约数,算法结束
4.如果 m 和 n 都是偶数,则 m /= 2,n /= 2,k *= 2
5.如果 m 是偶数,n 不是偶数,则 m /= 2
6.如果 n 是偶数,m 不是偶数,则 n /= 2
7.如果 m 和 n 都不是偶数,则 m =|m - n|,n = min(m, n)
下面给出Stein算法的代码实现
// Stein算法——递归实现
public static long gcd(long m, long n) {
if (m == 0)
return n;
if (n == 0)
return m;
if ((m & 1) == 0 && (n & 1) == 0)
return 2 * gcd(m >> 1, n >> 1);
else if ((m & 1) == 0)
return gcd(m >> 1, n);
else if ((n & 1) == 0)
return gcd(m, n >> 1);
else
return gcd(Math.abs(m - n), Math.min(m, n));
}
当然,相对来说使用循环实现,开销会更小一些。所以这里也给出Stein算法的循环实现
// Stein算法——循环实现
public static long gcd(long m, long n) {
long k = 0;
while (m != n) {
if (m == 0)
return n;
if (n == 0)
return m;
if ((m & 1) == 0 && (n & 1) == 0) {
m >>= 1;
n >>= 1;
k += 1;
} else if ((m & 1) == 0){
m >>= 1;
} else if ((n & 1) == 0){
n >>= 1;
} else {
long tmp = Math.abs(m - n);
n = Math.min(m, n);
m = tmp;
}
}
return m << k;
}
根据最小公倍数的计算公式lcm(a, b) = a * b / gcd(a, b);给出lcm的实现代码
public static long lcm(long m, long n) {
return m * n / gcd(m, n);
}
辗转相除法(欧几里得算法)和辗转相减法(更相减损法)相比,本质是相同的,但是由于辗转相除法一次取模操作相当于多次减法,所以对于规模较大的两个数而言,使用辗转相除法效率更高。
欧几里得算法与Stein算法相比,思路简单且实现代码量少,相对来说更加方便。但是对于数据长度较大的特殊情况时,使用Setin算法能够规避进行除法和取模操作,而通过位运算来替代。算法代码量更大,但是效率相对来说更高。
在实际引用中,不推荐使用穷举法和分解质因数法以及更相减损法,虽然简单易理解,但开销相对较大。
以上内容,挂一漏万。如有缺漏,欢迎指正。