小编典典

Java 计算并打印第n个质数

java

我正在尝试计算素数,已经完成了。但是我只想计算和打印第n个质数(用户输入),而计算其余的(不会打印),只会打印第n个质数。

到目前为止,这是我写的内容:

import java.util.Scanner;
/**
 * Calculates the nth prime number
 * @author {Zyst}
 */
public class Prime {
    public static void main(String[] args) {

        Scanner input = new Scanner(System.in);
        int n, 
            i = 2, 
            x = 2;

        System.out.printf("This program calculates the nth Prime number\n");
        System.out.printf("Please enter the nth prime number you want to find: ");
        n = input.nextInt();

        for(i = 2, x = 2; i <= n; i++) {
            for(x = 2; x < i; x++) {
                if(i % x == 0) {
                    break;
                }
            }
            if(x == i) {
                System.out.printf("\n%d is prime", x);
            }
        }
    }
}

这是我编写的用于计算从1到n的质数的程序。但是,我希望它仅显示第n个质数,

我想做的是每次进行int计数并对其进行 处理,当count == n时,它会打印出该数字,但是我不太清楚如何降落。


阅读 537

收藏
2020-03-16

共1个答案

小编典典

为了计算第n个素数,我知道两个主要的变体。

The straightforward way

也就是说,从找到的所有素数开始计数,直到找到所需的n th为止。

可以使用不同级别的复杂性和效率来完成此操作,并且在概念上有两种不同的实现方法。首先是

依次测试所有数字的素性

这可以通过类似的驱动程序功能来完成

public static int nthPrime(int n) {
    int candidate, count;
    for(candidate = 2, count = 0; count < n; ++candidate) {
        if (isPrime(candidate)) {
            ++count;
        }
    }
    // The candidate has been incremented once after the count reached n
    return candidate-1;
}

而决定效率的有趣部分是isPrime功能。

质数检查的一种明显方法是,将质数定义为大于1的数字,该数只能被1以及我们在学校学习到的数字整除,¹

Trial division

将定义直接转换为代码是

private static boolean isPrime(int n) {
    for(int i = 2; i < n; ++i) {
        if (n % i == 0) {
            // We are naive, but not stupid, if
            // the number has a divisor other
            // than 1 or itself, we return immediately.
            return false;
        }
    }
    return true;
}

但是,你很快就会发现,如果尝试使用它,它的简单性会伴随缓慢。通过该素数测试,你可以在几毫秒内找到第1000 个素数(7919)(在我的计算机上大约为20),但是找到10000 个素数104729,需要几秒钟(〜2.4s),第100000 个素数,1299709 ,几分钟(大约5分钟),百万分之一的素数15485863,大约需要八个半小时,而百万分之一的素数,179424673,则需要数周,依此类推。运行时复杂度要比二次方差-Θ(n²* log n)。

因此,我们希望加快原始性测试的速度。许多人采取的一个步骤是认识到n(除n自身以外)的除数最多为n/2。如果我们使用该事实,n/2而不是只让试算循环运行,n-1算法的运行时间将如何变化?对于复合数,循环下限没有任何改变。对于素数,试行次数减少了一半,因此总体而言,运行时间应减少一个小于2的因数。如果尝试一下,你会发现运行时间几乎精确地减少了一半,因此几乎所有尽管有更多的复合词而不是素数,但是花时间验证素数的素数。

现在,如果我们想找到百万分之一的质数,那并没有太大帮助,所以我们必须做得更好。尝试进一步降低循环限制,让我们看看n/2实际需要多少个数字。如果n/2是的除数n,则n/2是整数,换句话说,n可以被2整除。但是,循环不会超过2,因此它永远不会(除了n = 4)到达n/2。乔利(Jolly)好,那么下一个最大的除数是n多少?为什么,n/3当然。但是n/3只能将其除以n整数,换言之,如果n被3整除。那么循环将在3(或之前,在2)退出,并且永远不会到达n/3(除外n = 9)。下一个最大的除数…

