算法笔记-递归

如何用三行代码找到最终推荐人

本文创作灵感来源于 极客时间 王争老师的《数据结构与算法之美》课程,通过课后反思以及借鉴各位学友的发言总结,现整理出自己的知识架构,以便日后温故知新,查漏补缺。

是什么

什么是递归
  • 递归是一种非常高效、简洁的编码技巧,一种应用非常广泛的算法。通过调用自身从而解决问题,调用称之为递,返回称之为归。

为什么

为什么使用递归
  • 递归其实是一种解决问题思想的具现化。即将某个问题分成多数子问题,而解决子问题的思路和解决当前问题的思路是一样的。
  • 递归代码书写简洁,表达力强。

怎么办

怎么学习递归
  • 首先了解什么样的问题适合递归解决。
  • 1:一个问题可以分为几个子问题求解。2:该问题与之后分解的子问题,除了数据规模不同以外,解题思路是一样的。3:递归存在终止条件。
如何编写递归代码
  • 推导递推公式:解决当前问题与解决当前分体分解出来的子问题,都可以套用相同的公式求解。
  • 确定终止条件:将大问题分解成小问题,就在于小问题更好解决。那么递归分解出的问题,必须要有最小问题,也就是递归中递的终止条件。
  • 将递推公式转化成代码。
如何将递归代码改为非递归代码
  • 递归代码一:场景计算你在电影院的第几排,你的前面有 n 个人,第一个人是第三排。
    public int fn(int n){
     
        if(n == 1){
     
            return 3;
        }else{
     
            return fn(n - 1) + 1;
        }
    }
    //非递归代码
    public int notFn(int n){
     
        int sum = 3;
        for(int i = 1;i <= n; i++){
     
            sum += 1;
        }
        return sum;
    }

首先,推导出递推公式:fn(n) = fn(n-1) + 1 其中 fn(1) = 3。在上述递归代码中,n 代表的是我们前面有多少人,如果我们想知道自己当前是第几排,只需知道前面的人是第几排,然后加一就是我们自己的排数。所以推到出递推公式:fn(n) = fn(n-1) + 1。使用递归必须要有终止条件,我们的终止条件就是第一个人排在第三排,排数是确定的,即 fn(1) = 3。
非递归代码就是使用迭代循环实现,如上述的非递归代码。

  • 递归代码二:场景计算你爬上楼顶的方式有几种,每次只能上一节台阶或者两节台阶
    //递归代码
    public int fn(int n){
     
        if(n == 1){
     
            return 1;
        }
        if(n == 2){
     
            return 2;
        }
        return fn(n - 1) + fn(n - 2);
    }
    //非递归代码
    public int notFn(int n){
     
        int sum = 0;
        int prepre = 1;
        int pre = 2;
        for(int i = 3;i <= n; i++){
     
            sum = prepre + pre;
            pre = sum;
            prepre = pre;
        }
        return sum;
    }

上述实例:上楼梯,迈出第一步有两种选择:上一节台阶和上两节台阶,第二步的时候一样有两种选择,所以每次我们都是再求当且阶梯数减一和减二后的爬上楼梯的方式。则递推的推到公式就是:fn(n) = fn(n-1) + fn(n-2);终止条件就是,当 n = 1时 fn(1) = 1,当 n = 2 时 fn(2) = 2。非递归代码的实现,最难理解就是循环体中的代码 prepre 的值代表的就是 fn(n-2),pre 的值代表的就是 fn(n-1)。那么就好理解了,循环体内先求出 fn(n) 即 pre+prepre 。然后变更 pre 等于当前 fn(n)的值,prepre 等于当前 fn(n-1) 的值,则下次进去循环体就是:fn(n+1) = fn(n) + fn(n-1),就是当前的 pre + prepre 。

注意事项
  • 递归代码要警惕堆栈溢出。递归方法在每次调用自身的时候,都会在内存栈中存储函数的临时变量,如果递归层级高,深度大,则存在堆栈溢出的风险,造成系统崩溃。我们可以限制递归调用的深度,从而避免堆栈溢出。但是,递归允许调用的最大深度是由当前线程栈的剩余空间决定的,所以事先无法计算合适深度,如果实时计算又太过分复杂。当然,如果递归深度较小,比如低于50,那么可以采用限制递归深度的方法避免堆栈溢出。
  • 递归代码要警惕循环计算。当我们把当前问题分解成多个子问题,而子问题又可以分解成子子问题,在问题分解的过程当中,势必会出现相同的子问题。那么,在计算求解的过程当中,我们对相同的子问题进行过多次求解,这就是在浪费系统资源。对于这种情况,我们可以构建键值对对象,子问题作为键值,子问题的解作为值,进行存储。当我们对子问题求解的时候,可以先从键值对对象中查找是否有当前子问题的借,若有则不再计算直接取值,如果可球之后存储,以便下次访问。
  • 效率问题:递归代码多次调用的时候,就会产生可观的时间成本。递归的每一次调用,都要在内存栈中保存一次现场数据,增大了空间开销的成本。
如何用三行代码找到最终推荐人
    //获取最终推荐人
    public Long getTop(Long id){
     
        Long parentId = "SELECT parentId FROM table WHERE ID = id";
        if(parentId == null) return id;
        return getTop(parentId);
    }

上述是伪代码,大家应该可以看得明白。假如递归深度很深,上面的代码中没有做堆栈溢出的预防;假如推荐人关联关系出现错误指向,也没有做递归无限调用的验证。

总结

王争老师给出了两点高度总结:
1:写递归代码的关键就是找出如何将大问题转化为小问题的规律,并且基于此写出递推公式,进而推敲出递推终止条件,最后将递推公式和终止条件翻译成代码。
2:只要遇到递归,就把它抽象成递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。

你可能感兴趣的:(算法入门,递归,算法,数据结构,递推公式,递归调用)