递归在计算机科学中,递归是一种解决计算问题的方法,其中解决方案取决于同一类问题的更小子集
例如 递归遍历环形链表
/**
* 递归进行遍历
* @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);
}
首先需要确定自己的问题,能不能用递归的思路去解决
然后需要推导出递归的关系,父问题和子问题之间的关系, 以及递归的中止条件
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
@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);
}
# 思路
递归的终止条件是字符串的长度为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);
}
/**
* 二分查找
* @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 直接返回-1
return -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 > right
return 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);
}
递归冒泡排序原理:
递归冒泡排序是一种排序算法,它将数组分成已排序和未排序两部分。通过递归地比较相邻元素并交换它们的位置,每次递归都将未排序部分的最大元素移到已排序部分的末尾,直到整个数组有序。
实现思路:
bubble_sort()
函数来处理未排序部分,直到未排序部分长度为0或1,排序结束。可优化的地方及优势:
实现突出重点:
bubble_sort()
函数,将排序过程分解为子问题,直到基本情况(未排序部分长度为0或1)得到解决。# 思路
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
# 控制
1(递归)每次重新划分排序的区间,负责把已经排序的区间进行过滤
2(循环)负责两两比较交换。
/**
* 递归冒泡排序
*
* - 将数组划分成两部分,[0,j] [j+1,length - 1]
* - [0,j] 左边是未排序的部分
* - [j+1,length - 1] 右边是已经排序的部分
* - 在未排序的区间内,相邻的两个元素比较,如果前一个元素大于后一个元素,那么交换位置
*
*
* @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 或者数组的长度为0
if (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);
}
插入排序原理:
插入排序是一种直观的排序算法,类似于整理扑克牌。它从未排序的部分选取元素,并将其插入到已排序的序列中,直到所有元素都排好序为止。
实现思路:
这段代码使用递归来实现插入排序。在递归函数 insertion()
中,每次调用时,它从未排序的部分选择第一个元素 t = source[low]
,然后将其插入到已排序的序列中的适当位置。
具体实现过程如下:
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.length
if(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);
}
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(n−1)+f(n−2),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);
}
在Java中,递归爆栈是指递归调用导致调用栈溢出的情况。在解释递归爆栈时,我们可以涉及到Java的内存模型和变量存储位置的分析。
Java程序在运行时,内存被划分为不同的区域,其中涉及到:
在Java中,每次方法调用都会在栈上分配一定的空间,包括方法的参数、局部变量和返回地址。当一个方法被调用时,会将当前方法的上下文(包括参数、局部变量等)推入栈中,当方法执行结束时,栈顶的帧会被弹出。
递归函数在调用自身时,会持续地将新的调用帧推入栈中,如果递归调用的深度过大,栈空间会耗尽,导致栈溢出错误。
/**
* 递归求和
* @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);
}
# 目前只有C++ 和 scala 能针对尾递归优化,所以我们一般需要将递归转为循环来写
// 如果函数的最后一步,是调用一个函数,那么成为尾调用
function a(){
return b();
}
// 下面这个 三段代码并不能称为尾调用
function a(){
// 虽然调用了函数,但是又用到了外层函数的数值1
return b() + 1;
}
function a(){
// 最后异步并非调用函数
const c = b() + 1
return c;
}
function a(x){
// 虽然调用了函数,但是又用到了外层函数的数值x
return b() + x;
}
递归爆栈的问题通常发生在递归调用的深度过大时,导致栈空间耗尽。通过合理控制递归调用深度、优化算法或者考虑使用迭代等方法可以避免这类问题,在Java中,局部变量和方法调用的栈帧管理是导致递归爆栈的关键因素之一。
递归是一种强大的问题解决工具,能够简化问题、提高代码的清晰度和可读性。然而,在使用递归时,需要注意避免潜在的性能问题和堆栈溢出问题。选择适当的场景和合适的算法,可以充分发挥递归的优势,提高程序的效率和可维护性。