Python素数筛性能优化:1亿内素数内存省90%

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 是一个专为布尔值优化的高性能数组库。它采用混合存储策略,自动在以下两种模式间切换,以达到内存和性能的最佳平衡:

  1. 位图模式 (Bitmap):对于随机分布的数据,使用 1 个 bit 存储 1 个布尔值,相比 Python 的 28 字节,内存直接降低 224 倍。
  2. 行程长度编码模式 (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-arraybitarray 库有什么区别?

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 中的状态标记、高性能缓存中的存在性判断等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值