素数的定义
定义:在一个大于0的自然数中,除了1和此整数自身外,不能被其他自然数整除的数。
问题 1、判断一个数是否为素数
问题 2、求解[1,n]之间的素数
问题 3、求解 N 个素数。
问题 4、求解不小于n的最小素数
------------------------
问题 1、判断一个数是否为素数
思路<1> 试除法
思路<2> 试除法 + 抛去偶数
思路<3> 试除法 + 抛去偶数 + 缩小试除范围
思路<4> 对素数试除法 + 减少扫描区间
-----------------
思路(1) 试除法
基本思路:判断n是否为素数时,检查区间[2,n - 1]之间的数是否能整除n。
代码:
bool IsPrime(int nNum)
{
for (int i = 2;i < nNum;i++)
{
if (nNum % i == 0)
{
return false;
}
}
return true;
}
思路(2) 试除法 + 抛去偶数
具体思路:素数肯定是奇数,因为偶数肯定能被2整除,所以在检查区间[2,n - 1]之间的数时,只判断奇数是否能整除n即可。
代码:
///试除法 + 刨除偶数
bool IsPrime(int nNum)
{
if (nNum == 2)//2是素数,直接返回
{
return true;
}
if ((nNum & 1) == 0)//刨除偶数
{
return false;
}
for (int i = 3;i < nNum;i += 2)
{
if (nNum % i == 0)
{
return false;
}
}
return true;
}
注意:
(1)如果直接写for循环,那么上述代码就会错误的判断偶数为素数,因此需要把偶数单独处理,直接判断其是否为偶数。
(2)在单独处理偶数时,这里使用位操作进行做的,效率比取余操作高。
思路(3) 试除法 + 抛去偶数 + 缩小试除范围
具体思路:思路 1和思路 2的试除范围都在[2,n - 1],其实范围可以限制在[2,sqrt[n]],因为如果一个数不是素数,那么它肯定有至少有俩因子,而且因子也是成对出现的,而且一个因子是大于sqrt[n]的,一个因子是小于sqrt[n]的。
举例,100,可以分解为2 * 50、4 * 25、5 * 20、10 * 10
所以,对于每一对因子,我们只需要检查其小于sqrt[n]的因子即可,如果能整除,肯定也能被另一个因子整除。这样就能直接判断其是否能分解成成对的俩因子。
代码:
///试除法 + 刨除偶数 + 减少扫描区间
bool IsPrime(int nNum)
{
if (nNum == 2)
{
return true;
}
if ((nNum & 1) == 0)//刨除偶数
{
return false;
}
for (int i = 3;i * i <= nNum;i += 2)
{
if (nNum % i == 0)
{
return false;
}
}
return true;
}
思路(4) 对素数试除法 + 减少扫描区间
分析:
(1)合数(不是素数就是合数)是由若干个质数相乘而得来的,比如,合数6是有素数2和3相乘得到。15是由素数3和5相乘得到。
(2)前几个思路使用试除法时,也对合数进行了取余,所以造成浪费。比如判断103是否为素数时,如果采用试除法 + 抛去偶数 + 缩小试除范围的方法,则需要取余的整数为2,3,5,7,9,10。我们可以观察3和9,如果一个数N能被9整除,那么它肯定能被3整除。反过来说,如果N不能被3整除,则肯定不能被9整除。因此我们只需要检查3即可。对于10也是,如果N不能被2和5整除,它肯定也不能被10整除。
因此,我们要判断数N是否是素数时,只需要对[1,sqrt(N)]之间的素数进行试除即可。
注意,这里的前提是我们已知[1,sqrt(N)]之间的素数。
代码:假设我们已经知道素数集合nArrPrimeTable中,素数个数为nCount个。
//对素数试除法 + 减少扫描区间
bool IsPrime(int nNum)
{
if (nNum == 2)
{
return true;
}
for (int i = 0;i < nCount && nArrPrimeTable[i] * nArrPrimeTable[i] <= nNum;i++)
{
if (nNum % nArrPrimeTable[i] == 0)
{
return false;
}
}
return true;
}
此时的问题,就是怎么构造素数表? 这就是我们要解决的问题2。
思路(5) 预先存储所有素数 + 二分查找
思路(6) 预先存储所有素数 + O(1)查找
-----------------第一个问题结束------------------
现在的问题变成怎么求解[1,N]之间的素数。
问题 2、求解[1,n]之间的素数
要解决这个问题,必须要解决一个问题:用多大的数组存储这些素数?
这里可以使用素数定理,确定1-N之间的素数。
这里采用博主编程随想的说法,确定小于N的素数个数 = x/ln(x),公式中的 ln 表示自然对数。
假设要估计1,000,000以内有多少质数,用该公式算出是72,382个,而实际有78,498个,误差约8个百分点。
该公式的特点是:估算的范围越大,偏差率越小。
根据素数定理,可以根据最大值n,求解出[1,n]之间素数的个数,由于公式有误差,我们可以在x/ln(x)的基础上,扩大50%。
注意原文章扩大15%,貌似有误差。当N = 490时,求出[1,490]之间的素数为93个,但是1.15 * x/ln(x) = 90.9。
故,扩大15%还是不够的,我们这里为了简单,就扩大了50%。
下面给出具体的方法:
(1)试除法
(2)埃拉托斯特尼筛法
------------------
思路(1)试除法
具体来说,为了加速求解素数,这里采用试除法中最快的方法,即对素数试除法 + 减少扫描区间
代码:
//对素数试除法 + 减少扫描区间
int N = 100;
int nMaxLen = static_cast<int>((N / log(double(N))) * 1.5);
int* arrPrimeTable = new int[nMaxLen];
int nCount = 0;
bool IsPrime(int nNum)
{
if (nNum == 2)
{
return true;
}
if ((nNum & 1) == 0)
{
return false;
}
for (int i = 0;i < nCount && arrPrimeTable[i] * arrPrimeTable[i] <= nNum;i++)
{
if (nNum % arrPrimeTable[i] == 0)
{
return false;
}
}
return true;
}
void InitPrimeTable()
{
assert(N > 2);
for (int i = 2;i <= N;i++)
{
if (IsPrime(i))
{
arrPrimeTable[nCount++] = i;
}
}
}
思路(2) 埃拉托斯特尼筛法
方法:(下面的内容摘自维基百科)
思想:素数的倍数必然是合数。
注意:
(1) 每次筛除的总是素数的倍数。
(2) 算法终止的条件:到某一个素数Pt,使得Pt * Pt >= N 时终止。
举例:
详细列出算法如下:
- 列出2以后的所有序列:
- 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
- 标出序列中的第一个素数,也就是2,序列变成:
- 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
- 将剩下序列中,剔除2的倍数(用红色标出),序列变成:
- 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
- 如果现在这个序列中最大数小于最后一个标出的素数的平方,那么剩下的序列中所有的数都是素数,否则回到第二步。
- 本例中,因为25大于2的平方,我们返回第二步:
- 剩下的序列中第一个素数是3,将主序列中3的倍数划出(红色 + 下划线),主序列变成:
- 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
- 我们得到的素数有:2,3
- 25仍然大于3的平方,所以我们还要返回第二步:
- 现在序列中第一个素数是5,同样将序列中5的倍数划出,主序列成了:
- 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
- 我们得到的素数有:2 3 5 。
- 因为25等于5的平方,跳出循环.
结论:去掉红色的数字,2到25之间的素数是:2 3 5 7 11 13 17 19 23。
代码:
const int N = 1000;
bool arrPrimeTable[N + 1];
int nCount = 0;
void InitPrimeTable()
{
//初始化
for (int i = 2;i <= N;i++)
{
arrPrimeTable[i] = true;
}
//进行剔除
for (int i = 2;i * i <= N;i++)
{
if (arrPrimeTable[i])
{
//剔除i的倍数:t = x * i
for (int t = i + i;t <= N;t += i)
{
arrPrimeTable[t] = false;
}
}
}
//输出
for (int i = 2;i <= N;i++)
{
if (arrPrimeTable[i])
{
cout<<i<<" ";
nCount++;
}
}
cout<<endl<<"总计: "<<nCount<<" 个"<<endl;
}
针对埃拉托斯特尼筛法的优化:
(1)存储空间上的优化
1、使用位图方式存储
思想:使用一个位存储一个数,相比一个整形存储一个数,位图法消耗的空间是整形存储消耗的空间的八分之一。
2、位图存储 + 只存储奇数
思想:由于偶数肯定不是素数,可以仅仅存储奇数,这样空间有可以在位图的基础上省一半。
素数数组与数的对应关系:下标i对应的数为 3 + 2*i
arrPrimeTable[0]:3; arrPrimeTable[1]:5; arrPrimeTable[2]:7;...;
在使用埃拉托斯特尼筛法进行剔除数据时,
当i = 0时,把3的倍数(9,15,21...)置为false,其对应下标为3、6...
当i = 1时,把5的倍数(15,25...)置为false,其对应下标为6、11...
等等,思路和上面是一样的。
(2)算法上的优化
1、只存奇数
思路:和上面位图存储 + 只存储奇数的方法是一样的。只存储奇数即可减少待剔除数据的个数,还可以减少寻找下一个素数时遍历数组元素的个数。
2、优化剔除数据时的起点
思路:假设此次素数为n,基本的埃拉托斯特尼筛法要剔除的素数起点为2n。而其实筛选数据的起点可以为 n*n。之后把所有的n*n + i*n剔除。
注意:网上写的都是把n*n + 2*i*n剔除,通过测试是不对的。比如:n = 2,则i = 0时,删除4。i = 1时,删除8.注意此时6被略过了。
为啥起点可以为n * n?
原因:在n*n之前,n可以筛选的数是 2n,3n,4n...(n - 1)*n。其实这些数都已经被n之前的素数筛选过了,不用筛选了。
那为啥要加上i*n呢,这里n*n +i*n就相当于在n*n的基础上加上n。也是相当于删除素数n的倍数。
相比着之前,筛选数据的起点由i + i 变成了 i * i,所以这也相当于减少了筛选空间。
代码:
int* nArrPrimerTable = NULL;
int nCount = 0;
void InitPrimerTable(int N)
{
nArrPrimerTable = new int[N + 1];
//初始化
for (int i = 2;i <= N;i++)
{
nArrPrimerTable[i] = 1;
}
//剔除
for (int i = 2;i * i <= N;i++)
{
if (nArrPrimerTable[i])
{
for (int j = i * i;j <= N;j += i)
{
nArrPrimerTable[j] = 0;
}
}
}
//输出
for (int i = 2;i < N + 1;i++)
{
if (nArrPrimerTable[i])
{
cout<<i<<" ";
nCount++;
}
}
cout<<endl<<nCount<<endl;
}
******************************************
问题 3、求解 N 个素数。
由于不知道能够包含第N个素数的最小整数M,所以也不能直接使用埃拉托斯特尼筛法(需要知道最大整数)。
方法(1) 试除法
思路:每增加一个数,都可以使用素数试除法 + 减少扫描区间来验证是否为素数,直到找到N个素数。
代码:
方法(2) 确定包含N个素数的整数范围 + 埃拉托斯特尼筛法。
可以直接根据素数定理,确定一个整数M,使得[1,M]中间有N个素数,之后使用埃拉托斯特尼筛法。
问题 4、求解不小于n的最小素数
思路(1):直接使用试除法进行处理
代码:
#include <iostream>
#include <assert.h>
using namespace std;
bool IsPrime(int N)
{
if (N == 2)
{
return true;
}
if ((N & 1) == 0)
{
return false;
}
for (int i = 3;i * i <= N;i += 2)
{
if (N % i == 0)
{
return false;
}
}
return true;
}
int main()
{
int N = 0;
cin >> N;
assert(N > 1);
while(!IsPrime(N))
{
N++;
}
cout<<N<<endl;
}
思路(2):素数试除法求解求不小于N的最小素数
方法:先求出小于N的所有素数,之后使用这些素数判断大于等于N的数是不是素数。
(1)这里首先要求出所有小于N的素数,我们可以使用试除法求解。
(2)之后,在查找不小于N的第一个素数。
代码:使用试除法求解小于N的所有素数 + 求解不小于N的最小素数。
#include <iostream>
#include <cmath>
#include <assert.h>
using namespace std;
int* nArrPrimerTable = NULL;
int nCount = 0;
bool IsPrime(int N)
{
if (N == 2)
{
return true;
}
if ((N & 1) == 0)
{
return false;
}
for (int i = 0;i < nCount && nArrPrimerTable[i] * nArrPrimerTable[i] <= N;i++)
{
if (N % nArrPrimerTable[i] == 0)
{
return false;
}
}
return true;
}
void InitPrimerTable(int N)
{
int nMaxLen = static_cast<int>(N * 1.5 / log(double(N)));
nArrPrimerTable = new int[nMaxLen];
for (int i = 2;i <= N;i++)
{
if (IsPrime(i))
{
nArrPrimerTable[nCount++] = i;
}
}
}
int main()
{
int N = 0;
cin >> N;
assert(N > 1);
InitPrimerTable(N);
while(!IsPrime(N))
{
N++;
}
cout<<N<<endl;
}
思路(3):直接查表
思想:直接给出足够多的素数,之后直接大于n的第一个数,即可。
当然根据素数的存储方式不同,方法也不同。
如果使用位图法,可以直接找到下标为n的数,往后遍历即可
如果数组直接存储素数,则可以使用二分查找,找到第一个大于n的素数即可。
代码略。