前言

在算法竞赛和编程面试中,质数筛法是一个高频且重要的考点。面对海量数据,如何高效地找出一定范围内的所有质数?本文将用最通俗易懂的方式,为你“流食般投喂”两种经典筛法:埃拉托斯特尼筛法(埃氏筛)欧拉筛(线性筛)。我们将从原理、代码实现到复杂度分析,一步步拆解,确保你不仅能看懂,更能自己写出来。

1. 什么是质数筛法?

质数筛法的目标很简单:快速找出从 2 到 n 之间的所有质数

最朴素的方法是对于每个数 i (2 ≤ i ≤ n),检查它是否能被 2 到 √i 之间的数整除。这种方法的时间复杂度是 O(n√n),当 n 较大时(例如 n=10⁷)会非常慢。

筛法的核心思想是“标记”而非“判断”:我们从一个范围内所有数都是“候选质数”开始,然后逐步排除(筛掉)那些确定不是质数的数(即合数),最后剩下的就是质数。

2. 埃拉托斯特尼筛法(埃氏筛)

2.1 算法原理

埃氏筛的思路非常直观:

  1. 假设我们要筛出 [2, n] 的所有质数。
  2. 从最小的质数 2 开始,将 2 的所有倍数(4, 6, 8, ...)标记为合数。
  3. 找到下一个未被标记的数(此时是 3),它一定是质数,然后将 3 的所有倍数标记为合数。
  4. 重复这个过程,直到我们处理完所有 ≤ √n 的数。为什么是 √n?因为如果 n 有一个大于 √n 的因子,那么它必然还有一个小于 √n 的因子,这个小于 √n 的因子肯定已经被我们筛过了。
  5. 所有未被标记的数就是质数。

2.2 代码实现(C++)

#include <iostream>
#include <vector>
using namespace std;

vector<int> sieveOfEratosthenes(int n) {
    vector<bool> isPrime(n + 1, true); // 初始假设所有数都是质数
    isPrime[0] = isPrime[1] = false; // 0 和 1 不是质数
    
    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;
            }
        }
    }
    
    // 收集所有质数
    vector<int> primes;
    for (int i = 2; i <= n; i++) {
        if (isPrime[i]) {
            primes.push_back(i);
        }
    }
    return primes;
}

int main() {
    int n = 100;
    vector<int> primes = sieveOfEratosthenes(n);
    
    cout << "质数(2-" << n << "):" << endl;
    for (int prime : primes) {
        cout << prime << " ";
    }
    cout << endl;
    return 0;
}

