两个小例子轻松搞懂 java 中递归与尾递归的优化操作

废话不多说,我们直接上两个最常见的小例子:

一、递归,伪递归,迭代实现n!

package com.njbdqn.test02;

/**
 * 递归,伪递归,迭代实现n!
 */
public class RecursionTest {
 public static void main(String[] args) {
  System.out.println(recurse(5)); //递归显示
  System.out.println(camouflageRecurse(5, 1)); //伪递归
  System.out.println(iteration(5)); //迭代
 }

 /**
  * n的阶乘,尾递归实现方式
  *
  * @param n
  * @param result 计算保存的中间结果
  * @return 最终结果
  */
 public static int camouflageRecurse(int n, int result) {
  if (n == 1) {
   return result;
  } else {
   result = result * n;
   return camouflageRecurse(n - 1, result);
  }
 }

 /**
  * 求 n 的阶乘递归调用方式
  *
  * @param n n个数的阶乘
  * @return n个数阶乘的结果
  */
 public static int recurse(int n) {
  if (n == 1) {
   return 1;
  } else {
   return n * recurse(n - 1);
  }
 }

 /**
  * 用迭代的方法实现n的阶乘
  *
  * @param n
  * @return
  */
 public static int iteration(int n) {
  int result = 1;
  for (int i = 2; i <= n; ++i) {
   result *= i;
  }
  return result;
 }
}

二、斐波那契数列的递归和迭代实现求和

package com.njbdqn.test02;

/**
 * 斐波那契数列的递归和迭代实现求和
 * 0 1 1 2 3 5 8 13 21 34 55 89
 */
public class FibonacciTest {
 public static void main(String[] args) {
  System.out.println(fibonacciRecurse(14));
  System.out.println(fibonacciIteration(14));
  System.out.println(camouflageFibonacci(14,1,0));
 }
 /**
  * 递归调用实现斐波那契数列
  *
  * @param n
  * @return
  */
 public static int fibonacciRecurse(int n) {
  if (n == 1) {
   return 0;
  } else if (n == 2) {
   return 1;
  } else {
   return fibonacciRecurse(n - 1) + fibonacciRecurse(n - 2);
  }
 }
 /**
  * 迭代实现斐波那契数列
  * 0 1 1 2 3 5 8 13 21 34 55 89
  *
  * @param n
  * @return
  */
 public static int fibonacciIteration(int n) {
  int fab = 0; //最终结果 n的值
  int pre = 1; //记录n-1值
  int p = 0; //记录n-2的位置
  if (n == 1) {
   fab = 0;
  } else if (n == 2) {
   fab = 1;
  }
  for (int i = 2; i < n; ++i) {
   fab = pre + p;
   p = pre;
   pre = fab;
  }
  return fab;
 }
 /**
  * 斐波那契数列尾递归实现
  * 0 1 1 2 3 5 8 13 21 34 55 89
  *
  * @param n
  * @return
  */
  public static int camouflageFibonacci(int n, int result1,int result2) {
  if (n == 0) {
   return result1;
  } else {
   return camouflageFibonacci(n - 1, result2,result1+result2) ;
  }
 }
}

上述两个小例子我们都采用了迭代、递归和尾递归的方法去实现。迭代不必说,就是用我们java基础的 for 循环去实现。而在递归和尾递归实际上都是java 基础 oop 的自己调用自己方法的实现。尾递归实际上是对递归的优化。

递归

递归的本质是,某个方法中调用了自身。本质还是调用一个方法,只是这个方法正好是自身而已。

如第二个例子斐波那契数列的递归return fibonacciRecurse(n - 1) + fibonacciRecurse(n - 2)部分执行示意图如下所示:

递归的三大特性:

调用的是同一个方法

因为调用的是同一个方法,所以只需要写一个方法,就可以让你轻松调用无数次,所以调用的方法数可能非常巨大,其实在实际问题中往往都是方法数调用巨大的情况。

在自身中调用自身,本身就是嵌套调用(栈帧无法回收,开销巨大)

递归的局限性:

因为递归调用的方法数大都非常巨大和嵌套调用带来的栈帧无法回收,所以递归调用最大的诟病就是开销巨大,栈帧和堆一起爆掉,俗称内存溢出泄露。

java为了优化递归带来的内存溢出泄露,就有了尾递归的诞生。那么尾递归是如何优化递归的呢?

尾递归

尾递归优化是利用上面的第一个特点 “调用同一个方法” 来进行优化的。为了解决递归的开销大问题,使用尾递归优化,具体分两种方法:

尾递归优化方式:

