背景:统计质数个数是很基础的问题了,但是在n非常大的时候,不够快速的算法就会卡到效率瓶颈,下面是5个不同复杂度的算法。
(1)暴力解法
(2)缩小范围
(3)数论引入
(4)欧拉解法
(5)最快解法
1. 暴力解法:
直接挨个去看是不是质数,判断质数的方法就看是不是能够被1和自身之外的整数整除
class Solution:
def countPrimes(self, n):
"""
:type n: int
:rtype: int
"""
count = 2
if n < 2:
return 0
elif n == 2:
return 0
elif n == 3:
return 1
elif n == 4:
return 2
else:
for num in range(5, n, 2):
if self.isPrimes(num):
prList.append(num)
count += 1
return count
def isPrimes(self, n):
for div in range(2, n):
if n % div == 0:
return False
return True
这个方法可以说非常暴力了,最终的结果是时间超限
2. 缩小范围:缩小范围从两个方向入手考虑:
判断能不能被别的数整除,只需要判断比n小的质数即可,因为合数都是可以分解质因数的;
另一个角度:判断因子能够整除不用到n-1,只需要到sqrt(n)即可,即在其平方根以内的范围进行判断
class Solution:
def countPrimes(self, n):
"""
:type n: int
:rtype: int
"""
prList = [2, 3]
if n < 2:
return 0
elif n == 2:
return 0
elif n == 3:
return 1
elif n == 4:
return 2
else:
for num in range(5, n, 2):
if self.isPrimes(num, prList):
prList.append(num)
return len(prList)
def isPrimes(self, n, prList):
'''
use prime got to make judge
'''
max1 = pow(n, 0.5)
for div in prList:
if div > max1:
break
if n % div == 0:
return False
return True
这个方法加速不少,但是,在面对40万这个级别的数据的时候,就卡主了。
3. 引入数论:检索信息发现!所有的大于3的质数都是6的倍数两侧!即:6*n+1或者6*n-1,这个结论能将需要处理的数字从所有奇数再减少到所有6n两侧的奇数。
if n % 6 != 1 and n % 6 != 5:
return False
这个方法果然冲破了40万这个级别的运算,但是还是卡在了100万的门槛。
4. 欧拉方法
这个方法的思路和之前不同,不再是搞循环嵌套,而是希望一口气解决问题:构建一个长度为n的状态组,每找到一个质数,就将其的n倍的位置的标记全部标记为合数(置True),而合数不进行处理,如此的复杂度可以进一步下降。
不过初步的方案并没有通过速度要求,卡在了150万的门槛,分析:比如100,会被2 5分别置位,150 则被2 5 3分别置位,效率浪费很多。
继续优化:减少重新置位次数!
if n < 2:
return 0
elif n == 2:
return 0
elif n == 3:
return 1
ipl = []
for i in range(n): # 创建状态组
ipl.append(False)
primeList = []
count = 0
for i in range(2, n): # 这个循环做两个事情:判断质数、标记质数的N倍!如此,每个数字只会被置位一次
if not ipl[i - 1]:
primeList.append(i)
count += 1
for pr in primeList:
if pr * i >= n:
break
ipl[i * pr - 1] = True
if i % pr == 0:
break
return count
终于这个方案通过了速度测试,python3跑下来,系统给的时间是1500ms,不过看了下最优答案,才124ms!少了一个数量级!
5. 最快方法
class Solution:
def countPrimes(self, n):
"""
:type n: int
:rtype: int
"""
if n <= 2:
return 0
prime = [0]*n # 这个是类似状态组的东西
l = int(n**0.5) # 最大遍历上限为√n
for i in range(2, l+1):
if prime[i]: # 合数直接过
continue
prime[i*i:n:i] = [1]*len(prime[i*i:n:i]) #如果是质数,质数平方开始到n之间步距为i的每个位置存1
return len(prime) - sum(prime) -2 # 输出总数字与合数的差(-2是去除0 和1)
简单分析一下:
思想上和上边的方法大同小异,不过使用了比较高效的函数接口,同时每个质数只去判断其平方项之上的数字,且不去考虑有大于其平方根的质数因子(这个很重要!),对算法的提速达到了10倍,可以说良好的编程喜欢和细节的掌握能够让产出的东西发生翻天覆地的变化了。