Part One 感悟
今天学的内容应该是劝退大部分初学者的递归和迭代。同时学完了这一章,一个真正的程序员才开始慢慢地走上编程的正轨。
在学习了函数之后,愈发的觉得离开了函数,程序的编写就就变得异常的困难。同时在学习了递归和迭代之后,可以解决的问题种类变得更发的多,代码也变得愈发的简洁。
但是还是要警惕思维的怠惰之处。虽然递归和迭代的方法拓宽了程序员的眼界,提高了他们的能力,但是真正优秀的代码就对不是只依靠这两种方法编写出来的。依据业务需求编写代码,永远是最为优秀的方法。
Part Two knowledge
在函数(1)中我们讲解了库函数和自定义函数,并且介绍了与之相关的知识,接下来我们将介绍函数的声明,定义,递归和迭代。其中递归和迭代为本篇的重点。本篇附赠彩蛋。
2.1 嵌套调用&链式访问
2.1.1 嵌套调用
我们在函数(1)中已经了解了函数调用的方式,那么能不能在自定义的函数中调用其他的自定义函数呢?
答案是肯定的,以下面插入的代码为例。
#include <stdio.h>
void new_line()
{
printf("hehe\n");
}
void three_line()
{
int i = 0;
for(i=0; i<3; i++)
{
new_line();
}
}
int main()
{
three_line();
return 0;
}
这个代码就实现了函数的嵌套调用,执行结果是在屏幕上打印三个“hehe”。
但是嵌套调用有一个点值得注意,函数虽然可以嵌套调用,但是不可以嵌套定义,即在自定义函数A中去定义自定义函数B。
2.1.2 链式访问
链式访问指的是把一个函数的返回值作为另外一个函数的参数。
以下面的代码举例
可以观察到,ret是strlen函数的返回值,同时也是main函数的一个参数,这样就实现了链式访问。
2.2 函数的声明以及定义
2.2.1 函数的声明
当我们在编写自定义函数的时候,似乎总是在主函数的前后写上类似于“void Print(int x)”之类的代码,事实上,这一行简单的代码就是我们讲的函数的声明。
虽然这一串代码看着非常简单,事实上程序员只需要看到这个函数的声明就能对于这个函数的使用有着一个很清晰的认识。前面的返回类型告诉了程序员这个自定义函数是否有返回值,返回值的类型是什么(如果忘记写函数的返回类型,默认返回int);中间的函数名告诉了程序员这个函数能够实现的大概功能是什么(当然函数名称得有意义,不然你写个114514别人不仅不知道你这个函数想表达什么意思,同时还会觉得这个代码很臭罢);之后的形参内容告诉了传入的参数的类型是什么。所以这个函数的声明所包含的信息还是非常多的。
同时,C语言的标准也规定了(不代表所有的标准),函数必须先声明才能够使用,不然程序便会报出“无法解析的外部指令”的错误。当然,声明的位置可以在主函数的前面,也可以是在主函数的后面,因为当执行到自定义函数的时候,编译器会自上而下的扫描整个程序来查找是否有函数的声明,所以位置根据实际的需要进行安排即可。
函数的声明一般放在头文件中
2.2.2 函数的定义
函数的定义不同于函数的声明,函数的定义是指函数的具体实现,交待函数的功能实现。
例如这个非常简单的Add函数,最上方的便是函数的声明,而大括号的里面的内容便是函数的定义。换句更通俗易懂的话来说,函数的定义就是函数体。
2.3递归(重点)
什么是递归呢,很简单,一个函数调用他自己的编程技巧便是递归。
但是这未免也太抽象了吧。让我来细细的讲解一下递归。我们用一个实际问题来讲解一下。
假如,我现在想写一个代码,来实现将我输入的整数上的每一位数分别打印出来,并且使用自定义函数的方式完成。例如我输入“1234”,最后打印出来的结果应该是“1 2 3 4”
说实话,大部分人拿到这个要求的时候估计都没有办法下手,至少我一开始脑子一片空白,完全不知道怎么写,但是我们把问题进行一个细细的分析,就能思考出问题的解决步骤。
首先我们把这个问题简化一下,例如1234我想输出1 2 3 4,那么我先打印123的每一位,最后在打印一个4不就行了。我用1234除以10,这样4就被我们拿到了。现在我们貌似就得到了灵感,那么我现在想打印123的每一位,我先打印12的每一位,最后加上3不就可以了嘛。以此往复,最后我现在只用打印1这一位数,再加上2 3 4,这个问题不就解决了吗?这样的思考下,我们就不知不觉的利用了递归的技巧。
那么为什么会这样思考呢,因为很简单,在1234这个数字中,永远是最末尾的数最容易拿到,他最好打印,所以我们就通过不断地除,就能不断地获得1234的每一位数字,最后等到拆到最前面,在按照我们想要的顺序打印就可以了。
递归它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,
递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
我们再将递归完整的结合上述的问题来看。“将输入的整数上的每一位数分别打印出来,并且使用自定义函数的方式完成。”这个问题大并且复杂,然后我通过拆分问题,将原先复杂的问题变成了与之类似简单问题。最后我们不断的重复解决简单问题的操作,我们最终便将复杂的问题解决了。这样便是递归!
那么我们现在来实现一下这个代码。
按照我们想的思路,如果我要打印1234的每一位,我们就先打印123的每一位,在打印4......
void Print(int n) {
if (n > 9) {
Print(n / 10);
} //假如n是k位数,那么就先打印他的前k-1位。
printf("%d\n", (n % 10)); //前面的打印完了,在打印第k位。
}
int main() {
int n = 0;
scanf("%d", &n);
Print(n);
return 0;
}
这个代码我们可以看出来,这里的Print函数就是我们用于解决问题的函数。
接下来让我们用图来解释这个代码是怎么运行的。
当函数进行到主函数的时候,函数第一次执行Print,此时的n=1234;
因为n>9,所以执行第二次Print,第二次执行的n=123;
又因为n>9,所以第三次执行Print,第三次执行的n=12;
又因为n>9,所以第四次执行Print,此时的n=1,函数结束递归,这个时候打印1。由于第四次Print执行结束,回到第三次执行Print的地方,正是因为第三次递归引发了第四次Print函数的执行,此时第四次Print函数的执行完毕代表着第三次Print函数递归也执行完毕,此时执行if后面的语句,打印2,函数回到第二次执行Print的地方。由于第三次Print函数递归执行完毕,第二次Print函数执行if后面的语句,打印3,最后回到第一次执行Print函数的地方,此时打印4,这样的函数结束,问题解决。
这样的递归,可以懂吗?
这样来看,递归真的是个好东西,但是递归也不是万能的,首先,在写递归的时候就有着诸多的限制,例如必须存在着限制条件(例如上述代码中的if语句),每一次递归执行,函数就愈发的靠近这个条件,不然就会陷入死递归,函数不会有结果。
其次,递归在某些问题的解决上并非是一个好的方法。假如我们利用递归来解决斐波那契数的求解,当我们算到第50个数字的时候程序就会卡死,(因为产生了大量的重复数据),那么我们如何来赶紧代码呢?
迭代就是一个很好的方法!
2.4 迭代
迭代是循环的一种。看看这个代码就是在重复着加法的操作。
斐波那契数前两位是固定的,所以你输入的数如果是1或者2,返回1就可以了。
但是如果你输入的是5,那么c就是我们需要返回的结果,a和b就是c前面的两个数,当每次n-1,a、b就会往后移动一位,c的值自然就会更新。
int Fib (n) {
int a = 1;
int b = 1;
int c = 1;
while (n >= 3) {
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main() {
int n = 0;
scanf("%d", &n);
printf("%d\n", Fib(n));
return 0;
}
大概就像这样
彩蛋单独更新!