1. 从一道基础题说起:为什么最大公约数值得单独写一篇
如果你有半年以上的编程经验,大概率接触过“求两个数的最大公约数”这道题。它出现在大一C语言课、研究生机试、考研408、各类算法入门书的第一章,甚至在一些公司笔试题里换个马甲继续出现。一个看似简单的数论问题,却能拆出枚举、辗转相除、递归、位运算、扩展欧几里得、性能对比等多层内容,所以说它不是一道“会做就完了”的题,而是一道非常适合用来打磨基本功的题。
“最大公约数”这个数学概念本身很直白:如果有一个整数d,能同时整除a和b,并且是所有能同时整除它们的正整数里最大的那个,那d就是a和b的最大公约数。写成数学记号就是gcd(a, b),也就是Greatest Common Divisor。我们求它,不是为了应付考试,而是因为在现实工程中它的出场频率远比想象中高:分数约分、比例化简、数论加密、哈希表容量设计、字符串循环节判断、图像缩放计算、音频采样率转换等等,都会直接或间接用到最大公约数。
这篇博文围绕我实际写代码、讲课时积累的经验展开,重点讲两件事:一是枚举法怎么做到“思路正确但也能踩坑”,二是辗转相除法为什么只用两行代码就能秒杀暴力枚举,以及它背后的数学原理、递归陷阱、性能边界。最后我会结合自己做项目时的经验,聊聊求最大公约数在真实场景中会遇到哪些教科书不会讲的问题。
提示:这篇文章覆盖的代码以C/C++为主,但涉及的数学原理和思路适用于任何语言。你会Python、Java、Go都不影响阅读。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 枚举法:最直觉的解法,但藏着边界条件的坑
枚举法的思路一句话就能说清楚:从较大的数开始往下试,第一个能同时整除a和b的数就是最大公约数。许多教材的写法是从1往上找,记录最后一个能整除的,这样也能得到结果,但稍微想一下就知道,从大到小找能更快地结束循环——第一个满足条件的数就是答案,不用遍历完整段整数域。
用代码表示,最常见的入门写法是这样的:
c复制int gcdByLoop(int a, int b) {
int min = (a < b) ? a : b;
int gcd = 1;
for (int i = 1; i <= min; i++) {
if (a % i == 0 && b % i == 0) {
gcd = i;
}
}
return gcd;
}
这个写法逻辑完全正确,但我一直不建议初学者把“记录最后一个满足条件的值”作为默认方案。原因有两个:一是它必须完整遍历1到min的所有整数,哪怕min是100万,它也要老老实实跑100万次,潜在的性能浪费比较大;二是代码的可读性和目标感不如“从大到小找到第一个就return”的方案强。
从大到小的写法是这样的:
c复制int gcdByLoopDesc(int a, int b) {
int min = (a < b) ? a : b;
for (int i = min; i >= 1; i--) {
if (a % i == 0 && b % i == 0) {
return i;
}
}
return 1;
}
从功能上说,这两种枚举法的结果完全一样,但第二种通常执行得更快,因为它在大多数情况下不需要遍历完整段区间。比如求gcd(8, 12),从8往下试,第一个满足条件的是4,直接返回,连1的检查都不用做。而第一种写法,从1试到4,要循环4次才得到结果,虽然次数也不多,但数据一大差距就非常明显了。
2.1 枚举法的时间复杂度与最坏情况分析
枚举法的核心操作是取模运算(%)。对每个i都要执行两次取模,循环次数取决于min(a, b)的大小,所以时间复杂度是O(min(a, b))。当a和b都是千万级甚至更大的数时,这个算法的时间开销会膨胀到不可接受。
有人可能会说,从大到小枚举不是经常提前返回吗?确实,但我们必须考虑最坏情况。最坏情况是什么?是a和b互质,也就是它们的最大公约数恰好是1。此时无论从大到小还是从小到大,都必须把从min到1的所有整数全部试一遍,效率完全一样。比如gcd(999999937, 999999929),这两个都是大质数,枚举法需要循环约10亿次,在普通机器上要跑几秒,这在算法竞赛或高并发系统中是不可接受的。
这里我补充一个实际测试数据。我曾在本地跑过一组对比实验:求gcd(1000000000, 999999999),两者的最大公约数是1。用从大到小的枚举法,大约花费1.2秒;而用后面会讲的辗转相除法,耗时小于1微秒。差距达到了百万倍级别。这就是为什么枚举法只适合用来“理解概念”,不适合真正解决大规模问题。
2.2 枚举法常见错误:漏掉a或b为0的边界情况
枚举法看起来简单,但我见过太多人在这道题上栽在边界条件上。最常见的是没有处理a或b为0的情况。
数学上,gcd(a, 0)的定义是|a|,因为任何非零整数都能整除0,所以最大的公约数是a本身。比如gcd(12, 0) = 12,gcd(0, 5) = 5。但很多枚举法代码里,如果a是0,min(a, b)就是0,循环条件for (int i = min; i >= 1; i--)直接不执行,函数返回一个默认值,结果完全错误。
还有一个容易被忽视的边界是负数。虽然“最大公约数”通常定义在正整数范围内,但在实际工程中,我们可能拿到负数的输入。比如在分数化简中,分子是-12,分母是-18,你肯定希望gcd返回6,而不是-6或1。如果枚举法用负数直接参与取模,虽然C/C++的%运算在负数场景下也能给出结果,但因为C++中负数取模的符号与操作数相同,代码的可读性和跨语言一致性都很差。
所以,无论用哪种算法,我建议在函数入口处统一做一次“绝对值化+0值处理”:
``
