算法重温:递归(定义、案例、优化、递归改非递归)

目录

1. 递归定义及应用

      定义

      优缺点

      三要素 

2. 递归经典案例

      案例一:斐波那契数列(递归、非递归)

       递归实现

       非递归实现

      案例二:求阶乘(递归、非递归)

       递归实现

       非递归实现

      案例三:汉诺塔问题

      案例四:青蛙跳台阶

      案例五:快速排序(递归、非递归)

       递归实现

       非递归实现

      案例六:图的遍历——深度优先(递归、非递归)

       递归实现

       非递归实现

3. 递归的优化

 时间复杂度

 空间复杂度

4. 总结


1. 递归定义及应用

      定义

        递归,即在运行的过程中调用自己。 就是通过直接或间接调用自己,将一个复杂的问题拆              成若干容易解决的子问题,再分别解决这些子问题以达到解决原复杂问题的目的。

      优缺点

        优点:

        ① 代码清晰简洁,便于阅读和理解;

        ② 简化了复杂问题,针对某类问题提供了更简单的解题思路和策略

        缺点:

        ① 重复计算多,时间和空间消耗大(所以递归有一种优化就是减少重复计算导致的开销大)

        ② 调用栈溢出,每次调用都会在内存栈中分配空间,可能导致超出调用栈的容量

      三要素 

        ① 确定递归函数的参数和返回值:明确该函数的功能,确定需要的参数和返回类型

        ② 确定递归终止条件:明确递归的结束条件,避免一直调用自己造成内存栈溢出

        ③ 确定单层递归的逻辑:寻找原函数的等价关系式,明确每次递归具体进行的操作

2. 递归经典案例

        递归其实都可以转换为非递归实现,分为直接转换法和间接转换法。直接转换法直接使用循环替代递归,适用于结构简单的递归(尾递归、单项递归);间接转换法则是在循环的基础上利用栈来保存中间结果,达到替代递归的作用。后面会有几个案例同时使用递归和非递归来实现,帮助大家更好地理解递归。

      案例一:斐波那契数列(递归、非递归)

        题目:斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列: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 ∈ N*),求第n项的值

       递归实现

        解析:

        分析三要素

        ① 函数参数和返回值:参数为需要求值的序号,例如求第3项,参数就为3,函数返回类型为对应的值,是整型;

int f(int n){
    
}

        ② 结束条件:因为F(1)和F(2)的值明确给出,为1,故当n = 1或n = 2时结束,所以递归结束的条件为n <= 2;

int f(int n) {
	if (n <= 2)	return 1;
}

        ③ 等价关系: 题目已经给出 F(n) = F(n-1) + F(n-2) (n>=3),直接加入代码即可;

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

        注:这种递归实现的计算量是非常大的,涉及到大量的重复运算,效率很低,不建议直接使用,只做理解,在后面分析递归优化时会讲相应的改进方法。

        完整代码如下。 

        完整代码:

#include<iostream>
using namespace std;

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

int main() {
	int n;
	cout << "请输入n:";
	cin >> n;
	cout << "斐波那契数列第" << n << "项的值为" << f(n) <<endl;
}

       非递归实现

        解析:单项递归,可以直接使用循环代替,由题可知,从第三项开始为前两项和,所以我们可以用两个变量f1、f2来作为前两项,初始化为1;

int f1 = 1, f2 = 1;

        使用循环,从第三项开始为前两项和,用fn 代表第n项的值,即fn = f1 + f2,赋值完毕后将原来的第二项改为第一项,当前值改为第二项,这样就能保证下一次循环的数一直是前两项的和;

for (int i = 1; i <= n; i++) {
		if(i >= 3){			
			fn = f1 + f2;	// 从第三项开始,值为前两项的和
			f1 = f2;		// 更新前两项
			f2 = fn;
		}
	}

        由于fn作为最终结果,n为1或2情况下fn不参与计算,故需要初始化为1,完整代码如下

        完整代码:

#include<iostream>
using namespace std;