尾递归的形式:把递归调用的形式写成尾递归的形式

编译器对尾递归的优化:编译器碰到尾递归,自动按照某种特定的方式进行优化编译

尾递归的形式:

尾递归其实只是一种对递归的特殊写法,这种写法原本并不会带来跟递归不一样的影响,它只是写法不一样而已,写成这样不会有任何优化效果,该爆的栈和帧都还会爆

递归的本质是某个方法调用了自身,尾递归这种形式就要求:某个方法调用自身这件事,一定是该方法做的最后一件事(所以当有需要返回值的时候会是return f(n),没有返回的话就直接是f(n)了)

这个f(n)外不能加其他东西,因为这就不是最后一件事了,值返回来后还要再干点其他的活,变量空间还需要保留。比如如果有返回值的,你不能:乘个常数 return 3f(n);乘个n return n*f(n);甚至是 f(n)+f(n-1)…

另外,使用return的尾递归还跟函数式编程有一点关系

编译器对尾递归的优化

简单说就是重复利用同一个栈帧,不仅不用释放上一个,连下一个新的都不用开,效率非常高

一方面是因为在递归调用自身的时候,这一层函数已经没有要做的事情了,虽然被递归调用的函数是在当前的函数里,但是他们之间的关系已经在传参的时候了断了,也就是这一层函数的所有变量什么的都不会再被用到了,所以当前函数虽然没有执行完,不能弹出栈,但它确实已经可以出栈了

另一方面是正因为调用的是自身,所以需要的存储空间是一毛一样的,那干脆重新刷新这些空间给下一层利用就好了,不用销毁再另开空间

如第二个例子斐波那契数列的尾递归return camouflageFibonacci(n - 1, result2,result1+result2)部分执行示意图如下所示:

说到这里你很容易联想到JAVA中的自动垃圾回收机制,同是处理内存问题的机制,尾递归优化跟垃圾回收是不是有什么关系,这是不是就是JAVA不实现尾递归优化的原因?

垃圾回收(GC)与 尾递归

首先我们需要谈一下内存机制,这里我们需要了解内存机制的两个部分:栈和堆。

在Java中, JVM中的栈记录了线程的方法调用。每个线程拥有一个栈。在某个线程的运行过程中, 如果有新的方法调用,那么该线程对应的栈就会增加一个存储单元,即栈帧 (frame)。在frame 中,保存有该方法调用的参数、局部变量和返回地址。Java的参数和局部变量只能是 基本类型 的变量(比如 int),或者对象的引用(reference) 。因此,在栈中,只保存有基本类型的变量和对象引用。而引用所指向的对象保存在堆中。具体如下图所示:

当被调用方法运行结束时,该方法对应的帧将被删除,参数和局部变量所占据的空间也随之释放。线程回到原方法,继续执行。当所有的栈都清空时,程序也随之运行结束。如上所述,栈 (stack)可以自己照顾自己。但堆必须要小心对待。堆是 JVM中一块可自由分配给对象的区域。当我们谈论垃圾回收 (garbage collection) 时,我们主要回收堆(heap)的空间。Java的普通对象存活在堆中。与栈不同,堆的空间不会随着方法调用结束而清空(即使它在栈上的引用已经被清空了)(也不知道为什么不直接同步清空)。因此,在某个方法中创建的对象,可以在方法调用结束之后,继续存在于堆中。这带来的一个问题是,如果我们不断的创建新的对象,内存空间将最终消耗殆尽。如果没有垃圾回收机制的话,你就需要手动地显式分配及释放内存,如果你忘了去释放内存,那么这块内存就无法重用了(不管是什么局部变量还是其他的什么)。这块内存被占有了却没被使用,这种场景被称之为内存泄露。

如下图所示:第二个例子斐波那契数列的尾递归每次调用自己的方法相当于在内存中缓存一个Object 的camouflageFibonacci 方法对象的引用,不会去释放,直到程序结束。

最原始的情况,都是需要手动释放堆中的对象,所以你经常需要考虑对象的生存周期,但是JAVA则引入了一个自动垃圾回收的机制,它能智能地释放那些被判定已经没有用的对象。

尾递归优化和垃圾回收最本质的区别是,尾递归优化解决的是内存溢出的问题,而垃圾回收解决的是内存泄露的问题。

内存泄露:指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用。

内存溢出:指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。

从定义上可以看出内存泄露是内存溢出的一种诱因,不是唯一因素。

自动垃圾回收机制的特点是:

解决了所有情况下的内存泄露的问题,但还可以由于其他原因内存溢出

针对内存中的堆空间

