参考教材是Robert Kruse的《数据结构与程序设计——C语言》,希望和一起学习的数据结构的同学共同交流。
第一章 编程原则
关键词:优秀编程方法遵循的原则,发现高效算法的方法。
1.1 引言
许多程序员都遵守这样的格言:首先使程序运行起来,让后再逐步把功能完善。对于小型程序,或许比较有效。但是在大型项目中,时间一久,就容易把程序结构忘记,怎么样把分工与其他部分的程序衔接起来也很困难。
并且,在大型项目中,困难通常不是找不到解决办法,而是方法很多,怎么找到最佳的方法和算法。另外,在算法设计中,数据存储的方式多样:存在哪里,内存或者硬盘;如何存,列表还是文本;哪些数据需要被用来计算.....
所以,需要一个标准来衡量数据存储和算法效率。另外,调试程序和维护程序也需要花费很多时间。
1.2 Life游戏
<生命游戏是一个零玩家游戏。它包括一个二维矩形世界,这个世界中的每个方格居住着一个活着的或死了的细胞。一个细胞在下一个时刻生死取决于相邻八个方格中活着的或死了的细胞的数量。如果相邻方格活着的细胞数量过多,这个细胞会因为资源匮乏而在下一个时刻死去;相反,如果周围活细胞过少,这个细胞会因太孤单而死去。实际中,玩家可以设定周围活细胞的数目怎样时才适宜该细胞的生存。如果这个数目设定过高,世界中的大部分细胞会因为找不到太多的活的邻居而死去,直到整个世界都没有生命;如果这个数目设定过低,世界中又会被生命充满而没有什么变化。
实际中,这个数目一般选取2或者3;这样整个生命世界才不至于太过荒凉或拥挤,而是一种动态的平衡。这样的话,游戏的规则就是:当一个方格周围有2或3个活细胞时,方格中的活细胞在下一个时刻继续存活;即使这个时刻方格中没有活细胞,在下一个时刻也会“诞生”活细胞。
在这个游戏中,还可以设定一些更加复杂的规则,例如当前方格的状况不仅由父一代决定,而且还考虑祖父一代的情况。玩家还可以作为这个世界的“上帝”,随意设定某个方格细胞的死活,以观察对世界的影响。>
该游戏的规则如下:
1. 如果一个细胞周围有3个细胞为生(一个细胞周围共有8个细胞),则该细胞为生(即该细胞若原先为死,则转为生,若原 先为生,则保持不变) 。
2. 如果一个细胞周围有2个细胞为生,则该细胞的生死状态保持不变;
3. 在其它情况下,该细胞为死(即该细胞若原先为生,则转为死,若原先为死,则保持不变)
我们来写一个程序,来演示初始原胞如何一代代传递。
1.2.1 解决方案
经过思考,很多人觉得先建立两个数组,一个存放初始状态,另一个存放下一个状态,同时计算出每一个格子周围的alive数量有0.1.2.3.4.5.6.7.8,如果为2,状态保持不变;如果为3,状态有dead变alive;其他都是dead。
算法步骤如下:
1、初始化名为map的矩形数组,使其包含alive元胞的初始布局。
2、对数组每个元胞执行下面的步骤:
1)统计与元胞相邻的alive元胞的数量
2)如果统计数量为0.1.4.5.6.7.8,则设置另一个名为newmap的数组对应的元胞状态为dead;如果为3,将对应状态设为alive;如果为2,保持不变
3、将newmap矩形数组中的内容复制到map;
4、输出map的内容
1.2.2 Life游戏主程序
/*Conway life of game on a grid
* Uses: functions Initialize,WriteMap,NeighorborCount,UserSaysYes
*/
#include"common.h"
#include"life.h"
void main()
{
int row,col;
Grid map; //current generataion
Grid newmap; //next generation
Initialize(map);
WriteMap(map);
printf("This is the initial state you have chosen.\n"
"Press<Enter> to continue.\n");
while(getchar() != '\n')
;
do{
for(row=1;row<=MAXROW;row++)
for(col=1;col<=MAXCOL;col++)
switch(NeighborCount(map,row,col)){
case 0:
case 1:
newmap[row][col]=DEAD;
break;
case 2:
newmap[row][col]=map[row][col];break;
case 3:
newmap[row][col]=ALIVE;
break;
case 4:
case 5:
case 6:
case 7:
case 8:
newmap[row][col]=DEAD;
break;
}
CopyMap(map,newmap);
WriteMap(map);
printf("Do you wish to continue viewing the new generation");
}while(UserSaysYes());
}
common.h文件如下:
#include<stdio.h>
#include<stdlib.h>
typedef enum Boolean{FALSE,TURE} Boolean;
void Error(char *);
void Warning(char *);
/*Error:report program error
* void Error(char *s)
* {
* fprintf(stderr,"%s\n",s);
* exit(1); //terminate the pro
* }
*/
life.h文件如下:
#define MAXROW 20 /*maximum row range*/
#define MAXCOL 60 /*maximun column range*/
typedef enum state {DEAD,ALIVE} State;
typedef State Grid[MAXROW+2][MAXCOL+2];
void CopyMap(Grid map,Grid newmap);//将更新的map置换
void Initialize(Grid map); //初始化网格并输入初始布局
Boolean UserSaysYes(void); //询问用户是否继续下一代转换
int NeighborCount(Grid map,int row,int column);
//统计第row行第col列元胞周围元胞状态
void WriteMap(Grid map); //完成输出功能
1.3编程的风格
在编写Life游戏的函数之前,首先考虑编程的几个原则。
1.3.1 命名
一个程序要正确运行,最重要的就是知道各个变量代表的意思和函数的作用。编程原则:始终要非常谨慎地给变量和函数命名,同时要对变量和函数进行详细解释。
1.3.2 文档和格式
多数人认为文档是完成程序后一件很烦琐的事情,但是如何向别人阐述程序以及如何将每个细节与其他部分之间的关系整理清楚,写文档很有必要。并不是所有的文档都是适宜的。写文档的时间可能是写程序的时间还要多。
编程原则:尽量保持文档简洁,又能起到说明作用。
1.3.3 程序的细化和模块化
开发过程中最重要的部分是将问题细分到可以理解的更详细的小问题。如果细分后,任然难以理解,那么继续细分,知道容易理解位置。在大型组织中,高层管理者不必操心下属部门的具体细节,而是集中精力于主要的问题和目标,授予下属一定的权利。另一方面,中层必须将工作细分、分配。
编程原则:避免“一叶障目,不见泰山”。
编程原则:每个函数都应当完成一项任务,但必须要很好地完成。
中层并不需要将自己搜集到的所有资料都上交给上级,仅上交需要上级处理的部分。也不需要将上级那里得到的信息全部传达下级,仅需要传达给下级完成分配工作的所必须的信息。编写函数也该遵循这样的原则。
编程原则:每个函数都应该有所保留。
每个函数处理的数据类型有五种:输入、输出、输入输出、局部变量、全局变量。全局变量使用需谨慎。
1.4 编码、测试和进一步细化
编码是用计算机语言编写算法的过程。测试是有选择性地使用样本数据运行程序,并发现程序中存在的错误。进一步细化,将转向未编写的函数。
1.4.1 占位程序
对大型项目,各个函数编写完后需要独立进行调试和测试,而不是等到所有函数都完成。如果想正确地编译程序,必须在需要使用函数的地方放置一些替代代码(有一些简单的虚拟函数组成,称为占位程序stub)。
最简单的占位程序就是不执行任何操作,例如:
/*Initialize:initialize grid map*/
void Initialize(Grid grid)
{
}
/*WriteMap:write grid map*/
void WriteMap()
{
}
利用占位程序,至少可以编译程序,通常占位程序应该显示一条程序该函数被调用的消息。
1.4.2 计算相邻元胞的数目
现在进一步细化程序,计算row行col列相邻元胞的函数需查看毗邻位置。我们在网格的周围引入hedge(栅栏),就是额外增加两行两列在最外围。hedge中的cell状态都是dead。
/*NeighborCount: count neighbors of row,col
Precondition:The pair row,col is a valid cell
Postcondition:The function returns the number of living cell */
int NeighborCount(Grid map,int row,int col)
{
int i; //row of the cell
int j; //col of the cell
int count=o; //counter of living cell
for(i=row-1;i<=row+1;i++)
for(j=col-1;j<=col+1;j++)
if(map[i][j]=ALIVE)
count++;
if(map[i][j]=DEAD)
count--;
return count;
}
1.4.3 输入和输出
现在只剩下Initialize、WriteMap、CopyMap和UserSaysYes函数了,这些函数执行输入和输出。为了保证输入的有效性和一致性,必须对输入进行全面检查;输出也必须经过良好的组织,哪些该显示哪些不显示,都要经过考虑。
编程原则:将输入和输出函数划分为独立的函数,这样便于不同计算机系统进行定制和修改。
/*Initialize:initialize grid map;
Pre:none;
Post:All cells in the grid map must be set to initial configuration*/
void Initialize(Grid map)
{
int row,col;
printf("This program is a simulation of the game of life.\n"
"The grid has a size of %d rows and %d columns.\n",MAXROW,MAXCOL);
for(row=0;row<=MAXROW+1;row++)
for(col=0;col<=MAXCOL+1;col++)
map[i][j]=DEAD; %Set all grid empt
printf("On each line give a pair of state.\n");
scanf("%d%d",&row,&col);
while(row!=0||col!=0)
{
if(row>=1&&row<=MAXROW&&col>=1&&col<=MAXCOL)
map[row][col]=ALIVE;
else
printf("Values are out of range.\n")
scanf("%d%d",&row,&col);
}
while(getchar() !='\n')
;
}
输出函数WriteMap采用如下方法:列出矩形数组中所有内容,alive元胞用“*”,dead用“-“表示。
/*WriteMap(Grid map):display the grid map
* Pre:The rectangular array contains the current life
* Post:The current life is written for the user.*/
void WriteMap(Grid map)
{
int row,col;
putchar('\n');
putchar('\n');
for(row=1;row<=MAXROW;row++)
{
for(col=1;col<=MAXCOL;col++)
if(map[row][col]==ALIVE)
putchar('*');
else
putchar('-');
putchar('\n');
}
}
CopyMap函数将newmap矩形数组复制到map数组中。
/*CopyMap:copy newmap into map;
* Pre:The newmap has the current life configuration
* Post:The map has a copy of newmap
*/
void CopyMap(Grid map,Grid newmap)
{
int row,col;
for(row=0;row<=MAXROW+1;row++)
for(col=0;col<=MAXCOL+1;col++)
map[row][col]=newmap[row][col];
}
UserSaysYes函数用于确定用户是否愿意继续下一代计算。
/*
* Boolean UserSaysYes(void):TRUE if the user want to continue execution
* Pre:None.
* Post:reture true if the user's answer begin with y or Y
* false if the user's answer begin with n or N
*/
Boolean UserSaysYes(void)
{
int c;
printf("(y/n)?");
do{
while((c=getchar())=='\n')
;
if(c=='y'||c=='Y'||c=='n'||c=='N')
return(c=='y'||c=='Y');
printf("Please respond by typing one of the letter y or Y.\n");
}
while(1);
}
UserSaysYes函数不止在life程序中,很多其他的程序也会用到。因此,我们可以把它看做一个使用函数(utility)。
1.4.4 驱动程序
对于小型项目,编写完一个函数后,通常立即将其插入主程序中,然后调试和测试。对于大型项目而言,如何查看一个特定函数运行状态呢?这就需要单个测试和调试函数,常用的方法就是编写一个简短的辅助程序,为函数提供必要的输入。这个辅助程序称为”驱动程序“。
如下例程,测试NeighborCount函数,需要提供map矩阵数组,并输出结果。
/*Driver:test NeighborCount
* Pro:The user must supply an initial map;
* Post:return the values
*/
int main()
{
Grid map;
int i,j;
Initialize(map);
for(i=0;i<=MAXROW;i++)
{
for(j=0;j<=MAXCOL;j++)
printf("%3d",NeighborCount(map,i,j));
printf("\n");
}
return 0;
}
<pre code_snippet_id="272034" snippet_file_name="blog_20140403_11_1063274" name="code" class="cpp"><div style="top:687px"><pre code_snippet_id="272034" snippet_file_name="blog_20140403_11_1063274" name="code" class="cpp"><pre code_snippet_id="272034" snippet_file_name="blog_20140403_11_1063274">