【数据结构与算法】第十八篇:递归,尾递归,尾调用

知识概览

  • 一、递归的引入(递归现象)
  • 二、递归的调用过程与实例分析
  • 三、递归的基本思想
    • 小tip:链表递归的具体实例
  • 四、递归的一般使用条件
  • 五、实例分析:斐波那契数列
    • 1.原理剖析
    • 2.fib优化1 – 记忆化
    • 3.fib优化2
    • 4.fib优化3
  • 六、实例分析:青蛙跳台阶问题
  • 七、实例分析:汉诺塔问题
  • 八、递归转非递归分析
  • 九、尾调用,尾递归(了解)
    • 1. 尾调用的优化(了解)


一、递归的引入(递归现象)

递归思想想必大家都不陌生。它分为“递”和“归”两个过程。是一种常见的算法策略。类似于以下的故事场景:
1.从前有座山,山里有座庙,庙里有个老和尚,正在给小和
尚讲故事呢!故事是什么呢?【从前有座山,山里有座庙,
庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?
『从前有座山,山里有座庙,庙里有个老和尚,正在给小
和尚讲故事呢!故事是什么呢?……』】
2.GNU 是 GNU is Not Unix 的缩写
GNU → GNU is Not Unix → GNU is Not Unix is Not Unix → GNU is Not Unix is Not Unix is Not Unix
3.假设A在一个电影院,想知道自己坐在哪一排,但是前面人很多,
A 懒得数,于是问前一排的人 B【你坐在哪一排?】,只要把 B 的答案加一,就是 A 的排数。
B 懒得数,于是问前一排的人 C【你坐在哪一排?】,只要把 C 的答案加一,就是 B 的排数。
C 懒得数,于是问前一排的人 D【你坐在哪一排?】,只要把 D 的答案加一,就是 C 的排数。

二、递归的调用过程与实例分析

以下面这段代码为例

/**
     * 计算n的阶乘
     * 1*2*3*4......*(n-1)*n
                */
        public int Fac(int n){
            if(n<=1)return n;
            return n*Fac(n-1);
    }

【数据结构与算法】第十八篇:递归,尾递归,尾调用_第1张图片
时间,空间复杂度分析
递归所需要的时间:T(n)=T(n-1)+O(1)
根据时间对应表如下:
【数据结构与算法】第十八篇:递归,尾递归,尾调用_第2张图片可以得出时间复杂度为 : O(n)
空间复杂度为: O(n) (开辟了n个占空间)

几种优化思想
【数据结构与算法】第十八篇:递归,尾递归,尾调用_第3张图片

三、递归的基本思想

拆解思想
1.把规模较大的问题转化为同类型的规模较小的问题。
2.把规模较小的问题转化为规模更小的问题。
3.规模较小的问题可以直接得出他的答案。

求解
1.由最小规模问题的解得出较大规模问题的解
2.由较大规模问题的解不断得出规模更大问题的解
3.最后得出原来问题的解
这两种思想正好体现了递归的 ‘递’和‘归’两个过程
【数据结构与算法】第十八篇:递归,尾递归,尾调用_第4张图片类似于可以利用上面这种思想解题的都可以考虑递归,递归不是为了得到最优解,而是为了简化解题思想。
很多链表、二叉树相关的问题都可以使用递归来解决
✓ 因为链表、二叉树本身就是递归的结构(链表中包含链表,二叉树中包含二叉树)

小tip:链表递归的具体实例

2.两数相加
【数据结构与算法】第十八篇:递归,尾递归,尾调用_第5张图片

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
       if(l1==null)return l2;
       if(l2==null)return l1;
       int sum=l1.val+l2.val;
        ListNode head=new ListNode(sum%10);
        head.next=addTwoNumbers(l1.next,l2.next);
        if(sum>9)head.next=addTwoNumbers(head.next,new ListNode(1));
        return head;
    }
}

四、递归的一般使用条件

① 明确函数的功能 (重要!!!)
先不要去思考里面代码怎么写,首先搞清楚这个函数的干嘛用的,能完成什么功能?
② 明确原问题与子问题的关系
寻找 f(n) 与 f(n – 1) 的关系
③ 明确递归基(边界条件)
递归的过程中,子问题的规模在不断减小,当小到一定程度时可以直接得出它的解
重要:寻找递归基,相当于是思考:问题规模小到什么程度可以直接得出解?

五、实例分析:斐波那契数列

