背景
我们在小学时,学过最大公约数( Greatest Common Divisor)的知识,但是当时并没有学习比较高效的求最大公约数的方法。使用欧几里得算法( Euclidean Algorithm),可以高效地计算出自然数的最大公约数(两个自然数不能同时为 0)。本文带您体验这一过程。
正文
根据定义暴力计算
根据最大公约数的定义,我们可以用暴力的方式进行计算自然数 a 和自然数 b 的最大公约数( a=0 和 b=0 不能同时成立)。对应的 Python 代码如下
python
def brute_force_gcd(a, b):
if a == 0 and b == 0:
raise ValueError("a and b cannot be both 0")
if a == 0:
return b
if b == 0:
return a
candidate = min(a, b)
while True:
if a % candidate == 0 and b % candidate == 0:
return candidate
candidate -= 1
由于所有的自然数都能被 1 整除,所以上述代码求出的最大公约数 gcd≥1 总是成立。
观察
如果固定 b 的值,让 a 变化(也可以固定 a,让 b 变化,两种方式没有本质区别),我们看一看求出的结果是否有特殊的规律。
b=1 时
当 b=1 时,对任意自然数 a, gcd(a,1)=1 显然成立。
b=2 时
当 b=2 时,我们可以列举比较小的 a 值所对应的计算结果

看起来 gcd(a,2) 的值是在 1 和 2 之间交替出现。解释如下 ⬇️
- 当 a 是偶数时, 2∣2 并且 2∣a,所以 gcd(a,2)=2
- 当 a 是奇数时
- 因为 2∤a,所以 gcd(a,2)=2
- 那么 gcd(a,2)=1
b=3 时
当 b=3 时,我们可以列举比较小的 a 值所对应的计算结果

看起来 gcd(a,3) 的值会以 3,1,1 这样周期反复出现。解释如下 ⬇️
- 当 a≡0(mod3) 时, 3∣3 并且 3∣a,所以 gcd(a,3)=3
- 当 a≡1(mod3) 时
- 3∤a,所以 gcd(a,3)=3
- 2∤3,所以 gcd(a,3)=2
- 那么 gcd(a,3)=1
- 当 a≡2(mod3) 时
- 3∤a,所以 gcd(a,3)=3
- 2∤3,所以 gcd(a,3)=2
- 那么 gcd(a,3)=1
b=4
当 b=4 时,我们可以列举比较小的 a 值所对应的计算结果

