用递归法将一个整数n转化为字符串_算法笔记第一期——递归

递归

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

  • 代码重用

  • 问题分解

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

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

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

#include 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;}

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

dff34597a95aab65807cab62719d7bb3.png

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

递归思想

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

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

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

例1、阶乘的求法

题目描述:

输入一个n,用递归的方式求出n的阶乘(1

(n! = n×(n-1)×(n-2)…2×1)

例如,输出 5,输出 120

问题分析:

n! = n × (n-1)!

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

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

9dfcdc5601a86b3936e2b933b1911c36.png

程序如下

#include 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时,出现边界,开始返回

c0f9d7a3582c608f102259c03f50cf79.png

题目练习

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

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

参考程序

#include 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 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 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的最大公约数

8b7d65b2b16f2124b3355879399275f8.png

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

2a552e6d66d5945696aeb9447da0f6de.png

参考程序

#include 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

例如输出 18

输出 2 3 3

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

参考程序

#include 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

【输出】

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

【输入样例】

2

2

20

【输出样例】

1

4

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

参考程序

#include 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 #define N 1005int 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->ca->2->bc->1->b

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

参考程序

#include 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=2^7+2^3+2^0

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

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

进一步:7=2^2+2+2^0(21用2表示)

3=2+2^0

所以最后137可表示为:

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

又如:

1315=2^10+2^8+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 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。

【输入样例】

 17 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 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;}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值