素数筛
一、基本思想
通过标记非素数(即合数),从而得到所有素数。
二、算法步骤
-
初始化:
- 创建一个布尔型列表
sieve
,表示从 0 到n
每个数是否是素数。初始时,我们假设所有数都是素数,因此将列表中所有的元素都设置为True
,但sieve[0]
和sieve[1]
设为False
,因为 0 和 1 不是素数。
- 创建一个布尔型列表
-
筛选过程:
- 从 2 开始,遍历到
sqrt(n)
,⭐因为如果一个合数a
可以被分解成两个因数b
和c
,则必定存在b
或c
小于等于sqrt(a)
。因此,我们只需要检查小于等于sqrt(n)
的数,来筛选出所有的合数。 - 对于每一个被标记为素数的数
i
,我们将i
的倍数(即从i*i
开始的所有i
的倍数)标记为合数,即将sieve[j]
设置为False
。
- 从 2 开始,遍历到
-
收集素数:
- 遍历
sieve
列表,将其中值为True
的索引(即素数)收集到一个新列表primes
中。
- 遍历
⭐为什么从
i * i
开始标记?
-
标记合数的过程: 在算法中,
i
是当前的素数。如果i
是素数,那么我们从i * i
开始标记,因为:- 小于
i
的倍数(即2i
,3i
, …)已经在之前的素数处理中标记过了。 - 从
i * i
开始标记,可以确保我们不会重复标记已经处理过的合数,而且可以避免不必要的操作。
- 小于
-
避免重复计算:
- 如果
i
是素数,那么i
的倍数2i, 3i, 4i, ...
已经在之前的步骤中被标记过了(因为这些倍数的因数已经小于i
,且我们在处理更小的素数时已经将它们标记为合数)。 - 因此,开始从
i * i
标记是为了跳过那些已经被处理过的倍数,避免重复操作,从而提高效率。
- 如果
三、例子:
假设我们计算素数 n = 30
,我们要找出小于等于 30 的所有素数。
- 从
i = 2
开始,2
是素数,标记2
的倍数:4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30
。 - 然后
i = 3
,3
也是素数,标记3
的倍数:9, 12, 15, 18, 21, 24, 27, 30
。 - 接下来,
i = 4
,sieve[4]
已经是False
(在处理2
时就已经标记过了),跳过。 - 然后
i = 5
,5
是素数,标记5
的倍数:25, 30
。- 注意到
5 * 5 = 25
,这是第一次标记5
的倍数,而更小的倍数(如5 * 2 = 10
)已经在前面标记过了。
- 注意到
通过这种方式,我们避免了重复的计算,提升了效率。
四、核心代码
def sieve_of_eratosthenes(n):
# 初始化布尔数组,假设所有数都是素数
sieve = [True] * (n + 1) # 创建大小为 n+1 的布尔数组
sieve[0] = sieve[1] = False # 0 和 1 不是素数
# 从 2 开始筛选
for i in range(2, int(n ** 0.5) + 1): # 只需到 sqrt(n)⭐转换为int类型
if sieve[i]: # 如果 i 是素数
for j in range(i * i, n + 1, i): # 将 i 的倍数标记为合数
sieve[j] = False
# 返回所有值为 True 的索引,这些索引对应的数字是素数
primes = [i for i in range(2, n + 1) if sieve[i]]
return primes
代码执行
print(sieve_of_eratosthenes(30))
输出:
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
PS:留意标注⭐的地方