看起来 gcd(a,4) 的值会以 4,1,2,1 这样周期反复出现。解释如下 ⬇️
- 当 a≡0(mod4) 时, 4∣4 并且 4∣a,所以 gcd(a,4)=4
- 当 a≡1(mod4) 时
- 4∤a,所以 gcd(a,4)=4
- 3∤4,所以 gcd(a,4)=3
- 2∤a,所以 gcd(a,4)=2
- 那么 gcd(a,3)=1
- 当 a≡2(mod3) 时
- 4∤a,所以 gcd(a,4)=4
- 3∤4,所以 gcd(a,4)=3
- 2∣4,并且 2∣a,所以 gcd(a,4)=2
- 当 a≡3(mod4) 时
- 4∤a,所以 gcd(a,4)=4
- 3∤4,所以 gcd(a,4)=3
- 2∤a,所以 gcd(a,4)=2
- 那么 gcd(a,4)=1
一般的情形
分析了以上几个值比较小的 b 之后,我们尝试从中找规律。假设 a≥b 成立,看起来以下等式是成立的
gcd(a,b)=gcd(a−b,b)
我们看看能否证明它。为了方便描述,我们记 gcd(a,b)=g, gcd(a−b,b)=g′。
我们的目标是证明 g=g′ (或者找到两者不相等的反例)。
gcd(a,b)=g,根据最大公约数的定义,以下两者成立
- g∣a
- g∣b
所以 g∣(a−b)。 g 既是 a−b 的约数,又是 b 的约数,那么 g 是 a−b 和 b 的公约数 。按照最大公约数 的定义, g≤g′ 成立(因为 g′ 是 a−b 和 b 的最大公约数)。
反过来看, gcd(a−b,b)=g′,根据最大公约数的定义,以下两者成立
- g′∣(a−b)
- g′∣b
所以 g′∣((a−b)+b),即 g′∣a。 g′ 既是 b 的约数,又是 a 的约数,那么 g′ 是 a 和 b 的公约数 。按照最大公约数 的定义, g′≤g 成立(因为 g 是 a 和 b 的最大公约数)。
既然 g≤g′ 和 g′≤g 同时成立,那么 g=g′ 成立。也就是说,对正整数 a,b 而言,当 a≥b 成立时,以下等式总是成立。
gcd(a,b)=gcd(a−b,b)
在此基础上,可以推出 ⬇️ ( a,b 都是正整数)
gcd(a,b)=gcd(a−b,b)
=gcd(a−2b,b)
=gcd(a−3b,b)
⋯
=gcd(amodb,b)
所以对任意正整数 a,b 而言,以下等式成立
gcd(a,b)=gcd(amodb,b)
这就是欧几里得算法 ( Euclidean Algorithm) 的核心思想。
用 Python 程序实现欧几里得算法
以 55 和 34 为例,我们利用欧几里得算法来计算两者的最大公约数,具体过程如下 ⬇️
gcd(55,34)=gcd(55mod34,34)
=gcd(21,34)=gcd(34,21)
=gcd(34mod21,21)
=gcd(13,21)=gcd(21,13)
=gcd(21mod13,13)
=gcd(8,13)=gcd(13,8)
=gcd(13mod8,8)
=gcd(5,8)=gcd(8,5)
=gcd(8mod5,5)
=gcd(3,5)=gcd(5,3)
=gcd(5mod3,3)
=gcd(2,3)=gcd(3,2)
=gcd(3mod2,2)
=gcd(1,2)=gcd(2,1)
=gcd(2mod1,1)
=gcd(0,1)
=1
用代码来实现,可以这样写 ⬇️
python
def gcd(a, b):
if (a, b) == (0, 0):
raise ValueError("a and b cannot be both 0")
if b == 0:
return a
return gcd(b, a % b)
但我们用这段代码计算 gcd(55,34) 时,会出现这样的函数调用栈 ⬇️ (栈底在上方,栈顶在下方)
| 参数 a 的值 | 参数 b 的值 | 函数调用的情况 |
|---|---|---|
| 55 | 34 | gcd(55,34) |
| 34 | 21 | gcd(34,21) |
| 21 | 13 | gcd(21,13) |
| 13 | 8 | gcd(13,8) |
| 8 | 5 | gcd(8,5) |
| 5 | 3 | gcd(5,3) |
| 3 | 2 | gcd(3,2) |
| 2 | 1 | gcd(2,1) |
| 1 | 0 | gcd(1,0) |
验证
我们可以用如下的代码来简单验证欧几里得算法的计算结果
python
import matplotlib.pyplot as plt
def gcd(a, b):
if (a, b) == (0, 0):
raise ValueError("a and b cannot be both 0")
if b == 0:
return a
return gcd(b, a % b)
def plot_gcd_results(nums, gcd_results, b):
# plot_gcd_results 里的代码是 trae 帮我生成的,我做了些小调整
plt.figure(figsize=(12, 8))
plt.bar(nums, gcd_results, color='skyblue', edgecolor='black')
plt.xlabel('a', fontsize=12)
plt.ylabel('GCD(a, %d)' % b, fontsize=12)
plt.title('GCD(a, %d) for a from %d to %d' % (b, nums[0], nums[-1]), fontsize=14)
plt.xticks(nums)
plt.yticks(range(max(gcd_results) + 1))
plt.grid(axis='y', linestyle='--')
plt.tight_layout()
plt.show()
if __name__ == "__main__":
b = 4
nums = range(5 * b + 1)
gcd_results = [gcd(num, b) for num in nums]
plot_gcd_results(nums, gcd_results, b)
运行结果如下图所示

可以看到, gcd(a,4) 的值,以 4 为周期的(值是 4,1,2,1,⋯),这与我们前面分析的结果是一致的。
可以将第 24 行的 b 变量改为其他值来进行验证,这里就不赘述了。
