Java素数筛法:BitSieve类的精妙设计
这是一个设计非常精巧的内部工具类,虽然代码量不大,但包含了经典的算法思想和多重优化。
BitSieve
是一个用于在指定范围内高效查找大素数候选者的工具类。它的核心是实现了埃拉托斯特尼筛法(Sieve of Eratosthenes)的一种优化变体。
它的访问权限是包级私有(final class BitSieve
),这意味着它是一个内部辅助类,专门服务于 java.math
包中的其他类,特别是 java.math.BigInteger
。
核心设计与优化
BitSieve
的实现中包含了几个关键的优化点,极大地提升了筛选效率和空间利用率。
-
空间优化:只筛奇数 这是
BitSieve
最重要的一个优化。我们知道,除了2以外,所有素数都是奇数。因此,这个筛子完全忽略了偶数,只表示奇数。这直接将需要处理的数字量和存储空间减少了一半。类注释中给出了数字和索引的转换公式:
N = offset + (2*index + 1)
N
是筛子中某个位所代表的实际整数。offset
是一个偶数,代表筛子开始范围的基数。index
是该位在bits
数组中的索引。
-
紧凑存储:位数组(Bit Array) 为了最大化空间效率,它没有用一个
boolean
数组,而是用了一个long[] bits
数组。long
类型有64位,所以数组中的每一个long
元素可以表示64个不同的奇数是否为合数。set(int bitIndex)
: 将bitIndex
对应的位设置为1,表示这个数是合数,将它从素数候选者中“筛掉”。get(int bitIndex)
: 检查bitIndex
对应的位是否为1。unitIndex(int bitIndex)
和bit(int bitIndex)
: 这两个私有方法是实现位操作的关键。unitIndex
用来定位bitIndex
属于bits
数组中的哪个long
元素 (bitIndex >>> 6
);bit
用来生成一个掩码,定位到该long
元素中的具体哪一位 (1L << (bitIndex & 63)
)。
构造过程详解
BitSieve
有两个构造函数,它们协同工作,构成了两级筛选策略。
-
private BitSieve()
— 静态小筛子 这个私有构造函数只在类加载时被调用一次,用于创建一个静态的“小筛子”:private static final BitSieve smallSieve
。- 目的:这个小筛子的作用是预先计算并存储一批较小的素数(根据代码
length = 150 * 64
,它能处理到2 * (150*64) + 1 = 19201
范围内的奇数)。 - 构建过程:它通过自我筛选的方式构建。在一个
do-while
循环中,不断调用sieveSearch
找到下一个素数(即值仍为0的位),然后调用sieveSingle
将该素数的所有倍数从筛子中划掉(置为1)。 - 作用:这个
smallSieve
就像一个“素数模板库”,为构造更大范围的筛子提供了基础数据,避免了重复计算。
- 目的:这个小筛子的作用是预先计算并存储一批较小的素数(根据代码
-
BitSieve(BigInteger base, int searchLen)
— 主筛子 这是供BigInteger
调用的主要构造函数,用于在任意大的数base
之后,长度为searchLen
的范围内寻找素数。- 目的:构建一个代表
[base, base + 2*searchLen]
范围内奇数的筛子。 - 构建过程:它巧妙地利用了静态的
smallSieve
来加速筛选过程。- 它遍历
smallSieve
中所有的小素数(通过smallSieve.sieveSearch
)。 - 对于每一个小素数
p
(代码中是convertedStep
),它需要计算出在这个新的大筛子范围内,p
的第一个倍数落在哪里。这个计算是通过start = b.divideOneWord(convertedStep, q)
完成的,这里b
是base
的可变版本,该操作实际上是取模base % p
。 - 确定了起始位置后,调用
sieveSingle
,将这个大筛子中所有p
的倍数对应的位都置为1。
- 它遍历
- 效率:通过这种方式,它无需从头开始筛选,而是直接用已知的小素数作为“筛子”,快速地排除了目标范围内绝大多数的合数。
- 目的:构建一个代表
BitSieve()
这个构造函数的目标是创建一个“小型筛”(small sieve)。它本质上是使用埃拉托斯特尼筛法(Sieve of Eratosthenes)预先计算并存储一张小范围内的素数表。这张表随后会被用于更大数据范围的素数测试,以快速排除那些能被小素数整除的合数,从而提高效率。
BitSieve()
构造函数总览
// ... existing code ...private BitSieve() {length = 150 * 64;bits = new long[(unitIndex(length - 1) + 1)];// Mark 1 as compositeset(0);int nextIndex = 1;int nextPrime = 3;// Find primes and remove their multiples from sievedo {sieveSingle(length, nextIndex + nextPrime, nextPrime);nextIndex = sieveSearch(length, nextIndex + 1);nextPrime = 2*nextIndex + 1;} while((nextIndex > 0) && (nextPrime < length));}
// ... existing code ...
执行流程分析:
-
初始化:
length = 150 * 64;
: 定义筛的“长度”,即它能表示多少个奇数。这里是 9600 个。bits = new long[(unitIndex(length - 1) + 1)];
: 创建一个long
数组作为位存储。每个long
可以存储 64 个位。数组的大小通过调用unitIndex
计算得出,确保能容纳length
个位。
-
标记非素数:
set(0);
: 将索引为 0 的位设置为 1(标记为“非素数”)。根据类的设计,筛只存储奇数,索引i
代表数字2*i + 1
。因此,索引 0 代表数字 1,它不是素数。
-
筛选循环:
- 从第一个奇素数 3 (
nextPrime = 3
, 对应的索引nextIndex = 1
) 开始。 sieveSingle(...)
: 调用此方法将当前素数nextPrime
的所有倍数从筛中划去(即将对应的位设置为 1)。sieveSearch(...)
: 调用此方法寻找下一个未被划去的位,该位即代表下一个素数。nextPrime = 2*nextIndex + 1;
: 根据新找到的索引计算出下一个素数的值。- 循环不断进行,直到找不到新的素数或素数大小超出筛的范围。
- 从第一个奇素数 3 (
为什么从 nextIndex + nextPrime
下标开始?
这是这段代码最巧妙的地方。这个表达式是为了计算出第一个需要被划掉的数(也就是 3 * nextPrime
)在筛子里的索引。
我们来验证一下:
- 目标:对于一个素数
p
(nextPrime
),我们从它的第一个奇数倍数3p
开始筛选(因为1p
就是p
自己,是素数不能划掉;而2p
是偶数,不在我们的筛子里)。 - 目标索引:根据公式,
3p
这个数对应的索引应该是(3p - 1) / 2
。 - 代码中的
start
:代码中计算的起始索引是start = nextIndex + nextPrime
。 - 两者是否相等? 让我们来推导一下。
- 我们知道
p = nextPrime
= 2 * nextIndex + 1
。 - 代码的计算结果:
nextIndex + nextPrime
=nextIndex + p
=nextIndex + (2 * nextIndex + 1)
=3 * nextIndex + 1
。 - 理论的索引值:
(3p - 1) / 2
=(3 * (2 * nextIndex + 1) - 1) / 2
=(6 * nextIndex + 3 - 1) / 2
=(6 * nextIndex + 2) / 2
=3 * nextIndex + 1
。
- 我们知道
两者完全相等!
这说明 nextIndex + nextPrime
是一种非常高效且聪明的计算 3p
对应索引的方式,避免了乘法和除法,直接用加法就得到了结果。
补充说明: 标准的埃氏筛法通常从 p*p
开始筛选,因为小于 p*p
的合数一定会被更小的素数筛掉。这里的实现从 3p
开始,对于 p=3
的情况,3p
就是 p*p
,是最优的。对于 p>3
的情况,3p
会比 p*p
小,可能会做一些重复的工作(比如用 p=5
去筛 15
,但 15
已经被 p=3
筛过了),但因为计算起始点非常快,对于构建这个 smallSieve
来说,性能影响可以忽略不计,代码也更简洁。
接下来,我们递归分析它所调用的子函数。
sieveSearch()
: 寻找下一个素数
此方法在筛中线性搜索下一个素数候选。
// ... existing code ...private int sieveSearch(int limit, int start) {if (start >= limit)return -1;int index = start;do {if (!get(index))return index;index++;} while(index < limit-1);return -1;}
// ... existing code ...
- 从
start
索引开始,它遍历筛中的每个位。 if (!get(index))
: 它调用get()
检查位。如果get()
返回false
,意味着该位是 0,代表一个素数,于是立即返回当前索引index
。- 如果搜索到末尾仍未找到,返回 -1。
sieveSingle()
: 划掉倍数
这是执行“筛选”操作的核心,用于划掉一个素数的所有倍数。
// ... existing code ...private void sieveSingle(int limit, int start, int step) {while(start < limit) {set(start);start += step;}}
// ... existing code ...
start
: 开始筛选的索引。在BitSieve()
构造函数中,这个值被巧妙地设置为素数的平方对应的索引,这是一个常见的优化,因为小于该素数平方的合数已经被更小的素数筛掉了。step
: 步长,即当前正在处理的素数nextPrime
。- 循环不断地调用
set(start)
将是step
倍数的数对应的位标记为 1,直到超出筛的范围limit
。
start += step 是这个算法的关键一步。这里的 step 就是当前用来筛选的素数 nextPrime(我们称之为 p)。
- 筛选目标:我们的目标是划掉所有 p 的奇数倍数,例如 3p, 5p, 7p, ...
- 数值的差:这些连续的奇数倍数之间相差多少呢?(5p - 3p) = 2p,(7p - 5p) = 2p。它们之间的差值总是 2p。
- 索引的差:那么它们对应的索引相差多少呢?
- 3p 的索引是 i_3 = (3p - 1) / 2
- 5p 的索引是 i_5 = (5p - 1) / 2
- 索引的差是 i_5 - i_3 = ((5p - 1) / 2) - ((3p - 1) / 2) = (5p - 1 - 3p + 1) / 2 = 2p / 2 = p。
奇数倍数在数值上相差 2p,但它们在筛子里的索引正好相差 p。
在 sieveSingle 方法中,step 参数就是素数 p (nextPrime)。因此,start += step 正是让索引从一个奇数倍数精确地跳到下一个奇数倍数的位置。
BitSieve()
构造函数通过一系列精心设计的位操作和算法,高效地构建了一个素数筛。它首先初始化数据结构,然后通过一个主循环,不断地“搜索”下一个素数 (sieveSearch
),并“筛选”掉它的所有倍数 (sieveSingle
)。整个过程是全自动的,最终生成一个静态的、可复用的 smallSieve
对象,为 java.math
包中大整数的素性测试提供了关键的底层支持。
BitSieve(BigInteger base, int searchLen)
这个构造函数是 BitSieve
类的核心功能之一。它的主要目标是为一个大数范围(从 base
开始,长度为 searchLen
个奇数)创建一个素数筛。它利用静态成员 smallSieve
(之前分析过,它是一个预先计算好的小素数表)来高效地完成这个任务。
构造函数总览
// ... existing code ...BitSieve(BigInteger base, int searchLen) {/** Candidates are indicated by clear bits in the sieve. As a candidates* nonprimality is calculated, a bit is set in the sieve to eliminate* it. To reduce storage space and increase efficiency, no even numbers* are represented in the sieve (each bit in the sieve represents an* odd number).*/bits = new long[(unitIndex(searchLen-1) + 1)];length = searchLen;int start = 0;int step = smallSieve.sieveSearch(smallSieve.length, start);int convertedStep = (step *2) + 1;// Construct the large sieve at an even offset specified by baseMutableBigInteger b = new MutableBigInteger(base);MutableBigInteger q = new MutableBigInteger();do {// Calculate base mod convertedStepstart = b.divideOneWord(convertedStep, q);// Take each multiple of step out of sievestart = convertedStep - start;if (start%2 == 0)start += convertedStep;sieveSingle(searchLen, (start-1)/2, convertedStep);// Find next prime from small sievestep = smallSieve.sieveSearch(smallSieve.length, step+1);convertedStep = (step *2) + 1;} while (step > 0);}
// ... existing code ...
执行流程分析:
-
初始化:
bits = new long[...]
和length = searchLen
: 根据传入的searchLen
初始化筛子的大小。这个筛子将表示从base
开始的searchLen
个连续的奇数。int step = smallSieve.sieveSearch(...)
: 从smallSieve
中获取第一个素数。sieveSearch
返回的是索引,这里start=0
,所以它会返回第一个素数3
的索引,即1
。int convertedStep = (step * 2) + 1
: 将索引step
转换回它所代表的素数值。例如,索引1
变为(1*2)+1 = 3
。
-
主筛选循环 (
do-while
): 这个循环遍历smallSieve
中的每一个小素数,用它们来筛选我们正在构建的大筛子。- 计算起始点: 这是最关键和最复杂的部分。对于每个小素数
p
(convertedStep
),我们需要找到大于等于base
的第一个p
的奇数倍数,并计算出它在这个新筛子中的索引。start = b.divideOneWord(convertedStep, q)
: 计算base % p
,结果存入start
。这里使用MutableBigInteger
的divideOneWord
方法是为了性能,因为它比完整的BigInteger
除法快得多。start = convertedStep - start
: 计算从base
到下一个p
的倍数的偏移量。例如,如果base=100
,p=7
,则100 % 7 = 2
。下一个7的倍数是100 + (7-2) = 105
。这个偏移量start
就是5
。if (start % 2 == 0) start += convertedStep
: 我们的筛子只包含奇数。base
必须是偶数。如果计算出的偏移量start
是偶数,那么base + start
也是偶数,这不是我们想要的。所以我们需要下一个倍数,即再增加一个p
(convertedStep
),这样得到的base + start + p
就一定是奇数了。
- 执行筛选:
sieveSingle(searchLen, (start-1)/2, convertedStep)
: 调用sieveSingle
来划掉所有倍数。searchLen
: 筛选范围。(start-1)/2
: 这是起始索引。我们找到的第一个奇数倍数距离base
的偏移量是start
。根据公式N = offset + (2*index + 1)
,这里的offset
是base
,N
是base + start
。所以base + start = base + (2*index + 1)
,解得index = (start - 1) / 2
。convertedStep
: 这是步长,也就是素数p
本身。正如我们之前分析的,数值上相差2p
的奇数倍数,在索引上正好相差p
。
- 获取下一个素数:
step = smallSieve.sieveSearch(smallSieve.length, step+1)
: 在smallSieve
中寻找下一个素数的索引。convertedStep = (step * 2) + 1
: 将新索引转换为素数值。
- 循环直到
smallSieve
中所有的素数都被用完 (step > 0
)。
- 计算起始点: 这是最关键和最复杂的部分。对于每个小素数
这里的核心是巧妙地利用 smallSieve
预计算的素数,通过高效的模运算和索引转换,为任意大数 base
快速构建一个有效的素数筛,从而极大地加速了 BigInteger
寻找下一个素数的过程。
unitIndex()
& bit()
: 定位和生成位掩码
这两个静态方法是进行位操作的基础。
// ... existing code ...private static int unitIndex(int bitIndex) {return bitIndex >>> 6;}private static long bit(int bitIndex) {return 1L << (bitIndex & ((1<<6) - 1));}
// ... existing code ...
unitIndex(int bitIndex)
: 计算给定的bitIndex
位于bits
数组中的哪个long
元素内。因为一个long
是 64 位 (2^6),所以这里用无符号右移 6 位(等效于整除 64)来快速计算数组下标。bit(int bitIndex)
: 为给定的bitIndex
生成一个位掩码。bitIndex & 63
计算出位在long
元素内部的偏移量(0-63),然后1L
左移相应位数,得到一个只有该位为 1 的long
值。
set()
& get()
: 读写位
这两个方法利用上面的辅助函数来修改和查询筛中的特定位。
// ... existing code ...private boolean get(int bitIndex) {int unitIndex = unitIndex(bitIndex);return ((bits[unitIndex] & bit(bitIndex)) != 0);}private void set(int bitIndex) {int unitIndex = unitIndex(bitIndex);bits[unitIndex] |= bit(bitIndex);}
// ... existing code ...
set(int bitIndex)
: 将指定索引的位设置为 1。它首先定位到正确的long
元素,然后通过按位或|=
操作将掩码合并进去,从而将目标位置1,表示对应的数是合数。get(int bitIndex)
: 检查指定索引的位是否为 1。通过按位与&
操作,如果结果不为 0,则该位是 1(已被set
),返回true
。
retrieve
这个函数是 BitSieve
完成筛选工作后的“收获”阶段。它的核心任务是:从已经筛选过的位数组(bits
)中,找出第一个通过了严格素性测试的候选数,并将其作为结果返回。
BitSieve(BigInteger base, int searchLen) 这个构造函数在进行筛选时,它所使用的“武器”是 smallSieve,也就是一个预先计算好的、范围有限的小素数表。它只会用这些小素数(例如 3, 5, 7, 11, ... 直到几千)去划掉大数范围内的它们的倍数。
但是无法排除所有合数: 一个合数完全可以由两个非常大的素数相乘得到。
- 举个例子:假设我们正在一个很大的数(比如 10^50 附近)的范围内寻找素数。数字 P = p1 * p2,其中 p1 和 p2 都是非常大的素数(比如都是 25 位的素数)。
- P 显然是一个合数。
- 但是,p1 和 p2 自身都远远超出了 smallSieve 的范围。
- 因此,BitSieve 在筛选时,根本没有能力用 p1 或 p2 去划掉 P。
- 结果就是,在筛选结束后,P 对应的位在 bits 数组中仍然是 0,它成功“幸存”了下来,成为了一个素数候选者。
- retrieve 函数的“精选”作用: 正是因为存在上面说的情况,BitSieve 筛选后留下的“候选者”中,混杂了两种数:
- 真正的素数。
- 由大素数相乘构成的“伪装者”(合数)。
// ... existing code .../*** Test probable primes in the sieve and return successful candidates.*/BigInteger retrieve(BigInteger initValue, int certainty, java.util.Random random) {// Examine the sieve one long at a time to find possible primesint offset = 1;for (int i=0; i<bits.length; i++) {long nextLong = ~bits[i];for (int j=0; j<64; j++) {if ((nextLong & 1) == 1) {BigInteger candidate = initValue.add(BigInteger.valueOf(offset));if (candidate.primeToCertainty(certainty, random))return candidate;}nextLong >>>= 1;offset+=2;}}return null;}
}
执行流程分析:
-
初始化:
int offset = 1;
:offset
变量用于计算候选数的值。筛子中的第一个位(索引0)代表的奇数是initValue + 1
,第二个位(索引1)代表initValue + 3
,以此类推。这个offset
就代表了从initValue
开始的奇数增量(1, 3, 5, ...)。
-
外层循环: 遍历
bits
数组,该数组由多个long
类型的值组成,每个long
存储了64个位的信息。 -
寻找候选:
long nextLong = ~bits[i];
- 在筛子中,位为
1
表示对应的数是合数,位为0
表示是素数候选者。 - 通过按位取反操作
~
,nextLong
中的位为1
就直接代表了这是一个素数候选者,而位为0
代表是合数。这样做让后续的判断更直接。
- 在筛子中,位为
-
内层循环: 检查当前
long
(nextLong
) 中的全部64个位。 -
检查候选者:
if ((nextLong & 1) == 1)
- 通过
& 1
操作检查nextLong
的最低位。如果最低位是1
,说明我们找到了一个素数候选者。
- 通过
-
构建并测试候选数:
BigInteger candidate = initValue.add(BigInteger.valueOf(offset));
: 如果找到了候选者,就用初始基数initValue
加上当前的奇数偏移量offset
,从而构造出完整的候选数candidate
。if (candidate.primeToCertainty(certainty, random))
: 这是最关键的一步。BitSieve
只是排除了能被小素数整除的合数。一个数通过了筛选,不代表它一定是素数(例如,它可能是两个非常大的素数的乘积)。因此,这里必须调用BigInteger
的primeToCertainty
方法(通常是米勒-拉宾素性检验)进行一次严格的概率性素性测试。return candidate;
: 一旦有候选数通过了严格测试,它就被认为是我们要找的素数,函数立即返回这个结果。
-
更新状态:
nextLong >>>= 1;
: 使用无符号右移操作,将nextLong
的下一位移动到最低位,准备内层循环的下一次迭代。offset+=2;
: 因为筛子只表示奇数,所以每检查一个位,对应的数值就增加2。
-
无结果返回:
return null;
- 如果遍历完整个筛子,都没有找到任何一个能通过
primeToCertainty
测试的数,函数返回null
。
- 如果遍历完整个筛子,都没有找到任何一个能通过
总结
BitSieve
是 Java 中一个教科书级别的算法实现范例。它通过以下方式实现了高效的素数筛选:
- 算法层面:采用经典的埃拉托斯特尼筛法。
- 空间优化:只处理奇数,并使用位数组进行紧凑存储。
- 时间优化:采用“小筛子预计算”和“大筛子模板化筛选”的两级策略,避免了对每个大范围都从头计算,极大地提升了效率。
它是 BigInteger
能够快速生成大素数(这在密码学等领域至关重要)的幕后功臣。