正在运行的方法中的堆中的对象是不会被管理的,因为还有引用(栈帧没有被清空)

一般简单的自动垃圾回收机制是采用 引用计数 (reference counting)的机制。每个对象包含一个计数器。当有新的指向该对象的引用时,计数器加 1。当引用移除时,计数器减 1,当计数器为0时,认为该对象可以进行垃圾回收

与之相对,尾递归优化的特点是:

优化了递归调用时的内存溢出问题

针对内存中的堆空间和栈空间

只在递归调用的时候使用,而且只能对于写成尾递归形式的递归进行优化

正在运行的方法的堆和栈空间正是优化的目标

以上这篇两个小例子轻松搞懂 java 中递归与尾递归的优化操作就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • java实现递归菜单树

    本文实例为大家分享了java实现递归菜单树的具体代码,供大家参考,具体内容如下 1.表结构 SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for menu -- ---------------------------- DROP TABLE IF EXISTS `menu`; CREATE TABLE `menu` ( `id` int(11) NOT NULL AUTO_INCREMEN

  • java TreeUtil菜单递归工具类

    本文实例为大家分享了java TreeUtil菜单递归工具类的具体代码,供大家参考,具体内容如下 菜单树(详细) package com.admin.manager.storeService.util; import com.admin.manager.storeService.entity.Menu; import java.util.ArrayList; import java.util.List; /** * @author m * @date 2019/12/16 */ public c

  • JAVA用递归实现全排列算法的示例代码

    求一个n阶行列式,一个比较简单的方法就是使用全排列的方法,那么简述以下全排列算法的递归实现. 首先举一个简单的例子说明算法的原理,既然是递归,首先说明一下出口条件.以[1, 2]为例 首先展示一下主要代码(完整代码在后面),然后简述 //对数组array从索引为start到最后的元素进行全排列 public void perm(int[]array,int start) { if(start==array.length) { //出口条件 for(int i=0;i<array.length;i

  • Java排序算法三之归并排序的递归与非递归的实现示例解析

    归并有递归和非递归两种. 归并的思想是: 1.将原数组首先进行两个元素为一组的排序,然后合并为四个一组,八个一组,直至合并整个数组: 2.合并两个子数组的时候,需要借助一个临时数组,用来存放当前的归并后的两个数组: 3.将临时数组复制回原数组对应的位置. 非递归的代码如下: package mergesort; import java.util.Arrays; import java.util.Random; import java.util.Scanner; //归并排序的非递归算法 publ

  • 使用java API实现zip递归压缩和解压文件夹

    一.概述 在本篇文章中,给大家介绍一下如何将文件进行zip压缩以及如何对zip包解压.所有这些都是使用Java提供的核心库java.util.zip来实现的. 二.压缩文件 首先我们来学习一个简单的例子-压缩单个文件.将一个名为test1.txt的文件压缩到一个名为Compressed.zip的zip文件中. public class ZipFile { public static void main(String[] args) throws IOException { //输出压缩包 Fil

  • Java使用递归复制文件夹及文件夹

    递归调用copyDir方法实现,查询源文件目录使用字节输入流写入字节数组,如果目标文件目录没有就创建目录,如果迭代出是文件夹使用字节输出流对拷文件,直至源文件目录没有内容. /** * 复制文件夹 * @param srcDir 源文件目录 * @param destDir 目标文件目录 */ public static void copyDir(String srcDir, String destDir) { if (srcRoot == null) srcRoot = srcDir; //源

  • 两个小例子轻松搞懂 java 中递归与尾递归的优化操作

    废话不多说,我们直接上两个最常见的小例子: 一.递归,伪递归,迭代实现n! package com.njbdqn.test02; /** * 递归,伪递归,迭代实现n! */ public class RecursionTest { public static void main(String[] args) { System.out.println(recurse(5)); //递归显示 System.out.println(camouflageRecurse(5, 1)); //伪递归 Sy

  • 一篇文章轻松搞懂Java中的自旋锁

    前言 锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) .这些已经写好提供的锁为我们开发提供了便利. 在之前的文章<一文彻底搞懂面试中常问的各种"锁" >中介绍了Java中的各种"锁",可能对于不是很了解这些概念的同学来说会觉得有点绕,所以我决定拆分出来,逐步详细的介绍一下这些锁的来龙去脉,那么这篇文章就先来会一会"自旋锁". 正文 出现原因 在我们的

  • 一文搞懂Java中的序列化与反序列化

    目录 序列化和反序列化的概念 应用场景 序列化实现的方式 继承Serializable接口,普通序列化 继承Externalizable接口,强制自定义序列化 serialVersionUID的作用 静态变量不会被序列化 使用序列化实现深拷贝 常见序列化协议对比 小结 序列化和反序列化的概念 当我们在Java中创建对象的时候,对象会一直存在,直到程序终止时.但有时候可能存在一种"持久化"场景:我们需要让对象能够在程序不运行的情况下,仍能存在并保存其信息.当程序再次运行时 还可以通过该对

  • 一文搞懂Java中的反射机制

    前一段时间一直忙,所以没什么时间写博客,拖了这么久,也该更新更新了.最近看到各种知识付费的推出,感觉是好事,也是坏事,好事是对知识沉淀的认可与推动,坏事是感觉很多人忙于把自己的知识变现,相对的在沉淀上做的实际还不够,我对此暂时还没有什么想法,总觉得,慢慢来,会更快一点,自己掌握好节奏就好. 好了,言归正传. 反射机制是Java中的一个很强大的特性,可以在运行时获取类的信息,比如说类的父类,接口,全部方法名及参数,全部常量和变量,可以说类在反射面前已经衣不遮体了(咳咳,这是正规车).先举一个小栗子

  • 一文搞懂Java中对象池的实现

    目录 1. 什么是对象池 2. 为什么需要对象池 3. 对象池的实现 4. 开源的对象池工具 5. JedisPool 对象池实现分析 6. 对象池总结 最近在分析一个应用中的某个接口的耗时情况时,发现一个看起来极其普通的对象创建操作,竟然每次需要消耗 8ms 左右时间,分析后发现这个对象可以通过对象池模式进行优化,优化后此步耗时仅有 0.01ms,这篇文章介绍对象池相关知识. 1. 什么是对象池 池化并不是什么新鲜的技术,它更像一种软件设计模式,主要功能是缓存一组已经初始化的对象,以供随时可以

  • 一文带你搞懂Java中的泛型和通配符

    目录 概述 泛型介绍和使用 泛型类 泛型方法 类型变量的限定 通配符使用 无边界通配符 通配符上界 通配符下界 概述 泛型机制在项目中一直都在使用,比如在集合中ArrayList<String, String>, Map<String,String>等,不仅如此,很多源码中都用到了泛型机制,所以深入学习了解泛型相关机制对于源码阅读以及自己代码编写有很大的帮助.但是里面很多的机制和特性一直没有明白,特别是通配符这块,对于通配符上界.下界每次用每次百度,经常忘记,这次我就做一个总结,加

  • 一文带你搞懂Java中的递归

    目录 概述 递归累加求和 计算1 ~ n的和 代码执行图解 递归求阶乘 递归打印多级目录 综合案例 文件搜索 文件过滤器优化 Lambda优化 概述 递归:指在当前方法内调用自己的这种现象. 递归的分类: 递归分为两种,直接递归和间接递归. 直接递归称为方法自身调用自己. 间接递归可以A方法调用B方法,B方法调用C方法,C方法调用A方法. 注意事项: 递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出. 在递归中虽然有限定条件,但是递归次数不能太多.否则也会发生栈内存溢出. 构造方

  • 一文搞懂Java中的日期类

    目录 一.日期类 1.1 第一代日期类 1.2 第二代日期类Calendar 1.3 第三代日期类 一.日期类 在程序的开发中我们经常会遇到日期类型的操作,Java对日期类型的操作提供了很好的支持.在最初的版本下,java.lang包中的System.currentTimeMillis();可以获取当前时间与协调时间(UTC)1970年1月1日午夜之间的时间差(以毫秒为单位测量).我们往往通过调用该方法计算某段代码的耗时. public class TestTime { public stati

  • 一文带你搞懂Java中Object类和抽象类

    目录 一.抽象类是什么 二.初始抽象类 2.1 基本语法 2.2 继承抽象类 三.抽象类总结 四.Object类 4.1 初始Object 4.2 toString 4.3 equals 4.4 hashcode 一.抽象类是什么 在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类. 由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用.也是因为这个原因,通常在设计阶段决定要

  • 一文搞懂Java中的注解和反射

    目录 1.注解(Annotation) 1.1 什么是注解(Annotation) 1.2 内置注解 1.3 元注解(meta-annotation) 1.4 自定义注解 2.反射(Reflection) 2.1 反射和反射机制 2.2 Class类的获取方式和常用方法 2.3 反射的使用 1.注解(Annotation) 1.1 什么是注解(Annotation) 注解不是程序本身,可以在程序编译.类加载和运行时被读取,并执行相应的处理.注解的格式为"@注释名(参数值)",可以附加在

随机推荐