求质数(素数)算法,及算法优化

质数(素数):只能被1和其本身整除的数字(其中1和0不属于质数)

接下来我们用多种方法求1000以内(包含1000)的质数数量,并且统计每种方法的循环次数
(如果只想知道速度快的方法可以直接看方法五)

方法一:

循环遍历所有情况

int count1 = 0;//循环次数
int count2 = 0;//质数个数
for(int i=2;i<=1000;i++) {
    int flag = 0;//用作标记,如果是质数就为0,不是质数就为1
    for(int j=2;j<i;j++){
        count1++;
        if(i % j == 0 ){
            flag = 1;//不是质数,改为1
        }
    }
    if(flag == 0){
        count2++;
    }
}
System.out.println(count1);
System.out.println(count2);

此时总循环次数为498501次
质数个数有168个

方法二

改进点: 当一个数第一次被比自己小的数(不包含1)整除成功后,我们就可以立刻判断出这个数不是质数,所以我们使用break跳出循环,结束对这个数接下来的判断

int count1 = 0;//循环次数
int count2 = 0;//质数个数
for(int i=2;i<=1000;i++) {
    int flag = 0;//用作标记,如果是质数就为0,不是质数就为1
    for(int j=2;j<i;j++){
        count1++;
        if(i % j == 0 ){
            flag = 1;//不是质数,改为1
            break;//既然已经知道这个数不是质数,那么就可以结束对这个数字的判断
        }
    }
    if(flag == 0){
        count2++;
    }
}
System.out.println(count1);
System.out.println(count2);

此时总循环次数为78022次
质数个数有168个

方法三

改进点: 如果存在数字1能被数字2整除,那么一定存在这样一个小于等于数字1算术平方根的数字2(数学定理),所以一个数字在2~本身算术平方根这个数字区间内没有遇到能够被整除的数字,那么这个数就不是质数
  简单解释一下:因数都是成对出现的。比如,100的因数有:1和100,2和50,4和25,5和20,10和10。看出来没有?成对的因数,其中一个必然小于等于100的开平方,另一个大于等于100的开平方。

注释掉的部分可以进一步提高计算速度,但提高不是非常大

int count1 = 0;//质数个数
int count2 = 0;//循环次数
for(int i=2;i<=1000;i++) {
    int flag = 0;//用作标记,如果是质数就为0,不是质数就为1
    //if(Math.sqrt(i)!=(int)Math.sqrt(i)){
         for (int j = 2; j <= Math.sqrt(i); j++) {//j改为小于i的平方根
             count2++;
             if (i % j == 0) {
                 flag = 1;//不是质数,改为1
                 break;//既然已经知道这个数不是质数,那么就可以结束对这个数字的判断
             }
         }
     //}else {
     //    count2++;
     //}
    if (flag == 0) {
        count1++;
    }
}
System.out.println(count1);
System.out.println(count2);

此时总循环次数为5288次
质数个数有168个

方法四

改进点: 如果数字1取余数字2不等于0,那么数字一取余数字2的倍数一定也不为0。
例如:7%2!=0那么我们没必要再去算7%4和7%6,因为4=22,6=32
所以,我们只需要判断一个数是否能被小于他的质数除尽即可
在这里我们将已经算出来为的质数存到一个数组中,后续的%只需对该数组中小于这个数本身平方根的数字进行

int count1 = 0;//质数个数
int count2 = 0;//循环次数
int re[] =new int[500];//定义一个数组,用于储存质数
re[0] = 2;
int k = 0;
for(int i=2;i<=1000;i++) {
    int flag = 0;//用作标记,如果是质数就为0,不是质数就为1
    for(int j=0;j<=k;j++){
        count2++;
        if(re[j]>Math.sqrt(i)){//只去小于等于该数字本身平方根的数字
            break;
        }
        if(i % re[j] == 0 ){//只对质数取余
            flag = 1;//不是质数,改为1
            break;//既然已经知道这个数不是质数,那么就可以结束对这个数字的判断
        }
    }
    if (flag == 0) {
        re[++k] = i;
        count1++;
    }
}
System.out.println(count1);
System.out.println(count2);

