小小c#算法題 - 2 - 求素數


題目:(a) 求1~n內的所有素數。

         (b) 找出一個無序的整數數組內的所有素數。

這兩個題目是同樣的解法,只不過形式變了一下。這里只對(a)給出解法,(b)類似。

(1)第一種解法

首先明白什么是素數,素數也叫質數,就是只能被1和它本身整除的整數。像1,2,3,5,7都是素數。

明白了素數的概念,那么程序就很容易寫出來了。要想知道一個數x是否是素數,那么只需要把1~x之間的所有整數來除x,如果存在這么一個數(1和x除外),其能夠整除x(余數為0),那么x就不是素數,否則x就是素數。這是從概論出發的解法。那么下面就要看怎么優化了。

 

其實我們沒有必要檢查1~x之間的所有整數。只需檢查1~[根號x]之間的數就可以了。至於為什么,可以從乘法的角度理解,我們在做除法,其實反過來也就是乘法。我們用判斷15是否是素數的情況來舉個例子。當我們除3的時候,3*5=15。所以,在判斷3的同時也判斷了另一個數5。所以我們只需要檢查到根號x就可以了,當檢查到根號x的時候,根號x之后也檢查過了。

代碼(c#): 

         private  static  void PrintPrimes( int n)
        {

             // 輸出1~n之間的所有素數,n>=3
            Console.Write( " 1 2  ");
             int i, j =  0;
             for (i =  3; i <= n; i = i +  2)
            {
                 int k = ( int)Math.Sqrt(i);
                 for (j =  2; j <= k; j++)
                {
                     if ((i % j) ==  0)
                    {
                         break;
                    }
                }

                 if (j > k)
                {
                    Console.Write(i.ToString() +  "   ");
                }
            }
        }

 

注意for循環里遞增的間隔是2,而不是i++。因為偶數肯定不是素數,這樣也在一定程度上提高了效率。

 

(2)第二種解法

由於1~n是有序的且遞增的,所以可以有這么一種解法:建立一個標識數組,長度為n,下標與1~n每個數對應,在每次判斷完一個數是否是素數之后,將該數對應的標識位重置,0為素數,1為非素數,並且將這個數的所有倍數的標識位置1,這里其實就是剔除一些重復判斷。最后遍歷標識數組,輸出所有素數。但這樣做的代價是額外開辟了一段內存做為標識數組。當然,如果原數組不需要保存的話,你也可以直接在原數組上進行操作。

聽起來不錯,但其實比第一種方法快不了多少。通過用Stopwatch看運算時間,這種解法並不怎么樣,只有在數據量比較大的時候,其運行效率會比上一種方法要高那么一點點。我想是因為2的倍數太多了(方法一種循環步增是2),所以方法一已經剔除了相當一部分,再有,為每個素數計算倍數,然后再為標識數組賦值,也需要時間。

代碼(c#):

        private static void PrintPrimes2(int n)
        {
            int[] flags = new int[n];
            int j = 2;
            for (int i = 2; i <= n; i++)
            {
                if (flags[i - 1] == 0)
                {
                    if (!IsPrime(i))
                    {
                        flags[i - 1] = 1;
                    }

                    if (i <= n / 2)
                    {
                        j = 2;
                        while (i * j < n)
                        {
                            flags[i * j - 1] = 1;
                            j++;
                        }
                    }
                }
            }

            for (int k = 1; k < n; k++)
            {
                if (flags[k] == 0)
                {
                    Console.Write((k + 1).ToString() + " ");
                }
            }
        }

        private static bool IsPrime(int x)
        {
            if (x < 1)
            {
                throw new ArgumentException("Cannot be less than 1");
            }

            if (x < 3)
            {
                return true;
            }
            else
            {
                int k = (int)Math.Sqrt(x);
                int i = 2;
                for (; i <= k; i++)
                {
                    if (x % i == 0)
                    {
                        break;
                    }
                }

                if (i > k)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }


 (3)第三種解法

如果能事先構造好素數庫,而且很容易在構造的時候使之有序。那么判斷一個數是否是素數的話,直接二分查找就能很快的判斷。如果要查找n范圍內的素數的話,直接把庫中小於n的所有數取出來即可。所以,怎么構造一個素數庫呢?

首先,從解法一中我們知道,判斷一個數是否為素數,只需與2~Sqrt(n)之間的整數做取余操作即可。這個判斷過程的效率可以進一步的提高。試想,如果3,6,9都屬於2~Sqrt(n)之間的整數,如果我們要判斷的整數不能被3整除的話,肯定也不能被6和9整除,所以這些步驟可以省略。我們在構造的時候完全可以利用已經被構造的素數序列!

假設我們已經有素數序列: p1, p2, .. pn

現在要判斷pn+1是否是素數, 則僅需要(1, sqrt(pn+1))范圍內的所有素數序列即可,而不是其間的所有整數。

而這個素數序列顯然已經作為p1, p2, .. pn的一個子集被包含了!

構造素數庫或素數序列的代碼如下:

方法創建2~num之間的素數序列並返回。

        static List<int> CreatePrimes(int num)
        {
            List<int> primes = new List<int>();
            primes.Add(2);
            primes.Add(3);

            for (int i = 5; i <= num; i += 2)
            {
                bool isPrime = true;

                // 為什么j從1開始:因為i是一個奇數加偶數,所以只能是奇數,所以不可能被2整除,所以primes集合的第一個
                // 元素2就不用計算了
                for (int j = 1; primes[j] * primes[j] <= i; ++j)
                {
                    if (i % primes[j] == 0)
                    {
                        isPrime = false; 
                        break;
                    }
                }

                if (isPrime)
                {
                    primes.Add(i);
                }
            }

            return primes;
        }

如此一來,如果求素數操作很頻繁的話,那么一開始就可以構造一個比較大的素數序列存儲起來。然后判斷素數時用二分查找法查找,取素數序列時也很方便。

有一篇博客對求素數的算法寫得也不錯,見http://www.cnblogs.com/luluping/archive/2010/03/03/1677552.html

 

最后想提醒一點的是,目前我們處理的都是int,當然你也可以換作long, double。只是終究是有個范圍的。如果以字符串的形式給了一個相當大的數,遠遠超出了long的范圍,那么怎么判斷呢?

1. 自己構造一個新的數據結構來存儲,具體我不清楚。曾經有個搞芯片開發的人跟我說這不是問題。

2. 自己寫方法把求余操作實現,其實就是寫代碼實現小學的除法運算。

就寫這么多吧,希望對大家有所幫助。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM