求最大公約數不同演算法的時間比較(輾轉相除法,更相減損術等)

可樂yue發表於2019-03-07

後附完整原始碼:(含有求最大公倍數的函式,此程式碼測試的是面對不同組資料,各種演算法的效能分析,如需輸出最大公約數,只需加個輸出就好!)

一、 題目分析

首先,題目要求我們求出倆個正整數的最大公約數和最小公倍數,並提供了6種不同的演算法(遞迴與非遞迴算倆種)
1. 輾轉相除法:此種方法核心如其名,輾轉,大數放a中,小數放b中。大的除以小的,餘數為零,則小數為最大公約數,不為零則,將餘數與b比較大的放a,小的放b,如此往復!
2. 窮舉法:看到這個窮舉就知道它是最吃力不討好的演算法了。一個個窮舉不僅浪費CPU,還浪費時間對吧,對兩個正整數a,b如果能在區間[a,0]或[b,0]內能找到一個整數temp能同時被a和b所整除,則temp即為最大公約數。
3. 更相減損術
第一步:任意給定兩個正整數;判斷它們是否都是偶數。若是,則用2約簡;若不是則執行第二步。
第二步:以較大的數減較小的數,接著把所得的差與較小的數比較,並以大數減小數。繼續這個操作,直到所得的減數和差相等為止。
則第一步中約掉的若干個2與第二步中等數的乘積就是所求的最大公約數。
其中所說的“等數”,就是最大公約數。求“等數”的辦法是“更相減損”法。所以更相減損法也叫等值演算法!
4. Stein演算法
通過數學的思想進行驗證出:對於倆個倆個正整數數(x>y):
均為偶數gcd(x,y)=2gcd(x/2,y/2);
均為奇數gcd(x,y)=gcd((x+y)/2,(x-y)/2);
x奇y偶gcd(x,y)=gcd(x,y/2);
x偶y奇gcd(x,y) =gcd(x/2,y)或gcd(x,y)=gcd(y,x/2);
也就是說可以對倆奇倆偶和一奇一偶的情況進行化簡,使其更容易計算。
又因為該演算法採用位移運算所以減少了很多時間,所以也是很厲害的演算法。
我們的主要目的是為了比較各個演算法的效能優劣,通過程式的執行,輸出不同演算法計算20,40,80…200組甚至更大的資料的計算時間。

二、 演算法構造

可愛的狗狗走錯片場:
在這裡插入圖片描述
① 輾轉相除法(非遞迴):
演算法流程圖:
在這裡插入圖片描述
② 輾轉相除法(非遞迴):
演算法盒圖:
在這裡插入圖片描述

③ 輾轉相除法(遞迴):
在這裡插入圖片描述

④ 輾轉相除法(遞迴盒圖):
在這裡插入圖片描述
⑤ 窮舉法流程圖:
在這裡插入圖片描述
⑥ 窮舉法(盒圖):
在這裡插入圖片描述
⑦ 更相減損術(流程圖):
在這裡插入圖片描述

⑧ 更相減損術(盒圖):
在這裡插入圖片描述
⑨ Stein演算法(流程圖):
在這裡插入圖片描述
⑩ Stein演算法(遞迴盒圖):
在這裡插入圖片描述

三、 演算法實現

#include<iostream>
using namespace std;
#include<math.h>
#include<windows.h>
#include<time.h>
class Math
{
private:
	int temp;
public:
	int divistor(int,int); //NO.1,輾轉相除法
	int multiple(int,int);

    int digui(int,int);    //NO.2,輾轉相除法遞迴
	int multiple1(int,int);

	int divistor3(int,int);//NO.3,窮舉法
	int multiple3(int,int);

    int divistor4(int,int);//NO.4,更相減損術
	int multiple4(int,int);

    int gcd(int,int);     //NO.5,stein演算法遞迴演算法
	int multiple5(int,int);//求最大公倍數

    int gcd1(int,int);     //NO.6,stein演算法非遞迴演算法
	int multiple6(int,int);//求最大公倍數

};

int Math::divistor(int x,int y)//x是大數,y是小數,非遞迴實現   NO.1
{
	if(x<y)
	{
		temp=x;
		x=y;
		y=temp;
	}
	while(y!=0)       //當y是0時,最大公約數就是x;
	{
		temp=x%y;
		x=y;          //這3句不能交換位置,否則temp值會出錯
		y=temp;
	}
	return x;
}

int Math::multiple(int x,int y)  //求最大公倍數
{
	temp=divistor(x,y);//
	return ((x*y)/temp);
}



int Math::digui(int x,int y)//無需區別x,y的大小,遞迴方法
{
	if(x%y==0)
		return y;
	else
		return digui(y,x%y);
}

int Math::multiple1(int x,int y)  //求最大公倍數
{
	temp=digui(x,y);
	return ((x*y)/temp);
}