http://my.tv.sohu.com/us/444774565/728682239.shtml
http://my.tv.sohu.com/us/444774565/728682069.shtml
http://my.tv.sohu.com/us/444774565/728682062.shtml
http://my.tv.sohu.com/us/444774565/728682221.shtml
http://my.tv.sohu.com/us/444774565/728682047.shtml
http://my.tv.sohu.com/us/444774565/728682115.shtml
http://my.tv.sohu.com/us/444774565/728681894.shtml
http://my.tv.sohu.com/us/444774565/728681743.shtml
http://my.tv.sohu.com/us/444774565/728681787.shtml
https://tv.sohu.com/v/dXMvNDQ0Nzc0NTY1LzcyODY4MjIzOS5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc0NTY1LzcyODY4MjA2OS5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc0NTY1LzcyODY4MjA2Mi5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc0NTY1LzcyODY4MjIyMS5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc0NTY1LzcyODY4MjA0Ny5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc0NTY1LzcyODY4MjExNS5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc0NTY1LzcyODY4MTc4Ny5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc0NTY1LzcyODY4MTg5NC5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc0NTY1LzcyODY4MTc0My5zaHRtbA==.html
,,,,
http://my.tv.sohu.com/us/444775507/728683360.shtml
http://my.tv.sohu.com/us/444775507/728683252.shtml
http://my.tv.sohu.com/us/444775507/728683410.shtml
http://my.tv.sohu.com/us/444775507/728683173.shtml
http://my.tv.sohu.com/us/444775507/728682895.shtml
http://my.tv.sohu.com/us/444775507/728682774.shtml
http://my.tv.sohu.com/us/444775507/728682878.shtml
http://my.tv.sohu.com/us/444775507/728682869.shtml
http://my.tv.sohu.com/us/444775507/728682933.shtml
http://my.tv.sohu.com/us/444775507/728682700.shtml
https://tv.sohu.com/v/dXMvNDQ0Nzc1NTA3LzcyODY4MzM2MC5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc1NTA3LzcyODY4MzI1Mi5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc1NTA3LzcyODY4MzQxMC5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc1NTA3LzcyODY4MzE3My5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc1NTA3LzcyODY4Mjg5NS5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc1NTA3LzcyODY4Mjc3NC5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc1NTA3LzcyODY4Mjg3OC5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc1NTA3LzcyODY4Mjg2OS5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc1NTA3LzcyODY4MjkzMy5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0Nzc1NTA3LzcyODY4MjcwMC5zaHRtbA==.html
,,,,
http://my.tv.sohu.com/us/444499340/728684900.shtml
http://my.tv.sohu.com/us/444499340/728684796.shtml
http://my.tv.sohu.com/us/444499340/728685049.shtml
http://my.tv.sohu.com/us/444499340/728684779.shtml
http://my.tv.sohu.com/us/444499340/728685024.shtml
http://my.tv.sohu.com/us/444499340/728684760.shtml
http://my.tv.sohu.com/us/444499340/728684693.shtml
http://my.tv.sohu.com/us/444499340/728684681.shtml
http://my.tv.sohu.com/us/444499340/728684584.shtml
http://my.tv.sohu.com/us/444499340/728684578.shtml
https://tv.sohu.com/v/dXMvNDQ0NDk5MzQwLzcyODY4NDkwMC5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0NDk5MzQwLzcyODY4NDc5Ni5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0NDk5MzQwLzcyODY4NTA0OS5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0NDk5MzQwLzcyODY4NDc3OS5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0NDk5MzQwLzcyODY4NTAyNC5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0NDk5MzQwLzcyODY4NDc2MC5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0NDk5MzQwLzcyODY4NDY5My5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0NDk5MzQwLzcyODY4NDY4MS5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0NDk5MzQwLzcyODY4NDU4NC5zaHRtbA==.html
https://tv.sohu.com/v/dXMvNDQ0NDk5MzQwLzcyODY4NDU3OC5zaHRtbA==.html

2.3 关键优化与复杂度分析

  • 从 i*i 开始标记:对于质数 i,它的倍数 i×2, i×3, ..., i×(i-1) 已经被比 i 小的质数(2, 3, ..., i-1)标记过了。所以直接从 i×i 开始标记即可。
  • 时间复杂度:O(n log log n)。这个复杂度已经非常接近线性,对于 n ≤ 10⁷ 的情况完全够用。
  • 空间复杂度:O(n),需要一个大小为 n+1 的布尔数组。

2.4 埃氏筛的局限性

埃氏筛虽然高效,但它有一个小缺点:某些合数会被重复标记多次。例如,合数 12 会被质数 2 和 3 各标记一次(2×6 和 3×4)。当 n 非常大时(例如 10⁸),这种重复标记会带来一些不必要的开销。

3. 欧拉筛(线性筛)

3.1 算法原理

欧拉筛的目标是让每个合数只被标记一次,从而达到真正的 O(n) 时间复杂度。其核心在于:每个合数只被它的最小质因子筛掉

算法流程:

  1. 维护一个质数列表 primes 和一个标记数组 isPrime
  2. 从 2 遍历到 n:
    • 如果当前数 i 是质数(isPrime[i] == true),则加入质数列表。
    • 然后,用当前已得到的质数 primes[j] 去标记合数 i * primes[j]
    • 关键点:当 i % primes[j] == 0 时,立即停止标记。因为此时 primes[j] 是 i 的最小质因子,那么对于后续更大的质数 primes[k] (k > j),合数 i * primes[k] 的最小质因子应该是 primes[j] 而不是 primes[k],所以应该留到后面当 i' = (i / primes[j]) * primes[k] 时再由 primes[j] 来筛,这样才能保证每个合数只被最小质因子筛一次。

3.2 代码实现(C++)

#include <iostream>
#include <vector>
using namespace std;

