当前位置: 首页 > news >正文

数论之普通判别法、埃氏筛与线性筛的应用及其对比

前言

      质数作为数论的核心元素,其高效筛选一直是算法领域的重要课题。从古希腊数学家埃拉托斯特尼提出的朴素筛法,到现代优化的线性筛法,质数筛选技术的演进见证了算法设计的智慧。
 
      埃氏筛凭借直观的标记思想成为入门经典,却因重复筛选存在效率瓶颈;线性筛则通过“最小质因子”优化,实现了O(n)时间复杂度的突破。本文将聚焦这几种经典算法,解析其核心原理与实现细节,对比不同场景下的性能差异,带读者掌握从基础到优化的质数筛选技术。

核心思想介绍

普通判别法:

✅对于给定的大于1的整数n,若在2到n-1的范围内不存在能整除n的整数,则n为素数,如2、5、41等;反之则为合数,如4、9、24等。由于因数的对称性,我们在实际应用中,只需验证2到√n(n的平方根)之间的整数是否能整除n,对于非平方数n,如果它存在一个大于√n的因数a,那么必然存在一个对应的小于√n的因数b,使得a * b = n(例如:12,大于2√3的因数有4、6、12,小于2√3的因数有3、2、1),当然,对于平方数n(n = k²,k为整数),必然存在对称因数a = b = √n,使得a * b = n(例如:4,1和4为一对对称因子,还有一对对称因子为2和2)。总而言之,遍历到√n即可覆盖所有可能的因数组合,避免冗余计算,是数学中优化因数相关问题的经典方法。

素数(又称质数)是指在大于1的自然数中,除了1和它自身以外,没有其他正因数的自然数。
需要注意的是,1既不是素数也不是合数

所以我们可以设计如下算法(此处可当模板):

private boolean isPrime(int n) { // 判断参数n是否为素数if (n == 1) return false; // 1既不是素数,也不是合数if (n == 2) return true; // 2是唯一是素数的偶数if (n % 2 == 0) return false; // 大于2的偶数均为合数,起码有一个因子2for (int i = 3; i * i <= n; i += 2) { // 不需要对1、2进行试除,只需要遍历不大于√n的奇数if (n % i == 0) return false; // 存在因子,不为素数,直接返回false}return true; // 经过层层检验,没有除了1和n的其他因子,符合素数定义,返回true
}

步骤:

1.排除特殊值1;

2.处理唯一的偶素数2;

3.排除所有大于2的偶数;

4.遍历奇数试除,检验是否存在其他因子,减少了一半计算量;

5.确定素数并返回结果。

埃氏筛:

✅标记所有已知质数的倍数,未被标记的数即为质数,本质为质数的倍数一定不是质数,通过逐步排除非质数,最终保留下来的是指定范围内所有的质数,例如:

 1 2 3 4 5 6 7
 ✅ ✅ ✅ ✅ ✅ ✅ ✅
 8 9 10 11 12 13 14
 ✅ ✅ ✅ ✅ ✅ ✅ ✅
 15 16 17 18 19 20 21
 ✅ ✅ ✅ ✅ ✅ ✅ ✅
 1 2 3 4 5 6 7
 ✅ 质数 ✅ ❌ ✅ ❌ ✅
 8 9 10 11 12 13 14
 ❌ ✅ ❌ ✅ ❌ ✅ ❌
 15 16 17 18 19 20 21
 ✅ ❌ ✅ ❌ ✅ ❌ ✅
 1 2 3 4 5 6 7
 ✅ 质数 质数 ❌ ✅ ❌ ✅
 8 9 10 11 12 13 14
 ❌ ❌ ❌ ✅ ❌ ✅ ❌
 15 16 17 18 19 20 21
 ❌ ❌ ✅ ❌ ✅ ❌ ❌
 1 2 3 4 5 6 7
 ✅ 质数 质数 ❌ 质数 ❌ ✅
 8 9 10 11 12 13 14
 ❌ ❌ ❌ ✅ ❌ ✅ ❌
 15 16 17 18 19 20 21
 ❌ ❌ ✅ ❌ ✅ ❌ ❌

     (✅表示可能为质数,❌表示非质数)

1.初始化标记数组;

2.以质数2为起点开始筛选;

3.筛选下一个质数3;

4.筛选下一个质数5;

5.终止筛选并提取结果。

所以我们可以设计如下算法:

