[数学规律] 204. 计数质数 (质数判断方法:暴力解 → 缩小范围 → 引入bitmap)
204. 计数质数
题目链接:https://leetcode-cn.com/problems/count-primes/
分类:
- 数学规律(找出质数的判断方法)
- 算法优化(暴力解 → 缩小范围 → 引入bitmap)
- 哈希表(bitmap的思想实质上是哈希表)**
题目分析
质数就是一个数只能被1和它自己整除,注意1不是质数。下面会列举几种计算质数的方法,方法是不断优化的。
- 思路1采用的是根据质数的判定方法,枚举每个数字,判断是不是质数,在此基础上做优化;
- 思路2采用的是设置一个bitmap数组标记非质数,把质数的所有倍数下的数字全部标记为非质数,最后统计剩余的数字即为质数。
题目难点:
- 以暴力解为基础不断优化:找出质数,缩小范围;
- 跳脱暴力解的思想,从另一个角度统计质数:bitmap,找出所有非质数;
思路1:暴力解
统计[2,…,n-1]中质数的数量。
如何判断一个数是不是质数?
对于区间内的每个数i,都依次计算2~i-1是否存在能够整除i的数:如果存在,说明它不是质数,如果都不能整除i,则说明i是质数。
实现代码:
class Solution {
public int countPrimes(int n) {
int res = 0;
//计算2~n-1中有多少质数
for(int i = 2; i < n; i++){
//如果i是质数,则res+1
if(isPrime(i)) res++;
}
return res;
}
//判断一个数是否为质数
public boolean isPrime(int n){
//判断2~n-1区间内是否有可以整除n的数
for(int i = 2; i < n; i++){
//只要有一个数,就说明n不是质数,返回false
if(n % i == 0) return false;
}
return true;
}
}
-
时间复杂度:区间内共n-1-2+1=n-2个数,每个数最差情况下需要计算i-1次,所以最差时间复杂度为O(N^2)
-
存在的问题:最简单的算法,但是效率低,超时。
思路1的优化:缩小计算范围
思路1中判断一个数i是不是质数,需要计算2~i-1是否存在能够整除i的数,实际上并不需要计算那么多数,只需要计算到i/2即可,> i/2的数不可能能够整除i。
实现代码:
把原来isPrime里的for循环条件:
for(int i = 2; i < n; i++)
改为:
for(int i = 2; i <= n / 2; i++)
但时间复杂度仍为:O(1/2*N^2)=O(N^2)
还可以进一步缩小范围:
例如:判断12是不是质数,则会经历如下的过程:
12 = 2 * 6
12 = 3 * 4
12 = √12 * √12
12 = 4 * 3
12 = 6 * 2
...
(按前面isPrime的设计,非质数12在得到12%2==0后就会返回false,但如果是质数,则会计算下面这些所有的步骤,带来计算量的冗余,
这里举例的目的是为了找出计算的冗余在哪,以非质数为例只是为了让因子更好写)
可以发现以√12为分界线,后面做的操作实际上在前面已经做过了,所以for循环实际上只需要枚举到 i == √n,即 i * i <= n。
实现代码:
class Solution {
public int countPrimes(int n) {
int res = 0;
//计算2~n-1中有多少质数
for(int i = 2; i < n; i++){
//如果i是质数,则res+1
if(isPrime(i)) res++;
}
return res;
}
//判断一个数是否为质数
public boolean isPrime(int n){
//判断2~n-1区间内是否有可以整除n的数
for(int i = 2; i * i <= n; i++){
//只要有一个数,就说明n不是质数,返回false
if(n % i == 0) return false;
}
return true;
}
}
- 时间复杂度降为:O(N * √N),可以AC,但还有优化的空间。
思路2:引入bitmap
bitmap实际上就是设置一个类型为boolean,大小为n的数组,bitmap[i]表示数字 i 是不是质数,false表示不是质数,true表示是质数,初始时全部置为true。(这样设置是为了填充bitmap时方便处理)。
bitmap保存的就是[2,…,n-1]中每个数是不是质数,在填充完bitmap后,遍历一次bitmap,统计元素值为 true 的元素的数量,即为所有小于非负整数 n 的质数的数量。
如何填充bitmap?
因为bitmap初始时全部填充为true,所以bitmap[2] == true,可以作为第一个起点开始填充。
如果元素值bitmap[i]为true,说明它是质数,就将bitmap[i2],bitmap[i3],bitmap[i*4],…,全部置为false,表示这些数都不是质数,直到下标超过n时为止;
再选择bitmap[i+1]为true的元素为起点,将起点下标不断乘以2,且下标对应的元素值置false,直到下标超过n为止;
以此类推,当i == n时,说明整个数组都填充完毕,最后统计数组中置为true的元素数量即可。
思路2的优化
和思路1的优化一样,外层for-i循环并不需要遍历2~n-1所有数字,只需要枚举到√n即可,用代码可以表示为:
修改前的外层for-i循环:
for(int i = 2; i < n; i++)
修改后:
for(int i = 2; i * i <= n; i++)
//用i * i 来代替使用Math.sqrt。
实现代码:思路2优化后的代码
class Solution {
public int countPrimes(int n) {
boolean[] bitmap = new boolean[n];//bitmap[i]表示数字i是不是质数
//初始时填充true,为bitmap[2]作为第一个起点做准备
Arrays.fill(bitmap, true);
int res = 0;
//计算2~n-1中有多少质数
for(int i = 2; i * i <= n; i++){//填充bitmap的起点
//如果当前数字i是质数,则以i为起点不断乘以2,3,4,...,元素值全部置false
if(bitmap[i]){
for(int j = i * 2; j < n; j += i){//j+=i等价为i不断乘以2,3,4,...
bitmap[j] = false;
}
}
}
//统计bitmap中记录的质数个数
for(int i = 2; i < n; i++){
if(bitmap[i]) res++;
}
return res;
}
}