int main() {
	int f1 = 1, f2 = 1, fn = 1, n;	
	cout << "请输入n:";
	cin >> n;
	for (int i = 1; i <= n; i++) {
		if(i >= 3){			
			fn = f1 + f2;	// 从第三项开始,值为前两项的和
			f1 = f2;		// 更新前两项
			f2 = fn;
		}
	}
	cout << "斐波那契数列第" << n << "项的值为" << fn << endl;
}

      案例二:求阶乘(递归、非递归)

        题目:给定一个数n,求n的阶乘(从 1 开始乘以比前一个数大 1 的数,一直乘到 n,用公式表示就是:1×2×3×4×…×(n-2)×(n-1)×n=n! )

       递归实现

        解析:

        分析三要素

        ① 函数参数和返回值:参数为需要求阶乘的数n,返回值为整型;

int f(int n) {

}

        ② 结束条件:当 n = 1 时,显而易见f(1) = 1,这就是该递归的结束条件,当然n = 2时,f(2) = 2,也可以作为结束条件,以此类推,知道f(3) = 6,也可将其作为结束条件。所以当你知道某一时刻具体的结果时,就可以把它作为结束条件。这里为了方便理解,选择1作为结束条件;

int f(int n) {
	if (n == 1) return 1;
}

        ③ 等价关系:当n为1时,值为1。当n > 1时,我们要做的就是让f(n)里的参数向结束条件靠拢,此时就是n要逐渐减少直到1为止,由于要求n的阶乘,为了缩小范围,可以让f(n) = n * f(n-1),此时参数在向1靠拢,故关系表达式为f(n) = n*f(n-1),加入代码即可

int f(int n) {
	if (n == 1) return 1;
	return n * f(n - 1);
}

        完整代码如下 

        完整代码:

#include<iostream>
using namespace std;

int f(int n) {
	if (n == 1) return 1;
	return n * f(n - 1);
}

int main() {
	int n;
	cout << "请输入n:";
	cin >> n;
	cout  << n << "的阶乘为" << f(n) << endl;
}

       非递归实现

        解析:单项递归,可以直接使用循环代替,把1-n之间的整数都乘在一起即可

for (int i = 1; i <= n; i++) {
		ans *= i;
	}

        完整代码如下

        完整代码:

        

#include<iostream>
using namespace std;

int main() {
	int n, ans = 1;
	cout << "请输入n:";
	cin >> n;
	for (int i = 1; i <= n; i++) {
		ans *= i;
	}
	cout << n << "的阶乘为" << ans << endl;
}

      案例三:汉诺塔问题

        题目:汉诺塔问题是一个经典的问题。汉诺塔(Hanoi Tower),又称河内塔,源于印度一个古老传说。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,任何时候,在小圆盘上都不能放大圆盘,且在三根柱子之间一次只能移动一个圆盘。问应该如何操作?(每次只能移动1个盘子,大盘子只能放在小盘子下面)

        解析:

        分析三要素

        ① 函数参数和返回值:参数为汉诺塔的层数n,以及三个柱子的代号,函数主要负责打印当前的移动情况,不需要返回值,故使用void;

void hanoi(int n, char A, char B, char C) {
	
}

        ② 结束条件:当塔只有一层,即n=1时为结束,将A柱中的盘子移到C柱即可

void hanoi(int n, char A, char B, char C) {
	if (n == 1) {
		num++;	
		cout << "步数:" << num << "    编号:" << n << "---从" << A << "移到" << C << endl;
		return;
	}
}

        ③ 等价关系:

        假设有n个圆盘,我们需要缩小范围,那就将n-1个盘子看成一个整体,放置逻辑如下:

        Ⅰ.将n-1这个整体放到B柱

        Ⅱ.将第n个圆盘放到C柱

        Ⅲ.将n-1这个整体放到C柱

        明显Ⅰ和Ⅲ为递归调用,n的范围不断缩小直到为1时达到结束条件;

        

    hanoi(n - 1, A, C, B);	// 将n-1整体放到B柱上
	num++;
	cout << "步数:" << num << "    编号:" << n << "---从" << A << "移到" << C << endl;
	hanoi(n - 1, B, A, C);	// 将B柱上的n-1移到C上

        完整代码如下 

        完整代码:

#include<iostream>
using namespace std;

int num = 0;	//计步

