当前位置: 首页 > news >正文

Java素数筛法:BitSieve类的精妙设计

这是一个设计非常精巧的内部工具类,虽然代码量不大,但包含了经典的算法思想和多重优化。

BitSieve 是一个用于在指定范围内高效查找大素数候选者的工具类。它的核心是实现了埃拉托斯特尼筛法(Sieve of Eratosthenes)的一种优化变体。

它的访问权限是包级私有(final class BitSieve),这意味着它是一个内部辅助类,专门服务于 java.math 包中的其他类,特别是 java.math.BigInteger

核心设计与优化

BitSieve 的实现中包含了几个关键的优化点,极大地提升了筛选效率和空间利用率。

  1. 空间优化:只筛奇数 这是 BitSieve 最重要的一个优化。我们知道,除了2以外,所有素数都是奇数。因此,这个筛子完全忽略了偶数,只表示奇数。这直接将需要处理的数字量和存储空间减少了一半。

    类注释中给出了数字和索引的转换公式: N = offset + (2*index + 1)

    • N 是筛子中某个位所代表的实际整数。
    • offset 是一个偶数,代表筛子开始范围的基数。
    • index 是该位在 bits 数组中的索引。
  2. 紧凑存储:位数组(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 有两个构造函数,它们协同工作,构成了两级筛选策略。

  1. private BitSieve() — 静态小筛子 这个私有构造函数只在类加载时被调用一次,用于创建一个静态的“小筛子”:private static final BitSieve smallSieve

    • 目的:这个小筛子的作用是预先计算并存储一批较小的素数(根据代码 length = 150 * 64,它能处理到 2 * (150*64) + 1 = 19201 范围内的奇数)。
    • 构建过程:它通过自我筛选的方式构建。在一个 do-while 循环中,不断调用 sieveSearch 找到下一个素数(即值仍为0的位),然后调用 sieveSingle 将该素数的所有倍数从筛子中划掉(置为1)。
    • 作用:这个 smallSieve 就像一个“素数模板库”,为构造更大范围的筛子提供了基础数据,避免了重复计算。
  2. BitSieve(BigInteger base, int searchLen) — 主筛子 这是供 BigInteger 调用的主要构造函数,用于在任意大的数 base 之后,长度为 searchLen 的范围内寻找素数。

    • 目的:构建一个代表 [base, base + 2*searchLen] 范围内奇数的筛子。
    • 构建过程:它巧妙地利用了静态的 smallSieve 来加速筛选过程。
      1. 它遍历 smallSieve 中所有的小素数(通过 smallSieve.sieveSearch)。
      2. 对于每一个小素数 p(代码中是 convertedStep),它需要计算出在这个新的大筛子范围内,p 的第一个倍数落在哪里。这个计算是通过 start = b.divideOneWord(convertedStep, q) 完成的,这里 b 是 base 的可变版本,该操作实际上是取模 base % p
      3. 确定了起始位置后,调用 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 ...

执行流程分析:

  1. 初始化:

    • length = 150 * 64;: 定义筛的“长度”,即它能表示多少个奇数。这里是 9600 个。
    • bits = new long[(unitIndex(length - 1) + 1)];: 创建一个 long 数组作为位存储。每个 long 可以存储 64 个位。数组的大小通过调用 unitIndex 计算得出,确保能容纳 length 个位。
  2. 标记非素数:

    • set(0);: 将索引为 0 的位设置为 1(标记为“非素数”)。根据类的设计,筛只存储奇数,索引 i 代表数字 2*i + 1。因此,索引 0 代表数字 1,它不是素数。
  3. 筛选循环:

    • 从第一个奇素数 3 (nextPrime = 3, 对应的索引 nextIndex = 1) 开始。
    • sieveSingle(...): 调用此方法将当前素数 nextPrime 的所有倍数从筛中划去(即将对应的位设置为 1)。
    • sieveSearch(...): 调用此方法寻找下一个未被划去的位,该位即代表下一个素数。
    • nextPrime = 2*nextIndex + 1;: 根据新找到的索引计算出下一个素数的值。
    • 循环不断进行,直到找不到新的素数或素数大小超出筛的范围。

为什么从 nextIndex + nextPrime 下标开始?

这是这段代码最巧妙的地方。这个表达式是为了计算出第一个需要被划掉的数(也就是 3 * nextPrime)在筛子里的索引。

我们来验证一下:

  1. 目标:对于一个素数 p (nextPrime),我们从它的第一个奇数倍数 3p 开始筛选(因为 1p 就是 p 自己,是素数不能划掉;而 2p 是偶数,不在我们的筛子里)。
  2. 目标索引:根据公式,3p 这个数对应的索引应该是 (3p - 1) / 2
  3. 代码中的 start:代码中计算的起始索引是 start = nextIndex + nextPrime
  4. 两者是否相等? 让我们来推导一下。
    • 我们知道 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 ...
    

    执行流程分析:

    1. 初始化:

      • 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
    2. 主筛选循环 (do-while): 这个循环遍历 smallSieve 中的每一个小素数,用它们来筛选我们正在构建的大筛子。

      • 计算起始点: 这是最关键和最复杂的部分。对于每个小素数 p (convertedStep),我们需要找到大于等于 base 的第一个 p 的奇数倍数,并计算出它在这个新筛子中的索引。
        • start = b.divideOneWord(convertedStep, q): 计算 base % p,结果存入 start。这里使用 MutableBigInteger 的 divideOneWord 方法是为了性能,因为它比完整的 BigInteger 除法快得多。
        • start = convertedStep - start: 计算从 base 到下一个 p 的倍数的偏移量。例如,如果 base=100p=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 是 baseN 是 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;}
    }
    

    执行流程分析:

    1. 初始化:

      • int offset = 1;offset 变量用于计算候选数的值。筛子中的第一个位(索引0)代表的奇数是 initValue + 1,第二个位(索引1)代表 initValue + 3,以此类推。这个 offset 就代表了从 initValue 开始的奇数增量(1, 3, 5, ...)。
    2. 外层循环:  遍历 bits 数组,该数组由多个 long 类型的值组成,每个 long 存储了64个位的信息。

    3. 寻找候选long nextLong = ~bits[i];

      • 在筛子中,位为 1 表示对应的数是合数,位为 0 表示是素数候选者
      • 通过按位取反操作 ~nextLong 中的位为 1 就直接代表了这是一个素数候选者,而位为 0 代表是合数。这样做让后续的判断更直接。
    4. 内层循环: 检查当前 long (nextLong) 中的全部64个位。

    5. 检查候选者if ((nextLong & 1) == 1)

      • 通过 & 1 操作检查 nextLong 的最低位。如果最低位是 1,说明我们找到了一个素数候选者。
    6. 构建并测试候选数:

      • BigInteger candidate = initValue.add(BigInteger.valueOf(offset));: 如果找到了候选者,就用初始基数 initValue 加上当前的奇数偏移量 offset,从而构造出完整的候选数 candidate
      • if (candidate.primeToCertainty(certainty, random))这是最关键的一步BitSieve 只是排除了能被小素数整除的合数。一个数通过了筛选,不代表它一定是素数(例如,它可能是两个非常大的素数的乘积)。因此,这里必须调用 BigInteger 的 primeToCertainty 方法(通常是米勒-拉宾素性检验)进行一次严格的概率性素性测试。
      • return candidate;: 一旦有候选数通过了严格测试,它就被认为是我们要找的素数,函数立即返回这个结果。
    7. 更新状态:

      • nextLong >>>= 1;: 使用无符号右移操作,将 nextLong 的下一位移动到最低位,准备内层循环的下一次迭代。
      • offset+=2;: 因为筛子只表示奇数,所以每检查一个位,对应的数值就增加2。
    8. 无结果返回return null;

      • 如果遍历完整个筛子,都没有找到任何一个能通过 primeToCertainty 测试的数,函数返回 null

    总结

    BitSieve 是 Java 中一个教科书级别的算法实现范例。它通过以下方式实现了高效的素数筛选:

    • 算法层面:采用经典的埃拉托斯特尼筛法。
    • 空间优化:只处理奇数,并使用位数组进行紧凑存储。
    • 时间优化:采用“小筛子预计算”和“大筛子模板化筛选”的两级策略,避免了对每个大范围都从头计算,极大地提升了效率。

    它是 BigInteger 能够快速生成大素数(这在密码学等领域至关重要)的幕后功臣。

    相关文章:

  • 【无标题】对拓扑对偶框架关键质疑的深度解答
  • 【Java学习笔记】线程基础
  • 【Dify 案例】【自然语言转SQL案例】【五】【实战二】【财务管理查询商品信息数据】
  • 有哪些东西东西时间越长越值钱?做哪些事时间越长越值钱?伪长期增值陷阱​ 打造财富复利成长策略
  • 第十三章 模板
  • Spring Aop @AfterThrowing (异常通知): 使用场景
  • Cesium、ThreeWebGL详解(二)渲染引擎向GPU传数据、性能优化、引擎对比
  • 无人机测量风速的思路
  • 解决uni-app发布微信小程序主包大小限制为<2M的问题
  • 宽带中频10.4G采集卡
  • 类图:软件世界的“建筑蓝图”
  • NestJS中实现动态Cron任务管理
  • Babylon.js学习之路《十、高级几何体:自定义模型与复杂形状生成》
  • Tkinter基础函数知识点整理
  • SAM2论文解读-既实现了视频的分割一切,又比图像的分割一切SAM更快更好
  • Postman接口测试完整版
  • 【第二章:机器学习与神经网络概述】02.降维算法理论与实践-(3)多维尺度分析(Multidimensional Scaling, MDS)
  • 【2025 年】软件体系结构考试试卷-期末考试
  • 3.5.1_1 信道划分介质访问控制(上)
  • PX4无人机集成自带的深度相机进行gazebo仿真
  • 创新优典网站建设/关键词排名seo
  • 如何查公司网站谁家做的/东莞网络推广营销
  • 网站建设伍金手指下拉7/网络服务商在哪咨询
  • 企业门为什么要建设门户网站/yandex引擎
  • wordpress站点安装/网站关键词排名优化系统
  • 公司请人做的网站 域名属于谁/深圳网站建设资讯