int Math::divistor3(int x,int y)
{
	temp=(x>y)?y:x;
    while(temp>0)
	{
		if(x%temp==0&&y%temp==0)
			break;
		else temp--;
	}
		return temp;
}

int Math::multiple3(int x,int y)
{
	int p,q;
	p=x>y?x:y;//xiaozhi
	q=x>y?y:x;//dazhi
	temp=p;
	while(1)
	{
		if(p%q==0)
			break;
		else
			p+=temp;
	}
	return p;
}



int Math::divistor4(int x,int y)
{
	int i=0;
	int z=1;
	while(x%2==0 && y%2==0)  //判斷m和n能被多少個2整除
	{
		x/=2;
		y/=2;
		i+=1;
	}
	if(x<y)     //x是大數
	{
		temp=x;
		x=y;
		y=temp;
	}
	while(z)
	{
		z=x-y;
		x=(y>z)?y:z;
		y=(y<z)?y:z;
		if(y==x)
			break;
	}
	if(i==0)
		return y;
	else 
		return (int )pow(2,i)*y;
}

int Math::multiple4(int x,int y)  //求最大公倍數
{
	temp=divistor4(x,y);
	return ((x*y)/temp);
}


int Math::gcd(int x,int y)     //stein演算法遞迴演算法
{
	if(x<y)     //x是大數
	{
		temp=x;
		x=y;
		y=temp;
	}
	if(y==0) return x;
	if((x&0x1)==0&&(y&0x1)==0) return 2*gcd(x>>1,y>>1); //倆數都是偶數
	if((x&0x1)==0&&(y&0x1)!=0) return gcd(x>>1,y);     //一偶一奇
    if((x&0x1)!=0&&(y&0x1)==0) return gcd(x,y>>1);     //一奇一偶
	if((x&0x1)!=0&&(y&0x1)!=0) return gcd((x-y)>>1,y); //倆奇
}

int Math::multiple5(int x,int y)  //求最大公倍數
{
	temp=gcd(x,y);
	return ((x*y)/temp);
}

int Math::gcd1(int a,int b)        //stein演算法非遞迴
{
    int acc=0;
    while((a&0x1)==0&&(b&0x1)==0)
	{
        ++acc;
        a>>=1;
        b>>=1;
    }
    while((a&0x1)==0) a>>=1;
    while((b&0x1)==0) b>>=1;
    if(a<b) {int t=a; a=b; b=t;}
    while((a=(a-b)>>1)!=0)
	{
        while((a&0x1)==0) a>>=1;
        if (a<b) {int t=a; a=b; b=t;}
    }
    return b<<acc;
}

