素数又称质数,是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。
介绍四种求解自然数n以内素数集合的方法,求解效率依次递进,想看最有效率的方法直接翻到最后就好。
常规方法
算法原理:如果一个数字能被内的某个数整除,它就不是素数,反之就是素数。
时间复杂度:
// 判断单个自然数是不是素数
private static boolean isPrime(int num) {
for (int i = 2; i < num; i++) {
if (num % i == 0) return false;
}
return true;
}
// 求解素数集合
public static List<Integer> getPrimes(int n) {
List<Integer> primes = new ArrayList<>();
for (int i = 2; i <= n; i++) {
if (isPrime(i)) primes.add(i);
}
return primes;
}
常规方法进阶版
算法原理:因为,假设
,如果
,那么
,所以判断一个数是否素数时,只需判断该数能否被
内某个数整除即可。
时间复杂度:
// 判断单个自然数是不是素数
private static boolean isPrime(int num) {
if (num < 2) return false;
for (int i = 2; i * i <= num; i++) {// 区别于常规方法的地方
if (num % i == 0) return false;
}
return true;
}
// 求解素数集合
public static List<Integer> getPrimes(int n) {
List<Integer> primes = new ArrayList<>();
for (int i = 2; i <= n; i++) {
if (isPrime(i)) primes.add(i);
}
return primes;
}
埃氏筛法
算法原理:从2开始,将中每个素数的倍数都标记为合数,剩下的就都是素数。(对比欧拉筛法理解)
时间复杂度:
我的代码实现与算法原理稍有不同,因为代码逻辑将“收集素数”和“筛除合数”两件事情合并到了一个for循环里面
public static List<Integer> getPrimes(int n) {
List<Integer> primes = new ArrayList<>();
if (n < 2) return primes;
// 下标i表示数字i,arr[i]==false表示素数,arr[i]==true表示非质数
boolean[] arr = new boolean[n + 1];
arr[0] = arr[1] = true;// 0、1 既不是质数又不是合数,可用true表示
for (int i = 2; i <= n; i++) {
if (!arr[i]) {
primes.add(i);// 收集素数
for (int j = i * i; j <= n; j += i) arr[j] = true;// 筛除合数
}
}
return primes;
}
埃氏筛法的不足之处是会重复筛除一些合数,比如合数18,判断2是否素数时会被筛除一次,判断3是否素数时又被筛除一次。
就是因为这个原因,它的时间复杂度才到不了,接下来的欧拉筛法弥补了这个不足,将时间复杂度降到了最高效的
。
欧拉筛法
算法原理:从2开始,将中每个素数当作最小质因数去标记合数,剩下的就都是素数。(对比埃氏筛法理解)
时间复杂度:
public static List<Integer> getPrimes(int n) {
List<Integer> primes = new ArrayList<>();
if (n < 2) return primes;
// 下标i表示数字i,arr[i]==false表示质数,arr[i]==true表示非质数
boolean[] arr = new boolean[n + 1];
arr[0] = arr[1] = true;// 0、1 既不是质数又不是合数,可用true表示
for (int i = 2; i <= n; i++) {
if (!arr[i]) primes.add(i);// 是素数
for (int j = 0; j < primes.size() && i * primes.get(j) <= n; j++) {
arr[primes.get(j) * i] = true;// 素数 primes.get(j) 是合数 primes.get(j) * i 的最小质因数
if (i % primes.get(j) == 0) break;// 最重要的一句代码!!!可保证上一行 primes.get(j) 是最小质因数
}
}
return primes;
}
算法中break那行代码的原理最难理解,作用就是弥补埃氏筛法的不足,防止重复筛除合数。
代码含义:如果当前素数primes.get(j)是数字i的最小质因数,那么后面的素数就不可能是数字i及其倍数的最小质因数。
举个例子:假如当前数字是8,当前素数是2,理应跳出循环。如果继续,那么下一个素数是3,所以 ,但是24的最小质因数是2不是3,再继续
,32的最小质因数是2不是5,以此类推,可以发现8及其倍数的最小质因数都应该是2,所以到2这里就应该跳出循环了。