【算法】素数筛

news/2024/11/19 22:49:59/

目录

  • 1.概述
  • 2.代码实现
    • 2.1.试除法
    • 2.2.埃拉托斯特尼筛法
    • 2.3.欧拉筛法

1.概述

(1)在了解素数筛之前,我们先复习一下素数的定义:素数 (Prime number) 又称质数,一个大于 1 的自然数,除了 1 和它自身外,不能被其他自然数整除的数叫做质数;否则称为合数 (Composite number),并且规定 1 既不是质数也不是合数

(2)素数筛指将一堆数中的合数筛出掉,留下素数的一个过程。求某个大小范围内(例如 2 ~ n)的素数个数,是用到素数筛的最基础的问题,下面将以该问题为切入点,来具体介绍几种常见素数筛算法:试除法、埃拉托斯特尼筛法、欧拉法。

2.代码实现

2.1.试除法

(1)试除法是根据素数的定义而设计的算法,正如字面意思一样,尝试相除。要判断 n 是否为素数,则判断 [2, sqrt(n)] 内的整数是否为 n 的因数,即判断 n % i 的值是否为 0,如果为 0,则说明 i 是 n 的因数,故 n 不是素数,返回 false。当判断结束后仍未找到 n 的因数,则说明 n 为素数,返回 true 即可。

(2)上面判断的范围之所以为 [2, sqrt(n)] 而非 [2, n - 1],其目的在于减少循环次数。原理如下:

  • 根据素数的定义反推,如果一个数不是素数,那么它一定等于另外两个数(不包括 1 和其本身)的乘积。
  • 例如已知 num = sqrt(num) * sqrt(num),假设 num = i * j,那么 i 和 j 中一定有一个 <= sqrt(num),另一个 >= sqrt(num),因此只需判断较小的那个除数是否存在即可判断 n 是否为素数。

(3)具体的代码实现如下:

class Solution {/*** @param1: 要判断的数* @return: 如果 num 为 素数,返回 true,否则返回 false* @description: 判断 num 是否为素数*/public boolean isPrime(int num) {if (num < 2) {System.out.println("请输入大于 1 的自然数!");return false;}if (num == 2) {return true;}//判断 [2, sqrt(n)] 内的整数是否是 num 的因数for (int i = 2; i <= Math.sqrt(num); i++) {// i 是 num 的因数,故 num 不是素数if (num % i == 0)return false;}return true;}/*** @param1: 大于 1 的自然数 n* @return: 2 ~ n 之间的素数个数* @description: 获取 2 ~ n 之间的素数个数*/public int trialDivision(int n) {int cnt = 0;for (int i = 2; i <= n; i++) {if (isPrime(i)) {cnt++;}}return cnt;}
}

2.2.埃拉托斯特尼筛法

(1)埃拉托斯特尼筛法 (Sieve of Eratosthenes) ,简称埃氏筛,其思想是把 [2, n] 内小于等于 sqrt(n) 的所有素数的倍数(即合数)剔除,那么剩下的数就是 [2, n] 中的所有素数

(2)例如要找出 [2, n] 内的所有素数。首先用质数 2 去筛选,即把 2 留下,把 2 的倍数剔除掉;再用下一个质数,也就是 3 来筛选,把 3 留下,把 3 的倍数剔除掉;接下去用下一个质数 5 筛选,把 5 留下,把 5 的倍数剔除掉;不断重复下去…,直到筛选到最后一个小于等于 sqrt(n) 的素数为止,此时剩下的数即为 [2, n] 中的所有素数。

(3)具体的代码实现如下:

class Solution {/*** @param1: 大于 1 的自然数 n* @return: 2 ~ n 之间的素数个数* @description: 获取 2 ~ n 之间的素数个数*/public static int eratosthenes(int n) {// checked[i] = true: 表示数字 i 为合数(2 <= i <= n)boolean[] checked = new boolean[n + 1];//存储筛得到的素数List<Integer> primes = new ArrayList<>();for (int i = 2; i <= n; i++) {if (!checked[i]) {//将素数 i 加入到 primes 中primes.add(i);// j 表示倍数,从 2 倍开始剔除for (int j = 2; i * j <= n; j++) {//将素数 i 的倍数标记为合数checked[i * j] = true;}}}return primes.size();}
}

(4)埃氏筛法的效率不够高,其原因在于一个合数可能会被重复剔除。例如合数 12,在用质数 2 去筛选时,会被 2 * 6 = 12 剔除,也可能在用质数 3 去筛选时,被 3 * 4 = 12 剔除,这样一来,合数 12 被剔除了多次。一个合数越大,其质素因子越多,则重复被剔除的次数也就越多

2.3.欧拉筛法

(1)欧拉筛法与埃氏筛类似,不过欧拉筛做了一点改进:让每个合数只被它的最小质因子筛选一次,这样可以达到不重复筛选的目的

(2)首先来看具体的代码实现:

class Solution1 {/*** @param1: 大于 1 的自然数 n* @return: 2 ~ n 之间的素数个数* @description: 获取 2 ~ n 之间的素数个数*/public static int euler(int n) {// checked[i] = true: 表示数字 i 为合数(2 <= i <= n)boolean[] checked = new boolean[n + 1];//存储筛得到的素数List<Integer> primes = new ArrayList<>();for (int i = 2; i <= n; i++) {if (!checked[i]) {//将素数 i 加入到 primes 中primes.add(i);}for (int j = 0; j < primes.size(); j++) {int curPrime = primes.get(j);if (i * curPrime > n) {break;}checked[i * curPrime] = true;if (i % curPrime == 0) {//保证每个合数只被其最小的质因子剔除,从而避免了重复剔除break;}}}return primes.size();}
}

(3)欧拉筛法的代码需要仔细分析,下面先来分析关键代码:

// curPrime = primes.get(j)
if (i % curPrime == 0) {break;
}

上面的代码保证了每个合数只被其最小的质因子剔除,从而避免了重复剔除,具体分析如下:

  • i% primes.get(j) == 0 说明 primes.get(j) 是 i 的最小的素因子(primes 升序存放素数),所以 i 可以表示为如下形式:
// primes.get(j) 是 i 的最小质因子,故 x >= i
i = x * primes.get(j);
  • 从而可以推导出如下形式:
i * primes.get(j + 1) = x * primes.get(j) * primes.get(j + 1) = primes.get(j) * x * primes.get(j + 1);
  • 所以 i * primes.get(j + 1) 是 primes.get(j) 的倍数,当 i 往后递增到 y = x * primes.get(j + 1) 时,合数 i * primes.get(j + 1) = y * primes.get(j) 就会被剔除,这就相当于延迟剔除 i * primes.get(j + 1)
  • i * primes.get(j + 1) 是这样被剔除的,往后的 i * primes.get(j + 2) 也同理。

(4)举例说明如下:
① 例如 i = 12,此时 primes 中的素数有 2、3、5、7、11;
② 当进入到内层循环时,从 primes 中取出的第一个素数 curPrime = 2,此时 12 * 3 = 24 被剔除了(2 是 24 的最小质因子),但由于 12 % 2 == 0,因此退出了内层循环。
③ 那么为什么 12 % 2 == 0 时就得退出循环呢?而不是继续剔除 3 * 12 = 36 呢?

  • 首先,按照欧拉筛法的思想可知每个合数只被其最小的质因子剔除,显然合数 36 的最小质因子是 2,并非 3;
  • 其次,正如上面所推导的那样,36 可以通过 12 的最小素因子 2 与其它数相乘得到,即 36 = 2 * 18,因此 36 只是被延迟剔除了;
  • 最后,这样的延迟操作保证了每一个合数,只被其最小的质因子剔除一次,不会被重复剔除。

(4)欧拉筛法的正确性证明(即如何保证每个范围内的每个合数都会被剔除)

