这是一个设计非常精巧的内部工具类,虽然代码量不大,但包含了经典的算法思想和多重优化。
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 composite
set(0);
int nextIndex = 1;
int nextPrime = 3;
// Find primes and remove their multiples from sieve
do {
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,它不是素数。筛选循环:
nextPrime = 3
, 对应的索引 nextIndex = 1
) 开始。sieveSingle(...)
: 调用此方法将当前素数 nextPrime
的所有倍数从筛中划去(即将对应的位设置为 1)。sieveSearch(...)
: 调用此方法寻找下一个未被划去的位,该位即代表下一个素数。nextPrime = 2*nextIndex + 1;
: 根据新找到的索引计算出下一个素数的值。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
。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)。
奇数倍数在数值上相差 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 base
MutableBigInteger b = new MutableBigInteger(base);
MutableBigInteger q = new MutableBigInteger();
do {
// Calculate base mod convertedStep
start = b.divideOneWord(convertedStep, q);
// Take each multiple of step out of sieve
start = convertedStep - start;
if (start%2 == 0)
start += convertedStep;
sieveSingle(searchLen, (start-1)/2, convertedStep);
// Find next prime from small sieve
step = 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, ... 直到几千)去划掉大数范围内的它们的倍数。
但是无法排除所有合数: 一个合数完全可以由两个非常大的素数相乘得到。
// ... 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 primes
int offset = 1;
for (int i=0; i>>= 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
能够快速生成大素数(这在密码学等领域至关重要)的幕后功臣。