数据结构与程序设计的关联性
编写程序仅仅掌握语言是不够的,还必须掌握数据的组织、存储和运算方法。数据结构是积累程序设计经验的基础上形成的,是提高程序设计能力的基础和关键所在。下面解决一个简单的问题入手,说明数据结构与程序设计的关联性。
【问题描述】 求一名学生10次C语言程序设计的测试成绩总分和平均分。这十次测验的成绩分别为80,85,77,56,68,83,90,92,80,98
【程序示例】
main()
{int sum,verage; /*总分,平均分*/
int t1,t2,t3,t4,t5,t6,t7,t8,t10; /*10个变量存10次成绩*/
t1=80;t2=85;t3=77;t4=56;t5=68; /*分别赋值*/
t6=83;t7=90;t8=92;t9=80;t10=98;
sum=t1+t2+t3+t4+t5+t6+t7+t8+t9+t10; /*计算总分*/
average=sum/10; /*计算平均分*/
printf("总分=%d\n",sum);printf("平均分=%d\n",average);
}
如果要用该程序计算另一名学生的10次成绩就得修改程序,测试次数改变也要修改程序,程序扩展性与通用性较差。
【程序示例】
根据测试次数与测试成绩的关系,采用数组结构存储测验成绩,可以提高程序的适用范围。
main()
{ int sum,erage;int i;
int t[10]={80,85,77,56,68,83,90,92,80,98}; /*分别赋值*/
sum=0; /*总分置初值*/
for(i=0;i<10;i++)
sum=sum+t[i];
average=sum/10; /*计算平均分*/
printf("总分=%d\n",sum);printf("平均分=%d\n",average);
}
分析以上两个程序,虽然都可以正确解决问题,但采用不同的方式存储成绩数据,产生了不同的程序设计方式。因此,选择最佳的数据结构,并提供最佳的数据结构,并提供策略来有效的利用这些数据,可以高效、低耗的解决问题。
结构化程序设计与函数模块化
如前所述,著名的计算机科学家Wirth(沃思)用“算法+数据结构=程序”这样一个著名的公式来阐述了程序设计的实质,也可以理解为程序是在数据特定表示方式基础上对抽象算法的具体描述,即
程序结构=控制结构+数据结构
结构化程序设计是为使程序具有合理的结构,以保证程序正确性而规定的一套程序设计方法,是人们多年来研究和实验的结晶。
1.结构化程序设计目的
通过设计结构良好的程序,以程序良好的静态结构来保证程序动态执行的正确性,使程序易理解、易调试、易维护,以提高软件开发的效率,减少出错率。
2.结构化程序设计的构成单元
任何程序都可由顺序、选择、重复三种基本控制结构来组成。其中,顺序结构表示“处理1”先于“处理2”执行;选择结构表示当条件满足时执行“处理1”,否则执行“处理2”;而在重复结构中,首先判断条件是否满足,若满足则执行“处理”,并返回继续判断条件,直至条件不满足为止。这三种基本的控制结构共有的特点是单入口单出口。
利用三种基本控制结构,通过组合的方法,产生所需要的程序控制结构。
为灵活、方便的处理问题,程序语言也可引入其它控制结构设施,如goto语句。goto语句为无条件转向语句,其一般格式为:
goto语句标号
含义是无条件跳转到到语句标号所处位置继续执行后面的语句。显然,goto语句改变了程序的执行顺序。如果程序中不限制goto语句的使用,会使程序的静态结构与程序的动态执行差异很大,程序难阅读、难理解,破坏了单入口但出口的原则,使程序的正确性难以证明,错误难以局部化。因此限制goto语句的使用是必要的。
3.结构化程序设计方法
结构化程序设计的概念是E.W.Dijkstra于1969年首先提出的,他强调了从程序结构和风格上来研究程序设计问题。也将此方法称为“自顶向下”或“逐步求精”法。其一是“自顶向下,逐步求精”的设计思想,即整个设计应分为若干层次,逐步加以解决;而每一步是在前一步的基础上,对前一步的设计的细化。其二是“独立功能,一个入口,一个出口”的模块化结构,即把大而复杂的问题层层细化分解成若干个相对独立、功能单一的问题处理模块,而每个模块与外界联系只有一个入口与一个出口。其三是“仅用三种基本控制结构”的设计原则,即每个模块都只用三个基本结构来描述。
面向对象与抽象数据类型
1.面向对象的概念
Coad和Yourdon给出面向对象的概念:面向对象=对象+类+继承+通信。
对象:是指在应用问题中出现的各种实体、事件和规格说明等,它是由一组属性和在这组值上的一组服务构成的,其中属性值确定了对象的状态。
类:把具有相同属性和服务的对象归到同一类,而把一个类中的每一个对象称为该类的一个实例,它们具有相同的服务。
继承:面向对象方法的最有特色的方面。
各个类的对象间通过消息进行通信。
面向对象程序设计的特点是封装性(Encapsulate)、继承性(Inheritance)和多态性(Polymor-phism)。可以看出,与数据结构密切相关的是定义在数据结构上的一组操作。操作的种类和数目不同,即使逻辑结构相同,这个数据结构的用途也可能大不相同。定义在数据结构上的操作种类是没有限制的,可以根据具体需要而定义。
基本操作主要可列如下五种。
①插入:在数据结构中的指定位置上增添新的数据元素。
②删除:删除数据结构中某个指定的数据元素。
③更新:改变数据结构中某个元素的值,在概念上等价于删除和插入操作的组合。
④查找:在数据结构中寻找满足某个特定要求的数据元素(包括位置和值)。
⑤排序:(在线性结构中)重新安排数据元素之间的逻辑顺序关系,使数据元素按值由小到大或由大到小的次序排列。
根据增、删、改、查找、排序等基本操作的特性,所有操作可以分为两大类:一类是加工型操作,其操作结果改变了结构的值;另一类是引用型操作,其操作的结果不改变结构的值。
结构化的开发方法与面向对象的开发方法的不同之处:结构化的开发方法是面向过程的开发方法,首先着眼于系统要实现的功能。从系统的输入和输出出发,分析系统要实现的功能,用自顶向下、逐步细化的方式建立系统的功能结构和相应的程序模块结构。一旦程序功能需要修改,就会涉及多个模块的修改,修改量大,易出错,并会导致程序的退化。
面向对象的开发方法首先着眼于应用问题所涉及的对象,包括对象,对象属性、要求的操作、从而建立对象结构和为解决问题需要执行的时间序列,据此建立类的继承层次结构,通过各个类的实例之间的消息链接实现所需的功能。类的定义充分体现了抽象数据类型的思想,基于类的体系结构可以把对程序的修改局部化,如果系统功能的需求发生变化,只需修改类中的服务即可,此时类所代表的的对象基本不变,从而确保系统不致因修改而退化。
与结构化开发方法相比,采用面向对象开发方法开发的程序具有更高的可靠性、可修改性、可维护性、可复用性、可适用性和可理解性。
2.抽象数据类型与问题求解方法
抽象数据类型是近年来计算机科学中提出的最重要的概念之一。它集中体现了程序设计中一些基本的原则:分解、抽象和信息隐蔽。对一个抽象数据类型,可以用代数系统形式严格地给予定义,而直观上可以把抽象数据类型看成是定义了一组运算的数学模型。
抽象数据类型的概念不仅包含数学模型,同时还包括这个模型上的运算。数据的逻辑结构和运算的定义组成了数据结构规范,而数据的存储结构和运算算法的描述构成了数据结构的实现。规范是实现的准则和依据,规范指明“做什么”,而实现解决“怎么做”。从规范与实现两方面来讨论数据结构是抽象数据模型的观点,抽象数据类型不仅发展了数据抽象的概念,而且将数据抽象和过程抽象结合起来。一个抽象数据类型确定了一个模型,但将模型的实现细节隐藏起来;它定义了一组运算,但将运算的实现过程隐藏起来。
无论是从计算机理论还是从计算机工程的角度来看,抽象数据类型的概念都是十分重要的。一方面,它抽象和推广了高级程序设计语言中的类型概念。另一方面,也为软件工程提供了一个自顶向下的实现图示。
用抽象数据类型的概念来指导问题求解的过程,如下所示
数学模型(非形式算法)——抽象数据类型(伪语言程序)——数据结构(可执行程序)
其中第一步是选用适当的数学模型来描述要处理的问题,同时确定解决问题的算法的基本思想。第二步是用一种比较形式化的方法将解决问题的算法表达出来。描述算法的工具可以是一种类语言(比如说类似C的语言)。与这一工作并行的是为算法中用到的每个非基本的数据类型建立一个抽象数据类型,用函数名给这个类型上的每个操作命名,同时用对这些过程的调用来取代算法中的每个操作。第三步是为了每个抽象数据类型选择一种实现的方法,同时编写出在这些抽象数据类型上定义的所有操作的过程。伪语言程序中非标准语句也要用程序设计语言中的标准语句加以改写,以得到一个可执行的程序。
综上所述,可以看出数据结构与抽象数据类型的关系有点类似于程序设计语言中类型与值的关系。在高级语言中,整形代表语言(实际上是机器)中所能使用的全体整数的集合。与整数相关的一组运算包括加、减、乘、除、取余等。这些运算都是由系统内部实现的。程序中定义一个整形量(变量或常量)后,就可以施加上述运算进行处理,程序员只要会使用这些运算就行了,至于这个整形量在机器内部怎样存放,是十进制还是二进制,是占16位还是占32位,整数的处理究竟如何实现,采用了哪一种除法的算法等,对于用户来说都是隐藏的。与基本数据类型不同的是,系统并没有预先提供抽象数据类型的运算,系统也没有约定抽象数据类型的量(或称实例、实体)在计算机中怎样表示(或称存储),这些都要程序员来安排。所以要实现一个抽象数据类型,首先要用适当的方式来说明数学模型在计算机内的表示方法,这常常是借助于语言中已有的基本数据类型和构造组合类型等手段(如记录、数组等)来完成;其次还要根据选择的表示方法,建立一组过程来实现这个模型上的一组运算。选用的表示方法不同,实现运算的过程也就不同。有了这些基础,抽象数据类型就可以和基本数据类型处于同等的地位了。这种抽象数据类型的一个实体就是通常所说的一个数据类型。由于我们的研究都是基于目前的计算机系统和现有的高级语言的,因此在研究数据结构时,不仅要研究体现抽象数据类型的内在模型(逻辑结构)和这个模型上所定义操作(运算)的实现,更要仔细研究这个实体在目前系统中的表示(存储结构),因为不同的表示方法决定了不同的实现算法和不同的算法开销。
事实上,上面说明的不仅是问题求解的一般过程,也是目前软件自动生成常用的模式。数学模型——抽象数据类型——数据结构,恰好反映了信息转换的三个重要阶段,而在这个转换过程中,数据结构是基础,抽象数据类型是中枢。
3.抽象数据类型的实现途径具体定义抽象数据类型需要借助于高级语言,对于抽象数据类型(ADT)的具体实现则依赖于所选择的高级语言的功能。从程序设计的历史发展来看,有传统的面向过程的程序设计,“包”、“模型”设计,面向对象的程序设计等几种不同的实现方法。
(1)传统的面向过程的程序设计
这也是现在常用的方法,根据逻辑结构选择合适的存储结构,根据所要求的操作设计相应的子程序或子函数。
在标准Pascal、C等面向过程的语言中,用户可以借助过程和函数,利用固有的数据类型来表示和实现抽象数据类型。由于标准Pascal语言的程序结构框架是由严格规定次序的“段”(包括程序首部、标号说明、常量定义、类型定义、变量说明、过程或函数说明、语句部分)组成的,因此,用户如果要使用已定义的抽象数据类型,就必须将该类型说明和过程说明嵌入到自己的程序首部。可见,这类语言利用抽象数据类型进行程序设计的基本方法是,只有嵌入了该类型说明和过程说明之后才可以访问抽象数据类型,称之为数据结构受限的访问。
(2)“包”、“模型”的设计方法
这是面向模块结构的表示和实现。Ada语言提供了“包”(Package),Module-2语言提供了“模块”(Module)结构,Turbo Pascal语言提供了“单元”(Unit)结构,每个模块含有一个或多个抽象数据类型,它不仅可以单独编译,而且为外部使用抽象数据类型提供了方便。使用这类结构实现ADT比起第一种方法有了一定的进步。
(3)面向对象的程序设计
面向对象的程序设计(Object Oriental Programming,OOP)语言包括VC、VB、C++等,它借助对象描述抽象数据类型,存储结构的说明和操作函数的说明被封装在一个整体结构中,这个整体结构被称为“类”(Class),属于某个“类”的具体变量被称为“对象”(Object)。OOP与ADT的实现更加接近和一致。从前面对数据类型的讨论中可以看到,在面向对象的程序设计语言中,“类型”的概念与“操作”密切相关,同一种数据类型和不同操作组将组成不同的数据类型,结构说明和过程说明被统一在一个整体对象之中,其中,数据结构的定义为对象的属性域过程或函数定义在对象中,称为方法(Method),它是对对象的性能描述。
4.用C语言实现抽象数据类型
ADT包括定义和实现两方面,其中定义是独立于实现的。定义仅给出一个ADT的逻辑特性,不必考虑如何在计算机中实现。
(1)ADT的定义格式
例. 给出“简化线性表”的抽象数据类型定义。
ADT Linear_list{
数据元素:所有ai属于同一数据对象,i=1,2,……,n,n>=0;
结构关系:所有数据元素ai(i=1,2,……,n-1)存在次序关系<ai,ai+1>,a1无前驱,an无后继;
基本操作:设L为Linear_List,则有
①InitList(L):初始化空线性表。
②ListLength(L):求线性表的表长。
③GetData(L,i):取线性表的第i个元素。
④InsList(L,i,b):在线性表第i个位置插入元素b。
⑤DelList(L,i):删除线性表的第i个数据元素。
}ADT Linear_list
在上述ADT中,数据元素所属的数据对象没有局限于一个具体的整形、实型或其它类型,所具有的操作也是抽象的数据特性,并没有具体到某一种计算机语言指令与程序编码。
(2)用C语言实现ADT
用C语言实现ADT时,主要包括以下两个方面。
①通过结构体将int、float等基本数据类型组合到一起,构成一个结构体类型。再用typedef为该类型或该类型指针重新起一个名字,以增强程序的抽象性、简洁性和可读性。即采用C语言中typedef自定义类型来实现。
②用C语言的子函数实现各个操作。
typedef可以用来建立已定义好的数据类型的别名。为建立便于记忆和使用的类型名,也为了省略结构标记,C程序员经常使用typedef来定义结构类型。如typedef struct card Card;建立用户自定义的数据类型的新类型名为Card,它是类型struct card的别名,就可以利用Card类型名来代替struct card类型名。
typedef struct {
float realpart;
float imagpart;}Complex;
/*函数原型说明部分。注意函数头后面有分号*/
Complex create(float x,float y);
Complex add(Complex z1,Complex z2);
/*函数定义部分。注意函数头后面无分号*/
Complex create(float x,float y)
/*利用x,y创建复数z,并将z返回*/
{Complex z;
z.realpart=x;
z.imagpart=y;
return(z);
}
Complex add (Complex z1,complex z2)
/*求复数z1,z2的和sum,并将sum返回*/
{Complex sum;
sum.realpart=z1.realpart+z2.realpart;
sum.imagpart=z1.imagpart+z2.imagpart;
return(sum);
}
main()
{
float a,b;
Complex c1,c2,c3;
printf("\n\n\n Input realpart and imagpart:");
scanf("%f%f",&a,&b);
c1=create(a,b);
printf("\n\n\n Input realpart and imagpart:");
scanf("%f%f",&a,&b);
c2=creat(a,b);
c3=add(c1,c2);
printf("\n\n c1==%f+%f i",c1.realpart,c1.imagpart);
printf("\n\n c2==%f+%f i",c2.realpart,c2.imagpart);
printf("\n\n c3==c1+c2==%f+%f i",c3.realpart,c3.imagpart);
}
算法描述规范与设计风格
程序设计人员需要具备良好的程序设计风格。因为程序不仅要能正确的解决问题,也应当易于阅读与修改。养成良好的程序设计习惯是职业素质的重要环节。
1.算法表示格式与函数模块化
(1)算法表现形式
[函数返回值类型] 函数名([形式参数及说明])
{ 内部数据说明;
执行语句组;
} /*函数名*/
函数的定义主要由函数名和函数体组成,函数体用花括号“{”和“}”括起来。函数中用方括号括起来的部分如“[形式参数及说明]”为可选项,函数名之后的圆括号不可省略。函数的结果可由指针或其它方式传递到函数之外。执行语句可由各种类型的语句组成,两个语句之间用“;”号分隔。可将函数中表达式的值通过return语句返回给调用它的函数。最后的花括号“}”之后的/*函数名*/为注释部分,这是一种习惯写法,可按实际情况取舍。
(2)函数模块化
根据结构化程序设计的思想,将一个大任务分解成若干个功能独立的子任务,故程序可由一个主函数和若干个子函数构成。C的函数相当于其它语言中的子程序,用函数来实现特定的功能。一个完整的、可执行的C程序文件一般结构如下:
[包含文件语句]
[宏定义语句]
[自定义类型语句]
[所有子函数的原型说明]
[子函数1定义]
···
[子函数n定义]
[主函数定义]
其中,每个函数具有独立功效,但看函数名与注释即可明白函数的功能,一旦需要修改某个函数,并不影响其他函数的运作,延伸下去就是面向对象的程序设计理念。
2.算法描述要点
C语言具有高效、灵活、实用、精炼的特点,但也存在语法规范上不严密的缺陷,在算法描述上应避免。
(1)加上必要的注释
在每个程序或函数的开头附上简短的注释,对函数的处理的前提、参数的作用、提供的结果与函数完成的功能加以说明,使得人们简单阅读后就能明白函数在做什么;在程序中有技巧的地方加上必要的注释,则有画龙点睛的功效,便于理解函数功能的具体实现方法。
注释形式可以用“/*字符串*/”的形式,对C语言程序中的任何部分需要时加上必要的注释,以提高程序的可读性。
(2)避免函数返回值隐含说明
对于主函数而言,返回类型和参数列表一般可以不写出,系统会执行一些默认的操作,对于子函数而言,返回类型是必不可少的。因为C语言规定函数、参数、外部变量不加类型说明时则隐含为int类型,当编写的算法函数的函数值为int类型时必须用int显示说明,以避免类型失配或移植算法时出现错误;当函数返回值为空类型时必须用void显式说明,以避免混淆。
(3)预定义常量和类型
经常用到的常量如TURE、FALSE、Maxsize等可通过宏定义为符号常量。
#define TURE 1
#define FALSE 0
#define MAXSIZE 100
#define OK 1
#define ERROR 0
(4)使用有意义的函数名和变量名
使用有意义的函数名与变量名,见名达义,以增加程序的可读性,如用swap、max等函数名,只要一看这样的函数名,就明白了函数的功能是交换和求最大值。而用Average、Sum、Count做变量名,无须看注释,已经明确了这些变量的作用。
(5)避免可能出现的二义性表达
如表达式表示上,提倡使用i++或i=i+1,不应使用i=++i,因为后面的式子阅读困难,随编译器不同而异。
(6)规范多分支转向
在C语言中多分支switch语句中,每个case语句后都应使用break语句,使之规范到跳出此switch语句。
(7)简化输入、输出表述
输入输出函数中的类型部分不做严格要求,淡化表述。
(8)注意不同退出语句之间的区别
return<表达式>或return:用于函数结束。
break语句:可用在循环语句或switch语句中结束循环过程或跳出情况语句。
continue语句:可用在循环语句中结束本次循环,进入下一次循环过程。
exit语句:表示出现异常情况,控制退出用户程序。
3.与参数传递的相关技术
(1)变量的作用域
程序的编译单位是源程序文件,一个源程序文件可以包含一个或若干个子函数,在函数内定义的变量是内部变量,在函数外定义的变量是外部变量,又称全局变量或全程变量。
全局变量:程序中所有函数都可以访问的量。可以为本文件中其它函数所公用,其作用域从定义该变量的位置开始一直到文件结束。全局变量可以实现参数传递的某些功能,在其作用域范围内,全局变量可以将子函数中的值带出并进入其他函数,但如果在一个子函数中改变它,将会影响全局变量的值。
局部变量:只能在本函数中访问的量。只在本函数范围内有效,也就是说只有在本函数内才能使用他们,本函数以外不能使用。需要注意的是,不同函数中可以使用相同名字的变量,由于其作用域的范围不同,尽管有相同的名字,但他们代表不同的对象,作用域不同,互不干涉。
在一个函数内部,可以在复合语句中定义变量,但这种变量只在本复合语句中有效。变量作用域就是指包含该变量定义的最小范围。比如说,在子函数定义的变量其作用域就在该子函数体内,在复合语句中定义的变量其作用域就在该复合语句中,在文件开始处定义的变量在整个文件内有效。
(2)参数传递方式
参数传递是函数之间进行信息通信的重要渠道,其参数传递的主要方式有传值和传地址两类。函数参数表中的参数有两种:第一种参数只为操作提供待处理数据,又称值参;第二种参数既能为操作提供待处理数据,又能返回操作结果,也称变量参数。操作前提描述了操作执行之前数据结构和参数应满足的条件,操作结果描述操作执行之后,数据结构的变化状况和应返回结果。ADT可用现有程序设计语言中已有的数据类型,即基本数据类型来表示和实现。不同的语言表示和实现方法不尽相同,如ADT中“返回结果的参数”,Pascal语言用“变参”实现,C++语言通过“引用型参数”实现,而C语言用“指针参数”实现。C语言中调用函数时,实参代替形参的过程是一个单向的传值过程,在编译技术中称为值传递方式。C语言中指针的参数传递可以看作是传地址方式。