Leetcode 计算质数 -- 埃氏筛、线性筛解析

博客围绕leetcode计数质数题目,介绍了埃氏筛和线性筛两种算法。埃氏筛由希腊数学家提出,通过标记质数倍数为合数来计数,可从x⋅x开始标记优化;线性筛则优化了埃氏筛的冗余标记,让每个合数只被标记一次,时间复杂度为O(n)。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0 题目描述

leetcode原题链接:204. 计数质数
在这里插入图片描述

1 埃氏筛

很直观的思路是我们枚举每个数判断其是不是质数,枚举没有考虑到数与数的关联性,因此难以再继续优化时间复杂度。介绍一个常见的算法, 该算法由希腊数学家厄拉多塞(Eratosthenes ) 提出,称为厄拉多塞筛法,简称埃氏筛。
我们考虑这样一个事实:如果 x x x 是质数,那么大于 x x x x x x 的倍数 2 x , 3 x , … 2 x, 3 x, \ldots 2x,3x, 一定不是质数,因此我们可以从 这里入手。
我们设 i s Prime ⁡ [ i ] i s \operatorname{Prime}[i] isPrime[i] 表示数 i i i 是不是质数,如果是质数则为 1 1 1,否则为 0 0 0。 从小到大遍历每个数,如果这个数为质数,则将其所有的倍数都标记为合数(除了该质数本身),即 0 0 0,这样在运行结束的时候我们即能知道质数的个数。
这里还可以继续优化, 对于一个质数 x , x, x, 如果前面说的我们从 2 x 2 x 2x 开始标记其实是冗余的, 应该直接从 x ⋅ x x \cdot x xx 开始标记 , , , 因为 2 x , 3 x , … 2 x, 3 x, \ldots 2x,3x, 这些数一定在 x x x 之前就被其他数的倍数标记过了,例如 2 的所有倍数,3 的所有倍数等。
注意,0和1不是质数。

class Solution:
    # 埃氏筛
    def countPrimes(self, n: int) -> int:
        if n <= 0: return 0
        isPrime = [0]*2 + [1] *(n-2)
        res = 0
        for i in range(2, n, 1):
            if isPrime[i]:
                res += 1
                if i*i < n:
                    for j in range(i*i, n, i):
                        isPrime[j] = 0
        return res

复杂度分析
时间复杂度: O ( n log ⁡ n ) O(n\log n) O(nlogn)
空间复杂度: O ( n ) O(n) O(n),需要 O ( n ) O(n) O(n)的空间记录每个数是否为质数。

2 线性筛

埃氏筛其实还是存在冗余的标记操作,比如对于 15 这个数,它会同时被 3,5 两个数标记为合数,因此我们优化的目标是让每个合数只被标记一次,这样时间复杂度即能保证为 O ( n ) O(n) O(n),这就是接下来要介绍的线性筛。
相较于埃氏筛,需要多维护一个 p r i m e s pr i m e s primes 数组表示当前得到的质数集合。我们从小到大遍历,如果当前的数 x x x 是质数,就将其加入 p r i m e s primes primes数组。
另一点与埃氏筛不同的是, 「标记过程」不再仅当 x x x 为质数时才进行,而是对每个整数 x x x 都进行。对于整数 x x x,我们不再标记其所有的倍数 x ⋅ x , x ⋅ ( x + 1 ) , … , x \cdot x, x \cdot(x+1), \ldots, xx,x(x+1),, 而是只标记质数集合中的数与 x x x 相乘的数,即
primes 0 ∗ x , _{0} * x, 0x, primes 1 ∗ x , … , _{1} * x, \ldots, 1x,, 且在发现 x x x mod primes i = 0 _{i}=0 i=0 的时候结束当前标记。
核心点在于:如果 x x x 可以被 primes i i i 整除,那么对于合数 y = x ⋅ y=x \cdot y=x primes i + 1 i+1 i+1 而言,它一定在后面遍历到 x p r i m e s i ⋅ p r i m e s i + 1 \frac{x}{p r i m e s_{i}} \cdot p r i m e s_{i+1} primesixprimesi+1 这个数的时候会被标记,其他同理,这保证了每个合数只会被其「最小的质因数」筛去,即每个合数被标记一次。

class Solution:
    # 线性筛
    def countPrimes(self, n: int) -> int:
        if n <= 0: return 0
        isPrime = [0]*2 + [1] *(n-2)
        primes = []
        for i in range(2, n, 1):
            if isPrime[i]:
                primes.append(i)
            for _, prime in enumerate(primes):
                if i*prime >= n:
                    break
                isPrime[i*prime] = 0
                if i % prime == 0:
                    break
        return len(primes)

复杂度分析
时间复杂度: O ( n ) O(n) O(n),线性筛保证每个合数只被其最小质因子筛掉一次,所以理论上算法复杂度是 O ( n ) O(n) O(n)线性时间复杂度。
空间复杂度: O ( n ) O(n) O(n),需要 O ( n ) O(n) O(n)的空间记录每个数是否为质数。

参考资料

计算质数

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值