int Math::multiple6(int x,int y)  //求最大公倍數
{
	temp=gcd1(x,y);
	return ((x*y)/temp);
}
int main()
{
	int x,y,i,t;                  //t為陣列的元素個數
	char c[20];                   //用於判斷整數的正確性
	Math m;                       //建立物件m
	double z;                
	LARGE_INTEGER nFreq;          //時間
	LARGE_INTEGER nBeginTime;
	LARGE_INTEGER nEndTime;
	QueryPerformanceFrequency(&nFreq);

	cout<<"請輸入小於10000的測試組數(正整數:一組倆個數):"<<endl;
	cin.getline(c,7);                //7為函式讀取的制定字元
	if((c[0]-48)<=0||(c[0]-48)>9) {cout<<"輸入的不是正整數,請重新輸入!"; return 0;}
	for(i=1;i<20;i++)
	{
		if(c[i]=='\0') break;
		if((c[i]-48)<0||(c[0]-48)>9) {cout<<"輸入的不是正整數,請重新輸入!"; return 0;}
	}
	i=atoi(c);
	if(i<0||i>10000) {cout<<"輸入數超出範圍,請重新輸入!"; return 0;}
	int a[20000];
    t=2*i;
	srand((unsigned)time(NULL));  //產生隨機數種子,避免產生的隨機數相同
	for(i=0;i<t;i++)              //初始化陣列
	{
		a[i]=rand();
	}

    QueryPerformanceCounter(&nBeginTime);//開始計時
	for(i=0;i<t;i++)
	{
		x=a[i];
		y=a[i+1];
		i+=1;                     //讓x,y正好取得陣列中的元素,如果沒有i+=1會出錯
     	m.divistor(x,y);
	    //m.multiple(x,y);
	}
	QueryPerformanceCounter(&nEndTime);//結束計時
	z=(double)(nEndTime.QuadPart-nBeginTime.QuadPart)/(double)nFreq.QuadPart;//計算程式執行時間單位為s
	cout<<"輾轉相除法非遞迴所用時長:"<<z*1000<<"毫秒"<<endl;

	QueryPerformanceCounter(&nBeginTime);//開始計時
	for(i=0;i<t;i++)
	{
		x=a[i];
		y=a[i+1];
		i+=1;                     //讓x,y正好取得陣列中的元素,如果沒有i+=1會出錯
     	m.digui(x,y);
	    //m.multiple1(x,y);
	}
	QueryPerformanceCounter(&nEndTime);//結束計時
	z=(double)(nEndTime.QuadPart-nBeginTime.QuadPart)/(double)nFreq.QuadPart;//計算程式執行時間單位為s
	cout<<"輾轉相除法遞迴所用時長:"<<z*1000<<"毫秒"<<endl;

	QueryPerformanceCounter(&nBeginTime);//開始計時
	for(i=0;i<t;i++)
	{
		x=a[i];
		y=a[i+1];
		i+=1;                     //讓x,y正好取得陣列中的元素,如果沒有i+=1會出錯
     	m.divistor3(x,y);
	    //m.multiple3(x,y);
	}
	QueryPerformanceCounter(&nEndTime);//結束計時
	z=(double)(nEndTime.QuadPart-nBeginTime.QuadPart)/(double)nFreq.QuadPart;//計算程式執行時間單位為s
	cout<<"窮舉法所用時長:"<<z*1000<<"毫秒"<<endl;

	QueryPerformanceCounter(&nBeginTime);//開始計時
	for(i=0;i<t;i++)
	{
		x=a[i];
		y=a[i+1];
		i+=1;                     //讓x,y正好取得陣列中的元素,如果沒有i+=1會出錯
     	m.divistor4(x,y);
	    //m.multiple4(x,y);
	}
	QueryPerformanceCounter(&nEndTime);//結束計時
	z=(double)(nEndTime.QuadPart-nBeginTime.QuadPart)/(double)nFreq.QuadPart;//計算程式執行時間單位為s
	cout<<"更相減損術法所用時長:"<<z*1000<<"毫秒"<<endl;

	QueryPerformanceCounter(&nBeginTime);//開始計時
	for(i=0;i<t;i++)
	{
		x=a[i];
		y=a[i+1];
		i+=1;                     //讓x,y正好取得陣列中的元素,如果沒有i+=1會出錯
     	m.gcd(x,y);
	    //m.multiple5(x,y);
	}
	QueryPerformanceCounter(&nEndTime);//結束計時
	z=(double)(nEndTime.QuadPart-nBeginTime.QuadPart)/(double)nFreq.QuadPart;//計算程式執行時間單位為s
	cout<<"stein演算法非遞迴所用時長:"<<z*1000<<"毫秒"<<endl;

	QueryPerformanceCounter(&nBeginTime);//開始計時
	for(i=0;i<t;i++)
	{
		x=a[i];
		y=a[i+1];
		i+=1;                     //讓x,y正好取得陣列中的元素,如果沒有i+=1會出錯
     	m.gcd1(x,y);
	    //m.multiple6(x,y);
	}
	QueryPerformanceCounter(&nEndTime);//結束計時
	z=(double)(nEndTime.QuadPart-nBeginTime.QuadPart)/(double)nFreq.QuadPart;//計算程式執行時間單位為s
	cout<<"stein演算法遞迴所用時長:"<<z*1000<<"毫秒"<<endl;
	return 0;
}

四、執行結果:

③結果在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
通過不同組資料的測試我們發現:
當資料小的時候:輾轉相除法,更相減損術,stein演算法,都比較用時少。
只有窮舉法用時最長。(此時區別還不明顯!)
但資料變大的時候:stein遞迴演算法的好處就顯現出來,次之的就是輾轉相除法(非遞迴)了。

五、總結

這次的上機報告真的收穫了許多,比較演算法我也在上學期做過,比較不同排序的演算法效能,當時用的是clock()函式,因為測試的資料比較龐大,有30000組資料,所以clock()函式已經足夠顯現出演算法的區別。但是這次的程式,讓我學到了一個新的測試時間的函式,它更精確,區別更明顯!
高精度時控函式QueryPerformanceFrequency(),QueryPerformanceCounter()
原理:
QueryPerformanceCounter()這個函式返回高精確度效能計數器的值,它可以以微妙為單位計時.但是QueryPerformanceCounter()確切的精確計時的最小單位是與系統有關的,所以,必須要查詢系統得到QueryPerformanceCounter()返回的嘀噠聲的頻率. QueryPerformanceFrequency()提供了這個頻率值,返回每秒嘀噠聲的個數. 計算確切的時間是從第一次呼叫QueryPerformanceCounter()開始的 假設得到的LARGE_INTEGER為nStartCounter,過一段時間後再次呼叫該函式結的, 設得到nStopCounter. 兩者之差除以QueryPerformanceFrequency()的頻率就是開始到結束之間的秒數.由於計時函式本身要耗費很少的時間,要減去一個很少的時間開銷.但一般都把這個開銷忽略。
還有對字串有了更深一步的理解,cin,cin.getline(),cin.get()函式的區別!
cin.get()函式與cin.getline()函式區別:
cin.getline():在遇到回車符時,結束字串輸入並丟棄回車符。
cin.get():在遇到回車符時,則會保留回車符在輸入佇列。

相關文章