寻找质数的程序

33

我想要找到0到一个长变量之间的质数,但是我无法得到任何输出。

程序如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication16
{
    class Program
    {
        void prime_num(long num)
        {
            bool isPrime = true;
            for (int i = 0; i <= num; i++)
            {
                for (int j = 2; j <= num; j++)
                {
                    if (i != j && i % j == 0)
                    {
                        isPrime = false;
                        break;
                    }
                }
                if (isPrime)
                {
                    Console.WriteLine ( "Prime:" + i );
                }
                isPrime = true;
            }
        }

        static void Main(string[] args)
        {
            Program p = new Program();
            p.prime_num (999999999999999L);
            Console.ReadLine();
        }
    }
}

有人可以帮我找出程序可能存在的错误吗?


这个项目使用了哪个项目模板来创建? - Alexandre Brisebois
8
作业提醒!! - whatnick
12
可能是作业问题,只要提问者已经尝试回答作业问题并陷入了特定的问题(正如在这里看到的情况),那就没有什么问题。 - Eric J.
3
这件事实际上需要多久时间?999999999999999L 是一个相当大的数字。 - Guillermo Phillips
@GuillermoPhillips,即使按照下面的答案进行修复,也需要很长时间,而且并没有解释生成所有这些质数的目的。如果目的是对它们进行求和、查找间隙、双倍、三倍等第一次出现的位置,那么唯一可行的方法就是编写一个高效的多处理器分页版本的埃拉托斯特尼筛法,这需要在现代高端台式计算机上花费一个月或两个月,但可以扩展到超级计算机的数十万核心以在几秒钟内运行。 - GordonBGood
显示剩余3条评论
28个回答

83

您可以使用一个 几乎最优 的试除法筛法,在一行代码中更快地完成此操作:

Enumerable.Range(0, Math.Floor(2.52*Math.Sqrt(num)/Math.Log(num))).Aggregate(
    Enumerable.Range(2, num-1).ToList(), 
    (result, index) => { 
        var bp = result[index]; var sqr = bp * bp;
        result.RemoveAll(i => i >= sqr && i % bp == 0); 
        return result; 
    }
);

这里使用的是近似计算素数数量的公式π(x) < 1.26 x / ln(x)。我们只需要检查不超过x = sqrt(num)的质数。

请注意,埃拉托色尼筛法比试除法具有更好的运行时间复杂度(在正确实现时应该对更大的num值运行得更快)。


7
为什么会被踩?它回答了这个问题(我怎样才能做得更好?) - SLaks
12
看起来提问者有一份具体的家庭作业任务。如果他提交你的解答,老师将视为作弊行为。 - Jon B
4
是的,令人惊奇的是这个原理在2000多年前就被首次描述了。 - UpTheCreek
26
教练是正确的。使用其他答案也可以称为作弊。但是,它仍然回答了问题。 - SLaks
2
答案一直都在那里,他并没有进行重大的研究项目。 - whatnick
显示剩余28条评论

25

试试这个:

void prime_num(long num)
{

    // bool isPrime = true;
    for (long i = 0; i <= num; i++)
    {
        bool isPrime = true; // Move initialization to here
        for (long j = 2; j < i; j++) // you actually only need to check up to sqrt(i)
        {
            if (i % j == 0) // you don't need the first condition
            {
                isPrime = false;
                break;
            }
        }
        if (isPrime)
        {
            Console.WriteLine ( "Prime:" + i );
        }
        // isPrime = true;
    }
}

