“Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。”
一、题目描述
给定整数 n ,返回 所有小于非负整数 n 的质数的数量 。
示例 1:
输入:n = 10
输出:4
解释:小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。
示例 2:
输入:n = 0
输出:0
示例 3:
输入:n = 1
输出:0
提示:
0 <= n <= 5 * 106
二、思路分析
- 常规枚举,也就是暴力算法:
所谓质数,是指 一个大于1的自然数,除了1和它自身外,不能被其他自然数整除 ,那么就可以在遍历到当前元素的时候,判断其能不能被它之前所有的自然数整除,这样一来就需要双重 for 循环,解法如下:
int countPrimes(int n) {
int count = 0;
for(int i = 2; i < n; i++) {
bool sign = true;
for(int j = 2; j < i; j++) {
if(i % j == 0) {
sign = false;
break;
}
}
if(sign)
count++;
}
return count;
}
- 暴力算法优化
细究暴力计算的方法,我们可以发现,假如一个数为9,那么其二分之一(4.5)后的数都可以不用进行计算,因为后面的数乘上倍数之后肯定大于9,也就说9除以这些数肯定有余,因此也就不用计算了。
事实上这个范围还可以再缩小:考虑到如果y是x的因数,那么x/y也必然是x的因数,因此我们只要校验y或者x/y即可。而如果我们每次选择校验两者中的较小数,则不难发现较小数一定落在[2, √x]的区间中,因此我们只需要枚举[2, √x]中的所有数即可。并且,我们可以发现,一切非 2 偶数一定不可能为质数。所以,我们可以在此处进行另一步的优化。
int countPrimes(int n) {
if(n < 3) return 0;
//从3开始验算,所以初始值为1(2为质数)。
int count = 1;
for(int i = 3; i < n; i++) {
// 当某个数为 2 的 n 次方时(n为自然数),其 & (n - 1) 所得值将等价于取余运算所得值
// 如果 x = 2^n ,则 x & (n - 1) == x % n
if((i & 1) == 0) // 等价于 if(i % 2 == 0)
continue;
bool sign = true;
//用 j * j <= i 代替 j <= √i 会更好。
//因为我们已经排除了所有偶数,所以每次循环加二将规避偶数会减少循环次数
for(int j = 3; j * j <= i; j += 2) {
if(i % j == 0) {
sign = false;
break;
}
}
if(sign)
count++;
}
return count;
}
- 厄拉多塞筛法,简称埃氏筛 基本思路:将2~n中所有质数的倍数都删掉,剩余的数据就是质数。
说概念比较抽象,直接上栗子:
类比 1 到 64 的质数的筛选过程:从 2 开始遍历,把 2 的倍数都标记为 false(表示不是质数),再从 2 的下一位质数 3 (未被标记为 false 的元素)开始也同样标记其倍数。以此类推,最后仍为 true 的则为质数。
- ①新建一个大小为
64的数组,初始值标记为true。 - ②从
2开始遍历。如果布尔值为false,说明不为质数;如果为true,则为质数,计算质数数量的值+1。 - ③如果 xx 是质数,那么大于 xx 的 xx 的倍数 2x,3x,… 一定不是质数,那么将当前值的所有倍数标记为
false。继续循环找下一个质数(即布尔值为true的元素),继续标记非质数...
还有一种算法:线性筛也可以解决这个问题,但面试不作考察,这里就不深究了,感兴趣的读者可以去搜些文章看看。
三、AC 代码
埃氏筛:
int countPrimes(int n) {
int count = 0;
//初始默认所有数为质数
vector<bool> sign(n, true);
for (int i = 2; i < n; i++) {
if (sign[i]) {
count++;
for (int j = i + i; j < n; j += i) {
//排除不是质数的数
sign[j] = false;
}
}
}
return count;
}