递归

算法笔记第一期

递归

在学习递归之前,首先应该掌握的是函数的相关知识,如何定义以及调用函数。函数的作用在于

  • 代码重用
  • 问题分解

并且能够一定程度上体会模块化编程的思想。

递归实际上也是一种函数的应用。函数自己调用自己,这种调用称为“递归”。这种函数被称为“递归函数”。


观察以下程序,思考输出结果

#include <cstdio>

void function (int n) {
	if (n > 0) {
		function(n-1);
		for(int i = 1; i <= n; i++)
			printf ("#");
		printf("\n");
	}
}

int main () {
	function(5);
	return 0;
}

以上明显存在自我调用,因此是递归函数,上面函数执行过程如下图

每一次函数的调用,都会被存入系统栈空间。函数终止条件为n<=0,当到达使得n==0的调用函数,才会停止继续调用自身,返回上一次调用也就是function(1),接下来执行for循环语句,执行完该次调用,返回到function(2),继续执行for循环,依次类推,执行到function(5)。

递归思想

根据之前的例子,可以发现一个问题想用递归函数来解决,需要满足两个条件:

1、可以将原问题转换为一个新问题,新问题的解法相同于原问题,只是问题的规模缩小

2、递归函数要明确一个终止条件,在到达终止条件的的时候递回。

例1、阶乘的求法

​ 题目描述:

​ 输入一个n,用递归的方式求出n的阶乘(1<n<10),并输出答案。(n! = n×(n-1)×(n-2)…2×1)

​ 例如,输出 5,输出 120

问题分析:

​ n! = n × (n-1)!

​ 根据上面的分析结果来看,对于n的阶乘问题,我们可以转化为(n-1)的阶乘问题,同理(n-1)的阶乘可以转换为(n-2)的阶乘,这样问题的规模在不断缩小,直到1,结束递归。

递归关系式可以被表示如下

#include <cstdio>

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

int main () {
	int n;
	scanf ("%d", &n);
	printf("%d", factorial(n));
	return 0;
}

用5的阶乘作为例子,上述递归代码执行过程可表示为下图,当n=1时,出现边界,开始返回

题目练习

1.求斐波那契数列的第n项

输入n,按照斐波那契数列,输出第n项的大小(0<n<40)

参考程序

#include <cstdio>

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

int main () {
	int n;
	scanf ("%d", &n);
	printf("%d", fibonacci(n));
	return 0;
}

2.数的倒序

输入一个非负整数n(int范围内),使用递归的方法输出这个数的倒序结果(保留前置0)。

参考程序

#include <cstdio>

void reverse (int n) {
	if (n) {
		printf("%d", n%10);
		reverse(n/10);
	}
}


int main () {
	int n;
	scanf ("%d", &n);
	reverse(n);
	return 0;
}

3.将10进制的数转化为8进制的数

参考程序

#include <cstdio>

void convert (int n, int r) {
	if (n == 0) 
		return ;
	convert (n/r, r);
	printf("%d", n%r);
}


int main () {
	int n;
	scanf ("%d", &n);
	convert(n, 8);
	return 0;
}

小结

​ 采用“递归”思路解决问题的方法都是递归算法。

算法适用场景:

  • 数据的定义形式上是递归的,例如阶乘
  • 问题的解法是重复执行某种操作的,并且问题规模在缩小,有明显的边界。例如最大公约数、汉诺塔
  • 数据之间的逻辑关系是递归的,例如树、图的定义和操作

使用递归算法要注意的几点:

  • 明确递归终止条件(边界)
  • 给出递归终止的处理办法
  • 提取重复逻辑,缩小问题规模

巩固训练

1.求取最大公约数

​ 输入两个正整数m、n,求m和n的最大公约数(2000 > m,n > 1)

​ 例如输入

​ 56 48

​ 输出

​ 8

​ 采用辗转相除法进行处理,又称为欧几里德算法。过程是做除法运算,通过除数和余数反复做除法运算,当余数为0时,取当前算式除数为最大公约数。

例如求76和58的最大公约数

​ 根据辗转相除的结果,当余数为0时,那么2就是76和58的最大公约数,根据递归的思路(m,n)的最大公约数和(n, m%n)的最大公约数相同,问题规模缩小了,可以得出递归公式如下

\[gcd(m, n) = \begin{cases} m\quad\quad\quad\quad\quad\quad (n=0) \\ gcd(n, m\%n)\quad(n\,\not=\,0) \end{cases} \]

参考程序

#include <cstdio>

int gcd (int m, int n) {
	if (n == 0) return m;
	return gcd(n, m % n);
}

int main () {
	int m, n;
	scanf ("%d %d", &m, &n);
	printf("%d\n", gcd(m, n));
	return 0;
}

2.质因子分解

