写在前面于2020年添加:
这一篇本身是我写的前几篇博客。(本身一共也没写几个233333。)我自己本身也不知道又哪些人会无意间点开这篇文章,至少在我现在看来这一篇确实是没有太实际的用处。或许从这个角度上来说应该删了或者重写吧!但是,对我来说这一篇确实是具有不一样的意义也就没舍得删除。为了节约大家的时间我就直接把我认为大家可能想看的东西放(盗用)到前面吧!(是评论区的一个人的评论)
手动感谢!!!
——————————————————————————手动分割——————————————————————————
以前在上课中有个老师曾提到过,有过一个公司的笔试题中要求大家把一个1~n的累加用递归的方式写出代码。当时老师只是简单的说了递归很多时候都比循环写起来更简单,而且递归也比循环更考验思维。
然后,就是在省赛中名校的翻模版中发现,那些模板中大多数使用的都是递归很少把循环作为模板的。而且还把我的一个 gcd函数从我的循环方法改成递归方法后发现时间上也减少了许多。于是我就在今天通过自己写的gcd函数和并查集函数分别用循环和递归谢了代码来比较他们在运行过程中时间上的差距。
首先不论是递归还是循环在运行过程中计算的次数是相同的,但是,对于不同的计算运行的时间却是有长有短。(比如说同样是计算使用位运算的时间就比四则运算的时间更长。)
首先,是gcd 的函数:第一个gcd 求最大公因数的函数代码一如下:(gcd为函数部分)
<pre name="code" class="cpp">#include<iostream>
#include<ctime>
using namespace std;
int gcd(int a,int b){
int c=a;
int d=b;
while(d!=0){
int z=c%d;
c=d;
d=z;
}
return c; //通过gcd函数最后返回值就是两个数的最大公因数
}
int main(){
int n;
while(cin>>n){
long long st_time= clock();
int num[10000]={0};
for(int j=0;j<n;j++)
cin>>num[j];
for(int j=1;j<n;j++){
for(int k=1;k<1000;k++) //因为使用的数据并不复杂,所以原本运行
gcd(num[j-1],num[j]); //时间太短无法测出。我在此时通过反复循
num[j]=gcd(num[j-1],num[j]);//多计算几次使得时间放大1000倍易于测量时间。
cout<<num[j]<<" "; //这里是答案的输出。
}
cout<<endl;
long long end_time=clock();
cout<<"运行时间:"<<end_time-st_time<<endl; //最后输出时间
}
}
下面是使用代码输出的结果:
(图1)代码一的运行结果
下面是使用递归写的代码二(gcd为函数部分):
<pre name="code" class="cpp">#include<iostream>
#include<ctime>
using namespace std;
int gcd(int a,int b){
return b==0?a:gcd(b,a%b); //通过gcd 函数递归最后返回值就是两个数的最大公因数<strong>
}
int main(){
int n;
while(cin>>n){
long long st_time= clock();
int num[10000]={0};
for(int j=0;j<n;j++)
cin>>num[j];
for(int j=1;j<n;j++){
for(int k=1;k<1000;k++) //因为使用的数据并不复杂,所以原本运行
gcd(num[j-1],num[j]); //时间太短无法测出。我在此时通过反复循
num[j]=gcd(num[j-1],num[j]); //多计算几次使得时间放大1000倍易于测量时间。
cout<<num[j]<<" "; //这里是答案的输出。
}
cout<<endl;
long long end_time=clock();
cout<<"运行时间:"<<end_time-st_time<<endl; //最后输出时间
}
}
看到循环的时间是5174而递归的时间是4333,最后我们可以得出结果递归确实比循环要更加的节约时间,但是.......
我还是在其中发现了一个漏洞,首先看看函数部分。
循环:
int gcd(int a,int b){
int c=a;
int d=b;
while(d!=0){
int z=c%d;
c=d;
d=z;
}
return c;
}
递归:
int gcd(int a,int b){
return b==0?a:gcd(b,a%b);
}
我们可以发现在这两个函数中循环就多定义了两个变量而递归中并没有定义。定义变量的时间确实很短的,但是,如果放大了1000倍它也有可能影响我们的最终结果。
所以,我就把循环代码进行了修改使得我不必在函数中再次进行变量的定义的代码三:(gcd函数的定义中为更改部分。)
</pre><pre name="code" class="cpp">#include<iostream>
#include<ctime>
using namespace std;
int gcd(int &a,int &b){ //使用这种定义可以直接对传入的参<strong>
while(b!=0){ //数进行操作也就不需要重新定义变量
int z=a%b;
a=b;
b=z;
}
return a;
}
int main(){
int n;
while(cin>>n){
long long st_time= clock();
int num[10000]={0};
for(int j=0;j<n;j++)
cin>>num[j];
for(int j=1;j<n;j++){
for(int k=1;k<1000;k++)
gcd(num[j-1],num[j]);
num[j]=gcd(num[j-1],num[j]);
cout<<num[j]<<" ";
}
cout<<endl;
long long end_time=clock();
cout<<"运行时间:"<<end_time-st_time<<endl;
}
}
改变代码后的运行结果如下图:
(图三)代码三的运行结果
把定义去除后我们发现函数的运算时间确实得到了改进。而且这个时间与不需要定义变量的递归运算的时间也相当接近。正因如此,我还尝试着把原来的递归函数中也增加了两个变量的定义的代码四:
<pre name="code" class="cpp">#include<iostream>
#include<ctime>
using namespace std;
int gcd(int a,int b){
int c=a; //此处为修改部分
int d=b; //此处为修改部分
return d==0?c:gcd(c,d);
}
int main(){
int n;
while(cin>>n){
long long st_time= clock();
int num[10000]={0};
for(int j=0;j<n;j++)
cin>>num[j];
for(int j=1;j<n;j++){
for(int k=1;k<1000;k++)
gcd(num[j-1],num[j]);
num[j]=gcd(num[j-1],num[j]);
cout<<num[j]<<" ";
}
cout<<endl;
long long end_time=clock();
cout<<"运行时间:"<<end_time-st_time<<endl;
}
}
最后运行结果是:
(图四)代码四的运行结果
我们可以发现多了重新定义自变量这一步后时间又增加了很多。甚至还超过了刚开始需要重新定义的循环的代码一的结果。
(图五)代码一的运行结果
所以,到这里我们可以确定通过递归本身并不比循环更快,他之所以会比循环的时间更短是因为它可以直接省去再次定义变量的阶段,如果我们有办法可以更少的在函数中对变量的定义则使用循环就未必比递归更差。
同时,我还是用并查集尝试了递归和循环的区别结果发现,它的递归写法能比循环写法节约更多的时间。
并查集中输入数据如下:
(图六)接下来并查集的输入
首先是并查集的循环写法代码五:(find是寻根函数部分。)
#include<iostream>
#include<string.h>
#include<ctime>
#include<stdio.h>
using namespace std;
int num[100];
int find(int a){
int r=a;
while(num[r]!=-1)
r=num[r]; //找到根节点<strong>
int z=a;
while(num[z]!=-1){
int l=num[z];
num[z]=r;
z=l; //找到根节点后进行路径压缩<strong>
}
return r; //最后返回根节点<strong>
}
int main(){
int n,m;
freopen("数据输入.in","r",stdin);//因为在这里输入的数据较多所以使用freopen
while(cin>>n>>m){ //读入数据以保证不会因为输入时间而影响结果
long long st_time=clock();
int count=0;
memset(num,-1,sizeof(num));
while(m--){
int a,b;
cin>>a>>b;
int c;
int d;
for(int j=0;j<10000000;j++){
c=find(a);
d=find(b);
}
if(c!=d){
num[c]=d;
count++;
}
}
cout<<n-count<<endl; //输出根节点的数量
long long end_time=clock();
cout<<"程序运行的时间"<<end_time-st_time<<endl;
}
fclose(stdin);
}
上述代码的运行结果如下:
(图七)代码五的运行结果
下面是并查集使用递归写法代码六:( find是寻根函数部分)
#include<iostream>
#include<string.h>
#include<ctime>
#include<stdio.h>
using namespace std;
int num[100];
int find(int a){
if(num[a]==-1)
return a;
num[a]=find(num[a]);
return num[a]; //最后的返回值是a 的根节点<strong>
}
int main(){
int n,m;
freopen("数据输入.in","r",stdin);
while(cin>>n>>m){
long long st_time=clock();
int count=0;
memset(num,-1,sizeof(num));
while(m--){
int a,b;
cin>>a>>b;
int c;
int d;
for(int j=0;j<10000000;j++){
c=find(a);
d=find(b);
}
if(c!=d){
num[c]=d;
count++;
}
}
cout<<n-count<<endl; //输出根节点的数量
long long end_time=clock();
cout<<"程序运行的时间"<<end_time-st_time<<endl;
}
fclose(stdin);
}
运行结果如下:
(图八)代码六的运行结果
从上面两个代码中我们可以发现代码七中是无法减少对新的变量的定义。而且最终运行结果显示时间上几乎节约了一半。在大多数时候通过递归的函数能够减少对变量的定义而减少函数的运行时间。但是,递归中终究还是有一个致命的缺陷就是在递归次数过多时会需要极大的运行空间,甚至会终止程序的运行。所以,在递归和循环的使用上还是要根据具体情况而选择使用。
*以上内容只属于我个人的见解,包括程序的书写和运行都是我个人在我个人的dev-c+软件中运行的结果,绝无任何私造图片的行为。也许在我的实验中任然有漏洞存在,但是,实验结果绝对符合事实。
结语:我之所以会想到写这一篇是因为之前看了一个大神的博客,他写到“我们每次对一道题AC之后,绝不能就此满足。我们应该想办法让我们的代码能更加的减少时间复杂度和空间复杂度。”所以,我才想通过这一个从使用循环换成使用递归来减少运行的时间,也许有时候和AC飘过的差距就是这点差距呢?好了我希望大家能把自己所知道的函数模板都转化成递归代码,保留一份。说不定什么时候就派上用场了。(因为我之前写的那篇中格式上有缺陷所以我重新写了一次。)