筛素数我现在不手写埃氏了: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。

posted @ 2026-10-25 08:19  贝壳bkshell  阅读(3)  评论(0)    收藏  举报