等一下 我们有2 <-> n/23 <-> n/3n的除数成对出现。

如果我们考虑对(d, n/d)相应的除数n,要么d = n/d,即d = √n,或其中之一,比方说d,比其他较小。但是然后d*d < d*(n/d) = nd < √n。每对对应的除数n包含(至少)一个不超过的除数√n

如果 n 是复合的,则其最小平凡因数不超过 √n

因此,我们可以将循环限制降低到√n,从而降低算法的运行时复杂度。现在应该是Θ(n 1.5 *√(log n)),但是从经验上看,它似乎可以更好地扩展-但是,没有足够的数据可以从经验结果中得出可靠的结论。

这样就可以在大约16秒内找到百万分之一的质数,在不到9分钟的时间内找到百万分之一的质数,并且在大约四个半小时内可以找到百万分之一的质数。这仍然很慢,但与十年左右的时间相去甚远,这需要天真的审判部门。

由于存在素数的平方和两个近似素数的乘积,例如323 = 17 * 19,因此我们不能减小下面的试验除法循环的极限√n。因此,在保持试验划分的同时,我们现在必须寻找其他方法来改进算法。

一个容易看出的事情是,除2以外的其他素数都不是偶数,因此在处理完2之后我们只需要检查奇数即可。但是,这并没有太大的区别,因为偶数是最便宜的。复合-并且仍然花费大量时间来检验素数的素性。但是,如果将偶数视为候选除数,则会看到如果n被偶数整除,则n本身必须是偶数,因此(除2外)在除以大于2的任何偶数之前将被识别为合成数被尝试。因此,算法中发生的所有大于2的偶数除法必须保留非零余数。因此,我们可以忽略这些除法,仅检查2的除数,并检查3到3的奇数。√n。这将确定一个数字为质数或复合数所需的除法数减少了一半(并不完全是),因此将运行时间减半了。这是一个好的开始,但是我们可以做得更好吗?

另一个大数字家族是3的倍数。我们执行的每三等分乘以3的倍数,但是如果n可以被其中一个整除,那么它也可以被3整除,因此不能除以9、15、21, …我们在算法中执行的运算将永远保留0的余数。那么,如何跳过这些除法呢?好吧,被2或3整除的数字恰好是形式的数字6*k ± 1。从5开始(因为我们只对大于1的数字感兴趣),所以它们是5、7、11、13、17、19,…,从一个步到下一个步在2和4之间交替,即很容易,所以我们可以使用

private static boolean isPrime(int n) {
    if (n % 2 == 0) return n == 2;
    if (n % 3 == 0) return n == 3;
    int step = 4, m = (int)Math.sqrt(n) + 1;
    for(int i = 5; i < m; step = 6-step, i += step) {
        if (n % i == 0) {
            return false;
        }
    }
    return true;
}

这使我们又提高了将近1.5倍,因此我们需要大约一个半小时才能达到百万分之一的质数。

如果我们继续这条路线,则下一步是消除5的倍数。数字互质为2、3和5的形式是

30*k + 1, 30*k + 7, 30*k + 11, 30*k + 13, 30*k + 17, 30*k + 19, 30*k + 23, 30*k + 29

因此我们只需要将每三十个数字除以八(再加上三个最小的质数)即可。从一个步骤到下一个步骤(从7开始)循环到4、2、4、2、4、6、2、6。这仍然很容易实现,并且可以将速度提高1.25倍(减去更复杂的代码)。更进一步,将消除7的倍数,在每210个数字中除48个,然后除以11(480/2310),13(5760/30030),依此类推。p消除了倍数的每个素数都会产生(几乎)的加速p/(p-1),因此返回率降低,而成本(代码复杂度,用于步骤的查找表的空间)随每个素数而增加。

