算法一看就懂之「 递归 」

以前的文章我们已经聊过了「 数组和链表 」「 堆栈 」「 队列 」,今天我们来看看「 递归 」,固然「 递归 」并非一种数据结构,它是不少算法都使用的一种编程方法。它太广泛了,而且用它来解决问题很是的优雅,但它又不是那么容易弄懂,因此我特地用一篇文章来介绍它。算法

1、「 递归 」是什么?

递归 就是指函数直接或间接的调用本身,递归是基于栈来实现的。递归的经典例子就是 斐波拉契数列(Fibonacci)。通常若是能用递归来实现的程序,那它也能用循环来实现。用递归来实现的话,代码看起来更清晰一些,但递归的性能并不占优点,时间复杂度甚至也会更大一些。编程


上图为 斐波拉契数列 图例。数组

要实现递归,必须知足2个条件:微信

  1. 可调用本身数据结构

    就是咱们要解决的这个问题,能够经过函数调用本身的方式来解决,便可以经过将大问题分解为子问题,而后子问题再能够分解为子子问题,这样不停的分解。而且大问题与子问题/子子问题的解决思路是彻底同样的,只不过数据不同。所以这些问题都是经过某一个函数去解决的,最终咱们看到的就是不停得函数调用本身,而后就把问题化解了。架构

    若是这个问题不能分解为子问题,或子问题的解决方法与大问题不同,那就没法经过递归调用来解决。函数

  2. 可中止调用本身性能

    中止调用的条件很是关键,就是大问题不停的一层层分解为小问题后,最终必须有一个条件是来终止这种分解动做的(也就是中止调用本身),作递归运算必定要有这个终止条件,不然就会陷入无限循环。大数据

下面仍是以 斐波拉契数列(Fibonacci)为例,咱们来理解一下递归:spa

斐波拉契数列就是由数字 1,1,2,3,5,8,13…… 组成的这么一组序列,特色是每位数字都是前面相邻两项之和。若是咱们但愿得出第N位的数字是多少?

  1. 可使用循环的方式求解:

    这里就不列代码了,思路是:咱们知道最基本的状况是 f(0)=0,f(1)=1,所以咱们能够设置一个一个循环,循环从i=2开始,循环N-1次,在循环体内 f(i)=f(i-1)+f(i-2),直到i=N-1,这样循环结束的时候就求出了f(N)的值了。

  2. 更优雅的方式是使用递归的方式求解:

    咱们知道斐波拉契数列的逻辑就是:


    能够看出,这个逻辑是知足上面2个基本条件,假如求解 f(3),那 f(3)=f(2)+f(1),所以咱们得继续去求解f(2),而 f(2)=f(1)+f(0),所以整个求解过程其实就在不断的分解问题的过程,将大问题f(3),分解为f(2)和f(1)的问题,以此类推。既然能够分解成子问题,而且子问题的解决方法与大问题一致,所以这个问题是知足“可调用本身”的递归要求。

    同时,咱们也知道应该在什么时候中止调用本身,即当子问题变成了f(0)和f(1)时,就再也不须要往下分解了,所以也知足递归中“可中止调用本身”的这个要求。

    因此,斐波拉契数列问题能够采用递归的方式去编写代码,先看图:


    咱们将代码写出来:

    int Fb(int n){
       if(n<=1) return n==0?0:1;
       return Fb(n-1)+Fb(n-2); //这里就是函数本身调用本身
    }

    从上面的例子能够看出,咱们写递归代码最重要的就是写2点:

  3. 递推公式

    上面代码中,递推公式就是 Fb(n)=Fb(n-1)+Fb(n-2),正是这个公式,才能够一步步递推下去,这也是函数本身调用本身的关键点。所以咱们在写递归代码的时候最首先要作的就是思考整个逻辑中的递推公式。

  4. 递归中止条件

    上面代码中的中止条件很明显就是:if(n<=1) return n==0?0:1;这就是递归的出口,想出了递推公司以后,就要考虑递归中止条件是啥,没有中止条件就会无限循环了,一般递归的中止条件是程序的边界值。

    咱们对比实现斐波拉契数列问题的2种方式,能够看出递归的方式比循环的方式在程序结构上更简洁清晰,代码也更易读。但递归调用的过程当中会创建函数副本,建立大量的调用栈,若是递归的数据量很大,调用层次不少,就会致使消耗大量的时间和空间,不只性能较低,甚至会出现堆栈溢出的状况。

    咱们在写递归的时候,必定要注意递归深度的问题,随时作好判断,防止出现堆栈溢出。

    另外,咱们在思考递归逻辑的时候,不必在大脑中将整个递推逻辑一层层的想透彻,通常人都会绕晕的。大脑很辛苦的,咱们应该对它好一点。咱们只须要关注当前这一层是否成当即可,至于下一层不用去关注,当前这一层逻辑成立了,下一层确定也会成立的,最后只须要拿张纸和笔,模拟一些简单数据代入到公式中去校验一下递推公式对不对便可。

2、「 递归 」的算法实践?

咱们看看常常涉及到 递归 的 算法题(来源leetcode)

算法题:实现 pow(x, n) ,即计算 x 的 n 次幂函数。

说明:
    -100.0 < x < 100.0
    n 是 32 位有符号整数,其数值范围是 [−2^31, 2^31 − 1]

示例:
输入: 2.00000, 10
输出: 1024.00000

解题思路:

方法一:
暴力解法,直接写一个循环让n个x相乘嘛,固然了这种方式就没啥技术含量了,时间复杂度O(1),代码省略了。

方法二:
基于递归原理,很容易就找出递推公式 f(n)=x*f(n-1),再找出递归中止条件即n==0或1的状况就能够了。不过稍微须要注意的是,由于n的取值能够是负数,因此当n小于0的时候,就要取倒数计算。代码以下:
class Solution {
    public double myPow(double x, int n) {
        if(n==0) return 1;
        if(n==1) return x;
        if(n<0) return 1/(x*myPow(x,Math.abs(n)-1));
        return x*myPow(x,n-1);
    }
}
这个方法其实也有问题,当n的数值过大时,会堆栈溢出的,看来也是不最佳解,继续往下看。

方法三:
利用分治的思路,将n个x先分红左右两组,分别求每一组的值,而后再将两组的值相乘就是总值了。即 x的n次方 等于 x的n/2次方 乘以 x的n/2次方。以此类推,左右两组其实还能够分别各自继续往下分组,就是一个递推思想了。可是这里须要考虑一下当n是奇数的状况,作一个特殊处理便可,代码以下:
class Solution {
    public double myPow(double x, int n) {
        //若是n是负数,则改成正数,但把x取倒数
        if(n<0) {
            n = -n;
            x = 1/x;
        }
        return pow(x,n);

    }

    private double pow(double x, int n) {
        if(n==0) return 1;
        if(n==1) return x;
        double half = pow(x,n/2);
        //偶数个
        if(n%2==0) {
            return half*half;
        }
        //奇数个
        return half*half*x;
    }
}
这种方法的时间复杂度就是O(logN)了。

以上,就是对数据结构中「 递归 」的一些思考。

码字不易啊,喜欢的话不妨转发朋友,或点击文章右下角的“在看”吧。😊

本文原创发布于微信公众号「 不止思考 」,欢迎关注。涉及 思惟认知、我的成长、架构、大数据、Web技术 等。 

相关文章
相关标签/搜索