此时总循环次数为3467次
质数个数有168个

方法五

boolean prime[] = new boolean[1001];
int count1 = 0;//质数个数
int count2 = 0;//循环次数
for(int i=2;i<1000;i++){
    prime[i] = true;
}
for(int i=2;i<=1000;i++){
    if(prime[i]){
        for(int j=i+i;j<1000;j+=i){
            count2++;
            prime[j] = false;
        }
    }
}
for(int i=0;i<=1000;i++){
    if(prime[i] == true){
        count1++;
    }
}
System.out.println(count1);
System.out.println(count2);

此时总循环次数为1956次
质数个数有168个

讲解: 利用了筛法,首先,2是公认最小的质数,所以,先把所有2的倍数去掉;然后剩下的那些大于2的数里面,最小的是3,所以3也是质数;然后把所有3的倍数都去掉,剩下的那些大于3的数里面,最小的是5,所以5也是质数……
  上述过程不断重复,就可以把某个范围内的合数全都除去(就像被筛子筛掉一样),剩下的就是质数了。维基百科上有一张很形象的动画,能直观地体现出筛法的工作过程。
在这里插入图片描述

问题延伸

现在是让你求1000以内的素数,我们可以写死让i<=1000,但如果题目改为让你按从小到大的顺序,输出1000个质数该怎么办呢?第1000个质数的范围在哪里呢?
如果我们使用上面的方法四可以这样写

int count1 = 0;//质数个数
int count2 = 0;//循环次数
int re[] =new int[1001];//定义一个数组,用于储存质数
re[0] = 2;
int k = 0;
for(int i=2;;i++) {
	if(count1==1000){
		break;
	}
    int flag = 0;//用作标记,如果是质数就为0,不是质数就为1
    for(int j=0;j<=k;j++){
        count2++;
        if(re[j]>Math.sqrt(i)){//只去小于等于该数字本身平方根的数字
            break;
        }
        if(i % re[j] == 0 ){//只对质数取余
            flag = 1;//不是质数,改为1
            break;//既然已经知道这个数不是质数,那么就可以结束对这个数字的判断
        }
    }
    if (flag == 0) {
        re[++k] = i;
        count1++;
    }
}
System.out.println(count1);
System.out.println(count2);

但是我们知道方法四方法五要慢很多,但方法五中我们是提前定好了boolean prime[] = new boolean[1001];,第一千个质数我们应该将prime[]的大小定位多少呢?

这就涉及到了我们的数学知识, 数学家找到了一些公式,用来估计某个范围内的素数,大概有几个。在这些公式中,最简洁的就是 x/ln(x),公式中的 ln 表示自然对数(估计很多同学已经忘了啥叫自然对数)。假设要估计1,000,000以内有多少质数,用该公式算出是72,382个,而实际有78,498个,误差约8个百分点。该公式的特点是:估算的范围越大,偏差率越小。
  有了素数定理,就可以根据要打印的质数个数,反推出这些质数分布在多大的范围内。因为这个质数分布公式有一定的误差(通常小于15%)。为了保险起见,把反推出的素数分布范围再稍微扩大15%,应该就足够了。

空间优化

看到这你因为这已经是最优的解法了吗?不,现在我们只优化了时间,还没有优化空间。
有些程序猿会想出按位(bit)存储的思路。
  以Java为例。一个boolean占用4字节内存(boolean类型占了单独使用是4个字节,在数组中又是1个字节)。而1个字节有8个比特,每个比特可以表示0或1。所以,当你使用按位存储的方式,一个字节可以拿来当8个布尔型使用。所以,构造一个定长的byte数组,数组的每个byte存储8个布尔值。空间性能相比直接定义boolean,提高8倍(对于Java而言)。

最后总结

关于代码优化这件事,我认为没有最优,只有更优,科技永远不会停下前进的脚步

努力吧,少年!

评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值