通常,在消除了六个或七个素数(甚至更少)的倍数之后,很快就会停止。但是,在这里,我们可以进行到最后,当所有质数的倍数都被消除并且仅保留质数作为候选除数时。由于我们按顺序查找所有素数,因此在需要将每个素数用作候选除数之前就可以找到它们,然后可以将其存储以备将来使用。这将算法复杂度降低到-如果我没有计算错的话-O(n 1.5 /√(log n))。以用于存储素数的空间使用为代价。

使用试除法,即尽其所能,你必须尝试将所有素数√n除以或第一次除法n来确定的素数n。在大约半小时内即可找到百万分之一的质数。

那怎么样

快速素数测试

质数具有其他的数论特性,而不是通常没有复数的平凡除数。如果可以快速检查这些属性,它们可以构成概率或确定性素数测试的基础。该原型财产与皮埃尔·德·费马,谁,在早期的17名相关个世纪,发现

如果p为素数,则为所有p(a p -a)的除数a

这就是费马的所谓“小定理”,用等价的形式表示

让它p成为素数,a不能被整除p。然后p除以p- 1-1

大多数广泛的快速原始性检验(例如Miller-Rabin)的基础以及其变体或类似物的出现甚至更多(例如Lucas-Selfridge)。

因此,如果我们想知道一个不太小的奇数n是否是质数(通过试验除法有效地处理了偶数和小数),我们可以选择a不是倍数的任何数字(> 1)n,例如2和检查是否n除以n-1-1。由于n-1变得很大,所以最有效的方法是检查 a^(n-1) ≡ 1 (mod n),即通过模幂。如果这种一致性不成立,我们就知道这n是复合的。但是,如果成立,我们就不能断定n是质数,例如2^340 ≡ 1 (mod 341),而是341 = 11 * 31复合数。这样的复合数n,a^(n-1) ≡ 1 (mod n)称为基的Fermat伪素数a。

但是这种情况很少见。给定任何基础a > 1,尽管有无限数量的Fermat伪a素数作为基础,但它们比实际素数少得多。例如,在100000以下,只有78个base-2 Fermat伪素数和76 base-3 Fermat伪素数,但是9592个素数。因此,如果选择一个任意的奇数n > 1和一个任意的基数a > 1并找到a^(n-1) ≡ 1 (mod n),则很有可能n实际上是质数。

但是,我们处在稍微不同的情况下,我们n只能选择a。因此,对于一个奇怪的组合n,有多少a,1 < a < n-1可以a^(n-1) ≡ 1 (mod n)持有?不幸的是,有复合数字-Carmichael数-使得对的每个素 a数都具有同余性n。这意味着要通过费马检验将Carmichael数识别为复合数,我们必须选择一个基数,该基数是n的主要除数之一的倍数-可能没有很多这样的倍数。

但是我们可以加强费马测试,以便更可靠地检测复合材料。如果p是奇数素数,请写p-1 = 2*m。然后,如果0 < a < p

a^(p-1) - 1 = (a^m + 1) * (a^m - 1)

并p精确地将两个因子之一相除(两个因子相差2,因此它们的最大公约数是1或2)。如果m是偶数,我们可以a^m - 1用相同的方式拆分。继续,如果p-1 = 2^s * k有k奇数,写

a^(p-1) - 1 = (a^(2^(s-1)*k) + 1) * (a^(2^(s-2)*k) + 1) * ... * (a^k + 1) * (a^k - 1)

然后p精确地划分因素之一。这引起了强大的费马测试,

n > 2一个奇数。写n-1 = 2^s * kk奇怪。鉴于任何a1 < a < n-1,如果

  1. a^k ≡ 1 (mod n) 要么
  2. a^((2^j)*k) ≡ -1 (mod n)对于任何j与0 <= j < s