@WillNess,正如你所说,问题代码存在一个问题,即会进行大量荒谬的质数检查。实际上,问题代码根本没有产生任何输出的真正原因是混合了长范围检查限制变量和整数循环变量(自动扩展为长整型进行比较),这正是提问者所经历的:内部检查循环永远不会退出,因为循环变量的范围小于所检查的范围,因此永远不会产生输出。 - GordonBGood
1
@GordonBGood 2^32 < 10^10; 如果存在INT范围的环绕,'j'最终会达到0,并在'1%j'中导致除零。 i7核心是100吉浮点操作每秒芯片,100 * 10 ^ 9,因此这应该在不到一秒钟的时间内发生。也许,在C#中,“int”是64位的?然后,对于'i = 1',将运行到10 ^ 15,需要大约20分钟... 2小时的时间。 - Will Ness
@WillNess,好的,除法操作本身需要大约50个时钟周期,所以循环到除零状态的时间将会是几十秒钟;然而,它实际上永远不会变成零,因为每个被测试的数字在对-1进行测试时都具有零模,因此循环将永远旋转,根本找不到任何素数(它们都可以被-1均匀地整除,每次试验需要几十秒)。 - GordonBGood
@GordonBGood 哈哈,-1,好发现! :) - Will Ness
@GordonBGood 哇,C# 只比 primesieve 慢了几倍,这是一个成就!虽然 C# 不是我的语言,但读你的代码会很有趣。至于所有的优化,我只是一个传递者。 :) 几年前我自己发现了 Turner 筛法中“延迟”修复的方法,我相信/知道(许多)其他人也是如此,所以这引起了一些兴趣。干杯! :) - Will Ness
显示剩余3条评论

10

你只需要检查数字的平方根以下的奇数除数。换句话说,你的内循环应该从以下位置开始:

for (int j = 3; j <= Math.Sqrt(i); j+=2) { ... }

当你发现一个数字不是质数时,你可以立即退出函数,无需再检查任何更多的除数(我看到你已经在做了!)。

这只适用于num大于2的情况。

无平方根

你可以通过保持运行总和来避免使用平方根。例如:

int square_sum=1;
for (int j=3; square_sum<i; square_sum+=4*(j++-1)) {...}
这是因为1 +(3 + 5)+(7 + 9)的和会给你一个奇数平方数序列(1、9、25等)。 因此,j代表square_sum的平方根。 只要square_sum小于i,那么j就小于平方根。

@GuillermoPhillips,如果想要使用square_sum选项,正确且更高效的循环(仅除以奇数;还请注意检查平方根及其以下)应该是 for (int j=3, square_sum=9; square_sum <= i; square_sum+=4((j+=2)-1)) {...} - GordonBGood
@GuillermoPhillips,我认为square_sum的想法实际上在实践中并不是很有效,因为虽然它需要大约24个CPU时钟来计算(int)Math.Sqrt((double)i),但这只需要为每个'i'的值计算一次,而square_sum计算需要每个'j'循环一个CPU时钟; 这意味着一旦'i'的值达到一千左右,平方根计算实际上比运行square_sum计算所需的时间更短。 - GordonBGood

10
人们提到了一些有效实现的基本要素,但是没有人真正将这些要素组合起来。埃拉托斯特尼筛法是一个不错的开始,但是使用它,您会在达到设置的限制之前就耗尽内存。但这并不意味着它是无用的——当您进行循环时,您真正关心的是质数因子。因此,您可以先使用筛法创建质数因子的基础,然后在循环中使用它们来测试数字是否为质数。
然而,当您编写循环时,确实不希望在循环条件中使用sqrt(i),正如一些答案所建议的那样。您和我都知道sqrt是一个“纯”的函数,如果给定相同的输入参数,它总是给出相同的答案。不幸的是,编译器不知道这一点,因此如果在循环条件中使用类似'&lt;=Math.sqrt(x)'的东西,它将在每次循环迭代中重新计算该数字的平方根。
你可以通过几种不同的方式来避免这种情况。你可以在循环之前预先计算平方根,并在循环条件中使用预先计算的值,或者你可以采用另一种方法,将i < Math.sqrt(x) 更改为i * i < x。个人而言,我会预先计算平方根--我认为这更清晰,可能也更快--但这取决于循环的迭代次数(i*i表示它仍然在循环中进行乘法运算)。对于只有几个迭代的情况,i*i通常会更快。但是,随着足够多的迭代,每次迭代的i*i损失超过了执行一次sqrt的时间。
对于你处理的数字大小,这可能已经足够了--15位数的限制意味着平方根是7或8位数,这适合在相当合理的内存范围内。另一方面,如果你经常处理这个范围内的数字,你可能需要查看一些更复杂的素数检查算法,例如Pollard's or Brent's algorithms。这些算法更加复杂(说得婉转一点),但对于大数字来说速度要快得多。

