前言
最大公约数(GCD)和最小公倍数(LCM)是数论中最基础的概念。很多人只知道短除法求GCD,其实辗转相除法(欧几里得算法只需要几行代码,而且时间复杂度是O(logn)。
我并没有能力让你成为数论专家,我只是想让你理解辗转相除法的原理、掌握GCD与LCM的关系、知道GCD在实际问题中的巧妙应用。
摘要
从"求两数最大公约数"问题出发,剖析辗转相除法的数学原理与递归实现。通过短除法到辗转相除法的优化、GCD与LCM的转换关系、以及约分等实际应用,揭秘如何O(logn)求最大公约数。配合详细推导与代码实现,给出GCD问题的完整解法。
一、什么是最大公约数
周一早上,哈吉米问南北绿豆:"最大公约数是啥?"
南北绿豆:"最大公约数(Greatest Common Divisor,GCD)就是能同时整除两个数的最大整数。"
示例:
12和18的公约数:
12的约数:1, 2, 3, 4, 6, 12
18的约数:1, 2, 3, 6, 9, 18
公约数(共同的):1, 2, 3, 6
最大公约数:6
生活化场景:
阿西噶阿西:"想象你有12块巧克力和18块糖果,要平均分给小朋友,最多能分给几个人?"
分给1个人:12÷1=12,18÷1=18 ✓
分给2个人:12÷2=6,18÷2=9 ✓
分给3个人:12÷3=4,18÷3=6 ✓
分给4个人:12÷4=3,18÷4=4.5 ✗(糖果分不均)
分给5个人:12÷5=2.4 ✗
分给6个人:12÷6=2,18÷6=3 ✓
最多分给6个人 → gcd(12, 18) = 6
哈吉米:"懂了,GCD就是最大的公因数。"
二、方法1:短除法(暴力)
南北绿豆:"小学学过的短除法。"
2.1 短除法过程
示例:求gcd(12, 18)
2 | 12 18
───────────
3 | 6 9
───────────
2 3(不能再除了)
gcd = 2 × 3 = 6
暴力代码思路:
从min(a, b)往下试:
gcd(12, 18):
试12:12%12=0 ✓,18%12=6 ✗
试11:12%11=1 ✗
...
试6:12%6=0 ✓,18%6=0 ✓
答案:6
Java版本:
public int gcd(int a, int b) {
for (int i = Math.min(a, b); i >= 1; i--) {
if (a % i == 0 && b % i == 0) {
return i;
}
}
return 1;
}
时间复杂度:O(min(a, b))
哈吉米:"如果a和b很大,要试很多次。"
南北绿豆:"所以需要更聪明的方法。"
三、方法2:辗转相除法(欧几里得算法)
阿西噶阿西:"辗转相除法是2000多年前欧几里得发明的,超级巧妙。"
3.1 核心定理
辗转相除法定理:
gcd(a, b) = gcd(b, a % b)
为什么这个等式成立?
南北绿豆:"数学证明:设d = gcd(a, b),即:
- a = d × m
- b = d × n(m和n互质)
那么:
a % b = a - k × b = d × m - k × d × n = d × (m - k × n)
所以d也是b和a%b的公约数
反过来,b和a%b的公约数也是a和b的公约数
所以:gcd(a, b) = gcd(b, a % b)
哈吉米:"数学推导有点绕..."
阿西噶阿西:"不理解也没关系,记住这个规律就行,关键是它确实对。"
3.2 辗转相除法过程
示例:gcd(12, 18)
gcd(12, 18)
= gcd(18, 12)(调整顺序,让大的在前)
= gcd(12, 6)(18 % 12 = 6)
= gcd(6, 0)(12 % 6 = 0)
= 6(当b=0时,a就是GCD)
表格演示:
| 步骤 | a | b | a % b | 操作 |
|---|---|---|---|---|
| 1 | 18 | 12 | 6 | gcd(18,12) → gcd(12,6) |
| 2 | 12 | 6 | 0 | gcd(12,6) → gcd(6,0) |
| 3 | 6 | 0 | - | 返回6 |
再举个例子:gcd(48, 18)
| 步骤 | a | b | a % b | 操作 |
|---|---|---|---|---|
| 1 | 48 | 18 | 12 | gcd(48,18) → gcd(18,12) |
| 2 | 18 | 12 | 6 | gcd(18,12) → gcd(12,6) |
| 3 | 12 | 6 | 0 | gcd(12,6) → gcd(6,0) |
| 4 | 6 | 0 | - | 返回6 |
哈吉米:"每次b变成a%b,越来越小,最后变成0。"
南北绿豆:"对,当b=0时,a就是最大公约数。"
3.3 代码实现
Java版本(递归):
public int gcd(int a, int b) {
if (b == 0) {
return a;
}
return gcd(b, a % b);
}
Java版本(迭代):
public int gcd(int a, int b) {
while (b != 0) {
int temp = a % b;
a = b;
b = temp;
}
return a;
}
C++版本(递归):
int gcd(int a, int b) {
if (b == 0) {
return a;
}
return gcd(b, a % b);
}
C++版本(迭代):
int gcd(int a, int b) {
while (b != 0) {
int temp = a % b;
a = b;
b = temp;
}
return a;
}
Python版本(递归):
def gcd(a, b):
if b == 0:
return a
return gcd(b, a % b)
Python版本(迭代):
def gcd(a, b):
while b != 0:
a, b = b, a % b
return a
时间复杂度:O(log(min(a, b)))
为什么是O(logn)?
南北绿豆:"每次取模,b至少减少一半(斐波那契数列最坏情况)。"
最坏情况:a和b是相邻的斐波那契数
gcd(F_n, F_n-1)需要n-1次递归
而F_n ≈ φ^n(φ是黄金比例)
所以递归次数 ≈ log(F_n)
哈吉米:"从O(min(a,b))优化到O(logn),快太多了!"
四、最小公倍数LCM
南北绿豆:"GCD和LCM有个巧妙关系。"
4.1 LCM是什么
最小公倍数(Least Common Multiple,LCM):能同时被两个数整除的最小整数。
示例:
12的倍数:12, 24, 36, 48, 60, 72, ...
18的倍数:18, 36, 54, 72, 90, ...
公倍数:36, 72, 108, ...
最小公倍数:36
4.2 GCD与LCM的关系
核心公式:
a × b = gcd(a, b) × lcm(a, b)
所以:
lcm(a, b) = a × b / gcd(a, b)
示例:
a = 12, b = 18
gcd(12, 18) = 6
lcm(12, 18) = 12 × 18 / 6 = 216 / 6 = 36
代码:
Java版本:
public int lcm(int a, int b) {
return a / gcd(a, b) * b; // 注意:先除再乘,避免溢出
}
C++版本:
int lcm(int a, int b) {
return a / gcd(a, b) * b;
}
Python版本:
def lcm(a, b):
return a * b // gcd(a, b)
注意:a / gcd(a, b) * b而不是a * b / gcd(a, b),避免a×b溢出。
五、GCD的应用
5.1 约分
问题:把分数化简到最简形式
分数:12/18
gcd(12, 18) = 6
约分:12÷6 / 18÷6 = 2/3
代码:
public int[] simplify(int numerator, int denominator) {
int g = gcd(numerator, denominator);
return new int[]{numerator / g, denominator / g};
}
5.2 LeetCode 1071 - 字符串的最大公因子
题目:
对于字符串 s 和 t,只有在 s = t + ... + t(t 自身连接 1 次或多次)时,我们才认定 "t 能除尽 s"。
给定两个字符串 str1 和 str2 。返回最长字符串 x,要求满足 x 能除尽 str1 且 x 能除尽 str2。
示例:
输入:str1 = "ABCABC", str2 = "ABC"
输出:"ABC"
输入:str1 = "ABABAB", str2 = "ABAB"
输出:"AB"
思路:字符串的GCD就是长度的GCD。
代码:
Java版本:
public String gcdOfStrings(String str1, String str2) {
// 如果str1+str2 != str2+str1,说明没有公因子
if (!(str1 + str2).equals(str2 + str1)) {
return "";
}
// 长度的GCD
int len = gcd(str1.length(), str2.length());
return str1.substring(0, len);
}
private int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
哈吉米:"原来GCD还能用在字符串上!"
六、总结
6.1 核心要点
南北绿豆总结:
- 辗转相除法:
gcd(a, b) = gcd(b, a % b) - 时间复杂度:O(logn)
- GCD与LCM关系:
a × b = gcd × lcm - 应用:约分、字符串GCD、周期问题
6.2 递归 vs 迭代
阿西噶阿西:
- 递归:代码简洁,容易理解
- 迭代:避免栈溢出(数字很大时)
哈吉米:"一般用递归就够了。"
参考资料:
- 《算法导论》- Thomas H. Cormen
- 《具体数学》- Ronald L. Graham
- 欧几里得算法 - Wikipedia