什么是递归

递归

什么是递归?首先递归是一种广泛的算法或者编程技巧,很多数据结构和算法的实现都依赖递归,比如DFS(深度优先遍历),二叉树的前中后序遍历等等。

示例:

举个简单的例子,比如排队,你排在第n个位置,具体n是多少,你不知道,所以你就问你前面一个人:“你是第几个人?”然而你前面的人也不知道他是第几个人,所以他就问他前面的人是第几个人,这样一直向前问,知道问到第一个人,然后第二个人知道自己是第二然后告诉第三个人,然后第三个人知道自己是第三,然后一直向后面这样反馈,到你这里,你也就知道自己是第几个人了,把这个过程抽象成一个算法就是递归!去的过程叫做“递”,回来的过程叫做“归”,而这个递归函数就可以抽象成f(n)=f(n-1)+1,其中,f(1)=1。

递归满足条件

通过上面那个例子,我们可以理解递归到底是什么,那么怎么分析一个问题能否用递归来解决呢?

1.一个问题可以分解成几个子问题

就比如上面的例子中,想知道第n个位置是多少,可以分解为n的前面一个(n-1)是多少这样一个子问题。

2.这个问题与分解后的子问题,除了数据规模不同,求解思路一样

就是说我知道了(n-1)位置是多少,我就知道了n的位置是多少。

3.存在递归终止条件

上面的问题中,我们不可能一直问前面一个人,总会到底一个人的时候,那么当到了第一个人的时候,即f(1)=1的时候,就是递归的终止条件。

练习

我们在通过一个简单的例子来理解递归,比如n个台阶,一次只能上1步或者2步,问:有多少种走法?这是一道经典的递归算法题。

思路解析:首先我们第一步就有两种解法,一个是1步1个台阶,第二个是1步2个台阶,当我们走完第一步的时候,那么我们第二步有几种走法呢?还是两种,一步一个台阶和一步两个台阶,所以我们能推导出f(n)=f(n-1)+f(n-2);

为什么是f(n-1)和f(n-2)?你一步走了1个台阶还剩下(n-1)个,同理(n-2)!公式我们推导出来了,子问题也分析完了,那么终止条件呢?如果最后只剩下一个台阶我们可以一步走完,如果剩下两个台阶也可以一步走完,也可以分两步走完,如果剩下三个台阶就是f(3)= f(2)+f(1),所以我们定义终止条件是f(2)=2,f(1)=1。所以我们把公式和终止条件放到一起就是:

f(2)=2;
f(1)=1;
f(n)=f(n-1)+f(n-2);

通过这个公式,我们的最终代码就是:

int f(n){
  if(n == 1) return 1;
  if(n == 2) return 2;
  return f(n-1) + f(n-2);
}

注意:

我们平时思考问题的时候总是喜欢,按照线性思考,就是一条流水线似的,思考问题,但是面对递归问题,如果也这样思考,想把递归问题平铺展开,脑子就会一层一层循环的思考下一层怎么调用,这样的思维方式是错误的,很容易被绕进去,所以在思考递归问题的时候,我们只需要把递归抽象成一个递归公式,不用想一层层的调用关系,不用去分解递归的每个步骤。

递归产生问题:

递归代码可能导致堆栈溢出,因为函数调用会使用栈来保存临时变量,每调用一个函数,都会将临时变量封装为栈帧压入内存栈,等函数执行完成返回时,才出栈。系统栈或者虚拟机栈空间一般内存都不大。如果递归函数的求解的数据规模很大,调用层次很深,一直压入栈,就会有堆栈溢出的风险。

递归代码要注意重复计算,这个问题我们用刚才那个上台阶的例子解释,如图:

从图中我们可以看到计算f(5)需要计算f(4)和f(3),但是f(4)和f(3)已经被计算过了,所以会存在一个函数会存在多次重复计算,为了避免重复计算,我们可以通过一个数据结构(比如散列表)来保存已经进求过解的值,比如f(k)计算过后,存到散列表中,下次在遇到f(k)直接从散列表中取值就行,不需要再计算了,所以上面的代码可以优化为:

int f(n){
  if(n == 1) return 1;
  if(n == 2) return 2;
  //利用一个map数组存储key和value,key是n,value是f(n)的结果
  if( map.containsKey(n)){
    return map.get(n);
  }
  int res= f(n-1) + f(n-2);
  map.put(n,res);
  return res;
}

时间复杂度O(n),空间复杂度O(n);

递归代码改为非递归代码实现

递归代码好处:表达能力强,写起来简洁

递归代码弊端:空间、时间复杂度高,有堆栈溢出的风险,存在重复计算,过多的函数调用比较耗时。

我们先把前面示例中的f(n)=f(n-1) +1代码改写:

//递归写法
public int f1(int n){
        if(n == 1){
            return 1;
        }
        return f1(n-1)+1;
}
//非递归写法
public int f1(int n){
       int ret = 1;
        for(int i=2;i<=n;i++){
            ret= ret+1;
        }
        return ret;
    }

同样第二个练习题同样改写为:

public int f3(int n){
        if (n == 1) return 1;
        if (n == 2) return 2;
        
        int ret = 0;
        int pre = 1;
        int next = 2;
        
        for(int i=3;i<=n;i++){
            ret = pre + next;
            pre=next;
            next=ret;
        }
        return ret;
    }

这里读者直接看代码,可能会一脸懵逼,所以我给解释一下:

ret用来保存最终的返回值。

pre用来定义每次计算的前一个值。

next表示用来计算的后一个值。

我们来用上台阶的那张图充分解释一下这几个值的含义:比如当我们要计算f(3)的结果,那么从图中可以看,我们需要知道f(1)和f(2)的值,f(1)和f(2)的值我们都已经知道了,是1和2,那么f(3)就是3,好了我们现在知道了f(3)的值了,那么我们现在计算f(4)的值怎么算呢?从图中我们可以看出就需要知道f(2)和f(3)的值,f(3)我们已经知道了,那么是不是f(4)也就知道了,然后我们在计算f(5)的值,是不是需要知道f(3)和f(4)的值,f(3)和f(4)我们也都知道了,那么f(5)的值我们也知道了,同理f(6)、f(7)....f(n)都知道了,那么映射到代码怎么实现呢?

代码实现:通过定义终态值pre=1和next=2代表f(1)和f(2)的值,然后我们从i遍历到n,第一次遍历我们能得到f(3)的值,就是ret=pre+next,此时的ret就是f(3),那么接下来我们要求解f(4),需要f(2)就是next和f(3)就是ret,所以我们按照上台阶的图中的顺序,将pre = next,将next= ret,然后开始求解i=4,即f(4)的值,f(4)需要f(2)和f(3),那么此时pre就代表f(2),next代表f(3),刚才的(将pre = next,将next= ret)也就是实现这个语句的意思。然后重复pre = next,将next= ret,继续i=5,直到i=n,就可以求解了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值