递归和循环

概念

在日常编程中经常会遇到重复计算相同的问题,此时一般会采用递归或者循环来解决。无论是采用递归还是循环,都需要经历如下三步:首先需要找出计算问题的规律,用数学计算公式表达出来;然后再用代码编程来实现这个数学计算公式;最后采用递归或者循环的方式 多次运行这个数学计算公式,从而得出计算结果(为了保证程序的健壮性,往往还需要进行一些边界值处理)。

可以看到前两步都是相同,只是第三步到底是选择递归还是循环呢?要回答这个问题,我们首先来看下递归和循环各自的特点。
递归:在一个函数(或者方法)体内调用这个函数自身,直到某个条件满足(否则会一直执行下去,直到栈内存溢出)。
循环:通过设置一个初始值和终止条件,并在这个范围内重复计算。

下面通过一个简单的示例来展示递归和循环(以下示例都是java编写)。

示例:一个整数数组内所有成员的和

这是日常编程中经常遇到的、也是很简单的计算问题。比如:知道一个网站每天的pv,要统计一周内总pv,已知一周内每天的pv是一个数组{1231,2001,1874,1203,3030,2020,1122}。

下面的分别以循环和递归进行实现:
    
/**
     * 循环实现
     * @param array
     * @return
     */
    public int addArarry1(int [] array){
        int count= 0;
        if(array == null|| array.length<=0){
            return count;
        }


        for(int i=0;i<array.length;i++){
            count+=array[i];
        }
        return count;
    }
   
 /**
     * 递归实现
     * @param array
     * @param len
     * @return
     */
    public int addArray2(int[] array,int len){
        if(array == null|| len<=0){
            return 0;
        }
        return array[len-1] + addArray2(array,len-1);
    }

从这个简单的示例可以看出,递归实现的代码更加简洁,可阅读性也更强。但为什么在日常开发中,我们经常看到的是第一种循环实现,而非递归实现呢?这可能是因为,读书时期老师说过能用循环尽量不要用递归。这种说法也是有道理的,因为递归调用方法自身 是有额外的开销的。这里以java例,来分析递归。

Java的vm栈和递归

我们都知道在java的每个线程中,jvm都为其分配一个vm栈的内存空间(可以通过-Xss调整这个栈的大小)。在该线程中的很次方法调用,都会分配一个栈帧,并在该方法调用结束后自动释放栈帧。但在递归调用过程中,每次方法的调用都依赖下一次方法调用结果,在下一个方法返回之前,方法不会结束,也就是说栈帧不能被释放,如下:
 
这里只是模拟了上一个示例中对7个数进行递归相加,会进行7次递归方法调用,产生7个栈帧,并且只有在最后一次方法调用完成后,这7个栈帧才会被依次释放。

这也就是vm栈的由来,我们都知道栈是后进先出。递归的过程,其实就是依次把 栈帧压入vm栈,直到最后一个栈帧入栈,然后再依次出栈。上一个示例中的数组只有7个数,如果有上万个数,就会有一万个栈帧先入栈,然后再出栈, 而vm栈的空间是有限的(比如512k,通过-Xss设置),此时可能就会出现栈内存溢出。

这就是java中递归调用的本质(其他编程语言的本质也大致类似),这也就是老师为什么说在能用循环的情况下不要用递归。其中一个原因就是如果递归层次太深,有可能出现栈内存溢出。还有另外一个原因就是递归有可能出现重复计算,从而导致效率低下。比如下一个示例:

斐波那契数列

要求输出第N项斐波那契树。首先回忆下老师过的斐波那契数,用f(n)表示第n个斐波那契数:当n=0时,f(n)=0 ;当n=1时f(n)=1;当n>1时,f(n)=f(n-1)+f(n-2)。

这里公式已经列出来了,如果用递归实现,代码非常简洁,如下:
public int fibonacci1(int n){
        if(n<=0){
            return 0;
        }

        if(n==1){
            return 1;
        }
        return fibonacci1(n-1)+fibonacci1(n-2);
    }
