质数表的建立
质数
质数的定义:在大于1的自然数中,只能被1和它本身整除的数。
质数的判断
给定一个数$n$,我们要怎么判断这个数是不是一个质数呢?显然,根据定义,我们可以遍历2到$n$的每一个数(不包括$n$自身),如果$n$可以被某个数整除,那么显然$n$就不是质数。如果没有找到这样的数,那么$n$就是质数。可是,我们真的有必要从2遍历到$n$吗?
当然是没有必要的。当我们知道10能被2整除的时候,自然知道10能被5整除;当我们知道21能被3整除的时候,自然知道21能被7整除。其实可以隐约地感受到,假设$n=ab$,当$a$是一个较小的数的时候,$b$就是一个较大的数。问题就是,我们在感受到”较小“”较大“的时候,到底是以哪个数为界的呢?
当 $a = b$ 时,$n = a^2$,即得出$a=\sqrt{n}$。模糊地感觉到,这个界限应该是$\sqrt{n}$。那么我们接下来可以尝试证明一下这个命题:若能将$n$分解为两整数因数的乘积,其中一个因数大于$\sqrt{n}$,那么另外一个因数一定小于$\sqrt{n}$。假设$n = ab$,且$a>\sqrt{n}$,则:
\(b = \frac{n}{a} < \frac{n}{\sqrt{n}} = \sqrt{n}\) \(b < \sqrt{n}\)
好了,那么我们事实上就可以只遍历2到$\sqrt{n}$的数(包括$\sqrt{n}$,如果它是整数的话),就能知道一个数是质数与否(因为如果两个整数因数都大于$\sqrt{n}$,那么乘积也必定会大于$n$,与我们的条件是相悖的)。 下面给出判断质数的C++和Python代码。
C++:
#include<math.h>
#include<iostream>
using namespace std;
bool isPrime(int n)
{
if(n <= 1) return false;
int sq = (int)sqrt(n);
for(int i=2; i<=sq; i++)
if(n%i == 0) return false;
return true;
}
int main()
{
//判断1到20之间的数是否为质数
for(int i=1; i<=20; i++)
{
cout << i << " is ";
if(!isPrime(i)) cout << "not ";
cout << "a prime number" << endl;
}
return 0;
}
Python:
from math import sqrt
def isPrime(n):
if n <= 1:
return False
sq = int(sqrt(n))
for i in range(2, sq+1):
if n%i == 0:
return False
return True
for i in range(1, 21):
if isPrime(i):
print(str(i) + " is a prime number")
else:
print(str(i) + " is not a prime number")
质数表(素数表)的建立
知道如何判断一个数是否为质数,那么也就很简单地可以构建1到$n$的质数表了。质数表也有两种,一种是bool数组,用于判断下标是否为质数,另外一种就是把这个范围内所有的质数放在一个数组中。质数表的建立其实是非常有用的,比如在我们需要多次判断一个数是否为素数的时候,如果一直调用isPrime()函数,其实在时间上有相当大的浪费,所以我们可以把这个范围中的每个数都判断一次,存放在一个哈希表中,这样以后如果还需要判断,那么所需要的时间复杂度就是$O(1)$了。下面介绍两种构建质数表的方法,朴素遍历和筛法。
朴素遍历法
非常朴素,遍历1到$n$的所有数,然后通过isPrime函数判断,建立质数表。C++和Python代码如下,得到的prime_table为哈希表,prime_list为该范围内的质数数组
C++:
#include<math.h>
#include<vector>
#include<iostream>
using namespace std;
bool isPrime(int n)
{
if(n <= 1) return false;
int sq = (int)sqrt(n);
for(int i=2; i<=sq; i++)
{
if(n%i == 0) return false;
}
return true;
}
int main()
{
vector<int> prime_list;
vector<bool> prime_table(101);
for(int i=0; i<101; i++)
{
if(isPrime(i))
{
prime_table[i] = true;
prime_list.push_back(i);
}
}
for(int i=0; i<prime_list.size(); i++)
{
cout << prime_list[i] << endl;
}
return 0;
}
Python:
from math import sqrt
def isPrime(n):
if n <= 1:
return False
sq = int(sqrt(n))
for i in range(2, sq+1):
if n%i == 0:
return False
return True
prime_table = [False for i in range(0, 101)]
prime_list = []
for i in range(0, 101):
if isPrime(i):
prime_table[i] = True
prime_list.append(i)
print(prime_table)
print(prime_list)
筛法
可以看出,朴素遍历法构建素数表的时间复杂度是$O(n\sqrt{n})$。
接下来介绍Eratosthenes筛法,时间复杂度为$O(nloglogn)$。
这个笔者不会证明…想必证明的过程也会十分有趣。
筛法的思想非常简单,对于每一个质数,它的所有倍数必定不是质数,那么我们筛掉即可。首先我们知道2是素数,那么我们就可以筛去4、6、8、10等等。3没有被筛去,那么3是质数,然后3的倍数,即6、9、12等不是素数。然后到4,4已经被筛掉了,我们看5,5没有被筛掉,所以是质数…循环往复。下面给出C++和Python的代码。
(有一个小关键就是要首先把所有数都当作质数,再筛)
C++:
#include<vector>
#include<iostream>
int main()
{
vector<int> prime_list;
vector<bool> prime_table(101);
fill(prime_table.begin()+2, prime_table.end(), true); //要首先假设所有数都是质数
for(int i=2; i<101; i++) //我们需要事先确定0、1不是质数,2是质数
{
if(prime_table[i])
{
prime_list.push_back(i);
for(int j=i+i; j<101; j += i)
{
prime_table[j] = false;
}
}
}
for(int i=0; i<prime_list.size(); i++)
{
cout << prime_list[i] << endl;
}
return 0;
}
Python:
prime_table = [True for i in range(0, 101)]
prime_list = []
prime_table[0] = prime_table[1] = False
for i in range(0, 101):
if prime_table[i]:
for j in range(i+i, 101, i):
prime_table[j] = False
prime_list.append(i)
print(prime_table)
print(prime_list)
参考
[1]胡凡, 曾磊. 算法笔记[M]. 机械工业出版社, 2016.