递归
1.基础简介
递归在计算机科学中,递归是一种解决计算问题的方法,其中解决方案取决于同一类问题的更小子集
例如 递归遍历环形链表
- 基本情况(Base Case):基本情况是递归函数中最简单的情况,它们通常是递归终止的条件。在基本情况下,递归函数会返回一个明确的值,而不再进行递归调用。
- 递归情况(Recursive Case):递归情况是递归函数中描述问题规模较大的情况。在递归情况下,函数会调用自身来解决规模更小的子问题,直到达到基本情况。
优点
- 简洁清晰:递归能够将复杂的问题简化成更小的子问题,使得代码更加清晰易懂。
- 问题建模:递归能够自然地将问题建模成递归结构,使得问题的解决变得更加直观。
- 提高代码复用性:通过递归,可以在不同的情景中复用相同的解决方案。
缺点
- 性能损耗:递归调用涉及函数的重复调用和堆栈的频繁使用,可能会导致性能下降。
- 内存消耗:每次递归调用都需要在堆栈中存储函数的调用信息,可能会导致堆栈溢出的问题。
- 难以理解和调试:复杂的递归调用可能会导致代码的难以理解和调试,特别是递归函数中存在多个递归调用时。
常用场景
- 树和图的遍历:树和图的结构天然适合递归的处理方式,如深度优先搜索(DFS)。
- 分治算法:许多分治算法,如归并排序和快速排序,都是通过递归实现的。
- 动态规划:动态规划问题中,递归可以帮助描述问题的递归结构,但通常需要使用记忆化搜索或者自底向上的迭代方式来提高性能。
- 排列组合问题:许多排列组合问题,如子集、组合、排列等,可以通过递归实现。
/**
* 递归进行遍历
* @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.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
- 深入到最里层的 叫做递
- 从最里层出来的叫做归
- 在递过程中,外层函数内的局部变量(以及参数方法)并未消失,归的时刻还会用到。
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