【递归】:原理、应用与案例解析 ,助你深入理解递归核心思想

递归

1.基础简介

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

例如 递归遍历环形链表

  • 基本情况(Base Case):基本情况是递归函数中最简单的情况,它们通常是递归终止的条件。在基本情况下,递归函数会返回一个明确的值,而不再进行递归调用。
  • 递归情况(Recursive Case):递归情况是递归函数中描述问题规模较大的情况。在递归情况下,函数会调用自身来解决规模更小的子问题,直到达到基本情况。

优点

  1. 简洁清晰:递归能够将复杂的问题简化成更小的子问题,使得代码更加清晰易懂。
  2. 问题建模:递归能够自然地将问题建模成递归结构,使得问题的解决变得更加直观。
  3. 提高代码复用性:通过递归,可以在不同的情景中复用相同的解决方案。

缺点

  1. 性能损耗:递归调用涉及函数的重复调用和堆栈的频繁使用,可能会导致性能下降。
  2. 内存消耗:每次递归调用都需要在堆栈中存储函数的调用信息,可能会导致堆栈溢出的问题。
  3. 难以理解和调试:复杂的递归调用可能会导致代码的难以理解和调试,特别是递归函数中存在多个递归调用时。

常用场景

  1. 树和图的遍历:树和图的结构天然适合递归的处理方式,如深度优先搜索(DFS)。
  2. 分治算法:许多分治算法,如归并排序和快速排序,都是通过递归实现的。
  3. 动态规划:动态规划问题中,递归可以帮助描述问题的递归结构,但通常需要使用记忆化搜索或者自底向上的迭代方式来提高性能。
  4. 排列组合问题:许多排列组合问题,如子集、组合、排列等,可以通过递归实现。
/*** 递归进行遍历* @param node   下一个节点* @param before 遍历前执行的方法* @param after  遍历后执行的方法* @deprecated  递归遍历,不建议使用,递归深度过大会导致栈溢出。建议使用迭代器,或者循环遍历,或者使用尾递归,或者使用栈* @see #loop(Consumer, Consumer)*/
public void recursion(Node node, Consumer<Integer> before, Consumer<Integer> after){// 表示链表没有节点了,那么就退出(注意 环形链表的 末尾 不是null 而是头节点)if (node == sentinel){return;}// 反转位置就是逆序了before.accept(node.value);recursion(node.next, before, after);after.accept(node.value);
}
  1. 自己调用自己,说明每一个函数对应着一种解决方案,自己调用自己意味着解决方案是一样的或者说是有规律的
  2. 每次调用,函数处理的数据相对于上一次会缩减,而且最后会缩减至无需继续递归
  3. 内层函数调用(子集处理完成),外层函数才能调用完成

1.1.思路

首先需要确定自己的问题,能不能用递归的思路去解决

然后需要推导出递归的关系,父问题和子问题之间的关系, 以及递归的中止条件

