4、递归

一、简介

1.1、什么是递归

  递归(Recursion) 是一种解决问题或定义概念的方法,它依赖于将问题分解为一个或多个与原问题相似但规模更小的部分,并且这个过程会一直持续下去,直到遇到基本情况可以直接解决为止。
  想象一下你在整理一堆文件夹。每个文件夹里可能包含其他小的文件夹,这些小文件夹里又可能有更小的文件夹,一直这样下去。如果你要查看所有文件,你会怎么做呢?一种方法就是递归处理:首先打开当前的文件夹,然后对每个子文件夹重复同样的操作——打开并检查里面是否还有更多的子文件夹,如果有就继续打开,如果没有了(也就是找到了最后一个没有子文件夹的情况),那就直接查看这个文件夹里的文件。
  举例来说, 用于计算从1到给定整数n的累加和:

public class Recursion {

    public static int sum(int n) {
        // 基本情况:1的累加和就是1本身
        if (n == 1) {
            return 1;
        } else {
            // 递归步骤:n的累加和等于n加上(n-1)的累加和
            return n + sum(n - 1);
        }
    }

    public static void main(String[] args) {
        // 测试递归函数,计算从1到3的累加和
        int result = sum(3);
        System.out.println("从1累加到3的总和是: " + result);//从1累加到3的总和是: 6
    }
}

  所以,总的来说,递归就是一个不断进行自我操作并逐步简化问题的过程,直到遇到可以直接解决的基本情况。

1. 2、递归的思路

  • 深入到最里层叫做
  • 从最里层出来叫做
  • 的过程中,外层函数内的局部变量(以及方法参数)并未消失,的时候还可以用到

二、单路递归

2.1、说明

  单路递归(Single-path Recursion) 是一种在算法设计中常见的递归形式,它指的是在递归调用过程中,函数只按照一个固定的路径或方式逐步将原问题分解为规模更小的同类子问题。具体来说,在每次递归调用时,函数不会同时沿着多个不同的分支进行递归,而是根据当前输入值生成一个新的、更简单的输入值来继续下一次递归调用。

2.2、案例

1、递归二分查找。

public class Recursion {

    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {
        int array[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
        int recursion = recursion(array, 4, 0, array.length - 1);
        System.out.println("目标值的索引位置是:" + recursion);//目标值的索引位置是:3
    }

    /**
     * 递归二分查找
     *
     * @param array  被查找的数组
     * @param target 需要查询的目标值
     * @param i      查找的开始索引位置
     * @param j      查找的结束索引位置
     * @return 找的索引位置或-1
     */
    public static int recursion(int[] array, int target, int i, int j) {
        if (i > j) {
            return -1;//如果i>j说明没有找到,返回-1;
        }
        int m = (i + j) >>> 1;//取中间索引
        if (target > array[m]) {
            return recursion(array, target, m + 1, j); //如果目标值大于中间值
        } else if (target < array[m]) {
            return recursion(array, target, i, m - 1);//如果目标值小于中间值
        } else {
            return m; //如果目标值等于中间值
        }
    }
}

2、冒泡排序

2.1、说明

   冒泡排序(Bubble Sort) 是一种简单的排序算法,它重复地遍历要排序的数列,每次将两个元素进行比较,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

2.2、冒泡排序步骤

  1. 初始化: 假设我们有一个待排序的数组 arr,包含 n 个元素(索引从 0 到 n-1)。初始化时,数组是无序的。

  2. 第一轮冒泡:
    a、从数组的第一个元素开始,即从索引为 0 的位置。
    b、对相邻的元素进行比较,即首先比较 arr[0] 和 arr[1]。
    c、如果当前元素(例如 arr[i])比下一个元素(arr[i+1])大(对于升序排列),则交换这两个元素的位置。
    d、继续这个比较和交换的过程,一路向后直到处理到倒数第二个元素(arr[n-2] 和 arr[n-1])为止。这样一轮下来,最大的元素
    将被“冒泡”到数组的末尾。

   3. 重复冒泡过程:
    a、开始下一轮冒泡,但这次只需要遍历除了最后一个已排序元素之外的所有元素。也就是说,第二轮冒泡的范围是从 arr[0]
    到 arr[n-2]。
    b、再次逐对比较并交换,确保这一轮结束时,次大的元素也排到了正确的位置(仅次于上一轮的最大值)。

