← 目录 / 算法文档 · 模块一 数学基础 / 1.3 质数判断

1.3 质数判断

判断一个数是不是质数——从最朴素的逐一枚举,一步步优化到只需要检查几次。

本页目录
① 什么是质数

质数(也叫素数)是指一个大于 1 的自然数,除了 1 和它本身以外,不能被任何其他正整数整除。比如 2、3、5、7、11、13 都是质数;而 4 = 2×26 = 2×39 = 3×3 这些能被别的数整除的,叫做合数

⚠️
最容易搞错的一点:1 既不是质数,也不是合数。质数的定义要求"正好有两个不同的正因数"——1 和它本身。但 1 的因数只有它自己一个(1),凑不出"两个不同的因数",所以被单独排除在外,既不算质数也不算合数。判断质数时,n < 2 永远应该直接排除。
💡
2 是唯一的偶数质数。除了 2 以外,其余所有偶数都至少能被 2 整除——既能被 1、被自己整除,也能被 2 整除,因数个数超过两个,所以都是合数。后面优化的时候会专门用到这个性质。
② 基础写法:枚举到 n-1

最直接的想法:从 2 开始,把 2n-1 之间所有的数都拿来试一遍,看 n 能不能被整除。只要找到一个能整除的,n 就不是质数;如果一个都找不到,n 就是质数。

C++ · 枚举所有可能的因数
1bool IsPrime(int n)
2{
3 if (n < 2) return false; // 0、1 都不是质数
4 for (int i = 2; i < n; i++)
5 {
6 if (n % i == 0) return false; // 找到一个因数,n 不是质数
7 }
8 return true;
9}

这种写法完全正确,但效率不高——如果 n 是质数(比如 n = 37),循环会从 2 一直跑到 36,整整检查 35 次才能确定"一个因数都找不到"。n 越大,要检查的次数也越多,时间复杂度是 O(n)

③ 优化一:只需要检查到 √n

其实根本不需要检查到 n-1。关键的发现是:一个数的因数总是成对出现的。如果 an 的一个因数,那么 n / a 必然也是 n 的因数(因为 a × (n/a) = n)。而这一对因数里,较小的那个一定不超过 √n——如果两个因数都比 √n 大,它们的乘积就会比 n 还大,不可能等于 n

36 的因数对 —— 每一对都以 √36 = 6 为镜像中心
1
×
36
= 36
2
×
18
= 36
3
×
12
= 36
4
×
9
= 36
√36 = 6
6
×
6
= 36
蓝色格子(1 2 3 4)是"较小的那一半"因数,灰色格子(36 18 12 9)是它们各自配对的"较大的那一半"——只要把蓝色这些数检查一遍,灰色的因数自动就被发现了(因为它们是配对出现的),完全不需要单独检查。6 × 6 = 36 正好卡在中间,是这个数的平方根,也是检查范围的终点。
C++ · 只检查到 √n
1bool IsPrime(int n)
2{
3 if (n < 2) return false;
4 for (int i = 2; i * i <= n; i++)
5 {
6 if (n % i == 0) return false;
7 }
8 return true;
9}
⚠️
为什么写 i * i <= n,不直接写 i <= sqrt(n)sqrt() 函数算出来的是浮点数,存在精度误差——比如某些情况下 sqrt(25) 可能因为浮点误差算成 4.999999998 而不是干净的 5.0,导致循环提前一步结束,漏检了 i = 5 这个本该检查的因数。i * i <= n 全程都是整数运算,不会有任何精度问题,而且比调用 sqrt() 函数更快——这是竞赛代码里的标准写法。
🎯
这一个改动,把时间复杂度从 O(n) 降到了 O(√n)——当 n 很大时(比如十万、一百万),这个差距是巨大的:n = 1000000 时,基础写法最多要检查 999999 次,优化后只需要检查约 1000 次,快了近 1000 倍。
④ 优化二:跳过偶数因数

还能再快一点。上一节提到,除了 2 以外的偶数都不是质数——所以可以先把 n = 2 单独处理掉,再把其余的偶数 n 直接排除。剩下需要循环检查的 n 必然是奇数,而奇数永远不可能有偶数因数(如果一个偶数能整除 nn 就会是偶数,矛盾)——所以循环里可以直接跳过所有偶数,只检查奇数因数,相当于又把检查次数砍掉了接近一半:

C++ · 排除偶数,只检查奇数因数
1bool IsPrime(int n)
2{
3 if (n < 2) return false;
4 if (n == 2) return true; // 2 是质数(唯一的偶数质数)
5 if (n % 2 == 0) return false; // 排除其余偶数
6 for (int i = 3; i * i <= n; i += 2) // 只检查奇数
7 {
8 if (n % i == 0) return false;
9 }
10 return true;
11}

循环的起点从 2 改成了 3,每次增加 2i += 2)而不是 1,跳过了所有偶数:3, 5, 7, 9, ...。配合前面 i * i <= n 这个边界,整体检查次数又减少了接近一半。

⑤ 完整代码与对比

n = 37(质数)实际数一数三种写法各需要检查多少次,差距非常直观:

判断 37 是不是质数 —— 三种写法的检查次数对比
基础写法
35 次
只检查到 √n
2 次
+ 跳过偶数
1 次
基础写法要从 2 检查到 36,整整 35 次;只检查到 √37≈6.08 之后,只需要检查 i=2,3,4,5,65 次(这里实际只数了 i*i<=37 成立的次数,即 i 取到 6 为止);再跳过偶数只检查奇数 i=3,5,只需要 2 次就能确定 37 是质数。优化的效果立竿见影。
写法检查范围时间复杂度说明
基础写法2 ~ n-1,逐个检查O(n)最直观,但 n 较大时很慢
只检查到 √n2 ~ √nO(√n)核心优化,必须掌握
+ 跳过偶数3 ~ √n,只取奇数O(√n),常数减半锦上添花,复杂度量级不变但实际更快
💡
如果要判断很多个数是不是质数呢?本节的 IsPrime 函数适合"只判断一个数"的场景,每调用一次都要重新计算一遍。但如果题目要求"找出 1 到 100 万之间所有的质数",对每个数都跑一遍 IsPrime 会非常慢——这种场景下有专门的批量筛法,一次性把一个范围内所有的质数都标记出来,效率比逐个判断高得多。这个技巧叫线性筛,会在模块一的最后一节(1.8)详细介绍。