leetcode实战—素数(埃拉托色尼筛选法包括证明、哈希、RSA)

前言

素数这个概念人类已经研究了上千年,但是的具体的起源却不得而知。早在公元前300年,欧几里得就在他的著作元素中证明了有无穷多个素数,同时也证明了任何一个整数都能够被某一个素数整除。时至今日,素数在计算机科学这样一个和数学联系紧密的学科中也有这个广泛的应用,比如布隆过滤器、伪随机数、RSA加密算法等等,所以掌握素数的特性以及应用能够帮助我们解决不少实际问题。

简介

素数(又称质数)是一个只能被1和它自己整除的整数,换句话说他只有两个因数——1和它自己。比如3是一个素数,因为3只能被1和3整除,但是6不是素数因为它能被2和3整除。迄今为止发现的最大的素数是\(2^{57885161}-1\),由中央密苏里大学的数学家Curtis Cooper发现,大概有17,425,170位数字。

在公元前200年,古希腊数学家埃拉托色尼(Eratosthenes)创造了一套算法用来计算素数,这就是大家熟知的埃拉托色尼筛选法(the Sieve of Eratosthenes),这是最早的用来计算素数的方法。就拿100以内素数的方法来简单介绍一下这个筛选法:

首先我们找到第一个素数2,用圆圈标出来,然后划掉所有2的倍数4、6、8等等。然后我们找到下一个没有被划掉的数字(这个数字肯定不是前面更小数字的倍数),也就是3,用圆圈标出来,再划掉所有的3的倍数,注意这里6已经被划掉了,但是9可以继续被划掉。接下来再用圆圈标出下一个没有被划掉的数字5,划掉所有的5的倍数。如此往复,直到100的平方根10截止,所有的100以内的素数就都被圆圈圈起来了。

计数质数

来看看素数计算的最简单的版本,Leetcode第204题计数质数

统计所有小于非负整数 n 的质数的数量。

示例:

输入: 10

输出: 4

解释: 小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。

这道题通过我们在上面介绍的埃拉托色尼筛选法可以很轻松的做出来,只需要创建一个小于n的数组,从索引2开始一直到Math.sqrt(n)对整个队列进行筛选,最后统计一遍整个数组里面所有的素数就行了。

public int countPrimes(int n) {
    if (n <= 2) return 0;
    boolean[] primes = new boolean[n];  //创建整个素数数组
    Arrays.fill(primes, true);      //初始化全部位素数
    primes[0] = false;
    primes[1] = false;                  //划掉0和1
    int sqrt = (int)Math.sqrt(n);       //设置上界
    for (int i = 2; i <= sqrt; i++) {
        if (!primes[i]) continue;       //不是素数,可以跳过
        for (int multi = i<<1; multi < n ; multi += i){
            primes[multi] = false;      //划掉倍数
        }
    }
    int count = 0;
    for (boolean prime : primes) {      //统计数组中素数的数量
        if (prime) count++;
    }
    return count;
}

运行了上面的代码之后,执行时间17ms,才击败了60%的用户,代码还有很大的优化空间。我们从上一个小节的筛选法中就可以看到,我们经常要划掉已经被划掉的数字,比如在划掉了所有的2的倍数6之后,我们在划掉3的倍数的时候又划掉了6一次,这样就浪费了我们的资源,能不能直接跳过6去划掉9呢?所以这里就是需要考虑要从哪个数字开始划数字。在划掉2的倍数的时候,我们是划掉2x2、2x3、2x4、2x5等等,在划掉3的时候是划掉3x2(在素数2的那一轮已经被划掉了)、3x3、3x4、3x5等,在划掉5的时候是划掉5x2(素数2的一轮已经被划掉了)、5x3(素数3的一轮被划掉了)、5x4(等于10x2也在素数2的一轮被划掉了)、5x5等等。现在这个规律应该十分明显了,在划掉素数i的倍数的时候,所有比i小的数字的和i的乘积已经被划掉了,所以下一轮直接从i*i开始就行了,于是我们可以把上面代码里面的multi初始化为i*i,代码我就不贴出来了。

其实这道题还有挺多的改进方法的,比如使用BitSet来代替boolean的数组、用false代表是位置i是质数来缩短数组的初始化时间或者在第一个for循环里面就统计所有非质数省去最后一个循环,可以用很多方法减少冗余操作的时间,不过整体最终的时间复杂度是\(O(NloglogN)\),第一个for循环是不可避免的,下面我们来用一些数学知识来解释一下这个时间复杂度。

