文章简介

本文记录寻找素数的几种算法, 本文中的O()没有指明默认表示时间复杂度
代码使用Java

目录

  • 文章简介
  • 正文
    • 穷举计数法(判断O(n))
      • 优化: 布尔+break(判断 小于O(n))
      • 优化: 利用数学推导缩小除数范围(判断O(n\sqrt{n}n​))
        • 引申
      • 引申-素数的分布
    • 埃氏筛选法(整体O(nloglogn))
      • 空间与时间复杂度的优化
    • 欧氏筛法(整体O(n), 线性筛)
    • 后记

正文

素数/质数, 一个只可以被1和它本身整除的数, 换句话说, 不能被比他小的任意两个整数的乘积表示的数字.

打印1~n之间素数, 一直是一个入门级别的算法, 在学习编程思想的时候会出现在各种题里,
文中n取100, 打印结果都为:

2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97

我们最开始写的可能是这样的

穷举计数法(判断O(n))

 /*** 打印1到n间的所有素数, 包括n.** @param n 查找到n*/static void printPrimeNumber(int n) {for (int i = 1; i < n + 1; i++) {/*这个循环遍历1~n的所有数字i*//*一个被整除次数的标志位, 如果为2, 只被1和它本身整除了, 是素数, 其它>2的值就证明不是素数*/int divided = 0;for (int j = i; j >= 1; j--) {/*这个循环遍历1~i的所有数字, 如果能被整除, "能被整除"计数器+1 */if (i % j == 0) {divided++;}}if (divided == 2) {/*如上所述, 该数是素数*/System.out.print(i + " ");}}}

n=1000时,一百次平均用时10.76ms

优化: 布尔+break(判断 小于O(n))

 /*** 打印1到n间的所有素数, 包括n.** @param n 查找到n*/static void printPrimeNumberPro(int n) {/*for (int i = 1; i < n+1; i++) {*//* 1不是素数, 可以直接跳过*/for (int i = 2; i < n + 1; i++) {/*int divided = 0;*//*本质上, 判定一个数字是不是素数, 只需要看能不能被n与1以外的数字整除就行了, 利用一个bool量, 再加上break, 不要傻傻计数*/boolean isPrime = true;/*for (int j = i - 1; j > 1; j--) {*//*直觉上, 从小向大而不是从小向大遍历除数, 可以更快判定, 后面给出这点的证明*/for (int j = 2; j < i; j++) {if (i % j == 0) {/*divided++*/isPrime = false;/*break可以减少比较次数*/break;}}if (isPrime) {System.out.print(i + " ");}}}

n=1000时,一百次平均用时6.23ms

优化: 利用数学推导缩小除数范围(判断O(n\sqrt{n}n​))