斐波那契数列:1、1、2、3、5、8、13、21、34、……
F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n≥3)
◼ 编写一个函数求第 n 项斐波那契数
【数据结构与算法】第十八篇:递归,尾递归,尾调用_第6张图片
以上写法的空间复杂度为O(n).
你可能会疑惑为什么双路递归那么重复调用,为什么空间复杂度才O(n)级别,而时间复杂度却达到了恐怖的O(2n)?这里我们先抛出一个计算递归空间复杂度复杂度的通式
递归调用的空间复杂度=递归深度*每次递归调用所需要的辅助空间。
具体原理我们下文细细剖析~

1.原理剖析

根据以上的复杂度分析,对于斐波那契这种多路递归,时间复杂度为O(2n),这个时间复杂度是十分恐怖的。双路递归不可避免的重复计算很多。
【数据结构与算法】第十八篇:递归,尾递归,尾调用_第7张图片
【数据结构与算法】第十八篇:递归,尾递归,尾调用_第8张图片
【数据结构与算法】第十八篇:递归,尾递归,尾调用_第9张图片

2.fib优化1 – 记忆化

在上面的介绍种,斐波那契额数列性能差的一个重要因素就是拥有大量的重复计算。
假如我们从根节点向着一个方向先进行递归,记住已经算过的递归调用的返回值
然后在进行另一个分支计算时就不用再次进行重复计算了,大大减少了重复计算。我们用数组来实现这个思想。

 public static int fib2(int n){
        if(n<=2)return 1;
        int [] array=new int [n+1];
        array[1]=array[2]=1;
        return fib3(n,array);
    }
    public static int fib3(int n,int []array){
        if(array[n]==0){//说明该位置还没有赋值
            array[n]=fib3(n-1,array)+fib3(n-2,array);
        }
        return array[n];
    }
}

【数据结构与算法】第十八篇:递归,尾递归,尾调用_第10张图片
◼ 时间复杂度:O(n),空间复杂度:O(n)

3.fib优化2

去除递归操作

//去除递归优化时间复杂度O(n)
    public static int fib1(int n){
        int [] array=new int[n+1];
        array[1]=array[2]=1;
        for(int i=3;i<=n;i++){
            array[i]=array[i-1]+array[i-2];
        }
        return array[n];
    }

4.fib优化3

由于每次运算只需要用到数组中的 2 个元素,所以可以使用滚动数组来优化,创建一个空间为2的数组每次,依次向下更新数组元素的值。
这里的&运算符就是%运算符的作用。
【数据结构与算法】第十八篇:递归,尾递归,尾调用_第11张图片

public static int fib4(int n){
            if(n<=2)return 1;
            int [] arr=new int [2];
            arr[0]=arr[1]=1;
            for(int i=3;i<=n;i++){
                //始终着眼于两个数组
                arr[i&1]=arr[(i-1)&1]+arr[(i-2)&1];
            }
            return arr[n&1];
        }
        //上台阶
        public static int f(int n){
            if(n<=2)return n;
            return f(n-1)+f(n-2);

        }

这里为什么可以用&取代%(%2都可以改成&1)
【数据结构与算法】第十八篇:递归,尾递归,尾调用_第12张图片

六、实例分析:青蛙跳台阶问题

楼梯有 n 阶台阶,上楼可以一步上 1 阶,也可以一步上 2 阶,走完 n 阶台阶共有多少种不同的走法?
√假设 n 阶台阶有 f(n) 种走法,第 1 步有 2 种走法
✓ 如果上 1 阶,那就还剩 n – 1 阶,共 f(n – 1) 种走法
✓ 如果上 2 阶,那就还剩 n – 2 阶,共 f(n – 2) 种走法
√所以 f(n) = f(n – 1) + f(n – 2)
【数据结构与算法】第十八篇:递归,尾递归,尾调用_第13张图片

  public int climbStairs(int n){
        if(n<=2)return n;
        return climbStairs(n-1)+climbStairs(n-2);

    }

迭代优化

  public int climbStairs1(int n){
        if(n<=2)return n;
        int first=1;
        int second=2;
        for(int i=3;i<=n;i++){
            second=first+second;
            first=second-first;
        }
        return second;
    }

七、实例分析:汉诺塔问题

编程实现把 A 的 n 个盘子移动到 C(盘子编号是 [1, n] )
1.每次只能移动1个盘子
2.大盘子只能放在小盘子下面
【数据结构与算法】第十八篇:递归,尾递归,尾调用_第14张图片递归的基本思想,找到递归是条件
最初的思想是将n个木盘从A移动到C,B作为中间过渡柱子。
现在可以找到共同性(1)先将n-1个柱子从A移动到B,C作为中间过渡柱子。
(2)然后再将第n个柱子移动到C.(3)然后再将n-1个柱子从B移动到C,A最为中间过渡柱子。
找到则会个规律我们用代码来实现以下。

