一.青蛙跳台问题
二.汉诺塔问题
在对这些问题探讨的时候,我们需要了解递归的相关概念,以及对递归思想的进一步理解。
说的简单通俗一点,递归就是函数自己调用自己,如下这段代码:
#include<stdio.h>
int main()
{
printf("hello world\n");
main();//自己调用自己
return 0;
}
生成解决方案
这段代码毫无疑问陷入死循环,最终导致栈溢出(在下一篇博客<函数栈帧的创建和销毁>会把自己的理解分享给大家)。
所以在使用递归时,需要加上一个限制条件,当达到这个限制条件时,递归结束。
那么递归的思想是什么呢?
递归就是把一个复杂的问题拆分成n个与原问题类似的子问题,直到子问题不能再被拆解。
其中,递归需要拆开理解,递:传递,相当于拆解的过程。归:回归,相当于分解完子问题后,解决问题的一步。语言描述,晦涩枯燥。下面用一段具体的代码来说明:
例如:顺序打印一个整数的每一位。
输入:1234,输出1 2 3 4
首先,我们定义一个print函数帮我们完成打印功能,接着在main函数里面调用print函数,并且传递相关的参数:
#include<stdio.h>
void print(int n)
{
}
int main()
{
int input = 0;
scanf("%d",&input);
print(input);
return 0;
}
上面是一段伪代码,当我们把参数传给print函数时,我们就需要思考该怎么样才能输出我想要的结果。
1.首先我们需要得到1234的每一位,即让1234%10,得到个位,然后让12234/10,得到123,再让123%10得到3,最后依次类推,直到得到1(一位数)停止,那么这就是递归的限制条件。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void print(int n)
{
if (n < 9)//代表一位数的时候
{
printf("%d ", n);
}
else
{
print(n / 10);
printf("%d ", n % 10);
}
}
int main()
{
int input = 0;
scanf("%d", &input);
print(input);
return 0;
}
为了表达更加清楚,下面用图解的方式进行说明:
递推用红线,同时用红色数字标注n的值的变化,回归的时候用黄线。
如图,当n==1,即n<9时,函数停止调用,开始回归,此时n==1,进入if语句,打印在屏幕上一个1,
此时再次回归,执行printf语句,打印再屏幕上一个2,以此类推,最后回归到第一个调用print函数的main函数里面,最后return 0;程序执行结束,1 2 3 4输出再屏幕上面。
在分享完相关递归的思想后,下面步入正题:
青蛙跳台问题:
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求出该青蛙跳上一个n级台阶总共有多少种跳法?
其实这个题可以用函数递归来求解,过程类似于斐波那契数列定义,当然也能用动态规划求解,动态规划的方法显得效率更高,避免了冗余的计算。
下面,我们先用递归的方式进行求解。
当台阶只有1,即n==1,只有1种跳法。
当台阶为2,即n==2,则有2种跳法(一阶一阶的跳,或者一次跳两阶)
当台阶为3时,即n==3,则有3种跳法(一阶一阶的跳,或者先跳一阶,再跳两阶,又或者先跳两阶,在跳一阶)。
看到这里,我们可能会认为规律是:青蛙跳台阶的种数与阶数一样,其实不然,让子弹再飞一会。
当台阶为4时,即n==4,则有5种跳法(1.一阶一阶的跳,2.先跳两个一阶,最后跳一个两阶,3.先跳一个一阶,接着跳一个两阶,再最后跳一阶,4.两阶两阶的跳,5.先跳两阶,再一阶一阶的跳)
此时我们可以发现,青蛙跳台的种数==前面两次跳台的种数的和,不信,我们继续点一下不难发现,当n==5时,总数为8种,于是乎我们找到了这种规律,其实不难发现,这种规律跟斐波那契数列定义是一样的,那我们就定义一个函数fib来完成青蛙跳台需要的程序需求。那么我们开始接下来的代码实现:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
int fib(int n)
{
assert(n > 0);
if (n<=3)
{
return n;
}
else
{
return fib(n - 1) + fib(n - 2);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fib(n);
printf("%d", ret);
return 0;
}
运行结果如下:
,我们发现当我们输入较大数值时,程序所用时间明显变长,这是因为其实很多个数字都被重复计算了,显然这种用斐波那契数列定义求解的方式效率比较低,其实这里可以试着用动态规划来求解,算法效率明显高于斐波那契数列,因为这里我们着重讲解递归,所以关于动态规划求解就不过多赘述。
汉诺塔问题
原题大概描述:
假设共三根柱子A,B,C,所有盘子初始位置在A柱子
1.一次只能移动一个盘子
2.大盘需要放在下面,小盘在上面(禁止大盘叠小盘)
3.最终所有的盘子都在c柱子上。
求最小的移动次数,并且将移动的过程输出打印在屏幕上。
首先假设A,B,C三根柱子,其中A为起始柱子,B为中转柱子,C为目标柱子
若只有一个盘子,即n==1,则需要移动1次(A-->C)
若有两个盘子,即n==2,则需要移动3次(A-->B,A-->C,B-->C)
若又三个盘子,即n==3,则需要移动7次(A-->C, A-->B, C-->B, A-->C, B-->A, B-->C, A-->C)
从中我们可以猜测移动的种数应该是2^n-1。当有4个盘子时,则移动的次数为15种,当盘子数逐渐增加,很显然我们不能一个一个去数,那么我们该怎么办呢?
其实我们可以把上面的(n-1)看成一个盘子,但是我们可能会疑惑,题目不是规定一次只能移动一个盘子吗?
其实是这样理解的,假设有三个盘子,我们把第三个盘子头上的两个盘子当成一个整体,现在其实就相当于是两个盘子在移动,按照我们刚刚在上面写的,从A借助B,移到C(这是总的移动路线,当盘子数较多时,中间其实还有其他移动路径)。
现在进行拆分,移动这3个盘子,我们就需要移动头上这2个盘子,从A借助C移动到B(因为第二个盘子头上还有一个盘子,必须先把第一个盘子移开,才能移动第二个盘子。)我们惊奇的发现,当我们移完头上两个盘子时,下面的移动路径又变成了从B借助A,移到C(移动第三个盘子)。
多个盘子时,我们需要借助中转塔来进行移动。
三个盘子按照这样的逻辑移动,以次类推,四个盘子,则需要移动头上三个盘子,移动头上三个盘子,按照前面移动三个盘子的思路。第n个盘子这样移动,第n-1也能这样移动。并且移动路径最后都会回到这两条移动路径上面。以此递归。这里值得注意的是,pos1,2,3位置始终在变化,比如第一个路径B为中转塔,但在第二个路径下就变成了起始塔。
下面我们开始代码的实现,
定义一个Hanoi函数完成移动的逻辑,
定义一个move函数,打印移动的过程。
//n代表盘子个数
//pos1起始位置
//pos2中转位置
//pos3目标位置
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void move(char pos1, char pos2)
{
printf(" %c->%c ", pos1, pos2);
}
void Hanoi(int n, char pos1, char pos2, char pos3)
{
if (n == 1)
{
move(pos1, pos3);
}
else
{
Hanoi(n - 1, pos1, pos3, pos2);//从A借助C移动到B
move(pos1, pos3);//然后把底下那个大盘移动到C
Hanoi(n - 1, pos2, pos1, pos3);//最后从B借助A,移到C
}
}
int main()
{
int n = 0;
scanf("%d", &n);//移动的盘子数
Hanoi(n,'A','B','C');
return 0;
}
在这里用了两个递归,那么这两个递归是如何进行移动,传递,回归的呢?
其实我们调试,输入想要调试变量的名字F10开始调试,F11进入函数内部:
这里我重新自定义了一下count1,2,3。目的就是为了更好观察程序执行的步骤,从而推断两个递归实现的逻辑。
这里我用的编译环境是vs2022。
经过一番调试观察得到,其实两个递归其实依旧是从上到下一次执行。第一次调用Hanoi函数时,进入函数内部,n的值随之改变,继续从上到下一次执行,当n的值变为1时,第一个Hanoi函数回归,执行下面的程序,即count1++,count2++。当遇到第二个Hanoi函数时,继续调用,进入第二个Hanoi函数内部,从上到下执行。只有在函数回归时,才跳回上一个Hanoi函数,执行下面程序,count3++。以此类推。中间过程较为复杂。值得注意的是每次递推与回归需要注意n值的变化,这个很关键。
这篇博客就介绍到这里。