开始前,会这些就够了
- 认识整除、余数与数组下标;知道质数只有 1 和自身两个正因数。
- 能够用 for 和 break 逐步执行一个双层循环。
- 先读 复杂度与操作次数:区分最终结果正确与工作次数保证。
12=2×6,也等于3×4。筛质数时,从不同因子出发会反复划掉同一个合数;结果可能仍然正确,却重复做了工作。线性筛把每个合数交给一个唯一的组合处理。
它最容易抄错的地方只有一行:当当前质数能整除外层数字时,就退出内层循环。但先退出还是先标记,会把“少做重复工作”变成“漏掉合数”,甚至把4误认为质数。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- demo.py:完整可运行源码
- README.md:运行步骤与练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和运行验证记录
- SHA256SUMS.txt:文件摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B demo.py解压后,在含 demo.py 的目录打开终端。命令分别回车执行。额外拒绝负上界、布尔值、小数、字符串和超出教学上限的输入。预期输出来自这次运行;所有检查用显式 AssertionError,即使以 python -O 运行也不会关闭。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
看清每个合数到底由谁标记
正在准备实验控件…
上界包含 N;0 和 1 不算质数。两个错误模式分别演示重复标记和漏标。每格展示当前记录的因子与写入次数;正确模式下记录的是最小质因子;轨迹快照只用于教学,正文关闭观察回调时不保存整段历史。
给合数指定唯一负责人:最小质因子
质数是至少为2、只被1和自身整除的整数。0和1不属于质数。对于合数 q,取它最小的质因子 p,再令 i=q/p,就得到 q=i×p;p 和 i 都唯一。
例如12的最小质因子是2,所以应由 i=6、p=2 负责。虽然 i=4、p=3 也能算出12,但这不是它指定的那一组。目标不是让所有因子都来检查,而是只让负责人写入一次。
程序的 least[q] 保存 q 的最小质因子;质数自己的最小质因子就是自身。另存 marked[q] 只用于核对合数被写了几次,发现质数时给 least[i] 赋 i 不计作合数标记。
外层逐个往前走,内层只拿已经发现的质数
从 i=2 走到 N。若 least[i] 仍为0,说明没有更小数字生成过它,它就是新质数,把 i 加进按升序保存的 primes。随后依次取 primes 中的 p,准备标记 q=i×p。
若 q>N,内层直接结束,因为后面的 p 只会更大。否则写 least[q]=p,并把标记次数加1。最后检查 i%p==0;整除时结束本轮,开始下一个 i。N 是包含的上界,N=2 应当得到 [2]。
i=4 时先遇到 p=2,标记8,然后因为2整除4而停止,跳过 p=3 生成12的机会。等外层走到6,再由 p=2 标记12。这正是“只让最小质因子负责”的安排。
为什么不会漏掉,也不会由两组重复生成
先看不重复:内层处理到某个 p 时,在它之前没有更小的质数整除 i,否则早就退出。因此 p≤i 的最小质因子;q=i×p 的最小质因子一定是 p。每次写入都由正确负责人完成。
再看不遗漏:任取合数 q,令 p 是它的最小质因子,i=q/p。i 的质因子都不比 p 小,因此内层不会在 p 之前因整除而退出。又因为 i≥p,p 已经在质数清单里;循环最终会生成 q。
两点合起来:每个合数只对应 q/最小质因子 与该质因子这一组,所以恰好被标记一次。外层处理 N 个数,合数标记总共不到 N 次,算术操作按常数时间计时核心为 O(N)。Python 大整数的位成本与实际运行时间需要另算;本教学接口把 N 限制到100000。
删掉 break 会重复;把 break 提前会漏掉4
错误模式 no_break 仍使用质数清单,但不在整除后退出。N=30 时质数列表碰巧仍完全正确,12却被4×3和6×2写了两次;全部合数标记由19次增加到24次。因此只比最后的质数列表,抓不到这类性能性质退化。
另一个错误模式 early_break 把整除检查放在写入之前。i=2、p=2 时直接退出,没有标记4;外层到4时它仍是0,于是被加入质数清单。问题已经从重复工作变成错误分类。
这两个开关用于观察反例。正确顺序是先确认乘积在范围内,再标记,再在整除时停止;不能把“遇到整除就停”压缩成丢失顺序的口号。
保存最小质因子,还能反复除出完整因数
有了 least 表,分解12只需依次读 least[12]=2,除后得到6;再读 least[6]=2,得到3;最后读 least[3]=3,得到1。因此结果是 [2,2,3]。重复质因子必须保留,不能直接改成集合。
factor(1) 返回空列表,因为1没有质因子。输入必须在1到已经筛过的N之间,不能把表外的数直接交给它。每次至少除以2,所以分解一个 x 的循环次数不超过按二进制位数计的 O(log x)。
least、marked和质数清单都最多占O(N)空间。marked 是本实验的可观察检查表,业务只需要质数或最小质因子时可以省去它。更大范围是否该分段筛,需要根据内存和任务重新选择,本文未比较筛法实际速度。
用试除法核对,不用第二份筛法互相证明
保存完整代码为 demo.py,运行 python demo.py。本次已在 CPython 3.14.0 实跑,只用标准库;通过 Python 3.10 语法解析不代表在3.10实跑。
trial_least 从2到 isqrt(x) 逐个试除,找到第一个因子;若没有则 x 为质数。这与生成合数的筛法走不同路线。检查上界0到300,再加1000和10000,共303份范围、55848个整数,核对质数清单、最小因子、唯一标记次数及因子乘积。
额外拒绝负上界、布尔值、小数、字符串和超出教学上限的输入。预期输出来自这次运行;所有检查用显式 AssertionError,即使以 python -O 运行也不会关闭。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""Every composite has exactly one owner: its smallest prime times the rest."""
from math import isqrt, prod
def require(ok, message):
if not ok:
raise AssertionError(message)
def linear_sieve(limit, mode="correct", emit=None):
if type(limit) is not int or not 0 <= limit <= 100000:
raise ValueError("integer 0<=limit<=100000 required; bool excluded")
if mode not in ("correct", "no_break", "early_break"):
raise ValueError("unknown teaching mode")
least = [0] * (limit+1)
marked = [0] * (limit+1)
primes = []
for i in range(2,limit+1):
if least[i] == 0:
least[i] = i
primes.append(i)
if emit:
emit("prime", i, i, i, False)
for p in primes:
q = i*p
if q > limit:
break
divides = i % p == 0
if mode == "early_break" and divides:
if emit:
emit("skipped", i, p, q, True)
break
least[q] = p
marked[q] += 1
if emit:
emit("mark", i, p, q, mode == "correct" and divides)
if mode == "correct" and divides:
break
return primes, least, marked
def trial_least(value):
for divisor in range(2,isqrt(value)+1):
if value % divisor == 0:
return divisor
return value
def factor(value, least):
if type(value) is not int or not 1 <= value < len(least):
raise ValueError("value must be in 1..limit")
result = []
while value > 1:
p = least[value]
if p < 2 or value % p:
raise ValueError("invalid least-prime table")
result.append(p)
value //= p
return result
def main():
primes, least, marked = linear_sieve(30)
print(f"primes_to_30={primes}")
print(f"least[12]={least[12]}; factors[12]={factor(12,least)}; mark_count[12]={marked[12]}")
print(f"composite_marks={sum(marked)}; composite_count={29-len(primes)}")
redundant = linear_sieve(30,"no_break")
premature = linear_sieve(30,"early_break")
require(redundant[0] == primes and redundant[2][12] == 2, "remove-break counterexample")
require(4 in premature[0], "early-break counterexample")
print(f"without_break: primes_same={redundant[0]==primes}; marks[12]={redundant[2][12]}; total_marks={sum(redundant[2])}")
print(f"break_before_mark: wrongly_lists_4_as_prime={4 in premature[0]}")
limits = comparisons = 0
for limit in list(range(301)) + [1000,10000]:
primes, least, marked = linear_sieve(limit)
expected = [x for x in range(2,limit+1) if trial_least(x)==x]
require(primes == expected, "trial division prime list")
for x in range(2,limit+1):
want = trial_least(x)
require(least[x] == want, "smallest divisor")
require(marked[x] == int(want != x), "each composite exactly once")
factors = factor(x,least)
require(prod(factors) == x and all(trial_least(p)==p for p in factors), "factorization")
comparisons += 1
if limit >= 1:
require(factor(1,least) == [], "one has no prime factors")
limits += 1
for invalid in (-1, True, 3.5, "30",100001):
try:
linear_sieve(invalid)
except ValueError:
pass
else:
raise AssertionError("invalid input accepted")
print(f"limits_checked={limits}; number_checks={comparisons}; invalid_inputs=5")
print("PASS: trial divisors, prime lists, unique marks and factor products agree")
if __name__ == "__main__":
main()
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-24 使用 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查 复跑这份源码并核对完整输出。额外拒绝负上界、布尔值、小数、字符串和超出教学上限的输入。预期输出来自这次运行;所有检查用显式 AssertionError,即使以 python -O 运行也不会关闭。
运行后对照结果
primes_to_30=[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
least[12]=2; factors[12]=[2, 2, 3]; mark_count[12]=1
composite_marks=19; composite_count=19
without_break: primes_same=True; marks[12]=2; total_marks=24
break_before_mark: wrongly_lists_4_as_prime=True
limits_checked=303; number_checks=55848; invalid_inputs=5
PASS: trial divisors, prime lists, unique marks and factor products agree把负责人画出来,再增加自己的检查
练习不预先给答案:将上界换成42,挑一个含三个不同质因子的合数和一个质数平方。写下它们唯一应出现的 i、p,再运行正确模式观察。
接着分别打开两个错误模式,检查质数清单与标记次数。最后尝试在不保存 marked 数组的情况下累计写入总次数,并用“范围内整数个数减去质数个数”核对;明确0和1是否被排除。
记住这些要点
- 每个合数的最小质因子和剩余因子形成唯一一组负责人。
- 必须先标记 i×p,再在 p 整除 i 时退出。
- 正确质数列表不足以证明每个合数只处理一次。
- 试除法提供独立参考,最小质因子表还能用于因数分解。
别把例子用过头
- 把1列为质数或把N写成不包含的上界。
- 先执行整除 break,漏掉质数平方等合数。
- 只验证最终集合,忽略重复标记破坏线性工作量。
自己试一小步
上界42时,怎样同时检查结果正确和每个合数只有一个负责人?
- 手写两个选定合数的最小质因子。
- 记录正确与错误模式的标记来源。
- 增加总标记次数检查,并处理0、1、2边界。
做到这些,就算完成
- 每个标记来源满足乘积等于目标合数。
- 试除参考不读取 least 表。
- 练习不预填42范围的最终清单。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Linear Sieve:唯一最小质因子生成 (新标签页)↗
最小质因子唯一生成、先标记再整除退出,以及线性标记次数论证。
- Python math.isqrt 的整数平方根约定 (新标签页)↗
独立试除参考使用精确向下整数平方根,避免浮点开方边界误差。