编程珠玑2-性能监视工具 这个章节主要介绍利用性能监测工具来加速计算素数的程序,因为平时用java,因此调研了下java的性能监测工具,了解了下JFR,jmc自带JFR,安装jmc6.0试用了下,jmc6.0和之前的版本有些区别,可以参考https://stackoverflow.com/questions/50800070/java-mission-control-jmc-6-0-does-not-show-hot-methods-when-examining-a-jfr-fl ,不过JFR只能监测到方法级别,本篇暂且先关注求素数这个算法。
程序p1-p5的优化过程书中已经介绍的很清楚了,这里直接写出java版本的p6的程序如下:
/**
* 功能:普通方法找出小于n的素数
* */
private static int[] findPrimeNumbersBruteForce(int n) {
int count = 0;
//已经找到的素数集合
int[] primeNumbers = new int[n];
int pos = 0;
for (int i = 2; i <= n; i++) {
boolean isPrime = true;
for (int j=0; j < pos && (long)(primeNumbers[j] * primeNumbers[j]) <= i; j++) {
//count ++;
//System.out.println("i : " + i + ", primeNumbers[j] : " + primeNumbers[j]);
if (i % primeNumbers[j] == 0) {
isPrime = false;
break;
}
}
if (isPrime) {
primeNumbers[pos++] = i;
}
}
//System.out.println("findPrimeNumbersBruteForce count : " + count);
return primeNumbers;
}
习题2中提出问题:你能进一步提高写出比性能吗?根据提示,了解埃氏筛法(Sieve of Eratosthenes)。
埃氏筛法是一种由古希腊数学家埃拉托斯特尼(公元前250年)提出的一种简单检定素数的算法。算法思想是从1~n中依次删除2的倍数,3的倍数,.......,从而得到所有的素数。java代码实现如下所示:
/**
* 功能:一般埃氏筛法,找出小于等于n的素数
* */
private static int[] findPrimeNumbersSieve(int n) {
boolean[] mark = new boolean[n+1];
for(int i = 2; i <= n; i++){
mark[i] = true;
}
int i, count = 0;
int[] primeNumbers = new int[n];
for (i = 2; i * i <= n; i++) { //这层可以看作是素数的迭代,因此 i * i <= n
if (mark[i]) {
primeNumbers[count++] = i;
for (int j = i * i; j <= n; j += i) {
mark[j] = false;
}
}
}
for (; i <= n; i++) {
if (mark[i]) {
primeNumbers[count++] = i;
}
}
return primeNumbers;
}
埃氏筛法复杂度为O(nlnlnn)(证明过程可从文末参考资料中了解),因此埃氏筛法并不是线性筛法。一个合数会被标记多次,比如30,会被素数2,3,5各标记一次,如果一个合数质因子很多,那么会被标记很多次。对埃氏筛法进行改进,称为线性筛法(也有叫欧拉筛法),java代码实现如下:
/**
* 功能:线性筛法,找出小于等于n的素数
* */
private static int[] findPrimeNumbersLinearSieve( int n ) {
int count = 0;
boolean[] mark = new boolean[n+1];
for (int i = 2; i <= n; i++) {
mark[i] = true;
}
int[] primeNumbers = new int[n];
int pos = 0;
for (int i = 2 ; i <= n ; i++) { //这层是1...n的迭代
if (mark[ i ] )
primeNumbers[pos++] = i;
for(int j = 0 ; j < pos && i * primeNumbers[j] <= n; j++){
mark[ i * primeNumbers[j]] = false;
//count++;
//System.out.println("i : " + i + ", primeNumbers[j] : " + primeNumbers[j]);
if( i % primeNumbers[j] == 0 ) //保证了一个合数只被最小的素因子标记
break;
}
}
//System.out.println("findPrimeNumbersLinearSieve count : " + count);
return primeNumbers;
}
线性筛法的复杂度为O(n),线性筛法解决了一个合数被标记多次的问题,思路是一个合数只由最小素因子标记,重点在if( i % primeNumbers[j] == 0 ) break; 这行代码上:
primeNumbers中的素数是自增的,
如果i % primeNumbers[j] == 0,即i = k * primeNumbers[j],那么假设 x= i * primeNumbers[j+1] = k * primeNumbers[j] * primeNumbers[j+1] = k' * primeNumbers[j],因此 x的最小素因子是primeNumbers[j], 而不是primeNumbers[j+1],接下来的素数同理,所以x不用质数primeNumbers[j+1]标记。
有没有觉得和普通筛法(findPrimeNumbersBruteForce(int n))很像???但性能却相差很大,本机测试当n=100000000时,findPrimeNumbersBruteForce(int n)耗时20s+,而线筛的只用1s左右。
数学小知识复习
调和级数:调和级数是各项倒数为等差数列的级数,通常指:
1 + 1/2 + 1/3 + 1/4 + 1/5 + 1/6 + 1/7 + 1/8 +...
而 1 + 1/2 + 1/3 + 1/4 + 1/5 + 1/6 + 1/7 + 1/8 +... = ln(n) + C, C为欧拉常数数值是0.5772……
为什么1不是素数?
根据算术基本定理,每一个比1大的整数,要么本身是一个素数,要么可以写成唯一一组素数的乘积,最小的素数是2。如果1是素数,那么这一系列素数就不唯一了,会造成很多麻烦。
参考文章:
http://www.cnblogs.com/dc93/p/3930362.html, https://blog.csdn.net/OIljt12138/article/details/53861367,
http://www.cnblogs.com/zhuohan123/p/3233011.html
Mertens' 2nd theorem