还有其他算法可以处理更大的数字(二次筛法通用数域筛法),但我们暂时不会涉及它们——它们更加复杂,而且只适用于处理非常大的数字(GNFS在100位以上才开始有用)。


这篇关于素数算法的总结很有趣,让我在 Wikipedia 和 Wolfram 上浏览了一会儿。希望能修改此文章并加入链接。 - whatnick
你的理解有些不正确,关于“不想在循环条件中使用sqrt(i)”这一点,因为有办法让编译器和代码读者明确计算出限制范围,只需在实际循环范围检查之外计算一次限制即可,就像我在我的回答中所做的那样。 - GordonBGood

8
第一步:编写一个扩展方法来判断输入是否为质数。
public static bool isPrime(this int number ) {

    for (int i = 2; i < number; i++) { 
        if (number % i == 0) { 
            return false; 
        } 
    }

    return true;   
}

第二步:编写一个方法,打印出所有介于0和输入数字之间的质数。

public static void getAllPrimes(int number)
{
    for (int i = 0; i < number; i++)
    {
        if (i.isPrime()) Console.WriteLine(i);
    }
}

你的 isPrime 函数会对于 0,1 和任何负数返回 true。因此,在 getAllPrimes 函数内部的循环应该至少从 int i = 2; 开始。 - Will Ness
不,不是的,return true; 应该在 for 语句之后,就像现在一样(只是文本缩进不对)。但是,如所写的 isPrime 函数假定 2 <= number。在其他情况下,它将无法正常工作。因此,无论您在何处使用它,都要确保使用一个大于1的数字进行测试。这也是一个明智的选择,因为小于2的数字都不是质数,所以不需要检查这些数字。这意味着,只需更改 getAllPrimes 循环中的 i 的起始值,从 2 开始而不是从 0 开始。如果不这样做,您的程序将显示0和1为质数。 - Will Ness
打字错误:"所以不需要检查那些数字"(小于2)是否为质数。 - Will Ness
isPrime 函数中,你只需要检查到 number 的平方根。 - Wai Ha Lee
在isPrime()函数中,您可以运行循环直到"i <= number / 2"。因为对于number = 19的情况,您的for循环将迭代到i = 18。 - Naphstor

7
可能这只是我的个人观点,但你的程序中还有一个严重的错误(撇开已经得到彻底回答的“质数”问题)。
像其他回复者一样,我认为这是一份作业,这表明你想成为一名开发人员(大概率)。
你需要学会将代码分隔开来。这并不是在项目中总是需要做的事情,但知道如何做是很好的。
你的方法prime_num(long num)可能需要更好、更具描述性的名称。如果它应该找出小于给定数字的所有质数,则应将它们作为列表返回。这使得更容易区分显示和功能。
如果它只是返回包含质数的IList,那么你可以在主函数中显示它们(或调用另一个外部函数来美化它们),或者在后续计算中使用它们。
因此,我最好的建议是这样做:
public void main(string args[])
{
    //Get the number you want to use as input
    long x = number;//'number' can be hard coded or retrieved from ReadLine() or from the given arguments

    IList<long> primes = FindSmallerPrimes(number);

    DisplayPrimes(primes);
}

public IList<long> FindSmallerPrimes(long largestNumber)
{
    List<long> returnList = new List<long>();
    //Find the primes, using a method as described by another answer, add them to returnList
    return returnList;
}

public void DisplayPrimes(IList<long> primes)
{
    foreach(long l in primes)
    {
        Console.WriteLine ( "Prime:" + l.ToString() );
    }
}

即使你最终工作的地方不需要这样的分离,学会如何做也是很有好处的。