首先我们有数字n代表素数的上边界(不包含),p代表小于n的最大素数,内部循环设置false的时间是恒定的。

i==2时,内部的筛除操作进行了\(\frac{n}{2}\)次

i==3时,筛除了\(\frac{n}{3}\)次

i==4时,直接跳过,筛除了\(0\)次(所有的非素数都会跳过)

i==5时,筛除了\(\frac{n}{5}\)次

i==p时,筛除了\(\frac{n}{p}\)次

所以总体的运行时间可以看作:

\[\frac{n}{2}+\frac{n}{3}+\frac{n}{5}+...\frac{n}{p}
\]

等效于

\[n\times(\frac{1}{2}+\frac{1}{3}+\frac{1}{5}+...\frac{1}{p})
\]

现在就是要证明

\[\frac{1}{2}+\frac{1}{3}+\frac{1}{5}+...\frac{1}{p} = loglogn
\]

在进行下面的证明的时候,我们需要用到调和级数泰勒级数欧拉乘积公式,这几个公式的证明过程感兴趣的可以自己去看看(在我的参考文档里面,纯数学部分,比写代码难多了),就先说我们要用到的定理。



调和级数(Harmonic series)是一个发散的无穷级数,当\(n\)趋近于无穷大时,有一个近似公式:

\[1 + \frac{1}{2} + \frac{1}{3} + ... = \sum_{i=1}^{n}\frac{1}{i} = ln(n)+\gamma
\]

其中\(\gamma\)为欧拉常数,\(\gamma \approx 0.57721\)



泰勒级数(Taylor series)是1715年英国数学家布鲁克·泰勒提出的,在零点的导数求得的泰勒级数又叫麦克劳林级数,具体的原版的公式我就不在这里贴出来了,这里只贴一个常用的泰勒级数,也就是以\(e\)为底数的自然对数的麦克劳林序列

\[ln(1-x) = -\sum_{n=1}^{\infty} \frac{x^n}{n} = -x-\frac{x^2}{2}-\frac{x^3}{3}-..-\frac{x^n}{n}
\]

对任意属于\([-1,1)\)内的\(x\)都成立。左右的符号同时取反,可以得到:

\[ln(1-x)^{-1} = \sum_{n=1}^{\infty} \frac{x^n}{n} = x+\frac{x^2}{2}+\frac{x^3}{3}+..+\frac{x^n}{n}
\]



欧拉乘积公式(Euler product)是著名的瑞士数学家欧拉于1737年在俄罗斯的圣彼得堡科学院发表的重要公式,为数学家研究素数的分布奠定了基础,即:

\[\sum_{n} n^{-s} = \prod_{p}^1 (1-p^{-s})^{-1}
\]

其中n为自然数,p为素数。



继续我们的推理。

在欧拉公式中,我们如果将所有的\(s\)用\(1\)来代替就可以得到

\[\sum_{n} n^{-1} = \prod_{p}^1 (1-p^{-1})^{-1}
\]

对两侧同时使用\(log\)函数可以得到

\[ln(\sum_{n} n^{-1}) = ln(\prod_{p}^1 (1-p^{-1})^{-1})
\]

简化之后可以得到

\[ln(\sum_{n} n^{-1}) = \sum_{p} ln((1-p^{-1})^{-1})
\]

因为\(-1 < p^{-1} < 1\),所以可以对上面公式的右侧的每一项进行泰勒展开得到

\[ln((1-p^{-1})^{-1}) = \sum_{n=1}^{\infty} \frac{1}{np^{n}}
\]

当\(p\)趋近于无穷大的时候,右侧的公式收敛于\(\frac{1}{p}\),就可以得到

\[ln((1-p^{-1})^{-1}) = \frac{1}{p}
\]

代入到原公式可以得到

\[ln(\sum_{n} n^{-1}) =\sum_{p} \frac{1}{p}
\]

上述公式左侧log内部正好是调和级数,把调和级数的近似公式代入左侧可以得到

\[\sum_{p} \frac{1}{p} = ln(ln(n)+\gamma)
\]

忽略右边的常数\(\gamma\)就可以得到最后的时间复杂度为\(O(nln(ln(n)))\),也就是\(O(nloglogn)\)级别。

