筛素数我现在不手写埃氏了:bool-hybrid-array 里藏了个 BHA_lazy_sieve(用法速查)
起因:一个布尔数组库,更新日志里混进了"素数筛"
最近把 bool-hybrid-array 升到 9.13.11(pip install -U bool-hybrid-array),翻版本历史的时候注意到 9.13.3 悄悄加了个东西叫 BHA_lazy_sieve,9.13.4 又给它补了一堆成员函数。
一个做布尔混合数组的库,为什么要内置一个素数筛?我上手敲了半小时,结论是:它不是一个孤立的小玩具,而是把"筛素数"这件事,直接建模成了库本身最擅长的那张布尔表。 筛出来的结果也不是普通 list,而是能直接接进它家位运算、二维 BHA_List、序列化那一整套流程里的对象。
这篇先不展开性能,纯粹当一篇上手速查:把这个类怎么用、能干什么,按我实际敲出来的输出整理一遍。至于它到底适不适合替代你手里那版埃氏筛、什么场景该用什么场景别用,我打算放下一篇专门讲——这篇先把 API 摸熟。
仓库先放这儿:GitHub · Gitee,License 是 Apache-2.0 OR MulanPSL-2.0。下面所有输出都是本机 9.13.11 实跑。
一、它是什么:一个"惰性"的素数生成器
名字里的 lazy 是关键。它不是构造时就把某张表一口气全算完,而是你要一个、它算一个:
from bool_hybrid_array import BHA_lazy_sieve
s = BHA_lazy_sieve()
print([next(s) for _ in range(10)])
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
print(s.current) # 29 <- 当前停在哪一个素数
next() 驱动、current 看当前素数;想看已经筛出多少个,用 len(s) 或属性 s.discovered(下面 upto 的例子里能看到它和"≤N 的素数个数"对得上)。这种"边用边算"的模式,最贴合的是那种你事先不知道要用到第几个素数的场景——比如某个算法在循环里不断喊"给我下一个素数",喊停就停,不浪费。
二、速查表:八个常用动作
我把最常用的动作整理成一张表,下面逐条给出真实输出。
| 需求 | 写法 | 说明 |
|---|---|---|
| 构造 | BHA_lazy_sieve() |
空生成器,从零开始 |
| 逐个取 | next(s) |
惰性,要一个算一个 |
| 一次取 k 个 | s.take(k) |
返回前 k 个素数 |
| 取 ≤N 的全部 | s.upto(N) |
推进到 N,返回素数列表 |
| 数 ≤N 有几个 | BHA_lazy_sieve().count_le(N) |
只给计数 |
| 当前素数 | s.current |
属性,不是方法 |
| 已发现个数 | len(s) / s.discovered |
|
| 推倒重来 | s.reset() |
回到初始状态 |
逐个看输出。
一次取 k 个:
s2 = BHA_lazy_sieve()
print(s2.take(5))
# IntHybridArray([2, 3, 5, 7, 11])
注意返回类型是 IntHybridArray,不是 Python list——它是库里的整型混合数组,这点后面讲"为什么内建在这个库里"时很重要。
取 ≤ N 的所有素数:
u = BHA_lazy_sieve().upto(100)
print(len(u)) # 25
print(u[-1:]) # IntHybridArray([97])
100 以内 25 个素数,最后一个 97,数是对的。
只想要计数(π(N)):
print(BHA_lazy_sieve().count_le(1_000_000))
# 78498
一百万以内 78498 个素数,和标准的 π(10⁶) 一致。
reset 回到初始:
r = BHA_lazy_sieve()
r.upto(50)
print(len(r)) # 15
r.reset()
print(len(r)) # 1
筛到 50 是 15 个;reset() 之后计数归位(保留初始槽位),可以从头再来。
三、最让我意外的:它能 pickle,等于"筛素数进度"可以存盘
平时写埃氏筛,算到一半进程挂了,下次只能从头再来。这个生成器是可以直接序列化的:
import pickle
s3 = BHA_lazy_sieve()
s3.upto(200)
blob = pickle.dumps(s3) # 存起来
s4 = pickle.loads(blob) # 另一个进程/下次运行里读回来
print(len(s4)) # 46
print(s4.current) # 211
200 以内 46 个素数、当前停在 211——dump 再 load,进度完整接上了。 这意味着你可以把一张筛了一半的素数表,连同"筛到哪了"的游标一起落盘,冷启动直接续算,不必重跑前面全部。对那种需要长期、增量维护素数表的程序来说,这是个挺省心的能力。
(顺带说清楚:它身上没有单独的 .save/.load 方法,序列化走的是标准 pickle,我查过 hasattr(s3,'save') 是 False,别按库里面 BHA_List.save/load 那套去找。)
四、为什么一个布尔数组库要内置它:筛素数本来就是一张布尔表
想通这点之后,我觉得这个 API 放得其实很自然。
埃氏筛的核心数据结构是什么?是一张"某个数是不是还活着(没被划掉)"的布尔标记表:初始全 True,每确认一个素数,就把它的倍数批量标成 False。这跟 bool-hybrid-array 的看家本领——一张能在稠密位图和稀疏异常下标之间自动切换的布尔数组——本来就是同一种东西。
所以 BHA_lazy_sieve 内部维护的标记,用的就是库自己的布尔混合存储;筛出来的素数序列给你时是 IntHybridArray,跟库里的整型数组是一类对象。好处是下游不用做类型转换:
- 你可以把素数序列直接当成库里的混合数组,接它的存储优化;
- 配合二维
BHA_List,可以把多段筛的结果摞成一张二维结构逐行处理; - 它和库的其它对象共享同一套序列化思路,pickle / 落盘的心智模型是一致的。
换句话说,它的卖点不是"又一个素数算法实现",而是"素数"作为一种特殊的布尔序列,被纳入了这个库统一的数组抽象里。
五、什么情况下我会顺手用它
- 写算法时要"下一个素数"、事先不知道用到第几个——
next()惰性驱动,比自己维护全局筛状态干净; - 想要 ≤N 的素数表但不想每次都去抄一遍埃氏模板——
upto(N)一行; - 只想知道 π(N)——
count_le(N); - 需要增量、可中断、能落盘续算的素数表——pickle 存进度这招真的方便;
- 下游本来就在用 bool-hybrid-array 做布尔/整型数据处理,希望筛出来的东西零转换接进去。
内存上它也沿用了这库那套"稠密位图 + 稀疏异常段"的混合存储思路,不是朴素的每个数开一个 Python bool 对象的写法。
工具就先介绍到这儿。这篇我刻意没做横向跑分——因为我自己实测下来,BHA_lazy_sieve 和经典的 bytearray 埃氏筛、乃至 sympy 那种成熟实现,在不同用法下差距非常大,而且"惰性流式"和"一次性批量求全部"本来就是两种需求,单拎一个耗时数字出来容易误导。这个我下一篇专门用真实 benchmark 讲清楚:它快不快、省不省、到底该在什么时候用、什么时候老老实实回到经典埃氏。
本文基于 bool-hybrid-array 9.13.11 本机实测,命令与输出可复现;项目以 Apache-2.0 OR MulanPSL-2.0 双协议开源,requires-python >= 3.8。
浙公网安备 33010602011771号