浅析C#中的线性递归与尾递归

Posted ProMer_Wang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅析C#中的线性递归与尾递归相关的知识,希望对你有一定的参考价值。

C#中的线性递归与尾递归

1.递归的定义

  • 函数直接或者间接的调用自己
  • 使用递归式,必须有明确的结束递归的条件(如果没有声明一个可return即可返回出最终结果的条件,那么对不起,程序寄了(即死循环),轻则死循环自动报错退出,重则死机)
  • 所以,轻易不使用递归,如果一定要使用递归那就要保证好正确性;

2.递归的适用场所

  • 面向题目编程(常见的):

    通过使用递归,来减少重复代码的写入,通过函数快速返回自己想要的计算结果;例如:计算斐波那契数列的结果;

  • 面向功能实现的递归:

    例如:博主自己近期做的一个小功能,折现统计图的实现,做一个动态变化的刻度计算,根据给定的几个值,去动态的变化对应的刻度值。

    那么考虑的几个因素,①最大值②动态变化③刻度应保持的刻度准则(即规律,分刻度值)

    那么博主,就是通过递归的使用来计算给定值,求最大值的次幂。

    满足:

    ①满足最大值的10的*次幂;

    ②满足刻度准则,最大次幂已经得知,剩下的根据最大值次幂,去分刻度值即可;

    ③满足动态变化:根据分的刻度值,动态的计算对应的刻度值即可;

  • 至于其他的应用场景其实有很多,例如:二叉树等等;

  • 应用场景只有在考虑遇到某些问题的时候才会尽可能的去考虑什么方法比较好,什么方法比较便捷;我的理解是在大型的项目当中,如果对于一个性能要求极高,代码稳定的人来说,就有必要引出线性递归和尾递归的概念了;

3.线性递归

  • 定义:

    线性递归也就是普通递归,下一次递归数据的计算要依赖于上一次递归的结果和参数,当数据量较小时执行效率与尾递归几乎没区别,但当数据量较大,迭代次数较多时,由于每次递归都要在内存中开辟一个栈空间,用来存储上次递归的结果和参数,这样的算法将导致严重的内存开销,甚至造成内存溢出,抛出stackOverflowError。

    代码案例:

     public static int Sum(int n)
            
                if (n == 0)
                    return 0; 
                else
                    return n + Sum(n - 1);
            
    

4.尾递归

  • 定义:

    尾递归,函数在递归调用之前已经把所有的计算任务已经完毕了,他只要把得到的结果全交给子函数就可以了,无需保存什么,子函数其实可以不需要再去创建一个栈帧,直接把就着当前栈帧,把原先的数据覆盖即可;

    也即是线性迭代,尾递归函数的最后一步操作是递归,也即在进行递归之前,把全部的操作先执行完,这样的好处是,不用花费大量的栈空间来保存上次递归中的参数、局部变量等,这是因为上次递归操作结束后,已经将之前的数据计算出来,传递给当前的递归函数,这样上次递归中的局部变量和参数等就会被删除,释放空间,从而不会造成栈溢出。但是很多编译器并没有自动对尾递归优化的功能,也即当编译器判断出当前所执行的操作是递归操作时,不会理会它究竟是线性递归还是尾递归,这样也就不会删除掉之前的局部变量和参数等。另外,尾部递归一般都可转化为循环语句。
    一般来说,线性递归和尾递归的时间复杂度相差不大(当然也有例外情况,比如斐波拉契数列,这是因为其线性递归的实现,产生了大量冗余的计算,它的时间复杂度为指数级,而其尾递归的实现只需要线性级别的时间复杂度),但尾递归的空间复杂度比较小(这是在假定尾递归被优化的前提下),线性递归容易理解,尾部递归性能比较好。

    冷知识:尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码(有待考究验证)

     public static int Sum(int n, int a)
            
                if (n == 0)
                    return a;
                else
                    return Sum(n-1, a + n);
            
    

5.Main主程序

 static void Main(string[] args)
        
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            Console.WriteLine("线性递归递归函数函数运行中:结果为0", Sum(30));
            stopwatch.Stop();
            TimeSpan timespan = stopwatch.Elapsed; //  获取当前实例测量得出的总时间
            double milliseconds = timespan.TotalMilliseconds;  //  总毫秒数
            Console.WriteLine($"Sum函数运行的时间总毫秒数为milliseconds");

            Stopwatch stopwatch2 = new Stopwatch();
            stopwatch2.Start();
            Console.WriteLine("尾递归中递归函数函数运行中:结果为0", Sum(30,0));
            stopwatch2.Stop();
            TimeSpan timespan2 = stopwatch2.Elapsed; //  获取当前实例测量得出的总时间
            double milliseconds2 = timespan2.TotalMilliseconds;  //  总毫秒数
            Console.WriteLine($"TSum函数运行的时间总毫秒数为milliseconds2");
			//测试运行时间为:
        

运行时间:

6.总结

​ 在能使用尾递归时尽量使用尾递归,这样不仅能节省内存资源,而且执行效率更高并且尾递归在一定程度上避免了栈溢出;,但是相比于普通for循环,递归的效率是较低的,所以再不是非递归不可的环境中,尽量使用普通循环代替递归算法。


作者:ProMer_Wang

链接:https://blog.csdn.net/qq_43801020/article/details/127504640?spm=1001.2014.3001.5502

本文为ProMer_Wang的原创文章,著作权归作者所有,转载请注明原文出处,欢迎转载!

以上是关于浅析C#中的线性递归与尾递归的主要内容,如果未能解决你的问题,请参考以下文章

尾递归和线性递归

递归与尾递归

算法设计-分治递归与尾递归

JavaScript函数尾调用与尾递归

飞控之卡尔曼滤波浅析

Koltin 递归尾递归和记忆化