質數計算這一個我也是想了很久,網上參考偏少,但是自己也是想到了不少覺得值得分享的東西。下面就簡介一下。
1、合數分類的方法不是隻有一個。
2、一定範圍內的合數必定由兩個質數相乘。
3、週期性。
針對以上的特點我一共開發了10個不同的計算質數的版本。由於C++掌握的不好,這裏貼兩個源碼給大家看看。
1) 合數的分類不止一種方法。
我們先來說最直接的分類法,我們先排除2的所有倍數,再排除3的所有倍數,一直下去……這麼做的過程中,其實我們默認了一個分類方法:合數的集合可以分解爲 所有質數的倍數的集合的並集。即2的倍數集合、3的倍數集合、5……這樣有一個缺點就是一個數可能屬於多個子集,從而導致計算效率的下降。怎樣減少這一問題所帶來的效率影響,放在後面說,現在討論如何怎樣完全解決這一問題。
其實看到上面的分析,我們不難發現要根除這一問題並不困難,只要我們找一種分解合數集合的方案,使得各個子集的並集爲合數總集,並且各個子集沒有公共元素。最開始我的想法是按照因子的個數進行分類,即 2個質數因子構成的合數,3個質數因子構成的合數……但在開發的時候發現略有困難【不代表不可能,只是我沒做到】,於是我在找了一個集合分解方案,假如我們知道質數集合{2,3,5,7……};
第一個合數集合(2對應):2的n次方 (n>2);
第二個合數集合(3對應):已知的合數(2對應的合數)與3的任意次方的乘積U比當前元素小的質數與3的任意次方的乘積U3的n次方 (n>2);
……
第一個集合是2能構成的所有合數;第一個集合U第二個集合 是質數2和質數3能構成的所有合數;說到這裏希望大家明白我說了什麼。至於第二個問題就是第一個集合與第二個集合沒有任何交集,這一點是顯而易見的,第二個集合都能被3整除,而第一個集合只能被2整除。
下面給出的C++代碼就是利用了上面的這一分解方案,經過測試是ok的。
// ConsoleApplication2.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <math.h>
#include <iostream>
#include <fstream>
#include <time.h>
#include <conio.h>
#include <cstring>
#include <vector>
#include <iterator>
using namespace std;
#define len 500000000 //可以對2*len的所有質數求解
// 定義全局變量
bool prime_mark[len] = {}; // i位置表示 2*i+1的數,true 表示合數,false表示質數
int turn_array[1000000] = {}; //記錄須要翻轉的元素 修改爲鏈表形式對速度有優化
int main()
{
int k_time = 0;
int i_prime = 1; // 質數2 默認存在
time_t start, end;
start = clock(); // 時間
int range = len * 2 -1;
prime_mark[0] = true;
// prime_array[0] = 2;
// string astr="";
const int nFrameSize = 5000000;
int i_now = 0; //現在這批翻轉元素的起始位置
int i_len = 0; //turn_array 的存儲元素長度
long temp_data = 0;
int prime_now = 0; // 記錄當前循環下的質數
// cout<<"hello word";
long prime_loop = 0; // 後面循環用,
//ofstream of1("D:\\c_pluse_test\\test2.txt"); // 輸出目標文件
int i_prime_mark = 0;
while (i_prime_mark<len){
if (!prime_mark[i_prime_mark]){ // 如果是質數
i_prime++;
prime_now = i_prime_mark * 2 + 1; // 記錄當前循環下的質數
// prime_array[i_prime] = prime_now;
// 構成當前混合組
// 以下爲第一個部分 原有部分*新質素
if (i_prime_mark * 3 > len){
i_prime_mark++;
continue;
}
i_now = i_len;
for (int i_turn_array = 0; i_turn_array < i_now; i_turn_array++){ //所有已知的上一批次的合數
prime_loop = prime_now;
if (turn_array[i_turn_array]>(range / prime_loop))
continue;
for (;; prime_loop *= prime_now){
temp_data = turn_array[i_turn_array] * prime_loop;
if (temp_data <= range){
prime_mark[(temp_data - 1) / 2] = true; // 翻轉
if (temp_data <= range / prime_now){
turn_array[i_len++] = temp_data; // 存入下一次繼續計算的初始
}
}else{
break;
}
if (turn_array[i_turn_array] > (range / (prime_loop * prime_now)))
break;
}
}
// 翻轉純粹由當前質數構成的合數;
temp_data = prime_now;
while (true){
turn_array[i_len++] = temp_data; //存入下一次繼續計算的初始
if (temp_data >= (range / prime_now))
break;
temp_data *= prime_now;
prime_mark[(temp_data - 1) / 2] = true; // 翻轉
}
// 收縮數組
int i = 0;
int j = i_len-1;
int max_data = (range / (prime_now + 2));
while (true){
while ((turn_array[i] <= max_data) && (i <= j)){
i++;
} // 從左端找到一個無效數
while ((turn_array[j] > max_data) && (i <= j)){
j--;
} // 從右端找到一個有效數
if (i >= j){
i_len = i + 1; //一共翻轉了多少個元素
break;
}else{
turn_array[i] = turn_array[j];
j--;
}
}
}
//of1 << 1 << endl;
i_prime_mark++;
}
end = clock();
cout << end - start << endl << i_prime << endl;
}
上面這一段代碼就是前面想法的實現,由於我個人原因裏面的turn_array[]是數組形式,可以改爲用指針實現的鏈表,這樣後面的刪除操作就簡單多了。好啦,源碼都貼出來了,我也不解釋了。
2、一定範圍內的合數是由兩個已知質數相乘組成
這句話可能有點費解,我舉個栗子:假如我們把2,3,5所有的倍數已經排除了,我們便知道了下面這件事:小於下一個質數平方49的所有質數{7,11,13,17,19,23,29,31,37,41,43,47};現在我們到數字7的時候,我們排除掉7與{7,11,13,17,19,23……}的所有乘積。之後7的3次方之內還有合數沒被排除嗎?
首先:2,3,5的倍數已經被清除,所以剩下的未排除合數因子都大於等於7。
其次:最小由三個因子組成的質數爲7的3次方,所以7的三次方以內的未排除合數都是兩個兩個質數的乘積。
所以上面的問題答案是否定的。這就導致了一個優化點,可以一定程度上減少重複排除。
下面是源碼,不解釋了!
#include "stdafx.h"
#include <math.h>
#include <iostream>
#include <fstream>
#include <time.h>
#include <conio.h>
#include <cstring>
#include <vector>
#include <iterator>
using namespace std;
#define len 1000000000 + 1 //可以對2*len的所有質數求解
// 定義全局變量
#define greak 100
#define d_step 2 // 步長爲2 偶數不看
#define prime_num 100000
bool prime_mark[len] = {}; // i位置表示 2*i+1的數,true 表示合數,false表示質數
int prime[prime_num] = {}; //記錄前10000個質素
int main()
{
time_t start, end;
start = clock();
int prime_last=11; // 最大一個質數
int prime_all; // 質數的總個數
int prime_max = 0;
int loop_num;
int i_loop;
int loop_max;
int loop_end;
int i_prime_now = 0;
int i_prime_end = 0; //已經可以判定的質數
int i_prime_mark = 0; // 從4開始
int mark_end = 3;
prime[0] = 2;
i_prime_now = 0;
prime[1] = 3;
prime[2] = 5;
prime[3] = 7;
i_prime_end = 1;
while (true){
i_prime_now++;
if (i_prime_end < prime_num - 1){ //還可以繼續放質數
i_prime_mark = mark_end + d_step;
if (prime[i_prime_now] >(len - 1) / prime[i_prime_now])
mark_end = len - 1;
else
mark_end = prime[i_prime_now] * prime[i_prime_now];
while ((i_prime_end < prime_num)&(i_prime_mark<mark_end)){
if (!prime_mark[i_prime_mark]){ // 質數
i_prime_end++;
prime[i_prime_end] = i_prime_mark;
// cout << i_prime_mark << endl;
}
i_prime_mark += d_step;
}
}
//cout << prime[i_prime_now]<<endl;
if (prime[i_prime_now] >(len - 1) / prime[i_prime_now])
break;
// 排除
i_loop = i_prime_now;
loop_max = (len - 1) / prime[i_prime_now];
if (prime[i_prime_end] < (len - 1) / prime[i_prime_now]){
while (i_loop <= i_prime_end)
prime_mark[prime[i_loop++] * prime[i_prime_now]] = true;
i_loop = prime[i_loop - 1] + d_step;
for (; i_loop <= loop_max; i_loop += d_step)
prime_mark[i_loop*prime[i_prime_now]] = true;
}
else{
while (prime[i_loop] <= loop_max){
prime_mark[prime[i_loop++] * prime[i_prime_now]] = true;
}
}
}
// 計算質數總個數
prime_all = 1;
for (int i =3; i < len - 1; i += d_step){
if (!prime_mark[i]){
prime_all++;
}
}
end = clock();
cout << end - start << endl <<prime_all<<endl;
//std::cin >> i_loop ;
}
其實這裏可以說是最直接的分類方法外加一點優化,從潛力來說,應該是劣於第一種,但目前比第一種快。另外一個值得一提的是:當一部分質數的倍數被排除後(如2,3,5),到下一個質數7時,需要排除的元素一定是所有大於5並且尚未被排除的數的集合與7的乘積,而源碼中利用49以內的質數其實只是其中的一個子集。所以如果結合這一點來做進一步優化完全不是問題。
3、週期性;
大家想象一下,那個標記數組已經翻轉了所有2的倍數,3的倍數;如果細心觀察這個數組就會發現他有周期,而且週期爲2*3;所以我們可以利用這一點把最開始的6個狀態賦值給後面的所有元素。當翻轉5的所有倍數時,週期爲2*3*5。不過可惜的是當質數超過23後周期就大於10億了,不再有任何幫助。這裏說出來,只是讓大家知道這件事,或許某人就有好的利用方案也說不定。
我希望大家不要看源碼,而是根據我說的話,結合自己的思想創造出來,給源碼一方面是應對某些同學的需求,另一方面是想告訴你這不僅是理論,完全是可以開發的。(時間倉促,如有各種瑕疵,請海涵)。