/**
     * 柱子p1  p2  p3
     * 木板:
     *      1
     *      2
     *      3
     *      4
     *    .......
     *     n-1
     *      n
     *    按位置来说:p1:起始位置  p2:中间过渡位置  p3:最终位置
     *
     */
    //找到递归规律
    public static void  hanoi(int n,String p1,String p2,String p3){
        //就一块板子
        if(n==1){
            move(n,p1,p3);
            return;
        }
        hanoi(n-1,p1,p3,p2);
        move(n,p1,p3);
        hanoi(n-1,p2,p1,p3);
    }
    public static void move(int n,String from,String to){
        System.out.println("将"+n+"号盘子从"+from+"移动到"+to);
    }

八、递归转非递归分析

递归分为递和归两个过程,所以再递的过程中,先一直向更深的方向递归,然后数据规模小到一定程度,直接返回一个数然后递归的空间按时间开辟晚到近依次释放。这和栈的先进后出的特点十分相似。不出所料递归的底层也是通过栈实现的,所以一般递归都可以通过栈转化为非递归。
实例一:递归形式


    public void log(int n){
        if(n<1)return;
        log(n-1);
        int v=n+10;
        System.out.println(v);
    }

非递归实现(创建辅助对象)

 //非递归实现
    public void log1(int n){
        Stack<Model> stack=new Stack<>();
       while(n>0){
           stack.push(new Model(n,n+10));
           n--;
       }
       while(!stack.isEmpty()){
           System.out.println(stack.pop().getV());
       }
    }

package Test01;

public class Model {
   private int n;
    private int v;

    public Model(int n, int v) {
        this.n = n;
        this.v = v;
    }

    public int getN() {
        return n;
    }

    public void setN(int n) {
        this.n = n;
    }

    public int getV() {
        return v;
    }

    public void setV(int v) {
        this.v = v;
    }
}

九、尾调用,尾递归(了解)

◼ 尾调用:一个函数的最后一个动作是调用函数
◼如果最后一个动作是调用自身,称为尾递归(Tail Recursion),是尾调用的特殊情况
【数据结构与算法】第十八篇:递归,尾递归,尾调用_第15张图片◼ 一些编译器能对尾调用进行优化,以达到节省栈空间的目的(但是java编译器并不支持)
上面图中的代码为例,按照代码中的调用应该会向上不断开辟空间,但是如果是尾调用优化的话则不会在想上开辟空间直接重复利用test1的空间。
【数据结构与算法】第十八篇:递归,尾递归,尾调用_第16张图片那么问题来了:如果是尾递归肯定没得说,每次调用的栈空间肯定是相同大小的,如果是尾调用,调用其他方法,栈空间不一样大怎么办?
答:这个大可不必担心,因为,某些编译器的底层会对其在原有基础上进行扩容。

尾调用判断

}
    /**
     * 计算n的阶乘
     * 1*2*3*4......*(n-1)*n
                */
        public int Fac(int n){
            if(n<=1)return n;
            return n*Fac(n-1);
    }

这个不是尾调用因为,最后一步执行的是*,而不是调用函数。

  void test3(int n){
        int a=10;
        int b=a+10;
        test4(b);
        int c=a+b;//这里
        int d;
    }
    void test4(int b){
        int x1=30;
        int x2=50;
    }

不是尾调用,只有最后一句是调用函数的时候才是尾调用
为什么只有最后一句是尾调用的时候才是尾调用?
一上面这段代码为例。
【数据结构与算法】第十八篇:递归,尾递归,尾调用_第17张图片

1. 尾调用的优化(了解)

实例一:n的阶乘改成非递归

 //改成尾调用优化
    public int Fac1(int n) {
        return Fac2(n,1);
    }
    public int Fac2(int n,int result){
            if(n<=1)return n;
            //result:之前的结果
            return Fac2(n-1,result*n);
    }

实例二:斐波那契改成非递归

 //斐波那契实现为调用
    public int Fib(int n){
            return Fib1(n,1,1);
    }
    public int Fib1(int n,int first,int second){
            if(n<=1)return first;
            return Fib1(n-1,second,first+second);
    }

【数据结构与算法】第十八篇:递归,尾递归,尾调用_第18张图片

你可能感兴趣的:(数据结构与算法,unix,服务器)