f ( n ) = { 停止 , n = n u l l f ( n , n e x t ) , n ≠ n u l l f(n) = \begin{cases} 停止&, n = null \\ f(n,next)&, n \neq null \\ \end{cases} f(n)={停止f(n,next),n=null,n=null

  1. 深入到最里层的 叫做递
  2. 从最里层出来的叫做归
  3. 在递过程中,外层函数内的局部变量(以及参数方法)并未消失,归的时刻还会用到。

2.案例

2.1.案例1-求阶乘

@Test
@DisplayName("测试-递归-阶乘")
public void test1(){int factorial = factorial(5);logger.error("factorial :{}",factorial);
}/*** 阶乘* @param value 阶乘的值* @return 阶乘的结果*/
public int factorial(int value){// 递归的终止条件if(value ==1){return 1;}// 递归的公式 f(n) = n * f(n-1)return value * factorial(value-1);
}

在这里插入图片描述

2.2.案例2-字符串反转

  • 递:n从0开始,每次都从都头部对字符串进行分割,每次拼接的字符串只取第一位
  • 归:从 str.length() == 1开始归,从归开始拼接,自然是逆序的
# 思路递归的终止条件是字符串的长度为1, 递归的公式是 f(n) = f(n-1) + str.charAt(0) 从后往前拼接字符串
/*** 反向打印字符串序列* @param str 字符串* @return 反向打印的字符串*/
public String reverse(String str){if(str.length() == 1){return str;}logger.error("str.substring(1) = {} , str.CharArt(0) = {}",str.substring(1),str.charAt(0));// substring(1) 从下标为1的位置开始截取字符串, chatAt(0) 获取下标为0的字符return reverse(str.substring(1)) + str.charAt(0);
}@Test
@DisplayName("测试-递归-反向打印字符串序列")
public void test2(){String str = "abcdefg";String reverse = reverse(str);logger.error("reverse :{}",reverse);
}

在这里插入图片描述

2.3.案例3-递归二分查找

	/*** 二分查找* @param source 原始数组* @param target 目标值* @param left 左边界* @param right 右边界* @return 目标值的索引位置*/public int binaryFind(int source[],int target,int left,int right){// 先找到中间值int mid = (left + right) >>> 1;if (left > right){// 如果left > right 直接返回-1return -1;}if (source[mid] < target){// 如果中间值小于目标值,则在右边进行寻找return binaryFind(source,target,mid+1,right);} else if(source[mid] > target){// 如果中间值大于目标值 则在左边进行寻找return binaryFind(source,target,left,mid-1);} else {// 如果中间值等于目标值,则返回索引位置return mid;}}/*** 二分查找* @param source 原始数组* @param target 目标值* @return 目标值的索引位置*/public int search(int[] source,int target){// 二分查找 递归的终止条件是 left > rightreturn binaryFind(source,target,0,source.length-1);}@Test@DisplayName("测试-递归-二分查找")public void test3(){int[] source = {1,2,3,4,5,6,7,8,9,10};int target = 3;int index = search(source,target);logger.error("index :{}",index);}

在这里插入图片描述

2.4.案例4-递归冒泡排序

递归冒泡排序原理:

递归冒泡排序是一种排序算法,它将数组分成已排序和未排序两部分。通过递归地比较相邻元素并交换它们的位置,每次递归都将未排序部分的最大元素移到已排序部分的末尾,直到整个数组有序。

实现思路:

  1. 初始化:将整个数组视为未排序部分。
  2. 递归调用:递归地调用 bubble_sort() 函数来处理未排序部分,直到未排序部分长度为0或1,排序结束。
  3. 比较与交换:在每次递归中,从数组开始处向后遍历,比较相邻元素。如果前一个元素大于后一个元素,则交换它们的位置。
  4. 更新未排序部分:记录每次交换的位置,即最后一次交换的索引,作为下一次递归的边界,确保下一次递归只需处理未排序部分的子数组。
  5. 终止条件:递归终止条件是未排序部分长度为0或1,表示数组已排序完成。

可优化的地方及优势:

  • 优化点:递归冒泡排序在每次递归中,对未排序部分进行了全遍历,可能导致效率较低,尤其是对于大型数组。
  • 优势:递归冒泡排序的主要优势在于其简洁易懂的实现方式,易于理解和实现。

实现突出重点:

  • 递归调用:通过递归调用 bubble_sort() 函数,将排序过程分解为子问题,直到基本情况(未排序部分长度为0或1)得到解决。
  • 边界更新:每次递归后,更新未排序部分的边界,使下一次递归只需处理未排序部分的子数组。
  • 终止条件:设定递归终止条件,确保排序过程能够正确结束。
# 思路比较相邻的元素。如果第一个比第二个大,就交换他们两个。对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。针对所有的元素重复以上的步骤,除了最后一个。持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
# 控制1(递归)每次重新划分排序的区间,负责把已经排序的区间进行过滤2(循环)负责两两比较交换。

在这里插入图片描述

在这里插入图片描述

	/*** 递归冒泡排序* <ul>*     <li>将数组划分成两部分,[0,j] [j+1,length - 1]</li>*     <li>[0,j] 左边是未排序的部分</li>*     <li>[j+1,length - 1] 右边是已经排序的部分</li>*     <li>在未排序的区间内,相邻的两个元素比较,如果前一个元素大于后一个元素,那么交换位置</li>** </ul>* @param source*/public void sort (int [] source){bubble_sort(source,source.length-1);}/*** 递归冒泡排序* @param source 待排序的数组* @param j 未排序的区间的起始位置*/public void bubble_sort(int [] source,int j){// 递归的终止条件是数组的长度为1 或者数组的长度为0if (j == 0){return;}// x充 未排序 和已经排序的分界线int x = 0;// 每次都是从0开始for (int i = 0; i < j;i++){// 如果前一个元素比后一个元素大,那么交换位置if (source[i] > source[i+1]){int temp = source[i];source[i] = source[i+1];source[i + 1] = temp;x = i;}}// 递归调用(因为每次最大值都会移动到最后,所以每次的排序区间都往前进行移动)bubble_sort(source,x-1);}@Test@DisplayName("测试-递归-冒泡排序")public void test4(){int[] source = {4,3,2,1,5,6,7};sort(source);logger.error("source :{}",source);}

在这里插入图片描述

2.5.案例5-插入排序

插入排序原理:

插入排序是一种直观的排序算法,类似于整理扑克牌。它从未排序的部分选取元素,并将其插入到已排序的序列中,直到所有元素都排好序为止。

实现思路:

这段代码使用递归来实现插入排序。在递归函数 insertion() 中,每次调用时,它从未排序的部分选择第一个元素 t = source[low],然后将其插入到已排序的序列中的适当位置。

具体实现过程如下:

  1. 从右向左遍历已排序的部分,找到第一个比待插入元素小的位置。
  2. 将比待插入元素大的元素往后移一位,为待插入元素腾出空间。
  3. 插入待排序元素到找到的位置,插入位置为 i + 1,其中 i 是最后一个比待插入元素小的元素的下标。

递归终止条件:

递归的终止条件是当 low 等于数组长度时,表示所有元素都已处理完成,无需继续排序。

在这里插入图片描述

在这里插入图片描述

/*** 插入排序* @param source 原始数组*/
public void insert_sort(int[]source){// 递归调用插入排序 low 从1开始insertion(source,1);
}/*** 插入排序* @param source 原始数组* @param low 未排序数据的起始位置*/
private void insertion(int[]source,int low){// 递归的终止条件是 low == source.lengthif(low == source.length){return;}// 存储临时变量 (存放low指向的数据)int t = source[low];// 已经排序区域的指针int i = low -1;// 从右往左找,只要找到第一个比t小的就能确认插入位置while (i >=0 && source[i] > t ){// 如果没有找到插入位置 一直循环// 空出插入位置source[i+1] = source[i];i--;}// 找到了插入位置// 将t赋值给i +1 的位置就行了source[i + 1]  = t;insertion(source,low + 1);}@Test
@DisplayName("测试-递归-插入排序")
public void test5(){int[] source = {2,4,5,10,7,1};insert_sort(source);logger.error("source :{}",source);
}

在这里插入图片描述

多路递归

2.案例1-斐波那契数列

  • 每个递归只包含一个自身的调用,称之为single recursion
  • 如果每个递归函数包含多个自身的调用称为multi recursion

f ( n ) = { 0 , n = 0 1 , n = 1 f ( n − 1 ) + f ( n − 2 ) , n > 1 f(n) = \begin{cases} 0&, n = 0 \\ 1&, n = 1 \\ f(n-1)+f(n-2)&, n > 1 \\ \end{cases} f(n)= 01f(n1)+f(n2),n=0,n=1,n>1


	@Test@DisplayName("测试-递归-斐波那契数列")public void test1(){int factorial = factorial(10);logger.info("factorial:{}",factorial);}/*** 斐波那契数列* @param n 传入的参数* @return 返回的结果*/public int factorial(int n){// 递归的出口,当n为0时,返回0,当n为1或者2时,返回if (n == 0){return 0;}if (n == 1  || n == 2){return 1;}// 依次往下递归return factorial(n-1) + factorial(n -2);}

在这里插入图片描述

递归爆栈

1.分析

在Java中,递归爆栈是指递归调用导致调用栈溢出的情况。在解释递归爆栈时,我们可以涉及到Java的内存模型和变量存储位置的分析。

1.1 Java 内存模型:

Java程序在运行时,内存被划分为不同的区域,其中涉及到:

  • 堆(Heap):用于存储对象实例,由Java垃圾回收器进行管理和清理。
  • 栈(Stack):每个线程都有自己的栈,用于存储局部变量、方法调用和部分对象引用。
  • 方法区(Method Area):存储类的结构信息、静态变量、常量等。
  • 程序计数器(Program Counter):记录线程执行的当前位置。

1.2. 递归的内存分析:

在Java中,每次方法调用都会在栈上分配一定的空间,包括方法的参数、局部变量和返回地址。当一个方法被调用时,会将当前方法的上下文(包括参数、局部变量等)推入栈中,当方法执行结束时,栈顶的帧会被弹出。

1.3. 递归爆栈的原因:

递归函数在调用自身时,会持续地将新的调用帧推入栈中,如果递归调用的深度过大,栈空间会耗尽,导致栈溢出错误。

1.4. 变量存储位置分析:

  • 局部变量(Local Variables):在方法执行时,局部变量存储在栈帧中,并且随着方法的结束而被销毁。
  • 实例变量(Instance Variables):实例变量存储在对象的堆内存中,随着对象的创建和销毁而分配和释放。
  • 静态变量(Static Variables):静态变量存储在方法区中,它们在类加载时被初始化,在程序结束时销毁。

2.代码

/*** 递归求和* @param n 传入的参数* @return 返回的结果*/
public int add(int n){if (n == 1){return 1;}return add(n -1)  + n;
}@Test
@DisplayName("测试-递归-递归求和")
public void test2(){int sum = add(11111110);logger.error("sum:{}",sum);
}

在这里插入图片描述

3.解决

# 目前只有C++ 和 scala 能针对尾递归优化,所以我们一般需要将递归转为循环来写
  • 尾调用
// 如果函数的最后一步,是调用一个函数,那么成为尾调用
function a(){return b();
}// 下面这个 三段代码并不能称为尾调用
function a(){// 虽然调用了函数,但是又用到了外层函数的数值1return b() + 1;
}function a(){// 最后异步并非调用函数const c = b() + 1return c;
}function a(x){// 虽然调用了函数,但是又用到了外层函数的数值xreturn b() + x;
}

4.总结

递归爆栈的问题通常发生在递归调用的深度过大时,导致栈空间耗尽。通过合理控制递归调用深度、优化算法或者考虑使用迭代等方法可以避免这类问题,在Java中,局部变量和方法调用的栈帧管理是导致递归爆栈的关键因素之一。
递归是一种强大的问题解决工具,能够简化问题、提高代码的清晰度和可读性。然而,在使用递归时,需要注意避免潜在的性能问题和堆栈溢出问题。选择适当的场景和合适的算法,可以充分发挥递归的优势,提高程序的效率和可维护性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/691965.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

揭示端侧大语言模型的无限潜力:多种量化模型,可以在个人电脑或者手机上安装部署使用,几行代码进行调研可以离线使用

揭示端侧大语言模型的无限潜力:多种量化模型,可以在个人电脑或者手机上安装部署使用,几行代码进行调研可以离线使用。 MiniCPM 是面壁智能与清华大学自然语言处理实验室共同开源的系列端侧大模型,主体语言模型 MiniCPM-2B 仅有 24亿(2.4B)的非词嵌入参数量, 总计2.7B参数…

频段划分学习射频知识的意义

一、射频电路设计与低频电路设计的不同点 随着频率提高&#xff0c;相应电磁波的波长与变得可与分立电路元件的尺寸相比拟时&#xff0c;电阻、电容和电感这些元件的电响应&#xff0c;将偏离他们的理想频率特性。以 WIFI 2.4G 频段为例&#xff0c;当频率为 2437MHz&#xff0…

Ubuntu安装wireguard服务端,windows连接

需求&#xff1a;想要随时随地远程到ubuntu电脑的内网中&#xff0c;ping通所有的内网ip&#xff0c;方便通过内网ip进行远程 一、ubuntu上安装配置wireguard服务 1&#xff0c;更新软件包并安装wireguard服务端软件 sudo apt update sudo apt install wireguard 2&#xff0…

Flutter学习1 - Android开发者快速上手

1、对应关系 概念对应关系 AndroidFlutter页面Activity和FragmentWidget视图ViewWidget页面跳转IntentNavigater网络库okHttphttp数据存储SharedPreference和SQLiteshared_preferences和sqflite 布局对应关系 AndroidFlutter布局文件xmlWidget线性布局LinearLayoutRow和Col…

在线图片生成工具:定制化占位图片的利器

title: 在线图片生成工具&#xff1a;定制化占位图片的利器 date: 2024/2/20 14:08:16 updated: 2024/2/20 14:08:16 tags: 占位图片网页布局样式展示性能测试响应式设计在线生成开发工具 在现代的网页设计和开发中&#xff0c;占位图片扮演着重要的角色。占位图片是指在开发过…

力扣日记2.20-【回溯算法篇】491. 非递减子序列

力扣日记&#xff1a;【回溯算法篇】491. 非递减子序列 日期&#xff1a;2023.2.20 参考&#xff1a;代码随想录、力扣 ps&#xff1a;放了个寒假&#xff0c;日记又搁置了三星期……&#xff08;下跪忏悔&#xff09; 491. 非递减子序列 题目描述 难度&#xff1a;中等 给你一…

各种手型都合适,功能高度可定制,雷柏VT9PRO mini和VT9PRO游戏鼠标上手

去年雷柏推出了一系列支持4KHz回报率的鼠标&#xff0c;有着非常敏捷的反应速度&#xff0c;在游戏中操作体验十分出色。尤其是这系列4K鼠标不仅型号丰富&#xff0c;而且对玩家的操作习惯、手型适应也很好&#xff0c;像是VT9系列就主打轻巧&#xff0c;还有专门针对小手用户的…

二维红外流程

x.1 开激光器 先将TDG&#xff0c;TCU&#xff0c;Empower打开&#xff0c;等一分钟后将TDG和Empower的钥匙打到On上&#xff1b; 按顺序先后开MaiTai&#xff1b;ACE&#xff1b;TOPAS&#xff1b;AOM&#xff1b; 测量ACE出光口处功率&#xff08;3.8w&#xff09;&#x…

MybatisPlus多表联查-分页关联查询+根据id获取多表联查后的单行数据

分页关联查询 需求分析 有两张表w以及d&#xff0c;需要w的一些字段以及d的一些字段在前端显示 此时就需要用到关联查询&#xff0c;查询到的数据放入视图类&#xff0c;显示在前端 项目结构 视图类 package com.wedu.modules.tain.entity.vo;import lombok.Data;import ja…

【算法】约瑟夫环问题解析与实现

一、导言 约瑟夫环&#xff08;Josephus Problem&#xff09;是一个经典的数学问题&#xff0c;涉及一个编号为 1 到 n 的人围成一圈&#xff0c;从第一个人开始报数&#xff0c;报到某个数字 m 的人出列&#xff0c;然后再从下一个人开始报数&#xff0c;如此循环&#xff0c…

挑战杯 基于RSSI的室内wifi定位系统

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; wifi室内定位系统 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;…

仿照elementUI写个步骤条

效果图如下&#xff1a; 直接上代码&#xff1a; <template><div class""><div class"steps"><div class"step" v-for"(step, index) in steps" :key"index"><divclass"icon"click&…

深度学习发展的艺术

将人类直觉和相关数学见解结合后&#xff0c;经过大量研究试错后的结晶&#xff0c;产生了一些成功的深度学习模型。 深度学习模型的进展是理论研究与实践经验相结合的产物。科学家和工程师们借鉴了人类大脑神经元工作原理的基本直觉&#xff0c;并将这种生物学灵感转化为数学模…

公司有多个域名,是不是每个域名都安装一个SSL证书?

公司有多个域名&#xff0c;是不是每个域名都安装一个SSL证书&#xff1f; 拥有多个域名可能意味着会增加管理的复杂性和维护成本。如果您的公司有很多个域名&#xff0c;并不是每个域名都要逐个安装SSL证书。可以考虑购买多域名SSL证书或通配符SSL证书&#xff0c;而不是为每…

【Java】上传图片至阿里云oss服务器,文件上传以及图片base64上传

目录 引入maven依赖编写ossUtil编写Controller使用接口工具进行测试测试表单文件上传测试base64文件上传 首先需要在阿里云上开通oss服务&#xff0c;并获取到以下四个信息 endpoint: 购买的服务器节点地址accessKeyId: 接入的keyIdaccessKeySecret: 接入的key秘钥bucketName&…

[计算机网络]---UDP协议

前言 作者&#xff1a;小蜗牛向前冲 名言&#xff1a;我可以接受失败&#xff0c;但我不能接受放弃 如果觉的博主的文章还不错的话&#xff0c;还请点赞&#xff0c;收藏&#xff0c;关注&#x1f440;支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 目录 一、端口号…

MyBatis数据库查询

文章目录 什么是MyBatisMyBatis程序的创建MyBatis实现数据库查询传参查询插入实现添加操作获取自增ID删除实现修改实现#{}和${}SQL注入 like查询 resultMap和resultType多表查询 对于普遍的后端开发而言&#xff0c;其程序主要包含了后端主程序和数据库两个部分&#xff0c;用户…

C/C++ BM11 链表相加(二)

文章目录 前言题目解决方案一1.1 思路阐述1.2 源码 总结 前言 这道题使用链表来实现加法运算&#xff0c;主要是涉及到数据对位以及加法进位的处理。 题目 假设链表中每一个节点的值都在 0 - 9 之间&#xff0c;那么链表整体就可以代表一个整数。 给定两个这种链表&#xff0…

使用C# Net6连接国产达梦数据库记录

达梦官网&#xff1a;http://www.dameng.com/ 1 下载达梦并进行安装 下载地址&#xff1a;官网首页——服务与合作——下载中心&#xff08;https://www.dameng.com/list_103.html&#xff09; 根据需要自行下载需要的版本&#xff0c;测试版本为&#xff1a;x86 win64 DM8版…

微信商户平台:如何查看超级管理员

如果有普通员工身份&#xff0c;可以登录 https://pay.weixin.qq.com/index.php/core/account/info 扫码进入后&#xff0c;进入菜单&#xff1a;账户中心》商户信息&#xff0c; 即可看到超级管理员的信息。