生活中,如果1+2+3+4.....+100,大家基本上都会用等差数列计算,如果有人从1开始加,不是傻就是白X,那么程序中呢,是不是也是这样。今天无意中看到了尾递归,以前也写过,但是不知道这个专业名词,今天写一下对比下性能问题。
今天主要是看到了尾递归,所以联想到了这些,写下这篇文章,其中也把Benchmark (Nuget上的BenchmarkDotNet)的基准测试用了下,感觉比较好用,赞。Benchmark 需要在release下运行。
原则上所有的递归操作,都可以写成循环操作。尾递归是指,在函数返回的时候,调用自身本身,并且return语句不能包含表达式。这样编译器或者解释器就可以把尾递归做优化,试运行速度更快。
测试类
public classTestClass
{///
///for循环///
///
///
///
public int TestFor(intn)
{int s = 1;for (int i = 2; i < n + 1; i++)
{
s= s +i;
}returns;
}///
///等差数列///
///
///
///
public int TestForG(intn)
{int s = (1 + n) * n / 2;returns;
}///
///递归///
///
///
///
public int TestRec(intn)
{if (n == 1) return 1;return n + TestRec(n - 1);
}///
///尾递归///
///
///
///
public int TestTail(intn)
{return TestTail(1, n);
}public int TestTail(int sum, intn)
{if (n == 1) returnsum;
sum= sum +n;return TestTail(sum, n - 1);
}
}
View Code
基准测试
[SimpleJob(RuntimeMoniker.NetCoreApp30)]
[RPlotExporter]public classTestClassForBenchmark
{private readonly TestClass testClass = newTestClass();
[Params(100,500,1000,5000)]public intN;
[Benchmark]public voidTestFor()
{
testClass.TestFor(N);
}
[Benchmark]public voidTestForG()
{
testClass.TestForG(N);
}
[Benchmark]public voidTestRec()
{
testClass.TestRec(N);
}
[Benchmark]public voidTestTail()
{
testClass.TestTail(N);
}
}
View Code
Main程序调用
BenchmarkRunner.Run();
结果
用Benchmark的基准测试发现,运行时间:等差 < for < 尾递归(接近for) < 递归,for的运行速度比递归快,但是递归结构比较清晰,容易理解。
发现TestForG有点问题,接下来自己简单测试
实际用Stopwatch测试
TestClass testClass = newTestClass();
Stopwatch stopSwitch= newStopwatch();int n = 5000;
stopSwitch.Start();int sum =testClass.TestFor(n);
stopSwitch.Stop();
Console.WriteLine($"结果:{sum},TestFor时间:{stopSwitch.ElapsedTicks}");
stopSwitch.Start();
sum=testClass.TestForG(n);
stopSwitch.Stop();
Console.WriteLine($"结果:{sum},TestForG时间:{stopSwitch.ElapsedTicks}");
stopSwitch.Restart();
sum=testClass.TestRec(n);
stopSwitch.Stop();
Console.WriteLine($"结果:{sum},TestRec时间:{stopSwitch.ElapsedTicks}");
stopSwitch.Restart();
sum=testClass.TestTail(n);
stopSwitch.Stop();
Console.WriteLine($"结果:{sum},TestTail时间:{stopSwitch.ElapsedTicks}");
View Code
Stopwatch测试结果
1. 10次
结果:55,TestFor时间:2024结果:55,TestForG时间:3799结果:55,TestRec时间:1603结果:55,TestTail时间:2371
2. 100结果:5050,TestFor时间:1704结果:5050,TestForG时间:2708结果:5050,TestRec时间:1069结果:5050,TestTail时间:1401
3. 500结果:125250,TestFor时间:1794结果:125250,TestForG时间:3096结果:125250,TestRec时间:9398结果:125250,TestTail时间:2332
4. 1000结果:500500,TestFor时间:2080结果:500500,TestForG时间:4147结果:500500,TestRec时间:2003结果:500500,TestTail时间:2540
5. 5000结果:12502500,TestFor时间:1428结果:12502500,TestForG时间:3982结果:12502500,TestRec时间:6815结果:12502500,TestTail时间:2799
结论
1. for的运行速度比递归快,但是递归结构比较清晰,容易理解。
2. 等差计算不一定比for循环快
斐波那契数列对比
///
///循环实现 counter:运行次数///
public long Fib(int n, ref intcounter)
{if (n < 1) return 0;long a = 1, b = 1;longtemp;for (int i = 3; i <= n; i++)
{
counter++;
temp=a;
a=b;
b= temp +b;
}returnb;
}///
///递归实现///
public long FibRec(int n, ref intcounter)
{
counter++;if (n < 1) return 0;if (n < 3) return 1;return FibRec(n - 1, ref counter) + FibRec(n - 2, refcounter);
}///
///尾递归实现///
public long FibTailRec(int n, ref intcounter)
{if (n < 1) return 0;if (n < 3) return 1;return FibRec(1, 1, n, refcounter);
}public long FibRec(long last, long prev, int n, ref intcounter)
{
counter++;long temp = last +prev;if (n == 3) returntemp;
last=prev;
prev=temp;return FibRec(last, prev, n - 1, refcounter);
}
效果
counter是运行的次数,斐波那契数列直接用递归,n=100都直接堆栈溢出。递归用的好了,思路清晰,用的不好的话,数据稍微大点就是深深的坑。用递归尽量优化为尾递归,也就是返回的时候调用自身,不要有表达式。