void hanoi(int n, char A, char B, char C) {
	if (n == 1) {	// n=1时,直接将A柱移到C柱
		num++;	
		cout << "步数:" << num << "    编号:" << n << "---从" << A << "移到" << C << endl;
		return;
	}
	hanoi(n - 1, A, C, B);	// 将n-1整体放到B柱上
	num++;
	cout << "步数:" << num << "    编号:" << n << "---从" << A << "移到" << C << endl;
	hanoi(n - 1, B, A, C);	// 将B柱上的n-1移到C上
}
int main() {
	int n;
	cout << "请输入塔的层数:";
	cin >> n;
	hanoi(n, 'A', 'B', 'C');
}

      案例四:青蛙跳台阶

        题目:一只青蛙可以一次跳 1 级台阶或一次跳 2 级台阶,例如:跳上第一级台阶只有一种跳法:直接跳 1 级即可。跳上两级台阶,有两种跳法: 每次跳 1 级,跳两次; 或者一次跳 2 级.问要跳上第 n 级台阶有多少种跳法?

        解析:

        分析三要素

        ① 函数参数和返回值:参数为台阶数,返回值为跳法数量,都为整型;

int f(int n){

}

        ② 结束条件:显而易见,当n为1时,有一种跳法;n为2时,有两种跳法。故可把此作为结束条件,再简单分析一下,n为3时有三种跳法,也可作为结束条件,这里方便理解就把n<=2的情况作为结束条件了。

int f(int n) {
	if (n <= 2) return n;
}

        ③ 等价关系:我们可以这样思考,当跳上第n级台阶时只有两种跳法,一个是从第n-1级台阶跳一步,一个是从第n-2级台阶跳两步。所以就是跳到第n-1级台阶的跳法数量加上跳到第n-2级台阶的跳法数量,这就是这个问题的等价关系,即 f(n) = f(n-1) + f(n-2),是不是和斐波那契数列一模一样?

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

        和斐波那契的关系式一样,意味着效率同样低下,所以解决的思路也相同,详情可以看后面递归优化方面的内容。

        完整代码如下

        完整代码:

#include<iostream>
using namespace std;

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

int main() {
	int n;
	cout << "请输入台阶数:";
	cin >> n;
	cout << "\n青蛙跳到第" << n << "级台阶共有 " << f(n) << " 种跳法" << endl;
}

      案例五:快速排序(递归、非递归)

       递归实现

        解析:

        快速排序的原理在之前的文章里详细讲过了,还不清楚的可以点击这里

        分析三要素

        ① 函数参数和返回值:参数分别为数组的左下标left、右下标right和数组本身,因为是在函数中直接改变数组元素顺序,不需要返回值,故使用void即可;

void quickSort(int left, int right, int* a) {

}

        ② 结束条件:我们知道快速排序的原理就是左右下标分别移动与基准数做对比,而左下标应小于右下标,所以当左下标的值大于等于右下标时,就是函数结束的条件;

void quickSort(int left, int right, int* a) {
	if (left >= right) return;
}

        ③ 等价关系:两边下标不断向中间靠拢,寻找比基准数大的数和小的数并交换,当两个下标相遇时基准数归位

	int i, j, t, temp;	// temp为基准数
	temp = a[left];		// 设置第一个元素为基准数
	i = left;
	j = right;
	while (i < j)
	{
		while (a[j] >= temp && i < j) j--;	// 从右到左找小于基准值的数
		while (a[i] <= temp && i < j) i++;	// 从左到右找大于基准值的数
		if (i < j) {	// 交换
			t = a[i]; a[i] = a[j]; a[j] = t;
		}
	}
	a[left] = a[i];	a[i] = temp;	// 将基准数归位

        基准数归位后,我们需要将基准数左侧和右侧作为两个数组,再分别进行基准数归位的操作,所以就是对基准数左侧和右侧的内容进行递归操作;

quickSort(left, i - 1, a);
quickSort(i + 1, right, a);

        完整代码如下

        完整代码:

// c++
#include<iostream>
using namespace std;

void quickSort(int left, int right, int* a);
int main() {
	int n;
	cout << "请输入数组长度:";
	cin >> n;
	cout << "\n输入整数数组:";
	int* a = (int*)malloc(sizeof(int) * n);
	for (int i = 0; i < n; i++) {
		cin >> a[i];
	}
	quickSort(0, n - 1, a);
	cout << "\n排序后数组为:";
	for (int i = 0; i < n; i++) {
		cout << a[i] << " ";
	}
}

