汉诺塔/青蛙跳台:库函数/自定义/递归函数
介于来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. 函数的声明和定义
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. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。总之,具体情况具体分析。
- 到这里,函数就介绍完啦,递归例题得好好研究才行,学习去,冲!!!