前阵子搜“整除”这个词,结果页给我推了一连串重复的“整除整除整除”,底下还跟着一道数字特别唬人的条件题:“找出一个 n,让 n+20250412 能被 20240413 整除,而且 n+20240413 能被 20250412 整除。”说实话,这种题放在竞赛卷子里我一眼就能认出套路,但放在搜索热词里确实容易吓住人。这篇文章就把“整除”这件事一次性讲透:先定义清楚什么才算整除,再把热搜里的条件题完整解一遍,接着回到基础,讲判断一个数能否被3和5整除的流程图怎么画,最后落到C语言里/和%的实际用法和那些容易踩的坑。无论你是刚学编程的初学者,还是想复习数论基础的老手,应该都能从这里找到点有用的东西。
1. 整除到底在“整”什么:从热搜词说起
很多人一看到“整除”两个字,第一反应是“能除尽”,比如 6 ÷ 3 = 2,所以 6 能被 3 整除。这个直观理解没错,但不够严谨。数学上的定义是:如果存在一个整数 k,使得 a = b × k,并且 b 不等于 0,我们就说 a 能被 b 整除,记为 b | a。注意这里的 k 必须是整数,不能是小数,也不能带余数。换句话说,整除的本质是“余数为 0 的整数除法”,而不只是“除得尽”。
1.1 “a 能被 b 整除”和“b 整除 a”千万别搞反
这是初学整除最容易翻车的地方。中文说“a 能被 b 整除”,意思是 a 是分子、b 是分母,写成式子就是 a ÷ b 能除尽,也就是 b | a。反过来,“b 整除 a”也是同一个意思。但如果你写成 a | b,那含义就完全变了,变成了 b ÷ a 能除尽。
举个例子:说“12 能被 3 整除”,那是 3 | 12,因为 12 ÷ 3 = 4。如果你看到“3 能被 12 整除”,那就错了,因为 3 ÷ 12 等于 0.25,没有整数商。这个顺序问题在考试和写代码注释里都很常见,尤其是从英文“divisible by”翻译过来时,特别容易弄反。我的建议是:看到中文“被”字,先在脑子里翻译成被除数,再翻译成符号。
1.2 整除性质:为什么这些东西能拿来出题
整除之所以能成为各种条件题、竞赛题的素材,是因为它有一套非常漂亮的性质,其中最常用的有这么几条:
- 如果 b | a 且 b | c,那么 b | (a + c),也 b | (a - c)。简单说,两个都能被同一个数整除的数,相加、相减后仍然能被这个数整除。
- 如果 b | a,那么 b 能整除 a 的任何整数倍,也就是 b | (a × t) 对任意整数 t 成立。
- 结合上面两条,如果 b | a 且 b | c,那么 b | (a × x + c × y),其中 x、y 是任意整数。这条叫“整除的线性组合性质”,是解同余方程的核心武器。
- 如果 a | b 且 b | c,那么 a | c。这叫传递性。
为什么这些性质重要?因为很多题目不会直接问你“某数能不能被某数整除”,而是给你一个带 n 的表达式,比如“n + 20250412 能被 20240413 整除”,让你求 n 的取值范围。这时候如果你只盯着“能不能除尽”这一个层面,就没法下手了。正确的思路是把整除关系转成等式,设商为某个整数 k,然后用代数方法处理。
1.3 除法与整除的关系:先有除法,才有整除
整除和除法不是一回事,但紧密相关。普通除法允许结果是小数,比如 7 ÷ 2 = 3.5,但整除不允许,它要求商是整数。更严格地说,任意整数 a 除以正整数 b,都可以写成带余除法的形式:a = b × q + r,其中 q 是整数商,r 是余数,并且 0 ≤ r < b。当 r = 0 的时候,就是整除;r 不为 0 的时候,就是“不整除”。
这个“带余除法”的视角特别重要,因为它把整除问题转化成了“余数是否为 0”的问题。后面讲 C 语言的%运算符,以及解同余式,都是围绕这个视角展开的。可以说,整除不是一个孤立的概念,它和取模、同余、最大公约数都是一家人。
2. 把最热的那道整除条件题一步步解出来
回到热搜里那道题。条件是:
- n + 20250412 能被 20240413 整除;
- n + 20240413 能被 20250412 整除。
先别急着去猜 n,猜测对于这种年份级别的数字几乎不可能。我们得用同余的语言把条件翻译出来,然后按部就班地解。
2.1 先把两个条件翻译成同余式
设 a = 20240413,b = 20250412。那么原题就变成:
- n + b 能被 a 整除,等价于 n + b ≡ 0 (mod a);
- n + a 能被 b 整除,等价于 n + a ≡ 0 (mod b)。
写成同余式以后,思路就清晰了。第一个式子告诉我们 n ≡ -b (mod a),第二个式子告诉我们 n ≡ -a (mod b)。也就是说,n 同时满足两条同余方程,类似于解一个“同余方程组”。因为这两个模数明显不一样,所以需要找一个同时满足两个条件的 n。
比较有意思的是,a 和 b 之间有一个很显眼的关系:b - a = 9999。这个 9999 不是随便凑的,它让两个数字看起来无关,实际上差了一个奇数倍,算最大公约数的时候就派上用场了。
2.2 用欧几里得算法确认两个年份数互质
解同余方程组之前,必须知道 a 和 b 的最大公约数。如果最大公约数不互质,后面不一定有解;如果互质,则一定有唯一解(模 a × b 意义下)。我用欧几里得算法手算一遍:
- 20250412 - 20240413 = 9999;
- 20240413 ÷ 9999,商 2024,余 2437(因为 9999 × 2024 = 20237976,20240413 - 20237976 = 2437);
- 9999 ÷ 2437,商 4,余 251;
- 2437 ÷ 251,商 9,余 178;
- 251 ÷ 178,商 1,余 73;
- 178 ÷ 73,商 2,余 32;
- 73 ÷ 32,商 2,余 9;
- 32 ÷ 9,商 3,余 5;
- 9 ÷ 5,商 1,余 4;
- 5 ÷ 4,商 1,余 1;
- 4 ÷ 1,商 4,余 0。
所以 gcd(20240413, 20250412) = 1,两个数互质。这步很关键,因为互质意味着后面的同余方程组在模 a×b 下有唯一解,不会出现无解或多解纠缠的情况。
2.3 代入消元求出通解,并验证
既然两个模数互质,就可以放心解方程组了。从第一个条件出发,设:
n + b = a × t
也就是 n = a × t - b。把这个代入第二个条件 n ≡ -a (mod b):
(a × t - b) + a ≡ 0 (mod b)
化简一下,因为 -b 自己就能被 b 整除,直接约掉,得到:
a × (t + 1) ≡ 0 (mod b)
因为 gcd(a, b) = 1,所以 b 必须整除 (t + 1)。换句话说:
t + 1 = b × s
于是 t = b × s - 1。代回 n 的表达式:
n = a × (b × s - 1) - b = a × b × s - a - b
当我们取 s = 0 时,n = -a - b,这是个负数;取 s = 1 时,得到最小非负解:
n = a × b - a - b
验证一下:
- n + b = a × b - a = a × (b - 1),确实能被 a 整除;
- n + a = a × b - b = b × (a - 1),确实能被 b 整除。
所以这个表达式完全满足题目要求。代入 a = 20240413、b = 20250412,最小非负解就是:
n = 20240413 × 20250412 - 20240413 - 20250412
至于通解,就是在 n 的基础上加上 a × b 的任意整数倍,也就是所有满足条件的 n 都写成:
n ≡ a × b - a - b (mod a × b)
到这里,热搜那道题就算彻底解决了。你会发现整个过程没有用到任何“灵感”,就是把文字条件翻译成同余符号,再代入消元,利用互质性把因子剥离开。
2.4 这类题的通式:a、b 互质时,n 可以直接取 ab-a-b
这道题解完以后,我忍不住想提炼一个通式。以后凡是遇到那种“n + b 能被 a 整除,n + a 能被 b 整除”的条件题,只要 a、b 互质,直接取 n = a × b - a - b,就能同时满足两个条件。
为什么?因为 n + b = a × b - a = a × (b - 1),n + a = a × b - b = b × (a - 1)。你看,左边被 a 整除,右边被 b 整除,天然的对称。这个通式看着简单,但真的很有用,不管是做数学题还是写程序验证都方便。
3. 判断“能被3和5整除”的流程图,应该怎么画
题目里还提到一个看起来很基础的流程问题:判断一个数 n 能否同时被3和5整除。别看这题简单,它其实是很多初学程序员画的第一个流程图,也是很多考试里必考的基础题。既然单独拎出来了,我就用一整节聊聊这题背后的数学原理和流程图细节。
3.1 数学根子:3看数位和,5看末位,但程序里统一用取余
先复习一下整除判断的数学口诀:
- 判断一个数能否被3整除,最经典的规律是看各位数字之和。如果各位数字之和能被3整除,那么这个数就能被3整除。例如 123 的数字和是 1+2+3=6,6 能被 3 整除,所以 123 也能被 3 整除。原理是 10 ≡ 1 (mod 3),所以任何十进制数在模 3 意义下等于它各位数字之和。
- 判断一个数能否被5整除,只需要看末位是不是 0 或 5。因为 10 ≡ 0 (mod 5),高位部分在模 5 意义下全是 0,只剩个位起作用。
但如果你要写程序或者画流程图,大多数情况下直接用“取余”更普适:n % 3 == 0 说明 n 能被 3 整除,n % 5 == 0 说明 n 能被 5 整除。为什么程序里更推荐取余而不是数位和?因为取余操作对所有模数都通用,比如判断能否被 7 整除,总不能先算数位和再看某个规则吧,那规则会复杂到你不想背。取余则是一刀切,只要会算余数就行。
3.2 流程走向:一个菱形还是两个菱形
“同时被 3 和 5 整除”的流程图,标准做法是从输入 n 开始,然后先判断 n % 3 == 0 是否为真。如果为假,直接输出“不能同时被 3 和 5 整除”,结束;如果为真,再进入第二个判断,判断 n % 5 == 0。第二个判断如果为真,输出“能被 3 和 5 整除”;如果为假,同样输出“不能同时被 3 和 5 整除”。
这里有一个流程设计上的小选择:可以用两个连续的判断框,也可以把条件合并成一个 (n % 3 == 0 && n % 5 == 0)。画流程图的时候,很多教科书喜欢拆成两个菱形,因为能体现“逐步缩小范围”的思路;实际写代码的时候,我更喜欢合并成一个条件,因为可读性不差,而且少一层嵌套。
如果画拆分版,流程的节点顺序大致是这样:
- 开始
- 输入 n
- 判断 n % 3 == 0
- 否:输出“不能同时被3和5整除”,跳到结束
- 是:进入下一步
- 判断 n % 5 == 0
- 否:输出“不能同时被3和5整除”,跳到结束
- 是:输出“能被3和5整除”,跳到结束
- 结束
这个流程用文字看挺啰嗦,但画成图很直观。关键是两个判断框的下方和右方伸出分支时,必须标注“是/否”,不然别人读了根本不知道哪个分支对应哪个结果。
3.3 再进一步:为什么可以合并成 n % 15 == 0
如果你数学基础好一点,会发现“同时被3和5整除”其实等价于“被15整除”。因为 3 和 5 互质,一个数如果能同时被两个互质的数整除,那么它一定能被这两个数的乘积整除;反过来也成立。所以判断条件可以简化成:
n % 15 == 0
这一点在流程图上特别方便,只用一个判断框,就能同时覆盖两个条件。而且这个规律可以推广:判断一个数能否同时被 p 和 q 整除,如果 gcd(p, q) = 1,那么等价于判断它能否被 p × q 整除。这个方法在竞赛里可以帮你少写很多判断。
3.4 画流程图的几个细节
画流程图虽然基础,但经常有细节被忽略,我挑了三个最常见的点:
- 判断框是菱形,处理框是矩形,起止框是圆角矩形或椭圆。这个约定在很多考试里是硬性要求,别搞混。
- 所有分支必须有明确的标签。尤其是有多个判断框时,光有“是/否”还不够,最好在输出框里写明“输出什么内容”,否则容易引发歧义。
- 画完以后要沿着流程走一遍测试数据。比如 n = 15,第一步 n % 3 == 0 成立,进入第二判断;n % 5 == 0 也成立,输出“能被3和5整除”。再拿 n = 9,第一步成立,第二步失败,输出“不能同时被3和5整除”。两个分支都验证,才算画完。
4. C语言里的整除以和取余:/和%的边界在哪里
很多人把“整除”和“取余”混在一起说,但在C语言里,这是两个不同的运算符:/做除法,%做取余。它们的边界、行为、陷阱,我都会在这一节说清楚。
4.1 整型除法向零截断,余数跟被除数同号
C语言里,如果/两边都是整数,得到的结果也是整数,不会出现小数。这个“整数除法”的舍入规则是从 C99 开始明确为“向零截断”。通俗地说,就是直接砍掉小数部分,朝 0 的方向取整。
举个例子:
- 7 / 3 = 2,而不是 2.333...
- -7 / 3 = -2,因为 -7 / 3 = -2.333...,向零截断得到 -2。
- 7 / -3 = -2,同样是向零截断。
取余运算符%的结果满足这样一个关系:(a / b) * b + a % b == a。因此,如果除法向零截断,余数的符号就永远和被除数(也就是%左边的数)相同。试试看:
- 7 % 3 = 1,因为 7 - 2×3 = 1;
- -7 % 3 = -1,因为 -7 - (-2)×3 = -1;
- 7 % -3 = 1,因为 7 - (-2)×(-3) = 1,被除数是正数,余数就是正数。
这个细节特别容易踩坑。很多人在判断奇数时会写n % 2 == 1,当 n 是负数时,比如 n = -3,-3 % 2在 C 语言里等于 -1,和 1 不相等,结果就错了。正确的写法应该是n % 2 != 0,这样正负奇数都能正确识别。
4.2 实际代码:同时判断3和5整除并打印结果
结合前面讨论的取余思路,用C语言写一个完整的小程序:输入一个整数,判断它能否同时被3和5整除,并且顺手打印出 1 到 1000 范围内所有同时被3和5整除的数。
#include <stdio.h> int main(void) { int n; printf("请输入一个整数 n: "); if (scanf("%d", &n) != 1) { printf("输入格式错误\n"); return 1; } if (n % 3 == 0 && n % 5 == 0) { printf("%d 能被3和5同时整除\n", n); } else { printf("%d 不能被3和5同时整除\n", n); } printf("1到1000之间能被3和5同时整除的数:\n"); for (int i = 1; i <= 1000; i++) { if (i % 3 == 0 && i % 5 == 0) { printf("%d ", i); } } printf("\n"); return 0; }这段代码看起来平平无奇,但有几个值得注意的点。第一个是scanf要检查返回值,不然用户输入非数字时,程序会带着未初始化的 n 继续跑,输出结果完全不可控。第二个是判断条件用n % 3 == 0 && n % 5 == 0,两个判断用逻辑与连接,只要前一个为假,后一个就不会执行,这种短路行为在C语言里是明确保证的。第三个是输出 1 到 1000 的遍历,用取余判断逐个筛选,思路清晰,对初学者友好。
如果你想优化成单条件,可以用i % 15 == 0,结果完全一样。但说实话,写i % 3 == 0 && i % 5 == 0的可读性更好,因为直接对应题目描述,不需要读者额外知道“3和5互质所以等价于15整除”这个数学结论。
4.3 取余的典型工程场景
判断整除只是%的入门用法,取余在实际工程中有几个非常常见的使用场景:
- 循环队列的下标推进。队列缓冲区长度是 capacity,入队时
rear = (rear + 1) % capacity,出队时front = (front + 1) % capacity,这样下标永远不会越界。 - 哈希表的桶定位。很多哈希函数会把散列值对桶数取模,
bucket = hash(key) % bucket_count,让数据均匀落到固定范围。 - 分页计算。列表总数 total,每页大小 pageSize,总页数可以用
(total + pageSize - 1) / pageSize计算,这里巧妙地把向上取整转化成一次整除,不需要浮点数。 - 日期和星期推算。比如想知道某天是星期几,经常用天数的索引对 7 取模。
- 校验位计算。像身份证校验码、ISBN 校验位、银行卡 Luhn 算法,底层都是模运算。
这些场景的共同点是:在有限范围内循环、均匀分布、判断边界。模运算就像给数字“戴上手铐”,让它在 0 到 mod-1 之间来回走。
4.4 千万别踩的坑:除零、浮点取余、负数奇偶判断
C语言里围绕/和%有三个经典大坑,我每个都踩过不止一次。
第一个坑是除以零。任何整数除以 0,或者对 0 取余,都是未定义行为,程序可能崩溃,也可能悄悄返回一个没有意义的值。运行期崩溃还好排查,最怕的是编译器在优化时把未定义行为当成“不会发生”来处理,结果做出奇怪优化。所以,凡是除法或取余,前面的除数一定要检查是否为 0。
第二个坑是浮点数不能用%。C语言里%只接受整数操作数,如果你写7.0 % 3,编译直接报错。浮点数需要取余时要用fmod或fmodf,在<math.h>里。这一点和 Python 差别很大,Python 的%天然支持浮点数,跨语言移植时容易漏。
第三个坑是负数奇偶判断。前面已经提过n % 2 == 1的错误。写判断奇偶时,最稳的是n % 2 != 0,或者用位运算(n & 1) != 0。当然,现代编译器优化得很好,n % 2 != 0一般会被编译成和位运算差不多的指令,不需要为了性能去刻意写位运算。
5. 整除思维的实战延伸:从条件题到写代码的习惯
前面几节已经把概念、题目、流程图、C语言写法都过了一遍。最后这一节我想聊聊整除思维在实际问题中的延展,以及我在写代码和做题时养成的一些习惯,这些可能比单道题更有长期价值。
5.1 “整除条件题”的通解算法
回到热搜那道题,它本质上是同余方程组的求解。更一般地说,如果我们拿到形如:
- n + b 能被 a 整除
- n + a 能被 b 整除
这种“对称型”条件,解法其实可以模板化。第一步,转同余式;第二步,用欧几里得算法求 gcd(a, b),确认互质;第三步,设 n = a×t - b,代入第二个条件;第四步,利用互质条件得到 t 的表达式;第五步,代回并写通解。
如果你觉得这个流程不好记,可以直接记最终结论:a、b 互质时,最小非负解是 n = a×b - a - b。这一个式子能解决一大批“两个数互相整除对方的偏移量”的题目。我至今遇到类似的竞赛题,基本都是先套这个公式再验证,十有八九是对的。
5.2 模运算让一些看似复杂的问题变成一行代码
整除思维在实际开发里最迷人的地方,是很多复杂问题最终能被浓缩成一行取余判断。举个典型的例子:判断一个年份是不是闰年。规则是“能被4整除但不能被100整除,或者能被400整除”。翻译成逻辑表达式就是:
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
这一行代码,就是整除规则在历法里的活学活用。再比如,写一个轮询任务调度器,要按顺序给三个服务分发请求,最简单的实现就是service_id = request_count % 3,一行搞定。所以说,整除和取余并不是只在数学试卷里出现,它们是所有循环、周期、均匀分配问题的最底层工具。
5.3 我踩过的几个整除相关的坑
分享几个我个人真实踩过的坑,希望能帮你省点时间。
第一个坑是我刚学C语言时特别喜欢用int做除法,然后自信满满地把结果当精确值用。比如计算 5 个苹果平均分给 2 个人,5 / 2 在C语言里是 2,不是 2.5,于是实际业务里的金额就少算了。后来我养成了一个习惯:只要涉及除法结果不是整数,先想清楚自己需要的是商、余数还是浮点结果,再决定用/、%还是要转成浮点。
第二个坑是跨语言移植时的负数取余规律差异。在C语言里,-7 % 3 等于 -1;在 Python 里,-7 % 3 等于 2。这是因为 Python 的取余保证结果非负,而 C 语言只保证向零取整。如果你把一个模块从 Python 移植到 C,或者反过来,负数取余的结果会对不上。解决方法是先统一处理符号,或者查阅目标语言的规范,千万别想当然。
第三个坑是“整除判断”在大数计算里的溢出问题。在C语言里,两个大整数相乘得到的结果可能超过int范围。比如前面题里 20240413 × 20250412 这个量级,大约在 4×10^14,明显超过 32 位 int 的范围。实际验证时一定要用long long,不然结果溢出后你拿一个错误数字去验证,怎么都对不上。这个坑在竞赛题里非常常见,不少人公式推对了,却因为类型太小踩雷。
5.4 一个实用的小习惯:先翻译成同余式
最后分享一个我自己用了很久的习惯:但凡遇到整除条件的题目,第一步永远是把它改写成同余式。比如“A 能被 B 整除”,写成 A ≡ 0 (mod B),或者 A = B × k;再比如“A 除以 B 余 3”,写成 A ≡ 3 (mod B),或者 A = B × k + 3。一旦所有条件都变成了同余式,后面不管是用代入法、消元法,还是直接套中国剩余定理,路子一下子就多了。
这个习惯在工作里也很管用。比如排查日志时,你想知道某个请求发生在哪个时间段,可以用时间戳对周期取模;分析数据时,你想确认一条记录是否落在采样点,也是用主键对采样率取余。整除和同余本质上就是在处理“周期性”和“分布规律”,想明白这一层,很多问题都能转化为一行代码的事。
我个人在做题和写程序的时候,最深的体会就是:别嫌“整除”这个概念太基础,越是基础的概念,越容易藏坑。比如余数的符号规则、除零检查、类型溢出,这些看起来都是细节,但一旦踩中,排查成本极高。如果你能把整除、取余、同余这些点吃透,再看那些年份唬人的条件题,其实跟看“3和5整除判断”的感觉是一样的,都是纸老虎。