目录
一、模块化设计思想和方法
模块化设计方法:自顶向下设计,自下向上编程实现的设计方法。
学生成绩管理系统,若所有功能都在主函数中实现,程序的可读性、可修改性都很差,因此我们每个功能分别用一个函数来实现,主函数方便快捷的调用这些函数即可(模块化)。
先分析问题——每个问题都是由若干个子问题构成——子问题也可能由若干个小问题构成——细分下去,直至每个小问题独立并且足够小——编程实现每个小问题——将小问题组合(自顶向下设计,自下向上编程实现)
模块化的优点:复用。(1. 函数+函数构成程序。 2. 成熟软构件+修订=新软件。 3. 成熟软构件1+成熟软构件2+…=新软件系统(复用性最佳))
二、几个小例子
例1:以身份证号为参数比较年龄大小。
这是我写的程序:
问题在于:
(1)身份证号不仅仅只有数值,有时候会有字母,因此应用字符数组串表示。
(2)scanf_s获取字符串时,应该指明字符串长度,否则可能会出错!
(3)主函数中调用函数,不应该是int 函数名(),而是返回值=函数名()
正确的程序如下:
/* 以身份证号为参数,比较年龄大小 */ #include<stdio.h> #include<stdlib.h> #define IDLEN 19 // 定义一个长度:身份证号码长为18,加上结束符共19个 int AgeCompare(char *p1,char *p2); // 函数声明有分号! int main() { char id1[IDLEN],id2[IDLEN]; // 长度为18+1 int r; // 存储函数返回值 printf("请输入两个18位的身份证号:\n"); scanf_s("%s%s",id1,IDLEN,id2,IDLEN); r=AgeCompare(id1,id2); printf("比较结果是:%d\n",r); system("pause"); return 0; } int AgeCompare(char *p1,char *p2) // 函数定义没有分号! { int y1=0,y2=0,m1=0,m2=0,d1=0,d2=0; // 初始化!提取年、月、日 int i; // 计算年份 for(i=0;i<4;i++) { y1=y1*10+p1[6+i]-'0'; // 把字符串转化为数值,需要减去字符零'0'!!! y2=y2*10+p2[6+i]-'0'; // 使用前在等号右面使用了,因此使用前必须初始化! } // 计算月份 for(i=0;i<2;i++) { m1=m1*10+p1[10+i]-'0'; m2=m2*10+p2[10+i]-'0'; } // 计算天数 for(i=0;i<2;i++) { d1=d1*10+p1[12+i]-'0'; d2=d2*10+p2[12+i]-'0'; } // 比较大小 if(y1!=y2) { if(y1>y2) return 1; else return -1; } else { if(m1>m2) return 1; else if(m1<m2) return -1; else { if(d1>d2) return 1; else if(d1<d2) return -1; else return 0; } } }
下述错误原因:没有定义函数就调用。
进行调试时的技巧:
(1)加断点:看输入函数之前的数据是否正确;看参数传递是否正确;看函数输出数据是否正确。(因此断点加三处:进入函数前,进入函数后第一行,从函数中出来)
(2)用监视:看参数传递是否正确;看获取数据是否正确;看指针是否正确;看返回值是否正确。(监视加几处:y, m, d, id, p, r)
测试时的技巧:
为避免反复输入数据可能会造成错误,我们将scanf_s注释掉,直接输入数据,测试完成再取消注释。
例2. 拿球游戏。(根据这个例子,学习怎么对组合问题进行求解。)
分析:这是一个组合问题。
算法思想:
红球和白球必须有,因此红球i=1:3,白球j=1:5变化,求取8个球的情况下黑球数量k=8-i-j,用嵌套循环语句分别控制红球、白球的变化,求出黑球,并进行方案数量累加。
函数原型:(三个整型参数、三个变量指明拿球数、还要知道取了多少球,因此一共七个整形参数。)
int balls(int red, int white, int black, int minRed, int minWhite, int minBlack, int sumBalls);
函数说明:
输入:int red, int white, int black表示红白黑三种球的最大数量;int minRed, int minWhite, int minBlack表示拿三种球的最少数量;int sumBalls表示总共需要拿的球数量。
输出:总共方案数量。
功能:红球从minRed到red;白球从minWhite到white;黑球为sumBalls-当前拿的红球数-当前拿的白球数;如果这个黑球数量满足minBlack到black之间,则是一个合理地方案。
/* 拿球游戏 */ #include<stdio.h> #include<stdlib.h> int CountBalls(int red,int white,int black,int minRed,int minWhite,int minBlack,int sumBalls); int CountBalls(int red,int white,int black,int minRed,int minWhite,int minBlack,int sumBalls) // 函数原型后面不要有分号! { int i,j,k,result=0; for(i=minRed;i<=red;i++) { for(j=minWhite;j<=white;j++) { k=sumBalls-i-j; if(k>=minBlack && k<=black) { printf("%5d %5d %5d\n",i,j,k); ++result; } } } return result; } int main() { int r=3,w=5,b=6,sum=8,result; result=CountBalls(r,w,b,1,1,0,sum); printf("满足条件的方案数有:%d\n",result); // 结果没有输出的原因:刚开始没有加%d,那么他就不会输出数值结果 system("pause"); return 0; }
如图错误:
原因在于:没有加头文件!
例3. 报数游戏。
开始的时候要梳理思路,可先用小的数测试,例如n取4,看是哪位同学退出圈子,并且会造成什么变化。
需要两个变量:一个记录报号,一个是编号。用一个整数状态表示一个人是否在报数范围内,状态为1则报数,状态为0则不报数。
用n=4的例子来分析算法:
算法思想:
假设有n个人,用长度为n的一维数组data[n+1]存储这n个人,假设值为1表示在圆圈中,值为0表示出局。循环从1数到3的时候,第三个人出局,我们用i进行1到3的循环计数,m表示已经数到第几个人了,如果data[m]=0,则这个人不参与计数。另外,还需要统计剩余人数,我们可以用整数变量j表示,初始值为n。j是控制循环是否结束的。只要j>1,表示剩余人数不止一个,则循环下面操作:
(1)跳过data[m]=0
(2)i=3,则data[m]=0; i=1且m++,这时出局1人,j--,continue。
(3)i++,m++。
函数原型:int count(int n,int k);
输入:n为人数,k为间隔数,这里固定为k=3;
输出:出局后剩下的最后一人的编号;
功能:从1到k计数,第k个人出局,直至最后余下一人,返回这个人的编号。
/* 报数 */ #include<stdio.h> #include<stdlib.h> int count(int n,int k); int main() { int num,k=3; int s; scanf_s("%d",&num); printf("有%d个人参与报数\n",num); s=count(num,k); printf("余下人的编号为%d\n",s); system("pause"); return 0; } int count(int n,int k) { int *data=(int *)malloc((n+1)*sizeof(int)); // 定义data指针,通过malloc函数分配n+1个整数空间 int i,j,m; // i报数,j判断循环是否结束,m编号 if(data==NULL) // 如果空间分配失败,返回-1 { printf("malloc error!\n"); system("pause"); return -1; } for(i=1;i<n+1;i++) data[i]=1; // 如果分配成功,那么令这n个人状态都为1 j=n; i=1; m=1; while(j>1) // j>1循环未结束 { while(data[m]==0) // 当遇到状态为0的,报号i不变,编号+1 { ++m; m%=(n+1); if(m==0) ++m; } if(i==k) // 如果报数为3,那么状态置为0,并且报号重新从1开始,剩余人数-1 { data[m]=0; --j; i=1; } else // 没有报到3,报号+1 { ++i; } ++m; // 不管有没有报到3,编号都在+1,因此把它放在了外面 m%=(n+1); // 加这一步骤的目的是:m可能会进行好几轮,因此会有个轮回 // 过了第n个人之后,就又从第一个人开始计数 if(m==0) ++m; } free(data); // 写出malloc函数时,后面立马跟上free,再在中间插入其他的,以免忘记! return m; // 刚开始忘记写return返回值,因此输出的都是0,原因就是输出结果没有成功传递! }
调试时“监视”+“内存”一起使用。仅通过监视变量,看不到一块连续的内存空间,通过(调试——窗口——内存——复制想要看到的内存到里面——回车)!
例上图:当n设置为20时,这里有20个1。
课后作业:
我的疑问:
关于例3报数的游戏,运行结果好像不对,例如果有5个人,那么应该剩下第4个人,程序运行结果是3,找不到问题出在哪里。
例4. 逆序输出字符串。
要求:用两个函数实现。第一个函数不改变原字符串内容,第二个改变原字符串内容,实现逆序输出。
/* 逆序输出字符串 */ #include<stdio.h> #include<stdlib.h> #include<string.h> void reverse1(char *s,char *t); void reverse2(char *s); int main() { char s[]="Hello,everyone!"; char t[50]; reverse1(s,t); printf("Source string=%s\tReverse string=%s\n",s,t); reverse2(s); printf("Reverse string=%s\n",s); system("pause"); return 0; } void reverse1(char *s,char *t) // 没有改变原字符串内容 { int len=strlen(s); int i; for(i=0;i<len;++i) // 把原字符串的内容逆序赋给t数组 { t[len-1-i]=s[i]; } t[len]='\0'; // 结尾加结束字符串 } void reverse2(char *s) { int len=strlen(s); char *t=(char *)malloc(sizeof(char)*(len+1)); if(t==NULL) { return; } int i; for(i=0;i<len;++i) { t[len-1-i]=s[i]; // 把原字符串的内容逆序赋给t数组 } t[len]='\0'; for(i=0;i<len;++i) { s[i]=t[i]; // 然后再把t的内容给s(唉,这波操作图什么???) } free(t); }
我的疑问:
(1)数组和指针数组的区别?
(2)缺少了‘;’这个问题是什么原因?怎么解决?
例5. 汉诺塔游戏。(用这个例子了解递归问题)
我们要找规律,问题分析图:
解释:
第一步:将问题简化。假设只有两个圆盘。(将较小的暂存到C柱子上,将大的放到B,然后再将较小的放到B。)
第二步:对于有n个圆盘的汉诺塔,我们将圆盘分为两部分:最大的一块圆盘和其余的圆盘(即第n块圆盘和上面n-1个圆盘,上面n-1个圆盘看成一个整体)。
因此只需要两个函数、三个步骤即可实现:
(1)Hanoi(int n,char a,char b,char c); // n为圆盘数量,a, b, c是三个柱子名称
// 目的:将n个圆盘由a移到b,中间借助c柱子
(2)Move(int n,char a,char b);
step 1. Hanoi(n-1, a, c, b); // 将前n-1个圆盘由a移到c,中间借助b
step 2. Move(n, a, b); // 将第n个圆盘由a移到b
step 3. Hanoi(n-1, c, b ,a); // 把c上那n-1个圆盘放到b上,借助a
心得:Hanoi函数意义在于递归调用,不见得它能移动什么圆盘,真正起作用的是Move函数(即下面程序中的printf函数)!
(调试观察递归调用的过程)
(注意:若要移动n=64次,所需移动次数为1844亿亿次,我们看不到结果,因此我们用较小的数值进行测试)
/* 汉诺塔游戏 */ #include<stdio.h> #include<stdlib.h> void Hanoi(int n,char x,char y,char z); int main() { int h; printf("请输入圆盘数量:\n"); scanf_s("%d",&h); Hanoi(h,'a','b','c'); system("pause"); return 0; } void Hanoi(int n,char x,char y,char z) { if(n==1) // 使用递归函数时,一定要有递归结束条件!放在最前面! printf("%c->%c\n",x,y); // 只有一个圆盘,直接移动即可。 else { Hanoi(n-1,x,z,y); // 函数体内直接或间接调用自己本身,这种叫递归函数! printf("%c->%c\n",x,y); Hanoi(n-1,z,y,x); } }
递归函数
递归调用应该能够在有限次数内终止递归!若递归调用不加限制,将无限循环调用,因此必须在函数内部加控制语句,仅当满足一定条件时,递归终止,称为条件递归。
任何一个递归调用程序必须包括两部分:
(1)递归循环继续的过程
(2)递归调用结束的过程
/* 递归问题模型 */ if (递归终止条件成立) return 递归公式的初值; else return 递归函数调用返回的结果值;
递归与迭代
优点:直观、精炼、逻辑清楚、符合人的思维,逼近数学公式的表示,适合非数值计算领域(Hanoi塔、骑士游历、八皇后问题(回溯法))
缺点:增加了函数调用的开销,每次调用都需要进行参数传递,现场保护等耗费更多的时间和栈空间,应尽量用迭代形式替代递归形式。
补充知识点:(递归与迭代的区别)
迭代:循环结构,例如for,while循环
递归:选择结构,例如if else 调用自己,并在合适时机退出
看到的极易理解的解释:
(参考:递归与迭代)
(参考:递归与迭代 CSDN)
(参考:递归与迭代区别)