除了这个证明过程之外,在数论里面已经有一个定理证明这个时间复杂度——Mertens‘ second theorem(维基百科需要上外网,中文材料好像特别少)。

能够看到这个地方已经说明你有着超人般的毅力(还有什么比数学更令人头疼),如果觉得上面的讲解还不够清楚或者有很多数学的细节不太理解,可以看看参考文档。据我在网上查到的,还没有任何一篇文章能够像这篇一样把埃拉托色尼筛选法的时间复杂度讲的这么清楚的,希望能够帮助到大家。

应用1 哈希算法

哈希表是我们日常开发中用的非常多的一种数据结构,在执行搜索、插入或者删除的时候能够在O(i)的时间内完成操作,相比于二分搜索树最快也要O(logN)的时间,性能得到极大的提高。

哈希表是利用哈希算法将转化成数组中的一个索引,直接使用这个索引地址找到或者插入元素。常用的哈希算法是通过取模操作拿到索引。比如在下面的代码中,TABLE_SIZE是数组的大小,得到结果就是数组索引。

private int hash(int key) {
    return key % TABLE_SIZE;
}

但是在上面转化的过程中可能会出现冲突,也就是两个不同的Key通过哈希算法得到同一个索引,比如当keyTABLE_SIZE2*TABLE_SIZE,取模得到的值都是0,产生冲突,不管是索引往后顺移还是使用链表(或者红黑树)都会降低哈希表的性能。数组的长度越小,需要存储的数值越多,就越容易发生冲突,为了尽量减小冲突,通常对素数取模。

比如说现在有一组比较特殊的键key=[0,3,6...99],并且hashTable的大小是m=12,因为312的因数,所以所有的3的倍数都会被哈希到3的倍数的位置中

[0,15,30...]哈希到0

[3,18,33...]哈希到3

[6,21,36...]哈希到6

[9,24,39...]哈希到9

[12,27,42...]哈希到0

如此类推可知虽然我们表的大小是12,但是所有的数字只哈希到了0,3,6,9四个位置,明显不是我们想要的。如果我们把表的大小换成素数13,那么就不会有这么多的冲突。用素数的最大好处是可以尽量避免把有相同特性的元素(在这里特性是所有的键都能被3整除)放到集中的几个位置中。

虽说用素数取模能够减小冲突,但是前提是所有的键并不是完全随机而是有一定特点的。对于完全随机的输入,即使用素数也不能减少冲突。

应用2 RSA加密算法

RSA(Rivest–Shamir–Adleman)是最早的公钥密码系统之一,被广泛用于安全数据传输。在这样的密码系统中,加密密钥是公共的,而解密密钥却是私密的。在RSA中,这种不对称性是基于对两个大质数乘积进行因式分解的实践困难,即“因式分解问题”。 缩写词RSA由Ron Rivest,Adi Shamir和Leonard Adleman的姓氏的首字母组成,他们于1977年首次公开描述了该算法。Clifford Cocks,在英国情报局政府通信总部(GCHQ)工作的英国数学家。 于1973年开发了一个等效系统,但直到1997年才解密。

RSA算法是用两个大质数以及一个辅助值创建一个公共密钥发布出去。这两个素数必须保密。任何人都可以使用公共密钥对信息进行加密,但是只有知道质数的人才能对邮件进行解码。破解RSA加密被称为RSA问题。如果使用足够大的质数作为密钥,那么当前还没有方法可以使破解RSA加密。RSA算法的可行性证明涉及到了费马定律,这里就不多讲了,这里只是简单讲解一下RSA算法的工作过程。

RSA算法涉及到4个步骤:生成密钥、发布密钥、加密和解密

生成密钥

  1. 随机挑选两个不同的素数\(p\)和\(q\)
  2. 计算\(n=p*q\)
  3. 计算n的卡迈克尔函数值\(\lambda(n)\),也就是\(p-1\)和\(q-1\)的最小公倍数(least common multiple)
  4. 在\(1\)和\(\lambda(n)\)之间选取一个和\(\lambda(n)\)互质的整数\(e\)(也就是和\(\lambda(n)\)的最大公倍数是1的整数)
  5. 计算\(e\ mod\ \lambda(n)\)的一个模逆元\(d\),用人话说就是找到\(d\)使得\((d*e)\%(\lambda(n)) = 1\)