  4. 继续多轮冒泡:
    a、按照以上方式,重复进行多轮冒泡,每轮都将未排序部分的一个最大(或最小)元素推至其最终正确的位置。

  5. 设置终止条件:
    a、可以通过添加一个标志变量来优化冒泡排序,记录在某一轮中是否发生了交换操作。
    b、若在一轮循环中没有发生任何交换,则说明数组已经是有序的,此时可以直接跳出循环,提前结束排序过程。

   6. 完整流程:
    a、外层循环控制总共需要进行的轮数,最多可能进行 n-1 轮。
    b、内层循环负责每一轮的具体冒泡操作,其遍历范围随着外层循环的进行而逐渐缩小:[0, n-1-i],其中 i 是当前正在执行的轮数。
    c、当所有轮数完成或者满足提前结束条件时,整个冒泡排序过程结束,数组变为有序状态。

  总结来说,冒泡排序通过不断地遍历数组并将较大的元素逐步向上“冒泡”,直至整个数组变得完全有序。

2.3、代码案例

单独对于冒泡排序法,如下方式是最优解,后面的递归冒泡排序法仅仅是展示多种冒泡排序的实现方式。所以递归冒泡排序法性能肯定是要差一些的。

import java.util.Arrays;

public class Recursion3 {
    /**
     * 测试
     *
     * @param args
     */
    public static void main(String[] args) {
        int[] arrayToSort = {3, 5, 2, 7, 4, 1, 6};
        bubbleSort(arrayToSort);
        System.out.println("输出排序后的数组结果:" + Arrays.toString(arrayToSort));//输出排序后的数组结果:[1, 2, 3, 4, 5, 6, 7]
    }

    /**
     * 冒泡排序
     *
     * @param arr 需要排序的数组
     */
    public static void bubbleSort(int[] arr) {
        int n = arr.length;
        // 外层循环控制整个排序过程,总共需要进行 n-1 轮比较。
        // 每一轮都会把当前未排序部分的最大值“冒泡”到数组的末尾。
        for (int i = 0; i < n - 1; i++) {
            // 内层循环负责每一轮的具体比较和交换操作,
            // 遍历从第一个元素开始到最后一个尚未确定位置(即已经排序好的)元素之间的所有元素对。
            for (int j = 0; j < n - 1 - i; j++) {
                // 检查当前元素(arr[j])与下一个相邻元素(arr[j+1])的大小关系
                // 如果当前元素大于下一个元素,说明它们的顺序是错误的,当前元素较大时,执行交换操作,将较大的元素移到后面
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];  // 使用临时变量temp存储当前元素的值
                    arr[j] = arr[j + 1];//然后用下一个元素的值覆盖当前元素,
                    arr[j + 1] = temp;  // 最后将临时变量temp的值赋给下一个元素的位置
                }
            }
        }
    }
}

细节说明 内部循环为什么是【j < n - 1 - i】

1、在冒泡排序算法中,j < n - 1 - i 的条件是为了确保内层循环不会重复已经排好序的部分。

2、在每一轮外层循环(由变量 i 控制)中,我们试图通过比较和交换相邻元素来将当前未排序部分的最大值“冒泡”到数组的末尾。因此,在每轮结束后,末尾的 i+1 个元素已经是有序的,不需要在后续的轮次中再次检查和交换。

3、例如,对于长度为5的数组,第一轮结束后最大的元素会被移到末尾,所以第二轮只需要对前4个元素进行比较。第三轮则是前3个元素,以此类推。

4、具体到内层循环的迭代次数上:

  • 第一轮时,i = 0,所以内层循环会遍历 j < n - 1 - 0,即从第一个元素到最后一个元素。
  • 第二轮时,i = 1,那么内层循环只需遍历 j < n - 1 - 1,即从第一个元素到倒数第二个元素。
  • 如此下去,最后一轮(当所有元素都已排序时),i = n - 2,则内层循环只需执行一次(或者根本无需执行,如果数组在之前就已经完全有序了)。

5、因此,使用 j < n - 1 - i 这个条件可以避免对已知有序部分进行不必要的比较,从而提高冒泡排序的效率。

2.4、使用递归实现冒泡排序

