观前提示:本文由AI编写
在数论领域,素数(也叫质数)是最基础且至关重要的研究对象,它广泛应用于密码学、算法优化、数学建模等多个领域。对于 C++ 开发者而言,掌握素数的判断方法和高效筛法,是提升算法能力的必备技能。本文将从素数的核心概念出发,逐步讲解不同时间复杂度的素数判断方法,以及两种经典的素数筛法(埃氏筛、欧拉筛)。
一、素数的核心概念与严格定义
1. 概念理解
素数是指在大于 1 的自然数中,除了 1 和它自身之外,不再有其他正因数的数。简单来说,一个素数无法被任何介于 2 和它自身减 1 之间的自然数整除。与之相对的概念是合数,即大于 1 且不是素数的自然数(例如 4、6、8、9 等)。
2. 严格定义
设 n 是大于 1 的自然数,若对于任意整数 k 满足 2≤k≤n−1,都有 n%k=0,则称 n 为素数;否则,n 为合数。
3. 特殊说明
- 1 既不是素数,也不是合数(这是数论中的统一约定,因为 1 只有一个正因数,不满足素数的 "两个不同正因数" 要求)。
- 最小的素数是 2,同时 2 也是唯一的偶素数;其余素数均为奇数(例如 3、5、7、11 等)。
二、C++ 中素数的判断方法
判断一个给定的正整数 n 是否为素数,是最基础的素数相关问题。根据优化程度的不同,存在两种主流方法,时间复杂度分别为 O(n) 和 O(n)(进一步优化后可达到 O(logn) 级别)。
方法一:基础判断法(时间复杂度 O(n))
1. 算法思路
根据素数的定义,遍历从 2 到 n−1 的所有整数,依次判断这些整数是否能整除 n:
- 若存在某个整数能整除 n,则 n 是合数;
- 若遍历结束后,没有找到能整除 n 的整数,则 n 是素数。
2. C++ 代码实现
cpp
#include <iostream>
using namespace std;
// 判断n是否为素数(基础版 O(n))
bool isPrime_Basic(int n) {
// 处理边界情况:n<=1不是素数
if (n <= 1) {
return false;
}
// 遍历2到n-1,判断是否有因数
for (int i = 2; i < n; ++i) {
if (n % i == 0) {
return false; // 存在因数,不是素数
}
}
return true; // 无因数,是素数
}
int main() {
int num;
cout << "请输入一个正整数:";
cin >> num;
if (isPrime_Basic(num)) {
cout << num << " 是素数" << endl;
} else {
cout << num << " 不是素数" << endl;
}
return 0;
}
3. 缺点分析
该方法的时间复杂度为 O(n),当 n 较大时(例如 n=109),遍历次数过多,效率极低,无法满足实际需求。
方法二:优化判断法(时间复杂度 O(sqrt(n)))
1. 核心优化思路
我们可以通过下面的性质减少遍历次数:
- 性质 1:若 n 有一个大于 sqrt(n) 的因数 d,则必然存在一个对应的小于 n 的因数 n/d。因此,只需遍历到 n 即可,无需遍历到 n−1,此时时间复杂度降至O(sqrt(n))。
2. 分步优化实现
cpp
bool prime(int x){
// 边界条件:小于等于1的数既不是素数也不是合数,直接返回false
if(x<=1){
return false;
}
// 遍历从2开始
// 循环终止条件:i*i <= x(等价于i <= sqrt(x)),减少不必要的遍历
for(int i=2;i*i<=x;i++){
// 若x能被i整除,说明x存在除了1和自身之外的因数,不是素数,返回false
if(x%i==0){
return false;
}
}
// 遍历结束后未找到能整除x的因数,说明x是素数,返回true
return true;
}
三、素数筛法:批量筛选指定范围内的所有素数
当我们需要获取某个范围(例如 [2,N])内的所有素数时,逐个判断的方法效率较低,此时素数筛法是最优选择。主流的筛法有两种:埃拉托斯特尼筛法(埃氏筛)和欧拉筛(线性筛)。
1. 埃拉托斯特尼筛法(埃氏筛)
1. 算法原理
埃氏筛的核心思想是 "标记非素数":
- 初始化一个布尔数组
is_prime,长度为 N+1,初始值全部设为true,表示初始时假设所有数都是素数。 - 将数组的第 0 位和第 1 位设为
false(0 和 1 不是素数)。 - 从 2 开始遍历到 sqrt(N):
- 若当前数 i 是素数(
is_prime[i] == true),则将 i 的所有倍数(2i,3i,...,≤N)标记为false(这些倍数都是合数)。
- 若当前数 i 是素数(
- 遍历结束后,数组中值为
true的索引即为 [2,N] 范围内的所有素数。
2. 时间复杂度
埃氏筛的时间复杂度为 O(NloglogN),接近线性时间,在大多数场景下效率足够高。
3. C++ 代码实现
cpp
#include <iostream>
#include <vector>
using namespace std;
// 埃氏筛:筛选[2, n]范围内的所有素数
vector<int> Eratosthenes_Sieve(int n) {
vector<int> primes; // 存储筛选出的素数
if (n < 2) {
return primes;
}
vector<bool> is_prime(n + 1, true);
is_prime[0] = is_prime[1] = false; // 0和1不是素数
for (int i = 2; i * i <= n; ++i) { // 遍历到sqrt(n)即可
if (is_prime[i]) { // 若i是素数,标记其所有倍数为非素数
for (int j = i * i; j <= n; j += i) { // 从i*i开始标记,避免重复标记
is_prime[j] = false;
}
}
}
// 收集所有素数
for (int i = 2; i <= n; ++i) {
if (is_prime[i]) {
primes.push_back(i);
}
}
return primes;
}
// 主函数测试
int main() {
int N;
cout << "请输入筛选范围的最大值N:";
cin >> N;
vector<int> primes = Eratosthenes_Sieve(N);
cout << "[" << 2 << ", " << N << "]范围内的素数有:" << endl;
for (int p : primes) {
cout << p << " ";
}
cout << endl;
cout << "素数总数:" << primes.size() << endl;
return 0;
}
4. 注意事项
- 标记倍数时,从 i∗i 开始而非 2i,可以避免重复标记(例如 6 会被 2 和 3 标记,从 i∗i 开始可减少冗余操作)。
- 数组大小设为 n+1,是为了让索引与数字本身一一对应,方便后续收集素数。
2. 欧拉筛(线性筛)
1. 算法原理
欧拉筛(也叫线性筛)是对埃氏筛的进一步优化,其核心思想是 "每个合数仅被它的最小质因数标记一次",从而实现严格的线性时间复杂度。
算法步骤:
- 初始化两个数组:
is_prime(标记是否为素数,初始全为true)和primes(存储已筛选出的素数)。 - 将
is_prime[0]和is_prime[1]设为false。 - 遍历从 2 到 N 的每个数 i:
- 若
is_prime[i]为true,则将 i 加入primes数组(i 是素数)。 - 遍历
primes数组中的每个素数 p:- 若 i∗p>N,跳出循环(超出范围)。
- 标记
is_prime[i * p]为false(i∗p 是合数)。 - 若 i%p==0,跳出循环(保证每个合数仅被最小质因数标记)。
- 若
2. 时间复杂度
欧拉筛的时间复杂度为 O(N),是严格线性的,在筛选大范围素数时(例如 N=108),效率优于埃氏筛。
3. C++ 代码实现
cpp
#include <iostream>
#include <vector>
using namespace std;
// 欧拉筛(线性筛):筛选[2, n]范围内的所有素数
vector<int> Euler_Sieve(int n) {
vector<int> primes; // 存储筛选出的素数
if (n < 2) {
return primes;
}
vector<bool> is_prime(n + 1, true);
is_prime[0] = is_prime[1] = false;
for (int i = 2; i <= n; ++i) {
// 若i是素数,加入素数数组
if (is_prime[i]) {
primes.push_back(i);
}
// 遍历已有的素数,标记合数
for (int p : primes) {
if (1LL * i * p > n) { // 防止整数溢出,使用1LL强制转换为长整型
break;
}
is_prime[i * p] = false;
// 关键:i能被p整除时,跳出循环,保证最小质因数标记
if (i % p == 0) {
break;
}
}
}
return primes;
}
// 主函数测试
int main() {
int N;
cout << "请输入筛选范围的最大值N:";
cin >> N;
vector<int> primes = Euler_Sieve(N);
cout << "[" << 2 << ", " << N << "]范围内的素数有:" << endl;
for (int p : primes) {
cout << p << " ";
}
cout << endl;
cout << "素数总数:" << primes.size() << endl;
return 0;
}
4. 关键细节解析
- 为什么
i % p == 0时要跳出循环?假设 i=k∗p(p 是素数),那么 i∗p′=k∗p∗p′(p′ 是比 p 大的素数),此时 i∗p′ 的最小质因数是 p 而非 p′。如果继续循环,会导致 i∗p′ 被 p′ 标记,违反 "仅被最小质因数标记" 的原则,造成重复操作。 - 使用
1LL * i * p是为了防止 i 和 p 较大时,乘积溢出 int 类型的范围,导致程序出错。
四、总结
- 素数核心:大于 1 的自然数,仅能被 1 和自身整除;1 不是素数,2 是唯一偶素数。
- 素数判断 :
- 基础方法:O(n),效率低,仅适用于小数判断;
- 优化方法:遍历至 n + 排除偶数,接近 O(logn),适用于大数判断。
- 素数筛法 :
- 埃氏筛:O(NloglogN),实现简单,满足大多数场景需求;
- 欧拉筛:O(N),严格线性时间,通过 "最小质因数唯一标记" 优化,适用于大范围素数筛选。
- 应用场景:素数判断适用于单个数字的素性验证(如密码学中的大素数检测),素数筛法适用于批量获取指定范围内的素数(如算法竞赛中的数论问题)。