题述
给定一个值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
才能继续操作