教你精通Java语法之第十二章、递归

目录

一、递归

1.1递归的概念

1.1.1定义

1.1.2原理

1.1.3思路

1.2单路递归 

1.2.1阶乘

1.2.2正向输出数字 

1.2.3反向输出字符串

1.3多路递归

1.3.1斐波那契数列

1.3.2兔子问题

1.3.3青蛙爬楼梯

1.4汉诺塔问题

1.5猴子吃桃问题

1.6老鼠走迷宫问题

二、递归的时间复杂度


一、递归

1.1递归的概念

1.1.1定义

定义:递归是一种解决计算问题的方法,其中解决方案取决于同一类问题的更小子集。

比如单链表递归遍历的例子:

void f(Node node) {
    if(node == null) {
        return;
    }
    println("before:" + node.value)
    f(node.next);
    println("after:" + node.value)
}

说明:

  1. 自己调用自己,如果说每个函数对应着一种解决方案,自己调用自己意味着解决方案是一样的(有规律的)。

  2. 每次调用,函数处理的数据会较上次缩减(子集),而且最后会缩减至无需继续递归。

  3. 内层函数调用(子集处理)完成,外层函数才能算调用完成。

1.1.2原理

假设链表中有 3 个节点,value 分别为 1,2,3,以上代码的执行流程就类似于下面的伪码

// 1 -> 2 -> 3 -> null  f(1)

void f(Node node = 1) {
    println("before:" + node.value) // 1
    void f(Node node = 2) {
        println("before:" + node.value) // 2
        void f(Node node = 3) {
            println("before:" + node.value) // 3
            void f(Node node = null) {
                if(node == null) {
                    return;
                }
            }
            println("after:" + node.value) // 3
        }
        println("after:" + node.value) // 2
    }
    println("after:" + node.value) // 1
}

1.1.3思路

  1. 确定能否使用递归求解

  2. 推导出递推关系,即父问题与子问题的关系,以及递归的结束条件

  3. 深入到最里层叫做递

  4. 从最里层出来叫做归

  5. 在递的过程中,外层函数内的局部变量(以及方法参数)并未消失,归的时候还可以用到

1.2单路递归 

1.2.1阶乘

用递归方法求阶乘

  • 阶乘的定义 $n!= 1⋅2⋅3⋯(n-2)⋅(n-1)⋅n$,其中 n 为自然数,当然 0! = 1

 代码实现:

private static int fac(int num) {
    if (num < 1) {
        return 1;
    }
    return num * f(num - 1);
}

拆解伪码如下,假设 n 初始值为 3:

f(int num = 3) { // 解决不了,递
    return 3 * f(int num = 2) { // 解决不了,继续递
        return 2 * f(int num = 1) {
            if (num < 1) { // 可以解决, 开始归
                return 1;
            }
        }
    }
}

1.2.2正向输出数字 

public static void Print(int num){
        if(num>9){
            Print(num/10);
        }
        System.out.print(num%10);
    }

1.2.3反向输出字符串

