题目
什么是素数(质数)?
数学上指在大于1的整数中只能被1和它本身整除的数,0和1既不是质数也不是合数
解法
// 暴力遍历
var countPrimes = function (n) {
let count = 0
for (let i = 2; i < n; i++) {
console.log(`${i}${isPrime(i)}`)
if (isPrime(i)) count++
}
return count
}
var isPrime = function (n) {
for (let i = 2; i <= n; i++) {
if (!(n % i)) return false
}
return true
}
这是一个双循环,时间复杂度是 实际上由于因子是成对出现的,而变化的节点在位置,比如:
8 = 2 * 4
8 = sqrt(8) * sqrt(8) // 相同因子作为对称节点
8 = 4 * 2
所以我们第一步优化可以做的是,在判断某数是否为素数时只需遍历模运算到,即:
var isPrime = function (n) {
for (let i = 2; i * i <= n; i++) {
if (!(n % i)) return false
}
return true
}
筛数法
筛法是一种寻找质数的算法。它的基本思想是先列出所有的正整数,然后从最小的质数开始,划去它的倍数,剩下的就是质数。整个的流程大概是,刚开始最小的质数是2,删除小于n的2的所有倍数,再是3,删除小于n的3所有倍数,以此类推。
var countPrimes = function (n) {
let res = new Array(n).fill(true) // idx正好是从0到9
res[0] = false
res[1] = false
for (let i = 2; i <= n; i++) {
// if res[i] is prime number
if (res[i]) {
// 标记所有i的倍数
for (j = 2 * i; j < n; j += i) {
res[j] = false
}
}
}
return res.filter((item) => item === true).length
}
实际上还有两个可以优化的点,对于外层循环由于刚才提到的因子对称性,只需要遍历(含)前的所有质数,另一方面,内层循环存在重复的标记,比如 2 * 3 和 3 * 2都标记了6,所以其实循环初始值可以优化为 i * i。
var countPrimes = function (n) {
let res = new Array(n).fill(true) // idx正好是从0到n-1
res[0] = false
res[1] = false
for (let i = 2; i * i <= n; i++) {
if (res[i]) {
for (j = i * i; j < n; j += i) {
res[j] = false
}
}
}
console.log(res)
return res.filter((item) => item === true).length
}
时间复杂度
完全执行这段代码的运算数量(即时间复杂度)是约 n/2 + n/3 + n/5 + n/7 + n/11 + ... 一直到小于的最大质数,根据Mertans定理,所有不超过正整数n的素数的倒数之和与之间存在固定的差值。
用公式表示就是:
所以整个的时间复杂度小于
空间复杂度
空间复杂度为,实际上是用空间复杂度换取了时间复杂度。