输入一个正整数n,从小到大输出它的所有质因子 (1<n<100000)

例如输出 18

输出 2 3 3

思路:从2开始试除,因为2是最小的质因子,如果2是n的因子,那么将问题转换为n/2,如果2不是n的因子,尝试3,依次类推,直到n被除成1为止。

参考程序

#include <cstdio>

void prime_factor (int n, int k) {
	if (n == 1) 
		return;
	if (n % k == 0) {
		printf("%d ", k);
		prime_factor(n/k, k);
	} else 
		prime_factor(n, k+1);
}

int main () {
	int n;
	scanf ("%d", &n);
	prime_factor(n, 2);
	return 0;
}

3.分解因数

​ 给定一个正整数a,要求分解成若干个正整数的乘积,即a = a1 × a2 × a3 × .... × an,并且 1 < a1 <= a2 <= a3 <= ... <= an。求这样的分解总数有多少,a=a也是一种分解

【输入】

第1行是测试数据的组数n,后面跟着n行输入。每组测试数据占1行,包括一个正整数a(1<a<32768)。

【输出】

n行,每行输出对应一个输入。输出应是一个正整数,指明满足要求的分解的种数。

【输入样例】

2
2
20

【输出样例】

1
4

注意考虑到本身也是一种分解情况。有些类似上一题,但不相同,计算出一个因子数可以进行加1操作,每次递归时,可以先将本身的情况算进去,如果本身是质数,那么要做出加1 的操作并且要结束分解。

参考程序

#include <cstdio>

int function (int n, int k) {
	int ans = 1; //算上本身这种情况
	for (int i = k; i*i <= n; i++) { // i*i<=n 相当于边界
		if (n % i == 0) {
			ans += function(n/i, i);
		}
	}
	return ans;
}

int main () {
	int n, number;
	scanf ("%d", &n);
	for (int i = 1; i <= n; i++) {
		scanf ("%d", &number);
		printf("%d\n", function(number, 2));
	}

	return 0;
}

4.数的计算

【题目描述】

我们要求找出具有下列性质数的个数(包括输入的自然数n)。先输入一个自然数n(n≤1000),然后对此自然数按照如下方法进行处理:

不作任何处理;

在它的左边加上一个自然数,但该自然数不能超过原数的一半;

加上数后,继续按此规则进行处理,直到不能再加自然数为止。

【输入】

自然数n(n≤1000)。

【输出】

满足条件的数

【输入样例】

6

【输出样例】

6

对于6,满足条件的有6,16,26,36,126,136。如果f(6)表示6的计数情况,显然f(6) = f(1)+f(2)+f(3)+1,意思是当前数字的前一半的情况之和等于当前这个数的计数,加1是表示本身。很明显可以使用递归。每次除2操作后产生的数只要大于1 都可以计入总和。需要注意的是n可能有1000个,在我们递归的过程中可能出现重复计算例如计算f(24),在缩减规模的时候,f(6)会被计算多次,这样产生相当大的浪费,于是可以将每一个数字的计数情况记录在bin数组中,这样当我们需要缩小规模的时候,如果f(n)已经被计算过来,直接从数组中取出即可。这样做也叫做记忆化

参考程序

#include <cstdio>
#define N 1005
int bin[N];
int number_count (int n) {
	int ans = 1;
	for (int i = 1; i <= n / 2; i++) {
		if (!bin[i])
			bin[i] = number_count(i);
		ans += bin[i];
	}
	return ans;
}

int main () {
	int n;
	scanf ("%d", &n);
	printf("%d\n", number_count(n));
	return 0;
}

总结

为什么递归能够缩减问题规模?

​ 因为递归就是通过不断改变函数的参数,从而逼近触发“递归终止条件”,这个过程中使得问题大事化小、小事化了。因此要能够总结出正确的递归关系式终止条件

递归的缺点:

  • 重复计算,因此算法效率较低
  • 递归条件不适当的情况下,容易出现死循环和栈溢出,因此递归深度是有限的。
  • 递归中不要定义局部数组(容易爆栈)

加深训练

1.汉诺塔

【问题描述】

​ 约19世纪末,在欧州的商店中出售一种智力玩具,在一块铜板上有三根杆,最左边的杆上自上而下、由小到大顺序串着由64个圆盘构成的塔。目的是将最左边杆上的盘全部移到中间的杆上,条件是一次只能移动一个盘,且不允许大盘放在小盘的上面。

​ 假定圆盘从小到大编号为1, 2, ...

【输入】

输入为一个整数(小于20)后面跟三个单字符字符串。

整数为盘子的数目,后三个字符表示三个杆子的编号。

【输出】

输出每一步移动盘子的记录。一次移动一行。

每次移动的记录为例如 a->3->b 的形式,即把编号为3的盘子从a杆移至b杆。

【输入样例】

2 a b c

