题目描述
统计所有小于非负整数 n 的质数的数量。
-
示例 1:
输入:n = 10
输出:4
解释:小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。 -
示例 2:
输入:n = 0
输出:0 -
示例 3:
输入:n = 1
输出:0
提示:
0 <= n <= 5 * 106
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/count-primes
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
解题思路
思路一:暴力法
很容易想到的一个方法,遍历到 i 时,就看看**2~sqrt(i)**间有无数字可以被 i 整除,有则是合数,无则为质数。
思路二:埃氏(Eratosthenes)筛
遍历到质数 i 时,把 i×2、i×3…直至 i×j<n标记为合数。
初始全标记为质数,然后如一开始遍历到2时,2是质数标记,质数计数器+1,并把4、6、8…标记为合数,遍历到3时,3是质数标记,质数计数器+1,并把6、9…标记为合数,遍历到4时,4是合数标记,计数器不变,并把8、12…标记为合数,以此类推。
当然也不用遇到每个质数 i 都从2倍的、3倍的开始标记,可以从 i倍的 i、i+1倍的 i开始遍历。
思路三:线性筛
也不能说是思路二的优化,线性筛与埃氏筛相比,是拿空间换时间。
如果理解了思路二,会发现在遍历到3时,会把12标记一遍,在遍历到4时,也会把12标记一遍,即重复标记。线性筛便是通过开辟一额外数组以记录遍历过的质数,来防止重复遍历。
防止重复遍历这一概念比较难以理解,下面提出一种自认为较明了的理解方式。(斜体数字为遍历的当前数,加粗数字为数组中提取的数)
遍历到2–>2为质数标记–>把2放入数组中–>[2]–>2×2=4–>把4记录为合数
遍历到3–>3为质数标记–>把3放入数组中–>[2,3]–>3×2=6,3×3=9–>把6、9记录为合数
遍历到4–>4为质数标记–>不把4放入数组中–>[2,3]–>4×2=8,但是4%2=0,这里不把后续的12标记为合数–>只把8记录为合数
…
为什么遍历至能被整除便停止标记?
假设遍历至数字a,且a为质数标记,数组中有数字b1–>把a放入数组中,把a×b1标记为合数–>若a%b1=0(a/b1=x),那么对于数组中的下一个数字b2而言,a×b2这一个数会在遍历至a/b1×b2=x×b2时又被标记一次,因此不把a×b2这一个数在当前循环中标记为合数,而在遍历至x×b2时把a×b2这个数标记为合数。
代码详解
埃氏筛
class Solution {
public int countPrimes(int n) {
int[] nums = new int[n]; // 0与1的数组,0记为质数,1记为合数
int count = 0; // 质数计数器
for(int i = 2; i < n; ++i) { // 从2开始遍历
if(nums[i] == 0) {
++count; // 如果标记为质数则计数器+1
if((long)i * i < n) { // 注意要用long强转,不然越界
for(int j = i * i; j < n; j = j+i) { // 把i倍的i、i+1倍的i...标记为合数
nums[j] = 1;
}
}
}
}
return count;
}
}
线性筛
class Solution {
public int countPrimes(int n) {
int[] nums = new int[n]; // 0与1的数组,0记为质数,1记为合数
int count = 0; // 质数计数器
ArrayList<Integer> array = new ArrayList<>(); // 新开辟的数组,用于存放以遍历过的质数
for(int i = 2; i < n; ++i) {
if(nums[i] == 0) {
++count; // 标记为质数时计数器+1
array.add(i); // 且把该质数放入数组array中
}
for(int j = 0; j < array.size() && array.get(j) * i < n; ++j) {
nums[array.get(j) * i] = 1;
if(i % array.get(j) == 0) { // 一旦能被整除便停止内层循环
break;
}
}
}
return count;
}
}
注意点
- sqrt(a)比a×a计算速度慢,但在计算a×a时注意用long强转,防止越界。
- 线性筛与埃氏筛相比,用空间换时间,且该方法不好想到,因此本人更推荐埃氏筛,线性筛作为扩展。