题目描述
给定整数 n ,返回所有小于非负整数 n 的质数的数量。
示例 1
输入:n = 10
输出:4
解释:小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。
示例 2:
输入:n = 0
输出:0
示例 3:
输入:n = 1
输出:0
提示:
0 <= n <= 5 * 10^6
解题思路演进
这道题是经典的数论基础题。根据数据范围 n <= 5 * 10^6,我们可以推导出不同算法的时间复杂度表现。
方法一:暴力枚举(会超时 TLE)
最直观的想法是:遍历从 2 到 n-1 的每一个数字 i,然后判断 i 是否为质数。判断质数的方法是尝试用 2 到 sqrt(i) 之间的数字去整除 i。
代码实现:
cpp
class Solution {
public:
bool isPrime(int x) {
for (int i = 2; i * i <= x; i++) {
if (x % i == 0) return false;
}
return true;
}
int countPrimes(int n) {
int ans = 0;
for (int i = 2; i < n; i++) {
if (isPrime(i)) ans++;
}
return ans;
}
};
复杂度分析:
- 时间复杂度:O(N根号N)。当 N=5×106时,计算量达到十亿级别,在 LeetCode 上必定超时。
- 空间复杂度:O1。
方法二:埃拉托斯特尼筛法(Sieve of Eratosthenes)
既然暴力法会超时,我们需要一种更高效的算法。埃拉托斯特尼筛法(简称埃氏筛)是一种古老且经典的质数筛选算法。
核心思想:
如果 x 是质数,那么 x 的倍数(2x, 3x, 4x...)一定不是质数。我们可以从 2 开始遍历,将当前数字的倍数全部标记为"合数"。遍历结束后,未被标记的数字就是质数。
在实现埃氏筛时,有一个极其重要的优化细节:内层循环从 i * i 开始,而不是 2 * i。
cpp
for (int j = i * i; j < n; j += i) {
isPrime[j] = false;
}
为什么可以从 i * i 开始?
假设当前遍历到的质数是 i。对于 i 的倍数 i * k:
- 如果 k < i,那么 i * k 必然已经被比 i 更小的质数(比如 k 的某个质因数)筛选过了。
- 例如:当 i = 5 时,5 * 2 = 10(已被 2 筛掉),5 * 3 = 15(已被 3 筛掉),5 * 4 = 20(已被 2 筛掉)。
- 因此,为了避免重复标记(重复计算),我们从 i * i 开始标记即可,这是 i 的倍数中第一个尚未被更小质数标记的数字。
代码实现 (C++)
cpp
class Solution {
public:
int countPrimes(int n) {
// 边界条件:小于等于 2 的数没有质数
if (n <= 2) return 0;
// 创建布尔数组,isPrime[i] 表示数字 i 是否为质数
// 初始默认全部为 true (质数)
vector<bool> isPrime(n, true);
// 0 和 1 不是质数
isPrime[0] = false;
isPrime[1] = false;
// 从 2 开始筛,只需要遍历到 sqrt(n) 即可
for (int i = 2; i * i < n; i++) {
if (isPrime[i]) {
// 优化:从 i * i 开始标记,步长为 i
for (int j = i * i; j < n; j += i) {
isPrime[j] = false;
}
}
}
// 统计所有标记为 true 的数字
int count = 0;
for (int i = 2; i < n; i++) {
if (isPrime[i]) count++;
}
return count;
}
};
复杂度分析:
- 时间复杂度:ON loglogN 。这是埃氏筛的经典复杂度,非常接近于线性时间,对于 5×106的数据量可以轻松通过。
- 空间复杂度:ON 。需要一个长度为 N的布尔数组来记录状态。由于 vector<bool> 在 C++ 中经过了位压缩优化,实际占用内存非常小。
进阶拓展:线性筛(欧拉筛)
虽然埃氏筛已经足够优秀,但在某些极端情况下,可能会提到线性筛(欧拉筛)。
埃氏筛的痛点:一个合数可能会被多个质数重复标记。例如 12,会被 2 标记一次(2 * 6),也会被 3 标记一次(3 * 4),存在冗余计算。
线性筛的核心思想:保证每个合数只会被它的最小质因数筛掉。这样时间复杂度可以降到严格的 O(N)。
线性筛代码示例:
cpp
class Solution {
public:
int countPrimes(int n) {
vector<int> primes; // 存储已找到的质数
vector<bool> isPrime(n, true); // 标记数组
int ans = 0;
for (int i = 2; i < n; i++) {
if (isPrime[i]) {
primes.push_back(i);
ans++;
}
// 核心:用当前质数 primes[j] 去筛 i * primes[j]
for (int j = 0; j < primes.size() && i * primes[j] < n; j++) {
isPrime[i * primes[j]] = false;
// 保证每个合数只被它的最小质因数筛掉
if (i % primes[j] == 0) break;
}
}
return ans;
}
};