代码(这种方式有待优化,请看后面一个例子)

    /**
     * 测试
     *
     * @param args
     */
    public static void main(String[] args) {
        int[] arrayToSort = {5, 4, 3, 2, 1};
        bubbleSort(arrayToSort, arrayToSort.length - 1);
        System.out.println("排序结果:" + Arrays.toString(arrayToSort));//排序结果:[1, 2, 3, 4, 5]
    }

    /**
     * 冒泡排序(递归排序法)
     *
     * @param array 需要排序数组
     * @param j     未排序区域的右边界索引位置
     */
    public static void bubbleSort(int[] array, int j) {
        if (j == 0) {
            return;
        }
        for (int i = 0; i < j; i++) {
            if (array[i] > array[i + 1]) {
                int t = array[i];
                array[i] = array[i + 1];
                array[i + 1] = t;
            }
        }
        bubbleSort(array, j - 1);
    }

优化版

    /**
     * 冒泡排序(递归排序优化版).
     * 避免对已排序部分的进行的无谓比较
     *
     * @param array 需要排序数组
     * @param j     未排序区域的右边界索引位置
     */
    public static void bubbleSort(int[] array, int j) {
        if (j == 0) {
            return;
        }
        int x = 0;//记录了最后一次交换元素的位置 x,避免对已排序部分的进行的无谓比较
        for (int i = 0; i < j; i++) {
            if (array[i] > array[i + 1]) {
                int t = array[i];
                array[i] = array[i + 1];
                array[i + 1] = t;
                x = i;
            }
        }
        bubbleSort(array, x);
    }

3、递归插入

从值7开始,依次插入排序到正确的位置。

package recursion;

public class Recursion5 {

    /**
     * 测试
     *
     * @param args
     */
    public static void main(String[] args) {
        int[] arrayToSort = {1, 2, 3, 4, 8, 7, 0};
        //将索引5的值及后面的索引值进行插入排序。
        bubbleSort(arrayToSort, 5);
    }

    /**
     * 插入排序,递归,指定需要排序的值及后面的值进行插入排序。
     *
     * @param array
     * @param low   需要排序的值的索引
     */
    public static void bubbleSort(int[] array, int low) {
        if (low == array.length) {//索引等于数组长度就退出。
            return;
        }
        int t = array[low]; //需要排序的值
        int i = low - 1;//已排序好区域的结尾索引;

        while (i >= 0 && array[i] > t) {
            array[i + 1] = array[i];//把大的值后移一位
            i--;
        }

        //如果插入的位置和本身的位置相同则不进入赋值。
        if (i + 1 != low) {
            array[i + 1] = t;//找到位置,就插入。
        }

        bubbleSort(array, low + 1);//递归继续排序下一个索引。
    }
}

三、多路递归

  多路递归(Multirecursion)是一种在计算机科学和数学中使用的递归技术,它涉及到一个递归函数调用自身多次,并且每次调用可能沿着多个不同的分支路径进行。与单递归或直接递归(每次函数调用只产生一个后续的函数调用)不同,多路递归中,函数可以基于输入条件或状态同时生成多个递归子任务。

  例如,在斐波那契数列的计算中,多路递归的一个实现方式是同时计算两个相邻的斐波那契数,而非通过常规递归反复计算相同的子问题。具体来说,可以定义一个函数,它同时递归地求解第n项和第n-1项,这样就避免了重复计算并提高了效率。

  另一个应用多路递归的例子是在解决组合学中的全排列问题时,可以考虑将待排列集合分为头元素和其他元素两部分,然后对其他元素进行递归全排列,每种排列都会与头元素结合形成一个新的排列,这就涉及到了多路递归的思想。

  在更复杂的问题场景中,比如多路归并排序等算法中,也可以看到多路递归的应用,其中会同时处理多个递归得到的有序子序列,然后合并成一个有序的大序列。

  总结来说,多路递归的核心特点在于递归过程中形成的“多条路径”同步进行计算,从而减少重复工作、提高效率或者简化问题的表达。

3.1斐波那契数列

  • 什么是斐波那契数列
      斐波那契数列(Fibonacci sequence)是一个在数学中非常著名的数列,由意大利数学家莱昂纳多·斐波那契(Leonardo Fibonacci)引入,并以他命名。这个数列的特点是每个数字是前两个数字的和。

  斐波那契数列的标准定义如下:
    F(0) = 0
    F(1) = 1
    对于n >1,F(n) = F(n - 1) + F(n - 2)
  因此,斐波那契数列的前几项为:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ……
