题述
给定一个值n,统计2~n以内,素数的个数
说明:
素数,即质数。除0、1以外,只能被1和自身整除的自然数,即为素数。
例:
输入:100
输出:25
解决办法
方式一:(暴力解法)
思路
直接从2开始遍历,判断是否能被2到自身之间的数整除
代码示例
public class CountPrime {
public static void main(String[] args) {
System.out.println(countPrime(100));
}
private static int countPrime(int n) {
int count = 0;
for (int i = 2; i < n; i++) {
if (isPrime(i)) {
count++;
}
}
return count;
}
/**
* 判断输入的数是否为质数
*
* @param x 输入的数
* @return true:是;false:否
*/
private static boolean isPrime(int x) {
for (int i = 2; i < x; i++) {
if (x % i == 0) {
return false;
}
}
return true;
}
}
改进思路
上面的暴力解法虽然可以用,但其实还是存在可以优化的地方的。为啥,因为【乘法结合律】的存在。
2 * 50=100; 50 * 2 = 100;
4 * 25=100; 25 * 4 = 100;
以此类推。 如果 a能被x整除得到b,那么b也能被x整除得到a
所以,可以这么说,因为【乘法结合律】,所以我们在计算数能否被某个数整除的时候,可以不用完全遍历,只需要遍历到根号x就可以,如下图所示:(但是根号x不容易表示,所以换一个方式,i * i <= x)
private static boolean isPrime(int x) {
for (int i = 2; i * i <= x; i++) {
if (x % i == 0) {
return false;
}
}
return true;
}
方式二:(埃氏筛选法)
思路
利用合数的概念(非素数),素数*m必然是合数,因此可以从2开始遍历,将所有的合数做上标记,用空间换时间。什么意思呢?
比如,3是素数,那么肯定有如下规律:
3 * 2 = 6; 合数
3 * 3 = 9; 合数
3 * 4 = 12; 合数
3 * 5 = 15; 合数
以此类推,3 * m必然是合数,一直到 3 * m < n 。将这些提前给标记上
代码示例
public class CountPrime {
public static void main(String[] args) {
System.out.println(eratosthenes(100));
}
/**
* 判断输入的数是否为质数
*
* @param x 输入的数
* @return true:是;false:否
*/
private static int eratosthenes(int x) {
boolean[] isPrime = new boolean[x];
int count = 0;
for (int i = 2; i < x; i++) {
if (!isPrime[i]) {
count++;
// 标记合数
for (int j = 2 * i; j < x; j += i) {
isPrime[j] = true;
}
}
}
return count;
}
}
上面的标记合数循环,大家可能一时间反应不过来。j+=i
的原理是【加法分配律】。3 * i = (2 + 1) * i = 2 * i + 1 * i
。所以,为了累加i的系数m,每次+i
就可以了。
改进思路
上面的算法还可以继续改进,跟方式一的改进思路差不多。在内部循环标记合数的时候,存在重复标记情况。比如:(假定输入的n=100)
- 当
i=2
时,会标记2 * k (k>=2, 2 * k < x)
,即2 * 2,2 * 3,2 * 4,··· 2 * 50
,起码已经标记了50 - 2 + 1
个元素了; - 当
i=3
时,会标记3 * k (k>=2, 3 * k < x)
,即3 * 2,3 * 3,3 * 4,··· 3 * 33
,起码已经标记了33 - 2 + 1
个元素了; - 当
i=4
时,会标记4 * k (k>=2, 4 * k < x)
,即4 * 2,4 * 3,4 * 4,··· 4 * 25
,起码已经标记了25 - 2 + 1
个元素了。 - 当
i=5
时,会标记5 * k (k>=2, 5 * k < x)
,事实上,5 * 3
早在前面就被标记过了;5 * 4 = 20
,则早在i =2
时标记过了
显然,如果我们按部就班的标记,肯定会存在交叉、冗余标记。
从上面的描述我们可以总结出一个规律,当i > 2
, (2 * i) ~ (i-1) * i
的部分肯定已经被标记过了。所以,修改内部循环算法如下:
private static int eratosthenes(int x) {
long start = System.currentTimeMillis();
// 默认全部标记为素数
boolean[] isNotPrime = new boolean[x];
int count = 0;
outer:
for (int i = 2; i < x; i++) {
if (!isNotPrime[i]) {
count++;
// 标记合数
for (int j = i * i; j > 0 && j < x; j += i) {
isNotPrime[j] = true;
}
}
}
System.out.println("耗时:" + (System.currentTimeMillis()-start));
return count;
}
注意:当i比较大的时候,可能会出现i * i > Integer.MAX_VALUE
的情况,所以需要判断j > 0
才能继续操作
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/180523.html