面试官问小灰:如何用程序判断质数?
质数(Prime number),又称素数,指在大于 的自然数中,除了 和该数自身外,无法被其他自然数整除的数(也可定义为只有 与该数本身两个正因数的数)。
如何快速判断某个数是否为质数?如何再给定区间内筛出所有的质数?
以上两个问题是大厂面试官常常喜欢考察的。本文采用多种思路,对以上两个问题进行简析。
本文所有的函数参数均默认为自然数。
文章所涉及的代码使用C++语言,使用的缺省源如下:
# include <cstdio>
# include <ciso646>
namespace Main {
namespace Source {
typedef short unsigned int hu;
typedef unsigned int uint;
typedef long long unsigned int llu;
}
using namespace Source;
static inline const void main() {}
}
signed int main() { Main::main(); return 0; }
问题1:素数判断
判断一个非负整数 是否为质数。
解决方案 1.1
根据定义,可以写出如下代码:
static inline const bool isprime(const uint a) {
if (a == 0 or a == 1) return false;
for (register uint i(2); i < a; ++i) if (not(a % i)) return false;
return true;
}
时间复杂度 ,空间复杂度 , 内约可以解决 的问题。
解决方案 1.2
考虑优化。
我们观察一下一个合数 会被哪个数筛掉。可列出下表(记为表 1):
筛掉 的数 | |
---|---|
4 | 2 |
6 | 2 |
8 | 2 |
9 | 3 |
10 | 2 |
12 | 2 |
14 | 2 |
15 | 3 |
16 | 2 |
18 | 2 |
20 | 2 |
21 | 3 |
22 | 2 |
24 | 2 |
25 | 5 |
26 | 2 |
(左侧为 ,右侧为筛掉 的数。)
核心代码:
static inline const uint mpf(const uint c) {
for (register uint i(2); i < c; ++i) if (not(c % i)) return i;
return 0;
}
发现筛掉 的数都较小,我们想到,如果在一个比较小的范围内没有 的约数,那是否可以判断 是质数呢?
于是,我们考虑找到一个合数 的最小非 因数的上限。
设 为一个合数,令 为 的最小非 因数,令 ,显然 。
由于 为合数,故 ,故 ,而 为 的最小非 因数,故 。
故 ,。
所以,若 为合数,则 必定有一个不大于 的因数;若 中没有 的因数,则 为质数( 除外)。
所以枚举到 即可。
static inline const bool isprime(const llu a) {
if (a == 0 or a == 1) return false;
for (register uint i(2); 1ull * i * i <= a; ++i) if (not(a % i)) return false;
return true;
}
时间复杂度 ,空间复杂度 , 内约可以解决 内的问题。
问题2:区间内筛选素数
筛出 中的质数,得到一张 的质数表。
解决方案 2.1
可以通过上面 1.2 中的代码判断每个数是否是质数。
static inline const bool isprime(const llu a) {...}
enum { N = 1u << 20 };
static uint n;
static bool isp[N];
static uint prime[N], cp;
static inline const void main() {
scanf("%u", &n);
for (register uint i(0); i < n; ++i) if (isp[i] = isprime(i)) prime[cp++] = i;
for (register uint i(0); i < cp; ++i) printf("%u\n", prime[i]);
}
时间复杂度 ,空间复杂度 ,由于大部分数为合数,常数较小, 内约可以解决 的问题。
解决方案 2.2
观察表 1,我们发现,筛掉合数的数总是质数。
于是我们猜想:一个合数 的最小非 因数为质数。
证明:若 的不为 的最小因数为 且 并非质数,则 必然有约数 满足 ;此时必然有 ,又 ,故 且 ,与 是 的最小非 因数矛盾。得证。
于是可以优化一下 isprime
函数。
enum { N = 1 << 24 };
static uint n;
static bool isp[N];
static uint prime[N], cp;
static inline const bool isprime(const llu a) {
if (a == 0 or a == 1) return false;
for (register uint i(0); i < cp and 1ull * prime[i] * prime[i] <= a; ++i)
if (not(a % prime[i])) return false;
return true;
}
static inline const void main() {
scanf("%u", &n);
for (register uint i(0); i < n; ++i) if (isp[i] = isprime(i)) prime[cp++] = i;
for (register uint i(0); i < cp; ++i) printf("%u\n", prime[i]);
}
时间复杂度 (由素数定理 中素数密度约为 ),空间复杂度 , 内约可以解决 的问题。
图中的曲线分别表示质数数量 pi(n)(蓝)、n / ln n(绿)与 Li(n)(红)。
解决方案 2.3
既然可以用质数判断一个数是否为合数,那为什么不直接用质数筛出合数呢?这样可以减少很多不必要的计算吧。
容易想到,我们从 开始枚举,用 isp[i]
表示 之前有没有被筛过,若枚举到一个数未被筛过,则其为质数,用之筛去后面的合数。
enum { N = (const uint)4e7 };
static uint n;
static bool isp[N];
static uint prime[N], cp;
static inline const void main() {
scanf("%u", &n);
for (register uint i(0); i < n; ++i) isp[i] = true; isp[1] = isp[0] = false;
for (register uint i(0); i < n; ++i) {
if (isp[i]) {
prime[cp++] = i;
for (register uint j(i); j < n; j += i) isp[j] = false;
}
}
for (register uint i(0); i < cp; ++i) printf("%u\n", prime[i]);
}
时间复杂度 ,空间复杂度 , 内约可以解决 的问题。
这种方法被称为埃氏筛(埃拉托斯特尼筛法,sieve of Eratosthenes),是一种非常经典的入门筛法。
时间复杂度直观证明:
假设素数在区间内按照质数定理的结论均匀分布,将求和转化为积分,可得计算次数约为
解决方案 2.4
2.3 的主要缺点是合数被筛出多次,造成时间复杂度偏大。
考虑使每个合数 被其最小质因数筛掉。
设大于 的正整数 的最小质因数为 (若 为质数显然有 ),定义 。
考虑枚举素数 和大于 的正整数 ,使得 (此时显然 )。
那么,如果我们能找到一种方法枚举出所有这样的数对 ,我们就可以筛出所有合数(即 )。
比较显然地,有 ,故 等价于 。
于是,我们枚举满足 为质数且 的数对 即可。
考虑枚举 ,发现确定 后 不太容易枚举。
于是考虑枚举大于 的正整数 ,确定 后枚举质数 ,使得 。
我们确定 后从小到大枚举质数 ,则第一个满足 的质数 即为 ,此前枚举到的 均满足 。
于是可以写出如下代码:
enum { N = (const uint)2e8 };
static uint n;
static bool isp[N];
static uint prime[N], cp;
static inline const void main() {
scanf("%u", &n);
for (register uint i(0); i < n; ++i) isp[i] = true; isp[1] = isp[0] = false;
for (register uint i(2); i < n; ++i) {
if (isp[i]) prime[cp++] = i;
for (register uint j(0); j < cp and 1ull * i * prime[j] < n; ++j) {
isp[i * prime[j]] = false;
if (not(i % prime[j])) break;
// 注意到这里枚举到了首个满足 i mod prime[j] = 0 的 j,不能简单地将判断移至 for 语句中。
}
}
for (register uint i(0); i < cp; ++i) printf("%u\n", prime[i]);
}
时空复杂度 , 内约可以解决 的问题。
这种方法被称为线性筛法(欧拉筛法,sieve of Euler),是一种非常常用的筛法。
由于这种方法可以方便地区分筛掉合数的两个数之间是否存在倍数关系,故常常可用来筛积性函数。
好了,关于质数的一系列面试问题我们就聊到这里,记得一键三连哦~~