文章结构:算法描述(算法部分)与时间复杂度计算(数学部分)
问题引入:设计一个算法,求出区间[1,N]中素数的个数,要求时间复杂度尽可能地小。
问题分析:
1.一般的枚举(暴力)算法的思想就是,设置双层循环,对于1
2.考虑到整数因子的成对特性,即x是y的因数,y/x也是x的因数。在进行单次n的素性判断时,而如果我们每次选择校验两者中的较小数,则不难发现较小数一定落在[2,sqrt(n)]
进行n%i时,可将i的上限改为 n \sqrt{n} \quad n
即内层循环次数可以降到根号n,从而使整个算法的时间复杂度降到 O ( N 3 / 2 ) O(N^ {3/2}) O(N3/2)
优化后的代码如下:
bool isPrime(int x) {
for (int i = 2; i * i <= x; ++i) {
if (x % i == 0) {
return false;
}
}
return true;
}
int countPrimes(int n) {
int ans = 0;
for (int i = 2; i < n; ++i) {
ans += isPrime(i);
}
return ans;
}
//代码来源:LeetCode
3.事实上,至此,枚举已经难以继续优化。如果考虑到数与数的关联性,我们考虑这样一个事实:即如果 P是质数,那么比P大的2P,3P,4P…一定不是质数,因此我们根据这个思路来优化算法。
我们设 isPrime[P]表示数 P是不是质数,如果是质数则isPrime[P]=1,否则为 0。首先我们默认数组中的每个元素值都为1,即质数;从小到大遍历每个数,如果这个数为质数,则将其所有的倍数都标记为合数(除了该质数本身),即 isPrime[P]=0,通过每次遍历修改isPrime[i]的值,若值为1,给计数器加1。这样在程序结束后,就可以得到质数的个数。
这便是筛选法的思路。
代码如下:
int countPrimes(int n) {
if (n < 2) {
return 0;
}
int isPrime[n];
memset(isPrime, 1, sizeof(isPrime));//将数组所有元素初始化为0;
int ans = 0;
for (int i = 2; i < n; ++i) {
if (isPrime[i]) {
ans += 1;
if ((long long)i * i < n) {
//算法优化,对于一个质数p,如果从2p开始标记其实是冗余的,应该直接从p*p开始标记,因为2p,3p… 这些数一定在之前就被其他数的倍数标记过了,例如2的所有倍数,3的所有倍数等。
for (int j = i * i; j < n; j += i) {
isPrime[j] = 0;
}
}
}
}
return ans;
//代码来源:LeetCode
——————————————————————
首先,给出结论,埃氏筛的时间复杂度为
O ( n ln ln n ) O(n\ln \ln n) O(nlnlnn)
下面计算算法的时间复杂度。
1.最朴素的筛法
for(int i=2;i<=n;i++)
for(int j=2;j*i<=n;j++)
prime[i*j]=0;
外层循环为n,当外层循环确定一个数时,设为i,则内层的循环次数 K 为:
⌊ n i ⌋ − 1 < K < n i \lfloor {\frac ni}\rfloor-1
则循环总数为 n 2 + n 3 + n 4 + ⋯ + n n \frac n2+\frac n3+\frac n4+\cdots+\frac nn 2n+3n+4n+⋯+nn即 n × ( 1 2 + 1 3 + 1 4 + ⋯ + 1 n ) n\times(\frac 12+\frac 13+\frac 14+\cdots+\frac 1n) n×(21+31+41+⋯+n1)
易知,幂级数
ln ( 1 + t ) = t − t 2 2 + t 3 3 − t 4 4 + t 5 5 − ⋯ \ln(1+t)=t-\frac{t^2}{2}+\frac{t^3}{3}-\frac{t^4}{4}+\frac{t^5}{5}-\cdots ln(1+t)=t−2t2+3t3−4t4+5t5−⋯
令 t = 1 x t=\frac 1x t=x1则有
ln ( x + 1 x ) = 1 x − 1 2 x 2 + 1 3 x 3 − 1 4 x 4 + ⋯ \ln(\frac {x+1}{x})=\frac 1x-\frac{1}{2x^2}+\frac{1}{3x^3}-\frac{1}{4x^4}+\cdots ln(xx+1)=x1−2x21+3x31−4x41+⋯
将 1 x \frac 1x x1和 ln ( x + 1 x ) \ln(\frac {x+1}{x}) ln(xx+1)移到等式左边和右边,再分别将 x = 1 , 2 , 3 , … , n x=1,2,3,\ldots,n x=1,2,3,…,n代入上式,得
1 1 = ln 2 + 1 2 × 1 − 1 3 × 1 + 1 4 × 1 − ⋯ \frac 11=\ln 2+\frac {1}{2\times1}-\frac {1}{3\times1}+\frac {1}{4\times1}-\cdots 11=ln2+2×11−3×11+4×11−⋯
. . . . . . . . .
1 n = ln n + 1 n + 1 2 n 2 − 1 3 n 3 + 1 4 n 4 − ⋯ \frac 1n=\ln\frac {n+1}{n}+\frac {1}{2n^2}-\frac {1}{3n^3}+\frac {1}{4n^4}-\cdots n1=lnnn+1+2n21−3n31+4n41−⋯
将上式左右两边的对应项分别相加,我们得到:
∑ i = 1 n 1 i = ln ( ∏ i = 2 n i + 1 i ) + 1 2 ( 1 + 1 4 + 1 9 + ⋯ + 1 n 2 ) \sum_{i=1}^{n} {\frac 1i}=\ln({\prod_{i=2}^{n} {\frac {i+1}{i}}})+\frac12(1+\frac14+\frac19+\cdots+\frac{1}{n^2}) i=1∑ni1=ln(i=2∏nii+1)+21(1+41+91+⋯+n21)
− 1 3 ( 1 + 1 8 + 1 27 + ⋯ + 1 n 3 ) -\frac13(1+\frac18+\frac{1}{27}+\cdots+\frac{1}{n^3}) −31(1+81+271+⋯+n31) + 1 4 ( 1 + 1 16 + 1 81 + ⋯ + 1 n 4 ) − ⋯ +\frac14(1+\frac{1}{16}+\frac{1}{81}+\cdots+\frac{1}{n^4})-\cdots +41(1+161+811+⋯+n41)−⋯
(Hey!(>_<)冲啊,胜利就在眼前!)
即:当n充分大趋于无穷时: 左 边 = ln ( n + 1 ) + 1 2 ζ ( 2 ) − 1 3 ζ ( 3 ) + 1 4 ζ ( 4 ) − ⋯ 左边=\ln(n+1)+\frac12\zeta(2)-\frac13\zeta(3)+\frac14\zeta(4)-\cdots 左边=ln(n+1)+21ζ(2)−31ζ(3)+41ζ(4)−⋯
(感兴趣的小伙伴可以自行百度ζ(s)函数,这里不再赘述)
根据黎曼ζ函数性质可知,s>=2时,ζ(s)都是收敛于某一个常数的,如:
ζ ( 2 ) = π 2 6 \zeta(2)=\frac{\pi^2}{6} ζ(2)=6π2 ( 仅 知 ζ ( 3 ) 是 一 个 无 理 数 ) (仅知ζ(3)是一个无理数) (仅知ζ(3)是一个无理数)
后项是调和级数与自然对数的差值的极限 lim n → ∞ [ ( ∑ k = 1 n 1 k ) − ln n ] \lim_{n \to \infty} \left[\left(\sum_{k=1}^{n} {\frac 1k}\right)-\ln n\right] n→∞lim[(k=1∑nk1)−lnn]欧拉算出了后项的和,称为欧拉常数γ,约等于0.57721。
(没想到吧,又是我Euler) γ = ∑ m = 2 + ∞ ζ ( m ) ( − 1 ) m m \gamma\quad=\quad\sum_{m=2}^{+\infty} {\frac {\zeta(m)(-1)^m}{m}} γ=m=2∑+∞mζ(m)(−1)m
咳咳!常数项的减少并不影响级数的敛散性,记后项的和为常数gamma.
左 边 = ln ( n + 1 ) + γ 左边=\ln(n+1)+\gamma 左边=ln(n+1)+γ则在朴素的筛选方法下,此算法的时间复杂度为:
O ( n ln n ) O(n\ln n) O(nlnn)
2.只针对素数进行筛选的代码:
for(int i=2;i<=n;i++)
if(prime[i])
for(int j=2;j*i<=n;j++)
prime[i*j]=0;
对于经典的埃氏筛而言,对出现的所有的整数都进行向后筛选,在对朴素的筛法进行优化后,只会对质数向后筛选。
故在这种情况下,时间复杂度一定小于:
n ln n n\ln n nlnn
那具体的复杂度到底是多少呢?
不难于理解,这道题内层循环次数本质上是求1到N之间所有素数对应的N/P(i)(i表示第i个素数)之和,至于这个和的具体值是多少,业已超出我的知识范围(有些地方严格意义上已经涉及数论的内容,实在很难理解),历史上也有很多数学家付出心血研究这个问题。
感兴趣的同学可以参考一些数论的书籍,或移步至这位兄弟的文章~( ̄▽ ̄~)~:
参考资料:《什么是数学:对思想和方法的基本研究 R.科朗&H.罗宾》