1. 引言
计算素数(质数)是算法入门的经典问题,也是检验编程语言性能的试金石。当数据规模从“求100以内的素数”上升到“求1亿以内的素数”时,Python 新手写法的性能瓶颈会暴露无遗:内存爆炸、运行缓慢,甚至直接卡死。
本文将带你从最直观的试除法开始,逐步优化到埃拉托斯特尼筛法(埃氏筛)、欧拉筛(线性筛),最终引入一种内存极致优化的方案——bool-hybrid-array 库,让你在 1 亿范围内筛素数,内存占用降低 90% 以上,性能依然在线。
2. 新手写法:试除法
最直观的判断一个数 n 是否为素数的方法,就是检查它能否被 2 到 sqrt(n) 之间的整数整除。
import math
import time
def is_prime_trial(n):
if n < 2:
return False
for i in range(2, int(math.sqrt(n)) + 1):
if n % i == 0:
return False
return True
def primes_trial(limit):
"""试除法求素数列表"""
primes = []
for num in range(2, limit + 1):
if is_prime_trial(num):
primes.append(num)
return primes
# 测试:求 10 万以内的素数
start = time.time()
primes = primes_trial(100000)
end = time.time()
print(f"试除法(10万以内): {len(primes)} 个素数, 耗时 {end - start:.2f} 秒")
问题分析:
- 时间复杂度:约为 O(n * sqrt(n)),当 n 达到 1 亿时,计算量是天文数字,几乎不可完成。
- 空间复杂度:O(π(n)),只存储素数列表,内存尚可,但速度是致命短板。
3. 经典优化:埃拉托斯特尼筛法(埃氏筛)
埃氏筛的核心思想是:从 2 开始,将每个素数的倍数标记为合数。它用空间换时间,大幅提升了效率。
def sieve_of_eratosthenes(limit):
"""埃氏筛,返回布尔列表,is_prime[i] 表示 i 是否为素数"""
is_prime = [True] * (limit + 1)
is_prime[0] = is_prime[1] = False
for i in range(2, int(limit ** 0.5) + 1):
if is_prime[i]:
# 从 i*i 开始标记,避免重复标记
for j in range(i * i, limit + 1, i):
is_prime[j] = False
return is_prime
# 测试:求 1 亿以内的素数(仅演示思路,实际运行需注意内存)
# limit = 100_000_000
# is_prime = sieve_of_eratosthenes(limit)
# primes = [i for i, val in enumerate(is_prime) if val]
性能分析:
- 时间复杂度:O(n log log n),非常接近线性。
- 空间复杂度:O(n),使用 Python 的
list存储布尔值。这是内存瓶颈所在。一个 Python 的bool对象占用约 28 字节,1 亿个布尔值需要约 2.8 GB 内存,这还只是存储标记数组,还没算上最终的素数列表。
4. 进阶优化:欧拉筛(线性筛)
埃氏筛存在一个缺陷:一个合数可能被多个素数标记多次(例如 12 会被 2 和 3 各标记一次)。欧拉筛通过保证每个合数只被其最小质因子标记一次,实现了真正的线性时间复杂度。
def linear_sieve(limit):
"""欧拉筛(线性筛),返回素数列表"""
is_prime = [True] * (limit + 1)
primes = []
for i in range(2, limit + 1):
if is_prime[i]:
primes.append(i)
for p in primes:
if i * p > limit:
break
is_prime[i * p] = False
# 关键:当 i 能被 p 整除时,停止内层循环
if i % p == 0:
break
return primes
# 测试:求 1 亿以内的素数(同样注意内存)
# limit = 100_000_000
# primes = linear_sieve(limit)
# print(f"1亿以内素数个数: {len(primes)}")
性能分析:
- 时间复杂度:O(n),每个合数只被标记一次。
- 空间复杂度:依然是 O(n),
is_prime列表的内存问题没有解决。对于 1 亿的规模,Python 原生list的内存占用依然高达 2.8 GB。
5. 内存终极优化:bool-hybrid-array
从上面的分析可以看出,无论是埃氏筛还是欧拉筛,算法本身已经足够优秀,瓶颈在于 Python 布尔列表的内存占用。bool-hybrid-array 库正是为了解决这个问题而生。
5.1 核心原理
bool-hybrid-array 是一个专为布尔值优化的高性能数组库。它采用混合存储策略,自动在以下两种模式间切换,以达到内存和性能的最佳平衡:
- 位图模式 (Bitmap):对于随机分布的数据,使用 1 个 bit 存储 1 个布尔值,相比 Python 的 28 字节,内存直接降低 224 倍。
- 行程长度编码模式 (Run-Length Encoding, RLE):对于连续相同值(如素数筛中大量的
False),使用 RLE 压缩,只存储值和连续长度,内存占用极低。
库内部会自动分析数据特征,选择最优的存储方式,对用户完全透明。
5.2 性能对比(1亿规模素数筛)
| 指标 | Python list (bool) | numpy.bool_ | bool-hybrid-array |
|---|---|---|---|
| 1亿布尔值内存占用 | ~2.8 GB | ~100 MB | ~10 MB |
| 随机访问速度 | 基准 | 快 | 接近原生 list |
| 序列化到文件速度 | 慢 | 中等 | 快 |
数据说明:以上数据基于官方 README 及社区测试。
bool-hybrid-array在素数筛这种“大量连续False+ 少量True”的场景下,RLE 压缩效果极佳,内存节省效果远超普通位图。
5.3 如何使用
bool-hybrid-array 的 API 完全兼容 Python 原生 list,学习成本为零。
# 1. 安装
# pip install bool-hybrid-array
from bool_hybrid_array import BoolHybridArray
def sieve_with_bool_hybrid(limit):
"""使用 BoolHybridArray 进行埃氏筛"""
# 直接替换 list,用法完全一致
is_prime = BoolHybridArray([True] * (limit + 1))
is_prime[0] = False
is_prime[1] = False
for i in range(2, int(limit ** 0.5) + 1):
if is_prime[i]:
for j in range(i * i, limit + 1, i):
is_prime[j] = False
# 收集素数
primes = [i for i in range(limit + 1) if is_prime[i]]
return primes
# 测试:求 1 亿以内的素数
import time
limit = 100_000_000
start = time.time()
primes = sieve_with_bool_hybrid(limit)
end = time.time()
print(f"1亿以内素数个数: {len(primes)}")
print(f"耗时: {end - start:.2f} 秒")
# 内存占用将远低于原生 list 方案
5.4 更多特性
- 零拷贝序列化:支持快速将数组序列化到磁盘或从磁盘加载,适合缓存计算结果。
- 内存池预分配:减少频繁的内存分配和释放开销。
- MIT 协议:完全开源,可自由用于商业项目。
6. 总结与建议
从试除法到欧拉筛,再到 bool-hybrid-array,我们一步步见证了 Python 素数筛的性能进化之路。
- 小规模数据(< 100万):使用埃氏筛或欧拉筛,Python 原生
list足以应对。 - 大规模数据(> 1000万):强烈推荐使用
bool-hybrid-array替代原生list作为标记数组。它能在不改变算法逻辑的前提下,将内存占用降低一个数量级,让 1 亿以内的素数筛在普通 PC 上也能轻松运行。
这个优化思路不仅适用于素数筛,对于任何需要存储海量布尔标记的场景(如布隆过滤器、图遍历标记、大数据去重等)都具有极高的参考价值。
7. 常见问题 (FAQ)
Q1: bool-hybrid-array 和 bitarray 库有什么区别?
A1: bitarray 是纯位图实现,所有数据都按位存储。bool-hybrid-array 采用混合存储,能自动识别连续重复值并使用 RLE 压缩,在数据存在大量连续相同值时(如素数筛),内存效率更高。
Q2: 使用 bool-hybrid-array 后,算法速度会变慢吗?
A2: 在素数筛这类场景下,由于内存占用大幅降低,CPU 缓存命中率提高,整体速度通常与原生 list 相当甚至更快。具体性能取决于数据分布。
Q3: 如何安装 bool-hybrid-array?
A3: 直接使用 pip 安装即可:pip install bool-hybrid-array。项目完全开源,代码托管在 Gitee 上。
Q4: 这个库支持 Python 3 的哪个版本?
A4: 支持 Python 3.8 及以上版本。
Q5: 除了素数筛,这个库还能用在哪些地方?
A5: 任何需要高效存储和访问大量布尔值的场景都适用,例如:机器学习中的特征掩码、大数据 ETL 中的状态标记、高性能缓存中的存在性判断等。

960

被折叠的 条评论
为什么被折叠?