然后n是一个强(Fermat)可能的基本素数a。复合的强基础a(Fermat)可能素数称为基础的强(Fermat)伪素数a。强Fermat伪素甚至比普通的Fermat伪素稀有,低于1000000,有78498个素,245个base-2 Fermat伪素和46个base-2强Fermat伪素。更重要的是,对于任何奇怪的组合n,最多有(n-9)/4基地1 < a < n-1的这n是一个强大的费马伪素。

因此,如果n是奇数合成,则在1和(不包括边界)之间随机选择碱基的情况下,n通过kFermat检验的概率n-1小于1/4^k

一个强大的费马测试需要O(log n)的步骤,每个步骤涉及号码之一或两个乘法与O(log n)的位,所以复杂度为O((log n)的^ 3)与幼稚乘法[用于巨大n,更复杂的乘法算法可能值得]。

Miller-Rabin检验是具有随机选择碱基的k倍强费马检验。这是一个概率测试,但是对于足够小的范围,已知碱基的简短组合会给出确定的结果。

强大的Fermat测试是确定性APRCL测试的一部分。

建议在进行此类测试之前先进行前几个小素数的试验划分,因为划分相对便宜,并且可以淘汰大多数复合材料。

对于找到n第一个素数的问题,在可行的范围内测试所有数字的范围内,存在已知的基数组合,这些基数可以使多重强费马检验正确,从而可以提供更快的-O(n *(log n )4)-算法。

对于n < 2^32,基数2、7和61足以验证素数。使用它,大约六分钟内即可找到亿万个质数。

用主要除数(Eratosthenes筛子)消除复合材料

除了顺序研究数字并检查每个数字是否都是素数之外,还可以将整个相关数字视为一个整体,并一次性消除给定素数的倍数。这被称为Eratosthenes筛网:

查找素数不超过 N

  1. 列出所有数字,从2到 N
  2. 对于k2到N:中的每一个:如果k尚未去除,则为质数;划掉k合成的所有倍数
    质数是列表中没有舍去的数字。

该算法与试验划分从根本上不同,尽管两者均直接使用素数的可除性表征,这与Fermat检验和使用素数其他属性的类似测试形成对比。

在试验除法中,每个数n与所有不超过的较小√n素数和最小除数的素数配对n。由于大多数复合材料的主因数都很小,因此平均而言,检测复合材料很便宜。但是测试素数很昂贵,因为下面有很多素数√n。尽管合成词的数量比素数多得多,但是测试素数的成本非常高,以至于它完全支配了整个运行时间,并使试验划分成为一种相对较慢的算法。小于NO(N 1.5 /(log N)²)步的所有数字的尝试除法。

在筛子中,每个组合n都与其所有主除数配对,但仅与这些配对。因此,质数是便宜的数字,它们一次只被查看,而复合材料则更昂贵,它们被多次划掉。可能有人认为,由于筛子包含的“昂贵”数字比“便宜”的数字多,因此总体而言是一种不好的算法。但是,一个合成数没有很多不同的素数除数-的不同素数除数受n限制log n,但通常要小得多,该数字的不同素数除数的平均值<= nlog log n -因此,即使是筛子上的“昂贵”数字,平均也不会比试验部门的“便宜”数字昂贵(或几乎没有)。

筛选N每个质数pΘ(N/p)倍数要相交,因此相交的总数为Θ(∑ (N/p)) = Θ(N * log (log N))。与使用更快的素数测试的试验划分或顺序测试相比,这产生了更快的算法来查找素N数。

但是,筛子有一个缺点,它使用O(N)内存。(但是使用分段筛,可以在O(√N)不增加时间复杂度的情况下将其减小。)

为了找到n第一个素数,而不是最多的素数N,还存在一个问题,即事先不知道筛子应该到达多远。

后者可以使用素数定理求解。PNT说

π(x) ~ x/log x (equivalently: lim π(x)*log x/x = 1),

其中π(x)是素数不超过的数目x(在这里和下文中,log必须是自然对数,对于复杂的算法并不重要选择哪种碱为对数)。由此可知p(n) ~ n*log n,哪里p(n)n第素数,并且p(n)通过更深入的分析就可以知道有一个好的上限,特别是

