算法--递归算法

什么样的场景可以使用递归呢?

需要同时满足以下三个条件才可以使用递归:

1、一个问题的解可以分解为几个子问题

2、这个问题分解分解之后的子问题,除了数据规模不同,求解思路完成相同

3、递归存在终止条件

 

如何编写递归代码?

写递归代码的关键是如何找到将大问题分解为小问题的规律,并且基于此写出递归公式,然后再推导出终止条件,最后将递归公式和终止条件翻译成代码

这里需要注意的是:当我们遇到递归,把它抽象为递归公式的时候,不要想着层层调用,不要试图用人脑去分解每一个步骤,否则你就会被绕进去

示例:假如有n个台阶,每次你可以跨1个台阶和两个台阶,请问n个台阶有多少种走法?如果有7个台阶,你可以2、2、2、1这样子上去,也可以1、2、2、1、1这样子上去,总之,走法很多,那如何编写求得总共有多少种走法呢?

分析:走到第n个台阶有两种情况,第一种就是从n-1个台阶走上去,第二种情况,就是从n-2个台阶走上去,那么n-1个台阶呢,同样是两种情况,为(n-1)-1和(n-1)-2。同理n-2也有两种情况,为(n-2)-1和(n-2)-2。所以可以得到:当走到第n个的走法为 走到第n-1个台阶的走法和走到n-2个台阶的走法总和。分析到这里已经可以推到出递归公式了,就不要在推导下去了,否则你就很容易绕进入,走不出来

得到公式为:

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

递归公式已经推到出来了。接下来就需要考虑边界问题 ,也就是我们的终止条件。因为它有两种走台阶的方式,一个是一次走一个台阶,一个是一次走两个台阶,所有我们要考虑只有一个台阶和有两个台阶的情况。一个台阶那很明显只有1种情况;两个台阶,则有两种走法为(1、1)和2的情况

最终得到递归函数:

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

编写递归代码还需要考虑一下几种情况?

一)、递归需要警惕栈溢出

熟悉JVM虚拟机的同学都知道,方法的调用就是栈帧压入内存栈,调用完之后,就是栈帧出栈的过程。如果递归求解的数据规模很大,调用层次很深,一直入栈,就会存在栈溢出的风险。

代码示例:

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

    @Test
    public void test(){
        f(20000);
    }

运行结果:

使用递归实现累加,到20000就已经出现栈溢出的问题了。那如何解决呢?方案一:当递归超过一定的深度(比如:100)之后,我们就不在递归,直接返回报错。

改进后的代码示例:

    int depth=0; //定义全局变量表示递归的深度
    int f(int i){
        depth++;
        if(depth==100) throw new RuntimeException();
        
        if(i==1) return 1;
        return f(i-1)+i;
    }

方案二:把递归代码改为非递归代码,这在后面会进行详解。

 

二)、递归需要警惕重复计算

还是以上面的台阶为例,如果把整个问题分解,那就如下图:

递归

从上图种可以看出,计算f(5),需要先计算f(4)和f(3),计算f(4),也需要计算f(3)。其中f(3)被计算多次。为了避免重复计算的解决方案,可以通过数据结构(比如散列表)来保存已经求解过的f(k)。

优化上面台阶问题代码:

    //定义一个全局变量map,来保存已经计算过的值
    HashMap<Integer,Integer> map=new HashMap<>();
    int f(int n){
        if(n==1) return 1;
        if(n==2) return 2;

        if(map.containsKey(n)){ //判断是否存在计算过的值
            return map.get(n);
        }

        int sum=f(n-1)+f(n-2);
        map.put(n,sum);  //保存以计算过的值

        return sum;
    }

 

三)、递归需要警惕递归函数在调用过程种出现闭环的情况

如何查找最终推荐人?现在很多App都有这样的功能,用户A推荐用户B注册,用户B推荐用户C注册。A->B->C

我们可以说,C的最终推荐人是A,B的最终推荐人是A。而A没有最终推荐人。。我们的代码实现可能是这样


long findRootReferrerId(long actorId) {
  Long referrerId = select referrer_id from [table] where actor_id = actorId;
  if (referrerId == null) return actorId;
  return findRootReferrerId(referrerId);
}

在实际过程中,可能就会出现这种情况,测试同学在测试的时候,造了一条数据,表示用户C推荐了用户A。那递归就会形成闭环A->B->C->A。这种情况就会发生死循环,造成栈溢出。这种问题的解决方案有两种,

一)、限制递归的深度

二)、检测递归中是否出现环的情况

 

问题一:所有递归代码是否可以改写为非递归代码?

笼统的讲,是的,所有的递归都是借助栈来实现的。只要我们在内存堆上实现栈,手动模拟入栈和出栈的过程,就可以把递归代码改成看上去非递归代码的样子。

 

问题二:怎样将递归代码改写为非递归代码?

案例一:也就是上面使用递归累加求和的案例

案例二:也就是上面上台阶的案例

 

总结:递归的使用有利也有弊,利是递归的表达能力很强,写起来非常简洁,而弊端就是,空间复杂度高,有栈溢出的风险,存在重复计算,过多的调用函数等问题,所以在实际开发过程中,需要根据实际情况是使用递归的方式实现

递归就是一个不断调用自身的过程,从JVM机角度而言,递归就是一个不断入栈的过程,所以需要考虑边界条件,也就是终止条件,否则就会存在栈溢出的情况。同时也需要考虑递归调用层次问题,如果调用层次太深,也会存在栈溢出的风险。

 

参考:数据结构之美--王争

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值