公钥由模数\(n\)和指数\(e\)组成,私钥由模数\(n\)和指数\(d\)组成。\(p\)\(q\)和\(\lambda(n)\)都必须保密,因为他们能用来计算\(d\)。

使用密钥

如果B想发送消息给A,B会首先用A的公钥去加密信息,然后把加密的信息传递给A,最后A用自己的密钥去解密密文。对于第三方,如果没有A的密钥就无法解密B的密文,所以这个信息只有A能够看到。

加密过程

如果B想发送信息\(M\)给A,首先获得A的公钥\(n\)和\(e\),加密的密文就是

\(c=m^e\ mod\ n\)

解密过程

A收到B的信息\(c\)之后,用自己的密钥\(n\)和\(d\)解密

\(c^d\ mod\ n=(m^e\ mod\ n)^d\ mod\ n= m\)

举个例子:

  1. 生成密钥首先选取两个质数:\(p=61\),\(q=53\)
  2. 计算\(n=61\times53=3233\)
  3. 计算\(\lambda(n)=lcm(60,52) = 780\)
  4. 选取\(e=17\)
  5. 找到模逆元\(d=413\),因为\(413\times17=7021?\),\(7021\%780=1\)
  6. 假设信息为\(m=65\),加密的密文为\(c=65^{17}\ mod\ 3233=2790\)
  7. 收到信息后进行解密,解密信息为\(m=2790^{413}\ mod\ 3233=65\)

为什么说这个加密算法很难破解呢?因为在实践中很容易找到符合要求的一组特别大的\(e\)、\(d\)和\(n\)使得所有的小于\(n\)的整数\(m\)都能满足

\((m^d)^e\ mod\ n =\ m\)

但是如果我们仅仅知道\(e\)和\(n\),凭借现在计算机能够提供的算力却几乎不能计算出\(d\)(对于特别大的素数的因式分解是一件特别困难的事情),而没有\(d\)就不能解密信息。所以选取的素数的越大,破解的难度就越大,加密算法就越安全。

总结

这篇文章写着写着就扯远了,从leetcode题目到RSA加密算法。素数个数的计算用到了埃拉托色尼筛选法,时间复杂度为\(O(nloglogn)\),空间复杂度为\(O(n)\),这篇文章给出了详细的时间复杂度的证明,为了节省篇幅忽略了基础定理的证明。在哈希算法中我们使用素数来尽量减少键的冲突,提高效率。在RSA加密算法中,创作者利用了两个大素数乘积很难进行因式分解的特点以及费马小定律设计了一套加密和解密信息的规则,使RSA成为当今应用最多的非对称加密加密协议。

参考

Prime Numbers–Why are They So Exciting?

prime number

What is a Prime Number?

如何高效判定、筛选素数

LeetCode一求素数算法优化的简单研究

How is the time complexity of Sieve of Eratosthenes is n*log(log(n))?

调和级数近似求和公式推导

欧拉乘积公式的推导过程

泰勒公式

泰勒级数

Prime Numbers in Hash Functions

RSA (cryptosystem)

Why RSA Works: Three Fundamental Questions Answered

EULER AND THE PARTIAL SUMS OF THE PRIME HARMONIC SERIES

Mertens‘ theorems

更多内容请看我的个人博客

原文地址:https://www.cnblogs.com/sunshuyi/p/12681161.html

时间: 2024-10-08 20:44:53

leetcode实战—素数(埃拉托色尼筛选法包括证明、哈希、RSA)的相关文章

【算法设计与分析基础】1、埃拉托色尼筛选法