假设n=10,我们看看这个递归过程中发生了什么:
 
可以看到有很多的重复计算,比如在计算f(10)时,需要计算f(8);而在计算f(9)时又需要计算f(8),依次类推,就会发现有很多重复计算,尤其是在n较大的时候,使用递归来实现斐波那契数的效率愈发低下。

此时我们想到的就是如何把已经计算的值保存下来,防止重复计算。这里可以用一个辅助空间,比如一个HashMap,把计算过的值存储下来,在下次使用时先查询是否已经计算过,如果已经计算过就直接使用,否则再计算。此时就需要一个额外的容量为n的HashMap辅助空间,增加了空间复杂度,这个HashMap中保存了从1到n项的斐波拉契数。

就这个问题而已,我们不需要保存所有的n项斐波那契数,而是只需要返回第n项即可。此时采用循环实现,就会更加简单,首先根据f(0)和f(1)可以计算出f(2);再根据f(1)和f(2)又可以计算出f(3), 以此类推就可以计算出f(n)。具体实现如下:
/**
     * 循环实现斐波那契
     * @param n
     * @return
     */
    public long fibonacci2(int n) {
        long result=0;
        long pre1=0;
        long pre2=1;
        if(n==0) {
            return pre1;
        }
        if(n==1) {
            return pre2;
        }
        for (int i = 2; i <= n; i++) {
            result = pre1+pre2;
            //移动指针
            pre1 = pre2;
            pre2 = result;
        }
        return result;
    }
从循环的实现代码来看,虽然不如递归来得简洁,但缺是该问题的最优实现。

总结

通过上述分析,我们可以看出使用递归 可以是代码更简洁,并且有更好的可读性;但可能出现栈内存溢出,以及重复计算这两个问题。回到文章开头的问题,使用递归还是循环,我们该如何选择呢?

存在就是有道理的,不应该像老师说的尽量不用递归。而是应该根据实际情况,在递归深度不超过1000(我的笔记本在超过3000多时出现栈内存溢出,当然可以通过-Xss适当调大栈内存);并且如果使用递归没有重复计算的情况下,我们就可以优先选择递归。还是那句话,因为递归可以使我们的代码看起来更加优雅。

写这篇博客的目的不是为了批判递归,而是让大家更深入的了解递归,并在适当的时候拥抱递归。



  • 7
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
递归循环都是计算机程序中常见的控制结构,用于重复执行某个任务。它们在时间复杂度分析中起着关键作用,用来衡量算法执行效率随着输入规模增加而变化的速度。 **1. 递归**: 递归是指一个函数或过程调用自身来解决问题。递归的时间复杂度通常取决于两个因素:基本情况(没有递归调用的情况,其复杂度为 O(1))和递归情况(每次调用的复杂度)。如果递归情况的时间复杂度为 T(n),那么总的时间复杂度将是基本情况加上 n 次递归调用的复杂度,即 T(n) = a * T(n-1) + f(n),其中 a 是递归调用的次数,f(n) 是基本情况的复杂度。如果没有基本情况,或者基本情况的时间复杂度与递归情况相同(例如 Fibonacci 数列),递归的时间复杂度会呈现指数级增长,如 O(2^n) 或者 O(n!)。 **2. 循环**: 循环则直接按照预设的条件重复执行一段代码,比如 while、for 循环循环的时间复杂度通常用循环的次数来表示,假设循环体内的操作复杂度是恒定的,那么循环 n 次的时间复杂度就是 O(n)。如果是嵌套循环,时间复杂度会是两者的乘积,例如外层循环 n 次,内层循环 m 次,总复杂度为 O(n * m)。 **相关问题--:** 1. 递归何时会导致时间复杂度过高? 2. 什么情况下,使用循环递归更优? 3. 在数据结构或算法中,如何选择递归还是循环实现?
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值