【20180927】【C/C++基础知识】程序的模块化设计,拿球游戏(组合问题),汉诺塔游戏(递归问题),报数游戏(断点+内存调试),递归与迭代

目录

一、模块化设计思想和方法

二、几个小例子


一、模块化设计思想和方法

模块化设计方法:自顶向下设计,自下向上编程实现的设计方法。

学生成绩管理系统,若所有功能都在主函数中实现,程序的可读性、可修改性都很差,因此我们每个功能分别用一个函数来实现,主函数方便快捷的调用这些函数即可(模块化)。

先分析问题——每个问题都是由若干个子问题构成——子问题也可能由若干个小问题构成——细分下去,直至每个小问题独立并且足够小——编程实现每个小问题——将小问题组合(自顶向下设计,自下向上编程实现)

模块化的优点:复用。(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

(参考:递归与迭代区别

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Satisfying

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值