void quickSort(int left, int right, int* a) {
	if (left >= right) return;
	int i, j, t, temp;	// temp为基准数
	temp = a[left];		// 设置第一个元素为基准数
	i = left;
	j = right;
	while (i < j)
	{
		while (a[j] >= temp && i < j) j--;	// 从右到左找小于基准值的数
		while (a[i] <= temp && i < j) i++;	// 从左到右找大于基准值的数
		if (i < j) {	// 交换
			t = a[i]; a[i] = a[j]; a[j] = t;
		}
	}
	a[left] = a[i];	a[i] = temp;	// 将基准数归位
	quickSort(left, i - 1, a);		// 递归
	quickSort(i + 1, right, a);
	return;
}

       非递归实现

        解析:

        计算机完成函数间调用是通过工作栈来实现的,那么将递归改为非递归,也就是利用栈保存中间结果,来模拟递归的效果,称为间接转换法。 

        首先我们需要将快排的区间压入栈,再取出,对这段区间进行单趟快排。接收返回值,然后再将它的左区间和右区间分别压栈,再取出、快排。当左区间元素只有1个时,不再压栈。当栈中没有任何元素时,代表所有区间都已经经过快排了,此时排序完成。

        完整代码:

// 非递归
#include<iostream>
#include<stack>
using namespace std;

// 前后指针法
int PrevCurMethod(int begin, int end, int* a)
{
	int cur = begin;
	int prev = cur - 1;
	int key = a[end];

	while (cur <= end)
	{
		if (a[cur] <= key) 
		{
			prev++;
			int temp = a[prev];
			a[prev] = a[cur];
			a[cur] = temp;
		}
		cur++;
	}
	return prev;
}

void quickSort(int begin, int end, int* a)
{
	stack<int> st;
	st.push(begin);	// 左边先入
	st.push(end);
	// 栈为空,代表排序完成
	while (!st.empty())
	{
		// 因为是end后入的,所以出的时候就是end先出,要用right接收
		int right = st.top();
		st.pop();
		int left = st.top();
		st.pop();

		// 传left和right【代表传一个区间过去,对这个区间进行快排】
		int keyindex = PrevCurMethod(left, right, a);

		// 不能取等,取等代表这个区间只有1个元素,已经有序
		if (left < keyindex - 1)
		{
			// 这里也必须是左边先入,右边后入;对应前面的先出右、后出左
			st.push(left);
			st.push(keyindex - 1);
		}

		if (keyindex + 1 < right)
		{
			st.push(keyindex + 1);
			st.push(right);
		}

	}
}

int main() {
	int n;
	cout << "请输入数组长度:";
	cin >> n;
	cout << "\n输入整数数组:";
	int* a = (int*)malloc(sizeof(int) * n);
	for (int i = 0; i < n; i++) {
		cin >> a[i];
	}
	quickSort(0, n - 1, a);
	cout << "\n排序后数组为:";
	for (int i = 0; i < n; i++) {
		cout << a[i] << " ";
	}
}

      案例六:图的遍历——深度优先(递归、非递归)

       递归实现

        解析:

        这里主要讲递归方面的内容,就不引入图的遍历中一些复杂的概念了,为了方便理解,直接使用一个二维数组来代表各点之间的连接关系。

        假设一个图有n个节点,那么可以创建一个二维数组a[n][n],将其所有元素初始化为0,当两个节点有连接关系的时候,就把对应的元素值改为1。举个例子,假如节点0和节点3、节点4相连,那么a[0][3]、a[0][4]的值就为1,同理a[3][0]、a[4][0]的值也为1。

        节点不一定全部连接在一起,比如5个节点的图,1、3、5连接,2、4连接,我们只需输入想查询的节点就可以查到与该节点连接的其它节点。这个案例中的程序,可以让我们只需知道节点两两之间的关系就能查到节点之间的连接关系。在实际应用中,节点与节点间的关系可能是实时变化的,那利用这个程序就可以得到变化后的节点连通状态了。

        分析三要素

        ① 函数参数和返回值:函数的参数为存储图的信息的二维数组、记录遍历情况的数组、当前的节点和节点总数,因为是遍历,不需要返回值,只需另起一个数组记录各节点的遍历情况即可;

void dfs(int** a, int* visit, int num, int n) {

}

        ② 结束条件:因为是遍历,所以结束的条件就是节点被遍历过的情况,这里我们使用visited数组来记录各节点的遍历情况;

void dfs(int** a, int* visited, int num, int n) {
	if (visited[num] == 1) return;	// 节点n已经被遍历到
}

        ③ 等价关系:我们以一个节点为起点进行遍历,遇到未遍历过的连通节点就以那个节点为新起点进行遍历,很明显这是一个递归的过程