用递归反向打印字符串,n 为字符在整个字符串 str 中的索引位置。

  • :n 从 0 开始,每次 n + 1,一直递到 n == str.length() - 1

  • :从 n == str.length() 开始归,从归打印,自然是逆序的

    public static void Print(String str,int index){
            if(index

    拆解伪码如下,假设字符串为 "abc"。

    void reversePrint(String str, int index = 0) {
        void reversePrint(String str, int index = 1) {
            void reversePrint(String str, int index = 2) {
                void reversePrint(String str, int index = 3) { 
                    if (index == str.length()) {
                        return; // 开始归
                    }
                }
                System.out.println(str.charAt(index)); // 打印 c
            }
            System.out.println(str.charAt(index)); // 打印 b
        }
        System.out.println(str.charAt(index)); // 打印 a
    }

1.3多路递归

1.3.1斐波那契数列

概念:如果每个递归函数例包含多个自身调用,称之为 multi recursion

教你精通Java语法之第十二章、递归_第1张图片

public static int fib(int num) {
    if (num == 0) {
        return 0;
    }
    if (num == 1) {
        return 1;
    }
    return fib(num - 1) + fib(num - 2);
}

教你精通Java语法之第十二章、递归_第2张图片

1.3.2兔子问题

教你精通Java语法之第十二章、递归_第3张图片

  • 第一个月,有一对未成熟的兔子(黑色,注意图中个头较小)

  • 第二个月,它们成熟

  • 第三个月,它们能产下一对新的小兔子(蓝色)

  • 所有兔子遵循相同规律,求第 n 个月的兔子数

分析

兔子问题如何与斐波那契联系起来呢?设第 n 个月兔子数为 f(n)

  • f(n) = 上个月兔子数 + 新生的小兔子数

  • 而【新生的小兔子数】实际就是【上个月成熟的兔子数】

  • 因为需要一个月兔子就成熟,所以【上个月成熟的兔子数】也就是【上上个月的兔子数】

  • 上个月兔子数,即 f(n-1)

  • 上上个月的兔子数,即 f(n-2)

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

因此本质还是斐波那契数列,只是从其第一项开始

1.3.3青蛙爬楼梯

  • 楼梯有 n 阶

  • 青蛙要爬到楼顶,可以一次跳一阶,也可以一次跳两阶

  • 只能向上跳,问有多少种跳法

教你精通Java语法之第十二章、递归_第4张图片

  • 因此本质上还是斐波那契数列,只是从其第二项开始

  • 对应 leetcode 题目 70. 爬楼梯 - 力扣(LeetCode)

1.4汉诺塔问题

该游戏是在一块铜板装置上,有三根杆(编号A、B、C),在A杆自下而上、由大到小按顺序放置64个金盘(如图1)。游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。

教你精通Java语法之第十二章、递归_第5张图片

设移动盘子数为n,为了将这n个盘子从A杆移动到C杆,可以做以下三步:

(1)以C盘为中介,从A杆将1至n-1号盘移至B杆;

(2)将A杆中剩下的第n号盘移至C杆;

(3)以A杆为中介;从B杆将1至n-1号盘移至C杆。

递归法:

实际操作中,只有第二步可直接完成,而第一、三步又成为移动的新问题。以上操作的实质是把移动n个盘子的问题转化为移动n-1个盘,那一、三步如何解决?事实上,上述方法设盘子数为n, n可为任意数,该法同样适用于移动n-1个盘。因此,依据上法,可解决n -1个盘子从A杆移到B杆(第一步)或从B杆移到C杆(第三步)问题。现在,问题由移动n个盘子的操作转化为移动n-2个盘子的操作。依据该原理,层层递推,即可将原问题转化为解决移动n -2、n -3… … 3、2,直到移动1个盘的操作,而移动一个盘的操作是可以直接完成的。

//代码实现:
public static void Tower(int num,char A,char B,char C){
        //num表示盘子的个数,A,B,C表示A塔,B塔,C塔
        if(num==1){
            System.out.println("移动:"+A+"到"+C);
        }
        else{
            //如果有多个盘子,可以看成两个,最下面的,上面所有的盘子(num-1)
            //(1.)先移动上面所有的盘子到B盘,借助C盘
            Tower(num-1,A,C,B);
            //(2.)把最下面的盘移动到C盘
            System.out.println("移动:"+A+"到"+C);
            //(3.)再把B塔上的盘子移动到C盘,借助A盘
            Tower(num-1,B,A,C);
        }
    }

1.5猴子吃桃问题

猴子吃桃子问题:     有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个! 以后每天猴子都吃其中的一半,然后再多吃一个。当到第10天时, 想再吃时(即还没吃),发现只有1个桃子了。问题:最初共多少个桃子?

//代码实现
	//猴子吃桃问题
    //day10:1个桃子
    //day9:(day10+1)*2
    //day8:(day9+1)*2
public static int Peach(int day){
        if(day<0||day>10){
            return -1;
        }
        if(day==10){
            return 1;
        }
        else if(day>=1&&day<=9){
            return (Peach(day+1)+1)*2;
        }
        return -1;
    }

1.6老鼠走迷宫问题

目标:小球从1-1走到6-5

教你精通Java语法之第十二章、递归_第6张图片

 迷宫问题:

1.小球得到的路径,和程序员设置的找路策略有关:找路的上下左右的先后顺序

//代码实现
public static void main(String[] args) {
    	//思路:
        //1.设置迷宫,通过二维数组表示int[][] map=new int[8][7];
    	//2.规定:0表示可以走,1表示障碍物
        int[][] map=new int[8][7];//8行7列
        for (int i = 0; i < map.length; i++) {
            for (int j = 0; j < map[i].length; j++) {
                //设置最上面一行和最下面一行为1
                map[0][j]=1;
                map[7][j]=1;
                if(i>=1&&i<7){
                    //设置最右面和最左面一列为1
                    map[i][0]=1;
                    map[i][6]=1;
                }
            }
        }
        //输出迷宫
        disPlay(map);
        //设置障碍物
        map[2][1]=1;
        map[2][2]=1;
        disPlay(map);
        //找路
        findWay(map,1,1);
        disPlay(map);
    }
    public static void disPlay(int[][] map){
        System.out.println("==============================");
        for (int i = 0; i < map.length; i++) {
            for (int j = 0; j < map[i].length; j++) {
                System.out.print(map[i][j]+" ");
            }
            System.out.println();
        }
    }
    public static boolean findWay(int[][] map,int i,int j){
        //1.findWay方法找出迷宫的路径
        //2.如果找到,返回true,否则返回false
        //3.i,j表示老鼠的位置,初始化的位置为(1,1);
        //4.0表示可以走,1表示障碍物,2表示可以走,3表示走过但是走不通,是一条死路
        //5.当map[5][6]==2,说明找到了通路,此时就可以结束,否则继续找
        if(map[6][5]==2){//说明已经找到
            return true;
        }
        else{
            if(map[i][j]==0){ //表示当前位置可以走
                //假定该位置可以走通
                map[i][j]=2;
                //找路的逻辑:我的:右下左上(逻辑不通找路结果不同)或者【下右上左】
                if(findWay(map,i,j+1)){//右
                    return true;
                }
                else if(findWay(map,i+1,j)){//下
                    return true;
                }
                else if(findWay(map,i,j-1)){//左
                    return true;
                }
                else if(findWay(map,i-1,j)){//上
                    return true;
                }
                else{
                    map[i][j]=3;
                    return false;
                }
            }
            else{
                return false;
            }

        }
    }

2.存在回溯现象

//修改上述部分代码
        //设置障碍物
        map[3][1]=1;
        map[3][2]=1;
    	map[2][2]=1;
        disPlay(map);

二、递归的时间复杂度

递归式:                T(n) = aT(\frac{n}{b}) + f(n)

其中

  • T(n) 是问题的运行时间,n 是数据规模

  • a 是子问题个数

  • T(\frac{n}{b}) 是子问题运行时间,每个子问题被拆成原问题数据规模的 \frac{n}{b}

  • f(n) 是除递归外执行的计算

你可能感兴趣的:(JavaSE,java,开发语言,数据结构,算法)