【问题标题】:Are You A Prime Number你是质数吗
【发布时间】:2011-04-17 03:51:03
【问题描述】:

多年来,我一直对寻找更好的素数识别器的问题感兴趣。我意识到这是一个巨大的学术研究领域——我对此的兴趣实际上只是为了好玩。这是我在 C 语言中第一次尝试可能的解决方案(见下文)。

我的问题是,您能否提出改进建议(不引用网上的其他参考资料,我正在寻找实际的 C 代码)?我试图从中获得的是更好地理解确定此类解决方案的性能复杂性。

我是否认为这个解决方案的复杂度是 O(n^2)?

#include <stdio.h>
#include <math.h>

/* isprime                                                           */
/* Test if each number in the list from stdin is prime.              */
/* Output will only print the prime numbers in the list.             */

int main(int argc, char* argv[]) {

    int returnValue = 0;
    int i;
    int ceiling;
    int input = 0;
    int factorFound = 0;

    while (scanf("%d", &input) != EOF) {

        ceiling = (int)sqrt(input);
        if (input == 1) {
            factorFound = 1;
        }

        for (i = 2; i <= ceiling; i++) {
            if (input % i == 0) {
                factorFound = 1;
            } 
        }

        if (factorFound == 0) {
            printf("%d\n", input);
        }

        factorFound = 0;    
    } 

    return returnValue;
}