  • 假设我们要剔除合数 a,且 a 的最小质因数为 p1,令 a = p1b,那么显然 b < a,b 会先被外层 for 循环遍历到
  • 设 b 的最小质因数为 p2,当外层循环遍历到 b 时,此时要剔除它的倍数,由于 p1 是 a 的最小质因数,所以 p2 ≥ p1,即 p1 先于 p2 存放到 primes 中,这样就保证 b 在剔除 a 前不会 break 语句处跳出循环,即合数 a 必然会被剔除
  • 即使 p2 = p1,那么也会先剔除 a 后再退出循环(即先后执行下面的代码),这样就保证了每个合数都会被剔除
//剔除合数 a = b * p1 = b * p2
checked[i * curPrime] = true;
// p2 是 b 的最小质因数,即 b % p2 = i % curPrime = 0,b 已经被 p2 剔除过一次
if (i % curPrime == 0) {//保证每个合数只被其最小的素因子剔除,从而避免了重复剔除break;
}

(5)时空复杂度分析:

  • 时间复杂度:由于欧拉筛法保证了每个合数只被剔除一次,故即使代码看似有两层循环,但是其时间复杂度仍为 O(n);
  • 空间复杂度:比较明显,可直接看出来,为O(n);

http://www.ppmy.cn/news/18814.html

相关文章

20230122英语学习

If Flying Is Giving You More Anxiety Than Ever, Here’s How to Cope 害怕坐飞机&#xff1f;教你几招&#xff0c;克服飞行焦虑 In college, I grew a tumor that meant I hung out quite frequently in MRI machines.Though I’d never had a problem before, I found my…

【React】组件的创建与事件绑定

&#x1f4d8;前言 &#x1f6a9;&#x1f6a9;&#x1f6a9; &#x1f48e;个人主页: 阿选不出来 &#x1f4a8;&#x1f4a8;&#x1f4a8; &#x1f48e;个人简介: 一名大二在校生,学习方向前端,不定时更新自己学习道路上的一些笔记. &#x1f4a8;&#x1f4a8;&#x1f4a…

LeetCode_单周赛_329

2544. 交替数字和 代码1 转成字符串&#xff0c;逐个判断 class Solution {public int alternateDigitSum(int n) {char[] s ("" n).toCharArray();int t 1;int ans 0;for (int i 0; i < s.length; i) {ans (s[i] - 0) * t;t -t;}return ans;} }代码2 一…

react用高阶组件优化文件结构 帮助建立高阶组件应用思路

其实高阶组件是一个将组件写的更灵活的方式&#xff0c;他的应用场景在业务开发中会非常多样 这里 我们演示一种 主要还是解决问题的思想最重要 或者是 这个不叫解决问题 而是设计组件结构的思路 我们来模拟一个场景 在src下有一个 components 文件夹目录 在 components 下有…

C++编译之(1)-g++单/多文件/库的编译及C标准的发展历程

g编译入门 本文为您介绍g的编译用法&#xff1b;通过从最简单的单文件编译&#xff0c;到多文件编译&#xff0c;再到动态库、静态库的编译及使用&#xff1b; 例子都经过实际编译并运行&#xff0c;可谓全网最良心之作呐&#xff0c;放心拷贝粘贴学习&#xff01; 1、g的编译单…

【3-神经网络八股】北京大学TensorFlow2.0

课程地址&#xff1a;【北京大学】Tensorflow2.0_哔哩哔哩_bilibiliPython3.7和TensorFlow2.1六讲&#xff1a;神经网络计算&#xff1a;神经网络的计算过程&#xff0c;搭建第一个神经网络模型神经网络优化&#xff1a;神经网络的优化方法&#xff0c;掌握学习率、激活函数、损…

【C++】从0到1入门C++编程学习笔记 - 核心编程篇:类和对象(上)

文章目录一、封装1.1 封装的意义1.2 struct和class区别1.3 成员属性设置为私有二、对象的初始化和清理2.1 构造函数和析构函数2.2 构造函数的分类及调用2.3 拷贝构造函数调用时机2.4 构造函数调用规则2.5 深拷贝与浅拷贝2.6 初始化列表2.7 类对象作为类成员2.8 静态成员三、C对…

深入理解Mysql底层数据结构

一. 索引的本质 索引是帮助MySQL高效获取数据的排好序的数据结构。 二. 索引的数据结构 二叉树红黑树Hash表BTreeBTree mysql的索引采用的是B树的结构 mysql为什么不用二叉树&#xff0c;因为对于单边增长的数据列&#xff0c;二叉树和全表扫描差不多&#xff0c;效率没有什…