n*(log n + log (log n) - 1) < p(n) < n*(log n + log (log n)), for n >= 6.

因此,可以将其用作筛分极限,不会超过目标。

的O(N)空间需求可通过使用分段筛来克服。然后,可以记录下面的质数√N以减少O(√N / log N)内存消耗,并使用长度增加的段(当滤网接近N时为O(√N))。

如上所述,对算法进行了一些简单的改进:

  1. 开始舍弃的p仅是的倍数,而不是2*p
  2. 从筛子上消除偶数
  3. 从筛子上消除更多小质数的倍数
    这些方法都不能降低算法的复杂性,但是它们都可以极大地减少常数因子(与试验分割一样,p对于较大的变量,消除乘积的倍数会导致较小的加速,p而对较小的变量则增加的代码复杂度p)。

使用前两个改进产生

// Entry k in the array represents the number 2*k+3, so we have to do
// a bit of arithmetic to get the indices right.
public static int nthPrime(int n) {
    if (n < 2) return 2;
    if (n == 2) return 3;
    int limit, root, count = 1;
    limit = (int)(n*(Math.log(n) + Math.log(Math.log(n)))) + 3;
    root = (int)Math.sqrt(limit) + 1;
    limit = (limit-1)/2;
    root = root/2 - 1;
    boolean[] sieve = new boolean[limit];
    for(int i = 0; i < root; ++i) {
        if (!sieve[i]) {
            ++count;
            for(int j = 2*i*(i+3)+3, p = 2*i+3; j < limit; j += p) {
                sieve[j] = true;
            }
        }
    }
    int p;
    for(p = root; count < n; ++p) {
        if (!sieve[p]) {
            ++count;
        }
    }
    return 2*p+1;
}

在大约18秒内找到亿万个质数2038074743。通过存储打包的标志(每个标志一位)而不是booleans,可以将这段时间减少到大约15秒(此处为YMMV),因为减少的内存使用量可以提供更好的缓存位置。

打包标志,也消除3的倍数,并使用位旋转来更快地进行计数,

// Count number of set bits in an int
public static int popCount(int n) {
    n -= (n >>> 1) & 0x55555555;
    n = ((n >>> 2) & 0x33333333) + (n & 0x33333333);
    n = ((n >> 4) & 0x0F0F0F0F) + (n & 0x0F0F0F0F);
    return (n * 0x01010101) >> 24;
}

// Speed up counting by counting the primes per
// array slot and not individually. This yields
// another factor of about 1.24 or so.
public static int nthPrime(int n) {
    if (n < 2) return 2;
    if (n == 2) return 3;
    if (n == 3) return 5;
    int limit, root, count = 2;
    limit = (int)(n*(Math.log(n) + Math.log(Math.log(n)))) + 3;
    root = (int)Math.sqrt(limit);
    switch(limit%6) {
        case 0:
            limit = 2*(limit/6) - 1;
            break;
        case 5:
            limit = 2*(limit/6) + 1;
            break;
        default:
            limit = 2*(limit/6);
    }
    switch(root%6) {
        case 0:
            root = 2*(root/6) - 1;
            break;
        case 5:
            root = 2*(root/6) + 1;
            break;
        default:
            root = 2*(root/6);
    }
    int dim = (limit+31) >> 5;
    int[] sieve = new int[dim];
    for(int i = 0; i < root; ++i) {
        if ((sieve[i >> 5] & (1 << (i&31))) == 0) {
            int start, s1, s2;
            if ((i & 1) == 1) {
                start = i*(3*i+8)+4;
                s1 = 4*i+5;
                s2 = 2*i+3;
            } else {
                start = i*(3*i+10)+7;
                s1 = 2*i+3;
                s2 = 4*i+7;
            }
            for(int j = start; j < limit; j += s2) {
                sieve[j >> 5] |= 1 << (j&31);
                j += s1;
                if (j >= limit) break;
                sieve[j >> 5] |= 1 << (j&31);
            }
        }
    }
    int i;
    for(i = 0; count < n; ++i) {
        count += popCount(~sieve[i]);
    }
    --i;
    int mask = ~sieve[i];
    int p;
    for(p = 31; count >= n; --p) {
        count -= (mask >> p) & 1;
    }
    return 3*(p+(i<<5))+7+(p&1);
}