2
尽管其他人已经回答了这个问题,但我认为你的回答对OP来说非常有用,因为它教给他一些关于编程中关注点分离的知识。+1 - Hallaghan

7
EDIT_ADD: 如果Will Ness是正确的,问题的目的只是在程序运行时输出连续的质数流(按暂停/中断键暂停并按任意键重新开始),没有真正希望达到那个上限,那么代码应该不带上限参数,并对第一个'i'循环使用"true"的范围检查。另一方面,如果问题想要实际打印出上限内的质数,则以下代码将更有效地使用试除法仅针对奇数进行操作,并且具有不使用内存的优势(它也可以转换为与上述相同的连续循环):
static void primesttt(ulong top_number) {
  Console.WriteLine("Prime:  2");
  for (var i = 3UL; i <= top_number; i += 2) {
    var isPrime = true;
    for (uint j = 3u, lim = (uint)Math.Sqrt((double)i); j <= lim; j += 2) {
      if (i % j == 0)  {
        isPrime = false;
        break;
      }
    }
    if (isPrime) Console.WriteLine("Prime:  {0} ", i);
  }
}

首先,问题代码没有输出,因为它的循环变量是整数,而测试的限制是一个巨大的长整数,这意味着循环无法达到限制并产生内部循环 EDITED: ,其中变量'j'回环到负数; 当'j'变量回到-1时,测试的数字未通过素数测试,因为所有数字都可以被-1平均地整除 END_EDIT。即使这一点得到纠正,问题代码也会产生非常慢的输出,因为它会将大量的复合数(所有偶数加上奇数复合数)进行64位除法,并将其除以可能产生的每个素数的整个数字范围,直到那个十六次方的顶部数字。以上代码之所以有效,是因为它将计算限制在仅奇数上,并仅对当前测试数字的平方根进行模除

这需要一个小时左右才能显示出十亿个素数,因此可以想象,展示所有素数到万亿级别(10的16次方),尤其是随着范围的增加,计算变得越来越慢,需要花费多少时间。 END_EDIT_ADD

虽然@SLaks使用Linq的单行(有点)答案是可行的,但它实际上并不是埃拉托斯特尼筛法,因为它只是Trial Division的未优化版本,未优化的原因是它没有消除奇数素数,没有从发现的基础素数的平方开始,也没有停止对大于筛选顶部数字的平方根的基本素数进行筛选。由于存在多个嵌套枚举操作,它也非常慢。
事实上,这是对Linq聚合方法的滥用,并没有有效地使用生成的两个Linq范围中的第一个。可以通过以下方式将其变成优化的试除法,减少枚举开销:
static IEnumerable<int> primes(uint top_number) {
  var cullbf = Enumerable.Range(2, (int)top_number).ToList();
  for (int i = 0; i < cullbf.Count; i++) {
    var bp = cullbf[i]; var sqr = bp * bp; if (sqr > top_number) break;
    cullbf.RemoveAll(c => c >= sqr && c % bp == 0);
  } return cullbf; }

该实现比SLaks的答案快多了,但由于列表生成、多次枚举和多次除法(暗示的模数)运算仍然很慢且占用内存。
以下是真正的埃拉托斯特尼筛法实现,运行速度约为上述方法的30倍,而且使用的内存更少,因为它每个筛选数字只使用一个位表示,并将其枚举限制在最终的迭代器序列输出中,同时优化了仅处理奇合数,以及仅从基本质数的平方开始剔除基本质数,对于最大数字的平方根,如下所示:
static IEnumerable<uint> primes(uint top_number) {
  if (top_number < 2u) yield break;
  yield return 2u; if (top_number < 3u) yield break;
  var BFLMT = (top_number - 3u) / 2u;
  var SQRTLMT = ((uint)(Math.Sqrt((double)top_number)) - 3u) / 2u;
  var buf = new BitArray((int)BFLMT + 1,true);
  for (var i = 0u; i <= BFLMT; ++i) if (buf[(int)i]) {
      var p = 3u + i + i; if (i <= SQRTLMT) {
        for (var j = (p * p - 3u) / 2u; j <= BFLMT; j += p)
          buf[(int)j] = false; } yield return p; } }

