用函数递归解决青蛙跳台与汉诺塔问题(c语言)

一.青蛙跳台问题

二.汉诺塔问题

在对这些问题探讨的时候,我们需要了解递归的相关概念,以及对递归思想的进一步理解。

说的简单通俗一点,递归就是函数自己调用自己,如下这段代码:

#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值的变化,这个很关键。

这篇博客就介绍到这里。

  • 19
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值