汉诺塔/青蛙跳台:库函数/自定义/递归函数

汉诺塔/青蛙跳台:库函数/自定义/递归函数

介于来CSDN都不简单,是大佬,今后我将会围绕本章重点以及注意事项,以及些衍生经典例题知识进行详细讲解。

1. 库函数&如何学

  • 函数分为库函数和自定义函数,对于库函数:C语言的编译器提供了一些库函数,比如我们最常用的printf,scanf。为什么会有这样的库函数呢,原因在于程序员在使用某些功能函数,比如打印,计算字符功能十分频繁,每个程序员都设计出自己的函数,这样每个人标志不一样,容易弄出bug。
  • 这时候C语言就规定了一些库函数,规定了语法标准,比如:函数名,功能,参数,返回类型等等,但具体实现是由编译器厂商来实现的,比如VS是微软,clang是苹果公司实现的。
  • 库函数的种类是非常丰富的,我们现在不必将每个都学会,但在未来使用时候应该知道如何去学习这些库函数,以:cplusplus网站为例讲解,注:使用库函数一定要记得包含它的头文件哦,就像借别人东西得和物主打个招呼。

在这里插入图片描述

2. 自定义函数

  • 自定义函数:程序员自己定义设计函数,功能全靠程序员自己整,给了我们很大发挥空间,很爽,但有些细节得说下。当实参传递给形参时:形参开辟一块新的空间,然后实现其功能,然后返回结果给实参函数,形参调用完后就销毁。
    在这里插入图片描述
  • 传值调用:当自定义函数调用的时候,实参传给形参时,形参将是实参的一份临时拷贝,所以对形参的修改是不影响实参的。传址调用:形参改变可以改变实参内容,原因是传递是地址,通过*解引用可以改变实参值。

2.1 嵌套调用和链式访问

  • 嵌套调用:函数可以嵌套调用但不可以嵌套定义,意思就是可以在函数里面使用别的函数,但不能在函数里面在定义新的函数。
例:
#include <stdio.h>
void new_line()
{
 printf("hehe\n");
}
void three_line()
{
    int i = 0;
 for(i=0; i<3; i++)
   {
        new_line();  /嵌套new_line函数
   }
}
int main()
{
 three_line(); /main函数嵌套three_line函数
 return 0;
}
  • 链式访问:把一个函数的返回值作为另外一个函数的参数,就是一个函数里面的参数是另外一个函数返回值。
  • strlen:计算字符串长度函数(只能计算字符串长度),放回的是\0之前的字符个数。strcat:增加字符串内容,第二个参数将从所加数组最后一个字符(\0)开始覆盖增加,返回值是所增数组的地址。
    在这里插入图片描述
例:
#include <stdio.h>
#include <string.h>
int main()
{
 char arr[20] = "hello";
 int ret = strlen(strcat(arr,"bit"));
 printf("%d\n", ret);
 return 0;
}   /结果为8

/注:printf函数的返回值是打印在屏幕上字符的个数
#include <stdio.h>
int main()
{
	/先打印,然后返回打印在屏幕上字符个数
    printf("%d", printf("%d", printf("%d", 43)));
    /结果是啥?
    return 0;
}   /结果:4321

3. 函数的声明和定义

  • . 函数声明:1.告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。3. 函数的声明一般要放在头文件中的
  • 函数定义:函数的定义是指函数声明的具体实现,交待函数的功能实现,一般在源文件中,在之前文章:三子棋纯C实现扫雷游戏纯C实现 有过很多的声明和定义,声明和定义在不同文件中。
test.h文件
放置函数的声明
#ifndef __TEST_H__
#define __TEST_H__
/函数的声明
int Add(int x, int y);

test.c文件
#include "test.h"
/函数Add的实现
int Add(int x, int y)
{
 return x+y;
}

4. 递归函数

  • 什么是递归:程序调用自身的编程技巧称为递归,是一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,只需少量的程序就可解决问题,大大地减少了程序的代码量。重点:大事化小。
  • 使用递归条件:需存在限制条件,即当满足某个限制条件的时候,递归便不再继续。每次递归调用之后越来越接近这个限制条件。
  • 例1:
问:编写函数不允许创建临时变量,求字符串的长度。
int my_strlen(char* arr)
{
	if (*arr == '\0')
	{
		return 0;
	}
	else
	{
		return my_strlen((arr+1) )+ 1;
	}
}
int main()
{
	char* arr = "abcdef";
	int len = my_strlen(arr);
	printf("%d ", len);
	return 0;
}

在这里插入图片描述

  • 例2:
用递归求斐波那契数
int fib(int n)
{
 if (n <= 2)         
 return 1;
    else
    return fib(n - 1) + fib(n - 2);
}
  • 使用递归法求斐波那契时我们发现,但n越大时,求出结果越慢,原因是在递归过程中存在大量的重复,这时候我们该怎么办呢?
    在这里插入图片描述
  • 解决方法:1.将递归改写成非递归。2. 使用static对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代nonstatic 局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保存递归调用的中间状态,并且可为各个调用层所访问(慢慢体会)。