在这里插入图片描述

  该数列在自然界、艺术、建筑设计以及计算机科学等领域都展现出广泛而有趣的应用。此外,随着数列项数的增长,相邻两项之比逐渐逼近黄金分割比例(约为1.61803…),这也是它被称为“黄金分割数列”的原因。

代码实现

package recursion;

public class Recursion6 {

    /**
     * 测试
     *
     * @param args
     */
    public static void main(String[] args) {
        int f = f(2);
        System.out.println("结果为:" + f);//1
    }

    /**
     * 斐波那契数列
     *
     * @param n
     */
    public static int f(int n) {
        if (n == 0) {
            return 0;
        }
        if (n == 1) {
            return 1;
        }
        return f(n - 1) + f(n - 2);
    }
}

3.2斐波那契数列-优化-记忆法

1、默认的斐波那契数列存在一下几点缺点

  1. 效率低下(时间复杂度高): 这个递归函数在计算 f(n) 时会分别调用 f(n-1) 和
    f(n-2)。由于没有使用任何缓存或记忆化技术,对于较大的 n 值,该算法会产生大量的重复计算。例如,在计算 f(5) 时,它将多次计算
    f(4) 和 f(3),而在计算 f(4) 和 f(3) 时又会再次计算更低阶的斐波那契数。这种现象导致了指数级的时间复杂度,即 O(2^n)。

  2. 栈空间消耗大: 深度递归会导致调用栈非常深,对于较大 n值可能会超出Java方法调用栈的最大深度,从而抛出StackOverflowError异常。

2、解决方式

  1. 动态规划:通过一个数组或者数据结构存储已经计算过的斐波那契数,避免重复计算,降低时间复杂度至线性,即 O(n)。
public class Recursion7 {

    /**
     * 测试
     *
     * @param args
     */
    public static void main(String[] args) {
        int f = fibonacci(6);
        System.out.println("结果为:" + f);//结果为:8
    }

    public static int fibonacci(int n) {
        //缓存数据数组,用于保存计算过的斐波那契数列
        int[] cacheData = new int[n + 1];//长度等于计算的斐波那契数+1(为什么是+1 因为还有一个数是0)
        Arrays.fill(cacheData, -1);//将数组的默认值全设置为-1,等于-1表示未被计算出过;
        cacheData[0] = 0;//设置固定斐波那契数0的值;
        cacheData[1] = 1;//设置固定斐波那契数1的值;
        int f = f(n, cacheData);
        return f;
    }

    /**
     * 斐波那契数列-数组记忆保存法
     *
     * @param n         被计算的斐波那契数
     * @param cacheData 缓存数据
     */
    private static int f(int n, int[] cacheData) {

        //1、先去数组找,到就返回
        if (cacheData[n] != -1) {
            return cacheData[n];
        }
        //2.找不到就计算,计算出来的值保存到数组;
        int one = f(n - 1, cacheData);
        int two = f(n - 2, cacheData);
        cacheData[n] = one + two;
        return cacheData[n];
    }
}
  1. 迭代法:改写为循环结构而非递归,同样利用临时变量记录中间结果,以避免栈空间的过度消耗。
    /**
     * 测试
     *
     * @param args
     */
    public static void main(String[] args) {
        int f = fibonacci(6);
        System.out.println("结果为:" + f);//结果为:8
    }

    public static int fibonacci(int n) {
        // 初始化前两项
        if (n <= 0) {
            return 0;
        } else if (n == 1) {
            return 1;
        }

        // 对于n大于1的情况,使用循环进行计算
        int fib = 1; // 初始化为斐波那契数列的第二项
        int prevFib = 0; // 初始化为斐波那契数列的第一项

        for (int i = 2; i <= n; i++) { // 循环从i=2开始,包括n在内的所有项
            // 计算新的斐波那契数:当前项等于前两项之和
            int temp = fib;
            fib += prevFib;
            // 更新前一项的值
            prevFib = temp;
        }

        // 返回第n项斐波那契数(注意循环结束时fib已经是第n项)
        return fib;
    }
  • 17
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值