数据结构与算法【递归】Java实现

递归

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

特点:

  • 自己调用自己,如果说每个函数对应着一种解决方案,自己调用自己意味着解决方案是一样的(有规律的)
  • 每次调用,函数处理的数据会较上次缩减(子集),而且最后会缩减至无需继续递归
  • 内层函数调用(子集处理)完成,外层函数才能算调用完成

递归二分查找

具体实现代码如下

java 复制代码
    public int f(int[] a, int target, int i, int j) {
        if (i > j) {
            return -1;
        }
        int m = (i + j) >>> 1;
        if (target < a[m]) {
            return f(a, target, i, m - 1);
        } else if (a[m] < target) {
            return f(a, target, m + 1, j);
        } else {
            return m;
        }
    }

递归冒牌排序

未排区域为[0,j],已排区域为[j,数组长度-1]

具体实现代码如下

java 复制代码
    public void bubbleSort(int[] a, int j) {
        if (j==0){
            return;
        }
        int temp;
        for (int i = 1; i < j; i++) {
            if (a[i-1] > a[i]) {
                temp = a[i-1];
                a[i-1] = a[i];
                a[i] = temp;
            }
        }
        bubbleSort(a,j-1);
    }

但是这种实现方式存在一定的效率问题。比如说需要排序的数组为[1,2,3,4,5,7,6]。那么经过排序之后,应该为[1,2,3,4,5,6,7]。只需要将6,7交换就好。存在大量无用循环,因此我们可以对冒泡排序进行改进。使用变量x默认为0的位置,并接收上一次交换的下标值。当一次递归x仍然为0,则说明已经没有需要排序的元素了,因此可以直接结束递归。具体实现代码如下

java 复制代码
    public static void bubbleSort2(int[] a, int j) {
        if (j == 0) {
            return;
        }
        int temp;
        int x = 0;
        for (int i = 1; i < j; i++) {
            if (a[i - 1] > a[i]) {
                temp = a[i - 1];
                a[i - 1] = a[i];
                a[i] = temp;
                x = i;
            }
        }
        bubbleSort2(a, x);
    }

递归实现插入排序

未排区域为[low,数组长度-1],已排区域为[0,low]

具体实现代码为

java 复制代码
    public static void insertSort(int[] a, int low) {
        //结束递归条件
        if (low == a.length) {
            return;
        }
        int i = low - 1;
        int temp = a[low];
        //结束排序条件,当i=-1与找到第一个小于temp元素时
        while (i >= 0 && a[i] > temp) {
            a[i + 1] = a[i];
            i--;
        }
        a[i + 1] = temp;
        insertSort(a, low + 1);
    }

多路递归

上面的递归实现有一个特点就是每个递归函数只包含一个自身的调用,这称之为单路递归。如果每个递归函数例包含多个自身调用,称之为多路递归。

多路递归的经典案例:斐波那契数列

递推关系如下:

简单实现

java 复制代码
    public static int f(int n) {
        if (n == 0) {
            return 0;
        }
        if (n == 1) {
            return 1;
        }
        return f(n - 1) + f(n - 2);
    }

斐波那契数列的变种问题

兔子问题

  • 第一个月,有一对未成熟的兔子(黑色,注意图中个头较小)
  • 第二个月,它们成熟
  • 第三个月,它们能产下一对新的小兔子(蓝色)
  • 所有兔子遵循相同规律,求第n个月的兔子数

解决思路:设第 n 个月兔子数为 f(n)

  • f(n) = 上个月兔子数 + 新生的小兔子数
  • 而【新生的小兔子数】实际就是【上个月成熟的兔子数】
  • 因为需要一个月兔子就成熟,所以【上个月成熟的兔子数】也就是【上上个月的兔子数】
  • 上个月兔子数,即 f(n-1)
  • 上上个月的兔子数,即 f(n-2)

简单实现

复制代码
    public static int rabbit(int n){
        if (n==1){
            return 1;
        }
        if (n==2){
            return 1;
        }
        return rabbit(n-1)+rabbit(n-2);
    }

青蛙爬楼梯

  • 楼梯有 n 阶
  • 青蛙要爬到楼顶,可以一次跳一阶,也可以一次跳两阶
  • 只能向上跳,问有多少种跳法

解决思路:因为最后一跳只能为1或是2,那么当从第三个台阶开始时,跳法等于一个台阶的跳法加两个台阶的跳法之和

斐波那契数列的优化

在之前的实现代码中,它的运算流程如下

可以看到,存在许多重复运算。因此我们可以对其进行记忆化(做缓存)

java 复制代码
    public static int cache(int n) {
        int[] cache = new int[n + 1];
        Arrays.fill(cache, -1);
        cache[0] = 0;
        cache[1] = 1;
        return f1(cache, n);
    }

    public static int f1(int[] cache, int n) {
        if (cache[n] != -1) {
            return cache[n];
        }
        int x = f1(cache, n - 1);
        int y = f1(cache, n - 2);
        cache[n] = x + y;
        return cache[n];
    }

这种实现方式采用了以空间换取时间。

爆栈问题

每次调用方法时,JVM会给该方法分配一个内存空间,当递归次数过多时内存也会占用过多,当内存分配完毕,还要接着递归时,就会抛出StackOverflowError异常

尾调用

如果函数的最后一步是调用一个函数,那么称为尾调用,例如

java 复制代码
function a() {
    return b()
}

下面代码不能叫做尾调用

java 复制代码
function a() {
    const c = b()
    return c
}
  • 因为最后一步并非调用函数
java 复制代码
function a() {
    return b() + 1
}
  • 最后一步执行的是加法

一些语言的编译器能够对尾调用做优化,例如

java 复制代码
function a() {
    // 做前面的事
    return b() 
}

function b() {
    // 做前面的事
    return c()
}

function c() {
    return 1000
}

没优化之前的伪码

java 复制代码
function a() {
    return function b() {
        return function c() {
            return 1000
        }
    }
}

优化后伪码如下

java 复制代码
a()
b()
c()

相当于平级调用,而不是嵌套调用。之所以可以这样优化,是因为a的函数返回结果就是b的返回结果,那么a的内存可以直接释放。b的返回结果是c的返回结果,那么也可以直接释放b所占用的内存。

但是Java并不支持这种尾调用优化。因此需要避免递归次数过多导致的爆栈问题。

相关推荐
NE_STOP8 分钟前
SpringBoot--如何整体读取多个配置属性及其相关操作
java·spring
熬了夜的程序员8 分钟前
【华为机试】HJ61 放苹果
算法·华为·面试·golang
马特说19 分钟前
基于随机森林的金融时间序列预测系统:从数据处理到实时预测的完整流水线
算法·随机森林·金融
呆呆的小鳄鱼20 分钟前
leetcode:HJ18 识别有效的IP地址和掩码并进行分类统计[华为机考][字符串]
算法·leetcode·华为
apihz29 分钟前
通用图片搜索-搜狗源免费API接口使用指南
android·java·python·php·音视频
风象南1 小时前
基于 SpringBoot 的 REST API 与 RPC 调用的统一封装
java·spring boot·后端
素雪风华1 小时前
Jenkins+Gitee+Docker容器化部署
java·docker·gitee·jenkins·springboot·持续部署
艾莉丝努力练剑1 小时前
【C语言】学习过程教训与经验杂谈:思想准备、知识回顾(五)
c语言·开发语言·数据结构·学习·算法
freexyn1 小时前
Matlab自学笔记六十二:求解三角函数方程的通解周期解
笔记·算法·matlab
zstar-_1 小时前
【算法笔记】7.LeetCode-Hot100-图论专项
笔记·算法·leetcode