​private static boolean[] eSieve(int n) {// 初始化:默认所有数为素数(后续修正非素数)boolean[] isPrime = new boolean[n + 1];if (n >= 2) { // 0和1初始为false,2及以上先设为trueArrays.fill(isPrime, 2, n + 1, true);}// 从最小素数2开始筛选for (int i = 2; i * i <= n; i++) { // 因子对称性,节约时间if (isPrime[i]) { // 若i是素数,则标记其所有倍数为非素数// 从i*i开始标记(小于i*i的倍数已被更小的素数标记过)for (int j = i * i; j <= n; j += i) {isPrime[j] = false;}}}return isPrime;
}

为什么内层遍历可以从i * i开始?

例如:当i = 5时, 5 × 2 = 10已被i = 2标记,5 × 3 = 15已被i = 3标记, 5 × 4 = 20已被i = 2标记,这些倍数无需再次标记。而i * i = 25及之后的倍数( 25 、 30 、 35 …)才是首次需要被i = 5标记的,因为之前没有比i小的质数能覆盖这些倍数。故从i * i开始标记,能跳过已被处理的倍数,减少内层循环的执行次数,显著提升效率。

线性筛(欧拉筛):

✅每个合数仅被其最小质因数筛选,避免重复筛选,可以理解为对埃氏筛的一种优化,例如:合数12的最小质因数是2,因此仅在遍历到质因数 2 时,通过2 × 6将12筛除,而不会在遍历到3时通过3 × 4重复筛除。具体操作为:通过维护一个已找到的素数列表,对每个数i乘以列表中的素数,标记乘积为合数,且仅在遇到i的最小质因数时停止,确保每个合数只被标记一次。

所以我们可以设计如下算法:

​private List<Integer> buildPrime(int n) {// 素数标记数组:prime[i]为true表示i是素数boolean[] prime = new boolean[n + 1];// 初始化所有数为素数(后续会修正非素数)Arrays.fill(prime, true);// 0和1不是素数,手动标记为falseprime[0] = prime[1] = false;// 存储筛选出的素数列表List<Integer> list = new ArrayList<>();// 遍历2到n的每个数i,用于筛选和记录素数for (int i = 2; i <= n; i++) {// 若i未被标记为非素数,则i是素数,加入列表if (prime[i]) list.add(i);// 用已找到的素数筛选合数(线性筛核心逻辑)for (int j = 0; j < list.size(); j++) {// 获取当前素数列表中的第j个素数vint v = list.get(j);// 若v * i超过n,超出筛选范围,停止当前循环if (v * i > n) break;// 标记v * i为非素数(v是其最小质因数)prime[v * i] = false;// 若v是i的质因数,停止遍历素数(避免重复筛选)// 保证后续合数仅被其最小质因数标记if (i % v == 0) break;}}    // 返回筛选出的所有素数列表return list;
}

算法题示例:

例题一:2761. 和等于目标值的质数对 - 力扣(LeetCode)

给你一个整数 n 。如果两个整数 x 和 y 满足下述条件,则认为二者形成一个质数对:

  • 1 <= x <= y <= n

  • x + y == n

  • x 和 y 都是质数

请你以二维有序列表的形式返回符合题目要求的所有 [xi, yi] ,列表需要按 xi 的 非递减顺序 排序。如果不存在符合要求的质数对,则返回一个空数组。

注意:质数是大于 1 的自然数,并且只有两个因子,即它本身和 1 。

示例 1:

输入:n = 10
输出:[[3,7],[5,5]]
解释:在这个例子中,存在满足条件的两个质数对。
这两个质数对分别是 [3,7] 和 [5,5],按照题面描述中的方式排序后返回。

示例 2:

输入:n = 2
输出:[]
解释:可以证明不存在和为 2 的质数对,所以返回一个空数组。 

提示:

  • 1 <= n <= 10 ^ 6

class Solution {

    public List<List<Integer>> findPrimePairs(int n) {

       

    }

}