一个数字n可以被整除时, 意味着它可以被 除数和商的乘积表示, 设除数为j, 商为k
若:nj=k若: \frac{n}{j}=k\\ 若:jn​=k
此时
n=n∗n=j∗k又:j≠k\begin{aligned} n &= \sqrt{n}*\sqrt{n} \\ & = j*k\\\\ 又: j& \neq k \end{aligned} n又:j​=n​∗n​=j∗k​=k​
假设 j>kj>kj>k 必有
{j>nk<n\begin{aligned} \begin{cases} j > \sqrt{n}& \\ k < \sqrt{n}& \end{cases} \end{aligned} {j>n​k<n​​​​
那么,有了以上推导, 我们知道每次判断时除数只需要遍历到 ⌊n⌋\lfloor \sqrt{n} \rfloor⌊n​⌋ 就可以停止, 这样可以简化空间复杂度到 O(n\sqrt{n}n​)


引申

在 n>4n>4n>4时, 有:
n<n2\sqrt{n}<\frac{n}{2}n​<2n​
这印证了布尔优化一节中, 从前往后遍历会比从后往前遍历更容易找到除数的直觉.


给出这一节的代码

 /*** 打印1到n间的所有素数, 包括n.** @param n 查找到n*/static void printPrimeNumberPlus(int n) {for (int i = 2; i < n + 1; i++) {boolean isPrime = true;/*算一下开根, 让int自然floor */int sqrtI = (int) Math.sqrt(i);for (int j = 2; j <= sqrtI; j++) {if (i % j == 0) {isPrime = false;break;}}if (isPrime) {System.out.print(i + " ");}}}

n=1000时,一百次平均用时4.03ms


引申-素数的分布

根据素数定理, 素数在大数中分布更稀疏. 根据高斯发现的简介公式, 不大于n的整数中素数个数约为:
nln⁡n个\frac{n}{\ln{n}}个 lnnn​个


埃氏筛选法(整体O(nloglogn))

这种方法本质上还是遍历, 只不过对遍历范围进行了进一步的简化, 一边遍历一边减少后续需要的操作.
埃氏方法由古希腊数学家 埃拉托色尼 发明, 思路要点其实就是一句话

  • 素数的倍数都不是素数

这句话很简单, 但是用起来很神奇, 比如2就是一个最小的素数, 而它的倍数, 也就是>2的所有偶数都可以不用比较.

不过, 这种方法只是用来在范围内筛选素数, 而不是判定一个数字是不是素数.

简单的实现如:

 /*** 打印1到n间的所有素数, 包括n.** @param n 查找到n*/static void printPrimeNumberMax(int n) {/*准备好初值为0的数组, 数组下标对应数字, 数组内容对应是否为素数, 1不是, 0是*/int[] nums = new int[n + 1];/*先标记去掉0,1*/nums[0] = 1;nums[1] = 1;/*开始进行筛选, 不用判定, 只要没被标记直接加 */for (int i = 2; i < n + 1; i++) {if (nums[i] != 1) {/*为1时这个数字已经被标记为素数了, 直接跳过, 只看!=1的*/System.out.print(i + " ");/*方法的核心, 当找到任意素数时, 把所有该素数的倍数标记为非素数*/int index = i + i;while (index < n + 1) {nums[index] = 1;index += i;}}}}

n=1000时,一百次平均用时3.95ms

空间与时间复杂度的优化

上面例子中, 使用int数组的值0或1来标记当前下标对应的元素是否为素数, 不如直接简化为使用布尔量数组, 毕竟一个布尔型变量只占用1bit, 而int占用足足32bit.
一个很有意思的点在这个加号

假如此时i==7
这里的代码要开始依次标记范围内的2*7,3*7,4*7,5*7,6*7,7*7,8*7....等数字
你也许注意到了, 2*7已经作为2的倍数被2标记过一次
3*7作为3的倍数被标记过
4*7.
...
一直到7*7才开始是需要标记的值, 所以箭头位置的加号应该简化为乘号

 /*** 打印1到n间的所有素数, 包括n.** @param n 查找到n*/static void printPrimeNumberMaxSlim(int n) {/*数组下标对应数字, 数组内容对应是否为合数, false是初值表示是素数(不是合数), true表示是合数*/boolean[] nums = new boolean[n + 1];/*先标记去掉0,1*/nums[0] = true;nums[1] = true;for (int i = 2; i < nums.length; i++) {if (!nums[i]) {System.out.print(i + " ");/*减少标记: 从i*i开始*/int index = i * i;while (index < nums.length) {nums[index] = true;index += i;}}}}

欧氏筛法(整体O(n), 线性筛)

回到上面埃氏筛法的动态图, 注意观察12, 24,105等颜色, 你会注意到, 他们被标记了不止一次, 这是因为即使从i2i^2i2开始做筛选, 类似2∗6=122*6=122∗6=12和3∗4=123*4=123∗4=12这种可以由多组合数乘积得到到数字有被重复标记的可能. 而每次重复标记也是对性能的浪费.

这些浪费可以被欧氏筛法避免.
欧氏筛法的核心思想是只每个数字只由它的最小质因子标记

给出代码:

 /*** 打印1到n间的所有素数, 包括n.** @param n 查找到n*/static void printPrimeNumberMaxDulex(int n) {/*数组下标对应数字, 数组内容对应是否为合数, false是初值表示是素数(不是合数), true表示是合数*//*为了方便有序随机存取, 这里还是用数组存储, 根据上面的高斯分布表格, 这里的数组大小取1.2倍的高斯估计*//*新建数组, 比起上面的函数似乎新增了空间复杂度, 但实际上正常使用寻找素数的方法时, 往往需要这个数组来存储找到的素数, 只不过以往都是直接输出了*/int[] primeNumber = new int[(int) (1.2 * n / Math.log(n))];int indexForPrimeNumber = 0;boolean[] nums = new boolean[n + 1];/*现标记去掉0,1*/nums[0] = true;nums[1] = true;for (int i = 2; i < n + 1; i++) {if (!nums[i]) {/*输出非素数*/primeNumber[indexForPrimeNumber] = i;indexForPrimeNumber++;System.out.print(i + " ");}/*欧氏筛法: 标记任意数i的 已输出的素数倍 数字 , 直到i可以被其中一个素数整除(碰到了它的最小质因子)*/for (int prime : primeNumber) {if (i * prime > n) {/*超过范围, 停止标记*/break;}/*标记i的素数倍数字*/nums[i * prime] = true;if (i % prime == 0) {/*标记到它的最小质因子为止*/break;}}}}

具体的时间复杂度计算请参考站内@seh_sjij-【算法/数论】欧拉筛法详解:过程详述、正确性证明、复杂度证明

后记

本来只是想找一找和寻找素数有关的算法, 没想到越看越深, 筛法是一个很大的领域, 在欧氏筛法之上, 好像还有min_25筛法等其它筛法, 而埃氏筛法还有一种只标记前n\sqrt{n}n​的优化, 这篇博客花了过多时间, 等后续有空了再回过头来把埃氏的优化补全了. 以上

【算法学习】找素数的几种算法: 简单穷举, 埃氏筛法, 欧氏筛法, 从O(n2)到O(n)相关推荐

  1. 子图同构算法——Ullmann算法(1)不包含refine procedure的简单穷举算法。

    摘要: 转载请注明来自stanlysheng--talk is cheap, show me your code.http://www.cnblogs.com/stanly/ .谢谢.此文我也在CSD ...

  2. 三维点云学习(2)五种算法比较

    三维点云学习(2)五种算法比较 代码参考来自 黎老师github 本次测试包含五种算法比较: octree print("octree --------------")#时间统计c ...

  3. 95% 的算法都是基于这 6 种算法思想

    95% 的算法都是基于这 6 种算法思想 算法思想是解决问题的核心,万丈高楼起于平地,在算法中也是如此,95% 的算法都是基于这 6 种算法思想,结下了介绍一下这 6 种算法思想,帮助你理解及解决各种 ...

  4. 算法学习笔记13:哈希算法

    哈希算法(上):如何防止数据库中的用户信息被脱库 什么是哈希算法 应用一:安全加密 应用二:唯一标识 应用三:数据校验 应用四:散列函数 解答开篇 哈希算法(下):哈希算法在分布式系统中有哪些应用 应 ...

  5. 分类算法学习(二)——贝叶斯算法的原理及简单实现

    1.3.贝叶斯分类的基础--贝叶斯定理 每次提到贝叶斯定理,我心中的崇敬之情都油然而生,倒不是因为这个定理多高深,而是因为它特别有用.这个定理解决了现实生活里经常遇到的问题:已知某条件概率,如何得到两 ...

  6. 判断数组中某个元素除自身外是否和其他数据不同_算法工程师要懂的3种算法数据结构:线性表详解...

    算法思想有很多,业界公认的常用算法思想有8种,分别是枚举.递推.递归.分治.贪心.试探法.动态迭代和模拟.当然8种只是一个大概的划分,是一个"仁者见仁.智者见智"的问题. 其实这些 ...

  7. lm opencv 算法_Levenberg–Marquardt算法学习(和matlab的LM算法对比)

    回顾高斯牛顿算法,引入LM算法 惩罚因子的计算(迭代步子的计算) 完整的算法流程及代码样例 1.      回顾高斯牛顿,引入LM算法 根据之前的博文:Gauss-Newton算法学习 假设我们研究如 ...

  8. Python与Matlab算法学习一文通(快速排序算法)(更新中)

    想利用一些空余时间学一学python与matlab,与同学建立不知道能坚持多久的学习联盟,每周一部分题目,利用一周时间完成原理文档与程序编写.由于主要研究方向为其他方向,因此只会利用很少的空闲时间来学 ...

  9. 95% 的算法都是基于这 6 种算法思想,大厂Java面试必考点

    // 当前结点的 id 符合查找条件,返回当前结点 if(node.id === id) return node // 前结点的 id 不符合查找条件,继续查找它的每一个子结点 for(var i = ...

最新文章

  1. ICCV 2019 论文解读:用图神经网络改善视频的多标签分类
  2. Mybatis学习笔记(1)——第一个程序
  3. 每天一道LeetCode-----判断一个数是否是happy number(每一位的平方和最终为1)
  4. 安装redis提示[test] error 2_安装PHP Redis扩展
  5. Jquery,Ready函数.
  6. 前端开发者必备google插件
  7. python以下导入包的格式错误的是_Python结合Tableau,万字长文搞定传统线下连锁店数据分析...
  8. kindle刷机ttl_[原创]只需USB线对Kindle 3修砖的小白教程
  9. 三相逆变器双pi控制器参数如何调节_干货!单相光伏并网逆变器的环路控制
  10. 5s管理常用工具汇总
  11. 二重积分计算曲面表面积
  12. html 中数字一直往上加的动态效果,CSS动画:数字增量效果
  13. 实现Unity2D游戏中跳跃功能和相关问题解决
  14. 月均GMV超1500万,“组合营销”如何成为快手品牌出圈利器?
  15. 串口调试小节之五 串口参数设置查询
  16. 访问者模式(Visitor模式)
  17. 数据集:人群行为识别数据库总结
  18. 使用udp协议实现服务器端程序时,uIP中UDP协议实现的改进
  19. (第四章)OpenGL超级宝典学习:必要的数学知识
  20. 韩星李起光SNS被黑   社交网络安全刻不容缓

热门文章

  1. Cesium的坐标拾取详解
  2. 京东物流轨迹java_京东区块链 JD chain java demo实现
  3. 双十一买什么比较划算?四款实用性超强不吃灰的数码好物推荐
  4. 如何在Windows系统中设置Python程序定时运行
  5. LeetCode 344.Reverse String
  6. Python网络爬虫和信息提取:(动态网站)双色球数据爬取及写入数据库Sqlite、json和Excel表
  7. android 手机usb 驱动安装
  8. RRT(rapidly exploring random tree)算法学习笔记,机器人自主探索,路径规划
  9. eclipse building workspace sleeping rapidly
  10. BZOJ4706 B君的多边形 (超级卡特兰数/施罗德数)