【输出样例】

a->1->c
a->2->b
c->1->b

思路:假设有n个盘子要从a柱移动到b柱,那么考虑到问题规模缩小,将从上到下的n-1个盘子看出一个整体,那么需要做的是将n-1个盘子一起移动到c柱上,然后将a上的n号盘子移动到b上,最后将c柱上的n-1个盘子移动回b柱。这样就完成了整体的移动。

参考程序

#include <cstdio>


void hanoi (int n, char a, char b, char c) { //表示将n个盘子从a移动到b 借助c
	if (n == 0)
		return;
	hanoi(n-1, a, c, b);
	printf("%c->%d->%c\n", a, n, b);
	hanoi(n-1, c, b, a);
}

int main () {
	int n;
	char A, B, C;
	scanf("%d %c %c %c", &n, &A, &B, &C);
	hanoi(n, A, B, C);
	return 0;
}

2. 2的幂次方表示

【问题描述】

任何一个正整数都可以用2的幂次方表示。例如:

137=27+23+2^0

同时约定方次用括号来表示,即ab可表示为a(b)。由此可知,137可表示为:

2(7)+2(3)+2(0)

进一步:7=22+2+20(21用2表示)

3=2+2^0

所以最后137可表示为:

2(2(2)+2+2(0))+2(2+2(0))+2(0)

又如:

1315=210+28+2^5+2+1

所以1315最后可表示为:

2(2(2+2(0))+2)+2(2(2+2(0)))+2(2(2)+2(0))+2+2(0)

【输入】

一个正整数n(n≤20000)。

【输出】

一行,符合约定的n的0,2表示(在表示中不能有空格)。

【输入样例】

137

【输出样例】

2(2(2)+2+2(0))+2(2+2(0))+2(0)

思路:幂次方分解明显是递归的思想。由于n的最大为20000,2的15次方已经超过这个数了,因此可以将2的0到15次方存在数组b中,下标表示幂,b[0]表示2的0次方,b[2]表示2的2次方。这样便于从最接近n的幂开始寻找,找到第一个后n缩小为n-b[i]。然后将下标作为n继续分解。其中由于有+号需要注意如何利用全局变量,保证在递归回来后输出。

参考程序

#include <cstdio>
int b[16];
bool flag = true;

void init () {
	b[0] = 1;
	for (int i = 1; i < 16; i++)
		b[i] = b[i-1] * 2;
}

void power (int n) {
	while (n) //将当前的n全部分解完
		for (int i = 15; i >= 0; i --) {  //找到最接近n的2的幂
			if (b[i] <= n) {
				n -= b[i];
				if (flag) flag = false;
				else printf("+");
				if (i > 1) {
					printf("2(");
					flag = true;
					power(i);
					printf(")");
				}
				if (i == 1) //边界条件
					printf("2");
				else if (i == 0) //边界条件
					printf("2(0)");
			}
		}
}

int main () {
	init ();
	int n;
	scanf ("%d", &n);
	power(n);
	return 0;
}

3.放苹果

【问题描述】

把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?(用K表示)5,1,1和1,5,1 是同一种分法。

【输入】

第一行是测试数据的数目t(0≤t≤20)。以下每行均包含二个整数M和N,以空格分开。1≤M,N≤10。

【输出】

对输入的每组数据M和N,用一行输出相应的K。

【输入样例】

1
7 3

【输出样例】

8

思路:

​ 难点在于,找到递归关系式。首先考虑加入盘子的数量多于苹果的数量,那么势必会有空盘。并且空盘的数量至少是n-m。这些空盘都是无用的。

其次,对于m个苹果,n个盘子。总的情况中可以分为2类,第一类每个盘子至少有一个苹果,第二类至少有一个空盘。这两种情况,那么对于f(m,n),第一类只考虑每个盘子至少有一个苹果的情况数量,和从每个盘子拿一个苹果出来的情况数量是相同即f(m,n) = f(m-n, n),而第二类至少有一个空盘的情况,和将这个空盘拿走的情况数量是相同的,即f(m,n) = f(m, n-1)。将这两种情况组合就是总的情况。即f(m,n) = f(m-n,n) + f(m, n-1)。其中递归终点很显然,是盘子为1或者苹果数为0的时候只有一种情况。

参考程序

#include <cstdio>

int put_apple (int m, int n) {
	if (n > m)
		n = m;
	if (n == 1 || m == 0)
		return 1;
	return put_apple (m-n, n) + put_apple (m, n-1);
}

int main () {
	int t, m, n;
	scanf ("%d", &t);
	while (t --) {
		scanf("%d %d", &m, &n);
		printf("%d\n", put_apple(m, n));
	}
	return 0;
}

posted @ 2020-08-21 13:57  S_K_P  阅读(234)  评论(0)    收藏  举报