/使用非递归方法实现求斐波那契
#include <stdio.h>
int fib(int n)
{
	int result;
	int right_result;
	int left_result;
	result = right_result = 1;
	while (n > 2)
	{
		n -= 1;
		left_result = right_result;
		right_result = result;
		result = right_result + left_result;
	}
	return result;
}
int main()
{
	int n = 40;  /计算速度远远大于递归时速度
	int isn = fib(n);
	printf("%d", isn);
	return 0;
}
/编译运行发现运行速度非常快,原因是递归时间复杂度为O(N^2),而这个非递归为O(N)

5.经典例题

5.1 青蛙跳台阶问题

问题引入:一只青蛙要跳台阶,它可以一次跳一级台阶,也可以跳上两级台阶。问:求该青蛙跳上一个n级台阶总共有多少种跳法。

在这里插入图片描述

  • 分析:跳一个台阶1种跳法,跳两个台阶可以跳2下1个台阶和1下2个台阶,共有两种跳法,跳三个台阶可以先跳1个台阶,然后继续跳剩下2个台阶,或者先跳2个台阶,在继续跳剩下1个台阶。我们发现剩下2个台阶和1个台阶跳法不就是之前跳两个台阶和一个台阶跳法么!同理,跳四个台阶就是跳三个台阶和跳两个台阶的和,而跳三个台阶又是跳两次台阶和跳一次台阶和…
  • 这样就很容易想到用递归方法来快速实现。
/代码展示
int jump(int n)
{
	int i = 0;
	if (n == 1)
	{
		return 1;  /1个台阶1}
	if (n == 2)
	{
		return 2; /2个台阶2}
	if (n > 2)
	{ 
	/3个台阶等于跳2个台阶和跳1个台阶的和,跳n次等于跳n-1个台阶和n-2个台阶和
		return jump(n - 1) + jump(n - 2);
	}
}
int main()
{
	int n = 5;
	int sum = jump(n);
	printf("%d ", sum);
	return 0;
}

5.2 汉诺塔问题

  • 问题引入:印度一个古老的传说,印度教的“创造之神”梵天创造世界时做了 3 根金刚石柱,其中的一根柱子上按照从小到大的顺序摞着 64 个黄金圆盘。梵天命令一个叫婆罗门的门徒将所有的圆盘移动到另一个柱子上,移动过程中必须遵守以下规则:
  • 1.每次只能移动柱子最顶端的一个圆盘;
  • 2.每个柱子上,小圆盘永远要位于大圆盘之上;

在这里插入图片描述

  • 思路分析:当我们移动一个或两个盘时,非常好分析,但当盘数目增多时,分析盘移动过程就变得相当困难,解决这样的问题可以尝试用递归算法,将移动多个圆盘的问题分解成多个移动少量圆盘的小问题,这些小问题很容易解决,从而可以找到整个问题的解决方案,当我们移动3个盘时,看成先将3-1个盘放到辅助柱,将下面遗留盘放到目标柱,最后将辅助柱盘子放到目标柱子。
  • 可以总结出一个规律:对于 n 个圆盘的汉诺塔问题,移动圆盘的过程是:
    1.将起始柱上的 n-1 个圆盘移动到辅助柱上;
    2.将起始柱上遗留的 1 个圆盘移动到目标柱上;
    3.将辅助柱上的所有圆盘移动到目标柱上
  • 由此,n 个圆盘的汉诺塔问题就简化成了 n-1 个圆盘的汉诺塔问题。按照同样的思路,n-1 个圆盘的汉诺塔问题还可以继续简化,直至简化为移动 3 个甚至更少圆盘的汉诺塔问题。
#include <stdio.h>
void hanoi(int num, char sou, char tar,char aux)
{
    /统计移动次数
    static int i = 1;
    /如果圆盘数量仅有 1 个,则直接从起始柱移动到目标柱
    if (num == 1)
    {
        printf("第%d次:从 %c 移动至 %c\n", i, sou, tar);
        i++;
    }
    else 
    {
        /递归调用 hanoi() 函数,将 num-1 个圆盘从起始柱移动到辅助柱上
        hanoi(num - 1, sou, aux, tar);
        /将起始柱上剩余的最后一个大圆盘移动到目标柱上
        printf("第%d次:从 %c 移动至 %c\n", i, sou, tar);
        i++;
        /递归调用 hanoi() 函数,将辅助柱上的 num-1 圆盘移动到目标柱上
        hanoi(num - 1, aux, tar, sou);
    }
}
int main()
{
    /以移动 3 个圆盘为例,起始柱、目标柱、辅助柱分别用 A、B、C 表示
    hanoi(3, 'A', 'B', 'C');
    return 0;
}

在这里插入图片描述

6.总结

  • 总结:1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。总之,具体情况具体分析。
  • 到这里,函数就介绍完啦,递归例题得好好研究才行,学习去,冲!!!
    在这里插入图片描述
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值