最大公约数GCD:辗转相除法与应用

前言

最大公约数(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=1218÷1=18 ✓
分给2个人:12÷2=618÷2=9 ✓
分给3个人:12÷3=418÷3=6 ✓
分给4个人:12÷4=318÷4=4.5 ✗(糖果分不均)
分给5个人:12÷5=2.4 ✗
分给6个人:12÷6=218÷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也是ba%b的公约数

反过来,ba%b的公约数也是ab的公约数

所以: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)

表格演示

步骤aba % b操作
118126gcd(18,12) → gcd(12,6)
21260gcd(12,6) → gcd(6,0)
360-返回6

再举个例子:gcd(48, 18)

步骤aba % b操作
1481812gcd(48,18) → gcd(18,12)
218126gcd(18,12) → gcd(12,6)
31260gcd(12,6) → gcd(6,0)
460-返回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至少减少一半(斐波那契数列最坏情况)。"

最坏情况:ab是相邻的斐波那契数

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 核心要点

南北绿豆总结:

  1. 辗转相除法gcd(a, b) = gcd(b, a % b)
  2. 时间复杂度:O(logn)
  3. GCD与LCM关系a × b = gcd × lcm
  4. 应用:约分、字符串GCD、周期问题

6.2 递归 vs 迭代

阿西噶阿西

  • 递归:代码简洁,容易理解
  • 迭代:避免栈溢出(数字很大时)

哈吉米:"一般用递归就够了。"


参考资料

  • 《算法导论》- Thomas H. Cormen
  • 《具体数学》- Ronald L. Graham
  • 欧几里得算法 - Wikipedia