目录
ACM金牌带你零基础直达C语言精通-课程资料
一.作用域的基本概念
二.函数
1. 函数的定义和使用
2.为什么一定要有函数结构
3.形参与实参
4.函数的声明和定义
5.递归函数
此代码中递归函数执行流程:
练习:求斐波那契数列第n项的值:
欧几里得算法
(附加内容)扩展欧几里得算法
递归回溯练习:
6.变参函数
ACM金牌带你零基础直达C语言精通-课程资料
本笔记属于船说系列课程之一,课程链接:ACM金牌带你零基础直达C语言精通https://www.bilibili.com/cheese/play/ep159068?csource=private_space_class_null&spm_id_from=333.999.0.0
你也可以选择购买『船说系列课程-年度会员』产品『船票』,畅享一年内无限制学习已上线的所有船说系列课程:船票购买入口https://www.bilibili.com/cheese/pages/packageCourseDetail?productId=598
做题网站OJ:HZOJ - Online Judge
Leetcode :力扣 (LeetCode) 全球极客挚爱的技术成长平台
一.作用域的基本概念
作用域:定义的变量,作用的区域
代码演示:
#include<stdio.h> int main() { //a和b定义在main函数这个{}这个作用域中 //也就是在main函数中的任意位置都可以用a和b的变量 int a = 1, b = 2; for (int i = 0; i < 3; i++) { //c和d这两个变量在for语句后的{}这个作用域中 //想用或者访问c,d变量只能在这个作用域中进行使用或访问 int c = 1, d = 2; //因为for语句后在main函数这个作用域中 //所以for语句中也可以访问a,b变量 printf("a = %d, b = %d\n", a, b); //因为在for语句中的作用域,所以也可以访问c,d变量 printf("c = %d, d = %d\n", c, d); } //因为在main函数这个作用域中所以可以访问a, b变量 printf("a = %d, b = %d\n", a, b); //下面这段代码就不行,因为c,d变量没有在这个作用域中 //所以无法访问c,d变量 //printf("c = %d, d = %d\n", c, d); return 0; }
执行结果:
代码演示2:
#include<stdio.h> int main() { //这里a,b变量存储在外部作用域 int a = 1, b = 2; for (int i = 0; i < 3; i++) { //这里的a,b变量存储在内部作用域 int a = 3, b = 4; //如果内外部都有相同名的变量,内部访问都访问自己的变量 printf("in for: a = %d, b = %d\n", a, b); } //外部访问外部的变量,是不可访问到内部变量的 printf("outside : a = %d, b = %d\n", a, b) ; return 0; }
执行结果:
二.函数
1. 函数的定义和使用
代码演示:
#include <stdio.h> #include <math.h> //返回值类型 函数名 (参数列表) int sum(int a, int b) { //函数体 return a + b;//函数里必须有的return,进行对函数值的返回 } //定义一个函数,有两个参数,一个参数为flag,一个为x //当flag = 1,返回根号下x //当flag = 2,返回x * x //现在就需要去构思,函数名可以随便,但是必须是由字母数字和_组成,并且只能由字母和_开头 //思考返回值类型是什么,由于返回值有根号下x,所以返回值类型为double //函数名就随便起,满足上面的条件 //然后是参数,x和flag,类型都为int //最后构建函数体 double function_1(int flag, int x) { //这里用到数学函数,注意不要漏掉头文件 if (flag == 1) return sqrt(x); else if (flag == 2) return x * x; //这里需要考虑到flag不等于1,2,所以要else 返回一个0 else return 0; } //设计一个函数,函数的作用就是传入一个n,然后打印n次hello world //设计思路就是function_1中的思路 //但是这里的返回值是没用的,所以这里引用了void,表示没有返回值 void printf_hello_world(int n) { for (int i = 0; i < n; i++) { printf("hello world\n"); } return ;//不返回任何东西,表示函数结束 } int main() { //函数使用,直接用函数名(),()里需要填 //你定义的函数参数列表的类型,以及对应的参数个数 printf("3 + 4 = %d\n", sum(3, 4));//这里函数使用它返回的值就是3 + 4的值7 int flag, x; printf("intput flag and x\n"); scanf("%d%d", &flag, &x); //注意顺序不能乱写 //由于返回值类型是double所以格式占位符要用%lf printf("function_1 = %lf\n", function_1(flag, x)); int n; printf("input n\n"); scanf("%d", &n); printf_hello_world(n); return 0; }
代码执行结果:
2.为什么一定要有函数结构
代码演示:
#include <stdio.h> //start范围开始,end为范围结束,d为公差 int function_sum(int start, int end, int d) { int n = (end - start) / d + 1;//求项数 int num = start + (n - 1) * d;//求末项 int sum = (start + num) * n / 2;//求和 return sum; } int main() { //求等差数列的和 //求1-100,公差为1的和 //等差数列求和公式 sum = (首项 + 末项) * 项数 / 2; int sum1 = (1 + 100) * 100 / 2; //求1-100,公差为2的和 int n2 = (100 - 1) / 2 + 1;//求项数 int num2 = 1 + (n2 - 1) * 2;//求末项 int sum2 = (1 + num2) * n2 / 2;//求和 //求30-34534,公差为17的和 int n3 = (34534 - 30) / 17 + 1;//求项数 int num3 = 30 + (n3 - 1) * 17;//求末项 int sum3 = (30 + num3) * n3 / 2;//求和 printf("%d %d %d\n", sum1, sum2, sum3); //通过发现,我每次要去求等差数列的和都需要用3行代码去实现,特别繁琐,重复的代码也多; //现在就可以去用函数去封装起来,直接调用函数来实现这个过程 //现在掉调用函数来求等差数列的和 printf("%d ", function_sum(1, 100, 1)); printf("%d ", function_sum(1, 100, 2)); printf("%d\n", function_sum(30, 34534, 17)); return 0; }
运行结果:
通过代码发现,函数可以减少重复的代码,简洁了主函数中的代码,然主函数的代码更直观。
3.形参与实参
代码演示:
#include <stdio.h> void function_A(int a, int b) {//函数定义中()a,b是形参 a = a + 1; b = b * 2; printf("in function_A : a = %d, b = %d\n", a, b); return ; } int main() { int a = 1, b = 2; function_A(a, b);//调用函数()中a,b是实参 printf("in main : a = %d, b = %d\n", a, b); return 0; }
运行结果:
可以发现形参的改变是不影响实参的值,实参只是在给形参进行赋值
4.函数的声明和定义
#include<stdio.h> //如果没有这一行,那么在编译的过程中会报错 //因为程序的执行流程是从上到下的 int func_b(int x); int func_a(int x) { //如果没有上面的声明,那么编译器就会不知道有func_b这个函数的存在 //就会发生编译错误 //所以这就是声明的作用 if (x == 1) return func_b(x); printf("func_a : x * 2 = %d\n", x * 2); return 0; } int func_b(int x) { if (x == 2) return func_a(x); printf("func_b : x * 3 = %d\n", x * 3); return 0; } int main() { //该程序实现的是 //func_a函数传入的x为1就去调用func_b //如果不为1就打印x * 2的值 //func_b函数传入的x位2就去调用func_a //如果不为2就打印x * 3的值 func_a(2); func_a(1); return 0; }
代码执行结果:
函数的定义就是展现函数的实现过程,然后一定对于上面的代码去理解声明的作用;
5.递归函数
代码演示:
#include<stdio.h> int func(int n) { if (n == 1) return 1;//递归出口,防止无限调用本身,造成死循环 return n * func(n - 1); } int main() { //求n的阶层 int n; while (scanf("%d", &n) != EOF) {//循环读入,ctrl + c结束,linux系统下 printf("!%d = %d\n", n, func(n)); } return 0; }
执行结果:
此代码中递归函数执行流程:
通过流程图就可以清晰的理解,递归的过程。
练习:求斐波那契数列第n项的值:
斐波那契数列是指这样一个数列:1,1,2,3,5,8,13,21,34,55,89……这个数列从第3项开始 ,每一项都等于前两项之和。
代码实现:
#include <stdio.h> //对于func函数的理解 //func(n)返回的就是第n项的值 //比如func(1) = 1,第一项就等于1 int func(int n) { //当n为第一第二项时都为1,成为递归出口 if (n == 1 || n == 2) return 1; //由于斐波那契数列,等于前两项的和 //所以func(n) = func(n - 1) + func(n - 2); //func(n - 1) 和 func(n - 2) 分别代表前第一项和前第二项 //比如n = 5 //那么func(5) = func(4) + func(3); return func(n - 1) + func(n - 2); } int main() { int n; scanf("%d", &n); printf("func(%d) = %d\n", n, func(n)); return 0; }
执行结果:
如何去理解这个递归函数,当求第n(n > 2)项时,进入函数,然后返回func(n - 1) + func(n - 2)的值,然后再次调用函数,直到到达递归出口,进行一层一层的回溯,直到最开始调用函数func(n - 1) + func(n - 2),然后最后返回得到func(n)的值。对于这个过程可以通过自己画图,这样理解会更加深刻。
欧几里得算法
整数a,b的最大公约数一般表示为gcd(a,b);
什么是最大公约数,就是a和b同时能整除最大的数字, 比如32和24的最大公约数是8;
最终式子:gcd(a,b) = gcd(b, a % b)
解释:gcd(a,b)返回值就是a和b的最大公约数
现在需要证明: b 和 a % b的最大公约数是 a 和 b 的公约数。
先证明b 和 a % b的最大公约数也是a 和 b 的公约数
证明1:
假设 gcd(b, a % b) = c c是b和a % b 的最大公约数 设 b = x * c 因为c为b的公约数,所以设b等于x倍c 设 a - k * b = y * c 这个式子的推到过程 a % b是求 a / b 的余数 a / b的余数 + k倍b = a 所以 a % b = a - k * b y * c就是y倍他们的最大公约数c 现在得到两个式子 b = x * c a = k * b + y * c 将上面的式子带入到下面式子 a = k * x * c + y * c a = c * (k * x + y) 最终得到 b = x * c a = (k * x + y) * c 可以证明a和b也有c这个公约数证明了b 和 a % b的最大公约数也是a 和 b 的公约数
现在来证明b和a % b的最大公约数也是a 和 b 的最大公约数
证明2:
根据上面的推理得到 b = x * c a = (k * x + y) * c 如何去证明b和a % b的最大公约数也是a 和 b 的最大公约数 通过证明1开始的设 b = x * c a - k * b = y * c 可以的得到 gcd(x, y) = 1 因为b 和 a % b的最大公约数就是c 所以x和y是互斥的,他们的最大公约数就是1 所以只需要证明 x 和 (k * x + y)是互斥的,也就是他们的最大公约数为1,那么c就是a和b的最大公约数 反证法: gcd(x, k * x + y) = d 设 x = n * d 设 k * x + y = m * d 那么 y = m * d - k * x 带入x = n * d y = d * (m - k * n) 因为gcd(x, y) = 1 那么d只能为1,如果d为2或者其他数gcd(x, y)是不可能等于1的 最终证明 b和a % b的最大公约数也是a 和 b 的最大公约数通过证明1 和 证明2 让式子 gcd(a, b) = gcd(b, a % b)是成立的
下面来代码实现:
#include<stdio.h> //为什么递归出口是b == 0 //gcd(a, b) = gcd(b, a % b); //加入a = 12, b = 8 //通过递归一下 //gcd(12, 8) = gcd(8, 12 % 8); //gcd(8, 4) = gcd(4, 8 % 4); //gcd(4, 0) = a; //最终会发现当gcd函数中形参b等于0时 //现在返回的a值的就是gcd(a, b)最大公约数 //上面的两个a是不同的,一个是递归到最后的形参a,一个是最开始带入的形参a //所以最后证明了递归出口就是b == 0 int gcd_1(int a, int b) { if (b == 0) return a; return gcd_1(b, a % b); } //这种实现方式和gcd_1的效果一样 //只是程序写出来的方法不一样 int gcd_2(int a, int b) { //这里用到了?表达式, //当?前的判断语句成立时,就返回:前的语句 //判断语句不成立时,就返回:后的语句 //这里只有变量b,就用到非0及为真,为0就为假,刚好满足判断条件 return b ? gcd_2(b, a % b) : a; } int main() { int a, b; scanf("%d%d", &a, &b); printf("gcd_1(%d, %d) = %d\n", a, b, gcd_1(a, b)); printf("gcd_2(%d, %d) = %d\n", a, b, gcd_1(a, b)); return 0; }
执行结果:
(附加内容)扩展欧几里得算法
贝祖等式:
a和b都为整数,并且存在一组整数解x和y
利用贝祖等式推到过程:
需要证明的是,如何通过有了位置①x和y如何去得到位置②的x和y;
因为有最后一项,需要从后往前推,需要位置①去得到位置②;现在位置①换为最后一项,那么位置②就变为倒数第二项,有最后一项的x和y就可以得到倒数第二项的x和y,最后一直往前推,那么就可以得到最开始的x和y值;
证明: 位置① 因为 a % b = a - k * b 所以可以得到等式 b * x + (a % b) * y = c b * x + (a - k * b) * y = c b * x + a * y - k * b * y = c b * (x - k * y) + a * y = c 所以通过贝祖等式可以得到位置②对应的x和y _x = y, _y = x - k * y;现在得到了如何求前一项的x和y
等式: _x = y, _y = x - k * y;
现在通过代码实现来得到最开始的x和y,也就是gcd(a,b)最开始的系数x和y
#include<stdio.h> //这里用到了全局变量,只要在这句代码定义下面的函数,或者位置都可以使用该变量 //只要那个位置改变了,这个变量就会改变 //最后的值就是最后改变后的值 int x, y, _x, _y;//分别代表后一项的x和y和当前项的x和y int gcd_up(int a, int b) { if (b == 0) { x = 1, y = 0; return a; } int c = gcd_up(b, a % b); //通过证明出的等式 //将当前项的_x赋值后一项的y _x = y; //求到系数k int k = a / b; //将当前项的_y通过式子求到 _y = x - k * y; //最后再将后一项的x和y赋值成为当前项的x和y //因为在下次回溯中,当前项就变为前一项了 x = _x; y = _y; return c; } int main() { int a, b; scanf("%d%d", &a, &b); int c = gcd_up(a, b); printf("x = %d, y = %d, gcd(%d, %d) = %d\n", x, y, a, b, c); return 0; }
执行结果:
下面是思维导图:
这份图是代码的执行流程,在以后熟练后,这种图会自然的在自己的脑子中显现出来:
递归回溯练习:
对于该练习可能会比较难,可以在学习数组后在来尝试,对于这个题目理解完成后,基本对于递归和回溯就有了基本理解。
海贼oj235
代码实现:
#include<stdio.h> int n; int num[15]; int s = 0; int func(int x) { if (x > n) return 0;//递归结束出口 for (int i = x; i <= n; i++) { num[s++] = i; for (int j = 0; j < s; j++) { j && printf(" "); printf("%d", num[j]); } printf("\n"); func(i + 1); //回溯过程 s--; } return 0; } int main() { scanf("%d", &n); func(1); return 0; }
提交结果:
执行效果:
6.变参函数
下面实现一个变参函数,求参数中最大值;
#include <stdio.h> #include <stdarg.h> #include <stdint.h> //此位置的参数n表示后面的变参参数个数 int max_int(int n,...) { va_list args;//args表示变参列表 va_start(args, n);//这段代码表示,变参参数从参数n后面开始 int ans = INT32_MIN;//这里用到INT32_MIN,需要包含头文件<stdint.h> for (int i = 0; i < n; i++) { //开始从第一个位置获取变参参数 //并且为int类型 //这里获取后下次获取就是下一个变参参数 int a = va_arg(args, int); //判断当前参数的值是否大于之前的最大值 //如果大于就记录当前值为最大值 if (a > ans) ans = a; } return ans; } int main() { printf("max_int(%d, %d, %d, %d) = %d\n", 2, 4, 6, 1, max_int(4, 2, 4, 6, 1)); printf("max_int(%d, %d, %d, %d, %d) = %d\n", 10, 4, 6, 1, 88, max_int(5, 10, 4, 6, 1, 88)); return 0; }
执行结果:
测试可以不用和我的一样
7.主函数参数
通过代码来解释一下每个参数是什么:
#include<stdio.h> int main(int argc, char *argv[]) { printf("argc = %d\n", argc); //这个循环是打印argv每个参数,如果不能理解数组,就先理解实现的作用 for (int i = 0; i < argc; i++) { printf("argv[%d] = %s\n", i, argv[i]); } return 0; }
执行结果:
这里输入内容(Lskk@God 3.day %后面)的地方就叫命令行
通过执行发现,argc就是来获取命令行参数的个数,argv就是获取每个参数的名字
对于**env的内容在后续学习指针的位置会补上;
本章小结:
本章学习了函数,知道了什么是递归函数,也知道了变参函数,以及主函数也是有参数的。对于函数的学习在后面的写程序的过程中,希望把功能都封装成为一个函数,而主函数里只有调用函数的逻辑,而不是主函数中堆满的功能的实现。这样还可以加强对于函数的理解。做一做练习题,加强对函数的映像。
最后加油,看到这里你已经超过百分之95的人了。