【问题讨论】:

  • 实际上是O(n)。算法是O(n^0.5),但while循环使它成为O(m*n^0.5)O(m)
  • 这是最基本的素数生成器的一种形式。问题是,当 N 变得非常大时,该算法运行非常缓慢。查看下面提到的 AKS 测试,以及一般的筛分理论。我也会为不同的用途使用不同的方法,这取决于你想要做什么:你需要小素数还是大素数?可能是素数还是“真正的”素数?算法和实现将根据要求而有所不同。
  • 理想情况下,由于您在 N(输入)上循环,您应该建立一个已知 PN 的表并用它来划分数字,每次尝试一个新数字时,它会越来越快。如果你想知道 1 到 N 之间有多少个 PN,primes.utm.edu/howmany.shtml
  • 请注意,这是一个长期受到严格学术审查的领域。如果您这样做是为了好玩或作为编码练习,那很好。如果你是认真的,你有相当多的阅读要做......
  • 我实际上检查了我的 SO userId...它不是素数 :(

标签: c algorithm performance primes


【解决方案1】:
   for (i = 2; i <= ceiling; i++) {
        if (input % i == 0) {
            factorFound = 1;
            break;
        } 
    }

这是第一个改进,并且仍然保持在“相同”算法的范围内。看这个完全不需要任何数学。

除此之外,一旦你看到input 不能被 2 整除,就没有必要检查 4、6、8 等。如果任何偶数除以 input,那么肯定会是 2,因为它将所有偶数相除。

如果您想稍微跳出算法,可以使用Sheldon L. Cooper 在他的回答中提供的循环。 (这比让他从 cmets 更正我的代码更容易,尽管他的努力值得赞赏)

这利用了除 2 和 3 之外的每个素数都具有 n*6 + 1n*6 - 1 的形式,用于某些正整数 n。要看到这一点,只需注意如果m = n*6 + 2m = n*6 + 4,则n 可以被2 整除。如果m = n*6 + 3 则它可以被3 整除。

事实上,我们可以更进一步。如果p1, p2, .., pk 是第一个k 质数,则所有与其乘积互质的整数都标出所有剩余质数必须放入的“槽”。

要看到这一点,只需让k# 成为直到pk 的所有素数的乘积。那么如果gcd(k#, m) = gg 除以n*k# + m,所以如果g != 1,这个和是微不足道的复合。因此,如果您想根据 5# = 30 进行迭代,那么您的互质整数是 1、7、11、13、17、19、23 和 29。


从技术上讲,我没有证明我最后的主张。难度不大

如果g = gcd(k#, m),那么对于任何整数,ng 整除n*k# + m,因为它整除k#,所以它也必须整除n*k#。但它也除以m,因此它必须除以总和。上面我只证明了n = 1。我的错。


另外,我应该注意到,这些都不会改变算法的基本复杂性,它仍然是 O(n^1/2)。它所做的只是大幅减少用于计算实际预期运行时间的系数。

【讨论】:

  • 总体上不错,但包含一些错误。您的第二个 sn-p 代码已损坏。另外,您写了n*5 - 1 而不是n*6 - 1。而m = n*6 + 3 总是能被 3 整除,而不是 6。
  • @Sheldon。谢谢。所有令人尴尬的错误,我很高兴能够修复它们。
  • 您的代码仍然不正确。它应该是input % (i - 1) == 0 而不是(input - 1)。而while循环的条件应该是(i-1) &lt;= ceiling.
  • 也许我是书呆子,但现在第一个 sn-p 代码是错误的。我在之前的评论中指的是 while 循环。此代码将失败,输入 = 2。在这种情况下,条件应该只是 i &lt;= ceiling
  • @Sheldon 我知道你的意思。今天我的编辑功能严重不足。
【解决方案2】:

算法中每个素性检验的时间复杂度为O(sqrt(n))

您始终可以使用除 2 和 3 之外的所有素数都具有以下形式的事实:6*k+16*k-1。例如:

int is_prime(int n) {
  if (n <= 1) return 0;
  if (n == 2 || n == 3) return 1;
  if (n % 2 == 0 || n % 3 == 0) return 0;
  int k;
  for (k = 6; (k-1)*(k-1) <= n; k += 6) {
    if (n % (k-1) == 0 || n % (k+1) == 0) return 0;
  }
  return 1;
}

这种优化不会提高渐近复杂度。

编辑

鉴于您在代码中反复测试数字,您可能需要预先计算素数列表。只有 4792 个质数小于或等于 INT_MAX 的平方根(假设为 32 位整数)。

另外,如果输入的数字比较小,可以尝试计算sieve

以下是两种想法的结合:

#define UPPER_BOUND 46340  /* floor(sqrt(INT_MAX)) */
#define PRIME_COUNT 4792  /* number of primes <= UPPER_BOUND */

int prime[PRIME_COUNT];
int is_prime_aux[UPPER_BOUND];

void fill_primes() {
  int p, m, c = 0;
  for (p = 2; p < UPPER_BOUND; p++)
    is_prime_aux[p] = 1;
  for (p = 2; p < UPPER_BOUND; p++) {
    if (is_prime_aux[p]) {
      prime[c++] = p;
      for (m = p*p; m < UPPER_BOUND; m += p)
        is_prime_aux[m] = 0;
    }
  }
}

int is_prime(int n) {
  if (n <= 1) return 0;
  if (n < UPPER_BOUND) return is_prime_aux[n];
  int i;
  for (i = 0; i < PRIME_COUNT && prime[i]*prime[i] <= n; i++)
    if (n % prime[i] == 0)
      return 0;
  return 1;
}

在程序开始时调用fill_primes,然后再开始处理查询。它运行得非常快。

【讨论】:

  • “每个素性测试的时间复杂度是 O(sqrt(n))”——几乎没有!试除法(OP 的算法)很慢(直到对数因子),但 APR 或 AKS 渐近更快。
  • @Charles:好的,已添加说明。
【解决方案3】:

那里的代码只有 O(sqrt(n)lg(n)) 的复杂度。如果您假设基本的数学运算是 O(1)(在您开始使用 bignums 之前为真),那么它只是 O(sqrt(n))。

请注意,可以在比 O(sqrt(n)lg(n)) 更快的时间内执行素数测试。 This site 有许多 AKS primality test 的实现,已被证明可以在 O((log n)^12) 时间内运行。

还有一些非常非常快的概率测试 - 虽然速度很快,但有时会给出不正确的结果。例如Fermat primality test:

给定一个数字p,我们要测试素数,选择一个随机数a,并测试是否a^(p-1) mod p = 1。如果为假,p 绝对不是素数。如果为真,p可能是素数。通过使用a的不同随机值重复测试,可以降低误报的概率。

请注意,此特定测试存在一些缺陷 - 请参阅 Wikipedia 页面了解详细信息以及您可以使用的其他概率素数测试。

如果您想坚持当前的方法,仍然可以进行一些小的改进 - 正如其他人指出的那样,在 2 之后,所有进一步的素数都是奇数,因此您可以跳过两个潜在因素一次在循环中。当你找到一个因素时,你也可以立即爆发。但是,这不会改变算法的渐近最坏情况行为,它保持在 O(sqrt(n)lg(n)) - 它只是改变了最好的情况(到 O(lg(n))),并且将常数因子减少大约二分之一。

【讨论】:

    【解决方案4】:

    一个简单的改进是改变 for 循环,让它在找到一个因子时中断:

       for (i = 2; i <= ceiling && !factorFound; i++) {
            if (input % i == 0) {
                factorFound = 1;
    

    另一种可能性是将计数器增加 2(在检查 2 本身之后)。

    【讨论】:

      【解决方案5】:

      您能否提出改进建议

      给你……不是为了算法,而是为了程序本身:)

      • 如果您不打算使用 argcargv,请删除它们
      • 如果我输入“fortytwo”会怎样?比较 scanf() == 1,而不是 != EOF
      • 无需转换sqrt()的值
      • returnValue 不需要,你可以返回一个常量:return 0;
      • 不要将所有功能都包含在 main() 函数中,而是将您的程序分成尽可能多的函数。

      【讨论】:

        【解决方案6】:

        您可以在不增加太多代码复杂性的情况下对算法进行削减。 例如,您可以跳过验证中的偶数,并在找到因素后立即停止搜索。

        if (input < 2 || (input != 2 && input % 2 == 0))
          factorFound = 1;
        
        if (input > 3)
          for (i = 3; i <= ceiling && !factorFound; i += 2)
            if (input % i == 0)
              factorFound = 1;
        

        关于复杂度,如果 n 是您的输入数字,那么复杂度不会是 O(sqrt(n)),因为您最多只能进行 sqrt(n) 除法和比较吗?

        【讨论】:

          【解决方案7】:

          偶数(除了2)不能是素数。所以,一旦我们知道这个数不是偶数,我们就可以检查奇数是否是它的因数。

          for (i = 3; i <= ceiling; i += 2) {
                  if (input % i == 0) {
                      factorFound = 1;
                      break;
                  } 
              }
          

          【讨论】:

          • -1 “偶数不能是素数”是错误的陈述。只有一个偶素数,即 2。
          【解决方案8】:

          你的程序的时间复杂度是O(n*m^0.5)。使用n 输入中的素数。 m 输入中最大素数的大小,或者MAX_INT,如果你愿意的话。所以复杂度也可以写成O(n),其中n是要检查的素数。

          对于 Big-O,n(通常)是输入的大小,在您的情况下,这将是要检查的素数的数量。如果我把这个列表加倍(例如复制它),它会花费(+-)两倍的时间,因此O(n)

          【讨论】:

            【解决方案9】:

            这是我的算法,复杂度仍然是O(n^0.5),但我设法删除了代码中一些昂贵的操作...

            算法最慢的部分是modulus 操作,我已经设法消除sqrt 或做i * i &lt;= n

            这样我可以节省宝贵的周期......它基于sum of odd numbers is always a perfect square.这一事实

            既然我们正在迭代odd numbers,为什么不利用它呢? :)

            int isPrime(int n)
            {
                int squares = 1;
                int odd = 3;
            
                if( ((n & 1) == 0) || (n < 9)) return (n == 2) || ((n > 1) && (n & 1));
                else
                {
                    for( ;squares <= n; odd += 2)
                    {
                        if( n % odd == 0) 
                            return 0;
                        squares+=odd;
                    }
                    return 1;
                }
            }
            

            【讨论】:

              【解决方案10】:
              #include <stdio.h>
              #include <math.h>
              
              int IsPrime (int n) {
                int i, sqrtN;
                if (n < 2) { return 0; } /* 1, 0, and negatives are nonprime */
                if (n == 2) { return 2; }
                if ((n % 2) == 0) { return 0; } /* Check for even numbers */
                sqrtN = sqrt((double)n)+1; /* We don't need to search all the way up to n */
                for (i = 3; i < sqrtN; i += 2) {
                  if (n % i == 0) { return 0; } /* Stop, because we found a factor! */
                }
                return n;
              }
              
              int main()
              {
                int n;
                printf("Enter a positive integer: ");
                scanf("%d",&n);
                if(IsPrime(n))
                   printf("%d is a prime number.",n);
                else
                   printf("%d is not a prime number.",n);
                return 0;
              }
              

              【讨论】:

                【解决方案11】:

                没有办法改进算法。可能有一些微小的方法可以改进您的代码,但不是算法的基本速度(和复杂性)。

                编辑:当然,因为他不需要知道所有因素,只要知道它是否是质数。好地方。

                【讨论】:

                • 这是错误的。存在用于素数测试的 O((log n)^12) 算法。 en.wikipedia.org/wiki/Primality_test#Fast_deterministic_tests 其中一些算法在实践中可能并不实用,但肯定有比 O(n^2) 更好的算法。
                • THIS算法没办法改进,但他可以替换整个算法。所以答案是有效的。
                猜你喜欢
                • 2019-05-17
                • 1970-01-01
                • 2020-02-17
                • 2018-10-02
                • 2014-10-20
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2010-12-31
                相关资源
                最近更新 更多