for (int i = 0; i < n; i++) {
		if (i == num) continue;	// 本节点,跳过
		if (a[num][i] == 1 && visited[i] != 1) {	// 遇到有连接关系且未被遍历过的节点
			dfs(a, visited, i+1, n);	// 以新节点为起点继续遍历
		}
	}

         为了方便理解,我们以节点1作为第一个节点,而数组下标是从0开始的,所以我们在传入节点的时候,需要先将传入的数字减1,如果该节点没有被遍历过,更改其状态为已遍历,然后以它为新起点寻找下一个可遍历的节点;

void dfs(int** a, int* visited, int num, int n) {	// 从左至右:节点关系数组a,记录遍历情况数组visit,想查询连接关系的节点num,节点总数n
	num--;
	if (visited[num] == 1) return;	// 节点n已经被遍历到
	visited[num] = 1;	// 节点n未被遍历,记录
	for (int i = 0; i < n; i++) {
		if (i == num) continue;	// 本节点,跳过
		if (a[num][i] == 1 && visited[i] != 1) {	// 遇到有连接关系且未被遍历过的节点
			dfs(a, visited, i+1, n);	// 以新节点为起点继续遍历
		}
	}
}

        完整代码如下

        完整代码:

#include<iostream>
using namespace std;

void dfs(int** a, int* visited, int num, int n) {	// 从左至右:节点关系数组a,记录遍历情况数组visit,想查询连接关系的节点num,节点总数n
	num--;
	if (visited[num] == 1) return;	// 节点n已经被遍历到
	visited[num] = 1;	// 节点n未被遍历,记录
	for (int i = 0; i < n; i++) {
		if (i == num) continue;	// 本节点,跳过
		if (a[num][i] == 1 && visited[i] != 1) {	// 遇到有连接关系且未被遍历过的节点
			dfs(a, visited, i+1, n);	// 以新节点为起点继续遍历
		}
	}
}

int main() {
	int n;
	cout << "请输入节点个数:";
	cin >> n;
	int** m = new int*[n];	// 动态创建二维数组
	for (int i = 0; i < n; i++) {
		m[i] = new int[n];
	}
	for (int i = 0; i < n; i++) {
		if(i != n-1) cout << "\n请输入节点 " << i + 1 << " 与以下节点的连接关系(0为不相连,1为相连)" << endl;
		for (int j = i; j < n; j++) {
			if (i == j) {
				m[i][j] = 0;
				continue;
			}
			cout << "\n节点 " << j + 1 << " :";
			cin >> m[i][j];
			m[j][i] = m[i][j];
		}
	}
	int* visited = new int[n];
	int num;
	cout << "\n请输入想查询连接关系的节点:";
	cin >> num;
	dfs(m, visited, num, n);

	cout << "\n与节点" << num << "相连的节点有(包括节点 " << num << " )" << endl <<endl;
	for (int i = 0; i < 5; i++) {
		if (visited[i] == 1) {
			cout << "节点 " << i + 1 << "    ";
		}
		cout << endl;
	}
}

       非递归实现

        解析:

        这里也是使用栈来模拟递归的效果,先将起始点入栈并记录遍历状态,然后在循环中将栈顶取出作为起始点,找到所有与其相连且未被遍历过的点入栈并记录遍历状态,就这样不断循环取出栈顶遍历直到栈为空退出循环,遍历结束。

        完整代码:

#include<iostream>
#include<stack>
using namespace std;


// 非递归
void dfs_stack(int** a, int* visited, int num, int n) {
	num--;
	stack<int> st;
	st.push(num);	// 起始点入栈
	visited[num] = 1;
	while (!st.empty()) {
		int currNode = st.top();	// 取出栈顶元素作为新的起始点
		st.pop();	// 节点被遍历,出栈
		for (int i = 0; i < n; i++) {
			if (a[currNode][i] == 1 && visited[i] != 1) {	// 寻找所有连通的节点并压入栈
				st.push(i);
				visited[i] = 1;	// 记录被遍历到的节点
			}
		}
	}
}

