递归是如何工作的
简介:
递归的主要思想是分而治之,为求一个大规模问题的问题,将原问题划分成若干子问题 (递) ,当子问题规模小到一定程序,可以直接求解,即存在递归终止的条件,称做递归出口,之后再将足够小的子问题合并形成原问题的解 (归) 。
原理:
在执行嵌套函数,还未终止的过程中,未执行完的函数都会压到堆栈中去,返回函数的过程就是出栈的过程。
使用条件:
1.问题可分解成遵循重复模式的多个过程
2.最后一个下家满足终止条件,无需再往下交付,终结返回
3.递归深度不能太深,防止堆栈溢出
例题:
反转链表,例1->2->3->4经反转得到4->3->2->1
Node reverseList(Node head){
if (head == NULL || head.next == NULL) return head;//递归反转链表
Node newList = reverseList2(head.next);
head.next.next = head; //指针指向逆转
head.next = NULL; //尾结点指向NULL
return newList;
假设输入为
则执行过程如下所示
如何将递归用非递归的形式实现
递归与非递归:
非递归效率高,因为循环避免了一系列函数调用和返回中所涉及到的参数传递和返回值的额外开销,但是通常会引入循环来代替递归,这样造成了代码的可读性有所欠缺。
递归代码写出来思路清晰,可读性强。但是由于递归需要使用系统堆栈,所以空间消耗要比非递归代码要大很多。而且,如果递归深度太大,可能系统撑不住,造成堆栈溢出。
简而言之,非递归方便了计算机,麻烦了程序员,递归方便了程序员,麻烦了计算机
递归改进:
递归主要有三种:尾递归(即最后一句话进行递归)和单向递归(函数中只有一个递归调用地方)都可以用循环来避免递归,更复杂的情况则要引入栈来进行压栈出栈来改造成非递归(间接转换法),有些问题可以通过改变思路来实现非递归的实现(直接转换法)。
本文以斐波那契数列为例介绍了递归的实现以及剪枝操作和尾递归操作
间接转换法
每一个调用函数需要保存到栈帧的内容如下所示,另外通常每一个栈帧的数据都会使用一个结构体进行存储,而且这里的转换是纯机械性的,转换后并不能提高算法效率,一般还是需要依靠动态规划、深度优先搜索+剪枝等手段解决超时问题
例题:
斐波拉契数列F(1)=1,F(2)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 3,n ∈ N*)
递归形式的代码:
int fibonacci(int n) {
// flag 0 函数入口
if (n == 0 || n == 1) {
return 1;
}
else {
int result0 = fibonacci(n - 1);
// flag 1 第一个函数调用结束
// 拿到第一个函数的返回值,保存为局部遍历
int localVariable0 = result0;
int result1 = fibonacci(n - 2);
// flag 2 第二个函数调用结束
// 拿到第二个函数的返回值
int localVariable1 = result1;
return localVariable0 + localVariable1;
}
}
非递归形式的代码:
int fibonacciNoRecur(int n) {
Deque<Object[]> stack = new ArrayDeque<>(64); //栈 调用层数最大 64
// 当前函数(fibonacciNoRecur)的栈帧,栈帧size=1,仅用来接收递归调用的返回值
Object[] frameOfFibonacciNoRecur = { null };
stack.push(frameOfFibonacciNoRecur); // 入栈
// 函数调用第一层
// 栈帧含义:
// [0] 接收调用函数的返回值
// [1] flag,标记递归函数执行的位置
// [2] 入参,只有一个,即 n
// [3] [4] 两个局部变量
Object[] callFrame = { null, 0, n, null, null };
stack.push(callFrame);
while (stack.size() > 1) {// 如果栈大于 1,表示递归没有结束
Object[] frame = stack.peek();// 获取当前栈帧,peek()不退栈
int arg0 = (int)frame[2];// 当前入参
int flag = (int)frame[1];// 当前 flag 标志位
switch (flag) {
case 0: // flag == 0 函数入口
if (arg0 == 0 || arg0 == 1) {
stack.pop();// 退栈
stack.peek()[0] = 1;// 将返回值给调用者,即此时栈顶栈帧 0 号位置
}
else {
//函数调用,开辟栈空间
stack.push(new Object[]{ null, 0, arg0 - 1, null, null });
frame[1] = 1;// 修改当前栈帧的 flag=1
}
break;
case 1: // flag == 1 第一个函数调用结束
int return1 = (int)frame[0];// 拿到返回值,位于当前栈帧的 0 号位置
frame[3] = return1;// 将返回值存入局部变量
stack.push(new Object[]{ null, 0, arg0 - 2, null, null });// 第二次函数调用
frame[1] = 2;// 修改当前栈帧的 flag=2
break;
case 2:
int return2 = (int)frame[0];// 拿到返回值,位于当前栈帧的 0 号位置
frame[4] = return2;// 将返回值存入局部变量
int localVariable0 = (int)frame[3];// 取出局部变量
int localVariable1 = (int)frame[4];
// 退栈
stack.pop();// 退栈
stack.peek()[0] = localVariable0 + localVariable1;// 将返回值给调用者,即此时栈顶栈帧 0 号位置
}
}
// 递归调用结束,此时栈顶即当前函数(fibonacciNoRecur)的栈帧
frameOfFibonacciNoRecur = stack.pop();
return (int)frameOfFibonacciNoRecur[0];
}
此时的栈帧为
{null/*result*/,0/*flag*/,n/*入参*/,null/*第一个局部变量*/,null/*第二个局部变量*/}
递归与非递归代码的比对结果
递归的内存不够如何处理
这个涉及java的一些内容,暂时不会,今后再进行补充…