以上代码在Intel i7-2700K (3.5 GHz)上计算1000万范围内的所有质数,大约需要77毫秒。
可以使用using语句调用其中任意一个静态方法,并通过静态Main方法进行测试,如下所示:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

static void Main(string[] args) {
  Console.WriteLine("This program generates prime sequences.\r\n");

  var n = 10000000u;

  var elpsd = -DateTime.Now.Ticks;

  var count = 0; var lastp = 0u;
  foreach (var p in primes(n)) { if (p > n) break; ++count; lastp = (uint)p; }

  elpsd += DateTime.Now.Ticks;
  Console.WriteLine(
    "{0} primes found <= {1}; the last one is {2} in {3} milliseconds.",
    count, n, lastp,elpsd / 10000);

  Console.Write("\r\nPress any key to exit:");
  Console.ReadKey(true);
  Console.WriteLine();
}

这将显示序列中素数的数量,最后找到的素数以及枚举到那个位置所花费的时间。

EDIT_ADD: 然而,为了产生小于一万万亿(10的16次方)的素数的枚举,需要使用分段分页方法并使用多核处理,即使使用C ++和非常高度优化的PrimeSieve,这也需要超过400小时才能仅生成找到的素数数量,并且枚举所有素数需要数十倍的时间,因此需要一年时间来完成问题要求的任务。使用未优化的试除法算法进行尝试,则需要超级长时间,即使使用像10的200万次方年这样的优化试除法算法也需要非常非常长的时间(那是两百万个零的年份!)。

难怪他的台式机在尝试时只是坐着停滞不前!!!如果他尝试了一个较小的范围,如一百万,他仍然会发现实施需要几秒钟的时间。

我在这里发布的解决方案也无法胜任,即使是最后的埃拉托斯特尼筛法,也需要大约640TB的内存才能处理该范围内的问题。
这就是为什么只有像PrimeSieve这样的分页分段方法才能处理这种指定范围的问题,即使这样也需要很长时间,可能需要几周甚至几年,除非您可以访问具有数十万核心的超级计算机。

5

闻起来像是更多的作业。我的非常古老的图形计算器有一个类似于这样的素数程序。从技术上讲,内部除法检查循环只需要运行到i^(1/2)。您需要找到0和L之间的“所有”质数吗?另一个主要问题是,您的循环变量是“int”,而您的输入数据是“long”,这将导致溢出,使您的循环无法执行一次。修复循环变量。


实际上,长限制变量和整数循环变量的混合会导致与提问者经历的完全相同的问题:循环变量(自动扩展为长整型进行比较)小于由长整型变量指定的检查范围。这使得内部的“j”循环测试所有数字直到int.MaxValue(20亿多),然后回滚到int.MinValue(负20亿减去),当该循环变量“j”回到-1时,循环将退出,因为所有数字都可以被-1整除,因此被归类为非素数,并且没有输出结果。 - GordonBGood

2

C#中的一行代码:

Console.WriteLine(String.Join(Environment.NewLine, 
    Enumerable.Range(2, 300)
        .Where(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1)
        .All(nn => n % nn != 0)).ToArray()));                                    

2

埃拉托色尼筛法 的上述回答并不完全正确。如所写,它将找到1到1000000之间的所有质数。要找到1到num之间的所有质数,请使用以下方法:

private static IEnumerable Primes01(int num)
{
    return Enumerable.Range(1, Convert.ToInt32(Math.Floor(Math.Sqrt(num))))
        .Aggregate(Enumerable.Range(1, num).ToList(),
        (result, index) =>
            {
                result.RemoveAll(i => i > result[index] && i%result[index] == 0);
                return result;
            }
        );
}

聚合物的种子应该在1到num之间,因为这个列表将包含质数的最终列表。 Enumerable.Range(1, Convert.ToInt32(Math.Floor(Math.Sqrt(num))))是清除种子的次数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接