int main() {
	int n;
	cout << "请输入节点个数:";
	cin >> n;
	int** m = new int*[n];	// 动态创建二维数组
	for (int i = 0; i < n; i++) {
		m[i] = new int[n];
	}
	for (int i = 0; i < n; i++) {
		if(i != n-1) cout << "\n请输入节点 " << i + 1 << " 与以下节点的连接关系(0为不相连,1为相连)" << endl;
		for (int j = i; j < n; j++) {
			if (i == j) {
				m[i][j] = 0;
				continue;
			}
			cout << "\n节点 " << j + 1 << " :";
			cin >> m[i][j];
			m[j][i] = m[i][j];
		}
	}
	int* visited = new int[n];
	int num;
	cout << "\n请输入想查询连接关系的节点:";
	cin >> num;
	dfs_stack(m, visited, num, n);

	cout << "\n与节点" << num << "相连的节点有(包括节点 " << num << " )" << endl <<endl;
	for (int i = 0; i < 5; i++) {
		if (visited[i] == 1) {
			cout << "节点 " << i + 1 << "    ";
		}
		cout << endl;
	}
}

3. 递归的优化

 时间复杂度

  在递归过程中,很多子问题是被重复计算的,如斐波那契数列:

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

 可以明显看到,以 f(6) 为例,f(4)、f(3)均被重复计算,f(2)被调用了5次,如果所求的数值更大的   话还会被调用更多次,可见效率非常低,时间复杂度高,为O(2^n)

 优化方案:

 为了防止重复计算的问题,在我们得到一个中间结果时,可以将这个结果保存起来,这样下次再     用到它时就可以直接取出结果,避免重复运算,其实本质就是动态规划的思想

 代码如下

#include<iostream>
using namespace std;

int f(int n, int* f_r) {
	if (n <= 2)	return 1;
	if (f_r[n] != 0) return f_r[n];		// 不等于0,说明该结果已被记录,直接返回即可
	f_r[n] = f(n - 2, f_r) + f(n - 1, f_r);	// 结果未被记录,则计算并记录
	return f_r[n];
}
int main() {
	int n;
	cout << "请输入n:";
	cin >> n;
	int* f_r = new int[n];	// 动态创建数组,用于记录中间结果
	for (int i = 0; i < n + 1; i++) {
		f_r[i] = 0;		// 初始化为0,方便判断结果是否被记录
	}

	cout << "斐波那契数列第" << n << "项的值为" << f(n, f_r) << endl;
}

  通过优化,每个中间结果只被计算一次,所以时间复杂度为O(n) 

 空间复杂度

  在传统的递归中,典型的模型是首先执行递归调用,然后获取递归调用的返回值并计算结果。以    这种方式,在每次递归调用返回之前,您不会得到计算结果。传统地递归过程就是函数调用,涉    及返回地址、函数参数、寄存器值等压栈,这样空间的开销是非常大的,容易造成栈溢出

  若函数在尾位置调用自身(或是一个尾调用本身的其他函数等等),就是尾递归。尾递归也是递    归的一种特殊情形。尾递归是一种特殊的尾调用,即在尾部直接调用自身的递归函数。

  而当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建    一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当    这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的    栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率    会变得更高

  所以我们对递归进行空间复杂度的优化时就可以将原来的递归改为尾递归

  以求n的阶乘为例

  原递归:需要保存多个调用记录,空间复杂度O(n)

int f(int n) {
	if (n == 1) return 1;
	return n * f(n - 1);
}

  改为尾递归:只需保留一个调用记录,空间复杂度O(1)

#include<iostream>
using namespace std;

// 改为尾递归
int f(int n, int result) {
	if (n == 1) return result;
	return f(n - 1, n*result);
}

int main() {
	int n;
	cout << "请输入n:";
	cin >> n;
	int result = 1;
	cout << n << "的阶乘为" << f(n, result) << endl;
}

  但是,尾递归优化只在严格模式生效,而且有些环境也是不支持尾递归优化的,比如python

  那么我们就需要将递归改为循环,这一部分可以看上面案例中求斐波那契和求n阶乘的非递归实现

  总结一下,在空间占用方面:非尾递归 > 尾递归 > 循环

4. 总结

        递归虽是一种最基础的算法,但它的思想非常重要:将复杂问题简单化,这也为我们以后解决一些复杂问题时提供了思路。在使用递归的过程中,我们可以根据递归三要素帮助自己建立递归关系,同时也要注意该递归带来的时间和空间开销,并加以相关的优化(保存中间结果、改为尾递归、改为非递归),合理使用这个强大的工具解决问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值