前端面试算法篇——高效查找素数

239 阅读1分钟

题目

Leetcode题目链接

什么是素数(质数)?

数学上指在大于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
}

这是一个双循环,时间复杂度是O(n2)O(n^2) 实际上由于因子是成对出现的,而变化的节点在n\sqrt{n}位置,比如:

8 = 2 * 4
8 = sqrt(8) * sqrt(8) // 相同因子作为对称节点
8 = 4 * 2

所以我们第一步优化可以做的是,在判断某数是否为素数时只需遍历模运算到n\sqrt{n},即:

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
}

实际上还有两个可以优化的点,对于外层循环由于刚才提到的因子对称性,只需要遍历n\sqrt{n}(含)前的所有质数,另一方面,内层循环存在重复的标记,比如 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 + ... 一直到小于n\sqrt{n}的最大质数,根据Mertans定理,所有不超过正整数n的素数的倒数之和与lglgn\lg lg n之间存在固定的差值。

用公式表示就是: 1/2+1/3+1/5+1/7++1/pnlglgn+C1/2 + 1/3 + 1/5 + 1/7 + …… + 1/pn ≈ \lg lg n + C

所以整个的时间复杂度小于O(nlglgn)O(n\lg lg n)

空间复杂度

空间复杂度为O(n)O(n),实际上是用空间复杂度换取了时间复杂度。