在大约9秒钟内找到亿分之一的质数,这并不长。

还有其他类型的素数筛,尤其是Atkin筛,它利用了一个事实,即(有理数)素数的某些同余类是ℚ的一些二次扩展的代数整数环中的复合。这里不是扩展数学理论的地方,足以说Atkin筛比Eratosthenes筛具有更低的算法复杂度,因此对于较大的限制较为理想(对于较小的限制,未过度优化的Atkin筛具有更高的开销,因此可能比同等优化的Eratosthenes筛子慢)。DJ Bernstein的primegen库(用C编写)已针对2 32以下的数字进行了优化,并在约1.1秒内找到了亿分之一的质数(在此)。

快速的方法

如果我们只是想找到n个素数,存在也发现所有的小素数没有内在价值。如果我们可以跳过其中的大多数,则可以节省大量时间和工作。给定一个好的近似a(n)n日黄金p(n),如果我们有一个快速的方法来计算质数的数目π(a(n))不超过a(n),我们就可以筛小范围的上方或下方a(n),以确定之间的一些缺失或多余的素数a(n)和p(n)

我们已经看到了p(n)上面的一个容易计算的相当好的近似值,我们可以采取

a(n) = n*(log n + log (log n))

例如。

一个好的计算方法π(x)Meissel-Lehmer方法,该方法可以π(x)在大致O(x^0.7)时间内进行计算(确切的复杂程度取决于实现方式,Lagarias,Miller,Odlyzko,DelégliseRivat的改进使得一个计算的结果π(x)为O(x 2/3 /log² x)时间)。

从简单的近似开始a(n),我们进行计算e(n) = π(a(n)) - n。根据素数定理,附近的素数的密度a(n)约为1/log a(n),因此我们期望p(n)接近,因此b(n) = a(n) - log a(n)*e(n)我们将筛选比稍大的范围log a(n)*e(n)。为了p(n)使筛分范围内的置信度更高,可以将范围增加2倍,例如,几乎可以肯定该范围足够大。如果范围似乎太大,则可以使用更好的近似值b(n)来代替a(n),计算π(b(n))和进行迭代f(n) = π((b(n)) - n。通常,|f(n)|将小于|e(n)|。如果f(n)近似为-e(n)c(n) = (a(n) + b(n)) / 2则将是一个更好的近似p(n)。仅在极少数情况下f(n)由于非常接近e(n)(并且不是非常接近于0),因此找到足够好的近似值以p(n)使得最终的筛分阶段可以在与计算相当的时间内完成π(a(n))是一个问题。

通常,在对初始近似值进行一到两次改进之后,要筛分的范围足够小,以使筛分阶段的复杂度为O(n ^ 0.75)或更高。

此方法在不到8秒的时间内找到了亿万个质数,在10 秒内找到了10 12个质数,即29996224275833。

tl; dr:找到n第一个素数可以有效地完成,但是你想要的效率越高,涉及的数学越多。

我为这里准备的大多数讨论的算法准备了 Java代码,以防有人想玩弄它们。

¹撇开对过度感兴趣的灵魂的评论:现代数学中使用的质数的定义是不同的,适用于更一般的情况。如果我们使学校的定义包含负数-如果数字既不是1也不是-1并且只能被1,-1本身及其负数整除,那么它是质数-它定义了(对于整数)当今称为不可约元素但是,对于整数,素数和不可约元素的定义是一致的。

2020-03-16