就是筛选从1到n(不包括n)之间的所有质数 package algorithm.ch01; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import org.junit.Test; /** * 实现埃拉托色尼筛选法 * @author xiaof * */ public class Sieve { /** * 输入正整数 n > 1 * 输出:包含所有小于等于N的质数数组 * @p

算法笔记_012:埃拉托色尼筛选法

1 问题描述 Compute the Greatest Common Divisor of Two Integers using Sieve of Eratosthenes. 翻译:使用埃拉托色尼筛选法计算两个整数的最大公约数.(PS:最大公约数也称最大公因数,指两个或多个整数共有约数中最大的一个) 2 解决方案 2.1 埃拉托色尼筛选法原理简介 引用自百度百科: 埃拉托色尼筛选法(the Sieve of Eratosthenes)简称埃氏筛法,是古希腊数学家埃拉托色尼(Eratosthene

埃拉托色尼筛选法

埃拉托色尼筛选法(the Sieve of Eratosthenes)简称埃氏筛法,是古希腊数学家埃拉托色尼(Eratosthenes 274B.C.-194B.C.)提出的一种筛选法. 是针对自然数列中的自然数而实施的,用于求一定范围内的质数. (1)先把1删除(现今数学界1既不是质数也不是合数) (2)读取队列中当前最小的数2,然后把2的倍数删去(3)读取队列中当前最小的数3,然后把3的倍数删去(4)读取队列中当前最小的数5,然后把5的倍数删去(5)如上所述直到需求的范围内所有的数均删除或读

关于埃拉托色尼筛选法的整理(质数问题)

求素数的问题也有不少的算法,埃拉托色尼筛选法应该算是时间复杂度不大的一种算法.... 比如要求从1到n范围内的素数表,基本算法思想就是从第一个质数2开始,把2的所有倍数标记为非素数,然后进入到3,把3的所有倍数标记为非素数,然后跳过4(因为4是2的倍数而且已经被标记为非素数了),然后进入到5,把5的所有倍数标记为非素数,循环直至结束. 以上算法还可以继续改进,那就是最外层循环可以只遍历到根号n就行了.因为大于根号n的数要么是素数,要么就有一个小于根号n的因数(而这是不可能发生的,因为如果它有小于

数组拷贝、数组函数、通过数组函数来模拟数据结构的栈和队列、回调的意义、数组函数的排序问题、算法以及寻找素数的筛选法

1.数组的拷贝数组拷贝时指针的指向问题. 数组在拷贝时,指针的位置随之复制[这一点拷贝是完全一样]但是如果拷贝的数组的指针是非法的,那么拷贝出新指针的位置进行初始化<?php$arr1=array('123');end($arr1);next($arr1);//这个指针非法$arr2 = $arr1;//这里进行数组的拷贝var_dump(current($arr2));//得到指向‘123’元素的指针var_dump(current($arr1));//此时这个数组的指针有问题?> 但是拷贝

素数高效率筛选法

无需多言直接上代码吧! 1 //Eratosthenes 筛法(埃拉托斯特尼筛法) 2 memset(check, false, sizeof(check)) 3 int tot = 0; 4 for(int i=2; i<=N; i++) 5 if(!check[i]) 6 { 7 prime[tot++] = i; 8 for(int j=i*2; j<=N; j+=i) 9 check[j] = true; 10 } 11 12 //Euler 筛法(欧拉筛法)--线性筛选 13 mem

获取小于N的素数 优化筛选法的C++实现

孪生素数(间隔为2的相邻素数)的相关定理与推论 P1: 当 N 不小于 6 且 N-1 和 N+1 为 孪生素数, 则 N 一定是 6的倍数 T1:当 N 不小于 1 且 N=6x-1 或 N=6x+1 不是素数, 那么 N 一定不是 2和 3的倍数 P2:当N 不小于 5 时,若 N 为素数,那么N mod 6 =1或N mod 6 = 5 T2: 一个大于5的数有且只有整除6余数是 1 或者 5 才有可能是素数 一个数 整除6 的余数 可能是 1,2,3,4,5 但是 余数 为2,3,4的情

素数的筛选法

1 #include <cstdio> 2 #include <cstdlib> 3 #include <cstring> 4 #include <algorithm> 5 #include <iostream> 6 #include <cmath> 7 8 #define MAX 1000000 9 10 using namespace std; 11 int num[MAX]; 12 13 int main(int argc, c

关于素数的快速查找——素数筛选法

利用素数筛选法进行素数的快速查找.原理很简单,素数一定是奇数,素数的倍数一定不是素数.思路如下: 预定义N表示10000,即表示查找10000以内的素数,首先定义数组prime[]对N以内的数进行标记,奇数存为1,偶数存为0,最终实现结果为素数的prime值为1,因此将prime[2]赋值为1(2是素数).之后利用for循环,对N以内的奇数进行遍历(注意for循环的条件控制),for里用if判断是否为素数(奇数),若是,执行内部嵌套的for循环判断该奇数是否为素数,若是则标记为1,若不是则pri