【c++】素数详解:概念、定义及高效实现(判断方法 + 筛法)

观前提示:本文由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. 算法原理

埃氏筛的核心思想是 "标记非素数":

  1. 初始化一个布尔数组 is_prime,长度为 N+1,初始值全部设为 true,表示初始时假设所有数都是素数。
  2. 将数组的第 0 位和第 1 位设为 false(0 和 1 不是素数)。
  3. 从 2 开始遍历到 sqrt(N):
    • 若当前数 i 是素数(is_prime[i] == true),则将 i 的所有倍数(2i,3i,...,≤N)标记为 false(这些倍数都是合数)。
  4. 遍历结束后,数组中值为 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. 算法原理

欧拉筛(也叫线性筛)是对埃氏筛的进一步优化,其核心思想是 "每个合数仅被它的最小质因数标记一次",从而实现严格的线性时间复杂度。

算法步骤:

  1. 初始化两个数组:is_prime(标记是否为素数,初始全为 true)和 primes(存储已筛选出的素数)。
  2. is_prime[0]is_prime[1] 设为 false
  3. 遍历从 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 和自身整除;1 不是素数,2 是唯一偶素数。
  2. 素数判断
    • 基础方法:O(n),效率低,仅适用于小数判断;
    • 优化方法:遍历至 n + 排除偶数,接近 O(logn),适用于大数判断。
  3. 素数筛法
    • 埃氏筛:O(NloglogN),实现简单,满足大多数场景需求;
    • 欧拉筛:O(N),严格线性时间,通过 "最小质因数唯一标记" 优化,适用于大范围素数筛选。
  4. 应用场景:素数判断适用于单个数字的素性验证(如密码学中的大素数检测),素数筛法适用于批量获取指定范围内的素数(如算法竞赛中的数论问题)。
相关推荐
x976662 小时前
使用 HMAC-SHA256算法对MCU UID进行加密
单片机·嵌入式硬件·算法
尘心cx2 小时前
前端-APIs-day3
开发语言·前端·javascript
Dargon2882 小时前
MATLAB的Simulink的While子系统(动作子系统)
开发语言·matlab·simulink·mbd软件开发
Dargon2882 小时前
MATLAB的Simulink的可变子系统(选择子系统)
开发语言·matlab
崇山峻岭之间2 小时前
Matlab学习记录08
开发语言·学习·matlab
Swift社区2 小时前
LeetCode 452 - 用最少数量的箭引爆气球
算法·leetcode·职场和发展
lzh200409192 小时前
Set 和 Map 深入详解及其区别
数据结构·c++
吴佳浩 Alben2 小时前
Python入门指南(五) - 为什么选择 FastAPI?
开发语言·python·fastapi
速易达网络2 小时前
Java Web + Vue 前后端分离跨域解决方案
java·开发语言