class Solution {public List<List<Integer>> findPrimePairs(int n) {List<List<Integer>> list = new ArrayList<>();// 边界条件:n ≤ 3时无有效素数对(最小素数和为 2+2=4)if (n <= 3) return list;// 遍历i从2到n/2,确保i ≤ n-i(避免重复对)for (int i = 2; i <= n / 2; i++) {// 检查i和n-i是否均为素数if (isPrime(i) && isPrime(n - i)) {// 若均为素数,添加素数对(i, n-i)到结果列表list.add(new ArrayList<>(Arrays.asList(i, n - i)));}}return list;}/*** 判断一个数是否为素数* @param n待判断的整数* @return 若为素数返回true,否则返回false*/private boolean isPrime(int n) {if (n == 1) return false; // 1不是素数if (n == 2) return true;  // 2是素数(唯一偶素数)if (n % 2 == 0) return false; // 其他偶数均不是素数// 检查奇数因数:从3到sqrt(n),步长为2(只查奇数)for (int i = 3; i * i <= n; i += 2) {if (n % i == 0) return false; // 存在因数则不是素数}return true; // 无因数,是素数}
}

例题二:3618. 根据质数下标分割数组 - 力扣(LeetCode)

给你一个整数数组 nums

根据以下规则将 nums 分割成两个数组 A 和 B

  • nums 中位于 质数 下标的元素必须放入数组 A

  • 所有其他元素必须放入数组 B

返回两个数组和的 绝对 差值:|sum(A) - sum(B)|

质数 是一个大于 1 的自然数,它只有两个因子,1和它本身。

注意:空数组的和为 0。

示例 1:

输入: nums = [2,3,4]

输出: 1

解释:

  • 数组中唯一的质数下标是 2,所以 nums[2] = 4 被放入数组 A

  • 其余元素 nums[0] = 2 和 nums[1] = 3 被放入数组 B

  • sum(A) = 4sum(B) = 2 + 3 = 5

  • 绝对差值是 |4 - 5| = 1

示例 2:

输入: nums = [-1,5,7,0]

输出: 3

解释:

  • 数组中的质数下标是 2 和 3,所以 nums[2] = 7 和 nums[3] = 0 被放入数组 A

  • 其余元素 nums[0] = -1 和 nums[1] = 5 被放入数组 B

  • sum(A) = 7 + 0 = 7sum(B) = -1 + 5 = 4

  • 绝对差值是 |7 - 4| = 3

提示:

  • 1 <= nums.length <= 10 ^ 5

  • -10 ^ 9 <= nums[i] <= 10 ^ 9

class Solution {

    public long splitArray(int[] nums) {

       

    }

}

class Solution {/*** 计算数组按特定规则拆分后的绝对值结果* 规则:对数组索引进行素数判断,素数索引元素加正值,非素数索引元素加负值,最后返回总和的绝对值* @param nums 输入的整数数组* @return 计算后的绝对值结果*/public long splitArray(int[] nums) {int n = nums.length; // 获取数组长度// 边界条件:若数组只有一个元素,直接返回该元素的绝对值if (n == 1) return Math.abs(nums[0]);// 初始化素数标记数组:isPrime[i]表示索引i是否为素数boolean[] isPrime = new boolean[n];Arrays.fill(isPrime, true); // 先默认所有索引为素数isPrime[0] = isPrime[1] = false; // 0和1不是素数// 埃氏筛法筛选素数(针对数组索引)// 从2开始遍历,若当前索引i是素数,则标记其倍数索引为非素数for (int i = 2; i * i < n; i++) {if (isPrime[i]) { // 若i是素数// 从i*i开始,以i为步长标记所有倍数索引为非素数for (int j = i * i; j < n; j += i) {isPrime[j] = false;}}}long ans = 0; // 存储计算总和的变量// 遍历数组,根据索引是否为素数累加对应值for (int i = 0; i < n; i++) {// 若索引 i 是素数,加 nums[i];否则加-nums[i]ans += isPrime[i] ? (long) nums[i] : (long) -nums[i];}// 返回总和的绝对值return Math.abs(ans);}
}

例题三:2523. 范围内最接近的两个质数 - 力扣(LeetCode)

给你两个正整数 left 和 right ,请你找到两个整数 num1 和 num2 ,它们满足:

  • left <= nums1 < nums2 <= right  。

  • nums1 和 nums2 都是 质数 。

  • nums2 - nums1 是满足上述条件的质数对中的 最小值 。

请你返回正整数数组 ans = [nums1, nums2] 。如果有多个整数对满足上述条件,请你返回 nums1 最小的质数对。如果不存在符合题意的质数对,请你返回 [-1, -1] 。

示例 1:

输入:left = 10, right = 19
输出:[11,13]
解释:10 到 19 之间的质数为 11 ,13 ,17 和 19 。
质数对的最小差值是 2 ,[11,13] 和 [17,19] 都可以得到最小差值。
由于 11 比 17 小,我们返回第一个质数对。

示例 2:

输入:left = 4, right = 6
输出:[-1,-1]
解释:给定范围内只有一个质数,所以题目条件无法被满足。

提示:

  • 1 <= left <= right <= 10 ^ 6

class Solution {

    public int[] closestPrimes(int left, int right) {

       

    }

}

class Solution {// 构建小于等于n的所有质数列表(使用埃氏筛法优化版)private List<Integer> buildPrime(int n) {boolean[] prime = new boolean[n + 1];Arrays.fill(prime, true);// 0和1不是质数prime[0] = prime[1] = false;List<Integer> list = new ArrayList<>();// 遍历每个数字,筛选质数并标记其倍数为非质数for (int i = 2; i <= n; i++) {// 若当前数字未被标记为非质数,则是质数if (prime[i]) list.add(i);// 用已找到的质数标记其与i的乘积为非质数for (int j = 0; j < list.size(); j++) {int v = list.get(j);// 乘积超过n时无需继续标记if (v * i > n) break;// 标记v*i为非质数prime[v * i] = false;// 若i是v的倍数,停止当前循环(避免重复标记)if (i % v == 0) break;}}return list;}// 寻找[left, right]区间内差值最小的相邻质数对public int[] closestPrimes(int left, int right) {// 生成不大于right的所有质数List<Integer> prime = buildPrime(right);int i = 0;// 找到第一个大于等于left的质数位置while (i < prime.size()) {if (prime.get(i) >= left) break;i++;}// 初始化结果为[-1,-1](无符合条件时返回)int[] ans = new int[]{-1, -1};// 若剩余质数不足2个,直接返回默认结果if (i + 1 >= prime.size()) return ans;// 记录最小差值(初始值设为较大数避免溢出)int temp = Integer.MAX_VALUE / 2;// 遍历符合条件的质数,寻找最小差值的相邻对for (int j = i + 1; j < prime.size(); j++) {if (prime.get(j) - prime.get(j - 1) < temp) {ans[0] = prime.get(j - 1);ans[1] = prime.get(j);temp = prime.get(j) - prime.get(j - 1);}}return ans;}
}

方法对比:

   \ 时间复杂度 空间复杂度 适用场景
普通判别法O(√n)(单次)/ O(n√n)(批量) O(1)内存占用低,适合单个数字的素数判断或数据量较小的场景
 埃氏筛O(n log log n)O(n)实现简单,适合中等规模(n <= 10 ^ 7)的素数批量筛选
 线性筛O(n)O(n)无重复标记,时间最优,适合大规模(n > 10 ^ 7)的素数批量筛选

结语:

      在数学和计算机科学中,质数(素数)的筛选是一个基础且重要的问题。从判断单个数字是否为质数,到批量筛选一定范围内的所有质数,算法的效率往往决定了其实际应用价值。对于该部分内容,我们还得加强练习,这里再推荐一个相关的好题:P12157 [蓝桥杯 2025 省 Java B] 魔法科考试 - 洛谷。

http://www.dtcms.com/a/334803.html

相关文章:

  • PowerShell 第11章:过滤和比较(下)
  • 深度剖析Redisson分布式锁项目实战
  • redis存储原理与对象模型
  • 《A Practical Guide to Building Agents》文档学习
  • 数学建模:智能优化算法
  • PostgreSQL——事务处理与并发控制
  • CVE-2021-4300漏洞复现
  • 海康机器人3D相机的应用
  • ZKmall开源商城的数据校验之道:用规范守护业务基石
  • Vue 3与React内置组件全对比
  • 【lucene】SegmentInfos
  • 《Leetcode》-面试题-hot100-技巧
  • 科研工具的一些注意事项
  • 【minio】一、Linux本地部署MinIO
  • stringstream + getline()实现字符串分割
  • Java 10 新特性及具体应用
  • 二分查找。。
  • 【大语言模型 02】多头注意力深度剖析:为什么需要多个头
  • Python 类元编程(元类的特殊方法 __prepare__)
  • nflsoi 8.16 题解
  • 【数据结构】-2- 泛型
  • Python - 100天从新手到大师:第十一天常用数据结构之字符串
  • Java实现汉诺塔问题
  • AI Agents 2025年十大战略科技趋势
  • 【嵌入式C语言】六
  • .net印刷线路板进销存PCB材料ERP财务软件库存贸易生产企业管理系统
  • mit6.824 2024spring Lab1 MapReduce
  • 衡石使用指南嵌入式场景实践之仪表盘嵌入
  • 3 统一建模语言(UML)(上)
  • 力扣 hot100 Day75