都2023年了,你还不会用埃筛法求素数?

87 阅读4分钟

大家好,我是王不错。 (欢迎大家关注我的公众号:程序员王不错

今天看一道有趣的算法题吧。

统计素数个数是一道在面试中经常会被问到的问题。需求很简单,就是让我们统计给定n以内的素数个数
素数是指只能被1和它本身整除的自然数,0和1除外。 算法假定输入n,输出n以内的素数个数。如输入100,输出25,表示100以内的素数有25个。

解法一 简单直接的暴力法

暴力法就是直接循环判断每一个数字是否为素数,是则将计数变量加1。

这个算法比较简单,直接看代码:

// 先自定义一个函数,判断是否是素数:只能被1和自身整除的自然数,0和1除外
func isPrime(x int) bool {
    for i := 2; i < x; i++ {
        if x%i == 0 {
            return false
        }
    }
    return true
}

// 统计n以内素数个数
func countPrime(n int) int {
    count := 0
    for i := 2; i < n; i++ {
        if isPrime(i) {
            count++
        }
    }
    return count
}

当然,上面的代码可以稍微优化一下。

以判断12是否为素数为例,

12 = 2 * 6

12 = 3 * 4

12 = 4 * 3

12 = 6 * 2

不难看出,一半的计算其实是多余的,因为12/3=4,必然有12/4=3,所以只需要计算到一半就可以了。这个“一半”的界定是12 = 根号12 * 根号12。所以判断一个数x是否为素数,只需要判断到根号x即可。

修改后的代码为:


// 先自定义一个函数,判断是否是素数:只能被1和自身整除的自然数,0和1除外
func isPrime(x int) bool {
   for i := 2; i <= int(math.Sqrt(float64(x))); i++ {
       if x%i == 0 {
           return false
      }
  }
   return true
}

// 统计n以内素数个数
func countPrime(n int) int {
   count := 0
   for i := 2; i < n; i++ {
       if isPrime(i) {
           count++
      }
  }
   return count
}

Go中取根号需要导入math包。这里还有一种不用导包计算根号的写法:

for i := 2; i * i <= x; i++ {  
    // ...
}

这种写法不难理解,同样我们也更推荐你在for循环中使用这种写法。

解法二 巧妙的埃筛法

优化后的暴力法的时间复杂度仍然达到了O(n^3/2),有没有更快的解法呢?这里介绍埃筛法埃筛法的全称是埃拉托斯特尼筛法,也简称为埃氏筛法,是一种由希腊数学家埃拉托斯特尼所提出的一种简单检定素数的算法。

埃筛法的基本思想是:从2开始,将每个质数的倍数都标记成合数(即非素数),以达到筛选素数的目的。

也就是说,假如判断了 i 是素数,那么 i * 2, i * 3, i * 4....都可以被标记为合数。

来看代码:

// 埃筛法
func countPrime(n int) int {
  isPrime := make([]bool, n) //false代表素数
  count := 0
  for i := 2; i < n; i++ {
    if (!isPrime[i]) {
      count++
            // 将质数的倍数标记为合数
      for j := 2 * i; j < n; j += i {
                // j += i可以求到i的倍数(因为对j每次加i,相当于对i加倍)
        isPrime[j] = true
      }
    }
  }
  return count
}

同样上面的埃筛法代码仍然存在一个缺陷:对于一个合数,有可能被筛选多次。

例如素数i,每一次都要从2开始计算它的倍数,

如2 * 2 = 4, 2 * 3 = 6, 2 * 4 = 8, 2 * 5 = 10 ...

   3 * 2 = 6, 3 * 3 = 9, 3 * 4 = 12, 3 * 5 = 15 ...

我们发现,其实6已经在计算2的倍数时被标记为了合数,但是在计算3的倍数的时候又再一次重复标记了,这种情况在上述代码中很常见,做了很多无用功。

那么如何确保每个合数只被筛选一次呢?我们只要用它的最小质因子来筛选即可。

最终代码:


// 埃筛法
func countPrime(n int) int {
  isPrime := make([]bool, n) //false代表素数
  count := 0
  for i := 2; i < n; i++ {
    if (!isPrime[i]) {
      count++
            // 即j从i*i开始
      for j := i * i; j < n; j += i {
        isPrime[j] = true
      }
    }
  }
  return count
}

埃筛法的时间复杂度是O(nlogn) ,具体的证明过程比较复杂,这里就不赘述了。

以上就是统计素数的埃筛法,虽然代码很简单,但是其中的思路还是很值得推敲的。

好啦,今天的分享就到这里。

我是程序员王不错,如果文章内容有帮助到你,就点个“在看”吧~