算法--递归Recursion(十六)

以下是学习恋上数据结构与算法的记录,本篇主要内容是递归Recursion

递归(Recursion)

递归:函数(方法)直接或间接调用自身。是一种常用的编程技巧

函数的递归调用过程

在这里插入图片描述如果递归调用没有终止,将会一直消耗栈空间,最终导致栈内存溢出(Stack Overflow)
●所以必需要有一个明确的结束递归的条件,也叫作边界条件、递归基

实例分析
在这里插入图片描述注意:使用递归不是为了求得最优解,是为了简化解决问题的思路,代码会更加简洁,递归求出来的很有可能不是最优解,也有可能是最优解。

递归的基本思想

●拆解问题
✓把规模大的问题变成规模较小的同类型问题
✓规模较小的问题又不断变成规模更小的问题
✓规模小到一定程度可以直接得出它的解

●求解
✓由最小规模问题的解得出较大规模问题的解
✓由较大规模问题的解不断得出规模更大问题的解
✓最后得出原来问题的解

比如俄罗斯套娃?
凡是可以利用上述思想解决问题的,都可以尝试使用递归
✓很多链表、二叉树相关的问题都可以使用递归来解决
✓因为链表、二叉树本身就是递归的结构(链表中包含链表,二叉树中包含二叉树)

递归的使用套路
①明确函数的功能
✓先不要去思考里面代码怎么写,首先搞清楚这个函数的干嘛用的,能完成什么功能?
②明确原问题与子问题的关系
✓寻找f(n) 与f(n –1) 的关系
③明确递归基(边界条件)
✓递归的过程中,子问题的规模在不断减小,当小到一定程度时可以直接得出它的解
✓寻找递归基,相当于是思考:问题规模小到什么程度可以直接得出解?

练习1 –斐波那契数列
斐波那契数列:1、1、2、3、5、8、13、21、34、…
F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n≥3)
编写一个函数求第n 项斐波那契数

递归实现
递归调用的空间复杂度= 递归深度* 每次调用所需的辅助空间

int fib0(int n) {
		if(n<=2) return 1;
		return fib0(n-1)+fib0(n-2);
	}

fib递归函数的调用过程
在这里插入图片描述
出现了特别多的重复计算,这是一种“自顶向下”的调用过程

非递归实现

int fib2(int n) {
		if(n<=2) return 1;
		int array[] = new int[n+1];
		array[2] = array[1] =1;
		for(int i = 3; i<=n;i++) {
			array[i] = array[i-1] + array[i-2];
		}
		return array[n];
	}

非递归优化实现

int fib5(int n) {
		if(n<=2) return 1;
		int first = 1;
		int second = 1;
		for(int i=3;i<=n;i++) {
			second = first +second;
			first = second - first;
		}
		return second;
	}

在这里插入图片描述练习2 –上楼梯(跳台阶)
楼梯有n 阶台阶,上楼可以一步上1 阶,也可以一步上2 阶,走完n 阶台阶共有多少种不同的走法?
●假设n 阶台阶有f(n) 种走法,第1 步有2 种走法
✓如果上1 阶,那就还剩n –1 阶,共f(n –1) 种走法
✓如果上2 阶,那就还剩n –2 阶,共f(n –2) 种走法
所以f(n) = f(n –1) + f(n –2)

在这里插入图片描述

int climbStairs1(int n) {
		if (n <= 2) return n;
		return climbStairs1(n - 1) + climbStairs1(n - 2);
	}

跟斐波那契数列几乎一样,因此优化思路也是一致的

int climbStairs2(int n) {
		if (n <= 2) return n;
		int first = 1;
		int second = 2;
		for(int i=3;i<=n;i++) {
			second = first+second;
			first = second -first;
		}
		return second;
	}

练习3 –汉诺塔(Hanoi)
编程实现把A 的n 个盘子移动到C(盘子编号是[1, n] )
✓每次只能移动1个盘子
✓大盘子只能放在小盘子下面
在这里插入图片描述
3个盘子过程
在这里插入图片描述
在这里插入图片描述
汉诺塔–思路
其实分2 种情况讨论即可
✓当n == 1时,直接将盘子从A 移动到C
✓当n > 1时,可以拆分成3大步骤
①将n –1 个盘子从A 移动到B
在这里插入图片描述②将编号为n 的盘子从A 移动到C
③将n –1 个盘子从B 移动到C
在这里插入图片描述
步骤①③明显是个递归调用

汉诺塔–实现

/**
	 * 将 n 个碟子从 p1 挪动到 p3
	 * @param p2 中间的柱子
	 */
void hanoi(int n, String p1, String p2, String p3) {
		if(n == 1) {
			move(n, p1, p3);
			return ;
		}
		hanoi(n-1, p1, p3, p2);
		move(n, p1, p3);
		hanoi(n-1, p2, p1, p3);
	}
	/**
	 * 将 no 号盘子从 from 移动到 to
	 * @param no
	 * @param from
	 * @param to
	 */
	void move(int no, String from, String to) {
		System.out.println("将" + no + "号盘子从" + from + "移动到" + to);
	}

递归转非递归

递归调用的过程中,会将每一次调用的参数、局部变量都保存在了对应的栈帧(Stack Frame)中栈空间
在这里插入图片描述
若递归调用深度较大,会占用比较多的栈空间,甚至会导致栈溢出
在有些时候,递归会存在大量的重复计算,性能非常差
这时可以考虑将递归转为非递归(递归100%可以转换成非递归)

递归转非递归的万能方法
●自己维护一个栈,来保存参数、局部变量,但是空间复杂度依然没有得到优化
在这里插入图片描述
在这里插入图片描述

尾调用(Tail Call)

一个函数的最后一个动作是调用函数
如果最后一个动作是调用自身,称为尾递归(Tail Recursion),是尾调用的特殊情况
在这里插入图片描述
一些编译器能对尾调用进行优化,以达到节省栈空间的目的栈空间(Java只对尾递归进行优化,因为它的编译器不能改变栈帧)
在这里插入图片描述下面代码不是尾调用
在这里插入图片描述尾调用优化也叫做尾调用消除(Tail Call Elimination)
●如果当前栈帧上的局部变量等内容都不需要用了,当前栈帧经过适当的改变后可以直接当作被尾调用的函数的栈帧使用,然后程序可以jump 到被尾调用的函数代码

✓生成栈帧改变代码与jump 的过程称作尾调用消除或尾调用优化
✓尾调用优化让位于尾位置的函数调用跟goto语句性能一样高
✓消除尾递归里的尾调用比消除一般的尾调用容易很多
✓比如Java虚拟机(JVM)会消除尾递归里的尾调用,但不会消除一般的尾调用(因为改变不了栈帧),因此尾递归优化相对比较普遍,平时的递归代码可以考虑尽量使用尾递归的形式

jns为满足一定条件后跳转
在这里插入图片描述
尾递归示例1 –阶乘
在这里插入图片描述
尾递归示例2 –斐波那契数列
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_44961149/article/details/105096897