vector<int> eulerSieve(int n) {
    vector<bool> isPrime(n + 1, true);
    vector<int> primes;
    isPrime[0] = isPrime[1] = false;
    
    for (int i = 2; i <= n; i++) {
        if (isPrime[i]) {
            primes.push_back(i); // i 是质数,加入列表
        }
        // 用当前已知的质数去标记合数
        for (int j = 0; j < primes.size() && i * primes[j] <= n; j++) {
            isPrime[i * primes[j]] = false; // 标记合数
            // 关键:保证每个合数只被最小质因子筛掉
            if (i % primes[j] == 0) {
                break;
            }
        }
    }
    return primes;
}

int main() {
    int n = 100;
    vector<int> primes = eulerSieve(n);
    
    cout << "质数(2-" << n << "):" << endl;
    for (int prime : primes) {
        cout << prime << " ";
    }
    cout << endl;
    return 0;
}

3.3 算法正确性证明(感性理解)

为什么 if (i % primes[j] == 0) break; 能保证每个合数只被筛一次?

设合数 N = p × M,其中 p 是 N 的最小质因子,M 是另一个因子(可能为合数)。在欧拉筛中,N 会在 i = M 且用质数 primes[j] = p 去标记时被筛掉。因为 p 是 N 的最小质因子,所以 p 一定也是 M 的质因子(或等于 M)。当 i = M 时,我们遍历质数列表,当遇到 primes[j] = p 时,会标记 N = i * p,然后因为 i % p == 0,循环立即 break。这样,后续更大的质数就不会再去标记 N 了,因为那些质数不是 N 的最小质因子。

3.4 复杂度分析

  • 时间复杂度:O(n)。每个合数只被标记一次,每个数 i 的内层循环次数等于 i 的质因子个数(通常很少)。
  • 空间复杂度:O(n),需要布尔数组和质数列表。

4. 两种筛法对比与选择

特性 埃氏筛 欧拉筛(线性筛)
时间复杂度 O(n log log n) O(n)
核心思想 质数的倍数全是合数 每个合数只被最小质因子筛掉
代码复杂度 简单直观 稍复杂,需理解 break 条件
适用场景 n ≤ 10⁷,对性能要求不极致 n 非常大(10⁸ 以上),需要极致性能
额外功能 可稍作修改得到每个数的最小质因子 天然可记录每个数的最小质因子,可用于后续数论计算

5. 实战应用与变种

5.1 筛法求每个数的最小质因子

欧拉筛稍作修改即可在 O(n) 时间内求出 2 到 n 每个数的最小质因子,这在许多数论问题中非常有用。

vector<int> minPrimeFactor(int n) {
    vector<int> minPrime(n + 1, 0);
    vector<int> primes;
    
    for (int i = 2; i <= n; i++) {
        if (minPrime[i] == 0) { // i 是质数
            minPrime[i] = i;
            primes.push_back(i);
        }
        for (int p : primes) {
            if (p > minPrime[i] || i * p > n) break;
            minPrime[i * p] = p; // p 是 i*p 的最小质因子
        }
    }
    return minPrime;
}

5.2 区间筛法

当需要筛选的区间 [a, b] 很大(例如 b-a ≤ 10⁶,但 b 可能高达 10¹²)时,可以使用区间筛法:先用埃氏筛筛出 √b 以内的所有质数,再用这些质数去筛区间 [a, b]。

6. 总结

通过本文的“流食般投喂”,你应该已经掌握了:

  1. 埃氏筛:原理简单,代码好写,时间复杂度 O(n log log n) 对于大多数题目足够。
  2. 欧拉筛(线性筛):理解稍难,但能做到真正的 O(n),且能顺带求出最小质因子,适合高性能场景。

学习建议:先彻底理解并手敲埃氏筛,确保能独立写出。然后反复琢磨欧拉筛中 if (i % primes[j] == 0) break; 这一行的含义,可以尝试用几个小例子(如 n=20)在纸上模拟执行过程,观察每个合数是如何被筛掉的。

质数筛法是数论的基础工具,也是许多高级算法(如莫比乌斯反演、杜教筛)的前置知识。掌握它们,将为你的算法之路打下坚实基础。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