目 录
第1章 数据结构与算法 1
1.1 算 法 1
1.1.1算法的基本概念 1
1.1.2 算法复杂度 4
1.2 数据结构的基本概念 7
1.2.1什么是数据结构 7
1.2.2数据结构的图形表示 12
1.2.3线性结构与非线性结构 13
1.3线性表及其顺序存储结构 14
1.3.1线性表的基本概念 14
1.3.2线性表的顺序存储结构 15
1.3.3顺序表的插入运算 16
1.3.4顺序表的删除运算 17
1.4 栈和队列 19
1.4.1栈及其基本运算 19
1.4.2队列及其基本运算 20
1.5 线性链表 22
1.5.1线性链表的基本概念 22
1.5.2线性链表的基本运算 26
1.5.3循环链表及其基本运算 28
1.6 树与二叉树 29
1.6.1树的基本概念 29
1.6.2二叉树及其基本性质 32
1.6.3二叉树的存储结构 34
1.6.4二叉树的遍历 35
1.7 查找技术 36
1.7.1顺序查找 37
1.7.2二分法查找 37
1.8 排序技术 37
1.8.1交换类排序法 38
1.8.2插入类排序法 39
1.8.3选择类排序法 41
习 题 1 43
第2章 程序设计基础 45
2.1 程序设计方法与风格 45
2.2 结构化程序设针 46
2.2.1结构化程序设计的原则 46
2.2.2结构化程序设计的基本结构和特点 47
2.2.3结构化程序设计原则和方法的应用 48
2.3面向对象的程序设计 48
2.3.1关于面向对象方法 48
2.3.2面向对象方法的基本概念 51
习 题 2 54
第3章 软件工程基础 56
3.1 软件工程基本概念 56
3.1.1软件定义与软件特点 56
3.1.2软件危机与软件工程 57
3.1.3软件工程过程与软件生命周期 58
3.1.4软件工程的目标与原则 59
3.1.5软件开发工具与软件开发环境 60
3.2 结构化分析方法 61
3.2.1 需求分析与需求分析方法 61
3.2.2结构化分析方法 62
3.2.3软件需求规格说明书 66
3.3 结构化设计方法 67
3.3.1软件设计的基本概念 67
3.3.2概要设计 70
3.3.3详细设计 74
3.4 软件测试 78
3.4.1软件测试的目的 79
3.4.2软件测试的准则 79
3.4.3软件测试技术与方法综述 79
3.4.4软件测试的实施 86
3.5程序的调试 89
3.5.1基本概念 89
3.5.2软件调试方法 90
习 题 3 92
第4章 数据库设计基础 93
4.1 数据厍系统的基本概念 93
4.1.1数据、数据库、数据库管理系统 93
4.1.2数据库系统的发展 96
4.1.3数据库系统的基本特点 98
4.1.4数据库系统的内部结构体系 99
4.2 数据模型 100
4.2.1数据模型的基本概念 100
4.2.2E-R模型 101
4.2.3层次模型 105
4.2.4网状模型 105
4.2.5关系模型 106
4.3 关系代数 109
4.4 数据厍设计与管理 115
4.4.1数据库设计概述 115
4.4.2数据库设计的需求分析 116
4.4.3数据库概念设计 117
4.4.4数据库的逻辑设计 120
4.4.5数据库的物理设计 122
4.4.6数据库管理 122
习 题 4 123
习题参考答案 125
习题1参考答案 125
习题2参考答案 125
习题3参考答案 125
习题4参考答案 125
第1章 数据结构与算法
1.1 算 法
1.1.1算法的基本概念所谓算法是指解题方案的准确而完整的描述。
对于一个问题,如果可以通过一个计算机程序,在有限的存储空间内运行有限长的时间而得到正确的结果,则称这个问题是算法可解的。但算法不等于程序,也不等于计算方法。当然,程序也可以作为算法的一种描述,但程序通常还需考虑很多与方法和分析无关的细节问题,这是因为在编写程序时要受到计算机系统运行环境的限制。通常,程序的编制不可能优于算法的设计。
1.算法的基本特征作为一个算法,一般应具有以下几个基本特征。
(1)可行性(effectiveness)
针对实际问题设计的算法,人们总是希望能够得到满意的结果。但一个算法又总是在某个特定的计算工具上执行的,因此,算法在执行过程中往往要受到计算工具的限制,使执行结果产生偏差。例如,在进行数值计算时,如果某计算工具具有7位有效数字(如程序设计语言中的单精度运算),则在计算下列三个量
A=1012,B=1,C=-1012
的和时,如果采用不同的运算顺序,就会得到不同的结果,即
A+B+C=1012+1+(-1012)=0
A+C+B=1012+(-1012)+1=1
而在数学上,A+B+C与A+C+B是完全等价的。因此,算法与计算公式是有差别的。在设计一个算法时,必须要考虑它的可行性,否则是不会得到满意结果的。
(2)确定性(definiteness)
算法的确定性,是指算法中的每一个步骤都必须是有明确定义的,不允许有模棱两可的解释,也不允许有多义性。这一性质也反映了算法与数学公式的明显差别。在解决实际问题时,可能会出现这样的情况:针对某种特殊问题,数学公式是正确的,但按此数学公式设计的计算过程可能会使计算机系统无所适从。这是因为根据数学公式设计的计算过程只考虑了正常使用的情况,而当出现异常情况时,此计算过程就不能适应了。
(3)有穷性(finiteness)
算法的有穷性,是指算法必须能在有限的时间内做完,即算法必须能在执行有限个步骤之后终止。数学中的无穷级数,在实际计算时只能取有限项,即计算无穷级数值的过程只能是有穷的。因此,一个数的无穷级数表示只是一个计算公式,而根据精度要求确定的计算过程才是有穷的算法。
算法的有穷性还应包括合理的执行时间的含义。因为,如果一个算法需要执行千万年,显然失去了实用价值。
(4)拥有足够的情报一个算法是否有效,还取决于为算法所提供的情报是否足够。通常,算法中的各种运算总是要施加到各个运算对象上,而这些运算对象又可能具有某种初始状态,这是算法执行的起点或是依据。因此,一个算法执行的结果总是与输入的初始数据有关,不同的输入将会有不同的结果输出。当输入不够或输入错误时,算法本身也就无法执行或导致执行有错。一般来说,当算法拥有足够的情报时,此算法才是有效的,而当提供的情报不够时,算法可能无效。
综上所述,所谓算法,是一组严谨地定义运算顺序的规则,并且每一个规则都是有效的,且是明确的,此顺序将在有限的次数下终止。
2.算法的基本要素一个算法通常由两种基本要素组成:一是对数据对象的运算和操作,二是算法的控制结构。
(1)算法中对数据的运算和操作每个算法实际上是按解题要求从环境能进行的所有操作中选择合适的操作所组成的一组指令序列。因此,计算机算法就是计算机能处理的操作所组成的指令序列。
通常,计算机可以执行的基本操作是以指令的形式描述的。一个计算机系统能执行的所有指令的集合,称为该计算机系统的指令系统。计算机程序就是按解题要求从计算机指令系统中选择合适的指令所组成的指令序列。在一般的计算机系统中,基本的运算和操作有以下四类:
①算术运算:主要包括加、减、乘、除等运算。
②逻辑运算:主要包括“与”、“或”、“非”等运算。
③关系运算:主要包括“大于”、“小于”、“等于”、“不等于”等运算。
④数据传输:主要包括赋值、输入、输出等操作。
前面提到,计算机程序也可以作为算法的…种描述,但由于在编制计算机程序时通常要考虑很多与方法和分析无关的细节问题(如语法规则),因此,在设计算法的一开始,通常并不直接用计算机程序来描述算法,而是用别的描述工具(如流程图,专门的算法描述语言,甚至用自然语言)来描述算法。但不管用哪种工具来描述算法,算法的设计一般都应从上述四种基本操作考虑,按解题要求从这些基本操作中选择合适的操作组成解题的操作序列。算法的主要特征着重于算法的动态执行,它区别于传统的着重于静态描述或按演绎方式求解问题的过程。传统的演绎数学是以公理系统为基础的,问题的求解过程是通过有限次推演来完成的,每次推演都将对问题作进一步的描述,如此不断推演,直到直接将解描述出来为止。而计算机算法则是使用一些最基本的操作,通过对已知条件一步一步的加工和变换,从而实现解题目标。这两种方法的解题思路是不同的。
(2)算法的控制结构一个算法的功能不仅取决于所选用的操作,而且还与各操作之间的执行顺序有关。算法中各操作之间的执行顺序称为算法的控制结构。
算法的控制结构给出了算法的基本框架,它不仅决定了算法中各操作的执行顺序,而且也直接反映了算法的设计是否符合结构化原则。描述算法的工具通常有传统流程图、N-S结构化流程图、算法描述语言等。一个算法一般都可以用顺序、选择、循环三种基本控制结构组合而成。
3.算法设计基本方法计算机解题的过程实际上是在实施某种算法,这种算法称为计算机算法。计算机算法不同于人工处理的方法。
本节介绍工程上常用的几种算法设计方法,在实际应用时,各种方法之间往往存在着一定的联系。
(1)列举法列举法的基本思想是,根据提出的问题,列举所有可能的情况,并用问题中给定的条件检验哪些是需要的,哪些是不需要的。因此,列举法常用于解决“是否存在”或“有多少种可能”等类型的问题,例如求解不定方程的问题。
列举法的特点是算法比较简单。但当列举的可能情况较多时,执行列举算法的工作量将会很大。因此,在用列举法设计算法时,使方案优化,尽量减少运算工作量,是应该重点注意的。通常,在设计列举算法时,只要对实际问题进行详细的分析,将与问题有关的知识条理化、完备化、系统化,从中找出规律;或对所有可能的情况进行分类,引出一些有用的信息,是可以大大减少列举量的。
列举原理是计算机应用领域中十分重要的原理。许多实际问题,若采用人工列举是不可想象的,但由于计算机的运算速度快,擅长重复操作,可以很方便地进行大量列举。列举算法虽然是一种比较笨拙而原始的方法,其运算量比较大,但在有些实际问题中(如寻找路径、查找、搜索等问题),局部使用列举法却是很有效的,因此,列举算法是计算机算法中的一个基础算法。
(2)归纳法归纳法的基本思想是,通过列举少量的特殊情况,经过分析,最后找出一般的关系。显然,归纳法要比列举法更能反映问题的本质,并且可以解决列举量为无限的问题。但是,从一个实际问题中总结归纳出一般的关系,并不是一件容易的事情,尤其是要归纳出一个数学模型更为困难。从本质上讲,归纳就是通过观察一些简单而特殊的情况,最后总结出一般性的结论。
归纳是一种抽象,即从特殊现象中找出一般关系。但由于在归纳的过程中不可能对所有的情况进行列举,因此,最后由归纳得到的结论还只是一种猜测,还需要对这种猜测加以必要的证明。实际上,通过精心观察而得到的猜测得不到证实或最后证明猜测是错的,也是常有的事。
(3)递推所谓递推,是指从己知的初始条件出发,逐次推出所要求的各中间结果和最后结果。其中初始条件或是问题本身已经给定,或是通过对问题的分析与化简而确定。递推本质上也属于归纳法,工程上许多递推关系式实际上是通过对实际问题的分析与归纳而得到的,因此,递推关系式往往是归纳的结果。
递推算法在数值计算中是极为常见的。但是,对于数值型的递推算法必须要注意数值计算的稳定性问题。
(4)递归人们在解决一些复杂问题时,为了降低问题的复杂程度(如问题的规模等),一般总是将问题逐层分解,最后归结为一些最简单的问题。这种将问题逐层分解的过程,实际上并没有对问题进行求解,而只是当解决了最后那些最简单的问题后,再沿着原来分解的逆过程逐步进行综合,这就是递归的基本思想。由此可以看出,递归的基础也是归纳。在工程实际中,有许多问题就是用递归来定义的,数学中的许多函数也是用递归来定义的。递归在可计算性理论和算法设计中占有很重要的地位。
递归分为直接递归与间接递归两种。如果一个算法P显式地调用自己则称为直接递归。如果算法P调用另一个算法Q,而算法Q又调用算法P,则称为间接递归调用。
递归是很重要的算法设计方法之一。实际上,递归过程能将一个复杂的问题归结为若干个较简单的问题,然后将这些较简单的问题再归结为更简单的问题,这个过程可以一直做下去,直到最简单的问题为止。
有些实际问题,既可以归纳为递推算法,又可以归纳为递归算法。但递推与递归的实现方法是大不一样的。递推是从初始条件出发,逐次推出所需求的结果;而递归则是从算法本身到达递归边界的。通常,递归算法要比递推算法清晰易读,其结构比较简练。特别是在许多比较复杂的问题中,很难找到从初始条件推出所需结果的全过程,此时,设计递归算法要比递推算法容易得多。但递归算法的执行效率比较低。
(5)减半递推技术实际问题的复杂程度往往与问题的规模有着密切的联系。因此,利用分治法解决这类实际问题是有效的。所谓分治法,就是对问题分而治之。工程上常用的分治法是减半递推技术。
所谓“减半”,是指将问题的规模减半,而问题的性质不变;所谓“递推”,是指重复“减半”的过程。
下面举例说明利用减半递推技术设计算法的基本思想。
例1.1设方程f(x)=0在区间[a,b]上有实根,且f(a)与f(b)异号。利用二分法求该方程在区间[a,b]上的一个实根。
用二分法求方程实根的减半递推过程如下:
首先取给定区间的中点c=(a+b)/2。
然后判断f(c)是否为0。若f(c)=0,则说明c即为所求的根,求解过程结束;如果f(c)≠0,则根据以下原则将原区间减半:
若f(a)f(c)<0,则取原区间的前半部分;
若f(b)f(c)<0,则取原区间的后半部分。
最后判断减半后的区间长度是否已经很小:
若|a-b|<ε,则过程结束,取(a+b)/2为根的近似值;
若|a-b|≥ε,则重复上述的减半过程。
(6)回溯法前面讨论的递推和递归算法本质上是对实际问题进行归纳的结果,而减半递推技术也是归纳法的一个分支。在工程上,有些实际问题很难归纳出一组简单的递推公式或直观的求解步骤,并且也不能进行无限的列举。对于这类问题,一种有效的方法是“试”。通过对问题的分析,找出一个解决问题的线索,然后沿着这个线索逐步试探,对于每一步的试探,若试探成功,就得到问题的解,若试探失败,就逐步回退,换别的路线再进行试探。这种方法称为回溯法。回溯法在处理复杂数据结构方面有着广泛的应用。
1.1.2 算法复杂度算法的复杂度主要包括时间复杂度和空间复杂度。
1.算法的时间复杂度所谓算法的时间复杂度,是指执行算法所需要的计算工作量。
为了能够比较客观地反映出一个算法的效率,在度量一个算法的工作量时,不仅应该与所使用的计算机、程序设计语言以及程序编制者无关,而且还应该与算法实现过程中的许多细节无关。为此,可以用算法在执行过程中所需基本运算的执行次数来度量算法的工作量。基本运算反映了算法运算的主要特征,因此,用基本运算的次数来度量算法工作量是客观的也是实际可行的,有利于比较同一问题的几种算法的优劣。例如,在考虑两个矩阵相乘时,可以将两个实数之间的乘法运算作为基本运算,而对于所用的加法(或减法)运算忽略不计。又如,当需要在一个表中进行查找时,可以将两个元素之间的比较作为基本运算。
算法所执行的基本运算次数还与问题的规模有关。例如,两个20阶矩阵相乘与两个lO阶矩阵相乘,所需要的基本运算(即两个实数的乘法)次数显然是不同的,前者需要更多的运算次数。因此,在分析算法的工作量时,还必须对问题的规模进行度量。
综上所述,算法的工作量用算法所执行的基本运算次数来度量,而算法所执行的基本运算次数是问题规模的函数,即算法的工作量:f(n)
其中n是问题的规模。例如,两个n阶矩阵相乘所需要的基本运算(即两个实数的乘法)次数为n3,即计算工作量为n3,也就是时间复杂度为n3。
在具体分析一个算法的工作量时,还会存在这样的问题:对于一个固定的规模,算法所执行的基本运算次数还可能与特定的输入有关,而实际上又不可能将所有可能情况下算法所执行的基本运算次数都列举出来。例如,“在长度为n的一维数组中查找值为x的元素”,若采用顺序搜索法,即从数组的第一个元素开始,逐个与被查值x进行比较。显然,如果第一个元素恰为x,则只需要比较1次。但如果x为数组的最后一个元素,或者x不在数组中,则需要比较n次才能得到结果。因此,在这个问题的算法中,其基本运算(即比较)的次数与具体的被查值x有关。
在同一个问题规模下,如果算法执行所需的基本运算次数取决于某一特定输入时,可以用以下两种方法来分析算法的工作量。
(1)平均性态(Average Behavior)
所谓平均性态分析,是指用各种特定输入下的基本运算次数的加权平均值来度量算法的工作量。
设x是所有可能输入中的某个特定输入,p(x)是x出现的概率(即输入为x的概率),t(x)是算法在输入为x时所执行的基本运算次数,则算法的平均性态定义为
其中D。表示当规模为n时,算法执行时所有可能输入的集合。这个式子中的t(x)可以通过分析算法来加以确定;而p(x)必须由经验或用算法中有关的一些特定信息来确定,通常是不能解析地加以计算的。如果确定p(x)比较困难,则会给平均性态的分析带来困难。
(2)最坏情况复杂性(Worst-Case Complexity)
所谓最坏情况分析,是指在规模为n时,算法所执行的基本运算的最大次数。它定义为
显然,w(n)的计算要比A(n)的计算方便得多。由于w(n)实际上是给出了算法工作量的一个上界,因此,它比A(n)更具有实用价值。
下面通过一个例子来说明算法复杂度的平均性态分析与最坏情况分析。
例1.2采用顺序搜索法,在长度为n的一维数组中查找值为x的元素。即从数组的第一个元素开始,逐个与被查值x进行比较。基本运算为x与数组元素的比较。
首先考虑平均性态分析。
设被查项x在数组中出现的概率为q。当需要查找的x为数组中第i个元素时,则在查找过程中需要做i次比较,当需要查找的x不在数组中时(即数组中没有x这个元素),则需要与数组中所有的元素进行比较。即
其中i=n+1表示x不在数组中的情况。
如果假设需要查找的x出现在数组中每个位置上的可能性是一样的,则x出现在数组中每一个位置上的概率为q/n(因为前面已经假设x在数组中的概率为q),而x不在数组中的概率为1-q。即
其中i=n+l表示x不在数组中的情况。
因此,用顺序搜索法在长度为n的一维数组中查找值为x的元素,在平均情况下需要做的比较次数为
如果已知需要查找的x一定在数组中,此时q=l,则A(n)=(n+1)/2。这就是说,在这种情况下,用顺序搜索法在长度为n的一维数组中查找值为x的元素,在平均情况下需要检查数组中一半的元素。
如果已知需要查找的x有一半的机会在数组中,此时q=l/2,则
这就是说,在这种情况下,用顺序搜索法在长度为n的一维数组中查找值为x的元素,在平均情况下需要检查数组中3/4的元素。
再考虑最坏情况分析。
在这个例子中,最坏情况发生在需要杳找的x是数组中的最后一个元素或x不在数细中的时候。此时显然有
在上述例子中,算法执行的工作量是与具体的输入有关的,A(n)只是它的加权平均值,而实际上对于某个特定的输入,其计算工作量未必是A(n),且A(n)也不一定等于w(n)。但在另外一些情况下,算法的计算工作量与输入无关,即当规模为n时,在所有可能的输入下,算法所执行的基本运算次数是一定的,此时有A(n)=w(n)。例如,两个n阶的矩阵相乘,都需要做n。次实数乘法,而与输入矩阵的具体元素无关。
2.算法的空间复杂度一个算法的空间复杂度,一般是指执行这个算法所需要的内存空间。
一个算法所占用的存储空间包括算法程序所占的空间、输入的初始数据所占的存储空间以及算法执行过程中所需要的额外空间。其中额外空间包括算法程序执行过程中的工作单元以及某种数据结构所需要的附加存储空间(例如,在链式结构中,除了要存储数据本身外,还需要存储链接信息)。如果额外空间量相对于问题规模来说是常数,则称该算法是原地(in place)工作的。在许多实际问题中,为了减少算法所占的存储空间,通常采用压缩存储技术,以便尽量减少不必要的额外空间。
1.2 数据结构的基本概念利用计算机进行数据处理是计算机应用的一个重要领域。在进行数据处理时,实际需要处理的数据元素一般有很多,而这些大量的数据元素都需要存放在计算机中,因此,大量的数据元素在计算机中如何组织,以便提高数据处理的效率,并且节省计算机的存储空间,这是进行数据处理的关键问题。
显然,杂乱无章的数据是不便于处理的。而将大量的数据随意地存放在计算机中,实际上也是“白找苦吃”,对数据处理更是不利。
数据结构作为计算机的一门学科,主要研究和讨论以下三个方面的问题:
①数据集合中各数据元素之间所固有的逻辑关系,即数据的逻辑结构;
②在对数据进行处理时,各数据元素在计算机中的存储关系,即数据的存储结构;
③对各种数据结构进行的运算。
讨论以上问题的主要目的是为了提高数据处理的效率。所谓提高数据处理的效率,主要包括两个方面:一是提高数据处理的速度,二是尽量节省在数据处理过程中所占用的计算机存储空间。
本章主要讨论工程上常用的一些基本数据结构,它们是软件设计的基础。
1.2.1什么是数据结构计算机已被广泛用于数据处理。实际问题中的各数据元素之间总是相互关联的。所谓数据处理,是指对数据集合中的各元素以各种方式进行运算,包括插入、删除、查找、更改等运算,也包括对数据元素进行分析。在数据处理领域中,建立数学模型有时并不十分重要,事实上,许多实际问题是无法表示成数学模型的。人们最感兴趣的是知道数据集合中各数据元素之间存在什么关系,应如何组织它们,即如何表示所需要处理的数据元素。
下面通过两个实例来说明对同一批数据用不同的表示方法后,对处理效率的影响。
例1.3无序表的顺序查找与有序表的对分查找:
图1.1是两个子表。从图中可以看出,在这两个子表中所存放的数据元素是相同的,但它们在表中存放的顺序是不同的。在图1.1(a)所示的表中,数据元素的存放顺序是没有规则的;而在图1.1(b)所示的表中,数据元素是按从小到大的顺序存放的。我们称前者为无序表,后者为有序表。
下面讨论在这两种表中进行查找的问题。
首先讨论在图1.1(a)所示的无序表中进行查找。由于在图1.1(a)表中数据元素的存放顺序没有一定的规则,因此,要在这个表中查找某个数时,只能从第一个元素开始,逐个将表中的元素与被查数进行比较,直到表中的某个元素与被查数相等(即查找成功)或者表中所有元素与被查数都进行了比较且都不相等(即查找失败)为止。这种查找方法称为顺序查找。显然,在顺序查找中,如果被查找数在表的前部,则需要比较的次数就少;但如果被查找数在表的后部,则需要比较的次数就多。特别是当被查找数刚好是表中的第一个元素时(如被查数为35),只需要比较一次就查找成功;但当被查数刚好是表中最后一个元素(如被查数为46)或表中根本就没有被查数时(如被查数为67),则需要与表中所有的元素进行比较,在这种情况下,当表很大时,顺序查找是很费时间的。虽然顺序查找法的效率比较低,但由于图1.1(a)为无序表,没有更好的查找方法,只能用顺序查找。
现在再讨论在图1.1(b)所示的有序表中进行查找。由于有序表中的元素是从小到大进行排列的,在查找时可以利用这个特点,以便使比较次数大大减少。在有序表中查找一个数可以如下进行:
将被查数与表中的中间元素进行比较:若相等,则表示查找成功,查找过程结束;若被查数大于表中的这个中间元素时,则表示如果被查数在表中的话,只能在表的后半部,此时可以抛弃表的前半部而保留后半部;若被查数小于表中的这个中间元素,则表示如果被查数在表中的话,只能在表的前半部,此时可以抛弃表的后半部而保留前半部。然后对剩下的部分(前半部或后半部)再按照上述方法进行查找,这个过程一直做到在某一次的比较中相等(查找成功)或剩下的部分已空(查找失败)为止。例如,如果要在图1.1(b)所示的有序表中查找54,则首先与中间元素35进行比较,由于54大于35,再与后半部分的中间元素54进行比较,此时相等,共比较了2次就查找成功。如果采用顺序查找法,在图1.1(a)所示的无序表中查找54这个元素,需要比较9次。这种查找方法称为有序表的对分查找。
显然,在有序表的对分查找中,不论查找的是什么数,也不论要查找的数在表中有没有,都不需要与表中所有的元素进行比较,只需要与表中很少的元素进行比较。但需要指出的是,对分查找只适用于有序表,而对于无序表是无法进行对分查找的。
实际上,在日常工作和学习中也经常遇到对分查找。例如,当需要在词典中查找一个单词时,一般不是从第一页开始一页一页的往后找,而是考虑到词典中的各单词是以英文字母为顺序排列的,因此可以根据所查单词的第一个字母,直接翻到大概的位置,然后进行比较,根据比较结果再向前或向后翻,直到找到该单词为止。这种在词典中查单词的方法类似于对分查找。
由这个例子可以看出,数据元素在表中的排列顺序对查找效率是有很大影响的。
例1.4设有一学生情况登记表如表1.1所示。在表1.1中,每个学生的情况是以学号为顺序排列的。
显然,如果要在表1.1中查找给定学号的某学生的情况是很方便的,只要根据给定的学号就可以立即找到该学生的情况。但是,如果要在该表中查找成绩在90分以上的所有学生的情况,则需要从头到尾扫描全表,才能将成绩在90分以上的所有学生找到。在这种情况下,为了找到成绩在90分以上的学生情况,对于成绩在90分以下的所有学生情况也都要被扫描到。由此可以看出,要在表1.1中查找给定学号的学生情况虽然很方便,但要查找成绩在某个分数段中的学生情况时,实际上需要查看表中所有学生的成绩,其效率是很低的,尤其是当表很大时更为突出。
表1.1学生情况登记表学 号
姓 名
性 别
年 龄
成 绩
学 号
姓 名
性 别
年 龄
成 绩
970156
张小明
男
20
86
970163
王 伟
男
20
65
970157
李小青
女
19
83
970164
胡 涛
男
19
95
970158
赵 凯
男
19
70
970165
周 敏
女
20
87
970159
李启明
男
21
91
970166
杨雪辉
男
22
89
970160
刘 华
女
18
78
970167
吕永华
男
18
61
970161
曾小波
女
19
90
970168
梅 玲
女
17
93
970162
张 军
男
18
80
970169
刘 健
男
20
75
为了便于查找成绩在某个分数段中的学生情况,可以将表1.1中所登记的学生情况进行重新组织。例如,将成绩在90分以上(包括90分,下同)、80~89分、70~79分、60~69分之间的学生情况分别登记在四个独立的子表中,分别如表1.2、表1.3、表1.4与表1.5所示。现在如果要查找90分以上的所有学生的情况,就可以直接在表1.2中进行查找,从而避免了对成绩在90分以下的学生情况进行扫描,提高了查找效率。
表1.2成绩在90分以上的学生情况登记表学 号
姓 名
性 别
年 龄
成 绩
学 号
姓 名
性 别
年 龄
成 绩
970159
李启明
男
21
91
970164
胡涛
男
19
95
970161
曾小波
女
19
90
970168
梅玲
女
17
93
表1.3成绩在80~89分之间的学生情况登记表学 号
姓 名
性 别
年 龄
成 绩
学 号
姓 名
性 别
年 龄
成 绩
970156
张小明
男
20
86
970165
周 敏
女
20
87
970157
李小青
女
19
83
970166
杨雪辉
男
22
89
970162
张 军
男
18
80
表1.4成绩在70~79分之间的学生情况登记表学 号
姓 名
性 别
年 龄
成 绩
学 号
姓 名
性 别
年 龄
成 绩
970158
赵 凯
男
19
70
970169
刘健
男
20
75
970160
刘 华
女
18
78
表1.5成绩在60~69分之间的学生情况登记表学 号
姓 名
性 别
年 龄
成 绩
学 号
姓 名
性 别
年 龄
成 绩
970163
王 伟
男
20
65
4970167
吕永华
男
18
61
由例1-4可以看出,在对数据进行处理时,可以根据所做的运算不同,将数据组织成不同的形式,以便于做该种运算,从而提高数据处理的效率。
简单地说,数据结构是指相互有关联的数据元素的集合。例如,向量和矩阵就是数据结构,在这两个数据结构中,数据元素之间有着位置上的关系。又如,图书馆中的图书卡片目录,则是一个较为复杂的数据结构,对于列在各卡片上的各种书之间,可能在主题、作者等问题上相互关联,甚至一本书本身也有不同的相关成分。
数据元素具有广泛的含义。一般来说,现实世界中客观存在的一切个体都可以是数据元素。例如:
描述一年四季的季节名春、夏、秋、冬可以作为季节的数据元素;
表示数值的各个数
18、11、35、23、16、…
可以作为数值的数据元素;
表示家庭成员的各成员名父亲、儿子、女儿可以作为家庭成员的数据元素。
甚至每一个客观存在的事件,如一次演出、一次借书、一次比赛等也可以作为数据元素。
总之,在数据处理领域中,每一个需要处理的对象都可以抽象成数据元素。数据元素一般简称为元素。
在实际应用中,被处理的数据元素一般有很多,而且,作为某种处理,其中的数据元素一般具有某种共同特征。例如,{春,夏,秋,冬)这四个数据元素有一个共同特征,即它们都是季节名,分别表示了一年中的四个季节,从而这四个数据元素构成了季节名的集合。又如,{父亲,儿子,女儿)这三个数据元素也有一个共同特征,即它们都是家庭的成员名,从而构成了家庭成员名的集合。一般来说,人们不会同时处理特征完全不同且互相之间没有任何关系的各类数据元素,对于具有不同特征的数据元素总是分别进行处理。
一般情况下,在具有相同特征的数据元素集合中,各个数据元素之间存在有某种关系(即联系),这种关系反映了该集合中的数据元素所固有的一种结构。在数据处理领域中,通常把数据元素之间这种固有的关系简单地用前后件关系(或直接前驱与直接后继关系)来描述。
例如,在考虑一年四个季节的顺序关系时,则“春”是“夏”的前件(即直接前驱,下同),而“夏”是“春”的后件(即直接后继,下同)。同样,“夏”是“秋”的前件,“秋”是“夏”的后件;“秋”是“冬”的前件,“冬”是“秋”的后件。
在考虑家庭成员间的辈分关系时,则“父亲”是“儿子”和“女儿”的前件,而“儿子”与“女儿”都是“父亲”的后件。
前后件关系是数据元素之间的一个基本关系,但前后件关系所表示的实际意义随具体对象的不同而不同。一般来说,数据元素之间的任何关系都可以用前后件关系来描述。
1.数据的逻辑结构前面提到,数据结构是指反映数据元素之间关系的数据元素集合的表示。更通俗地说,数据结构是指带有结构的数据元素的集合。在此,所谓结构实际上就是指数据元素之间的前后件关系。
由上所述,一个数据结构应包含以下两方面的信息:
①表示数据元素的信息;
②表示各数据元素之间的前后件关系。
在以上所述的数据结构中,其中数据元素之间的前后件关系是指它们的逻辑关系,而与它们在计算机中的存储位置无关。因此,上面所述的数据结构实际上是数据的逻辑结构。
所谓数据的逻辑结构,是指反映数据元素之间逻辑关系的数据结构。
由前面的叙述可以知道,数据的逻辑结构有两个要素:一是数据元素的集合,通常记为D;二是D上的关系,它反映了D中各数据元素之间的前后件关系,通常记为R。即一个数据结构可以表示成
B=(D,R)
其中B表示数据结构。为了反映D中各数据元素之间的前后件关系,一般用二元组来表示。例如,假设a与b是D中的两个数据,则二元组(a,b)表示a是b的前件,b是a的后件。这样,在D中的每两个元素之间的关系都可以用这种二元组来表示。
例1.5一年四季的数据结构可以表示成
例1.6家庭成员数据结构可以表示成
例1.7 n维向量
也是一种数据结构。即X=(D,R),其中数据元素的集合为
关系为
对于一些复杂的数据结构来说,它的数据元素可以是另一种数据结构。
例如,m×n的矩阵
是一个数据结构。在这个数据结构中,矩阵的每一行
可以看成是它的一个数据元素。即这个数据结构的数据元素的集合为
D上的一个关系为
显然,数据结构A中的每一个数据元素A(i=1,2,…,m)又是另一个数据结构,即数据元素的集合为
Di上的一个关系为
2.数据的存储结构数据处理是计算机应用的一个重要领域,在实际进行数据处理时,被处理的各数据元素总是被存放在计算机的存储空间中,并且,各数据元素在计算机存储空间中的位置关系与它们的逻辑关系不一定是相同的,而且一般也不可能相同。例如,在前面提到的一年四个季节的数据结构中,“春”是“夏”的前件,“夏”是“春”的后件,但在对它们进行处理时,在计算机存储空间中,“春”这个数据元素的信息不一定被存储在“夏”这个数据元素信息的前面,而可能在后面,也可能不是紧邻在前面,而是中间被其他的信息所隔开。又如,在家庭成员的数据结构中,“儿子”和“女儿”都是“父亲”的后件,但在计算机存储空间中,根本不可能将“儿子”和“女儿”这两个数据元素的信息都紧邻存放在“父亲”这个数据元素信息的后面,即在存储空间中与“父亲”紧邻的只可能是其中的一个。由此可以看出,一个数据结构中的各数据元素在计算机存储空间中的位置关系与逻辑关系是有可能不同的。
数据的逻辑结构在计算机存储空间中的存放形式称为数据的存储结构(也称数据的物理结构)。
由于数据元素在计算机存储空间中的位置关系可能与逻辑关系不同,因此,为了表示存放在计算机存储空间中的各数据元素之间的逻辑关系(即前后件关系),在数据的存储结构中,不仅要存放各数据元素的信息,还需要存放各数据元素之间的前后件关系的信息。
一般来说,一种数据的逻辑结构根据需要可以表示成多种存储结构,常用的存储结构有顺序、链接、索引等存储结构。而采用不同的存储结构,其数据处理的效率是不同的。因此,在进行数据处理时,选择合适的存储结构是很重要的。
1.2.2数据结构的图形表示一个数据结构除了用二元关系表示外,还可以直观地用图形表示。在数据结构的图形表示中,对于数据集合D中的每一个数据元素用中间标有元素值的方框表示,一般称之为数据结点,并简称为结点;为了进一步表示各数据元素之间的前后件关系,对于关系R中的每一个二元组,用一条有向线段从前件结点指向后件结点。
例如,一年四季的数据结构可以用如图1.2所示的图形来表示。
又如,反映家庭成员间辈分关系的数据结构可以用如图1-3所示的图形表示。
显然,用图形方式表示一个数据结构是很方便的,并且也比较直观。有时在不会引起误会的情况下,在前件结点到后件结点连线上的箭头可以省去。例如,在图1.3中,即使将“父亲”结点与“儿子”结点连线上的箭头以及“父亲”结点与“女儿”结点连线上的箭头都去掉,同样表示了“父亲”是“儿子”与“女儿”的前件,“儿子”与“女儿”均是“父亲”的后件,而不会引起误会。
例1.8用图形表示数据结构B=(D,R),其中
这个数据结构的图形表示如图1.4所示。
在数据结构中,没有前件的结点称为根结点;没有后件的结点称为终端结点(也称为叶子结点)。例如,在图1.2所示的数据结构中,元素“春”所在的结点(简称为结点“春”,下同)为根结点,结点“冬”为终端结点;在图l-3所示的数据结构中,结点“父亲”为根结点,结点“儿子”与“女儿”均为终端结点;在图1.4所示的数据结构中,有两个根结点d1与d2,有三个终端结点d6、d7、d5、出。数据结构中除了根结点与终端结点外的其他结点一般称为内部结点。
通常,一个数据结构中的元素结点可能是在动态变化的。根据需要或在处理过程中,可以在一个数据结构中增加一个新结点(称为插入运算),也可以删除数据结构中的某个结点(称为删除运算)。插入与删除是对数据结构的两种基本运算。除此之外,对数据结构的运算还有查找、分类、合并、分解、复制和修改等。在对数据结构的处理过程中,不仅数据结构中的结点(即数据元素)个数在动态地变化,而且,各数据元素之间的关系也有可能在动态地变化。例如,一个无序表可以通过排序处理而变成有序表;一个数据结构中的根结点被删除后,它的某一个后件可能就变成了根结点;在一个数据结构中的终端结点后插入一个新结点后,则原来的那个终端结点就不再是终端结点而成为内部结点了。有关数据结构的基本运算将在后面讲到具体数据结构时再介绍。
1.2.3线性结构与非线性结构如果在一个数据结构中一个数据元素都没有,则称该数据结构为空的数据结构。在一个空的数据结构中插入一个新的元素后就变为非空;在只有一个数据元素的数据结构中,将该元素删除后就变为空的数据结构。
根据数据结构中各数据元素之间前后件关系的复杂程度,一般将数据结构分为两大类型:线性结构与非线性结构。
如果一个非空的数据结构满足下列两个条件:
①有且只有一个根结点;
②每一个结点最多有一个前件,也最多有一个后件。
则称该数据结构为线性结构。线性结构又称线性表。
由此可以看出,在线性结构中,各数据元素之间的前后件关系是很简单的。如例1.5中的一年四季这个数据结构,以及例1.7中的n维向量数据结构,它们都属于线性结构。
特别需要说明的是,在一个线性结构中插入或删除任何一个结点后还应是线性结构。根据这一点,如果一个数据结构满足上述两个条件,但当在此数据结构中插入或删除任何一个结点后就不满足这两个条件了,则该数据结构不能称为线性结构。例如,图1.5所示的数据结构显然是满足上述两个条件的,但它不属于线性结构这个类型,因为如果在这个数据结构中删除结点A后,就不满足上述的条件(1)。
如果一个数据结构不是线性结构,则称之为非线性结构。如例1.6中反映家庭成员间辈分关系的数据结构,以及例1.8中的数据结构,它们都不是线性结构,而是属于非线性结构。显然,在非线性结构中,各数据元素之间的前后件关系要比线性结构复杂,因此,对非线性结构的存储与处理比线性结构要复杂得多。
线性结构与非线性结构都可以是空的数据结构。一个空的数据结构究竟是属于线性结构还是属于非线性结构,这要根据具体情况来确定。如果对该数据结构的运算是按线性结构的规则来处理的,则属于线性结构;否则属于非线性结构。
1.3线性表及其顺序存储结构
1.3.1线性表的基本概念线性表(Linear List)是最简单、最常用的一种数据结构。
线性表由一组数据元素构成。数据元素的含义很广泛,在不同的具体情况下,它可以有不同的含义。例如,一个n维向量(x1,x2,…,xn)是一个长度为n的线性表,其中的每一个分量就是一个数据元素。又如,英文小写字母表(a,b,c,…,z)是一个长度为26的线性表,其中的每一个小写字母就是一个数据元素。再如,一年中的四个季节(春,夏,秋,冬)是一个长度为4的线性表,其中的每一个季节名就是一个数据元素。
矩阵也是一个线性表,只不过它是一个比较复杂的线性表。在矩阵中,既可以把每一行看成是一个数据元素(即一个行向量为一个数据元素),也可以把每一列看成是一个数据元素(即一个列向量为一个数据元素)。其中每一个数据元素(一个行向量或一个列向量)实际上又是一个简单的线性表。
数据元素可以是简单项(如上述例子中的数、字母、季节名等)。在稍微复杂的线性表中,一个数据元素还可以由若干个数据项组成。例如,某班的学生情况登记表是一个复杂的线性表,表中每一个学生的情况就组成了线性表中的每一个元素,每一个数据元素包括姓名、学号、性别、年龄和健康状况5个数据项,如表1.6所示。在这种复杂的线性表中,由若干数据项组成的数据元素称为记录(record),而由多个记录构成的线性表又称为文件(file)。因此,上述学生情况登记表就是一个文件,其中每一个学生的情况就是一个记录。
表1.6学生情况登记表姓 名
学 号
性 别
年 龄
健康状况
王 强刘建平赵 军葛文华
…
800356
800357
800361
800367
…
男男女男
…
19
20
19
2l
…
良好一般良好较差
…
综上所述,线性表是由n(n≥0)个数据元素a1,a2,…,an组成的一个有限序列,表中的每一个数据元素,除了第一个外,有且只有一个前件,除了最后一个外,有且只有一个后件。即线性表或是一个空表,或可以表示为
其中ai(i=l,2,…,n)是属于数据对象的元素,通常也称其为线性表中的一个结点。
显然,线性表是一种线性结构。数据元素在线性表中的位置只取决于它们自己的序号,即数据元素之间的相对位置是线性的。
非空线性表有如下一些结构特征:
①有且只有一个根结点a1,它无前件;
②有且只有一个终端结点an,它无后件;
③除根结点与终端结点外,其他所有结点有且只有一个前件,也有且只有一个后件。线性表中结点的个数n称为线性表的长度。当n=0时,称为空表。
1.3.2线性表的顺序存储结构在计算机中存放线性表,一种最简单的方法是顺序存储,也称为顺序分配。
线性表的顺序存储结构具有以下两个基本特点:
①线性表中所有元素所占的存储空间是连续的;
②线性表中各数据元素在存储空间中是按逻辑顺序依次存放的。
由此可以看出,在线性表的顺序存储结构中,其前后件两个元素在存储空间中是紧邻的,且前件元素一定存储在后件元素的前面。
在线性表的顺序存储结构中,如果线性表中各数据元素所占的存储空间(字节数)相等,则要在该线性表中查找某一个元素是很方便的。
假设线性表中的第一个数据元素的存储地址(指第一个字节的地址,即首地址)为ADR(a1),每一个数据元素占k个字节,则线性表中第i个元素ai在计算机存储空间中的存储地址为
即在顺序存储结构中,线性表中每一个数据元素在计算机存储空间中的存储地址由该元素在线性表中的位置序号惟一确定。一般来说,长度为n的线性表
在计算机中的顺序存储结构如图1.6所示。
在程序设计语言中,通常定义一个一维数组来表示线性表的顺序存储空间。因为程序设计语言中的一维数组与计算机中实际的存储空间结构是类似的,这就便于用程序设计语言对线性表进行各种运算处理。
在用一维数组存放线性表时,该一维数组的长度通常要定义得比线性表的实际长度大一些,以便对线性表进行各种运算,特别是插入运算。在一般情况下,如果线性表的长度在处理过程中是动态变化的,则在开辟线性表的存储空间时要考虑到线性表在动态变化过程中可能达到的最大长度。如果开始时所开辟的存储空间太小,则在线性表动态增长时可能会出现存储空间不够而无法再插入新的元素;但如果开始时所开辟的存储空间太大,而实际上又用不着那么大的存储空间,则会造成存储空间的浪费。在实际应用中,可以根据线性表动态变化过程中的一般规模来决定开辟的存储空间量。
在线性表的顺序存储结构下,可以对线性表进行各种处理。主要的运算有以下几种:
①在线性表的指定位置处加入一个新的元素(即线性表的插入);
②在线性表中删除指定的元素(即线性表的删除);
③在线性表中查找某个(或某些)特定的元素(即线性表的查找);
④对线性表中的元素进行整序(即线性表的排序);
⑤按要求将一个线性表分解成多个线性表(即线性表的分解);
⑥按要求将多个线性表合并成一个线性表(即线性表的合并);
⑦复制一个线性表(即线性表的复制);
⑧逆转一个线性表(即线性表的逆转)等。
下面两小节主要讨论线性表在顺序存储结构下的插入与删除的问题。
1.3.3顺序表的插入运算首先举一个例子来说明如何在顺序存储结构的线性表中插入一个新元素。
例1.9 图1.7(a)为一个长度为8的线性表顺序存储在长度为10的存储空间中。现在要求在第2个元素(即18)之前插入一个新元素87。其插入过程如下:
首先从最后一个元素开始直到第2个元素,将其中的每一个元素均依次往后移动一个位置,然后将新元素87插入到第2个位置。
插入一个新元素后,线性表的长度变成了9,如图1.7(b)所示。
如果再要在线性表的第9个元素之前插入一个新元素14,则采用类似的方法:将第9个元素往后移动一个位置,然后将新元素插入到第9个位置。插入后,线性表的长度变成了10,如图1.7(c)所示。
现在,为线性表开辟的存储空间已经满了,不能再插入新的元素了。如果再要插入,则会造成称为“上溢”的错误。
一般来说,设长度为n的线性表为
现要在线性表的第i个元素ai之前插入一个新元素b,插入后得到长度为n+1的线性表为
则插入前后的两线性表中的元素满足如下关系
在一般情况下,要在第i(1≤i≤n)个元素之前插入一个新元素时,首先要从最后一个(即第n个)元素开始,直到第i个元素之间共n-i+1个元素依次向后移动一个位置,移动结束后,第i个位置就被空出,然后将新元素插入到第i项。插入结束后,线性表的长度就增加了1。
显然,在线性表采用顺序存储结构时,如果插入运算在线性表的末尾进行,即在第n个元素之后(可以认为是在第n+1个元素之前)插入新元素,则只要在表的末尾增加一个元素即可,不需要移动表中的元素;如果要在线性表的第1个元素之前插入一个新元素,则需要移动表中所有的元素。在一般情况下,如果插入运算在第i(1≤i≤n)个元素之前进行,则原来第i个元素之后(包括第i个元素)的所有元素都必须移动。在平均情况下,要插入一个新元素,需要移动表中一半的元素。因此,在线性表顺序存储的情况下,要插入一个新元素,其效率是很低的,特别是在线性表比较大的情况下更为突出,由于数据元素的移动而消耗较多的处理时间。
1.3.4顺序表的删除运算首先举一个例子来说明如何在顺序存储结构的线性表中删除一个元素。
例1.10图1.8(a)为一个长度为8的线性表顺序存储在长度为10的存储空间中。现在要求删除线性表中的第1个元素(即删除元素29)。其删除过程如下:
从第2个元素开始直到最后一个元素,将其中的每一个元素均依次往前移动一个位置。此时,线性表的长度变成了7,如图1.8(b)所示。
如果再要删除线性表中的第6个元素,则采用类似的方法:将第7个元素往前移动一个位置。此时,线性表的长度变成了6,如图1.8(c)所示。
一般来说,设长度为n的线性表为
现要删除第i个元素,删除后得到长度为n-1的线性表为
则删除前后的两线性表中的元素满足如下关系:
在一般情况下,要删除第i(1≤i≤n)个元素时,则要从第i+1个元素开始,直到第n个元素之间共n-i个元素依次向前移动一个位置。删除结束后,线性表的长度就减小了1。
显然,在线性表采用顺序存储结构时,如果删除运算在线性表的末尾进行,即删除第n个元素,则不需要移动表中的元素;如果要删除线性表中的第1个元素,则需要移动表中所有的元素。在一般情况下,如果要删除第i(1≤i≤n)个元素,则原来第i个元素之后的所有元素都必须依次往前移动一个位置。在平均情况下,要在线性表中删除一个元素,需要移动表中一半的元素。因此,在线性表顺序存储的情况下,要删除一个元素,其效率也是很低的,特别是在线性表比较大的情况下更为突出,由于数据元素的移动而消耗较多的处理时间。
由线性表在顺序存储结构下的插入与删除运算可以看出,线性表的顺序存储结构对于小线性表或者其中元素不常变动的线性表来说是合适的,因为顺序存储的结构比较简单。但这种顺序存储的方式对于元素经常需要变动的大线性表就不太合适了,因为插入与删除的效率比较低。
1.4 栈和队列
1.4.1栈及其基本运算
1.什么是栈栈实际上也是线性表,只不过是一种特殊的线性表。在这种特殊的线性表中,其插入与删除运算都只在线性表的一端进行。即在这种线性表的结构中,一端是封闭的,不允许进行插入与删除元素;另一端是开口的,允许插入与删除元素。在顺序存储结构下,对这种类型线性表的插入与删除运算是不需要移动表中其他数据元素的。这种线性表称为栈。
栈(stack)是限定在一端进行插入与删除的线性表。
在栈中,允许插入与删除的一端称为栈顶,而不允许插入与删除的另一端称为栈底。栈顶元素总是最后被插入的元素,从而也是最先能被删除的元素;栈底元素总是最先被插入的元素,从而也是最后才能被删除的元素。即栈是按照“先进后出”(FILO-First In Last Out)或“后进先出”(LIFO—Last In First Out)的原则组织数据的,因此,栈也被称为“先进后出”表或“后进先出”表。由此可以看出,栈具有记忆作用。
通常用指针top来指示栈顶的位置,用指针bottom指向栈底。
往栈中插入一个元素称为入栈运算,从栈中删除一个元素(即删除栈顶元素)称为退栈运算。栈顶指针top动态反映了栈中元素的变化情况。
图1.9是栈的示意图。
栈这种数据结构在日常生活中也是常见的。例如,子弹夹是一种栈的结构,最后压入的子弹总是最先被弹出,而最先压入的子弹最后才能被弹出。又如,在用一端为封闭另一端为开口的容器装物品时,也是遵循“先进后出”或“后进先出”原则的。
2.栈的顺序存储及其运算
与一般的线性表一样,在程序设计语言中,用一维数组S(1:m)作为栈的顺序存储空间,其中m为栈的最大容量。通常,栈底指针指向栈空间的低地址一端(即数组的起始地址这一端)。图1.10(a)是容量为10的栈顺序存储空间,栈中已有6个元素;图1.10(b)与图1.10(c)分别为入栈与退栈后的状态。
在栈的顺序存储空间S(1:m)中,S(bottom)通常为栈底元素(在栈非空的情况下),s(top)为栈顶元素。top=0表示栈空;top=m表示栈满。
栈的基本运算有三种:入栈、退栈与读栈顶元素。下面分别介绍在顺序存储结构下栈的这三种运算。
(1)入栈运算入栈运算是指在栈顶位置插入一个新元素。这个运算有两个基本操作:首先将栈顶指针进一(即top加1),然后将新元素插入到栈顶指针指向的位置。
当栈顶指针已经指向存储空间的最后一个位置时,说明栈空间已满,不可能再进行入栈操作。这种情况称为栈“上溢”错误。
(2)退栈运算退栈运算是指取出栈顶元素并赋给一个指定的变量。这个运算有两个基本操作:首先将栈顶元素(栈顶指针指向的元素)赋给一个指定的变量,然后将栈顶指针退一(即top减1)。
当栈顶指针为0时,说明栈空,不可能进行退栈操作。这种情况称为栈“下溢”错误。
(3)读栈顶元素读栈顶元素是指将栈顶元素赋给一个指定的变量。必须注意,这个运算不删除栈项元素,只是将它的值赋给一个变量,因此,在这个运算中,栈项指针不会改变。
当栈顶指针为0时,说明栈空,读不到栈顶元素。
1.4.2队列及其基本运算
1.什么是队列在计算机系统中,如果一次只能执行一个用户程序,则在多个用户程序需要执行时,这些用户程序必须先按照到来的顺序进行排队等待。这通常是由计算机操作系统来进行管理的。
在操作系统中,用一个线性表来组织管理用户程序的排队执行,原则是:
①初始时线性表为空;
②当有用户程序来到时,将该用户程序加入到线性表的末尾进行等待;
③当计算机系统执行完当前的用户程序后,就从线性表的头部取出一个用户程序执行。
由这个例子可以看出,在这种线性表中,需要加入的元素总是插入到线性表的末尾,并且又总是从线性表的头部取出(删除)元素。这种线性表称为队列。
队列(queue)是指允许在一端进行插入、而在另~端进行删除的线性表。允许插入的一端称为队尾,通常用一个称为尾指针(rear)的指针指向队尾元素,即尾指针总是指向最后被插入的元素;允许删除的一端称为排头(也称为队头),通常也用一个排头指针(front)指向排头元素的前一个位置。显然,在队列这种数据结构中,最先插入的元素将最先能够被删除,反之,最后插入的元素将最后才能被删除。因此,队列又称为“先进先出”(FIFO——First In First Out)或“后进后出”(LILO——Last In Last Out)的线性表,它体现了“先来先服务”的原则。在队列中,队尾指针rear与排头指针front共同反映了队列中元素动态变化的情况。图1.11是具有6个元素的队列示意图。
往队列的队尾插入一个元素称为入队运算,从队列的排头删除一个元素称为退队运算。
图1.12是在队列中进行插入与删除的示意图。由图1.12可以看出,在队列的末尾插入一个元素(入队运算)只涉及队尾指针rear的变化,而要删除队列中的排头元素(退队运算)只涉及排头指针front的变化。
与栈类似,在程序设计语言中,用一维数组作为队列的顺序存储空间。
2.循环队列及其运算在实际应用中,队列的顺序存储结构一般采用循环队列的形式。
所谓循环队列,就是将队列存储空间的最后一个位置绕到第一个位置,形成逻辑上的环状空间,供队列循环使用,如图1.13所示。在循环队列结构中,当存储空间的最后一个位置已被使用而再要进行入队运算时,只要存储空间的第一个位置空闲,便可将元素加入到第一个位置,即将存储空间的第一个位置作为队尾。
在循环队列中,用队尾指针real指向队列中的队尾元素,用排头指针front指向排头元素的前一个位置,因此,从排头指针front指向的后一个位置直到队尾指针rear指向的位置之间所有的元素均为队列中的元素。
循环队列的初始状态为空,即rear=front=m,如图1.13所示。
循环队列主要有两种基本运算:入队运算与退队运算。
每进行一次入队运算,队尾指针就进一。当队尾指针rear=m+1时,则置rear=l。
每进行一次退队运算,排头指针就进一。当排头指针front=m+1时,则置front=l。
图1.14(a)是一个容量为8的循环队列存储空间,且其中已有6个元素。图1.14(b)是在图1.14(a)的循环队列中又加入了2个元素后的状态。图1.14(c)是在1.14(b)的循环队列中退出了1个元素后的状态。
由图1.14中循环队列动态变化的过程可以看出,当循环队列满时有front=rear,而当循环队列空时也有front=rear。即在循环队列中,当front=rear时,不能确定是队列满还是队列空。在实际使用循环队列时,为了能区分队列满还是队列空,通常还需增加一个标志s,s值的定义如下:
由此可以得出队列空与队列满的条件如下:
队列空的条件为s=0;
队列满的条件为s=l且front=rear。
下面具体介绍循环队列入队与退队的运算。
假设循环队列的初始状态为空,即:s=0,且front=rear=m。
(1)入队运算入队运算是指在循环队列的队尾加入一个新元素。这个运算有两个基本操作:首先将队尾指针进一(即rear=rear+1),并当rear=m+l时置rear=1;然后将新元素插入到队尾指针指向的位置。
当循环队列非空(s=1)且队尾指针等于排头指针时,说明循环队列已满,不能进行入队运算,这种情况称为“上溢”。
(2)退队运算退队运算是指在循环队列的排头位置退出一个元素并赋给指定的变量。这个运算有两个基本操作:首先将排头指针进一(即front=front+1),并当front=m+l时置front=l;然后将排头指针指向的元素赋给指定的变量。
当循环队列为空(s=0)时,不能进行退队运算,这种情况称为“下溢”。
1.5 线性链表
1.5.1线性链表的基本概念前面主要讨论了线性表的顺序存储结构以及在顺序存储结构下的运算。线性表的顺序存储结构具有简单、运算方便等优点,特别是对于小线性表或长度固定的线性表,采用顺序存储结构的优越性更为突出。
但是,线性表的顺序存储结构在某些情况下就显得不那么方便,运算效率不那么高。实际上,线性表的顺序存储结构存在以下几方面的缺点:
①在一般情况下,要在顺序存储的线性表中插入一个新元素或删除一个元素时,为了保证插入或删除后的线性表仍然为顺序存储,则在插入或删除过程中需要移动大量的数据元素。在平均情况下,为了在顺序存储的线性表中插入或删除一个元素,需要移动线性表中约一半的元素;在最坏情况下,则需要移动线性表中所有的元素。因此,对于大的线性表,特别是元素的插入或删除很频繁的情况下,采用顺序存储结构是很不方便的,插入与删除运算的效率都很低。
②当为一个线性表分配顺序存储空间后,如果出现线性表的存储空间已满,但还需要插入新的元素时,就会发生“上溢”错误。在这种情况下,如果在原线性表的存储空间后找不到与之连续的可用空间,则会导致运算的失败或中断。显然,这种情况的出现对运算是很不利的。也就是说,在顺序存储结构下,线性表的存储空间不便于扩充。
③在实际应用中,往往是同时有多个线性表共享计算机的存储空间,例如,在一个处理中,可能要用到若干个线性表(包括栈与队列)。在这种情况下,存储空间的分配将是一个难题。如果将存储空间平均分配给各线性表,则有可能造成有的线性表的空间不够用,而有的线性表的空间根本用不着或用不满,这就使得在有的线性表空间无用而处于空闲的情况下,另外一些线性表的操作由于“上溢”而无法进行。这种情况实际上是计算机的存储空间得不到充分利用。如果多个线性表共享存储空间,对每一个线性表的存储空间进行动态分配,则为了保证每一个线性表的存储空间连续且顺序分配,会导致在对某个线性表进行动态分配存储空间时,必须要移动其他线性表中的数据元素。这就是说,线性表的顺序存储结构不便于对存储空间的动态分配。
由于线性表的顺序存储结构存在以上这些缺点,因此,对于大的线性表,特别是元素变动频繁的大线性表不宜采用顺序存储结构,而是采用下面要介绍的链式存储结构。
假设数据结构中的每一个数据结点对应于一个存储单元,这种存储单元称为存储结点,简称结点。
在链式存储方式中,要求每个结点由两部分组成:一部分用于存放数据元素值,称为数据域;另一部分用于存放指针,称为指针域。其中指针用于指向该结点的前一个或后一个结点(即前件或后件)。
在链式存储结构中,存储数据结构的存储空间可以不连续,各数据结点的存储顺序与数据元素之间的逻辑关系可以不一致,而数据元素之间的逻辑关系是由指针域来确定的。
链式存储方式既可用于表示线性结构,也可用于表示非线性结构。在用链式结构表示较复杂的非线性结构时,其指针域的个数要多一些。
1.线性链表线性表的链式存储结构称为线性链表。
为了适应线性表的链式存储结构,计算机存储空间被划分为一个一个小块,每一小块占若干字节,通常称这些小块为存储结点。
为了存储线性表中的每一个元素,一方面要存储数据元素的值,另一方面要存储各数据元素之间的前后件关系。为此目的,将存储空间中的每一个存储结点分为两部分:一部分用于存储数据元素的值,称为数据域;另一部分用于存放下一个数据元素的存储序号(即存储结点的地址),即指向后件结点,称为指针域。由此可知,在线性链表中,存储空间的结构如图1.15所示。
在线性链表中,用一个专门的指针HEAD指向线性链表中第一个数据元素的结点(即存放线性表中第一个数据元素的存储结点的序号)。线性表中最后一个元素没有后件,因此,线性链表中最后一个结点的指针域为空(用NULL或0表示),表示链表终止。线性链表的逻辑结构如图1.17所示。
线性链表中存储结点的结构如图1.16所示。
下面举一个例子来说明线性链表的存储结构。
设线性表为(a1,a2,a3,a4,a5),存储空间具有10个存储结点,该线性表在存储空间中的存储情况如图1.18(a)所示。为了直观地表示该线性链表中各元素之间的前后件关系,还可以用如图1.18(b)所示的逻辑状态来表示,其中每一个结点上面的数字表示该结点的存储序号(简称结点号)。
一般来说,在线性表的链式存储结构中,各数据结点的存储序号是不连续的,并且各结点在存储空间中的位置关系与逻辑关系也不一致。在线性链表中,各数据元素之间的前后件关系是由各结点的指针域来指示的,指向线性表中第一个结点的指针HEAD称为头指针,当HEAD=NULL(或0)时称为空表。
对于线性链表,可以从头指针开始,沿各结点的指针扫描到链表中的所有结点。下面的算法是从头指针开始,依次输出各结点值。
上面讨论的线性链表又称为线性单链表。在这种链表中,每一个结点只有一个指针域,由这个指针只能找到后件结点,但不能找到前件结点。因此,在这种线性链表中,只能顺指针向链尾方向进行扫描,这对于某些问题的处理会带来不便,因为在这种链接方式下,由某一个结点出发,只能找到它的后件,而为了找出它的前件,必须从头指针开始重新寻找。
为了弥补线性单链表的这个缺点,在某些应用中,对线性链表中的每个结点设置两个指针,一个称为左指针(Llink),用以指向其前件结点;另一个称为右指针(Rlink),用以指向其后件结点。这样的线性链幕称为双向链表,其逻辑状态如图1.19所示。
2.带链的栈栈也是线性表,也可以采用链式存储结构。图1.20是栈在链式存储时的逻辑状态示意图。
在实际应用中,带链的栈可以用来收集计算机存储空间中所有空闲的存储结点,这种带链的栈称为可利用栈。由于可利用栈链接了计算机存储空间中所有的空闲结点,因此,当计算机系统或用户程序需要存储结点时,就可以从中取出栈项结点,如图1.21(b)所示;当计算机系统或用户程序释放一个存储结点(该元素从表中删除)时,则要将该结点放回到可利用栈的栈顶,如图1.21(a)所示。由此可知,计算机中的所有可利用空间都可以以结点为单位链接在可利用栈中。随着其他线性链表中结点的插入与删除,可利用栈处于动态变化之中,即可利用栈经常要进行退栈与入栈操作。
3.带链的队列与栈类似,队列也是线性表,也可以采用链式存储结构。图1.22(a)是队列在链式存储时的逻辑状态示意图。图1.22(b)是将新结点p插入队列的示意图。图1.22(c)是将排头结点p退出队列的示意图。
1.5.2线性链表的基本运算线性链表的运算主要有以下几个:
①在线性链表中包含指定元素的结点之前插入一个新元素。
②在线性链表中删除包含指定元素的结点。
③将两个线性链表按要求合并成一个线性链表。
④将一个线性链表按要求进行分解。
⑤逆转线性链表。
⑥复制线性链表。
⑦线性链表的排序。
⑧线性链表的查找。
本小节主要讨论线性链表的插入与删除。
1.在线性链表中查找指定元素在对线性链表进行插入或删除的运算中,总是首先需要找到插入或删除的位置,这就需要对线性链表进行扫描查找,在线性链表中寻找包含指定元素值的前一个结点。当找到包含指定元素的前一个结点后,就可以在该结点后插入新结点或删除该结点后的一个结点。
在非空线性链表中寻找包含指定元素值x的前一个结点p的基本方法如下:
从头指针指向的结点开始往后沿指针进行扫描,直到后面已没有结点或下一个结点的数据域为x为止。因此,由这种方法找到的结点p有两种可能:当线性链表中存在包含元素x的结点时,则找到的p为第一次遇到的包含元素x的前一个结点序号;当线性链表中不存在包含元素x的结点时,则找到的p为线性链表中的最后一个结点号。
2.线性链表的插入线性链表的插入是指在链式存储结构下的线性表中插入一个新元素。
为了要在线性链表中插入一个新元素,首先要给该元素分配一个新结点,以便用于存储该元素的值。新结点可以从可利用栈中取得。然后将存放新元素值的结点链接到线性链表中指定的位置。
假设可利用栈与线性链表如图1.23(a)所示。现在要在线性链表中包含元素x的结点之前插入一个新元素b。其插入过程如下:
(1)从可利用栈取得一个结点,设该结点号为p(即取得结点的存储序号存放在变量p中),并置结点p的数据域为插入的元素值b。经过这一步后,可利用栈的状态如图1.23(b)所示。
(2)在线性链表中寻找包含元素x的前一个结点,设该结点的存储序号为q。线性链表如图1.23(b)所示。
(3)最后将结点p插入到结点q之后。为了实现这一步,只要改变以下两个结点的指针域内容:
①使结点p指向包含元素x的结点(即结点q的后件结点)。
②使结点q的指针域内容改为指向结点p。
这一步的结果如图1.23(c)所示。此时插入就完成。
由线性链表的插入过程可以看出,由于插入的新结点取自于可利用栈,因此,只要可利用栈不空,在线性链表插入时总能取到存储插入元素的新结点,不会发生“上溢”的情况。而且,由于可利用栈是公用的,多个线性链表可以共享它,从而很方便地实现了存储空间的动态分配。另外,线性链表在插入过程中不发生数据元素移动的现象,只需改变有关结点的指针即可,从而提高了插入的效率。
3.线性链表的删除线性链表的删除是指在链式存储结构下的线性表中删除包含指定元素的结点。
为了在线性链表中删除包含指定元素的结点,首先要在线性链表中找到这个结点,然后将要删除结点放回到可利用栈。
假设可利用栈与线性链表如图1.24(a)所示。现在要在线性链表中删除包含元素x的结点,其删除过程如下:
(1)在线性链表中寻找包含元素x的前一个结点,设该结点序号为q。
(2)将结点q后的结点p从线性链表中删除,即让结点q的指针指向包含元素x的结点p的指针指向的结点。
经过上述两步后,线性链表如图1.24(b)所示。
(3)将包含元素x的结点p送回可利用栈。经过这一步后,可利用栈的状态如图1.24(c)所示。此时,线性链表的删除运算完成。
从线性链表的删除过程可以看出,在线性链表中删除一个元素后,不需要移动表的数据元素,只需改变被删除元素所在结点的前一个结点的指针域即可。另外,由于可利用栈是用于收集计算机中所有的空闲结点,因此,当从线性链表中删除一个元素后,该元素的存储结点就变为空闲,应将该空闲结点送回到可利用栈。
1.5.3循环链表及其基本运算前面所讨论的线性链表中,其插入与删除的运算虽然比较方便,但还存在一个问题,在运算过程中对于空表和对第一个结点的处理必须单独考虑,使空表与非空表的运算不统一。为了克服线性链表的这个缺点,可以采用另一种链接方式,即循环链表(Circular Linked List)的结构。
循环链表的结构与前面所讨论的线性链表相比,具有以下两个特点:
(1)在循环链表中增加了一个表头结点,其数据域为任意或者根据需要来设置,指针域指向线性表的第一个元素的结点。循环链表的头指针指向表头结点。
(2)循环链表中最后一个结点的指针域不是空,而是指向表头结点。即在循环链表中,所有结点的指针构成了一个环状链。
图1.25是循环链表的示意图。其中图1.25(a)是一个非空的循环链表,图1.25(b)是一个空的循环链表。在此,所谓的空表与非空表是针对线性表中的元素而言。
在循环链表中,只要指出表中任何一个结点的位置,就可以从它出发访问到表中其他所有的结点,而线性单链表做不到这一点。
另外,由于在循环链表中设置了一个表头结点,因此,在任何情况下,循环链表中至少有一个结点存在,从而使空表与非空表的运算统一。
循环链表的插入和删除的方法与线性单链表基本相同。但由循环链表的特点可以看出,在对循环链表进行插入和删除的过程中,实现了空表与非空表的运算统一。
1.6 树与二叉树
1.6.1树的基本概念树(tree)是一种简单的非线性结构。在树这种数据结构中,所有数据元素之间的关系具有明显的层次特性。图1.26表示了一棵一般的树。由图1.26可以看出,在用图形表示树这种数据结构时,很像自然界中的树,只不过是一棵倒长的树,因此,这种数据结构就用“树”来命名。
在树的图形表示中,总是认为在用直线连起来的两端结点中,上端结点是前件,下端结点是后件,这样,表示前后件关系的箭头就可以省略。
在现实世界中,能用树这种数据结构表示的例子有很多。例如,图1.27中的树表示了学校行政关系结构;图1.28中的树反映了一本书的层次结构。由于树具有明显的层次关系,因此,具有层次关系的数据都可以用树这种数据结构来描述。在所有的层次关系中,人们最熟悉的是血缘关系,按血缘关系可以很直观地理解树结构中各数据元素结点之间的关系,因此,在描述树结构时,也经常使用血缘关系中的一些述语。
下面介绍树这种数据结构中的一些基本特征,同时介绍有关树结构的基本术语。
在树结构中,每一个结点只有一个前件,称为父结点,没有前件的结点只有一个,称为树的根结点,简称为树的根。例如,在图1.26中,结点R是树的根结点。
在树结构中,每一个结点可以有多个后件,它们都称为该结点的子结点。没有后件的结点称为叶子结点。例如,在图1.26中,结点C、M、F、E、X、G、S、L、Z、A均为叶子结点。
在树结构中,一个结点所拥有的后件个数称为该结点的度。例如,在图1.26中,根结点R的度为4;结点T的度为3;结点K、B、N、H的度为2;结点P、Q、D、O、Y、W的度为l。叶子结点的度为0。在树中,所有结点中的最大的度称为树的度。例如,图1.26所示的树的度为4。
前面已经说过,树结构具有明显的层次关系,即树是一种层次结构。在树结构中,_般按如下原则分层:
根结点在第1层。
同一层上所有结点的所有子结点都在下一层。例如,在图1.26中,根结点R在第1层;结点K、P、Q、D在第2层;结点B、E、N、O、T在第3层;结点C、H、X、Y、S、w、z、A在第4层;结点M、F、G、L在第5层。
树的最大层次称为树的深度。例如,图1.26所示的树的深度为5。
在树中,以某结点的一个子结点为根构成的树称为该结点的一棵子树。例如,在图1.26中:结点R有4棵子树,它们分别以K、P、Q、D为根结点;结点P有1棵子树,其根结点为N;结点T有3棵子树,它们分别以W、Z、A为根结点。
在树中,叶子结点没有子树。
在计算机中,可以用树结构来表示算术表达式。
在一个算术表达式中,有运算符和运算对象。一个运算符可以有若干个运算对象。例如,取正(+)与取负(-)运算符只有一个运算对象,称为单目运算符;加(+)、减(-)、乘(*)、除(/)、乘幂(**)运算符有两个运算对象,称为双目运算符;三元函数f(x,y,z)中的f为函数运算符,它有三个运算对象,称为三目运算符。一般来说,多元函数运算符有多个运算对象,称为多目运算符。算术表达式中的一个运算对象可以是子表达式,也可以是单变量(或单变数)。例如,在表达式a*b+c中,运算符“+”有两个运算对象,其中a*b为子表达式,c为单变量;而在子表达式a*b中,运算符“*”有两个运算对象a和b,它们都是单变量。
用树来表示算术表达式的原则如下:
①表达式中的每一个运算符在树中对应一个结点,称为运算符结点。
②运算符的每一个运算对象在树中为该运算符结点的子树(在树中的顺序为从左到右)。
③运算对象中的单变量均为叶子结点。
根据以上原则,可以将表达式
a*(b+c/d)+e*h-g*f(s,t,x+y)
用如图1.29所示的树来表示。表示表达式的树通常称为表达式树。由图1.29可以看出,表示一个表达式的表达式树是不惟一的,如上述表达式可以表示成如图1.29(a)和图1.29(b)两种表达式树。
树在计算机中通常用多重链表表示。多重链表中的每个结点描述了树中对应结点的信息,而每个结点中的链域(即指针域)个数将随树中该结点的度而定,其一般结构如图2.30所示。
在表示树的多重链表中,由于树中每个结点的度一般是不同的,因此,多重链表中各结点的链域个数也就不同,这将导致对树进行处理的算法很复杂。如果用定长的结点来表示树中的每个结点,即取树的度作为每个结点的链域个数,这就可以使对树的各种处理算法大大简化。但在这种情况下,容易造成存储空间的浪费,因为有可能在很多结点中存在空链域。后面将介绍用二叉树来表示一般的树,会给处理带来方便。
1.6.2二叉树及其基本性质
1.什么是二叉树二叉树(binary tree)是一种很有用的非线性结构。二叉树不同于前面介绍的树结构,但它与树结构很相似,并且,树结构的所有术语都可以用到二叉树这种数据结构上。
二叉树具有以下两个特点:
①非空二叉树只有一个根结点;
②每一个结点最多有两棵子树,且分别称为该结点的左子树与右子树。
由以上特点可以看出,在二叉树中,每一个结点的度最大为2,即所有子树(左子树或右子树)也均为二叉树,而树结构中的每一个结点的度可以是任意的。另外,二叉树中的每一个结点的子树被明显地分为左子树与右子树。在二叉树中,一个结点可以只有左子树而没有右子树,也可以只有右子树而没有左子树。当一个结点既没有左子树也没有右子树时,该结点即是叶子结点。
图1.31(a)是一棵只有根结点的二叉树,图1.31(b)是一棵深度为4的二叉树。
2.二叉树的基本性质二叉树具有以下几个性质:
性质1在二叉树的第k层上,最多有2k-1(k≥1)个结点。
根据二叉树的特点,这个性质是显然的。
性质2深度为m的二叉树最多有2m-1个结点。
深度为m的二叉树是指二叉树共有m层。
根据性质l,只要将第1层到第m层上的最大的结点数相加,就可以得到整个二叉树中结点数的最大值,即
21-1+22-1+…+2m-1=2m-1
性质3在任意一棵二叉树中,度为0的结点(即叶子结点)总是比度为2的结点多一个。
对于这个性质说明如下:
假设二叉树中有n0个叶子结点,n1个度为1的结点,n2个度为2的结点,则二叉树中总的结点数为
n=n0+nl+n2 (1)
由于在二叉树中除了根结点外,其余每一个结点都有惟一的一个分支进入。设二叉树中所有进入分支的总数为m,则二叉树中总的结点数为
n=m+1 (2)
又由于二叉树中这m个进入分支是分别由非叶子结点射出的。其中度为l的每个结点射出1个分支,度为2的每个结点射出2个分支。因此,二叉树中所有度为1与度为2的结点射出的分支总数为n1+2n2。而在二叉树中,总的射出分支数应与总的进入分支数相等,即
m=n1+2n2 (3)
将(3)代入(2)式有
n=n1+2n2+1 (4)
最后比较(1)式和(4)式有
n0+n1+n2=n1+2n2+1
化简后得
n0=n2+1
即:在二叉树中,度为0的结点(即叶子结点)总是比度为2的结点多一个。
例如,在图1.31(b)所示的二叉树中,有3个叶子结点,有2个度为2的结点,度为0的结点比度为2的结点多一个。
性质4具有n个结点的二叉树,其深度至少为[1og2n]+1,其中[log2n]表示取log2n的整数部分。
这个性质可以由性质2直接得到。
3.满二叉树与完全二叉树满二叉树与完全二叉树是两种特殊形态的二叉树。
(1)满二叉树所谓满二叉树是指这样的一种二叉树:除最后一层外,每一层上的所有结点都有两个子结点。这就是说,在满二叉树中,每一层上的结点数都达到最大值,即在满二叉树的第k层上有2k-1个结点,且深度为m的满二叉树有2m-1个结点。
图1.32(a)、1.32(b)、1.32(c)分别是深度为2、3、4的满二叉树。
(2)完全二叉树所谓完全二叉树是指这样的二叉树:除最后一层外,每一层上的结点数均达到最大值;在最后一层上只缺少右边的若干结点。
更确切地说,如果从根结点起,对二叉树的结点自上而下、自左至右用自然数进行连续编号,则深度为m、且有n个结点的二叉树,当且仅当其每一个结点都与深度为m的满二叉树中编号从1到n的结点一一对应时,称之为完全二叉树。
图1.33(a)、1.33(b)分别是深度为3、4的完全二叉树。
对于完全二叉树来说,叶子结点只可能在层次最大的两层上出现;对于任何一个结点,若其右分支下的子孙结点的最大层次为p,则其左分支下的子孙结点的最大层次或为p,或为p+l。
由满二叉树与完全二叉树的特点可以看出,满二叉树也是完全二叉树,而完全二叉树一般不是满二叉树。
完全二叉树还具有以下两个性质:
性质5具有n个结点的完全二叉树的深度为[10g2n]+1。
性质6设完全二叉树共有n个结点。如果从根结点开始,按层序(每一层从左到右)用自然数l,2,…,n给结点进行编号,则对于编号为k(k=1,2,…,n)的结点有以下结论:
①若k=l,则该结点为根结点,它没有父结点;若k>1,则该结点的父结点编号为INT(k/2)。
②若2k≤n,则编号为k的结点的左子结点编号为2k;否则该结点无左子结点(显然也没有右子结点)。
③若2k+l≤n,则编号为k的结点的右子结点编号为2k+l;否则该结点无右子结点。
根据完全二叉树的这个性质,如果按从上到下、从左到右顺序存储完全二叉树的各结点,则很容易确定每一个结点的父结点、左子结点和右子结点的位置。
1.6.3二叉树的存储结构在计算机中,二叉树通常采用链式存储结构。
与线性链表类似,用于存储二叉树中各元素的存储结点也由两部分组成:数据域与指针域。但在二叉树中,由于每一个元素可以有两个后件(即两个子结点),因此,用于存储二叉树的存储结点的指针域有两个:一个用于指向该结点的左子结点的存储地址,称为左指针域;另一个用于指向该结点的右子结点的存储地址,称为右指针域。图1.34为二叉树存储结点的示意图。其中:L(i)为结点i的左指针域,即L(i)为结点i的左予结点的存储地址;R(i)为结点i的右指针域,即R(i)为结点i的右子结点的存储地址;V(i)为数据域。
由于二叉树的存储结构中每一个存储结点有两个指针域,因此,二叉树的链式存储结构也称为二叉链表。图1.35(a)、1.35(b)、1.35(c)分别表示了一棵二叉树、二叉链表的逻辑状态、二叉链表的物理状态。其中BT称为二叉链表的头指针,用于指向二叉树根结点(即存放二叉树根结点的存储地址)。
对于满二叉树与完全二叉树来说,根据完全二叉树的性质6,可以按层序进行顺序存储,这样,不仅节省了存储空间,又能方便地确定每一个结点的父结点与左右子结点的位置,但顺序存储结构对于一般的二叉树不适用。
1.6.4二叉树的遍历二叉树的遍历是指不重复地访问二叉树中的所有结点。
由于二又树是一种非线性结构,因此,对二叉树的遍历要比遍历线性表复杂得多。在遍历二叉树的过程中,当访问到某个结点时,再往下访问可能有两个分支,那么先访问哪一个分支呢?对于二叉树来说,需要访问根结点、左子树上的所有结点、右子树上的所有结点,在这三者中,究竟先访问哪一个?也就是说,遍历二叉树的方法实际上是要确定访问各结点的顺序,以便不重不漏地访问到二叉树中的所有结点。
在遍历二叉树的过程中,一般先遍历左子树,然后再遍历右子树。在先左后右的原则下,根据访问根结点的次序,二叉树的遍历可以分为三种:前序遍历、中序遍历、后序遍历。下面分别介绍这三种遍历的方法。
1.前序遍历(DLR)
所谓前序遍历是指在访问根结点、遍历左子树与遍历右子树这三者中,首先访问根结点,然后遍历左子树,最后遍历右子树;并且,在遍历左、右子树时,仍然先访问根结点,然后遍历左子树,最后遍历右子树。因此,前序遍历二叉树的过程是一个递归的过程。
下面是二叉树前序遍历的简单描述:
若二叉树为空,则结束返回。
否则:(1)访问根结点;
(2)前序遍历左子树;
(3)前序遍历右子树。
在此特别要注意的是,在遍历左右子树时仍然采用前序遍历的方法。如果对图1.35(a)中的二叉树进行前序遍历,则遍历的结果为F,C,A,D,B,E,G,H,P(称为该二叉树的前序序列)。
2.中序遍历(LDR)
所谓中序遍历是指在访问根结点、遍历左子树与遍历右子树这三者中,首先遍历左子树,然后访问根结点,最后遍历右子树;并且,在遍历左、右子树时,仍然先遍历左子树,然后访问根结点,最后遍历右子树。因此,中序遍历二叉树的过程也是一个递归的过程。
下面是二叉树中序遍历的简单描述:
若二叉树为空,则结束返回。
否则:(1)中序遍历左子树;
(2)访问根结点;
(3)中序遍历左子树。
在此也要特别注意的是,在遍历左右子树时仍然采用中序遍历的方法。如果对图1.35(a)中的二叉树进行中序遍历,则遍历结果为A,C,B,D,F,E,H,G,P(称为该二叉树的中序序列)。
3.后序遍历(LRD)
所谓后序遍历是指在访问根结点、遍历左子树与遍历右子树这三者中,首先遍历左子树,然后遍历右子树,最后访问根结点,.并且,在遍历左、右子树时,仍然先遍历左子树,然后遍历右子树,最后访问根结点。因此,后序遍历二叉树的过程也是一个递归的过程。
下面是二叉树后序遍历的简单描述:
若二叉树为空,则结束返回。
否则:(1)后序遍历左子树;
(2)后序遍历右子树;
(3)访问根结点。
在此也要特别注意的是,在遍历左右子树时仍然采用后序遍历的方法。如果对图1.35(a)中的二叉树进行后序遍历,则遍历结果为A,B,D,C,H,P,G,E,F(称为该二叉树的后序序列)。
1.7 查找技术查找是数据处理领域中的一个重要内容,查找的效率将直接影响到数据处理的效率。
所谓查找是指在一个给定的数据结构中查找某个指定的元素。通常,根据不同的数据结构,应采用不同的查找方法。
1.7.1顺序查找顺序查找又称顺序搜索。顺序查找一般是指在线性表中查找指定的元素,其基本方法如下:
从线性表的第一个元素开始,依次将线性表中的元素与被查元素进行比较,若相等则表示找到(即查找成功);若线性表中所有的元素都与被查元素进行了比较但都不相等,则表示线性表中没有要找的元素(即查找失败)。
在进行顺序查找过程中,如果线性表中的第一个元素就是被查找元素,则只需做一次比较就查找成功,查找效率最高;但如果被查的元素是线性表中的最后一个元素,或者被查元素根本不在线性表中,则为了查找这个元素需要与线性表中所有的元素进行比较,这是顺序查找的最坏情况。在平均情况下,利用顺序查找法在线性表中查找一个元素,大约要与线性表中一半的元素进行比较。
由此可以看出,对于大的线性表来说,顺序查找的效率是很低的。虽然顺序查找的效率不高,但在下列两种情况下也只能采用顺序查找:
(1)如果线性表为无序表(即表中元素的排列是无序的),则不管是顺序存储结构还是链式存储结构,都只能用顺序查找。
(2)即使是有序线性表,如果采用链式存储结构,也只能用顺序查找。
1.7.2二分法查找二分法查找只适用于顺序存储的有序表。在此所说的有序表是指线性表中的元素按值非递减排列(即从小到大,但允许相邻元素值相等)。
设有序线性表的长度为n,被查元素为x,则对分查找的方法如下:
将x与线性表的中间项进行比较:
若中间项的值等于x,则说明查到,查找结束;
若x小于中间项的值,则在线性表的前半部分(即中间项以前的部分)以相同的方法进行查找;
若x大于中间项的值,则在线性表的后半部分(即中间项以后的部分)以相同的方法进行查找。
这个过程一直进行到查找成功或子表长度为0(说明线性表中没有这个元素)为止。
显然,当有序线性表为顺序存储时才能采用二分查找,并且,二分查找的效率要比顺序查找高得多。可以证明,对于长度为n的有序线陛表,在最坏情况下,二分查找只需要比较10g2n次,而顺序查找需要比较n次。
1.8 排序技术排序也是数据处理的重要内容。所谓排序是指将一个无序序列整理成按值非递减顺序排列的有序序列。排序的方法有很多,根据待排序序列的规模以及对数据处理的要求,可以采用不同的排序方法。本节主要介绍一些常用的排序方法。
排序可以在各种不同的存储结构上实现。在本节所介绍的排序方法中,其排序的对象一般认为是顺序存储的线性表,在程序设计语言中就是一维数组。
1.8.1交换类排序法所谓交换类排序法是指借助数据元素之间的互相交换进行排序的一种方法。冒泡排序法与快速排序法都属于交换类的排序方法。
1.冒泡排序法冒泡排序法是一种最简单的交换类排序方法,它是通过相邻数据元素的交换逐步将线性表变成有序。
冒泡排序法的基本过程如下:
首先,从表头开始往后扫描线性表,在扫描过程中逐次比较相邻两个元素的大小。若相邻两个元素中,前面的元素大于后面的元素,则将它们互换,称之为消去了一个逆序。显然,在扫描过程中,不断地将两相邻元素中的大者往后移动,最后就将线性表中的最大者换到了表的最后,这也是线性表中最大元素应有的位置。
然后,从后到前扫描剩下的线性表,同样,在扫描过程中逐次比较相邻两个元素的大小。若相邻两个元素中,后面的元素小于前面的元素,则将它们互换,这样就又消去了一个逆序。显然,在扫描过程中,不断地将两相邻元素中的小者往前移动,最后就将剩下线性表中的最小者换到了表的最前面,这也是线性表中最小元素应有的位置。
对剩下的线性表重复上述过程,直到剩下的线性表变空为止,此时的线性表已经变为有序。
在上述排序过程中,对线性表的每一次来回扫描后,都将其中的最大者沉到了表的底部,最小者象气泡一样冒到表的前头。冒泡排序由此而得名,且冒泡排序又称下沉排序。
假设线性表的长度为n,则在最坏情况下,冒泡排序需要经过n/2遍的从前往后的扫描和n/2遍的从后往前的扫描,需要的比较次数为n(n-1)/2。但这个工作量不是必需的,一般情况下要小于这个工作量。
图1.36是冒泡排序的示意图。图中有方框的元素位置表示扫描过程中最后一次发生交换的位置。由图1.36可以看出,整个排序实际上只用了2遍从前往后的扫描和2遍从后往前的扫描就完成。
2.快速排序法在前面所讨论的冒泡排序法中,由于在扫描过程中只对相邻两个元素进行比较,因此,在互换两个相邻元素时只能消除一个逆序。如果通过两个(不是相邻的)元素的交换,能够消除线性表中的多个逆序,就会大大加快排序的速度。显然,为了通过一次交换能消除多个逆序,就不能象冒泡排序法那样对相邻两个元素进行比较,因为这只能使相邻两个元素进行交换,从而只能消除一个逆序。下面介绍的快速排序法可以实现通过一次交换而消除多个逆序。
快速排序法也是一种互换类的排序方法,但由于它比冒泡排序法的速度快,因此称之为快速排序法。
快速排序法的基本思想如下:
从线性表中选取一个元素,设为T,将线性表后面小于T的元素移到前面,而前面大于T的元素移到后面,结果就将线性表分成了两部分(称为两个子表),T插入到其分界线的位置处,这个过程称为线性表的分割。通过对线性表的一次分割,就以T为分界线,将线性表分成了前后两个子表,且前面子表中的所有元素均不大于T,而后面子表中的所有元素均不小于T。
如果对分割后的各子表再按上述原则进行分割,并且,这种分割过程可以一直做下去,直到所有子表为空为止,则此时的线性表就变成了有序表。
由此可知,快速排序法的关键是对线性表进行分割,以及对各分割出的子表再进行分割,这个过程如图1.37所示。
在对线性表或子表进行实际分割时,可以按如下步骤进行:
首先,在表的第一个、中间一个与最后一个元素中选取中项,设为P(k),并将P(k)赋给T,再将表中的第一个元素移到P(k)的位置上。
然后设置两个指针i和j分别指向表的起始与最后的位置。反复操作以下两步:
(1)将j逐渐减小,并逐次比较P(j)与T,直到发现一个P(j)<T为止,将P(j)移到P(i)的位置上。
(2)将i逐渐增大,并逐次比较P(i)与T,直到发现一个P(i)>T为止,将P(i)移到P(j)的位置上。
上述两个操作交替进行,直到指针i与j指向同一个位置(即i=j)为止,此时将T移到P(i)的位置上。
在快速排序过程中,随着对各子表不断的进行分割,划分出的子表会越来越多,但一次又只能对一个子表进行再分割处理,需要将暂时不分割的子表记忆起来,这就要用一个栈来实现。在对某个子表进行分割后,可以将分割出的后一个子表的第一个元素与最后一个元素的位置压入栈中,而继续对前一个子表进行再分割;当分割出的子表为空时,可以从栈中退出一个子表(实际上只是该子表的第一个元素与最后一个元素的位置)进行分割。这个过程直到栈空为止,此时说明所有子表为空,没有子表再需要分割,排序就完成了。
1.8.2插入类排序法冒泡排序法与快速排序法本质上都是通过数据元素的交换来逐步消除线性表中的逆序。本小节讨论另一类排序的方法,即插入类排序法。
1.简单插入排序法所谓插入排序,是指将无序序列中的各元素依次插入到已经有序的线性表中。
我们可以想像,在线性表中,只包含第1个元素的子表显然可以看成是有序表。接下来的问题是,从线性表的第2个元素开始直到最后一个元素,逐次将其中的每一个元素插入到前面已经有序的子表中。一般来说,假设线性表中前j-1个元素已经有序,现在要将线性表中第j个元素插入到前面的有序子表中,插入过程如下:
首先将第j个元素放到一个变量T中,然后从有序子表的最后一个元素(即线性表中第j-1个元素)开始,往前逐个与T进行比较,将大于T的元素均依次向后移动一个位置,直到发现一个元素不大于T为止,此时就将T(即原线性表中的第j个元素)插入到刚移出的空位置上,有序子表的长度就变为j了。
图1.38给出了插入排序的示意图。图中画有方框的元素表示刚被插入到有序子表中。
在简单插入排序法中,每一次比较后最多移掉一个逆序,因此,这种排序方法的效率与冒泡排序法相同。在最坏情况下,简单插入排序需要n(n-1)/2次比较。
2.希尔排序法希尔排序法(Shell Sort)属于插入类排序,但它对简单插入排序做了较大的改进。
希尔排序法的基本思想如下:
将整个无序序列分割成若干小的子序列分别进行插入排序。
子序列的分割方法如下:
将相隔某个增量h的元素构成一个子序列。在排序过程中,逐次减小这个增量,最后当h减到1时,进行一次插入排序,排序就完成。
增量序列一般取ht=n/2k(k=l,2,…,[log2n]),其中n为待排序序列的长度。
图1.39为希尔排序法的示意图。
在希尔排序过程中,虽然对于每一个子表采用的仍是插入排序,但是,在子表中每进行一次比较就有可能移去整个线性表中的多个逆序,从而改善了整个排序过程的性能。
希尔排序的效率与所选取的增量序列有关。如果选取上述增量序列,则在最坏情况下,希尔排序所需要的比较次数为O(n1.5)。
1.8.3选择类排序法
1.简单选择排序法选择排序法的基本思想如下:
扫描整个线性表,从中选出最小的元素,将它交换到表的最前面(这是它应有的位置);然后对剩下的子表采用同样的方法,直到子表空为止。
对于长度为n的序列,选择排序需要扫描n-1遍,每一遍扫描均从剩下的子表中选出最小的元素,然后将该最小的元素与子表中的第一个元素进行交换。图1.40是这种排序法的示意图,图中有方框的元素是刚被选出来的最小元素。
简单选择排序法在最坏情况下需要比较n(n-l)/2次。
2.堆排序法堆排序法属于选择类的排序方法。
堆的定义如下:
具有n个元素的序列(h1,h2,…,hn),当且仅当满足
(i=1,2,…,n/2)时称之为堆。本节只讨论满足前者条件的堆。
由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项。
在实际处理中,可以用一维数组H(1:n)来存储堆序列中的元素,也可以用完全二叉树来直观地表示堆的结构。例如,序列(91,85,53,36,47,30,24,12)是一个堆,它所对应的完全二叉树如图1.41所示。由图1.41可以看出,在用完全二叉树表示堆时,树中所有非叶子结点值均不小于其左、右子树的根结点值,因此,堆顶(完全二叉树的根结点)元素必为序列的n个元素中的最大项。
在具体讨论堆排序法之前,先讨论这样一个问题:在一棵具有n个结点的完全二叉树[用一维数组H(1:n)表示]中,假设结点H(m)的左右子树均为堆,现要将以H(m)为根结点的子树也调整为堆。这是调整建堆的问题。
例如,假设图1.42(a)是某完全二叉树的一棵子树。显然,在这棵子树中,根结点47的左、右子树均为堆。现在为了将整个子树调整为堆,首先将根结点47与其左、右子树的根结点值进行比较,此时由于左子树根结点91大于右子树根结点53,且它又大于根结点47,因此,根据堆的条件,应将元素47与91交换,如图1.42(b)所示。经过这一次交换后,破坏了原来左子树的堆结构,需要对左子树再进行调整,将元素85与47进行交换,调整后的结果如图1.42(c)所示。
由这个例子可以看出,在调整建堆的过程中,总是将根结点值与左、右子树的根结点值进行比较,若不满足堆的条件,则将左、右子树根结点值中的大者与根结点值进行交换。这个调整过程一直做到所有子树均为堆为止。
有了调整建堆的算法后,就可以将一个无序序列建成为堆。
假设无序序列H(1:n)以完全二叉树表示。从完全二叉树的最后一个非叶子结点(即第n/2个元素)开始,直到根结点(即第一个元素)为止,对每一个结点进行调整建堆,最后就可以得到与该序列对应的堆。
根据堆的定义,可以得到堆排序的方法如下:
(1)首先将一个无序序列建成堆。
(2)然后将堆顶元素(序列中的最大项)与堆中最后一个元素交换(最大项应该在序列的最后)。不考虑已经换到最后的那个元素,只考虑前n-1个元素构成的子序列,显然,该子序列已不是堆,但左、右子树仍为堆,可以将该子序列调整为堆。反复做第(2)步,直到剩下的子序列为空为止。
堆排序的方法对于规模较小的线性表并不适合,但对于较大规模的线性表来说是很有效的。在最坏情况下,堆排序需要比较的次数为O(nlog2n)。
习 题 1
一、选择题
1.算法的时间复杂度是指
A)执行算法程序所需要的时间
B)算法程序的长度
c)算法执行过程中所需要的基本运算次数
D)算法程序中的指令条数
2.算法的空间复杂度是指
A)算法程序的长度
B)算法程序中的指令条数
C)算法程序所占的存储空间
D)算法执行过程中所需要的存储空间
3.下列叙述中正确的是
A)线性表是线性结构 B)栈与队列是非线性结构
c)线性链表是非线性结构D)二叉树是线性结构
4.数据的存储结构是指
A)数据所占的存储空间量 B)数据的逻辑结构在计算机中的表示
C)数据在计算机中的顺序存储方式 D)存储在外存中的数据
5.下列关于队列的叙述中正确的是
A)在队列中只能插入数据 B)在队列中只能删除数据
c)队列是先进先出的线性表 D)队列是先进后出的线性表
6.下列关于栈的叙述中正确的是
A)在栈中只能插入数据 B)在栈中只能删除数据
C)栈是先进先出的线性表 D)栈是先进后出的线性表
7.设有下列二叉树:
对此二叉树中序遍历的结果为
A)ABCDEF B)DBEAFC C)ABDECF D)DEBFCA
8.在深度为5的满二叉树中,叶子结点的个数为
A)32 B)31 C)16 D)15
9.对长度为n的线性表进行顺序查找,在最坏情况下所需要的比较次数为
A)n+l B)n C)(n+1)/2 D)n/2
10.设树T的度为4,其中度为1,2,3,4的结点个数分别为4,2,1,l。则T中的叶子结点数为
A)8 B)7 C)6 D)5
二、填空题
1.在长度为n的有序线性表中进行二分查找,需要的比较次数为________。
2.设一棵完全二叉树共有700个结点,则在该二叉树中有________个叶子结点。
3.设一棵二叉树的中序遍历结果为DBEAFC,前序遍历结果为ABDECF,则后序遍历结果为________。
4.在最坏情况下,冒泡排序的时间复杂度为________。
5.在一个容量为15的循环队列中,若头指针front=6,尾指针rear=9,则该循环队列中共有________个元素。
第2章 程序设计基础
2.1 程序设计方法与风格程序设计是一门技术,需要相应的理论、技术、方法和工具来支持。就程序设计方法和技术的发展而言,主要经过了结构化程序设计和面向对象的程序设计阶段。
除了好的程序设计方法和技术之外,程序设计风格也是很重要的。因为程序设计风格会深刻地影响软件的质量和可维护性,良好的程序设计风格可以使程序结构清晰合理,使程序代码便于维护,因此,程序设计风格对保证程序的质量是很重要的。
一般来讲,程序设计风格是指编写程序时所表现出的特点、习惯和逻辑思路。程序是由人来编写的,为了测试和维护程序,往往还要阅读和跟踪程序,因此程序设计的风格总体而言应该强调简单和清晰,程序必须是可以理解的。可以认为,著名的“清晰第一,效率第二”的论点已成为当今主导的程序设计风格。
要形成良好的程序设计风格,主要应注重和考虑下述一些因素。
1.源程序文档化源程序文档化应考虑如下几点:
(1)符号名的命名:符号名的命名应具有一定的实际含义,以便于对程序功能的理解。
(2)程序注释:正确的注释能够帮助读者理解程序。注释一般分为序言性注释和功能性注释。序言性注释通常位于每个程序的开头部分,它给出程序的整体说明,主要描述内容可以包括:程序标题、程序功能说明、主要算法、接口说明、程序位置、开发简历、程序设计者、复审者、复审日期、修改日期等。功能性注释的位置一般嵌在源程序体之中,主要描述其后的语句或程序做什么。
(3)视觉组织:为使程序的结构一目了然,可以在程序中利用空格、空行、缩进等技巧使程序层次清晰。
2.数据说明的方法在编写程序时,需要注意数据说明的风格,以便使程序中的数据说明更易于理解和维护。一般应注意如下几点:
(1)数据说明的次序规范化。鉴于程序理解、阅读和维护的需要,使数据说明次序固定,可以使数据的属性容易查找,也有利于测试、排错和维护。
(2)说明语句中变量安排有序化。当一个说明语句说明多个变量时,变量按照字母顺序排序为好。
(3)使用注释来说明复杂数据的结构。
3.语句的结构程序应该简单易懂,语句构造应该简单直接,不应该为提高效率而把语句复杂化。一般应注意如下:
(1)在一行内只写一条语句;
(2)程序编写应优先考虑清晰性;
(3)除非对效率有特殊要求,程序编写要做到清晰第一,效率第二;
(4)首先要保证程序正确,然后才要求提高速度;
(5)避免使用临时变量而使程序的可读性下降;
(6)避免不必要的转移;
(7)尽可能使用库函数;
(8)避免采用复杂的条件语句;
(9)尽量减少使用“否定”条件的条件语句;
(10)数据结构要有利于程序的简化;
(11)要模块化,使模块功能尽可能单一化;
(12)利用信息隐蔽,确保每一个模块的独立性;
(13)从数据出发去构造程序;
(14)不要修补不好的程序,要重新编写;
4.输入和输出输入和输出信息是用户直接关心的,输入和输出方式和格式应尽可能方便用户的使用,因为系统能否被用户接受,往往取决于输入和输出的风格。无论是批处理的输入和输出方式,还是交互式的输入和输出方式,在设计和编程时都应该考虑如下原则:
(1)对所有的输入数据都要检验数据的合法性;
(2)检查输入项的各种重要组合的合理性;
(3)输入格式要简单,以使得输入的步骤和操作尽可能简单;
(4)输入数据时,应允许使用自由格式;
(5)应允许缺省值;
(6)输入一批数据时,最好使用输入结束标志;
(7)在以交互式输入/输出方式进行输入时,要在屏幕上使用提示符明确提示输入的请求,同时在数据输入过程中和输入结束时,应在屏幕上给出状态信息;
(8)当程序设计语言对输入格式有严格要求时,应保持输入格式与输入语句的一致性;给所有的输出加注释,并设计输出报表格式。
2.2 结构化程序设针由于软件危机的出现,人们开始研究程序设计方法,其中最受关注的是结构化程序设计方法。20世纪70年代提出了“结构化程序设计(structured programming)”的思想和方法。结构化程序设计方法引入了工程思想和结构化思想,使大型软件的开发和编程都得到了极大的改善。
2.2.1结构化程序设计的原则结构化程序设计方法的主要原则可以概括为自顶向下,逐步求精,模块化,限制使用goto语句。
1.自顶向下:程序设计时,应先考虑总体,后考虑细节;先考虑全局目标,后考虑局部目标。不要一开始就过多追求众多的细节,先从最上层总目标开始设计,逐步使问题具体化。
2.逐步求精:对复杂问题,应设计一些子目标作过渡,逐步细化。
3.模块化:一个复杂问题,.肯定是由若干稍简单的问题构成。模块化是把程序要解决的总目标分解为分目标,再进一步分解为具体的小目标,把每个小目标称为一个模块。
4.限制使用goto语句实际上,结构化程序设计方法的起源来自对GOTO语句的认识和争论。肯定的结论是,在块和进程的非正常出口处往往需要用GOTO语句,使用GOTO语句会使程序执行效率较高;在合成程序目标时,GOTO语句往往是有用的,如返回语句用GOTO。否定的结论是,GOTO语句是有害的,是造成程序混乱的祸根,程序的质量与GOTO语句的数量成反比,应该在所有高级程序设计语言中取消GOTO语句。取消GOTO语句后,程序易理解、易排错、易维护,程序容易进行正确性证明。作为争论的结论,1974年Knuth发表了令人信服的总结,并证实了:
(1)滥用GOTO语句确实有害,应尽量避免;
(2)完全避免使用GOTO语句也并非是个明智的方法,有些地方使用GOTO语句,会使程序流程更清楚、效率更高;
(3)争论的焦点不应该放在是否取消GOTO语句,而应该放在用什么样的程序结构上。
其中最关键的是,肯定以提高程序清晰性为目标的结构化方法。
2.2.2结构化程序设计的基本结构和特点结构化程序设计方法是程序设计的先进方法和工具。采用结构化程序设计方法编写程序,可使程序结构良好、易读、易理解、易维护。1966年,Boehm和Jacopini证明了程序设计语言仅仅使用顺序、选择和重复三种基本控制结构就足以表达出各种其他形式结构的程序设计方法。
1.顺序结构:顺序结构是一种简单的程序设计,它是最基本、最常用的结构,如图2.1所示。顺序结构是顺序执行结构,所谓顺序执行,就是按照程序语句行的自然顺序,一条语句一条语句地执行程序。
2.选择结构:选择结构又称为分支结构,它包括简单选择和多分支选择结构,这种结构可以根据设定的条件,判断应该选择哪一条分支来执行相应的语句序列。图2.2列出了包含2个分支的简单选择结构。
3.重复结构:重复结构又称为循环结构,它根据给定的条件,判断是否需要重复执行某一相同的或类似的程序段,利用重复结构可简化大量的程序行。在程序设计语言中,重复结构对应两类循环语句,对先判断后执行循环体的称为当型循环结构,如图2.3所示。对先执行循环体后判断的称为直到型循环结构,如图2.4所示。
总之,遵循结构化程序的设计原则,按结构化程序设计方法设计出的程序具有明显的优点,其一,程序易于理解、使用和维护。程序员采用结构化编程方法,便于控制、降低程序的复杂性,因此容易编写程序。便于验证程序的正确性,结构化程序清晰易读,可理解性好,程序员能够进行逐步求精、程序证明和测试,以确保程序的正确性,程序容易阅读并被人理解,便于用户使用和维护。其二,提高了编程工作的效率,降低了软件开发成本。由于结构化编程方法能够把错误控制到最低限度,因此能够减少调试和查错时间。结构化程序是由一些为数不多的基本结构模块组成,这些模块甚至可以由机器自动生成,从而极大地减轻了编程工作量。
2.2.3结构化程序设计原则和方法的应用基于对结构化程序设计原则、方法以及结构化程序基本构成结构的掌握和了解,在结构化程序设计的具体实施中,要注意把握如下要素:
1.使用程序设计语言中的顺序、选择、循环等有限的控制结构表示程序的控制逻辑;
2.选用的控制结构只准许有一个入口和一个出口;
3.程序语句组成容易识别的块,每块只有一个入口和一个出口:
4.复杂结构应该用嵌套的基本控制结构进行组合嵌套来实现;
5.语言中所没有的控制结构,应该采用前后一致的方法来模拟;
6.严格控制GOTO语句的使用。其意思是指:
(1)用一个非结构化的程序设计语言去实现一个结构化的构造;
(2)若不使用GOTO语句会使功能模糊:
(3)在某种可以改善而不是损害程序可读性的情况下。
2.3面向对象的程序设计
2.3.1关于面向对象方法今天,面向对象(obiect oriented)方法已经发展成为主流的软件开发方法。面向对象方法的形成同结构化方法一样,起源于实现语言,首先对面向对象的程序设计语言开展研究,随之逐渐形成面向对象分析和设计方法。面向对象方法和技术历经30多年的研究和发展,已经越来越成熟和完善,应用也越来越深入和广泛。
面向对象的软件开发方法在20世纪60年代后期首次提出,以60年代末挪威奥斯陆大学和挪威计算中心共同研制的SIMULA语言为标志,面向对象方法的基本要点首次在SIMULA语言中得到了表达和实现。后来一些著名的面向对象语言(如Smalltalk、C++、Java、Eiffel)的设计者都曾从SIMULA得到启发。随着80年代美国加州的Xerox研究中心推出Smalltalk语言和环境,使面向对象程序设计方法得到比较完善的实现。Smalltalk-80等一系列描述能力较强、执行效率较高的面向对象编程语言的出现,标志着面向对象的方法与技术开始走向实用。
面向对象方法的本质,就是主张从客观世界固有的事物出发来构造系统,提倡用人类在现实生活中常用的思维方法来认识、理解和描述客观事物,强调最终建立的系统能够映射问题域,也就是说,系统中的对象以及对象之间的关系能够如实地反映问题域中固有事物及其关系。
面向对象方法之所以日益受到人们的重视和应用,成为流行的软件开发方法,是源于面向对象方法的以下主要优点。
1.与人类习惯的思维方法一致传统的程序设计方法是面向过程的,其核心方法是以算法为核心,把数据和过程作为相互独立的部分,数据代表问题空间中的客体,程序则用于处理这些数据,在计算机内部数据和程序是分开存放的,这样的做法往往会发生使用错误的数据调用正确的程序模块的情况。其原因是,传统的程序设计方法忽略了数据和操作之间的内在联系,用这种方法设计出来的软件系统其解空间与问题空间不一致,使人感到难于理解。实际上,用计算机解决的问题都是现实世界中的问题,这些问题无非由一些相互间存在一定联系的事物所组成,每个具体的事物都具有行为和属性两方面的特征。因此,把描述事物静态属性的数据结构和表示事物动态行为的操作放在一起构成一个整体,才能完整、自然地表示客观世界中的实体。
面向对象方法和技术以对象为核心。对象是由数据和容许的操作组成的封装体,与客观实体有直接的对应关系。对象之间通过传递消息互相联系,以模拟现实世界中不同事物彼此之间的联系。
面向对象的设计方法与传统的面向过程的方法有本质不同,这种方法的基本原理是,使用现实世界的概念抽象地思考问题从而自然地解决问题。它强调模拟现实世界中的概念而不强调算法,它鼓励开发者在软件开发的绝大部分过程中都用应用领域的概念去思考。
2.稳定性好面向对象方法基于构造问题领域的对象模型,以对象为中心构造软件系统。它的基本作法是用对象模拟问题领域中的实体,以对象间的联系刻画实体间的联系。因为面向对象的软件系统的结构是根据问题领域的模型建立起来的,而不是基于对系统应完成的功能的分解,所以,当对系统的功能需求变化时并不会引起软件结构的整体变化,往往仅需要作一些局部性的修改。由于现实世界中的实体是相对稳定的,因此,以对象为中心构造的软件系统也是比较稳定的。而传统的软件开发方法以算法为核心,开发过程基于功能分析和功能分解。用传统方法所建立起来的软件系统的结构紧密地依赖于系统所要完成的功能,当功能需求发生变化时将引起软件结构的整体修改。事实上,用户需求变化大部分是针对功能的,因此,这样的软件系统是不稳定的。
3.可重用性好软件重用是指在不同的软件开发过程中重复使用相同或相似软件元素的过程。重用是提高软件生产率的最主要的方法。
传统的软件重用技术是利用标准函数库,也就是试图用标准函数库中的函数作为“预制件”来建造新的软件系统。但是,标准函数缺乏必要的“柔性”,不能适应不同应用场合的不同需要,并不是理想的可重用的软件成分。实际的库函数往往仅提供最基本、最常用的功能,在开发一个新的软件系统时,通常多数函数是开发者自己编写的,甚至绝大多数函数都是新编的。
使用传统方法学开发软件时,人们强调的是功能抽象,认为具有功能内聚性的模块是理想的模块,也就是说,如果一个模块完成一个且只完成一个相对独立的子功能,那么这个模块就是理想的可重用模块,而且这样的模块也更容易维护。基于这种认识,通常尽量把标准函数库中的函数做成功能内聚的。但是,事实上具有功能内聚性的模块并不是自含的和独立的,相反,它必须在数据上运行。如果要重用这样的模块,则相应的数据也必须重用。如果新产品中的数据与最初产品中的数据不同,则要么修改数据要么修改这个模块。
事实上,离开了操作数据便无法处理,而脱离了数据的操作也是毫无意义的,我们应该对数据和操作同样重视。在面向对象方法中所使用的对象,其数据和操作是作为平等伙伴出现的。因此,对象具有很强的自含性,此外,对象所固有的封装性,使得对象的内部实现与外界隔离,具有较强的独立性。由此可见,对象提供了比较理想的模块化机制和比较理想的可重用的软件成分。
面向对象的软件开发技术在利用可重用的软件成分构造新的软件系统时,有很大的灵活性。有两种方法可以重复使用一个对象类:一种方法是创建该类的实例,从而直接使用它;另一种方法是从它派生出一个满足当前需要的新类。继承性机制使得子类不仅可以重用其父类的数据结构和程序代码,而且可以在父类代码的基础上方便地修改和扩充,这种修改并不影响对原有类的使用。可见,面向对象的软件开发技术所实现的可重用性是自然的和准确的。
4.易于开发大型软件产品当开发大型软件产品时,组织开发人员的方法不恰当往往是出现问题的主要原因。用面向对象范型开发软件时,可以把一个大型产品看作是一系列本质上相互独立的小产品来处理,这就不仅降低了开发的技术难度,而且也使得对开发工作的管理变得容易。这就是为什么对于大型软件产品来说,面向对象范型优于结构化范型的原因之一。许多软件开发公司的经验都表明,当把面向对象技术用于大型软件开发时,软件成本明显地降低了,软件的整体质量也提高了。
5.可维护性好用传统的开发方法和面向过程的方法开发出来的软件很难维护,是长期困扰人们的一个严重问题,是软件危机的突出表现。
由于下述因素的存在,使得用面向对象的方法开发的软件可维护性好。
(1)用面向对象的方法开发的软件稳定性比较好。
如前所述,当对软件的功能或性能的要求发生变化时,通常不会引起软件的整体变化,往往只需对局部作一些修改。由于对软件的改动较小且限于局部,自然比较容易实现。
(2)用面向对象的方法开发的软件比较容易修改。
在面向对象方法中,核心是类(对象),它具有理想的模块机制,独立性好,修改一个类通常很少会牵扯到其他类。如果仅修改一个类的内部实现部分(私有数据成员或成员函数的算法),而不修改该类的对外接口,则可以完全不影响软件的其他部分。
面向对象技术特有的继承机制,使得对所开发的软件的修改和扩充比较容易实现,通常只需从已有类派生出一些新类,无需修改软件原有成分。
面向对象技术的多态性机制,使得当扩充软件功能时对原有代码的修改进一步减少,需要增加的新代码也比较少。
(3)用面向对象的方法开发的软件比较容易理解。
在维护已有软件的时候,首先需要对原有软件与此次修改有关的部分有深入理解,才能正确地完成维护工作。传统软件之所以难于维护,在很大程度上是因为修改所涉及的部分分散在软件各个地方,需要了解的面很广,内容很多,而且传统软件的解空间与问题空间的结构很不一致,更增加了理解原有软件的难度和工作量。
面向对象的技术符合人们习惯的思维方式,用这种方法所建立的软件系统的结构与问题空间的结构基本一致。因此,面向对象的软件系统比较容易理解。
对面向对象软件系统进行修改和扩充,通常是通过在原有类的基础上派生出一些新类来实现。由于对象类有很强的独立性,当派生新类的时候通常不需要详细了解基类中操作的实现算法。因此,了解原有系统的工作量可以大幅度降低。
(4)易于测试和调试。
为了保证软件质量,对软件进行维护之后必须进行必要的测试,以确保要求修改或扩充的功能已正确地实现了,而且没有影响到软件未修改的部分。如果测试过程中发现了错误,还必须通过调试改正过来。显然,软件是否易于测试和调试,是影响软件可维护性的一个重要因素。
对用面向对象的方法开发的软件进行维护,往往是通过从已有类派生出一些新类来实现。因此,维护后的测试和调试工作也主要围绕这些新派生出来的类进行。类是独立性很强的模块,向类的实例发消息即可运行它,观察它是否能正确地完成相应的工作,因此对类的测试通常比较容易实现。
2.3.2面向对象方法的基本概念关于面向对象方法,对其概念有许多不同的看法和定义,但是都涵盖对象及对象属性与方法、类、继承、多态性几个基本要素。下面分别介绍面向对象方法中这几个重要的基本概念,这些概念是理解和使用面向对象方法的基础和关键。
1.对象(object)
对象是面向对象方法中最基本的概念。对象可以用来表示客观世界中的任何实体,也就是说,应用领域中有意义的、与所要解决的问题有关系的任何事物都可以作为对象,它既可以是具体的物理实体的抽象,也可以是人为的概念,或者是任何有明确边界和意义的东西。例如,一个人、一家公司、一个窗口、贷款和借款等,都可以作为一个对象。总之,对象是对问题域中某个实体的抽象,设立某个对象就反映了软件系统保存有关它的信息并具有与它进行交互的能力。
面向对象的程序设计方法中涉及的对象是系统中用来描述客观事物的一个实体,是构成系统的一个基本单位,它由一组表示其静态特征的属性和它可执行的一组操作组成。
例如,一辆汽车是一个对象,它包含了汽车的属性(如颜色、型号、载重量等)及其操作(如启动、刹车等)。一个窗口是一个对象,它包含了窗口的属性(如大小、颜色、位置等)及其操作(如打开、关闭等)。
客观世界中的实体通常都既具有静态的属性,又具有动态的行为,因此,面向对象方法学中的对象是由描述该对象属性的数据以及可以对这些数据施加的所有操作封装在一起构成的统一体。对象可以做的操作表示它的动态行为,在面向对象分析和面向对象设计中,通常把对象的操作也称为方法或服务。
属性即对象所包含的信息,它在设计对象时确定,一般只能通过执行对象的操作来改变。如对象Person的属性有姓名、年龄、体重等。不同对象的同一属性可以具有相同或不同的属性值。如张三的年龄为19,李四的年龄为20。张三、李四是两个不同的对象,他们共同的属性“年龄”的值不同。要注意的是,属性值应该指的是纯粹的数据值,而不能指对象。
操作描述了对象执行的功能,若通过消息传递,还可以为其他对象使用。操作的过程对外是封闭的,即用户只能看到这一操作实施后的结果。这相当于事先已经设计好的各种过程,只需要调用就可以了,用户不必去关心这一过程是如何编写的。事实上,这个过程已经封装在对象中,用户也看不到。对象的这一特性,即是对象的封装性。
对象有如下一些基本特点:
(1)标识惟一性。指对象是可区分的,并且由对象的内在本质来区分,而不是通过描述来区分。
(2)分类性。指可以将具有相同属性和操作的对象抽象成类。
(3)多态性。指同一个操作可以是不同对象的行为。
(4)封装性。从外面看只能看到对象的外部特性,即只需知道数据的取值范围和可以对该数据施加的操作,根本无需知道数据的具体结构以及实现操作的算法。对象的内部,即处理能力的实行和内部状态,对外是不可见的。从外面不能直接使用对象的处理能力,也不能直接修改其内部状态,对象的内部状态只能由其自身改变。
(5)模块独立性好。对象是面向对象的软件的基本模块,它是由数据及可以对这些数据施加的操作所组成的统一体,而且对象是以数据为中心的,操作围绕对其数据所需做的处理来设置,没有无关的操作。从模块的独立性考虑,对象内部各种元素彼此结合得很紧密,内聚性强。
2.类(Class)和实例(Instance)
将属性、操作相似的对象归为类,也就是说,类是具有共同属性、共同方法的对象的集合。所以,类是对象的抽象,它描述了属于该对象类型的所有对象的性质,而一个对象则是其对应类的一个实例。
要注意的是,当使用“对象”这个术语时,既可以指一个具体的对象,也可以泛指一般的对象,但是,当使用“实例”这个术语时,必然是指一个具体的对象。
例如:Integer是一个整数类,它描述了所有整数的性质。因此任何整数都是整数类的对象,而一个具体的整数“123”是类Integer的一个实例。
由类的定义可知,类是关于对象性质的描述,它同对象一样,包括一组数据属性和在数据上的一组合法操作。例如,一个面向对象的图形程序在屏幕左下角显示一个半径3cm的红颜色的圆,在屏幕中部显示一个半径4cm的绿颜色的圆,在屏幕右上角显示一个半径lcm的黄颜色的圆。这三个圆心位置、半径大小和颜色均不相同的圆,是三个不同的对象。但是,它们都有相同的属性(圆心坐标、半径、颜色)和相同的操作(显示自己、放大缩小半径、在屏幕上移动位置,等等)。因此,它们是同一类事物,可以用“Circle类”来定义。
3.消息(Message)
面向对象的世界是通过对象与对象间彼此的相互合作来推动的,对象间的这种相互合作需要一个机制协助进行,这样的机制称为“消息”。消息是一个实例与另一个实例之间传递的信息,它请求对象执行某一处理或回答某一要求的信息,它统一了数据流和控制流。消息的使用类似于函数调用,消息中指定了某一个实例,一个操作名和一个参数表(可空)。接收消息的实例执行消息中指定的操作,并将形式参数与参数表中相应的值结合起来。消息传递过程中,由发送消息的对象(发送对象)的触发操作产生输出结果,作为消息传送至接受消息的对象(接受对象),引发接受消息的对象一系列的操作。所传送的消息实质上是接受对象所具有的操作/方法名称,有时还包括相应参数,图2.5表示了消息传递的概念。
消息中只包含传递者的要求,它告诉接受者需要做哪些处理,但并不指示接受者应该怎样完成这些处理。消息完全由接受者解释,接受者独立决定采用什么方式完成所需的处理,发送者对接受者不起任何控制作用。一个对象能够接受不同形式、不同内容的多个消息;相同形式的消息可以送往不同的对象,不同的对象对于形式相同的消息可以有不同的解释,能够做出不同的反映。一个对象可以同时往多个对象传递信息,两个对象也可以同时向某个对象传递消息。
例如,一个汽车对象具有“行驶”这项操作,那么要让汽车以时速50公里行驶的话,需传递给汽车对象“行驶”及“时速50公里”的消息。
通常,一个消息由下述三部分组成:
①接收消息的对象的名称;
②消息标识符(也称为消息名);
③零个或多个参数。
例如,MyCircle是一个半径4cm、圆心位于(100,200)的Circle类的对象,也就是Circle类的一个实例,当要求它以绿颜色在屏幕上显示自己时,在C++语言中应该向它发下列消息:
MyCircle.Show(GREEN);
其中,MyCircle是接收消息的对象的名字,Show是消息名,Green是消息的参数。
4.继承(Inheritance)
继承是面向对象的方法的一个主要特征。继承是使用已有的类定义作为基础建立新类的定义技术。已有的类可当作基类来引用,则新类相应地可当作派生类来引用。
广义地说,继承是指能够直接获得已有的性质和特征,而不必重复定义它们。
面向对象软件技术的许多强有力的功能和突出的优点,都来源于把类组成一个层次结构的系统:一个类的上层可以有父类,下层可以有子类。这种层次结构系统的一个重要性质是继承性,一个类直接继承其父类的描述(数据和操作)或特性,子类自动地共享基类中定义的数据和方法。
为了更深入、具体地理解继承性的含义,图2.6示出了实现继承机制的原理。
图中以A、B两个类为例,其中类B是从类A派生出来的子类,它除了具有自己定义的特性(数据和操作)之外,还从父类A继承特性。当创建类A的实例al的时候,a1以类A为样板建立实例变量。
当创建类B的实例b1的时候,b1既要以类B为样板建立实例变量,又要以类A为样板建立实例变量,bl所能执行的操作既有类B中定义的方法,又有类A中定义的方法,这就是继承。
继承具有传递性,如果类C继承类B,类B继承类A,则类C继承类A。因此,一个类实际上继承了它上层的全部基类的特性,也就是说,属于某类的对象除了具有该类所定义的特性外,还具有该类上层全部基类定义的特性。
继承分为单继承与多重继承。单继承是指,一个类只允许有一个父类,即类等级为树形结构。多重继承是指,一个类允许有多个父类。多重继承的类可以组合多个父类的性质构成所需要的性质。因此,功能更强,使用更方便;但是,使用多重继承时要注意避免二义性。继承性的优点是,相似的对象可以共享程序代码和数据结构,从而大大减少了程序中的冗余信息,提高软件的可重用性,便于软件修改维护。另外,继承性使得用户在开发新的应用系统时不必完全从零开始,可以继承原有的相似系统的功能或者从类库中选取需要的类,再派生出新的类以实现所需要的功能。
5.多态性(Polymorphism)
对象根据所接受的消息而做出动作,同样的消息被不同的对象接受时可导致完全不同的行动,该现象称为多态性。在面向对象的软件技术中,多态性是指子类对象可以像父类对象那样使用,同样的消息既可以发送给父类对象也可以发送给子类对象。
例如,在两个类Male(男性)和Female(女性)都有一项属性为Friend。一个人的朋友必须属于类Male或Female,这是一个多态性的情况。因为,Friend指向两个类之一的实例。如果Tom的朋友或者是Mary或者是John,类Male就不知道Friend应该与哪个类关联。这里参照量Friend必须是多态的,多态意味着可以关联不同的实例,而实例可以属于不同的类。
多态性机制不仅增加了面向对象软件系统的灵活性,进一步减少了信息冗余,而且显著地提高了软件的可重用性和可扩充性。当扩充系统功能增加新的实体类型时,只需派生出与新实体类相应的新的子类,完全无需修改原有的程序代码,甚至不需要重新编译原有的程序。利用多态性,用户能够发送一般形式的消息,而将所有的实现细节都留给接受消息的对象。
习 题 2
一、选择题
1.结构化程序设计主要强调的是
A)程序的规模 B)程序的易读性
c)程序的执行效率 D)程序的可移植性
2.对建立良好的程序设计风格,下面描述正确的是
A)程序应简单、清晰、可读性好
B)符号名的命名只要符合语法
c)充分考虑程序的执行效率
D)程序的注释可有可无
3.在面向对象方法中,一个对象请求另一对象为其服务的方式是通过发送
A)调用语句 B)命令 c)口令 D)消息
4.信息隐蔽的概念与下述哪一种概念直接相关?
A)软件结构定义 B)模块独立性 C)模块类型划分 D)模块耦合度
5.下面对对象概念描述错误的是
A)任何对象都必须有继承性
B)对象是属性和方法的封装体
c)对象间的通讯靠消息传递
D)操作是对象的动态属性二、填空题
1.结构化程序设计的三种基本逻辑结构为顺序、选择和________。
2.源程序文档化要求程序应加注释。注释一般分为序言性注释和________。
3.在面向对象方法中,信息隐蔽是通过对象的________性来实现的。
4.类是一个支持集成的抽象数据类型,而对象是类的________。
5.在面向对象方法中,类之间共享属性和操作的机制称为________。
第3章 软件工程基础
3.1 软件工程基本概念
3.1.1软件定义与软件特点计算机软件(software)是计算机系统中与硬件相互依存的另一部分,是包括程序、数据及相关文档的完整集合。其中,程序是软件开发人员根据用户需求开发的、用程序设计语言描述的、适合计算机执行的指令(语句)序列。数据是使程序能正常操纵信息的数据结构。文档是与程序开发、维护和使用有关的图文资料。可见软件由两部分组成:一是机器可执行的程序和数据;二是机器不可执行的,与软件开发、运行、维护、使用等有关的文档。
国标(GB)中对计算机软件的定义为:与计算机系统的操作有关的计算机程序、规程、规则,以及可能有的文件、文档及数据。
软件在开发、生产、维护和使用等方面与计算机硬件相比存在明显的差异。深入理解软件的定义需要了解软件的特点:
①软件是一种逻辑实体,而不是物理实体,具有抽象性。软件的这个特点使它与其他工程对象有着明显的差异。人们可以把它记录在纸上或存储介质上,但却无法看到软件本身的形态,必须通过观察、分析、思考、判断,才能了解它的功能、性能等特性。
②软件的生产与硬件不同,它没有明显的制作过程。一旦研制开发成功,可以大量拷贝同一内容的副本。所以对软件的质量控制,必须着重在软件开发方面下功夫。
③软件在运行、使用期间不存在磨损、老化问题。软件虽然在生存周期后期不会因为磨损而老化,但为了适应硬件、环境以及需求的变化要进行修改,而这些修改又会不可避免的引入错误,导致软件失效率升高,从而使得软件退化。
④软件的开发、运行对计算机系统具有依赖性,受计算机系统的限制,这导致了软件移植的问题。
⑤软件复杂性高,成本昂贵。软件是人类有史以来生产的复杂度最高的工业产品。软件涉及人类社会的各行各业、方方面面,软件开发常常涉及其他领域的专门知识。软件开发需要投入大量、高强度的脑力劳动,成本高,风险大。
⑥软件开发涉及诸多的社会因素。许多软件的开发和运行涉及软件用户的机构设置,体制问题以及管理方式等,甚至涉及到人们的观念和心理,软件知识产权及法律等问题。
软件根据应用目标的不同,是多种多样的。软件按功能可以分为:应用软件、系统软件、支撑软件(或工具软件)。应用软件是为解决特定领域的应用而开发的软件。例如,事务处理软件,工程与科学计算软件,实时处理软件,嵌入式软件,人工智能软件等应用性质不同的各种软件。系统软件是计算机管理自身资源,提高计算机使用效率并为计算机用户提供各种服务的软件。如操作系统,编译程序,汇编程序,网络软件,数据库管理系统等。支撑软件是介于系统软件和应用软件之间,协助用户开发软件的工具性软件,包括辅助和支持开发和维护应用软件的工具软件,如需求分析工具软件,设计工具软件,编码工具软件,测试工具软件,维护工具软件等,也包括辅助管理人员控制开发进程和项目管理的工具软件,如,计划进度管理工具软件,过程控制工具软件,质量管理及配置管理工具软件等。
3.1.2软件危机与软件工程软件工程概念的出现源自软件危机。
20世纪60年代末以后,“软件危机”这个词频繁出现。所谓软件危机是泛指在计算机软件的开发和维护过程中所遇到的一系列严重问题。实际上,几乎所有的软件都不同程度地存在这些问题。
随着计算机技术的发展和应用领域的扩大,计算机硬件性能/价格比和质量稳步提高,软件规模越来越大,复杂程度不断增加,软件成本逐年上升,质量没有可靠的保证,软件己成为计算机科学发展的“瓶颈”。
具体地说,在软件开发和维护过程中,软件危机主要表现在:
①软件需求的增长得不到满足。用户对系统不满意的情况经常发生。
②软件开发成本和进度无法控制。开发成本超出预算,开发周期大大超过规定日期的情况经常发生。
③软件质量难以保证。
④软件不可维护或维护程度非常低。
⑤软件的成本不断提高。
⑥软件开发生产率的提高赶不上硬件的发展和应用需求的增长。
总之,可以将软件危机归结为成本、质量、生产率等问题。
分析带来软件危机的原因,宏观方面是由于软件日益深入社会生活的各个层面,对软件需求的增长速度大大超过了技术进步所能带来的软件生产率的提高。而就每一项具体的工程任务来看,许多困难来源于软件工程所面临的任务和其他工程之间的差异以及软件和其他工业产品的不同。
在软件开发和维护过程中,之所以存在这些严重的问题,一方面与软件本身的特点有关,例如,在软件运行前,软件开发过程的进展难衡量,质量难以评价,因此管理和控制软件开发过程相当困难;在软件运行过程中,软件维护意味着改正或修改原来的设计;另外,软件的显著特点是规模庞大,复杂度超线性增长,在开发大型软件时,要保证高质量,极端复杂困难,不仅涉及技术问题(如分析方法、设计方法、版本控制),更重要的是必须有严格而科学的管理。另一方面与软件开发和维护方法不正确有关,这是主要原因。
为了消除软件危机,通过认真研究解决软件危机的方法,认识到软件工程是使计算机软件走向工程科学的途径,逐步形成了软件工程的概念,开辟了工程学的新兴领域——软件工程学。软件工程就是试图用工程、科学和数学的原理与方法研制、维护计算机软件的有关技术及管理方法。
关于软件工程的定义,国标(GB)中指出,软件工程是应用于计算机软件的定义、开发和维护的一整套方法、工具、文档、实践标准和工序。
1968年在北大西洋公约组织会议(NATO会议)上,讨论摆脱软件危机的办法,软件工程(software engineering)作为一个概念首次被提出,这在软件技术发展史上是一件大事。其后的几十年里,各种有关软件工程的技术、思想、方法和概念不断地被提出,软件工程逐步发展成为一门独立的科学。在会议上,德国人Fritz Bauer认为:“软件工程是建立并使用完善的工程化原则,以较经济的手段获得能在实际机器上有效运行的可靠软件的一系列方法”。
1993年,IEEE(Institute of Electrical&Electronic Engineers,电气和电子工程师学会)给出了一个更加综合的定义:“将系统化的、规范的、可度量的方法应用于软件的开发,运行和维护的过程,即将工程化应用于软件中”。
这些主要思想都是强调在软件开发过程中需要应用工程化原则。
软件工程包括3个要素,即方法、工具和过程。方法是完成软件工程项目的技术手段;工具支持软件的开发、管理、文档生成;过程支持软件开发的各个环节的控制、管理。
软件工程的进步是近几十年软件产业迅速发展的重要原动力。从根本上来说,其目的是研究软件的开发技术,软件工程的名称意味着用工业化的开发方法来替代小作坊式的开发模式。但是,几十年的软件开发和软件发展的实践证明,软件开发是既不同于其他工业工程,也不同于科学研究。软件不是自然界的有形物体,它作为人类智慧的产物有其本身的特点,所以软件工程的方法、概念、目标等都在发展,有的与最初的想法有了一定的差距。但是认识和学习过去和现在的发展演变,真正掌握软件开发技术的成就,并为进一步发展软件开发技术,以适应时代对软件的更高期望是有极大意义的。
软件工程的核心思想是把软件产品(就像其他工业产品一样)看作是一个工程产品来处理。把需求计划、可行性研究、工程审核、质量监督等工程化的概念引入到软件生产当中,以期达到工程项目的三个基本要素:进度、经费和质量的目标。同时,软件工程也注重研究不同于其他工业产品生产的一些独特特性,并针对软件的特点提出了许多有别于一般工业工程技术的一些技术方法。代表性的有结构化的方法、面向对象方法和软件开发模型及软件开发过程等。
特别地,从经济学的意义上来说,考虑到软件庞大的维护费用远比软件开发费用要高,因而开发软件不能只考虑开发期间的费用,而且应考虑软件生命周期内的全部费用。因此,软件生命周期的概念就变得特别重要。在考虑软件费用时,不仅仅要降低开发成本,更要降低整个软件生命周期的总成本。
3.1.3软件工程过程与软件生命周期
1.软件工程过程(Software Engineering Process)
ISO 9000定义:软件工程过程是把输入转化为输出的一组彼此相关的资源和活动。
定义支持了软件工程过程的两方面内涵。
其一,软件工程过程是指为获得软件产品,在软件工具支持下由软件工程师完成的一系列软件工程活动。基于这个方面,软件工程过程通常包含4种基本活动:
①P(Plan)——软件规格说明。规定软件的功能及其运行时的限制。
②D(Do)——软件开发。产生满足规格说明的软件。
③C(Check)——软件确认。确认软件能够满足客户提出的要求。
④A(Action)——软件演进。为满足客户的变更要求,软件必须在使用的过程中演进。
事实上,软件工程过程是一个软件开发机构针对某类软件产品为自己规定的工作步骤,它应当是科学的、合理的,否则必将影响软件产品的质量。
通常把用户的要求转变成软件产品的过程也叫做软件开发过程。此过程包括对用户的要求进行分析,解释成软件需求,把需求变换成设计,把设计用代码来实现并进行代码测试,有些软件还需要进行代码安装和交付运行。
其二,从软件开发的观点看,它就是使用适当的资源(包括人员、硬软件工具、时间等),为开发软件进行的一组开发活动,在过程结束时将输入(用户要求)转化为输出(软件产品)。
所以,软件工程的过程是将软件工程的方法和工具综合起来,以达到合理、及时地进行计算机软件开发的目的。软件工程过程应确定方法使用的顺序、要求交付的文档资料、为保证质量和适应变化所需要的管理、软件开发各个阶段完成的任务。
2.软件生命周期(Software Life Cycle)
通常,将软件产品从提出、实现、使用维护到停止使用退役的过程称为软件生命周期。也就是说,软件产品从考虑其概念开始,到该软件产品不能使用为止的整个时期都属于软件生命周期。一般包括可行性研究与需求分析、设计、实现、测试、交付使用以及维护等活动,如图3.1所示。这些活动可以有重复,执行时也可以有迭代。
还可以将软件生命周期分为如图3.1所示的软件定义、软件开发及软件运行维护三个阶段。图3.1所示的软件生命周期的主要活动阶段是:
①可行性研究与计划制定。确定待开发软件系统的开发目标和总的要求,给出它的功能、性能、可靠性以及接口等方面的可能方案,制定完成开发任务的实施计划。
②需求分析。对待开发软件提出的需求进行分析并给出详细定义。编写软件规格说明书及初步的用户手册,提交评审。
⑧软件设计。系统设计人员和程序设计人员应该在反复理解软件需求的基础上,给出软件的结构、模块的划分、功能的分配以及处理流程。在系统比较复杂的情况下,设计阶段可分解成概要设计阶段和详细设计阶段。编写概要设计说明书、详细设计说明书和测试计划初稿,提交评审。
④软件实现。把软件设计转换成计算机可以接受的程序代码。即完成源程序的编码,编写用户手册、操作手册等面向用户的文档,编写单元测试计划。
⑤软件测试。在设计测试用例的基础上,检验软件的各个组成部分。编写测试分析报告。
⑥运行和维护。将已交付的软件投入运行,并在运行使用中不断地维护,根据新提出的需求进行必要而且可能的扩充和删改。
3.1.4软件工程的目标与原则
1.软件工程的目标软件工程的目标是,在给定成本、进度的前提下,开发出具有有效性、可靠性、可理解性、可维护性、可重用性、可适应性、可移植性、可追踪性和可互操作性且满足用户需求的产品。
软件工程需要达到的基本目标应是:付出较低的开发成本;达到要求的软件功能;取得较好的软件性能;开发的软件易于移植;需要较低的维护费用;能按时完成开发,及时交付使用。
基于软件工程的目标,软件工程的理论和技术性研究的内容主要包括:软件开发技术和软件工程管理。
(1)软件开发技术软件开发技术包括:软件开发方法学、开发过程、开发工具和软件工程环境,其主体内容是软件开发方法学。软件开发方法学是根据不同的软件类型,按不同的观点和原则,对软件开发中应遵循的策略、原则、步骤和必须产生的文档资料都做出规定,从而使软件的开发能够进入规范化和工程化的阶段,以克服早期的手工方法生产中的随意性和非规范性做法。
(2)软件工程管理软件工程管理包括:软件管理学、软件工程经济学、软件一心理学等内容。
软件工程管理是软件按工程化生产时的重要环节,它要求按照预先制定的计划、进度和预算执行,以实现预期的经济效益和社会效益。统计数据表明,多数软件开发项目的失败,并不是由于软件开发技术方面的原因,它们的失败是由于不适当的管理造成的。因此人们对软件项目管理重要性的认识有待提高。软件管理学包括人员组织、进度安排、质量保证、配置管理、项目计划等。
软件工程经济学是研究软件开发中成本的估算、成本效益分析的方法和技术,用经济学的基本原理来研究软件工程开发中的经济效益问题。
软件心理学是软件工程领域具有挑战性的一个全新的研究视角,它是从个体心理、人类行为、组织行为和企业文化等角度来研究软件管理和软件工程的。
2.软件工程的原则为了达到上述的软件工程目标,在软件开发过程中,必须遵循软件工程的基本原则。这些原则适用于所有的软件项目。这些基本原则包括抽象、信息隐蔽、模块化、局部化、确定性、一致性、完备性和可验证性。
①抽象。抽取事物最基本的特性和行为,忽略非本质细节。采用分层次抽象,自顶向下,逐层细化的办法控制软件开发过程的复杂性。
②信息隐蔽。采用封装技术,将程序模块的实现细节隐藏起来,使模块接口尽量简单。
③模块化。模块是程序中相对独立的成分,一个独立的编程单位,应有良好的接口定义。模块的大小要适中,模块过大会使模块内部的复杂性增加,不利于对模块的理解和修改,也不利于模块的调试和重用。模块太小会导致整个系统表示过于复杂,不利于控制系统的复杂性。
④局部化。要求在一个物理模块内集中逻辑上相互关联的计算资源,保证模块间具有松散的耦合关系,模块内部有较强的内聚性,这有助于控制解的复杂性。
⑤确定性。软件开发过程中所有概念的表达应是确定的、无歧义且规范的。这有助于人与人的交互不会产生误解和遗漏,以保证整个开发工作的协调一致。
⑥一致性。包括程序、数据和文档的整个软件系统的各模块应使用己知的概念、符号和术语;程序内外部接口应保持一致,系统规格说明与系统行为应保持一致。
⑦完备性。软件系统不丢失任何重要成分,完全实现系统所需的功能。
⑧可验证性。开发大型软件系统需要对系统自顶向下,逐层分解。系统分解应遵循容易检查、测评、评审的原则,以确保系统的正确性。
3.1.5软件开发工具与软件开发环境现代软件工程方法之所以得以实施,其重要的保证是软件开发工具和环境的保证,使软件在开发效率、工程质量等多方面得到改善。软件工程鼓励研制和采用各种先进的软件开发方法、工具和环境。工具和环境的使用进一步提高了软件的开发效率、维护效率和软件质量。
1.软件开发工具早期的软件开发除了一般的程序设计语言外,尚缺少工具的支持,致使编程工作量大,质量和进度难以保证,导致人们将很多的精力和时间花费在程序的编制和调试上,而在更重要的软件的需求和设计上反而得不到必要的精力和时间投入。软件开发工具的完善和发展将促进软件开发方法的进步和完善,促进软件开发的高速度和高质量。软件开发工具的发展是从单项工具的开发逐步向集成工具发展的,软件开发工具为软件工程方法提供了自动的或半自动的软件支撑环境。同时,软件开发方法的有效应用也必须得到相应工具的支持,否则方法将难以有效的实施。
2.软件开发环境软件开发环境或称软件工程环境是全面支持软件开发全过程的软件工具集合。这些软件工具按照一定的方法或模式组合起来,支持软件生命周期内的各个阶段和各项任务的完成。
计算机辅助软件工程(CASE,Computer Aided Software Engineering)是当前软件开发环境中富有特色的研究工作和发展方向。CASE将各种软件工具、开发机器和一个存放开发过程信息的中心数据库组合起来,形成软件工程环境。CASE的成功产品将最大限度地降低软件开发的技术难度并使软件开发的质量得到保证。
3.2 结构化分析方法软件开发方法是软件开发过程所遵循的方法和步骤,其目的在于有效地得到一些工作产品,即程序和文档,并且满足质量要求。 软件开发方法包括分析方法、设计方法和程序设计方法。
结构化方法经过30多年的发展,已经成为系统、成熟的软件开发方法之一。结构化方法包括已经形成了配套的结构化分析方法、结构化设计方法和结构化编程方法,其核心和基础是结构化程序设计理论。
3.2.1 需求分析与需求分析方法
1.需求分析软件需求是指用户对目标软件系统在功能、行为、性能、设计约束等方面的期望。需求分析的任务是发现需求、求精、建模和定义需求的过程。需求分析将创建所需的数据模型、功能模型和控制模型。
(1)需求分析的定义
1997年IEEE软件工程标准词汇表对需求分析定义如下:
①用户解决问题或达到目标所需的条件或权能;
②系统或系统部件要满足合同、标准、规范或其他正式规定文档所需具有的条件或权能;
③一种反映①或②所描述的条件或权能的文档说明。
由需求分析的定义可知,需求分析的内容包括:提炼、分析和仔细审查已收集到的需求;确保所有利益相关者都明白其含义并找出其中的错误、遗漏或其他不足的地方;从用户最初的非形式化需求到满足用户对软件产品的要求的映射;对用户意图不断进行提示和判断。
(2)需求分析阶段的工作需求分析阶段的工作,可以概括为四个方面:
①需求获取 需求获取的目的是确定对目标系统的各方面需求。涉及到的主要任务是建立获取用户需求的方法框架,并支持和监控需求获取的过程。
需求获取涉及的关键问题有:对问题空间的理解;人与人之间的通信;不断变化的需求。
需求获取是在同用户的交流过程中不断收集、积累用户的各种信息,并且通过认真理解用户的各项要求,澄清那些模糊的需求,排除不合理的,从而较全面地提炼系统的功能性与非功能性需求。一般功能性与非功能性需求包括系统功能、物理环境、用户界面、用户因素、资源、安全性、质量保证及其他约束。
要特别注意的是,在需求获取过程中,容易产生诸如与用户存在交流障碍,相互误解,缺乏共同语言,理解不完整,忽视需求变化,混淆目标和需求等问题,这些问题都将直接影响到需求分析和系统后续开发的成败。
②需求分析对获取的需求进行分析和综合,最终给出系统的解决方案和目标系统的逻辑模型。
③编写需求规格说明书 需求规格说明书作为需求分析的阶段成果,可以为用户、分析人员和设计人员之间的交流提供方便,可以直接支持目标软件系统的确认,又可以作为控制软件开发进程的依据。
④需求评审在需求分析的最后一步,对需求分析阶段的工作进行复审,验证需求文档的一致性、可行性、完整性和有效性。
2.需求分析方法常见的需求分析方法有:
①结构化分析方法。主要包括:面向数据流的结构化分析方法(SA--Structured analysis),面向数据结构的Jackson方法(JSD-Jackson system development method),面向数据结构的结构化数据系统开发方法(DSSD-Data structured system development method)。
②面向对象的分析方法(OOA-Object-Oriented method)。
从需求分析建立的模型的特性来分,需求分析方法又分为静态分析方法和动态分析方法。
3.2.2结构化分析方法
1.关于结构化分析方法结构化分析方法是结构化程序设计理论在软件需求分析阶段的运用。它是20世纪70年代中期倡导的基于功能分解的分析方法,其目的是帮助弄清用户对软件的需求。
对于面向数据流的结构化分析方法,按照DeMarco的定义,“结构化分析就是使用数据流图(DFD)、数据字典(DD)、结构化英语、判定表和判定树等工具,来建立一种新的、称为结构化规格说明的目标文档。”
结构化分析方法的实质是着眼于数据流,自顶向下,逐层分解,建立系统的处理流程,以数据流图和数据字典为主要工具,建立系统的逻辑模型。
结构化分析的步骤如下:
①通过对用户的调查,以软件的需求为线索,获得当前系统的具体模型;
②去掉具体模型中非本质因素,抽象出当前系统的逻辑模型;
③根据计算机的特点分析当前系统与目标系统的差别,建立目标系统的逻辑模型;
④完善目标系统并补充细节,写出目标系统的软件需求规格说明;
⑤评审直到确认完全符合用户对软件的需求。
2.结构化分析的常用工具
(1)数据流图(DFD--Data Flow Diagram)
数据流图是描述数据处理过程的工具,是需求理解的逻辑模型的图形表示,它直接支持系统的功能建模。
数据流图从数据传递和加工的角度,来刻画数据流从输入到输出的移动变换过程。数据流图中的主要图形元素与说明如下:
加工(转换)。输入数据经加工变换产生输出。
数据流。沿箭头方向传送数据的通道,一般在旁边标注数据流名。
存储文件(数据源)。表示处理过程中存放各种数据的文件。
源,潭。表示系统和环境的接口,属系统之外的实体。
一般通过对实际系统的了解和分析后,使用数据流图为系统建立逻辑模型。建立数据流图的步骤如下:
第1步:由外向里:先画系统的输入输出,然后画系统的内部。
第2步:自顶向下:顺序完成顶层、中间层、底层数据流图。
第3步:逐层分解。
数据流图的建立从顶层开始,顶层的数据流图形式如图3.2所示。顶层数据流图应该包含所有相关外部实体,以及外部实体与软件中间的数据流,其作用主要是描述软件的作用范围,对总体功能、输入、输出进行抽象描述,并反映软件和系统、环境的关系。
对复杂系统的表达应采用控制复杂度策略,需要按照问题的层次结构逐步分解细化,使用分层的数据流图表达这种结构关系,分层的数据流图的形式如图3.3所示。
为保证构造的数据流图表达完整、准确、规范,应遵循以下数据流图的构造规则和注意事项:
①对加工处理建立唯一、层次性的编号,且每个加工处理通常要求既有输入又有输出;
②数据存储之间不应该有数据流;
③数据流图的一致性。它包括数据守恒和数据存储文件的使用,即某个处理用以产生输出的数据没有输入,即出现遗漏,另一种是一个处理的某些输入并没有在处理中使用以产生输出;数据存储(文件)应被数据流图中的处理读和写,而不是仅读不写、或仅写不读;
④父图、子图关系与平衡规则。相邻两层DFD之间具有父、子关系,子图代表了父图中某个加工的详细描述,父图表示了子图间的接口。子图个数不大于父图中的处理个数。所有子图的输入、输出数据流和父图中相应处理的输入、输出数据流必须一致。
图3.4是银行取款业务的数据流图。
(2)数据字典(DD—Data Dictionary)
数据字典是结构化分析方法的核心。数据字典是对所有与系统相关的数据元素的一个有组织的列表,以及精确的、严格的定义,使得用户和系统分析员对于输入、输出、存储成分和中间计算结果有共同的理解。数据字典把不同的需求文档和分析模型紧密地结合在一起,与各模型的图形表示配合,能清楚地表达数据处理的要求。
概括地说,数据字典的作用是对DFD中出现的被命名的图形元素的确切解释。通常数据字典包含的信息有:名称、别名、何处使用/如何使用、内容描述、补充信息等。例如,对加工的描述应包括:加工名、反映该加工层次的加工编号、加工逻辑及功能简述、输入/输出数据流等。
在数据字典的编制过程中,常使用定义式方式描述数据结构。表3.1给出了常用的定义式符号。
表3.1数据字典定义式方式中出现的符号符 号
含 义
=
表示“等于”,“定义为”,“由什么构成”
[…|…]
表示“或”,即选择括号中用“|”号分隔的各项中的某一项
+
表示“与”,“和”
n{}m
表示“重复”,即括号中的项要重复若干次,n,m是重复次数的上下限
(…)
表示“可选”,即括号中的项可以没有
**
表示“注释”
..
连接符
例如,银行取款业务的数据流图中,存储文件“存折”的DD定义如下:
存折=户名+所号+账户+开户日+性质+(印密)+1{存取行)50
户名=2{字母}24
所号=“001”..“999”
账号=“00000001”..“99999999”
开户日=年+月+日
性质=“1”..“6”
印密=“0”
存取行=日期+(摘要)+支出+存入+余额+操作+复核
日期=年+月+日
年=“00”..“99”
月=“01”..“12”
日=“0l”..“31”
摘要=1{字母}4
支出=金额
金额=“0000000.01”..“9999999.99”
操作=“00001”..“99999”
(3)判定树使用判定树进行描述时,应先从问题定义的文字描述中分清哪些是判定的条件,哪些是判定的结论,根据描述材料中的连接词找出判定条件之间的从属关系、并列关系、选择关系,根据它们构造判定树。
例如,某货物托运管理系统中,对发货情况的处理要依赖检查发货单,检查发货单受货物托运金额、欠款等条件的约束,可以使用类似分段函数的形式来描述这些约束和处理。对这种约束条件的描述,如果使用自然语言,表达易出现不准确和不清晰。如果使用如图3.5所示的判定树来描述,则简捷清晰。
(4)判定表判定表与判定树相似,当数据流图中的加工要依赖于多个逻辑条件的取值,即完成该加工的一组动作是由于某一组条件取值的组合而引发的,使用判定表描述比较适宜。
判定表由四部分组成,如图3.6所示。其中标识为①的左上部称条件项,列出了各种可能的条件。标识为②的右上部称条件项,它列出了各种可能的条件组合。标识为③的左下部称基本动作项,它列出了所有的操作。标识为④的右下部称动作项,它列出在对应的条件组合下所选的操作。
图3.7为“检查发货单”判定表,其中“√”表示满足对应条件项时执行的操作。
判定表或判定树是以图形形式描述数据流图的加工逻辑,它结构简单,易读易懂。尤其遇到组合条件的判定,利用判定表或判定树可以使问题的描述清晰,而且便于直接映射到程序代码。在表达一个加工逻辑时,判定树、判定表都是好的描述工具,根据需要还可以交叉使用。
l
2
3
4
条件
发货单金额
>$500
>$500
≤$500
≤$500
赊欠情况
>60天
≤60天
>60天
≤60天
操作
不发出批准书
√
发出批准书
√
√
√
发出发货单
√
√
√
发出赊欠报告
√
图3.7“检查发货单”判定表
3.2.3软件需求规格说明书软件需求规格说明书(SRS,Software Requirement Specification)是需求分析阶段的最后成果,是软件开发中的重要文档之一。
1.软件需求规格说明书的作用软件需求规格说明书的作用是:
①便于用户、开发人员进行理解和交流。
②反映出用户问题的结构,可以作为软件开发工作的基础和依据。
③作为确认测试和验收的依据。
2.软件需求规格说明书的内容软件需求规格说明书是作为需求分析的一部分而制定的可交付文档。该说明把在软件计划中确定的软件范围加以展开,制定出完整的信息描述、详细的功能说明、恰当的检验标准以及其他与要求有关的数据。
软件需求规格说明书所包括的内容和书写框架如下:
一、概述
二、数据描述
·数据流图
·数据字典
·系统接口说明
·内部接口
三、功能描述
·功能
·处理说明
·设计的限制
四、性能描述
·性能参数
·测试种类
·预期的软件响应,
·应考虑的特殊问题
五、参考文献目录
六、附录其中,概述是从系统的角度描述软件的目标和任务。
数据描述是对软件系统所必须解决的问题作出的详细说明。
功能描述中描述了为解决用户问题所需要的每一项功能的过程细节。对每一项功能要给出处理说明和在设计时需要考虑的限制条件。
在性能描述中说明系统应达到的性能和应该满足的限制条件,检测的方法和标准,预期的软件响应和可能需要考虑的特殊问题。
参考文献目录中应包括与该软件有关的全部参考文献,其中包括前期的其他文档、技术参考资料、产品目录手册以及标准等。
附录部分包括一些补充资料。如列表数据、算法的详细说明、框图、图表和其他材料。
3.软件需求规格说明书的特点软件需求规格说明书是确保软件质量的有力措施,衡量软件需求规格说明书质量好坏的标准、标准的优先级及标准的内涵是:
①正确性。体现待开发系统的真实要求。
②无歧义性。对每一个需求只有一种解释,其陈述具有惟一性。
⑧完整性。包括全部有意义的需求,功能的、性能的、设计的、约束的,属性或外部接口等方面的需求。
④可验证性。描述的每一个需求都是可以验证的,即存在有限代价的有效过程验证确认。
⑤一致性。各个需求的描述不矛盾。
⑥可理解性。需求说明书必须简明易懂,尽量少包含计算机的概念和术语,以便用户和软件人员都能接受它。
⑦可修改性。SRS的结构风格在需求有必要改变时是易于实现的。
⑧可追踪性。每一个需求的来源、流向是清晰的,当产生和改变文件编制时,可以方便地引证每一个需求。
软件需求规格说明书是一份在软件生命周期中至关重要的文件,它在开发早期就为尚未诞生的软件系统建立了一个可见的逻辑模型,它可以保证开发工作的顺利进行,因而应及时地建立并保证它的质量。
作为设计的基础和验收的依据,软件需求规格说明书应该是精确而无二义性的,需求说明书越精确,则以后出现错误、混淆、反复的可能性越小。用户能看懂需求说明书,并且发现和指出其中的错误是保证软件系统质量的关键,因而需求说明书必须简明易懂,尽量少包含计算机的概念和术语,以便用户和软件人员双方都能接受它。
3.3 结构化设计方法
3.3.1软件设计的基本概念
1.软件设计的基础软件设计是软件工程的重要阶段,是一个把软件需求转换为软件表示的过程。软件设计的基本目标是用比较抽象概括的方式确定目标系统如何完成预定的任务,即软件设计是确定系统的物理模型。
软件设计的重要性和地位概括为以下几点:
①软件开发阶段(设计、编码、测试)占据软件项目开发总成本绝大部分,是在软件开发中形成质量的关键环节;
②软件设计是开发阶段最重要的步骤,是将需求准确地转化为完整的软件产品或系统的惟一途径;
③软件设计作出的决策,最终影响软件实现的成败;
④设计是软件工程和软件维护的基础。
从技术观点来看,软件设计包括软件结构设计、数据设计、接口设计、过程设计。其中,结构设计是定义软件系统各主要部件之间的关系;数据设计是将分析时创建的模型转化为数据结构的定义;接口设计是描述软件内部、软件和协作系统之间以及软件与人之间如何通信;过程设计则是把系统结构部件转换成软件的过程性描述。
从工程管理角度来看,软件设计分两步完成:概要设计和详细设计。概要设计(又称结构设计)将软件需求转化为软件体系结构、确定系统级接口、全局数据结构或数据库模式;详细设计确立每个模块的实现算法和局部数据结构,用适当方法表示算法和数据结构的细节。
软件设计的一般过程是:软件设计是一个迭代的过程;先进行高层次的结构设计;后进行低层次的过程设计;穿插进行数据设计和接口设计。
2.软件设计的基本原理软件设计遵循软件工程的基本目标和原则,建立了适用于在软件设计中应该遵循的基本原理和与软肿设计有关的概念。
(1)抽象抽象是一种思维工具,就是把事物本质的共同特性提取出来而不考虑其他细节。软件设计中考虑模块化解决方案时,可以定出多个抽象级别。抽象的层次从概要设计到详细设计逐步降低。在软件概要设计中的模块分层也是由抽象到具体逐步分析和构造出来的。
(2)模块化模块是指把一个待开发的软件分解成若干小的简单的部分。如高级语言中的过程、函数、子程序等。每个模块可以完成一个特定的子功能,各个模块可以按一定的方法组装起来成为一个整体,从而实现整个系统的功能。
模块化是指解决一个复杂问题时自顶向下逐层把软件系统划分成若干模块的过程。
为了解决复杂的问题,在软件设计中必须把整个问题进行分解来降低复杂性,这样就可以减少开发工作量并降低开发成本和提高软件生产率。但是划分模块并不是越多越好,因为这会增加模块之间接口的工作量,所以划分模块的层次和数量应该避免过多或过少。
(3)信息隐蔽信息隐蔽是指,在一个模块内包含的信息(过程或数据),对于不需要这些信息的其他模块来说是不能访问的。
(4)模块独立性模块独立性是指,每个模块只完成系统要求的独立的子功能,并且与其他模块的联系最少且接口简单。
模块的独立程度是评价设计好坏的重要度量标准。衡量软件的模块独立性使用耦合性和内聚性两个定性的度量标准。
①内聚性:内聚性是一个模块内部各个元素间彼此结合的紧密程度的度量。内聚是从功能角度来度量模块内的联系。
内聚有如下的种类,它们之间的内聚性由弱到强排列为:
偶然内聚:指一个模块内的各处理元素之间没有任何联系。
逻辑内聚:指模块内执行几个逻辑上相关的功能,通过参数确定该模块完成哪一个功能。
时间内聚:把需要同时或顺序执行的动作组合在一起形成的模块为时间内聚模块。比如初始化模块,它顺序为变量置初值。
过程内聚:如果一个模块内的处理元素是相关的,而且必须以特定次序执行则称为过程内聚。
通信内聚:指模块内所有处理功能都通过使用公用数据而发生关系。这种内聚也具有过程内聚的特点。
顺序内聚:指一个模块中各个处理元素和同一个功能密切相关,而且这些处理必须顺序执行,通常前一个处理元素的输出就是下一个处理元素的输入。
功能内聚:指模块内所有元素共同完成一个功能,缺一不可,模块已不可再分。这是最强的内聚。
内聚性是信息隐蔽和局部化概念的自然扩展。一个模块的内聚性越强则该模块的模块独立性越强。作为软件结构设计的设计原则,要求每一个模块的内部都具有很强的内聚性,它的各个组成部分彼此都密切相关。
②耦合性:耦合性是模块间互相连接的紧密程度的度量。
耦合性取决于各个模块之间接口的复杂度、调用方式以及哪些信息通过接口。耦合可以分为下列几种,它们之间的耦合度由高到低排列为:
内容耦合:如一个模块直接访问另一模块的内容,则这两个模块称为内容耦合。
公共耦合:若一组模块都访问同一全局数据结构,则它们之间的耦合称之为公共耦合。
外部耦合:一组模块都访问同一全局简单变量(而不是同一全局数据结构),且不通过参数表传递该全局变量的信息,则称为外部耦合。
控制耦合:若一模块明显地把开关量、名字等信息送入另一模块,控制另一模块的功能,则为控制耦合。
标记耦合:若两个以上的模块都需要其余某一数据结构子结构时,不使用其余全局变量的方式而是用记录传递的方式,即两模块间通过数据结构交换信息,这样的耦合称为标记耦合。
数据耦合:若一个模块访问另一个模块,被访问模块的输入和输出都是数据项参数,即两模块间通过数据参数交换信息,则这两个模块为数据耦合。
非直接耦合:若两个模块没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的,则称这两个模块为非直接耦合。非直接耦合独立性最强。
上面仅是对耦合机制进行的一个分类。可见,一个模块与其他模块的耦合性越强则该模块的模块独立性越弱。原则上讲,模块化设计总是希望模块之间的耦合表现为非直接耦合方式。但是,由于问题所固有的复杂性和结构化设计的原则,非直接耦合往往是不存在的。
耦合性与内聚性是模块独立性的两个定性标准,耦合与内聚是相互关联的。在程序结构中,各模块的内聚性越强,则耦合性越弱。一般较优秀的软件设计,应尽量做到高内聚,低耦合,即减弱模块之间的耦合性和提高模块内的内聚性,有利于提高模块的独立性。
3.结构化设计方法与结构化需求分析方法相对应的是结构化设计方法。结构化设计就是采用最佳的可能方法设计系统的各个组成部分以及各成分之间的内部联系的技术。也就是说,结构化设计是这样一个过程,它决定用哪些方法把哪些部分联系起来,才能解决好某个具体有清楚定义的问题。
结构化设计方法的基本思想是将软件设计成由相对独立、单一功能的模块组成的结构。下面重点以面向数据流的结构化方法为例讨论结构化设计方法。
3.3.2概要设计
1.概要设计的任务软件概要设计的基本任务是:
(1)设计软件系统结构在需求分析阶段,已经把系统分解成层次结构,而在概要设计阶段,需要进一步分解,划分为模块以及模块的层次结构。划分的具体过程是:
①采用某种设计方法,将一个复杂的系统按功能划分成模块。
②确定每个模块的功能。
③确定模块之间的调用关系。
④确定模块之间的接口,即模块之间传递的信息。
⑤评价模块结构的质量。
(2)数据结构及数据库设计数据设计是实现需求定义和规格说明过程中提出的数据对象的逻辑表示。数据设计的具体任务是:确定输入、输出文件的详细数据结构;结合算法设计,确定算法所必需的逻辑数据结构及其操作;确定对逻辑数据结构所必须的那些操作的程序模块,限制和确定各个数据设计决策的影响范围;需要与操作系统或调度程序接口所必需的控制表进行数据交换时,确定其详细的数据结构和使用规则;数据的保护性设计:防卫性、一致性、冗余性设计。
数据设计中应注意掌握以下设计原则:
①用于功能和行为的系统分析原则也应用于数据。
②应该标识所有的数据结构以及其上的操作。
③应当建立数据字典,并用于数据设计和程序设计。
④低层的设计决策应该推迟到设计过程的后期。
⑤只有那些需要直接使用数据结构、内部数据的模块才能看到该数据的表示。
⑥应该开发一个由有用的数据结构和应用于其上的操作组成的库。
⑦软件设计和程序设计语言应该支持抽象数据类型的规格说明和实现。
(3)编写概要设计文档。在概要设计阶段,需要编写的文档有,概要设计说明书、数据库设计说明书、集成测试计划等。
(4)概要设计文档评审。在概要设计中,对设计部分是否完整地实现了需求中规定的功能、性能等要求,设计方案的可行性,关键的处理及内外部接口定义正确性、有效性,各部分之间的一致性等都要进行评审,以免在以后的设计中出现大的问题而返工。
常用的软件结构设计工具是结构图(SC Structure Chart),也称程序结构图。使用结构图描述软件系统的层次和分块结构关系,它反映了整个系统的功能实现以及模块与模块之间的联系与通讯,是未来程序中的控制层次体系。
结构图是描述软件结构的图形工具。结构图的基本图符如图3.8所示。
模块用一个矩形表示,矩形内注明模块的功能和名字;箭头表示模块间的调用关系。在结构图中还可以用带注释的箭头表示模块调用过程中来回传递的信息。如果希望进一步标明传递的信息是数据还是控制信息,则可用带实心圆的箭头表示传递的是控制信息,用带空心圆的箭心表示传递的是数据。
根据结构化设计思想,结构图构成的基本形式如图3.9所示。
经常使用的结构图有四种模块类型:传入模块,传出模块、变换模块和协调模块。其表示形式和含义如图3.10所示。
下面通过图3.11进一步了解程序结构图的有关术语。
深度:表示控制的层数。
上级模块、从属模块:上、下两层模块a和b,且有a调用b,则a是上级模块,b是从属模块。
宽度:整体控制跨度(最大模块数的层)的表示。
扇入:调用一个给定模块的模块个数。
扇出:一个模块直接调用的其他模块数。
原子模块:树中位于叶子结点的模块。
2.面向数据流的设计方法在需求分析阶段,主要是分析信息在系统中加工和流动的情况。面向数据流的设计方法定义了一些不同的映射方法,利用这些映射方法可以把数据流图变换成结构图表示的软件结构。首先需要了解数据流图表示的数据处理的类型,然后针对不同类型分别进行分析处理。
(1)数据流类型典型的数据流类型有两种:变换型和事务型。
①变换型。变换型是指信息沿输入通路进入系统,同时由外部形式变换成内部形式,进入系统的信息通过变换中心,经加工处理以后再沿输出通路变换成外部形式离开软件系统。变换型数据处理问题的工作过程大致分为三步,即取得数据、变换数据和输出数据,如图3.12所示。相应于取得数据、变换数据、输出数据的过程,变换型系统结构图由输入、中心变换和输出等三部分组成,如图3.13所示。
变换型数据流图映射的结构图如图3.14所示。
②事务型。在很多软件应用中,存在某种作业数据流,它可以引发一个或多个处理,这些处理能够完成该作业要求的功能,这种数据流就叫做事务。事务型数据流的特点是接受一项事务,根据事务处理的特点和性质,选择分派一个适当的处理单元(事务处理中心),然后给出结果。这类数据流归为特殊的一类,称为事务型数据流,如图3.15所示。在一个事务型数据流中,事务中心接收数据,分析每个事务以确定它的类型,根据事务类型选取一条活动通路。
事务型数据流图映射的结构图如图3.16所示。
在事务型数据流系统结构图中,事务中心模块按所接受的事务类型,选择某一事务处理模块执行,各事务处理模块并列。每个事务处理模块可能要调用若干个操作模块,而操作模块又可能调用若干个细节模块。
(2)面向数据流设计方法的实施要点与设计过程面向数据流的结构设计过程和步骤是:
第1步:分析、确认数据流图的类型,区分是事务型还是变换型。
第2步:说明数据流的边界。
第3步:把数据流图映射为程序结构。对于事务流区分事务中心和数据接收通路,将它映射成事务结构。对于变换流,区分输出和输入分支,并将其映射成变换结构。
第4步:根据设计准则对产生的结构进行细化和求精。
下面分别讨论变换型和事务型数据流图转换成程序结构图的实施步骤。
①变换型将变换型映射成结构图,又称为变换分析。其步骤如下:
第1步:确定数据流图是否具有变换特性。一般地说,一个系统中所有的信息流都可以认为是变换流,但是,当遇有明显的事务特性的信息流时,建议采用事务分析方法进行设计。在这时应该观察在整个数据流图中哪种属性占优势,先确定数据流的全局特性。此外还应把具有全局特性的不同特点的局部区域孤立出来,根据这些子数据流的特点作部分的处理。
第2步:确定输入流和输出流的边界,划分出输入、变换和输出,独立出变换中心。
第3步:进行第一级分解,将变换型映射成软件结构(参见图3.15),其中输入数据处理模块协调对所有输入数据的接收:变换中心控制模块管理对内部形式的数据的所有操作;输出数据处理控制模块协调输出信息的产生过程。
第4步:按上述步骤如出现事务流也可按事务流的映射方式对各个子流进行逐级分解,直至分解到基本功能。
第5步:对每个模块写一个简要说明,内容包括该模块的接口描述、模块内部的信息、过程陈述、包括的主要判定点及任务等。
第6步:利用软件结构的设计原则对软件结构进一步转化。
②事务型将事务型映射成结构图,又称为事务分析。其步骤如下:
事务分析的设计步骤与变换分析设计步骤大致类似,主要差别仅在于由数据流图到软件结构的映射方法不同(参见图3.14和3.16)。它是将事务中心映射成为软件结构中发送分支的调度模块,将接收通路映射成软件结构的接收分支。
3.设计的准则大量软件设计的实践证明,以下的设计准则是可以借鉴为设计的指导和对软件结构图进行优化。这些准则是:
①提高模块独立性。对软件结构应着眼于改善模块的独立性,依据降低耦合提高内聚的原则,通过把一些模块取消或合并来修改程序结构。
②模块规模适中。经验表明,当模块增大时,模块的可理解性迅速下降。但是当对大的模块分解时,不应降低模块的独立性。因为,当对一个大的模块分解时,有可能会增加模块间的依赖。
③深度、宽度、扇出和扇入适当。如果深度过大,则说明有的控制模块可能简单了。如果宽度过大,则说明系统的控制过于集中。而扇出过大则意味模块过分复杂,需要控制和协调过多的下级模块,这时应适当增加中间层次。扇出太小则可以把下级模块进一步分解成若干个子功能模块,或者合并到上级模块中去。扇入越大则共享该模块的上级模块数目越多。
经验表明,好的软件设计结构通常顶层高扇出,中间扇出较少,底层高扇入。
④使模块的作用域在该模块的控制域内。模块的作用域是指模块内一个判定的作用范围,凡是受这个判定影响的所有模块都属于这个判定的作用域。模块的控制域是指这个模块本身以及所有直接或间接从属于它的模块的集合。在一个设计得很好的系统中,所有受某个判定影响的模块应该都从属于做出判定的那个模块,最好局限于做出判定的那个模块本身及它的直属下级模块。对于那些不满足这一条件的软件结构,修改的办法是:将判定点上移或者将那些在作用范围内但是不在控制范围内的模块移到控制范围以内。
⑤应减少模块的接口和界面的复杂性。模块的接口复杂是软件容易发生错误的一个主要原因。应该仔细设计模块接口,使得信息传递简单并且和模块的功能一致。
⑥设计成单入口、单出口的模块。
⑦设计功能可预测的模块。如果一个模块可以当作一个“黑盒”,也就是不考虑模块的内部结构和处理过程,则这个模块的功能就是可以预测的。
3.3.3详细设计详细设计的任务,是为软件结构图中的每一个模块确定实现算法和局部数据结构,用某种选定的表达工具表示算法和数据结构的细节。表达工具可以由设计人员自由选择,但它应该具有描述过程细节的能力,而且能够使程序员在编程时便于直接翻译成程序设计语言的源程序。本节重点对过程设计进行讨论。
在过程设计阶段,要对每个模块规定的功能以及算法的设计,给出适当的算法描述,即确定模块内部的详细执行过程,包括局部数据组织、控制流、每一步具体处理要求和各种实现细节等。其目的是确定应该怎样来具体实现所要求的系统。
常见的过程设计工具有:
图形工具:程序流程图,N-S,PAD,HIPO。
表格工具:判定表。
语言工具:PDL(伪码)。
下面讨论其中几种主要的工具:
1.程序流程图程序流程图是一种传统的、应用广泛的软件过程设计表示工具,通常也称为程序框图。程序流程图表达直观、清晰,易于学习掌握,且独立于任何一种程序设计语言。
构成程序流程图的最基本图符及含义如图3.17所示。
按照结构化程序设计的要求,程序流程图构成的任何程序描述限制为如图3.18所示的5种控制结构。
图3.18所示的程序流程图构成的5种控制结构的含义是:
顺序型:几个连续的加工步骤依次排列构成;
选择型:由某个逻辑判断式的取值决定选择两个加工中的一个;
先判断重复型:先判断循环控制条件是否成立,成立则执行循环体语句;
后判断重复型:重复执行某些特定的加工,直到控制条件成立;
多分支选择型:列举多种加工情况,根据控制变量的取值,选择执行其中之一。
通过把程序流程图的5种基本控制结构相互组合或嵌套,可以构成任何复杂的程序流程图。
例如,下面是简单托运货物运费计算的问题。
设货物重量x,客户信息y,输入x、y后,计算运费的具体要求是:
如果O<x≤15(设为条件1),则用公式1计算后,循环3次完成同样的“记账”和“输出”操作,然后程序结束;
如果x>15(设为条件2),则用公式2计算后,循环3次完成同样的“记账”和“输出”操作,然后程序结束。
该问题程序的程序流程图描述如图3.19。
程序流程图虽然简单易学,但是若程序员不受任何约束,随意转移控制,会破坏结构化设计的原则,而且程序流程图不易表示数据结构。
2.N-S图为了避免流程图在描述程序逻辑时的随意性与灵活性,1973年Nossi和Shneiderman发表了题为“结构化程序的流程图技术”的文章,提出了用方框图来代替传统的程序流程图,通常也把这种图称为N-S图。
N-S图的基本图符及表示的5种基本控制结构如图3.20所示。
例如,上述问题程序的N-S图描述如图3.21所示。
N-S图有以下特征:
(1)每个构件具有明确的功能域;
(2)控制转移必须遵守结构化设计要求;
(3)易于确定局部数据和(或)全局数据的作用域;
(4)易于表达嵌套关系和模块的层次结构;
3.PAD图
PAD图是问题分析图(Problem Analysis Diagram)的英文缩写。它是继程序流程图和方框图之后,提出的又一种主要用于描述软件详细设计的图形表示工具。
PAD图的基本图符及表示的5种基本控制结构,如图3.22所示。
例如,上述问题程序的PAD图描述如图3.23所示。
PAD图有以下特征:
①结构清晰,结构化程度高;
②易于阅读;
③最左端的纵线是程序主干线,对应程序的第一层结构;每增加一层PAD图向右扩展一条纵线,故程序的纵线数等于程序层次数。
④程序执行:从PAD图最左主干线上端结点开始,自上而下、自左向右依次执行,程序终止于最左主干线。
4.PDL(Procedure Design Lauguage)
过程设计语言(PDL)也称为结构化的英语和伪码,它是一种混合语言,采用英语的词汇和结构化程序设计语言的语法,类似编程语言。
用PDL表示的基本控制结构的常用词汇如下:
顺序:
条件:IF/THEN/ELSE/ENDIF
循环:DO WHILE/ENDDO
循环:REPEAT UNTIL/ENDREPEAT
分支:CASE_OF/WHEN/SELECT/WHEN/SELECT/ENDCASE
例如,上述问题程序的描述如下,它是类似C语言的PDL。
/*计算运费*/
count();
{输入x;输入y;
if (0<x≤15)条件1{公式1计算;call sub;}
else if(x>15){公式2计算;call sub;}
}
sub();
{for(i=1,3,i++) do{记账;输出;}
}
PDL可以由编程语言转换得到,也可以是专门为过程描述而设计的。但应具备以下特征:
①有为结构化构成元素、数据说明和模块化特征提供的关键词语法;
②处理部分的描述采用自然语言语法:
③可以说明简单和复杂的数据结构;
④支持各种接口描述的子程序定义和调用技术。
3.4 软件测试随着计算机软、硬件技术的发展,计算机的应用领域越来越广泛,方方面面的应用对软件的功能要求也就越来越强,而且软件的复杂程度也就越来越高。但是,如何才能确保软件的质量并保证软件的高度可靠性呢?无疑,通过对软件产品进行必要的测试是非常重要的一个环节。软件测试也是在软件投入运行前对软件需求、设计、编码的最后审核。
软件测试的投入,包括人员和资金投入是巨大的,通常其工作量、成本占软件开发总工作量、总成本的40%以上,而且具有很高的组织管理和技术难度。
软件测试是保证软件质量的重要手段,其主要过程涵盖了整个软件生命期的过程,包括需求定义阶段的需求测试、编码阶段的单元测试、集成测试以及后期的确认测试、系统测试,验证软件是否合格、能否交付用户使用等。
3.4.1软件测试的目的
1983年IEEE将软件测试定义为:使用人工或自动手段来运行或测定某个系统的过程,其目的在于检验它是否满足规定的需求或是弄清预期结果与实际结果之间的差别。
关于软件测试的目的,Grenford J.Myers在《The ArtofSoftwareTesting》一书中给出了更深刻的阐述:
软件测试是为了发现错误而执行程序的过程;
一个好的测试用例是指很可能找到迄今为止尚未发现的错误的用例;
一个成功的测试是发现了至今尚未发现的错误的测试。
Myers的观点告诉人们测试要以查找错误为中心,而不是为了演示软件的正确功能。
3.4.2软件测试的准则鉴于软件测试的重要性,要做好软件测试,设计出有效的测试方案和好的测试用例,软件测试人员需要充分理解和运用软件测试的一些基本准则:
1.所有测试都应追溯到需求软件测试的目的是发现错误,而最严重的错误不外乎是导致程序无法满足用户需求的错误。
2.严格执行测试计划,排除测试的随意性软件测试应当制定明确的测试计划并按照计划执行。测试计划应包括:所测软件的功能、输入和输出、测试内容、各项测试的目的和进度安排、测试资料、测试工具、测试用例的选择、资源要求、测试的控制方式和过程等。
3.充分注意测试中的群集现象经验表明,程序中存在错误的概率与该程序中已发现的错误数成正比。这一现象说明,为了提高测试效率,测试人员应该集中对付那些错误群集的程序。
4.程序员应避免检查自己的程序为了达到好的测试效果,应该由独立的第三方来构造测试。因为从心理学角度讲,程序人员或设计方在测试自己的程序时,要采取客观的态度是程度不同地存在障碍的。
5.穷举测试不可能所谓穷举测试是指把程序所有可能的执行路径都进行检查的测试。但是,即使规模较小的程序,其路径排列数也是相当大的,在实际测试过程中不可能穷尽每一种组合。这说明,测试只能证明程序中有错误,不能证明程序中没有错误。
6.妥善保存测试计划、测试用例、出错统计和最终分析报告,为维护提供方便。
3.4.3软件测试技术与方法综述软件测试的方法和技术是多种多样的。对于软件测试方法和技术,可以从不同的角度加以分类。
若从是否需要执行被测软件的角度,可以分为静态测试和动态测试方法。若按照功能划分可以分为白盒测试和黑盒测试方法。
1.静态测试与动态测试
(1)静态测试静态测试包括代码检查、静态结构分析、代码质量度量等。静态测试可以由人工进行,充分发挥人的逻辑思维优势,也可以借助软件工具自动进行。经验表明,使用人工测试能够有效地发现30%到70%的逻辑设计和编码错误。
代码检查主要检查代码和设计的一致性,包括代码的逻辑表达的正确性,代码结构的合理性等方面。这项工作可以发现违背程序编写标准的问题,程序中不安全、不明确和模糊的部分,找出程序中不可移植部分、违背程序编程风格的问题,包括变量检查、命名和类型审查、程序逻辑审查、程序语法检查和程序结构检查等内容。代码检查包括代码审查、代码走查、桌面检查、静态分析等具体方式。
代码审查:小组集体阅读、讨论检查代码。
代码走查:小组成员通过用“脑”研究、执行程序来检查代码。
桌面检查:由程序员自己检查自己编写的程序。程序员在程序通过编译之后,进行单元测试之前,对源代码进行分析、检验,并补充相关文档,目的是发现程序的错误。
静态分析:对代码的机械性、程式化的特性分析方法,包括控制流分析、数据流分析、接口分析、表达式分析。
(2)动态测试静态测试不实际运行软件,主要通过人工进行。动态测试是基于计算机的测试,是为了发现错误而执行程序的过程。或者说,是根据软件开发各阶段的规格说明和程序的内部结构而精心设计一批测试用例(即输入数据及其预期的输出结果),并利用这些测试用例去运行程序,以发现程序错误的过程。
设计高效、合理的测试用例是动态测试的关键。测试用例(Test Case)是为测试设计的数据。测试用例由测试输入数据和与之对应的预期输出结果两部分组成。测试用例的格式为:
[(输入值集),(输出值集)]
下面重点讨论动态的白盒测试方法和黑盒测试方法。
2.白盒测试方法与测试用例设计白盒测试方法也称结构测试或逻辑驱动测试。它是根据软件产品的内部工作过程,检查内部成分,以确认每种内部操作符合设计规格要求。白盒测试把测试对象看作一个打开的盒子,允许测试人员利用程序内部的逻辑结构及有关信息来设计或选择测试用例,对程序所有的逻辑路径进行测试。通过在不同点检查程序的状态来了解实际的运行状态是否与预期的一致。所以,白盒测试是在程序内部进行,主要用于完成软件内部操作的验证。
白盒测试的基本原则是:保证所测模块中每一独立路径至少执行一次;保证所测模块所有判断的每一分支至少执行一次;保证所测模块每一循环都在边界条件和一般条件下至少各执行一次:验证所有内部数据结构的有效性。
按照白盒测试的基本原则,“白盒”法是穷举路径测试。在使用这一方案时,测试者必须检查程序的内部结构,从检查程序的逻辑着手,得出测试数据。贯穿程序的独立路径数是天文数字,但即使每条路径都测试了仍然可能有错误。第一,穷举路径测试决不能查出程序是否违反了设计规范,即程序本身是个错误的程序;第二,穷举路径测试不可能查出程序中因遗漏路径而出错;第三,穷举路径测试可能发现不了一些与数据相关的错误。
白盒测试的主要方法有逻辑覆盖、基本路径测试等。
(1)逻辑覆盖测试逻辑覆盖是泛指一系列以程序内部的逻辑结构为基础的测试用例设计技术。通常所指的程序中的逻辑表示有判断、分支、条件等几种表示方式。
①语句覆盖。选择足够的测试用例,使得程序中每个语句至少都能被执行一次。
例3.1设有程序流程图表示的程序如图3.24。
按照语句覆盖的测试要求,对图3.24的程序设计如下测试用例1和测试用例2。
语句覆盖是逻辑覆盖中基本的覆盖,尤其对单元测试来说。但是语句覆盖往往没有关注判断中的条件有可能隐含的错误。
②路径覆盖。执行足够的测试用例,使程序中所有可能的路径都至少经历一次。
例3.2设有程序流程图表示的程序如图3.25。
对图3.25的程序设计如表3.2列出的一组测试用例,就可以覆盖该程序的全部4条路径:ace,abd,abe,acd。
表3.2
测试用例
通过路径
测试用例
通过路径
[(A=2,B=0,X=3),(输出略)]
(ace)
[(A=2,B=1,X=1),(输出略)]
(abe)
[(A=1,B=0,X=1),(输出略)]
(abd)
[(A=3,B=0,X=1),(输出略)]
(acd)
③判定覆盖。使设计的测试用例保证程序中每个判断的每个取值分支(T或F)至少经历一次。
根据判定覆盖的要求,对如图3.26所示的程序,如果其中包含条件i≥j的判断为真值(即为“T”)和为假值(即为“F”)的程序执行路径至少经历一次,仍然可以使用例3.1的测试用例1和测试用例2。
程序每个判断中若存在多个联立条件,仅保证判断的真假值往往会导致某些单个条件的错误不能被发现。例如,某判断是“x<l或y>5”,其中只要一个条件取值为真,无论另一个条件是否错误,判断的结果都为真。这说明,仅有判断覆盖还无法保证能查出在判断的条件中的错误,需要更强的逻辑覆盖。
④条件覆盖。设计的测试用例保证程序中每个判断的每个条件的可能取值至少执行一次。
例3.3设有程序流程图表示的程序如图3.26。
按照条件覆盖的测试要求,对图3.26的程序判断框中的条件i≥j和条件j<5设计如下测试用例1和测试用例2,就能保证该条件取真值和取假值的情况至少执行一次。
条件覆盖深入到判断中的每个条件,但是可能会忽略全面的判断覆盖的要求。有必要考虑判断一条件覆盖。
⑤判断一条件覆盖。设计足够的测试用例,使判断中每个条件的所有可能取值至少执行一次,同时每个判断的所有可能取值分支至少执行一次。
例3.4设有程序流程图表示的程序如图3.27。
按照判断一条件覆盖的测试要求,对图3.27程序的两个判断框的每个取值分支至少经历一次,同时两个判断框中的三个条件的所有可能取值至少执行一次,设计如下测试用例1、测试用例2和测试用例3,就能保证满足判断一条件覆盖。
判断一条件覆盖也有缺陷,对质量要求高的软件单元,可根据情况提出多重条件组合覆盖以及其他更高的覆盖要求。
(2)基本路径测试基本路径测试的思想和步骤是,根据软件过程性描述中的控制流程确定程序的环路复杂性度量,用此度量定义基本路径集合,并由此导出一组测试用例对每一条独立执行路径进行测试。
例3.5设有程序流程图表示的程序如图3.28。
对图3.28的程序流程图确定程序的环路复杂度,方法是:
环路复杂度=程序流程图中的判断框个数+1则环路复杂度的值即为要设计测试用例的基本路径数,图3.28所示的程序环路复杂度为3,设计如表3.3列出的一组测试用例,覆盖的基本路径是:abf,acef.acdf。
表3.3
测试用例
通过路径
[(A=-2,B=0),(输出略)]
(abf)
[(A=5,B=0),(输出略)]
(acef)
[(A=5,B=5),(输出略)]
(acdf)
3.黑盒测试方法与测试用例设计黑盒测试方法也称功能测试或数据驱动测试。黑盒测试是对软件已经实现的功能是否满足需求进行测试和验证。黑盒测试完全不考虑程序内部的逻辑结构和内部特性,只依据程序的需求和功能规格说明,检查程序的功能是否符合它的功能说明。所以,黑盒测试是在软件接口处进行,完成功能验证。黑盒测试只检查程序功能是否棼照需求规格说明书的规定正常使用,程序是否能适当地接收输入数据而产生正确的输出信息,并且保持外部信息(如数据库或文件)的完整性。
黑盒测试主要诊断功能不对或遗漏、界面错误、数据结构或外部数据库访问错误、性能错误、初始化和终止条件错。
黑盒测试方法主要有等价类划分法、边界值分析法、错误推测法、因果图等,主要用于软件确认测试。
(1)等价类划分法等价类划分法是一种典型的黑盒测试方法。它是将程序的所有可能的输入数据划分成若干部分(及若干等价类),然后从每个等价类中选取数据作为测试用例。对每一个等价类,各个输入数据对发现程序中的错误的几率都是等效的,因此只需从每个等价类中选取一些有代表性的测试用例进行测试而发现错误。
使用等价类划分法设计测试方案,首先需要划分输入集合的等价类。等价类包括:
①有效等价类:合理、有意义的输入数据构成的集合。可以检验程序中符合规定的功能、性能。
②无效等价类:不合理、无意义的输入数据构成的集合。可以检验程序中不符合规定的功能、性能。
为此,需要研究程序的功能说明,从而确定输入数据的有效等价类和无效等价类。
等价类划分法实施步骤分为两步:
第l步:划分等价类;
第2步:根据等价类选取相应的测试用例。
例3.6程序实现输入3个边长(设为a,b,c),判断能否构成三角形。对该程序考虑等价类划分法。
满足测试三角形构成条件程序的等价类划分如表3.4所示。
表3.4
输入条件
有效等价类
无效等价类
①边长a,b,c限制
a>0或b>0或c>0
a<=0或b<=0或c<=0
②边长关系限制
a+b>c或b+c>a或a+c>b
a+b<=c或b+c<=a或a+c<=b
根据表3.4划分的等价类,可以设计以下的测试用例:
对满足输入条件①和②的有效等价类设计的测试用例:
[(a=3,b=4,c=5),(符合三角形构成条件)]
对满足输入条件①的无效等价类设计的测试用例:
[(a=-3,b=4,c=5),(无效输入)]
对满足输入条件②的无效等价类设计的测试用例:
[(a=3,b=4,c=8),(无效输入)]
划分等价类常用的几条原则是:
若输入条件规定了确切的取值范围,则可划分出一个有效等价类和两个无效等价类;
若输入条件规定了输入值的集合(或有“必须如何”的条件),可确定一个有效等价类和一个无效等价类;
若输入条件是一个布尔量,则可确定一个有效等价类和一个无效等价类;
若输入数据是一组值,且程序要对每个值分别处理。可为每个输入值确定一个有效等价类和一个无效等价类;
若规定了输入数据必须遵守一定规则,则可确定一个有效等价类和若干个无效等价类;
若已划分的等价类中各元素在程序中处理方式不同,须将该等价类进一步划分(更小的等价类)。
(2)边界值分析法边界值分析法是对各种输入、输出范围的边界情况设计测试用例的方法。
经验表明,程序错误最容易出现在输入或输出范围的边界处,而不是在输入范围的内部。因此针对各种边界情况设计测试用例,可以查出更多的错误。
使用边界值分析方法设计测试用例,确定边界情况应考虑选取正好等于,刚刚大于,或刚刚小于边界的值作为测试数据,这样发现程序中错误的概率较大。
边界值分析方法的使用要注意以下几点:
①如果输入条件规定了取值范围或数据个数,则可选择正好等于边界值、刚刚在边界范围内和刚刚超越边界外的值进行测试;
②针对规格说明的每个输入条件,使用上述原则;
③对于有序数列,选择第一个和最后一个作为测试数据。
例如,对例3.6中的判断三角形构成的程序,如果在等价类划分法中加入边界值分析的思想,即选取该等价类的边界值,则会使等价类划分法更有效。考虑等价类划分法加入边界值分析的例3.6的测试用例可以设计如下:
对满足输入条件①的无效等价类设计的测试用例:
[(a=0,b=4,c=5),(无效输入)]
或[(a=3,b=0,c=5),(无效输入)]
或[(a=3,b=4,c=0),(无效输入)]
对满足输入条件②的无效等价类设计的测试用例:
[(a=3,b=4,c=7),(无效输入)]
或[(a=9,b=4,c=5),(无效输入)]
或[(a=3,b=8,c=5),(无效输入)]
一般多用边界值分析法来补充等价类划分方法。
(3)错误推测法人们可以靠经验和直觉推测程序中可能存在的各种错误,从而有针对性地编写检查这些错误的例子,这就是错误推测法。
错误推测法的基本想法是:列举出程序中所有可能有的错误和容易发生错误的特殊情况,根据它们选择测试用例。错误推测法针对性强,可以直接切入可能的错误,直接定位,是一种非常实用、有效的方法。但是它需要丰富的经验和专业知识。
错误推测法的实施步骤一般是,对被测软件首先列出所有可能有的错误和易错情况表,然后基于该表设计测试用例。
例如,一般程序中输入为“0”或输出为“0”的情形是易错情况,测试者可以设计输入值为0的测试情况,以及使输出强迫为0的测试情况。
例如,要测试一个排序子程序,特别需要检查的情况是:输入表为空;输入表只含有一个元素;输入表的所有元素的值都相同;输入表已经排过序。这些隋况都是在程序设计时可能忽略的特殊情况。
实际上,无论是使用白盒测试方法还是黑盒测试方法,、或是其他测试方法,针对一种方法设计的测试用例,仅仅是易于发现某种类型的错误,对其他类型的错误不易发现。所以没有一种用例设计方法能适应全部的测试方案,而是各有所长。综合使用各种方法来确定合适的测试方案,应该考虑在测试成本和测试效果之间的一个合理折中。
3.4.4软件测试的实施软件测试是保证软件质量的重要手段,软件测试是一个过程,其测试流程是该过程规定的程序,目的是使软件测试工作系统化。
软件测试过程一般按4个步骤进行,即单元测试、集成测试、验收测试(确认测试)和系统测试。通过这些步骤的实施来验证软件是否合格,能否交付用户使用。
1.单元测试单元测试是对软件设计的最小单位——模块(程序单元)进行正确性检验的测试。单元测试的目的是发现各模块内部可能存在的各种错误。
单元测试的依据是详细设计说明书和源程序。
单元测试的技术可以采用静态分析和动态测试。对动态测试通常以白盒动态测试为主,辅之以黑盒测试。
单元测试主要针对模块的下列5个基本特性进行:
①模块接口测试——测试通过模块的数据流。例如,检查模块的输入参数和输出参数、全局量、文件属性与操作等都属于模块接口测试的内容。
②局部数据结构测试。例如,检查局部数据说明的一致性,数据的初始化,数据类型的一致以及数据的下溢、上溢等。
③重要的执行路径的检查。
④出错处理测试。检查模块的错误处理功能。
⑤影响以上各点及其他相关点的边界条件测试。
单元测试是针对某个模块,这样的模块通常并不是一个独立的程序,因此模块自己不能运行,而要靠辅助其他模块调用或驱动。同时,模块自身也会作为驱动模块去调用其他模块,也就是说,单元测试要考虑它和外界的联系,必须在一定的环境下进行,这些环境可以是真实的也可以是模拟的。模拟环境是单元测试常用的。
所谓模拟环境就是在单元测试中,用一些辅助模块去模拟与被测模块的相联系的其他模块,即为被测模块设计和搭建驱动模块和桩模块,如图3.29所示。
其中,驱动(Driver)模块相当于被测模块的主程序。它接收测试数据,并传给被测模块,输出实际测试结果。桩(Stub)模块通常用于代替被测模块调用的其他模块,其作用仅做少量的数据操作,是一个模拟子程序,不必将子模块的所有功能带入。
2.集成测试集成测试是测试和组装软件的过程。它是把模块在按照设计要求组装起来的同时进行测试,主要目的是发现与接口有关的错误。集成测试的依据是概要设计说明书。
集成测试所涉及的内容包括:软件单元的接口测试、全局数据结构测试、边界条件和非法输入的测试等。
集成测试时将模块组装成程序通常采用两种方式:非增量方式组装与增量方式组装。
非增量方式也称为一次性组装方式。将测试好的每一个软件单元一次组装在一起再进行整体测试。
增量方式是将已经测试好的模块逐步组装成较大系统,在组装过程中边连接边测试,以发现连接过程中产生的问题。最后通过增殖,逐步组装到所要求的软件系统。
增量方式包括自顶向下、自底向上、自顶向下与自底向上相结合的混合增量方法。
(1)自顶向下的增量方式将模块按系统程序结构,从主控模块(主程序)开始,沿控制层次自项向下地逐个把模块连接起来。自顶向下的增量方式在测试过程中能较早地验证主要的控制和判断点。
自项向下集成的过程与步骤如下:
①主控模块作为测试驱动器。直接附属于主控模块的各模块全都用桩模块代替。
②按照一定的组装次序,每次用一个真模块取代一个附属的桩模块。
③当装入每个真模块时都要进行测试。
④做完每一组测试后再用一个真模块代替另一个桩模块。
⑤可以进行回归测试(即重新再做过去做过的全部或部分测试),以便确定没有新的错误发生。
例3.7对图3.30(a)所示程序结构进行自顶向下的增量方式组装测试。
自顶向下的增量方式的组装过程如图3.30(b)~(f)所示。
(2)自底向上的增量方式自底向上集成测试方法是从软件结构中最底层的、最基本的软件单元开始进行集成和测试。在模块的测试过程中需要从子模块得到的信息可以直接运行子模块得到。由于在逐步向上组装过程中下层模块总是存在的,因此不再需要桩模块,但是需要调用这些模块的驱动模块。
自底向上集成的过程与步骤如下:
①低层的模块组成簇,以执行某个特定的软件子功能。
②编写一个驱动模块作为测试的控制程序,和被测试的簇连在一起,负责安排测试用例的输入及输出。
⑧对簇进行测试。
④拆去各个小簇的驱动模块,把几个小簇合并成大簇,再重复做②、③及④步。 这样在软件结构上逐步向上组装。
例3.8对图3.31(a)所示程序结构进行自底向上的增量方式的组装测试。
自底向上的增量方式的组装过程如图3.31(b)至(d)所示。
(3)混合增量方式自顶向下增量的方式和自底向上增量的方式各有优缺点,一种方式的优点是另一种方式的缺点。
自顶向下测试的主要优点是能较早显示出整个程序的轮廓,主要缺点是,当测试上层模块时使用桩模块较多,很难模拟出真实模块的全部功能,使部分测试内容被迫推迟,直至换上真实模块后再补充测试。
自底向上测试从下层模块开始,设计测试用例比较容易,但是在测试的早期不能显示出程序的轮廓。
针对自顶向下、自底向上方法各自的优点和不足,人们提出了自顶向下和自底向上相结合、从两头向中间逼近的混合式组装方法,被形象称之为“三明治”方法。这种方式,结合考虑软件总体结构的良好设计原则,在程序结构的高层使用自顶向下方式,在程序结构的低层使用自底向上方式。
3.确认测试确认测试的任务是验证软件的功能和性能及其他特性是否满足了需求规格说明中确定的各种需求,以及软件配置是否完全、正确。
确认测试的实施首先运用黑盒测试方法,对软件进行有效性测试,即验证被测软件是否满足需求规格说明确认的标准。复审的目的在于保证软件配置齐全、分类有序,以及软件配置所有成分的完备性、一致性、准确性和可操作性,并且包括软件维护所必需的细节。
4.系统测试系统测试是将通过测试确认的软件,作为整个基于计算机系统的一个元素,与计算机硬件、外设、支持软件、数据和人员等其他系统元素组合在一起,在实际运行(使用)环境下对计算机系统进行一系列的集成测试和确认测试。由此可知,系统测试必须在目标环境下运行,其功用在于评估系统环境下软件的性能,发现和捕捉软件中潜在的错误。
系统测试的目的是在真实的系统工作环境下检验软件是否能与系统正确连接,发现软件与系统需求不一致的地方。
系统测试的具体实施一般包括:功能测试、性能测试、操作测试、配置测试、外部接口测试、安全性测试等。
3.5程序的调试
3.5.1基本概念在对程序进行了成功的测试之后将进入程序调试(通常称Debug,即排错)。程序调试的任务是诊断和改正程序中的错误。它与软件测试不同,软件测试是尽可能多地发现软件中的错误。先要发现软件的错误,然后借助于一定的调试工具去执行找出软件错误的具体位置。软件测试贯穿整个软件生命期,调试主要在开发阶段。
由程序调试的概念可知,程序调试活动由两部分组成,其一是根据错误的迹象确定程序中错误的确切性质、原因和位置。其二,对程序进行修改,排除这个错误。
1.程序调试的基本步骤
(1)错误定位从错误的外部表现形式入手,研究有关部分的程序,确定程序中出错位置,找出错误的内在原因。确定错误位置占据了软件调试绝大部分的工作量。
从技术角度来看,错误的特征和查找错误的难度在于:
①现象与原因所处的位置可能相距很远。就是说,现象可能出现在程序的一个部位,而原因可能在离此很远的另一个位置。高耦合的程序结构中这种情况更为明显。
②当纠正其他错误时,这一错误所表现出的现象可能会消失或暂时性消失,但并未实际排除。
⑧现象可能并不是由错误引起的(如舍入误差)。
④现象可能是由于一些不容易发现的人为错误引起的。
⑤错误现象可能时有时无。,
⑥现象是由于难于再现的输入状态(例如实时应用中输入顺序不确定)引起。
⑦现象可能是周期出现的。如在软件、硬件结合的嵌入式系统中常常遇到。
(2)修改设计和代码,以排除错误排错是软件开发过程中一项艰苦的工作,这也决定了调试工作是一个具有很强技术性和技巧性的工作。软件工程人员在分析测试结果的时候会发现,软件运行失效或出现问题,往往只是潜在错误的外部表现,而外部表现与内在原因之间常常没有明显的联系。如果要找出真正的原因,排除潜在的错误,不是一件易事。因此可以说,调试是通过现象,找出原因的一个思维分析的过程。
(3)进行回归测试,防止引进新的错误因为修改程序可能带来新的错误,重复进行暴露这个错误的原始测试或某些有关测试,以确认该错误是否被排除、是否引进了新的错误。如果所做的修正无效,则撤销这次改动,重复上述过程,直到找到一个有效的解决办法为止。
2.程序调试的原则在软件调试方面,许多原则实际上是心理学方面的问题。因为调试活动由对程序中错误的定性、定位和排错两部分组成,因此调试原则也从以下两个方面考虑。
(1)确定错误的性质和位置时的注意事项:
①分析思考与错误征兆有关的信息。
②避开死胡同。如果程序调试人员在调试中陷入困境,最好暂时把问题抛开,留到后面适当的时间再去考虑,或者向其他人讲解这个问题,去寻求新的解决思路。
③只把调试工具当做辅助手段来使用。利用调试工具,可以帮助思考,但不能代替思考。因为调试工具给人提供的是一种无规律的调试方法。
④避免用试探法,最多只能把它当做最后手段。这是一种碰运气的盲目的动作,它的成功机率很小,而且还常把新的错误带到问题中来。
(2)修改错误的原则
①在出现错误的地方,很可能还有别的错误。经验表明,错误有群集现象,当在某一程序段发现有错误时,在该程序段中还存在别的错误的概率也很高。因此,在修改一个错误时,还要观察和检查相关的代码,看是否还有别的错误。
②修改错误的一个常见失误是只修改了这个错误的征兆或这个错误的表现,而没有修改错误本身。如果提出的修改不能解释与这个错误有关的全部现象,那就表明了只修改了错误的一部分。
③注意修正一个错误的同时有可能会引入新的错误。人们不仅需要注意不正确的修改,而且还要注意看起来是正确的修改可能会带来的副作用,即引进新的错误。因此在修改了错误之后,必须进行回归测试。
④修改错误的过程将迫使人们暂时回到程序设计阶段。修改错误也是程序设计的一种形式。一般说来,在程序设计阶段所使用的任何方法都可以应用到错误修正的过程中来。
⑤修改源代码程序,不要改变目标代码。
3.5.2软件调试方法调试的关键在于推断程序内部的错误位置及原因。从是否跟踪和执行程序的角度,类似于软件测试,软件调试可以分为静态调试和动态调试。软件测试中讨论的静态分析方法同样适用静态调试。静态调试主要指通过人的思维来分析源程序代码和排错,是主要的调试手段,而动态调试是辅助静态调试的。主要的调试方法可以采用:
1.强行排错法作为传统的调试方法,其过程可概括为,设置断点、程序暂停、观察程序状态、继续运行程序。这是目前使用较多、效率较低的调试方法。涉及的调试技术主要是设置断点和监视表达式。例如:
①通过内存全部打印来排错。
②在程序特定部位设置打印语句——即断点法。输出存储器内容,就是在程序执行到某一行的时候,计算机自动停止运行,并保留这时各变量的状态,方便检查,校对。
③自动调试工具。可供利用的典型的语言功能有打印出语句执行的追踪信息、追踪子程序调用,以及指定变量的变化情况。自动调试工具的功能是设置断点,当程序执行到某个特定的语句或某个特定的变量值改变时,程序暂停执行。程序员可在终端上观察程序此时的状态。
应用以上任何一种技术之前,都应当对错误的征兆进行全面彻底的分析,得出对出错位置及错误性质的推测,再使用一种适当的排错方法来检验推测的正确性。
2.回溯法该方法适合于小规模程序的排错。即一旦发现了错误,先分析错误征兆,确定最先发现“症状”的位置。然后,从发现“症状”的地方开始,沿程序的控制流程,逆向跟踪源程序代码,直到找到错误根源或确定错误产生的范围。
回溯法对于小程序很有效,往往能把错误范围缩小到程序中的一小段代码,仔细分析这段代码不难确定出错的准确位置。但随着源代码行数的增加,潜在的回溯路径数目很多,回溯会变得很困难,而且实现这种回溯的开销大。
3.原因排除法原因排除法是通过演绎和归纳,以及二分法来实现的。
演绎法是一种从一般原理或前提出发,经过排除和精化的过程来推导出结论的思考方法。演绎法排错是测试人员首先根据已有的测试用例,设想及枚举出所有可能出错的原因作为假设。然后再用原始测试数据或新的测试,从中逐个排除不可能正确的假设。最后,再用测试数据验证余下的假设确定出错的原因。
归纳法是一种从特殊推断出一般的系统化思考方法。其基本思想是从一些线索(错误征兆或与错误发生有关的数据)着手,通过分析寻找到潜在的原因,从而找出错误。
二分法实现的基本思想是,如果已知每个变量在程序中若干个关键点的正确值,则可以使用定值语句(如赋值语句、输入语句等)在程序中的某点附近给这些变量赋正确值,然后运行程序并检查程序的输出。如果输出结果是正确的,则错误原因在程序的前半部分;反之,错误原因在程序的后半部分。对错误原因所在的部分重复使用这种方法,直到将出错范围缩小到容易诊断的程度为止。
上面的每一种方法都可以使用调试工具来辅助完成。例如,可以使用带调试功能的编译器、动态调试器、自动测试用例生成器以及交叉引用工具等。
需要注意的一个实际问题是,调试的成果是排错,为了修改程序中错误,往往会采用“补丁程序”来实现,而这种做法会引起整个程序质量的下降,但是从目前程序设计发展的状况看,对大规模的程序的修改和质量保证,又不失为一种可行的方法。
习 题 3
一、选择题
1.在软件生命周期中,能准确地确定软件系统必须做什么和必须具备哪些功能的阶段是
A)概要设计 B)详细设计 c)可行性分析 D)需求分析
2.下面不属于软件工程的3个要素的是
A)工具 B)过程 c)方法 D)环境
3.检查软件产品是否符合需求定义的过程称为
A)确认测试 B)集成测试 C)验证测试 D)验收测试
4.数据流图用于抽象描述一个软件的逻辑模型,数据流图由一些特定的图符构成。下列图符名标识的图符不属于数据流图合法图符的是
A)控制流 B)加工 C)数据存储 D)源和潭
5.下面不属于软件设计原则的是
A)抽象 B)模块化 c)自底向上 D)信息隐蔽
6.程序流程图(PFD)中的箭头代表的是
A)数据流 B)控制流 c)调用关系 D)组成关系
7.下列T且中为需求分析常用T且的县
A)PAD B)PFD C)N-S D)DFD
8.在结构化方法中,软件功能分解属于下列软件开发中的阶段是
A)详细设计 B)需求分析 c)总体设计 D)编程调试
9.软件调试的目的是
A)发现错误 B)改正错误 c)改善软件的性能D)挖掘软件的潜能
10.软件需求分析阶段的工作,可以分为四个方面:需求获取,需求分析,编写需求规格说明书,以及
A)阶段性报告 B)需求评审 C)总结 D)都不正确二、填空题
1.软件是程序、数据和________的集合。
2.Jackson方法是一种面向________的结构化方法。
3.软件工程研究的内容主要包括:________技术和软件工程管理。
4.数据流图的类型有________和事务型。
5.软件开发环境是全面支持软件开发全过程的________集合
第4章 数据库设计基础数据库技术是计算机领域的一个重要分支。在计算机应用的三大领域(科学计算、数据处理和过程控制)中,数据处理约占其中的70%,而数据库技术就是作为一门数据处理技术发展起来的。随着计算机应用的普及和深入,数据库技术变得越来越重要了,而了解、掌握数据库系统的基本概念和基本技术是应用数据库技术的前提。本章首先介绍数据库系统的基础知识,然后对基本数据模型进行讨论,特别是其中的E-R模型和关系模型;之后再介绍关系代数及其在关系数据库中的应用,并对关系的规范化理论作了简单说明;最后,较为详细地讨论了数据库的设计过程。
4.1 数据厍系统的基本概念计算机科学与技术的发展,计算机应用的深入与拓展,使得数据库在计算机应用中的地位与作用日益重要,它在商业中、事务处理中占有主导地位。近年来在统计领域、在多媒体领域以及智能化应用领域中的地位与作用也变得十分重要。随着网络应用的普及,它在网络中的应用也日渐重要。因此,数据库已成为构成一个计算机应用系统的重要的支持性软件。
4.1.1数据、数据库、数据库管理系统
1.数据数据(Data)实际上就是描述事物的符号记录。
计算机中的数据一般分为两部分,其中一部分与程序仅有短时间的交互关系,随着程序的结束而消亡,它们称为临时性(Transient)数据,这类数据一般存放于计算机内存中;而另一部分数据则对系统起着长期持久的作用,它们称为持久性(Persistent)数据。数据库系统中处理的就是这种持久性数据。
软件中的数据是有一定结构的。首先,数据有型(type)与值(Value)之分,数据的型给出了数据表示的类型,如整型、实型、字符型等,而数据的值给出了符合给定型的值,如整型值15。随着应用需求的扩大,数据的型有了进一步的扩大,它包括了将多种相关数据以一定结构方式组合构成特定的数据框架,这样的数据框架称为数据结构(Data structure),数据库中在特定条件下称之为数据模式(Data schema)。
在过去的软件系统中是以程序为主体,而数据则以私有形式从属于程序,此时数据在系统中是分散、凌乱的,这也造成了数据管理的混乱,如数据冗余度高,数据一致性差以及数据的安全性差等多种弊病。近10多年来,数据在软件系统中的地位产生了变化,在数据库系统及数据库应用系统中数据已占有主体地位,而程序已退居附属地位。在数据库系统中需要对数据进行集中、统一的管理,以达到数据被多个应用程序共享的目标。
2.数据库数据库(Database,简称DB)是数据的集合,它具有统一的结构形式并存放于统一的存储介质内,是多种应用数据的集成,并可被各个应用程序所共享。
数据库存放数据是按数据所提供的数据模式存放的,它能构造复杂的数据结构以建立数据间内在联系与复杂的关系,从而构成数据的全局结构模式。
数据库中的数据具有“集成”、“共享”之特点,亦即是数据库集中了各种应用的数据,进行统一的构造与存储,而使它们可被不同应用程序所使用。
3.数据库管理系统数据库管理系统(Database Management System,简称DBMS)是数据库的机构,它是一种系统软件,负责数据库中的数据组织、数据操纵、数据维护、控制及保护和数据服务等。数据库中的数据是具有海量级的数据,并且其结构复杂,因此需要提供管理工具。数据库管理系统是数据库系统的核心,它主要有如下几方面的具体功能:
(1)数据模式定义。数据库管理系统负责为数据库构建模式,也就是为数据库构建其数据框架。
(2)数据存取的物理构建。数据库管理系统负责为数据模式的物理存取及构建提供有效的存取方法与手段。
(3)数据操纵。数据库管理系统为用户使用数据库中的数据提供方便,它一般提供查询、插入、修改以及删除数据的功能。此外,它自身还具有做简单算术运算及统计的能力,而且还可以与某些过程性语言结合,使其具有强大的过程性操作能力。
(4)数据的完整性、安全性定义与检查。数据库中的数据具有内在语义上的关联性与一致性,它们构成了数据的完整性,数据的完整性是保证数据库中数据正确的必要条件,因此必须经常检查以维护数据的正确。
数据库中的数据具有共享性,而数据共享可能会引发数据的非法使用,因此必须要对数据正确使用作出必要的规定,并在使用时做检查,这就是数据的安全性。
数据完整性与安全性的维护是数据库管理系统的基本功能。
(5)数据库的并发控制与故障恢复。数据库是一个集成、共享的数据集合体,它能为多个应用程序服务,所以就存在着多个应用程序对数据库的并发操作。在并发操作中如果不加控制和管理,多个应用程序间就会相互干扰,从而对数据库中的数据造成破坏。因此,数据库管理系统必须对多个应用程序的并发操作做必要的控制以保证数据不受破坏,这就是数据库的并发控制。
数据库中的数据一旦遭受破坏,数据库管理系统必须有能力及时进行恢复,这就是数据库的故障恢复。
(6)数据的服务。数据库管理系统提供对数据库中数据的多种服务功能,如数据拷贝、转存、重组、性能监测、分析等。
为完成以上六个功能,数据库管理系统一般提供相应的数据语言(Data Language),它们是:
数据定义语言(Data Definition Language简称DDL)。该语言负责数据的模式定义与数据的物理存取构建。
数据操纵语言(Data Manipulation Language简称DML)。该语言负责数据的操纵,包括查询及增、删、改等操作。
数据控制语言(Data Control Language简称DCL)。该语言负责数据完整性、安全性的定义与检查以及并发控制、故障恢复等功能,包括系统初启程序、文件读写与维护程序、存取路径管理程序、缓冲区管理程序、安全性控制程序、完整性检查程序、并发控制程序、事务管理程序、运行日志管理程序、数据库恢复程序等。
上述数据语言按其使用方式具有两种结构形式:
交互式命令语言。它的语言简单,能在终端上即时操作,它又称为自含型或自主型语言。
宿主型语言。它一般可嵌入某些宿主语言(Host Language)中,如C,C++和COBOL等高级过程性语言中。
此外,数据库管理系统还有为用户提供服务的服务性(Utility)程序,包括数据初始装入程序、数据转存程序、性能监测程序、数据库再组织程序、数据转换程序、通信程序等。
目前流行的DBMS均为关系数据库系统,比如ORACLE、sybase的PowerBuilder及IBM的DB2、微软的SQLServer等,它们均为严格意义上的DBMS系统。另外有一些小型的数据库,如微软的Vasual Foxpro和Access等,它们只具备数据库管理系统的一些简单功能。
4.数据库管理员由于数据库的共享性,因此对数据库的规划、设计、维护、监视等需要有专人管理,称他们为数据库管理员(Database Administrator简称DBA)。其主要工作如下:
(1)数据库设计(Database Design)。DBA的主要任务之一是做数据库设计,具体的说是进行数据模式的设计。由于数据库的集成与共享性,因此需要有专门人员(即DBA)对多个应用的数据需求作全面的规划、设计与集成。
(2)数据库维护。DBA必须对数据库中的数据安全性、完整性、并发控制及系统恢复、数据定期转存等进行实施与维护。
(3)改善系统性能,提高系统效率。DBA必须随时监视数据库运行状态,不断调整内部结构,使系统保持最佳状态与最高效率。当效率下降时,DBA需采取适当的措施,如进行数据库的重组、重构等。
5.数据库系统数据库系统(Database System简称DBS)由如下几部分组成:数据库(数据)、数据库管理系统(软件)、数据库管理员(人员)、系统平台之一——硬件平台(硬件)、系统平台之二——软件平台(软件)。这五个部分构成了一个以数据库为核心的完整的运行实体,称为数据库系统。
在数据库系统中,硬件平台包括:
计算机:它是系统中硬件的基础平台,目前常用的有微型机、小型机、中型机、大型机及巨型机。
网络:过去数据库系统一般建立在单机上,但是近年来它较多的建立在网络上,从目前形势看,数据库系统今后将以建立在网络上为主,而其结构形式又以客户/服务器(C/S)方式与浏览器/服务器(B/S)方式为主。
在数据库系统中,软件平台包括:
操作系统:它是系统的基础软件平台,目前常用的有各种UNIX(包括LINUX)与WINDOWS两种。
数据库系统开发工具:为开发数据库应用程序所提供的工具,它包括过程性程序设计语言如C,C++等,也包括可视化开发工具VB、PB、Delphi等,它还包括近期与INTERNET有关的HTML及XML等以及一些专用开发工具。
接口软件:在网络环境下数据库系统中数据库与应用程序,数据库与网络间存在着多种接口,它们需要用接口软件进行联接,否则数据库系统整体就无法运作,这些接口软件包括ODBC,JDBC,OLEDB,CORBA,COM,DCOM等。
6.数据库应用系统(Database Application System简称DBAS)
利用数据库系统进行应用开发可构成一个数据库应用系统,数据库应用系统是数据库系统再加上应用软件及应用界面这三者所组成,具体包括:数据库、数据库管理系统、数据库管理员,硬件平台、软件平台、应用软件、应用界面。其中应用软件是由数据库系统所提供的数据库管理系统(软件)及数据库系统开发工具所书写而成,而应用界面大多由相关的可视化工具开发而成。
数据库应用系统的7个部分以一定的逻辑层次结构方式组成一个有机的整体。如果不计数据库管理员(人员)并将应用软件与应用界面记成应用系统,则数据库应用系统的结构如图4.1所示。
下面以一个用户读取某数据记录为例,展示在数据库系统中访问数据的具体执行过程,该过程如图4.2所示,对其各个步骤简单说明如下:
①用户程序中有一条读数据库记录的DML语句,当计算机执行到该语句时,即向DBMS发出读取相应记录的命令。
②DBMS接到该命令后,首先访问该用户对应的子模式,检查该操作是否在合法授权范围内及欲读记录的正确性、有效性,若不合法则拒绝执行,并向应用程序状态返回区发出回答状态信息;反之执行下一步。
③DBMS读取模式描述并从子模式映像到全局模式,从而确定所需的逻辑记录类型。
④DBMS从逻辑模式映像到存储模式,从而确定读入哪些物理记录以及具体的地址信息。
⑤DBMS向操作系统发出从指定地址读取记录的命令。
⑥操作系统执行读命令,按指定地址从数据库中把记录读入系统缓冲区,并在操作结束后向DBMS作出回答。
⑦DBMS按照模式将读入系统缓冲区中的内容映像成用户要求读取的逻辑记录。
⑧DBMS将导出的逻辑记录送入用户工作区,并将操作执行情况的状态信息返回给用户。
⑨DBMS将已执行的操作载入运行日志。
⑩应用程序根据返回的状态信息决定是否利用该数据进行操作等。
如果用户是更新一个记录内容,则执行过程类似。首先读出目标记录,并在用户工作区中进行修改,然后向DBMS发出“写回修改数据”的数据库指令即可。
4.1.2数据库系统的发展数据管理发展至今已经历了三个阶段:人工管理阶段、文件系统阶段和数据库系统阶段。人工管理阶段是在20世纪50年代中期以前,主要用于科学计算,硬件无磁盘,直接存取,软件没有操作系统。20世纪50年代后期到20世纪60年代中期,进入文件系统阶段。20世纪60年代之后,数据管理进入数据库系统阶段。随着计算机应用领域不断扩大,数据库系统的功能和应用范围也愈来愈广,到目前已成为计算机系统的基本及主要的支撑软件。
1.文件系统阶段文件系统是数据库系统发展的初级阶段,它提供了简单的数据共享与数据管理能力,但是它无法提供完整的、统一的、管理和数据共享的能力。由于它的功能简单,因此它附属于操作系统而不成为独立的软件,目前一般将其看成仅是数据库系统的雏形,而不是真正的数据库系统。
2.层次数据库与网状数据库系统阶段从20世纪60年代末期起,真正的数据库系统——层次数据库与网状数据库开始发展,它们为统一管理与共享数据提供了有力支撑,这个时期数据库系统蓬勃发展形成了有名的“数据库时代”。但是这两种系统也存在不足,主要是它们脱胎于文件系统,受文件的物理影响较大,对数据库使用带来诸多不便,同时,此类系统的数据模式构造烦琐不宜于推广使用。
3.关系数据库系统阶段关系数据库系统出现于20世纪70年代,在80年代得到蓬勃发展,并逐渐取代前两种系统。关系数据库系统结构简单,使用方便,逻辑性强物理性少,因此在80年代以后一直占据数据库领域的主导地位。但是由于此系统来源于商业应用,适合于事务处理领域而对非事务处理领域应用受到限制,因此在80年代末期兴起与应用技术相结合的各种专用数据库系统。
工程数据库系统:是数据库与工程领域的结合;
图形数据库系统:是数据库与图形应用的结合:
图像数据库系统:是数据库与图像应用的结合;
统计数据库系统:是数据库与工程应用的结合;
知识库系统:是数据库与人工智能应用领域的结合;
分布式数据库系统:是数据库与网络应用的结合;
并行数据库系统:是数据库与多机并行应用的结合;
面向对象数据库系统:是数据库与面向对象方法的结合。
关于数据管理三个阶段中的软硬件背景及处理特点,简单概括在表4.1中。
表4.1数据管理三个阶段的比较
人工管理
文件系统
数据库系统
背景
应用背景
科学计算
科学计算、管理
大规模管理
硬件背景
无直接存取设备
磁盘、磁鼓
大容量磁盘
软件背景
没有操作系统
有文件系统
有数据库管理系统
处理方式
批处理
联机实时处理批处理
联机实时处理分布处理批处理
特点
数据管理者
人
文件系统
数据库管理系统
数据面向对象
某个应用程序
某个应用程序
现实世界
数据共享程度
无共享冗余度大
共享性差冗余度大
共享性大冗余度小
数据独立性
不独立,完全依赖于程序
独立性差
具有高度的物理独立性和一定的逻辑独立性
数据结构化
无结构
记录内有结构整体无结构
整体结构化,用数据模型描述
数据控制能力
应用程序自己控制
应用程序自己控制
由DBMS提供数据安全性、完整性、并发控制和恢复
目前,数据库技术也与其他信息技术一样在迅速发展之中,计算机处理能力的增强和越来越广泛的应用是促进数据库技术发展的重要动力。一般认为,未来的数据库系统应支持数据管理、对象管理和知识管理,应该具有面向对象的基本特征。在关于数据库的诸多新技术中,下面三种是比较重要的:
面向对象数据库系统:用面向对象方法构筑面向对象数据模型使其具有比关系数据库系统更为通用的能力;
知识库系统:用人工智能中的方法特别是用谓词逻辑知识表示方法构筑数据模型,使其模型具有特别通用的能力;
关系数据库系统的扩充:利用关系数据库作进一步扩展,使其在模型的表达能力与功能上有进一步的加强,如与网络技术相结合的Web数据库、数据仓库及嵌入式数据库等。
4.1.3数据库系统的基本特点数据库技术是在文件系统基础上发展产生的,两者都以数据文件的形式组织数据,但由于数据库系统在文件系统之上加入了DBMS对数据进行管理,从而使得数据库系统具有以下特点:
1.数据的集成性数据库系统的数据集成性主要表现在如下几个方面:
(1)在数据库系统中采用统一的数据结构方式,如在关系数据库中采用二维表作为统一结构方式。
(2)在数据库系统中按照多个应用的需要组织全局的统一的数据结构(即数据模式),数据模式不仅可以建立全局的数据结构,还可以建立数据间的语义联系从而构成一个内在紧密联系的数据整体。
(3)数据库系统中的数据模式是多个应用共同的、全局的数据结构,而每个应用的数据则是全局结构中的一部分,称为局部结构(即视图),这种全局与局部的结构模式构成了数据库系统数据集成性的主要特征。
2.数据的高共享性与低冗余性由于数据的集成性使得数据可为多个应用所共享,特别是在网络发达的今天,数据库与网络的结合扩大了数据关系的应用范围。数据的共享自身又可极大地减少数据冗余性,不仅减少了不必要的存储空间,更为重要的是可以避免数据的不一致性。所谓数据的一致性是指在系统中同一数据的不同出现应保持相同的值,而数据的不一致性指的是同一数据在系统的不同拷贝处有不同的值。因此,减少冗余性以避免数据的不同出现是保证系统一致性的基础。
3.数据独立性数据独立性是数据与程序间的互不依赖性,即数据库中数据独立于应用程序而不依赖于应用程序。也就是说,数据的逻辑结构、存储结构与存取方式的改变不会影响应用程序。
数据独立性一般分为物理独立性与逻辑独立性两级。
(1)物理独立性:物理独立性即是数据的物理结构(包括存储结构、存取方式等)的改变,如存储设备的更换、物理存储的更换、存取方式改变等都不影响数据库的逻辑结构,从而不致引起应用程序的变化。
(2)逻辑独立性:数据库总体逻辑结构的改变,如修改数据模式、增加新的数据类型、改变数据间联系等,不需要相应修改应用程序,这就是数据的逻辑独立性。
4.数据统一管理与控制数据库系统不仅为数据提供高度集成环境,同时它还为数据提供统一管理的手段,这主要包含以下三个方面:
(1)数据的完整性检查:检查数据库中数据的正确性以保证数据的正确。
(2)数据的安全性保护:检查数据库访问者以防止非法访问。
(3)并发控制:控制多个应用的并发访问所产生的相互干扰以保证其正确性。
4.1.4数据库系统的内部结构体系数据库系统在其内部具有三级模式及二级映射,三级模式分别是概念级模式、内部级模式与外部级模式,二级映射则分别是概念级到内部级的映射以及外部级到概念级的映射。这种三级模式与二级映射构成了数据库系统内部的抽象结构体系,如图4.3所示。
1.数据库系统的三级模式.
数据模式是数据库系统中数据结构的一种表示形式,它具有不同的层次与结构方式。
(1)概念模式。概念模式(Conceptual Schema)是数据库系统中全局数据逻辑结构的描述,是全体用户(应用)公共数据视图。此种描述是一种抽象的描述,它不涉及具体的硬件环境与平台,也与具体的软件环境无关。
概念模式主要描述数据的概念记录类型以及它们间的关系,它还包括一些数据间的语义约束,对它的描述可用DBMS中的DDL语言定义。
(2)外模式。外模式(External Schema)也称子模式(Subschema)或用户模式(User's schema)。它是用户的数据视图,也就是用户所见到的数据模式,它由概念模式推导而出。概念模式给出了系统全局的数据描述而外模式则给出每个用户的局部数据描述。一个概念模式可以有若干个外模式,每个用户只关心与它有关的模式,这样不仅可以屏蔽大量无关信息而且有利于数据保护。在一般的DBMS中都提供有相关的外模式描述语言(外模式DDL)。
(3)内模式。内模式(Internal Schema)又称物理模式(Physical Schema),它给出了数据库物理存储结构与物理存取方法,如数据存储的文件结构、索引、集簇及hash等存取方式与存取路径,内模式的物理性主要体现在操作系统及文件级上,它还未深入到设备级上(如磁盘及磁盘操作)。内模式对一般用户是透明的,但它的设计直接影响数据库的性能。DBMS一般提供相关的内模式描述语言(内模式DDL)。
数据模式给出了数据库的数据框架结构,数据是数据库中的真正的实体,但这些数据必须按框架所描述的结构组织,以概念模式为框架所组成的数据库叫概念数据库(Conceptual Database),以外模式为框架所组成的数据库叫用户数据库(user's Database),以内模式为框架所组成的数据库叫物理数据库(Physical Database)。这三种数据库中只有物理数据库是真实存在于计算机外存中,其他两种数据库并不真正存在于计算机中,而是通过两种映射由物理数据库映射而成。
模式的三个级别层次反映了模式的三个不同环境以及它们的不同要求,其中内模式处于最底层,它反映了数据在计算机物理结构中的实际存储形式,概念模式处于中层,它反映了设计者的数据全局逻辑要求,而外模式处于最外层,它反映了用户对数据的要求。
2.数据库系统的两级映射数据库系统的三级模式是对数据的三个级别抽象,它把数据的具体物理实现留给物理模式,使用户与全局设计者不必关心数据库的具体实现与物理背景;同时,它通过两级映射建立了模式间的联系与转换,使得概念模式与外模式虽然并不具备物理存在,但是也能通过映射而获得其实体。此外,两级映射也保证了数据库系统中数据的独立性,亦即数据的物理组织改变与逻辑概念级改变相互独立,使得只要调整映射方式而不必改变用户模式。
(1)概念模式到内模式的映射。该映射给出了概念模式中数据的全局逻辑结构到数据的物理存储结构间的对应关系,此种映射一般由DBMS实现。
(2)外模式到概念模式的映射。概念模式是一个全局模式而外模式是用户的局部模式。一个概念模式中可以定义多个外模式,而每个外模式是概念模式的一个基本视图。外模式到概念模式的映射给出了外模式与概念模式的对应关系,这种映射一般也是由DBMS来实现的。
4.2 数据模型
4.2.1数据模型的基本概念数据库中的数据模型可以将复杂的现实世界要求反映到计算机数据库中的物理世界,这种反映是一个逐步转化的过程,它分为两个阶段:由现实世界开始,经历信息世界而至计算机世界,从而完成整个转化。
现实世界(real world):用户为了某种需要,需将现实世界中的部分需求用数据库实现,这样,我们所见到的是客观世界中的划定边界的一个部分环境,它称为现实世界。
信息世界(information world):通过抽象对现实世界进行数据库级上的刻画所构成的逻辑模型叫信息世界。信息世界与数据库的具体模型有关,如层次、网状、关系模型等。
计算机世界(computer world):在信息世界基础上致力于其在计算机物理结构上的描述,从而形成的物理模型叫计算机世界。现实世界的要求只有在计算机世界中才得到真正的物理实现,而这种实现是通过信息世界逐步转化得到的。
数据是现实世界符号的抽象,而数据模型(data model)则是数据特征的抽象,它从抽象层次上描述了系统的静态特征、动态行为和约束条件,为数据库系统的信息表示与操作提供一个抽象的框架。数据模型所描述的内容有三个部分,它们是数据结构、数据操作与数据约束。
(1)数据结构。数据模型中的数据结构主要描述数据的类型、内容、性质以及数据间的联系等。数据结构是数据模型的基础,数据操作与约束均建立在数据结构上。不同数据结构有不同的操作与约束,因此,一般数据模型的分类均以数据结构的不同而分。
(2)数据操作。数据模型中的数据操作主要描述在相应数据结构上的操作类型与操作方式。
(3)数据约束。数据模型中的数据约束主要描述数据结构内数据间的语法、语义联系,它们之间的制约与依存关系,以及数据动态变化的规则,以保证数据的正确、有效与相容。
数据模型按不同的应用层次分成三种类型,它们是概念数据模型(conceptual data model)、逻辑数据模型(logic data model)、物理数据模型(physical data model)。
概念数据模型简称概念模型,它是一种面向客观世界、面向用户的模型;它与具体的数据库管理系统无关,与具体的计算机平台无关。概念模型着重于对客观世界复杂事物的结构描述及它们之间的内在联系的刻画。概念模型是整个数据模型的基础。目前,较为有名的概念模型有E-R模型、扩充的E-R模型、面向对象模型及谓词模型等。
逻辑数据模型又称数据模型,它是一种面向数据库系统的模型,该模型着重于在数据库系统一级的实现。概念模型只有在转换成数据模型后才能在数据库中得以表示。目前,逻辑数据模型也有很多种,较为成熟并先后被人们大量使用过的有:层次模型、网状模型、关系模型、面向对象模型等。
物理数据模型又称物理模型,它是一种面向计算机物理表示的模型,此模型给出了数据模型在计算机上物理结构的表示。
4.2.2E-R模型概念模型是面向现实世界的,它的出发点是有效和自然地模拟现实世界,给出数据的概念化结构。长期以来被广泛使用的概念模型是E-R模型(entity-relationship model)(或实体联系模型),它于1976年由Peter Chen首先提出。该模型将现实世界的要求转化成实体、联系、属性等几个基本概念,以及它们间的两种基本联接关系,并且可以用一种图非常直观地表示出来。
1.E-R模型的基本概念
(1)实体现实世界中的事物可以抽象成为实体,实体是概念世界中的基本单位,它们是客观存在的且又能相互区别的事物。凡是有共性的实体可组成一个集合称为实体集(entity set)。如小赵、小李是实体,他们又均是学生而组成一个实体集。
(2)属性现实世界中事物均有一些特性,这些特性可以用属性来表示。属性刻画了实体的特征。一个实体往往可以有若干个属性。每个属性可以有值,一个属性的取值范围称为该属性的值域(value domain)或值集(value set)。如小赵年龄取值为17,小李为19。
(3)联系现实世界中事物间的关联称为联系。在概念世界中联系反映了实体集间的一定关系,如工人与设备之间的操作关系,上、下级间的领导关系,生产者与消费者之间的供求关系。
实体集间的联系有多种,就实体集的个数而言有:
①两个实体集间的联系。两个实体集间的联系是一种最为常见的联系,前面举的例子均属两个实体集间的联系。
②多个实体集间的联系。这种联系包括三个实体集间的联系以及三个以上实体集间的联系。如工厂、产品、用户这三个实体集间存在着工厂提供产品为用户服务的联系。
③一个实体集内部的联系。一个实体集内有若干个实体,它们之间的联系称实体集内部联系。如某公司职工这个实体集内部可以有上、下级联系。
实体集间联系的个数可以是单个也可以是多个。如工人与设备之间有操作联系,另外还可以有维修联系。两个实体集间的联系实际上是实体集间的函数关系,这种函数关系可以有下面几种:
一对一(one t0 one)的联系,简记为1:1。这种函数关系是常见的函数关系之一,如学校与校长间的联系,一个学校与一个校长间相互一一对应。
一对多(one to many)或多对一(many to one)联系,简记为l:M(1:m)或M:1(m:1)。这两种函数关系实际上是一种函数关系,如学生与其宿舍房间的联系是多对一的联系(反之,则为一对多联系),即多个学生对应一个房间。
多对多(many to many)联系,简记为M:N或m:n。这是一种较为复杂的函数关系,如教师与学生这两个实体集间的教与学的联系是多对多的,因为一个教师可以教授多个学生,而一个学生又可以受教于多个教师。
2.E-R模型三个基本概念之间的联接关系
E-R模型由上面三个基本概念组成。由实体、联系、属性三者结合起来才能表示现实世界。
(1)实体集(联系)与属性间的联接关系实体是概念世界中的基本单位,属性附属于实体,它本身并不构成独立单位。一个实体可以有若干个属性,实体以及它的所有属性构成了实体的一个完整描述。因此实体与属性间有一定的联接关系。如在人事档案中每个人(实体).可以有:编号、姓名、性别、年龄、籍贯、政治面貌等若干属性,它们组成了一个有关人(实体)的完整描述。
属性有属性域,每个实体可取属性域内的值。一个实体的所有属性取值组成了一个值集叫元组(tuple)。在概念世界中,可以用元组表示实体,也可用它区别不同的实体。如在人事档案简表4.2中,每一行表示一个实体,这个实体可以用一组属性值表示。比如:(101,赵英俊,男,18,浙江,团员),(102,王平,男,21,江苏,党员),这两个元组分别表示两个不同的实体。
实体有型与值之别,一个实体的所有属性构成了这个实体的型,如人事档案中的实体,它的型是由编号、姓名、性别、年龄、籍贯、政治面貌等属性组成,而实体中属性值的集合(即元组)则构成了这个实体的值。
相同型的实体构成了实体集。如表4.2中的每一行是一个实体,它们均有相同的型,因此表内诸实体构成了一个实体集。
表4.2人事档案简表编 号
姓 名
性 别
年 龄
籍 贯
政治面貌
101
赵英俊
男
18
浙江
团员
102
王 平
男
21
江苏
党员
103
吴亦奇
女
20
辽宁
群众
104
刘 过
男
21
陕西
群众
105
李美丽
女
18
安徽
团员
联系也可以附有属性,联系和它的所有属性构成了联系的一个完整描述,因此,联系与属性间也有联接关系。如有教师与学生两个实体集间的教与学的联系,该联系尚可附有属性“教室号”。
(2)实体(集)与联系实体集间可通过联系建立联接关系,一般而言,实体集间无法建立直接关系,它只能通过联系才能建立起联接关系。如教师与学生之间无法直接建立关系,只有通过“教与学”的联系才能在相互之间建立关系。
在E-R模型中有三个基本概念以及它们之间的两种基本联接关系。它们将现实世界中的错综复杂的现象抽象成简单明了的几个概念与关系,具有极强的概括性和表达能力。因此,E—R模型目前已成为表示概念世界的有力工具。
3.E-R模型的图示法
E-R模型可以用一种非常直观的图的形式表示,这种图称为E-R图(entity-relationship diagram)。在E-R图中我们分别用下面不同的几何图形表示E-R模型中的三个概念与两个联接关系。
(1)实体集表示法在E-R图中用矩形表示实体集,在矩形内写上该实体集的名字。如实体集学生(student)、课程(course)可用图4.4表示。
(2)属性表示法在E-R图中用椭圆形表示属性,在椭圆形内写上该属性的名称。如学生有属性:学号(S#)、姓名(Sn)及年龄(Sa),它们可以用图4.5表示。
(3)联系表示法在E-R图中用菱形(内写上联系名)表示联系。如学生与课程间的联系SC,用图4.6表示。
三个基本概念分别用三种几何图形表示。它们之间的联接关系也可用图形表示。
(4)实体集(联系)与属性间的联接关系属性依附于实体集,因此,它们之间有联接关系。在E-R图中这种关系可用联接这两个图形间的无向线段表示(一般情况下可用直线)。如实体集student有属性S孝(学号)、Sn(学生姓名)及Sa(学生年龄);实体集course有属性C孝(课程号)、Cn(课程名)及P孝(预修课号),此时它们可用图4.7联接。
属性也依附于联系,它们之间也有联接关系,因此也可用无向线段表示。如联系SC可与学生的课程成绩属性G建立联接并可用图4.8表示。
(5)实体集与联系间的联接关系在E-R图中实体集与联系间的联接关系可用联接这两个图形间的无向线段表示。如实体集student与联系SC间有联接关系,实体集course与联系SC间也有联接关系,因此它们之间可用无向线段相联,构成一个如图4.9所示的图。
有时为了进一步刻画实体间的函数关系,还可在线段边上注明其对应函数关系,如l:1,1:n,n:m等,如student与course间有多对多联系,此时在图中可以用图4.10所示的形式表示。
实体集与联系间的联接可以有多种,上面所举例子均是两个实体集间联系叫二元联系,也可以是多个实体集间联系,叫多元联系。如工厂、产品与用户间的联系FPU是一种三元联系,此种联接关系可用图4.11表示。
一个实体集内部可以有联系。如某公司职工(employee)间上、下级管理(manage)的联系,此时,其联接关系可用图4.12(a)表示。
实体集间可有多种联系。如教师(T)与学生(S)之间可以有教学(E)联系也可有管理(M)联系,此种联接关系可用图4.12(b)表示。
由矩形、椭圆形、菱形以及按一定要求相互间联接的线段构成了一个完整的E-R图。
例4.1 由前面所述的实体集student,course以及附属于它们的属性和它们间的联系SC以及附属于SC的属性G构成了一个学生课程联系的概念模型,可用图4.13的E-R图表示。
在概念上,E-R模型中的实体、属性与联系是三个有明显区别的不同概念。但是在分析客观世界的具体事物时,对某个具体数据对象,究竟它是实体,还是属性或联系,则是相对的,所做的分析设计与实际应用的背景以及设计人员的理解有关。这是工程实践中构造E-R模型的难点之一。
4.2.3层次模型层次模型是最早发展起来的数据库模型。层次模型(hierarchical model)的基本结构是树形结构,这种结构方式在现实世界中很普遍,如家族结构、行政组织机构,它们自项向下、层次分明。图4.14给出了一个学校行政机构图的简化E-R图,略去了其中的属性。
由图论中树的性质可知,任一树结构均有下列特性:
(1)每棵树有且仅有一个无双亲结点,称为根(root)。
(2)树中除根外所有结点有且仅有一个双亲。因此,树结构是受到一定限制的,从E-R模型观点看,它对于联系也加上了许多限制。
层次数据模型支持的操作主要有查询、插入、删除和更新。在对层次模型进行插入、删除、更新操作时,要满足层次模型的完整性约束条件:进行插入操作时,如果没有相应的双亲结点值就不能插入子女结点值;进行删除操作时,如果删除双亲结点值,则相应的子女结点值也被同时删除;进行更新操作时,应更新所有相应记录,以保证数据的一致性。
层次模型的数据结构比较简单,操作简单;对于实体间联系是固定的、且预先定义好的应用系统,层次模型有较高的性能;同时,层次模型还可以提供良好的完整性支持。但由于层次模型形成早,受文件系统影响大,模型受限制多,物理成分复杂,操作与使用均不甚理想,它不适合于表示非层次性的联系;对于插入和删除操作的限制比较多;此外,查询子女结点必须通过双亲结点。
4.2.4网状模型网状模型(Network model)的出现略晚于层次模型。从图论观点看,网状模型是一个不加任何条件限制的无向图。网状模型在结构上较层次模型好,不像层次模型那样要满足严格的条件。图4.15是学校行政机构图中学校与学生联系的简化E-R图。
在实现中,网状模型将通用的网络拓扑结构分成一些基本结构。一般采用的分解方法是将一个网络分成若干个二级树,即只有两个层次的树。换句话说,这种树是由一个根及若干个叶所组成。为实现的方便,一般规定根结点与任一叶子结点间的联系均是一对多的联系(包含一对一联系)。
在网状模型的DBTG(data base task group)标准中,基本结构简单二级树叫系(set),系的基本数据单位是记录(record),它相当于E-R模型中的实体(集);记录又可由若干数据项(data item)组成,它相当于E-R模型中的属性。系有一个首记录(owner record),它相当于简单二级树的根;系同时有若干个成员记录(member record),它相当于简单二级树中的叶;首记录与成员记录之间的联系用有向线段表示(线段方向仅表示由首记录至成员记录的方向,而并不表示搜索方向),在系中首记录与成员记录间是一对多联系(包括一对一联系)。图4.16给出了一个系的实例。
一般地,现实世界的一个实体结构往往可以由若干个系组成。在网状模型的数据库管理系统中,一般提供DDL语言,用它可以构造系。网状模型中的基本操作是简单二级树中的操作,它包括查询、增加、删除、修改等操作,对于这些操作,不仅需要说明做什么,还需要说明怎么做。比如,在进行查询时,不但要说明查找对象,而且还要规定存取的路径。在DBTG报告中,提供了在系上进行操纵的DML语言,它们有包括打开(OPEN)、关闭(CLOSE)、定位(FIND)、取(GET)、删除(DELETE)、存储(STORE)等在内的许多操作。
网状模型明显优于层次模型,不管是数据表示或数据操纵均显示了更高的效率、更为成熟。但是,网状模型数据库系统也有一定的不足,在使用时涉及到系统内部的物理因素较多,用户操作使用并不方便,其数据模式与系统实现也均不甚理想。
4.2.5关系模型
1.关系的数据结构关系模型采用二维表来表示,简称表。二维表由表框架(Frame)及表的元组(Tuple)组成。表框架由n个命名的属性(Attribute)组成,n称为属性元数(Arity)。每个属性有一个取值范围称为值域(Domain)。表框架对应了关系的模式,即类型的概念。
在表框架中按行可以存放数据,每行数据称为元组,实际上,一个元组是由n个元组分量所组成,每个元组分量是表框架中每个属性的投影值。一个表框架可以存放m个元组,m称为表的基数(Cardinality)。
一个n元表框架及框架内m个元组构成了一个完整的二维表。表4.3给出了有关学生(s)二维表的一个实例。
二维表一般满足下面7个性质:
①二维表中元组个数是有限的——元组个数有限性。
②二维表中元组均不相同——元组的惟一性。
表4.3二维表的一个实例
S群
Sn
Sd
Sa
2001001
2001002
2001003
2001004
张浩然李一明王 伟赵坚强
EE
EE
EE
EE
18
19
18
20
③二维表中元组的次序可以任意交换——元组的次序无关性。
④二维表中元组的分量是不可分割的基本数据项——元组分量的原子性。
⑤二维表中属性名各不相同——属性名惟一性。
⑥二维表中属性与次序无关,可任意交换——属性的次序无关性。
⑦二维表属性的分量具有与该属性相同的值域——分量值域的同一性。
满足以上7个性质的二维表称为关系(Relation),以二维表为基本结构所建立的模型称为关系模型。
关系模型中的一个重要概念是键(Key)或码。键具有标识元组、建立元组间联系等重要作用。
在二维表中凡能惟一标识元组的最小属性集称为该表的键或码。
二维表中可能有若干个键,它们称为该表的候选码或候选键(Candidata Key)。
从二维表的所有候选键中选取一个作为用户使用的键称为主键(Primary key)或主码,一般主键也简称键或码。
表A中的某属性集是某表B的键,则称该属性集为A的外键(Foreign Key)或外码。
表中一定要有键,因为如果表中所有属性的子集均不是键,则表中属性的全集必为键(称为全键),因此也一定有主键。
在关系元组的分量中允许出现空值(Null Value)以表示信息的空缺。空值用于表示未知的值或不可能出现的值,一般用NULL表示。一般关系数据库系统都支持空值,但是有两个限制,即关系的主键中不允许出现空值,因为如主键为空值则失去了其元组标识的作用;需要定义有关空值的运算。
关系框架与关系元组构成了一个关系。一个语义相关的关系集合构成一个关系数据库(Relational Database)。关系的框架称为关系模式,而语义相关的关系模式集合构成了关系数据库模式(Relational Database Schema)。
关系模式支持子模式,关系子模式是关系数据库模式中用户所见到的那部分数据模式描述。关系子模式也是二维表结构,关系子模式对应用户数据库称视图(View)。
2.关系操纵关系模型的数据操纵即是建立在关系上的数据操纵,一般有查询、增加、删除及修改四种操作。
(1)数据查询用户可以查询关系数据库中的数据,它包括一个关系内的查询以及多个关系间的查询。
①对一个关系内查询的基本单位是元组分量,其基本过程是先定位后操作。所谓定位包括纵向定位与横向定位两部分,纵向定位即是指定关系中的一些属性(称列指定),横向定位即是选择满足某些逻辑条件的元组(称行选择)。通过纵向与横向定位后一个关系中的元组分量即可确定了。在定位后即可进行查询操作,就是将定位的数据从关系数据库中取出并放入至指定内存。
②对多个关系间的数据查询则可分为三步:第一步,将多个关系合并成一个关系;第二步,对合并后的一个关系作定位;第三步,操作。其中第二步与第三步为对一个关系的查询。对多个关系的合并可分解成两个关系的逐步合并,如有三个关系R1,R2与R3,合并过程是先将R1与R2合并成R4,然后再将R4与R3合并成最终结果R5。
因此,对关系数据库的查询可以分解成一个关系内的属性指定、一个关系内的元组选择、两个关系的合并三个基本定位操作以及一个查询操作。
(2)数据删除数据删除的基本单位是一个关系内的元组,它的功能是将指定关系内的指定元组删除。它也分为定位与操作两部分,其中定位部分只需要横向定位而无需纵向定位,定位后即执行删除操作。因此数据删除可以分解为一个关系内的元组选择与关系中元组删除两个基本操作。
(3)数据插入数据插入仅对一个关系而言,在指定关系中插入一个或多个元组。在数据插入中不需定位,仅需做关系中元组插入操作,因此数据插入只有一个基本操作。
(4)数据修改数据修改是在一个关系中修改指定的元组与属性。数据修改不是一个基本操作,它可以分解为删除需修改的元组与插入修改后的元组两个更基本的操作。
以上四种操作的对象都是关系,而操作结果也是关系,因此都是建立在关系上的操作。这四种操作可以分解成六种基本操作,称为关系模型的基本操作:
①关系的属性指定;
②关系的元组选择;
③两个关系合并;
④一个或多个关系的查询;
⑤关系中元组的插入;
⑥关系中元组的删除。
3.关系中的数据约束关系模型允许定义三类数据约束,它们是实体完整性约束、参照完整性约束以及用户定义的完整性约束,其中前两种完整性约束由关系数据库系统自动支持。对于用户定义的完整性约束,则由关系数据库系统提供完整性约束语言,用户利用该语言写出约束条件,运行时由系统自动检查。
(1)实体完整性约束(Entity Integrity Constraint)
该约束要求关系的主键中属性值不能为空值,这是数据库完整性的最基本要求,因为主键是惟一决定元组的,如为空值则其惟一性就成为不可能的了。
(2)参照完整性约束(Reference Integrity Constraint)
该约束是关系之间相关联的基本约束,它不允许关系引用不存在的元组:即在关系中的外键要么是所关联关系中实际存在的元组,要么就为空值。比如在关系s(S#、SN、SD、SA)与SC(s#、C#、G)中,SC中主键为(S#,C#)而外键为S#,SC与S通过S#相关联,参照完整性约束要求SC中的S#的值必在S中有相应元组值,如有SC(S13,C8,70),则必在S中存在S(S13,…)。
(3)用户定义的完整性约束(User defined Integrity Constraint)
这是针对具体数据环境与应用环境由用户具体设置的约束,它反映了具体应用中数据的语义要求。
实体完整性约束和参照完整性约束是关系数据库所必需遵守的规则,在任何一个关系数据库管理系统(RDBMS)中均由系统自动支持。
4.3 关系代数关系数据库系统的特点之一是它建立在数学理论的基础之上,有很多数学理论可以表示关系模型的数据操作,其中最为著名的是关系代数(relational algebra)与关系演算(relational calculus)。数学上已经证明两者在功能上是等价的。下面将介绍关于关系数据库的理论——关系代数。
1.关系模型的基本操作关系是由若干个不同的元组所组成,因此关系可视为元组的集合。n元关系是一个n元有序组的集合。
设有一个n元关系R,它有n个域,分别是D1,D2,…,Dn,此时,它们的笛卡尔积是:
该集合的每个元素都是具有如下形式的n元有序组:
该集合与n元关系R有如下联系:
即n元关系R是n元有序组的集合,是它的域的笛卡尔积的子集。
关系模型有插入、删除、修改和查询四种操作,它们又可以进一步分解成六种基本操作:
①关系的属性指定。指定一个关系内的某些属性,用它确定关系这个二维表中的列,它主要用于检索或定位。
②关系的元组的选择。用一个逻辑表达式给出关系中所满足此表达式的元组,用它确定关系这个二维表的行,它主要用于检索或定位。
用上述两种操作即可确定一张二维表内满足一定行、列要求的数据。
③两个关系的合并。将两个关系合并成一个关系。用此操作可以不断合并从而可以将若干个关系合并成一个关系,以建立多个关系间的检索与定位。
用上述三个操作可以进行多个关系的定位。
④关系的查询。在一个关系或多个关系间做查询,查询的结果也为关系。
⑤关系元组的插入。在关系中增添一些元组,用它完成插入与修改。
⑥关系元组的删除。在关系中删除一些元组,用它完成删除与修改。
2.关系模型的基本运算由于操作是对关系的运算,而关系是有序组的集合,因此,可以将操作看成是集合的运算。
(1)插入设有关系R需插入若干元组,要插入的元组组成关系,则插入可用集合并运算表示为:
(2)删除设有关系R需删除一些元组,要删除的元组组成关系,则删除可用集合差运算表示为:
(3)修改修改关系R内的元组内容可用下面的方法实现:
①设需修改的元组构成关系,则先做删除得:
②设修改后的元组构成关系R”,此时将其插入即得到结果:
(4)查询用于查询的三个操作无法用传统的集合运算表示,需要引入一些新的运算。
①投影(Projection)运算对于关系内的域指定可引入新的运算叫投影运算。投影运算是一个一元运算,一个关系通过投影运算(并由该运算给出所指定的属性)后仍为一个关系。是这样一个关系,它是R中投影运算所指出的那些域的列所组成的关系。设R有n个域:,则在R上对域的投影可表示成为下面的一元运算:
②选择(selection)运算选择运算也是一个一元运算,关系R通过选择运算(并由该运算给出所选择的逻辑条件)后仍为一个关系。这个关系是由R中那些满足逻辑条件的元组所组成。设关系的逻辑条件为F,则R满足F的选择运算可写成为:
逻辑条件F是一个逻辑表达式,它由下面的规则组成。
它可以具有的形式,其中,是域(变量)或常量,但,又不能同为常量,是比较符,它可以是<,>,≤,≥,=及≠。叫基本逻辑条件。
由若干个基本逻辑条件经逻辑运算得到,逻辑运算为∧(并且)、∨(或者)及~(否)构成,称为复合逻辑条件。
有了上述两个运算后,我们对一个关系内的任意行、列的数据都可以方便地找到。
③笛卡尔积(Cartesian Product)运算对于两个关系的合并操作可以用笛卡尔积表示。设有n元关系R及m元关系S,它们分别有p、q个元组,则关系R与S经笛卡尔积记为R×S,该关系是一个n+m元关系,元组个数是p×q,由R与S的有序组组合而成。
表4.4给出了两个关系R、S的实例以及R与S的笛卡尔积T=R×S。
3.关系代数中的扩充运算关系代数中除了上述几个最基本的运算外,为操纵方便还需增添一些运算,这些运算均可由基本运算导出。常用的扩充运算有交、除、连接及自然连接等。
(1)交(intersection)运算关系R与S经交运算后所得到的关系是由那些既在R内又在S内的有序组所组成,记为R∩S。
表4.5给出了两个关系R与S及它们经交运算后得到的关系T。
交运算可由基本运算推导而得:
(2)除(division)运算如果将笛卡尔积运算看作乘运算的话,那么除运算就是它的逆运算。当关系T=R×s时,则可将除运算写成为:
S称为T除以R的商(quotient)。
由于除是采用的逆运算,因此除运算的执行是需要满足一定条件的。设有关系T、R,T能被除的充分必要条件是:T中的域包含R中的所有属性;T中有一些域不出现在R中。
在除运算中s的域由T中那些不出现在R中的域所组成,对于s中任一有序组,由它与关系R中每个有序组所构成的有序组均出现在关系T中。
表4.6给出了关系R及一组S,对这一组不同的S给出了经除法运算后的商R/S,从中可以清楚地看出除法的含义及商的内容。
除法运算不是基本运算,它可以由基本运算推导而出。设关系R有域,关系S有域,此时有:
除法的定义虽然较复杂,但在实际中,除法的意义还是比较容易理解的。
例4.2设关系R给出了学生修读课程的情况,关系S给出了所有课程号,分别如表4.7(a)、(b)所示。试找出修读所有课程的学生号。
解:修读所有课程的学生号可用T=R/S表示,结果如表4.7(c)所示。
(3)连接(join)与自然连接(naturaljoin)运算在数学上,可以用笛卡尔积建立两个关系间的连接,但这样得到的关系庞大,而且数据大量冗余。在实际应用中一般两个相互连接的关系往往须满足一些条件,所得到的结果也较为简单。这样就引入了连接运算与自然连接运算。
连接运算又可称为θ—连接运算,这是一种二元运算,通过它可以将两个关系合并成一个大关系。设有关系R、S以及比较式iθj,其中i为R中的域,j为s中的域,θ含义同前。则可以将R、S在域i,j上的θ连接记为:
它的含义可用下式定义:
即R与S的θ连接是由R与S的笛卡尔积中满足限制iθj的元组构成的关系,一般其元组的数目远远少于R×S的数目。应当注意的是,在θ连接中,i与j需具有相同域,否则无法作比较。
在θ连接中如果θ为“=”,就称此连接为等值连接,否则称为不等值连接;如θ为“<”时称为小于连接;如θ为“>”时称为大于连接。
例4.3设有关系R、S分别如表4.8(a)、(b)所示,则为表4.8(c)所示的关系,而为如表4.8(d)所示的关系。
在实际应用中最常用的连接是一个叫自然连接的特例。它满足下面的条件:
①两关系间有公共域;
②通过公共域的相等值进行连接。
设有关系R、S,R有域,S有域,并且,与分别为相同域,此时它们自然连接可记为:
自然连接的含义可用下式表示:
例4.4设关系R、S如表4.9(a)、(b)所示,则如表4.9(c)所示。
在以上运算中最常用的是投影运算、选择运算、自然连接运算、并运算及差运算。
4.关系代数的应用实例关系代数虽然形式简单,但它已经足以表达对表的查询、插入、删除及修改等要求。在所有这些操作中,查询是最复杂的操作。在20世纪70年代,关系数据库系统始终无法走向商品化,最主要的原因就是它的查询效率低下。关系数据库的查询语言一般是非过程语言,即仅仅说明要查询的要求,而不说明如何去进行查询。最终,通过查询优化技术解决了此问题,而对于查询语句(即代数表达式)本身的优化即代数优化是最基本的技术。下面通过一个例子来体会一下如何将关系代数应用于查询。
例4.5建立一个学生选课的关系数据库,它由下面三个关系模式组成:
S(S#,Sn,Sd,Sa);
C(C#,Cn,P#);
SC(S#,C#,G)。
其中S#,C#,Sn,Sd,Sa,Cn,P#,G分别表示学号、课程号、学生姓名、学生系别、学生年龄、课程名、预修课程号、成绩,而S,C,SC则分别表示学生、课程、学生选课关系。
写出对关系模式S、C和SC中的下述查询表达式:
(1)检索学生所有情况:
(2)检索学生年龄大于等于20岁的学生姓名:
(3)检索预修课号为C2的课程的课程号:
(4)检索课程号为C,且成绩为A的所有学生姓名:
注意:这是一个涉及到两个关系的检索,此时需用连接运算。
(5)检索s1所修读的所有课程名及其预修课号:
(6)检索年龄为23岁的学生所修读的课程名:
注意:这是涉及到三个关系的检索。
(7)检索至少修读S,所修读的一门课的学生姓名。
这个例子比较复杂,需作一些分析。将问题分以下三步解决:
第1步:取得S,修读的课程号,它可以表示为:
第2步:取得至少修读为S,修读的一门课的学号:
第3步:最后结果为:
分别将R、w代入后即得检索要求之表达式:
对于一般较为复杂的查询,都是通过这样多步来解决的。注意到该过程中会产生一些中间的表,而查询优化中一般应尽可能使这些中间表比较小。
4.4 数据厍设计与管理数据库设计是数据库应用的核心。本节讨论数据库设计的任务特点、基本步骤和方法,重点介绍数据库的需求分析、概念设计及逻辑设计三个阶段,并用实际例子说明如何进行相关的设计。此外本节还简单讨论数据库管理的内容及DBA的工作。
4.4.1数据库设计概述在数据库应用系统中的一个核心问题就是设计一个能满足用户要求,性能良好的数据库,这就是数据库设计(Database design)。
数据库设计的基本任务是根据用户对象的信息需求、处理需求和数据库的支持环境(包括硬件、操作系统与DBMS)设计出数据模式。所谓信息需求主要是指用户对象的数据及其结构,它反映了数据库的静态要求;所谓处理需求则表示用户对象的行为和动作,它反映了数据库的动态要求。数据库设计中有一定的制约条件,它们是系统设计平台,包括系统软件、工具软件以及设备、网络等硬件。因此,数据库设计即是在一定平台制约下,根据信息需求与处理需求设计出性能良好的数据模式。
在数据库设计中有两种方法,一种是以信息需求为主,兼顾处理需求,称为面向数据的方法(data-oriented approach);另一种方法是以处理需求为主,兼顾信息需求,称为面向过程的方法(process-oriented approach)。这两种方法目前都有使用,在早期由于应用系统中处理多于数据,因此以面向过程的方法使用较多,而近期由于大型系统中数据结构复杂、数据量庞大,而相应处理流程趋于简单,因此用面向数据的方法较多。由于数据在系统中稳定性高,数据己成为系统的核心,因此面向数据的设计方法已成为主流方法。
数据库设计目前一般采用生命周期(1ife cycle)法,即将整个数据库应用系统的开发分解成目标独立的若干阶段。它们是:需求分析阶段、概念设计阶段、逻辑设计阶段、物理设计阶段、编码阶段、测试阶段、运行阶段、进一步修改阶段。在数据库设计中采用上面几个阶段中的前四个阶段,并且重点以数据结构与模型的设计为主线,如图4.17所示。
4.4.2数据库设计的需求分析需求收集和分析是数据库设计的第一阶段,这一阶段收集到的基础数据和一组数据流图(Data Flow Diagram简记为DFD)是下一步设计概念结构的基础。概念结构是整个组织中所有用户关心的信息结构,对整个数据库设计具有深刻影响。而要设计好概念结构,就必须在需求分析阶段用系统的观点来考虑问题、收集和分析数据及其处理。
需求分析阶段的任务是通过详细调查现实世界要处理的对象(组织、部门、企业等),充分了解原系统的工作概况,明确用户的各种需求,然后在此基础上确定新系统的功能。新系统必须充分考虑今后可能的扩充和改变,不能仅按当前应用需求来设计数据库。
调查的重点是“数据”和“处理”,通过调查要从中获得每个用户对数据库的如下要求:
①信息要求。指用户需要从数据库中获得信息的内容与性质。由信息要求可以导出数据要求,即在数据库中需存储哪些数据。
②处理要求。指用户要完成什么处理功能,对处理的响应时间有何要求,处理的方式是批处理还是联机处理。
③安全性和完整性的要求。
为了很好地完成调查的任务,设计人员必须不断地与用户交流,与用户达成共识,以便逐步确定用户的实际需求,然后分析和表达这些需求。需求分析是整个设计活动的基础,也是最困难、最花时间的一步。需求分析人员既要懂得数据库技术,又要对应用环境的业务比较熟悉。
分析和表达用户的需求,经常采用的方法有结构化分析方法和面向对象的方法。结构化分析(structured Analysis,简称SA方法)方法用自顶向下、逐层分解的方式分析系统。用数据流图表达了数据和处理过程的关系,数据字典对系统中数据的详尽描述,是各类数据属性的清单。对数据库设计来讲,数据字典是进行详细的数据收集和数据分析所获得的主要结果。
数据字典是各类数据描述的集合,它通常包括5个部分,即数据项,是数据的最小单位;数据结构,是若干数据项有意义的集合;数据流,可以是数据项,也可以是数据结构,表示某一处理过程的输入或输出;数据存储,处理过程中存取的数据,常常是手工凭证、手工文档或计算机文件;处理过程。
数据字典是在需求分析阶段建立,在数据库设计过程中不断修改、充实、完善的。
在实际开展需求分析工作时有两点需要特别注意:
第一,在需求分析阶段一个重要而困难的任务是收集将来应用所涉及的数据。若设计人员仅仅按当前应用来设计数据库,新数据的加入不仅会影响数据库的概念结构,而且将影响逻辑结构和物理结构,因此设计人员应充分考虑到可能的扩充和改变,使设计易于更动。
第二,必须强调用户的参与,这是数据库应用系统设计的特点。数据库应用系统和广泛的用户有密切的联系,其设计和建立又可能对更多人的工作环境产生重要影响。因而,设计人员应该和用户充分合作进行设计,并对设计工作的最后结果承担共同的责任。
4.4.3数据库概念设计
1.数据厍概念设计概述数据库概念设计的目的是分析数据间内在语义关联,在此基础上建立一个数据的抽象模型。数据库概念设计的方法有以下两种:
(1)集中式模式设计法这是一种统一的模式设计方法,它根据需求由一个统一机构或人员设计一个综合的全局模式。这种方法设计简单方便,它强调统一与一致,适用于小型或并不复杂的单位或部门,而对大型的或语义关联复杂的单位则并不适合。
(2)视图集成设计法这种方法是将一个单位分解成若干个部分,先对每个部分作局部模式设计,建立各个部分的视图,然后以各视图为基础进行集成。在集成过程中可能会出现一些冲突,这是由于视图设计的分散性形成的不一致所造成的,因此需对视图作修正,最终形成全局模式。
视图集成设计法是一种由分散到集中的方法,它的设计过程复杂但它能较好地反映需求,适合于大型与复杂的单位,避免设计的粗糙与不周到,目前此种方法使用较多。
2.数据库概念设计的过程使用E-R模型与视图集成法进行设计时,需要按以下步骤进行:首先选择局部应用,再进行局部视图设计,最后对局部视图进行集成得到概念模式。
(1)选择局部应用根据系统的具体情况,在多层的数据流图中选择一个适当层次的数据流图,让这组图中每一部分对应一个局部应用,以这一层次的数据流图为出发点,设计分E-R图。
(2)视图设计视图设计一般有三种设计次序,它们是:
①自顶向下。这种方法是先从抽象级别高且普遍性强的对象开始逐步细化、具体化与特殊化,如学生这个视图可先从一般学生开始,再分成大学生、研究生等,进一步再由大学生细化为大学本科与专科,研究生细化为硕士生与博士生等,还可以再细化成学生姓名、年龄、专业等细节。
②由底向上。这种设计方法是先从具体的对象开始,逐步抽象,普遍化与一般化,最后形成一个完整的视图设计。
③由内向外。这种设计方法是先从最基本与最明显的对象着手逐步扩充至非基本、不明显的其他对象,如学生视图可从最基本的学生开始逐步扩展至学生所读的课程、上课的教室与任课的教师等其他对象。
上面3种方法为视图设计提供了具体的操作方法,设计者可根据实际情况灵活掌握,可以单独使用也可混合使用。有某些共同特性和行为的对象可以抽象为一个实体。对象的组成成分可以抽象为实体的属性。
在进行设计时,实体与属性是相对而言的。同一事物,在一种应用环境中作为“属性”,在另一种应用环境中就必须作为“实体”。但是,在给定的应用环境中,属性必须是不可分的数据项,属性不能与其他实体发生联系,联系只发生在实体之间。
例4.6学籍管理局部应用中主要涉及的实体包括学生、宿舍、档案材料、班级、班主任。这些实体之间的联系有:
①一个宿舍可以住多个学生,一个学生只能住在一个宿舍中,因此宿舍与学生之间是l:N的联系。
②一个班有若干名学生,一个学生只能属于一个班级,因此班级与学生之间也是1:N的联系。
③班主任与学生之间是1:N的联系。
④学生和他自己的档案材料之间是1:1的联系。
⑤班级与班主任之间都是1:1的联系。
于是,省略了实体的属性后学籍管理的E-R图如图4.18所示。
对应于各实体的属性分别为:
学生:{学号,姓名,出生日期,所在系,何时入学,平均成绩}
档案材料:{档案号,…}
班级:{班级号,学生人数}
班主任:{职工号,姓名,性别,是否为优秀班主任}
宿舍:{宿舍编号,地址,人数}
教室:{教室编号,地址,容量}
其中有下划线的属性为实体的码。
例4.7课程管理局部视图的设计:在这一视图中共有五个实体,分别是学生、课程、教室、教师及教科书,描述这些实体的属性分别为:
学生:{学号,姓名,年龄,性别,入学时间}
课程:{课程号,课程名,学时数}
选修:{学号,课程号,成绩}
教科书:{书号,书名,ISBN,作者,出版时间,关键字}
教室:{教室编号,地址,容量}
同样,省略了实体的属性后课程管理的E-R图如图4.19所示。
(3)视图集成视图集成的实质是将所有的局部视图统一与合并成一个完整的数据模式。在进行视图集成时,最重要的工作便是解决局部设计中的冲突。在集成过程中由于每个局部视图在设计时的不一致性因而会产生矛盾,引起冲突,常见冲突有下列几种:
①命名冲突。命名冲突有同名异义和同义异名两种。如上面的实例中学生属性“何时入学”与“入学时间”属同义异名。
②概念冲突。同一概念在一处为实体而在另一处为属性或联系。
③域冲突。相同的属性在不同视图中有不同的域,如学号在某视图中的域为字符串而在另一个视图中可为整数,有些属性采用不同度量单位也属域冲突。
④约束冲突。不同的视图可能有不同的约束。
视图经过合并生成的是初步E-R图,其中可能存在冗余的数据和冗余的实体间联系。冗余数据和冗余联系容易破坏数据库的完整性,给数据库维护增加困难。因此,对于视图集成后所形成的整体的数据库概念结构还必须进行进一步验证,确保它能够满足下列条件:
整体概念结构内部必须具有一致性,即不能存在互相矛盾的表达;
整体概念结构能准确地反映原来的每个视图结构,包括属性、实体及实体间的联系;
整体概念结构能满足需求分析阶段所确定的所有要求;
整体概念结构最终还应该提交给用户,征求用户和有关人员的意见,进行评审、修改和优化,然后把它确定下来,作为数据库的概念结构,作为进一步设计数据库的依据。
例4.8学籍管理局部视图与课程管理局部视图的集成。
根据上面所述的方法,集成过程可按下面步骤进行:
①消除冲突这两个子E-R图存在着多方面的冲突:
1)班主任也属于教师,学籍管理中的班主任实体与课程管理中的教师实体属于异名同义,可以统一称为教师。
2)将班主任改为教师后,教师与学生之间呈现两种不同类型的联系:指导联系和教学联系。由于指导联系实际上可以包含在教学联系之中,因此可以将这两种联系综合为教学联系。
3)调整学生实体属性组成及次序,调整结果可为:
学生:{学号,姓名,出生日期,年龄,所在系,年级,平均成绩}
②消除冗余
1)学生实体中的年龄可以由出生日期推算出来,属于冗余数据。
学生:{学号,姓名,出生日期,所在系,年级,平均成绩}
2)教室实体与班级实体之间的上课联系可以由教室与课程之间的开设联系、课程与学生之间的选修联系、学生与班级之间的组成联系三者推导出来,因此属于冗余联系。
3)学生实体中的平均成绩可以从选修联系中的成绩属性中推算出来。
如果需要经常查询学生的平均成绩,可以考虑保留该冗余数据,提高效率。但是为了维护数据一致性,应采用一定的机制以保持数据的一致。
这样,集成这两个子E-R图后的学生管理予系统的E-R图如图4.20所示。
学生管理子系统的基本E-R图还必须进一步和教师管理子系统以及后勤管理子系统等的基本E-R图合并,才能生成整个学校管理系统的基本E-R图。
4.4.4数据库的逻辑设计
1.从E-R图向关系模式转换数据库的逻辑设计主要工作是将E-R图转换成指定RDBMS中的关系模式。首先,从E-R图到关系模式的转换是比较直接的,实体与联系都可以表示成关系,E-R图中属性也可以转换成关系的属性。实体集也可以转换成关系。E-R模型与关系间的转换如表4.10所示。
表4.10 E—R模型与关系间的比较表
E-R模型
关 系
E-R模型
关 系
属性实体
属性元组
实体集联系
关系关系
下面讨论由E-R图转换成关系模式时会遇到的一些转换问题。
(1)命名与属性域的处理关系模式中的命名可以用E-R图中原有命名,也可另行命名,但是应尽量避免重名,RDBMS一般只支持有限种数据类型而E-R中的属性域则不受此限制,如出现有RDBMS不支持的数据类型时则要进行类型转换。
(2)非原子属性处理
E-R图中允许出现非原子属性,但在关系模式中一般不允许出现非原子属性,非原子属性主要有集合型和元组型。如出现此种情况时可以进行转换,其转换办法是集合属性纵向展开而元组属性则横向展开。
例4.9学生实体有学号、学生姓名及选读课程,其中前两个为原子属性而后一个为集合型非原子属性,因为一个学生可选读若干课程,设有学生S1307,王承志,他修读Database,Operating System及Computer Network三门课,此时可将其纵向展开用关系形式如表4.11所示。
表4.11学生实体学 号
学生姓名
选读课程
S1307
S1307
S1307
王承志王承志王承志
Database
Operating System
Computer Network
(3)联系的转换在一般情况下联系可用关系表示,但是在有些情况下联系可归并到相关联的实体中。
2.逻辑模式规范化及调整、实现
(1)规范化在逻辑设计中还需对关系做规范化验证。
(2)RDBMS
对逻辑模式进行调整以满足RDBMS的性能、存储空间等要求,同时对模式做适应RDBMS限制条件的修改,它们包括如下内容:
①调整性能以减少连接运算;
②调整关系大小,使每个关系数量保持在合理水平,从而可以提高存取效率;
③尽量采用快照(snapshot),因在应用中经常仅需某固定时刻的值,此时可用快照将某时刻值固定,并定期更换,此种方式可以显著提高查询速度。
3.关系视图设计逻辑设计的另一个重要内容是关系视图的设计,它又称为外模式设计。关系视图是在关系模式基础上所设计的直接面向操作用户的视图,它可以根据用户需求随时创建,一般RDBMS均提供关系视图的功能。
关系视图的作用大致有如下几点:
(1)提供数据逻辑独立性:使应用程序不受逻辑模式变化的影响。数据的逻辑模式会随着应用的发展而不断变化,逻辑模式的变化必然会影响到应用程序的变化,这就会产生极为麻烦的维护工作。关系视图则起了逻辑模式与应用程序之间的隔离墙作用,有了关系视图后建立在其上的应用程序就不会随逻辑模式修改而产生变化,此时变动的仅是关系视图的定义。
(2)能适应用户对数据的不同需求:每个数据库有一个非常庞大的结构,而每个数据库用户则希望只知道他们自己所关心的那部分结构,不必知道数据的全局结构以减轻用户在此方面的负担。此时,可用关系视图屏蔽用户所不需要的模式,而仅将用户感兴趣的部分呈现出来。
(3)有一定数据保密功能:关系视图为每个用户划定了访问数据的范围,从而在应用的各用户间起了一定的保密隔离作用。
4.4.5数据库的物理设计数据库物理设计的主要目标是对数据库内部物理结构作调整并选择合理的存取路径,以提高数据库访问速度及有效利用存储空间。在现代关系数据库中已大量屏蔽了内部物理结构,因此留给用户参与物理设计的余地并不多,一般的RDBMS中留给用户参与物理设计的内容大致有如下几种:索引设计、集簇设计和分区设计。
4.4.6数据库管理数据库是一种共享资源,它需要维护与管理,这种工作称为数据库管理(Database administration),而实施此项管理的人则称为数据库管理员(Database administrator)简称DBA。数据库管理一般包含如下一些内容:数据库的建立、数据库的调整、数据库的重组、数据库的安全性控制与完整性控制、数据库的故障恢复和数据库的监控。下面对这些管理内容作简单讨论。
1.数据库的建立数据库的建立包括两部分内容,数据模式的建立及数据加载。
(1)数据模式建立数据模式由DBA负责建立,DBA利用RDBMS中的DDL语言定义数据库名,定义表及相应属性,定义主关键字、索引、集簇、完整性约束、用户访问权限,申请空间资源,定义分区等,此外还需定义视图。
(2)数据加载在数据模式定义后即可加载数据,DBA可以编制加载程序将外界数据加载至数据模式内,从而完成数据库的建立。
2.数据库的调整在数据库建立并经一段时间运行后往往会产生一些不适应的情况,此时需要对其作调整,数据库的调整一般由DBA完成,调整包括下面一些内容:
(1)调整关系模式与视图使之更能适应用户的需求;
(2)调整索引与集簇使数据库性能与效率更佳;
(3)调整分区、数据库缓冲区大小以及并发度使数据库物理性能更好。
3.数据库的重组数据库在经过一定时间运行后,其性能会逐步下降,下降的原因主要是由于不断的修改、删除与插入所造成的。由于不断的删除而造成盘区内废块的增多而影响I/O速度,由于不断的删除与插入而造成集簇的性能下降,同时也造成了存储空间分配的零散化,使得一个完整表的空间分散,从而造成存取效率下降。基于这些原因需要对数据库进行重新整理,重新调整存贮空间,此种工作叫数据库重组。一般数据库重组需花大量时间,并做大量的数据搬迁工作。实际中,往往是先做数据卸载,然后再重新加载从而达到数据重组的目的。目前一般RDBMS都提供一定手段,以实现数据重组功能。
4.数据库安全性控制与完整性控制数据库是一个单位的重要资源,它的安全性是极端重要的,DBA应采取措施保证数据不受非法盗用与破坏。此外,为保证数据的正确性,使录入库内的数据均能保持正确,需要有数据库的完整性控制。
5.数据库的故障校复一旦数据库中的数据遭受破坏,需要及时进行恢复,RDBMS一般都提供此种功能,并由DBA负责执行故障恢复功能。
6.数据库监控
DBA需随时观察数据库的动态变化,并在发生错误、故障或产生不适应情况时随时采取措施,如数据库死锁、对数据库的误操作等;同时还需监视数据库的性能变化,在必要时对数据库作调整。
习 题 4
一、选择题
1.在数据管理技术的发展过程中,经历了人工管理阶段、文件系统阶段和数据库系统阶段。其中数据独立性最高的阶段是
A)数据库系统 B)文件系统 c)人工管理 D)数据项管理
2.下述关于数据库系统的叙述中正确的是
A)数据库系统减少了数据冗余
B)数据库系统避免了一切冗余
C)数据库系统中数据的一致性是指数据类型一致
D)数据库系统比文件系统能管理更多的数据
3.数据库系统的核心是
A)数据库 B)数据库管理系统
C)数据模型 D)软件工具
4.用树形结构来表示实体之间联系的模型称为
A)关系模型 B)层次模型 c)网状模型 D)数据模型
5.关系表中的每一横行称为一个
A)元组 B)字段 c)属性 D)码
6.按条件f对关系R进行选择,其关系代数表达式是
A) B) C) D)
7.关系数据库管理系统能实现的专门关系运算包括
A)排序、索引、统计 B)选择、投影、连接
c)关联、更新、排序 D)显示、打印、制表
8.在关系数据库中,用来表示实体之间联系的是
A)树结构 B)网结构 C)线性表D)--维表
9.数据库设计包括两个方面的设计内容,它们是
A)概念设计和逻辑设计 B)模式设计和内模式设计
C)内模式设计和物理设计 D)结构特性设计和行为特性设计
10.将E-R图转换到关系模式时,实体与联系都可以表示成
A)属性 B)关系 C)键 D)域二、填空题
1.一个项目具有一个项目主管,一个项目主管可管理多个项目,则实体“项目主管”与实体“项目”的联系属于________的联系。
2.数据独立性分为逻辑独立性与物理独立性。当数据的存储结构改变时,其逻辑结构可以不变,因此,基于逻辑结构的应用程序不必修改,称为________。
3.数据库系统中实现各种数据管理功能的核心软件称为________。
4.关系模型的完整性规则是对关系的某种约束条件,包括实体完整性、________和自定义完整性。
5.在关系模型中,把数据看成一个二维表,每一个二维表称为一个________。
习题参考答案习题1参考答案一、选择题
1.C) 2.D) 3.A) 4.B) 5.C) 6.D) 7.B) 8.B) 9.B) 10.A)
二、填空题
1.log2n 2.350 3.DEBFCA 4.n(n-1)/2 5.3
习题2参考答案一、选择题
1.B) 2.A) 3.D) 4.B) 5,A)
二、填空题
1.重复(或循环) 2.功能性 3.封装 4.实例 5.继承
习题3参考答案一、选择题
1.D) 2.D) 3.A) 4.A) 5.C) 6.B) 7.D) 8.C) 9.B) 10.B)
二、填空题
1.文档 2.数据流 3.软件开发 4.变换型 5.软件工具
习题4参考答案一、选择题
1.A) 2.A) 3.B) 4.B) 5.A) 6.C) 7.B) 8.D) 9.A) 10.B)
二、填空题
1.一对多(或1:N) 2.逻辑独立性 3.数据库管理系统 4.参照完整性 5.关系
第1章 数据结构与算法 1
1.1 算 法 1
1.1.1算法的基本概念 1
1.1.2 算法复杂度 4
1.2 数据结构的基本概念 7
1.2.1什么是数据结构 7
1.2.2数据结构的图形表示 12
1.2.3线性结构与非线性结构 13
1.3线性表及其顺序存储结构 14
1.3.1线性表的基本概念 14
1.3.2线性表的顺序存储结构 15
1.3.3顺序表的插入运算 16
1.3.4顺序表的删除运算 17
1.4 栈和队列 19
1.4.1栈及其基本运算 19
1.4.2队列及其基本运算 20
1.5 线性链表 22
1.5.1线性链表的基本概念 22
1.5.2线性链表的基本运算 26
1.5.3循环链表及其基本运算 28
1.6 树与二叉树 29
1.6.1树的基本概念 29
1.6.2二叉树及其基本性质 32
1.6.3二叉树的存储结构 34
1.6.4二叉树的遍历 35
1.7 查找技术 36
1.7.1顺序查找 37
1.7.2二分法查找 37
1.8 排序技术 37
1.8.1交换类排序法 38
1.8.2插入类排序法 39
1.8.3选择类排序法 41
习 题 1 43
第2章 程序设计基础 45
2.1 程序设计方法与风格 45
2.2 结构化程序设针 46
2.2.1结构化程序设计的原则 46
2.2.2结构化程序设计的基本结构和特点 47
2.2.3结构化程序设计原则和方法的应用 48
2.3面向对象的程序设计 48
2.3.1关于面向对象方法 48
2.3.2面向对象方法的基本概念 51
习 题 2 54
第3章 软件工程基础 56
3.1 软件工程基本概念 56
3.1.1软件定义与软件特点 56
3.1.2软件危机与软件工程 57
3.1.3软件工程过程与软件生命周期 58
3.1.4软件工程的目标与原则 59
3.1.5软件开发工具与软件开发环境 60
3.2 结构化分析方法 61
3.2.1 需求分析与需求分析方法 61
3.2.2结构化分析方法 62
3.2.3软件需求规格说明书 66
3.3 结构化设计方法 67
3.3.1软件设计的基本概念 67
3.3.2概要设计 70
3.3.3详细设计 74
3.4 软件测试 78
3.4.1软件测试的目的 79
3.4.2软件测试的准则 79
3.4.3软件测试技术与方法综述 79
3.4.4软件测试的实施 86
3.5程序的调试 89
3.5.1基本概念 89
3.5.2软件调试方法 90
习 题 3 92
第4章 数据库设计基础 93
4.1 数据厍系统的基本概念 93
4.1.1数据、数据库、数据库管理系统 93
4.1.2数据库系统的发展 96
4.1.3数据库系统的基本特点 98
4.1.4数据库系统的内部结构体系 99
4.2 数据模型 100
4.2.1数据模型的基本概念 100
4.2.2E-R模型 101
4.2.3层次模型 105
4.2.4网状模型 105
4.2.5关系模型 106
4.3 关系代数 109
4.4 数据厍设计与管理 115
4.4.1数据库设计概述 115
4.4.2数据库设计的需求分析 116
4.4.3数据库概念设计 117
4.4.4数据库的逻辑设计 120
4.4.5数据库的物理设计 122
4.4.6数据库管理 122
习 题 4 123
习题参考答案 125
习题1参考答案 125
习题2参考答案 125
习题3参考答案 125
习题4参考答案 125
第1章 数据结构与算法
1.1 算 法
1.1.1算法的基本概念所谓算法是指解题方案的准确而完整的描述。
对于一个问题,如果可以通过一个计算机程序,在有限的存储空间内运行有限长的时间而得到正确的结果,则称这个问题是算法可解的。但算法不等于程序,也不等于计算方法。当然,程序也可以作为算法的一种描述,但程序通常还需考虑很多与方法和分析无关的细节问题,这是因为在编写程序时要受到计算机系统运行环境的限制。通常,程序的编制不可能优于算法的设计。
1.算法的基本特征作为一个算法,一般应具有以下几个基本特征。
(1)可行性(effectiveness)
针对实际问题设计的算法,人们总是希望能够得到满意的结果。但一个算法又总是在某个特定的计算工具上执行的,因此,算法在执行过程中往往要受到计算工具的限制,使执行结果产生偏差。例如,在进行数值计算时,如果某计算工具具有7位有效数字(如程序设计语言中的单精度运算),则在计算下列三个量
A=1012,B=1,C=-1012
的和时,如果采用不同的运算顺序,就会得到不同的结果,即
A+B+C=1012+1+(-1012)=0
A+C+B=1012+(-1012)+1=1
而在数学上,A+B+C与A+C+B是完全等价的。因此,算法与计算公式是有差别的。在设计一个算法时,必须要考虑它的可行性,否则是不会得到满意结果的。
(2)确定性(definiteness)
算法的确定性,是指算法中的每一个步骤都必须是有明确定义的,不允许有模棱两可的解释,也不允许有多义性。这一性质也反映了算法与数学公式的明显差别。在解决实际问题时,可能会出现这样的情况:针对某种特殊问题,数学公式是正确的,但按此数学公式设计的计算过程可能会使计算机系统无所适从。这是因为根据数学公式设计的计算过程只考虑了正常使用的情况,而当出现异常情况时,此计算过程就不能适应了。
(3)有穷性(finiteness)
算法的有穷性,是指算法必须能在有限的时间内做完,即算法必须能在执行有限个步骤之后终止。数学中的无穷级数,在实际计算时只能取有限项,即计算无穷级数值的过程只能是有穷的。因此,一个数的无穷级数表示只是一个计算公式,而根据精度要求确定的计算过程才是有穷的算法。
算法的有穷性还应包括合理的执行时间的含义。因为,如果一个算法需要执行千万年,显然失去了实用价值。
(4)拥有足够的情报一个算法是否有效,还取决于为算法所提供的情报是否足够。通常,算法中的各种运算总是要施加到各个运算对象上,而这些运算对象又可能具有某种初始状态,这是算法执行的起点或是依据。因此,一个算法执行的结果总是与输入的初始数据有关,不同的输入将会有不同的结果输出。当输入不够或输入错误时,算法本身也就无法执行或导致执行有错。一般来说,当算法拥有足够的情报时,此算法才是有效的,而当提供的情报不够时,算法可能无效。
综上所述,所谓算法,是一组严谨地定义运算顺序的规则,并且每一个规则都是有效的,且是明确的,此顺序将在有限的次数下终止。
2.算法的基本要素一个算法通常由两种基本要素组成:一是对数据对象的运算和操作,二是算法的控制结构。
(1)算法中对数据的运算和操作每个算法实际上是按解题要求从环境能进行的所有操作中选择合适的操作所组成的一组指令序列。因此,计算机算法就是计算机能处理的操作所组成的指令序列。
通常,计算机可以执行的基本操作是以指令的形式描述的。一个计算机系统能执行的所有指令的集合,称为该计算机系统的指令系统。计算机程序就是按解题要求从计算机指令系统中选择合适的指令所组成的指令序列。在一般的计算机系统中,基本的运算和操作有以下四类:
①算术运算:主要包括加、减、乘、除等运算。
②逻辑运算:主要包括“与”、“或”、“非”等运算。
③关系运算:主要包括“大于”、“小于”、“等于”、“不等于”等运算。
④数据传输:主要包括赋值、输入、输出等操作。
前面提到,计算机程序也可以作为算法的…种描述,但由于在编制计算机程序时通常要考虑很多与方法和分析无关的细节问题(如语法规则),因此,在设计算法的一开始,通常并不直接用计算机程序来描述算法,而是用别的描述工具(如流程图,专门的算法描述语言,甚至用自然语言)来描述算法。但不管用哪种工具来描述算法,算法的设计一般都应从上述四种基本操作考虑,按解题要求从这些基本操作中选择合适的操作组成解题的操作序列。算法的主要特征着重于算法的动态执行,它区别于传统的着重于静态描述或按演绎方式求解问题的过程。传统的演绎数学是以公理系统为基础的,问题的求解过程是通过有限次推演来完成的,每次推演都将对问题作进一步的描述,如此不断推演,直到直接将解描述出来为止。而计算机算法则是使用一些最基本的操作,通过对已知条件一步一步的加工和变换,从而实现解题目标。这两种方法的解题思路是不同的。
(2)算法的控制结构一个算法的功能不仅取决于所选用的操作,而且还与各操作之间的执行顺序有关。算法中各操作之间的执行顺序称为算法的控制结构。
算法的控制结构给出了算法的基本框架,它不仅决定了算法中各操作的执行顺序,而且也直接反映了算法的设计是否符合结构化原则。描述算法的工具通常有传统流程图、N-S结构化流程图、算法描述语言等。一个算法一般都可以用顺序、选择、循环三种基本控制结构组合而成。
3.算法设计基本方法计算机解题的过程实际上是在实施某种算法,这种算法称为计算机算法。计算机算法不同于人工处理的方法。
本节介绍工程上常用的几种算法设计方法,在实际应用时,各种方法之间往往存在着一定的联系。
(1)列举法列举法的基本思想是,根据提出的问题,列举所有可能的情况,并用问题中给定的条件检验哪些是需要的,哪些是不需要的。因此,列举法常用于解决“是否存在”或“有多少种可能”等类型的问题,例如求解不定方程的问题。
列举法的特点是算法比较简单。但当列举的可能情况较多时,执行列举算法的工作量将会很大。因此,在用列举法设计算法时,使方案优化,尽量减少运算工作量,是应该重点注意的。通常,在设计列举算法时,只要对实际问题进行详细的分析,将与问题有关的知识条理化、完备化、系统化,从中找出规律;或对所有可能的情况进行分类,引出一些有用的信息,是可以大大减少列举量的。
列举原理是计算机应用领域中十分重要的原理。许多实际问题,若采用人工列举是不可想象的,但由于计算机的运算速度快,擅长重复操作,可以很方便地进行大量列举。列举算法虽然是一种比较笨拙而原始的方法,其运算量比较大,但在有些实际问题中(如寻找路径、查找、搜索等问题),局部使用列举法却是很有效的,因此,列举算法是计算机算法中的一个基础算法。
(2)归纳法归纳法的基本思想是,通过列举少量的特殊情况,经过分析,最后找出一般的关系。显然,归纳法要比列举法更能反映问题的本质,并且可以解决列举量为无限的问题。但是,从一个实际问题中总结归纳出一般的关系,并不是一件容易的事情,尤其是要归纳出一个数学模型更为困难。从本质上讲,归纳就是通过观察一些简单而特殊的情况,最后总结出一般性的结论。
归纳是一种抽象,即从特殊现象中找出一般关系。但由于在归纳的过程中不可能对所有的情况进行列举,因此,最后由归纳得到的结论还只是一种猜测,还需要对这种猜测加以必要的证明。实际上,通过精心观察而得到的猜测得不到证实或最后证明猜测是错的,也是常有的事。
(3)递推所谓递推,是指从己知的初始条件出发,逐次推出所要求的各中间结果和最后结果。其中初始条件或是问题本身已经给定,或是通过对问题的分析与化简而确定。递推本质上也属于归纳法,工程上许多递推关系式实际上是通过对实际问题的分析与归纳而得到的,因此,递推关系式往往是归纳的结果。
递推算法在数值计算中是极为常见的。但是,对于数值型的递推算法必须要注意数值计算的稳定性问题。
(4)递归人们在解决一些复杂问题时,为了降低问题的复杂程度(如问题的规模等),一般总是将问题逐层分解,最后归结为一些最简单的问题。这种将问题逐层分解的过程,实际上并没有对问题进行求解,而只是当解决了最后那些最简单的问题后,再沿着原来分解的逆过程逐步进行综合,这就是递归的基本思想。由此可以看出,递归的基础也是归纳。在工程实际中,有许多问题就是用递归来定义的,数学中的许多函数也是用递归来定义的。递归在可计算性理论和算法设计中占有很重要的地位。
递归分为直接递归与间接递归两种。如果一个算法P显式地调用自己则称为直接递归。如果算法P调用另一个算法Q,而算法Q又调用算法P,则称为间接递归调用。
递归是很重要的算法设计方法之一。实际上,递归过程能将一个复杂的问题归结为若干个较简单的问题,然后将这些较简单的问题再归结为更简单的问题,这个过程可以一直做下去,直到最简单的问题为止。
有些实际问题,既可以归纳为递推算法,又可以归纳为递归算法。但递推与递归的实现方法是大不一样的。递推是从初始条件出发,逐次推出所需求的结果;而递归则是从算法本身到达递归边界的。通常,递归算法要比递推算法清晰易读,其结构比较简练。特别是在许多比较复杂的问题中,很难找到从初始条件推出所需结果的全过程,此时,设计递归算法要比递推算法容易得多。但递归算法的执行效率比较低。
(5)减半递推技术实际问题的复杂程度往往与问题的规模有着密切的联系。因此,利用分治法解决这类实际问题是有效的。所谓分治法,就是对问题分而治之。工程上常用的分治法是减半递推技术。
所谓“减半”,是指将问题的规模减半,而问题的性质不变;所谓“递推”,是指重复“减半”的过程。
下面举例说明利用减半递推技术设计算法的基本思想。
例1.1设方程f(x)=0在区间[a,b]上有实根,且f(a)与f(b)异号。利用二分法求该方程在区间[a,b]上的一个实根。
用二分法求方程实根的减半递推过程如下:
首先取给定区间的中点c=(a+b)/2。
然后判断f(c)是否为0。若f(c)=0,则说明c即为所求的根,求解过程结束;如果f(c)≠0,则根据以下原则将原区间减半:
若f(a)f(c)<0,则取原区间的前半部分;
若f(b)f(c)<0,则取原区间的后半部分。
最后判断减半后的区间长度是否已经很小:
若|a-b|<ε,则过程结束,取(a+b)/2为根的近似值;
若|a-b|≥ε,则重复上述的减半过程。
(6)回溯法前面讨论的递推和递归算法本质上是对实际问题进行归纳的结果,而减半递推技术也是归纳法的一个分支。在工程上,有些实际问题很难归纳出一组简单的递推公式或直观的求解步骤,并且也不能进行无限的列举。对于这类问题,一种有效的方法是“试”。通过对问题的分析,找出一个解决问题的线索,然后沿着这个线索逐步试探,对于每一步的试探,若试探成功,就得到问题的解,若试探失败,就逐步回退,换别的路线再进行试探。这种方法称为回溯法。回溯法在处理复杂数据结构方面有着广泛的应用。
1.1.2 算法复杂度算法的复杂度主要包括时间复杂度和空间复杂度。
1.算法的时间复杂度所谓算法的时间复杂度,是指执行算法所需要的计算工作量。
为了能够比较客观地反映出一个算法的效率,在度量一个算法的工作量时,不仅应该与所使用的计算机、程序设计语言以及程序编制者无关,而且还应该与算法实现过程中的许多细节无关。为此,可以用算法在执行过程中所需基本运算的执行次数来度量算法的工作量。基本运算反映了算法运算的主要特征,因此,用基本运算的次数来度量算法工作量是客观的也是实际可行的,有利于比较同一问题的几种算法的优劣。例如,在考虑两个矩阵相乘时,可以将两个实数之间的乘法运算作为基本运算,而对于所用的加法(或减法)运算忽略不计。又如,当需要在一个表中进行查找时,可以将两个元素之间的比较作为基本运算。
算法所执行的基本运算次数还与问题的规模有关。例如,两个20阶矩阵相乘与两个lO阶矩阵相乘,所需要的基本运算(即两个实数的乘法)次数显然是不同的,前者需要更多的运算次数。因此,在分析算法的工作量时,还必须对问题的规模进行度量。
综上所述,算法的工作量用算法所执行的基本运算次数来度量,而算法所执行的基本运算次数是问题规模的函数,即算法的工作量:f(n)
其中n是问题的规模。例如,两个n阶矩阵相乘所需要的基本运算(即两个实数的乘法)次数为n3,即计算工作量为n3,也就是时间复杂度为n3。
在具体分析一个算法的工作量时,还会存在这样的问题:对于一个固定的规模,算法所执行的基本运算次数还可能与特定的输入有关,而实际上又不可能将所有可能情况下算法所执行的基本运算次数都列举出来。例如,“在长度为n的一维数组中查找值为x的元素”,若采用顺序搜索法,即从数组的第一个元素开始,逐个与被查值x进行比较。显然,如果第一个元素恰为x,则只需要比较1次。但如果x为数组的最后一个元素,或者x不在数组中,则需要比较n次才能得到结果。因此,在这个问题的算法中,其基本运算(即比较)的次数与具体的被查值x有关。
在同一个问题规模下,如果算法执行所需的基本运算次数取决于某一特定输入时,可以用以下两种方法来分析算法的工作量。
(1)平均性态(Average Behavior)
所谓平均性态分析,是指用各种特定输入下的基本运算次数的加权平均值来度量算法的工作量。
设x是所有可能输入中的某个特定输入,p(x)是x出现的概率(即输入为x的概率),t(x)是算法在输入为x时所执行的基本运算次数,则算法的平均性态定义为
其中D。表示当规模为n时,算法执行时所有可能输入的集合。这个式子中的t(x)可以通过分析算法来加以确定;而p(x)必须由经验或用算法中有关的一些特定信息来确定,通常是不能解析地加以计算的。如果确定p(x)比较困难,则会给平均性态的分析带来困难。
(2)最坏情况复杂性(Worst-Case Complexity)
所谓最坏情况分析,是指在规模为n时,算法所执行的基本运算的最大次数。它定义为
显然,w(n)的计算要比A(n)的计算方便得多。由于w(n)实际上是给出了算法工作量的一个上界,因此,它比A(n)更具有实用价值。
下面通过一个例子来说明算法复杂度的平均性态分析与最坏情况分析。
例1.2采用顺序搜索法,在长度为n的一维数组中查找值为x的元素。即从数组的第一个元素开始,逐个与被查值x进行比较。基本运算为x与数组元素的比较。
首先考虑平均性态分析。
设被查项x在数组中出现的概率为q。当需要查找的x为数组中第i个元素时,则在查找过程中需要做i次比较,当需要查找的x不在数组中时(即数组中没有x这个元素),则需要与数组中所有的元素进行比较。即
其中i=n+1表示x不在数组中的情况。
如果假设需要查找的x出现在数组中每个位置上的可能性是一样的,则x出现在数组中每一个位置上的概率为q/n(因为前面已经假设x在数组中的概率为q),而x不在数组中的概率为1-q。即
其中i=n+l表示x不在数组中的情况。
因此,用顺序搜索法在长度为n的一维数组中查找值为x的元素,在平均情况下需要做的比较次数为
如果已知需要查找的x一定在数组中,此时q=l,则A(n)=(n+1)/2。这就是说,在这种情况下,用顺序搜索法在长度为n的一维数组中查找值为x的元素,在平均情况下需要检查数组中一半的元素。
如果已知需要查找的x有一半的机会在数组中,此时q=l/2,则
这就是说,在这种情况下,用顺序搜索法在长度为n的一维数组中查找值为x的元素,在平均情况下需要检查数组中3/4的元素。
再考虑最坏情况分析。
在这个例子中,最坏情况发生在需要杳找的x是数组中的最后一个元素或x不在数细中的时候。此时显然有
在上述例子中,算法执行的工作量是与具体的输入有关的,A(n)只是它的加权平均值,而实际上对于某个特定的输入,其计算工作量未必是A(n),且A(n)也不一定等于w(n)。但在另外一些情况下,算法的计算工作量与输入无关,即当规模为n时,在所有可能的输入下,算法所执行的基本运算次数是一定的,此时有A(n)=w(n)。例如,两个n阶的矩阵相乘,都需要做n。次实数乘法,而与输入矩阵的具体元素无关。
2.算法的空间复杂度一个算法的空间复杂度,一般是指执行这个算法所需要的内存空间。
一个算法所占用的存储空间包括算法程序所占的空间、输入的初始数据所占的存储空间以及算法执行过程中所需要的额外空间。其中额外空间包括算法程序执行过程中的工作单元以及某种数据结构所需要的附加存储空间(例如,在链式结构中,除了要存储数据本身外,还需要存储链接信息)。如果额外空间量相对于问题规模来说是常数,则称该算法是原地(in place)工作的。在许多实际问题中,为了减少算法所占的存储空间,通常采用压缩存储技术,以便尽量减少不必要的额外空间。
1.2 数据结构的基本概念利用计算机进行数据处理是计算机应用的一个重要领域。在进行数据处理时,实际需要处理的数据元素一般有很多,而这些大量的数据元素都需要存放在计算机中,因此,大量的数据元素在计算机中如何组织,以便提高数据处理的效率,并且节省计算机的存储空间,这是进行数据处理的关键问题。
显然,杂乱无章的数据是不便于处理的。而将大量的数据随意地存放在计算机中,实际上也是“白找苦吃”,对数据处理更是不利。
数据结构作为计算机的一门学科,主要研究和讨论以下三个方面的问题:
①数据集合中各数据元素之间所固有的逻辑关系,即数据的逻辑结构;
②在对数据进行处理时,各数据元素在计算机中的存储关系,即数据的存储结构;
③对各种数据结构进行的运算。
讨论以上问题的主要目的是为了提高数据处理的效率。所谓提高数据处理的效率,主要包括两个方面:一是提高数据处理的速度,二是尽量节省在数据处理过程中所占用的计算机存储空间。
本章主要讨论工程上常用的一些基本数据结构,它们是软件设计的基础。
1.2.1什么是数据结构计算机已被广泛用于数据处理。实际问题中的各数据元素之间总是相互关联的。所谓数据处理,是指对数据集合中的各元素以各种方式进行运算,包括插入、删除、查找、更改等运算,也包括对数据元素进行分析。在数据处理领域中,建立数学模型有时并不十分重要,事实上,许多实际问题是无法表示成数学模型的。人们最感兴趣的是知道数据集合中各数据元素之间存在什么关系,应如何组织它们,即如何表示所需要处理的数据元素。
下面通过两个实例来说明对同一批数据用不同的表示方法后,对处理效率的影响。
例1.3无序表的顺序查找与有序表的对分查找:
图1.1是两个子表。从图中可以看出,在这两个子表中所存放的数据元素是相同的,但它们在表中存放的顺序是不同的。在图1.1(a)所示的表中,数据元素的存放顺序是没有规则的;而在图1.1(b)所示的表中,数据元素是按从小到大的顺序存放的。我们称前者为无序表,后者为有序表。
下面讨论在这两种表中进行查找的问题。
首先讨论在图1.1(a)所示的无序表中进行查找。由于在图1.1(a)表中数据元素的存放顺序没有一定的规则,因此,要在这个表中查找某个数时,只能从第一个元素开始,逐个将表中的元素与被查数进行比较,直到表中的某个元素与被查数相等(即查找成功)或者表中所有元素与被查数都进行了比较且都不相等(即查找失败)为止。这种查找方法称为顺序查找。显然,在顺序查找中,如果被查找数在表的前部,则需要比较的次数就少;但如果被查找数在表的后部,则需要比较的次数就多。特别是当被查找数刚好是表中的第一个元素时(如被查数为35),只需要比较一次就查找成功;但当被查数刚好是表中最后一个元素(如被查数为46)或表中根本就没有被查数时(如被查数为67),则需要与表中所有的元素进行比较,在这种情况下,当表很大时,顺序查找是很费时间的。虽然顺序查找法的效率比较低,但由于图1.1(a)为无序表,没有更好的查找方法,只能用顺序查找。
现在再讨论在图1.1(b)所示的有序表中进行查找。由于有序表中的元素是从小到大进行排列的,在查找时可以利用这个特点,以便使比较次数大大减少。在有序表中查找一个数可以如下进行:
将被查数与表中的中间元素进行比较:若相等,则表示查找成功,查找过程结束;若被查数大于表中的这个中间元素时,则表示如果被查数在表中的话,只能在表的后半部,此时可以抛弃表的前半部而保留后半部;若被查数小于表中的这个中间元素,则表示如果被查数在表中的话,只能在表的前半部,此时可以抛弃表的后半部而保留前半部。然后对剩下的部分(前半部或后半部)再按照上述方法进行查找,这个过程一直做到在某一次的比较中相等(查找成功)或剩下的部分已空(查找失败)为止。例如,如果要在图1.1(b)所示的有序表中查找54,则首先与中间元素35进行比较,由于54大于35,再与后半部分的中间元素54进行比较,此时相等,共比较了2次就查找成功。如果采用顺序查找法,在图1.1(a)所示的无序表中查找54这个元素,需要比较9次。这种查找方法称为有序表的对分查找。
显然,在有序表的对分查找中,不论查找的是什么数,也不论要查找的数在表中有没有,都不需要与表中所有的元素进行比较,只需要与表中很少的元素进行比较。但需要指出的是,对分查找只适用于有序表,而对于无序表是无法进行对分查找的。
实际上,在日常工作和学习中也经常遇到对分查找。例如,当需要在词典中查找一个单词时,一般不是从第一页开始一页一页的往后找,而是考虑到词典中的各单词是以英文字母为顺序排列的,因此可以根据所查单词的第一个字母,直接翻到大概的位置,然后进行比较,根据比较结果再向前或向后翻,直到找到该单词为止。这种在词典中查单词的方法类似于对分查找。
由这个例子可以看出,数据元素在表中的排列顺序对查找效率是有很大影响的。
例1.4设有一学生情况登记表如表1.1所示。在表1.1中,每个学生的情况是以学号为顺序排列的。
显然,如果要在表1.1中查找给定学号的某学生的情况是很方便的,只要根据给定的学号就可以立即找到该学生的情况。但是,如果要在该表中查找成绩在90分以上的所有学生的情况,则需要从头到尾扫描全表,才能将成绩在90分以上的所有学生找到。在这种情况下,为了找到成绩在90分以上的学生情况,对于成绩在90分以下的所有学生情况也都要被扫描到。由此可以看出,要在表1.1中查找给定学号的学生情况虽然很方便,但要查找成绩在某个分数段中的学生情况时,实际上需要查看表中所有学生的成绩,其效率是很低的,尤其是当表很大时更为突出。
表1.1学生情况登记表学 号
姓 名
性 别
年 龄
成 绩
学 号
姓 名
性 别
年 龄
成 绩
970156
张小明
男
20
86
970163
王 伟
男
20
65
970157
李小青
女
19
83
970164
胡 涛
男
19
95
970158
赵 凯
男
19
70
970165
周 敏
女
20
87
970159
李启明
男
21
91
970166
杨雪辉
男
22
89
970160
刘 华
女
18
78
970167
吕永华
男
18
61
970161
曾小波
女
19
90
970168
梅 玲
女
17
93
970162
张 军
男
18
80
970169
刘 健
男
20
75
为了便于查找成绩在某个分数段中的学生情况,可以将表1.1中所登记的学生情况进行重新组织。例如,将成绩在90分以上(包括90分,下同)、80~89分、70~79分、60~69分之间的学生情况分别登记在四个独立的子表中,分别如表1.2、表1.3、表1.4与表1.5所示。现在如果要查找90分以上的所有学生的情况,就可以直接在表1.2中进行查找,从而避免了对成绩在90分以下的学生情况进行扫描,提高了查找效率。
表1.2成绩在90分以上的学生情况登记表学 号
姓 名
性 别
年 龄
成 绩
学 号
姓 名
性 别
年 龄
成 绩
970159
李启明
男
21
91
970164
胡涛
男
19
95
970161
曾小波
女
19
90
970168
梅玲
女
17
93
表1.3成绩在80~89分之间的学生情况登记表学 号
姓 名
性 别
年 龄
成 绩
学 号
姓 名
性 别
年 龄
成 绩
970156
张小明
男
20
86
970165
周 敏
女
20
87
970157
李小青
女
19
83
970166
杨雪辉
男
22
89
970162
张 军
男
18
80
表1.4成绩在70~79分之间的学生情况登记表学 号
姓 名
性 别
年 龄
成 绩
学 号
姓 名
性 别
年 龄
成 绩
970158
赵 凯
男
19
70
970169
刘健
男
20
75
970160
刘 华
女
18
78
表1.5成绩在60~69分之间的学生情况登记表学 号
姓 名
性 别
年 龄
成 绩
学 号
姓 名
性 别
年 龄
成 绩
970163
王 伟
男
20
65
4970167
吕永华
男
18
61
由例1-4可以看出,在对数据进行处理时,可以根据所做的运算不同,将数据组织成不同的形式,以便于做该种运算,从而提高数据处理的效率。
简单地说,数据结构是指相互有关联的数据元素的集合。例如,向量和矩阵就是数据结构,在这两个数据结构中,数据元素之间有着位置上的关系。又如,图书馆中的图书卡片目录,则是一个较为复杂的数据结构,对于列在各卡片上的各种书之间,可能在主题、作者等问题上相互关联,甚至一本书本身也有不同的相关成分。
数据元素具有广泛的含义。一般来说,现实世界中客观存在的一切个体都可以是数据元素。例如:
描述一年四季的季节名春、夏、秋、冬可以作为季节的数据元素;
表示数值的各个数
18、11、35、23、16、…
可以作为数值的数据元素;
表示家庭成员的各成员名父亲、儿子、女儿可以作为家庭成员的数据元素。
甚至每一个客观存在的事件,如一次演出、一次借书、一次比赛等也可以作为数据元素。
总之,在数据处理领域中,每一个需要处理的对象都可以抽象成数据元素。数据元素一般简称为元素。
在实际应用中,被处理的数据元素一般有很多,而且,作为某种处理,其中的数据元素一般具有某种共同特征。例如,{春,夏,秋,冬)这四个数据元素有一个共同特征,即它们都是季节名,分别表示了一年中的四个季节,从而这四个数据元素构成了季节名的集合。又如,{父亲,儿子,女儿)这三个数据元素也有一个共同特征,即它们都是家庭的成员名,从而构成了家庭成员名的集合。一般来说,人们不会同时处理特征完全不同且互相之间没有任何关系的各类数据元素,对于具有不同特征的数据元素总是分别进行处理。
一般情况下,在具有相同特征的数据元素集合中,各个数据元素之间存在有某种关系(即联系),这种关系反映了该集合中的数据元素所固有的一种结构。在数据处理领域中,通常把数据元素之间这种固有的关系简单地用前后件关系(或直接前驱与直接后继关系)来描述。
例如,在考虑一年四个季节的顺序关系时,则“春”是“夏”的前件(即直接前驱,下同),而“夏”是“春”的后件(即直接后继,下同)。同样,“夏”是“秋”的前件,“秋”是“夏”的后件;“秋”是“冬”的前件,“冬”是“秋”的后件。
在考虑家庭成员间的辈分关系时,则“父亲”是“儿子”和“女儿”的前件,而“儿子”与“女儿”都是“父亲”的后件。
前后件关系是数据元素之间的一个基本关系,但前后件关系所表示的实际意义随具体对象的不同而不同。一般来说,数据元素之间的任何关系都可以用前后件关系来描述。
1.数据的逻辑结构前面提到,数据结构是指反映数据元素之间关系的数据元素集合的表示。更通俗地说,数据结构是指带有结构的数据元素的集合。在此,所谓结构实际上就是指数据元素之间的前后件关系。
由上所述,一个数据结构应包含以下两方面的信息:
①表示数据元素的信息;
②表示各数据元素之间的前后件关系。
在以上所述的数据结构中,其中数据元素之间的前后件关系是指它们的逻辑关系,而与它们在计算机中的存储位置无关。因此,上面所述的数据结构实际上是数据的逻辑结构。
所谓数据的逻辑结构,是指反映数据元素之间逻辑关系的数据结构。
由前面的叙述可以知道,数据的逻辑结构有两个要素:一是数据元素的集合,通常记为D;二是D上的关系,它反映了D中各数据元素之间的前后件关系,通常记为R。即一个数据结构可以表示成
B=(D,R)
其中B表示数据结构。为了反映D中各数据元素之间的前后件关系,一般用二元组来表示。例如,假设a与b是D中的两个数据,则二元组(a,b)表示a是b的前件,b是a的后件。这样,在D中的每两个元素之间的关系都可以用这种二元组来表示。
例1.5一年四季的数据结构可以表示成
例1.6家庭成员数据结构可以表示成
例1.7 n维向量
也是一种数据结构。即X=(D,R),其中数据元素的集合为
关系为
对于一些复杂的数据结构来说,它的数据元素可以是另一种数据结构。
例如,m×n的矩阵
是一个数据结构。在这个数据结构中,矩阵的每一行
可以看成是它的一个数据元素。即这个数据结构的数据元素的集合为
D上的一个关系为
显然,数据结构A中的每一个数据元素A(i=1,2,…,m)又是另一个数据结构,即数据元素的集合为
Di上的一个关系为
2.数据的存储结构数据处理是计算机应用的一个重要领域,在实际进行数据处理时,被处理的各数据元素总是被存放在计算机的存储空间中,并且,各数据元素在计算机存储空间中的位置关系与它们的逻辑关系不一定是相同的,而且一般也不可能相同。例如,在前面提到的一年四个季节的数据结构中,“春”是“夏”的前件,“夏”是“春”的后件,但在对它们进行处理时,在计算机存储空间中,“春”这个数据元素的信息不一定被存储在“夏”这个数据元素信息的前面,而可能在后面,也可能不是紧邻在前面,而是中间被其他的信息所隔开。又如,在家庭成员的数据结构中,“儿子”和“女儿”都是“父亲”的后件,但在计算机存储空间中,根本不可能将“儿子”和“女儿”这两个数据元素的信息都紧邻存放在“父亲”这个数据元素信息的后面,即在存储空间中与“父亲”紧邻的只可能是其中的一个。由此可以看出,一个数据结构中的各数据元素在计算机存储空间中的位置关系与逻辑关系是有可能不同的。
数据的逻辑结构在计算机存储空间中的存放形式称为数据的存储结构(也称数据的物理结构)。
由于数据元素在计算机存储空间中的位置关系可能与逻辑关系不同,因此,为了表示存放在计算机存储空间中的各数据元素之间的逻辑关系(即前后件关系),在数据的存储结构中,不仅要存放各数据元素的信息,还需要存放各数据元素之间的前后件关系的信息。
一般来说,一种数据的逻辑结构根据需要可以表示成多种存储结构,常用的存储结构有顺序、链接、索引等存储结构。而采用不同的存储结构,其数据处理的效率是不同的。因此,在进行数据处理时,选择合适的存储结构是很重要的。
1.2.2数据结构的图形表示一个数据结构除了用二元关系表示外,还可以直观地用图形表示。在数据结构的图形表示中,对于数据集合D中的每一个数据元素用中间标有元素值的方框表示,一般称之为数据结点,并简称为结点;为了进一步表示各数据元素之间的前后件关系,对于关系R中的每一个二元组,用一条有向线段从前件结点指向后件结点。
例如,一年四季的数据结构可以用如图1.2所示的图形来表示。
又如,反映家庭成员间辈分关系的数据结构可以用如图1-3所示的图形表示。
显然,用图形方式表示一个数据结构是很方便的,并且也比较直观。有时在不会引起误会的情况下,在前件结点到后件结点连线上的箭头可以省去。例如,在图1.3中,即使将“父亲”结点与“儿子”结点连线上的箭头以及“父亲”结点与“女儿”结点连线上的箭头都去掉,同样表示了“父亲”是“儿子”与“女儿”的前件,“儿子”与“女儿”均是“父亲”的后件,而不会引起误会。
例1.8用图形表示数据结构B=(D,R),其中
这个数据结构的图形表示如图1.4所示。
在数据结构中,没有前件的结点称为根结点;没有后件的结点称为终端结点(也称为叶子结点)。例如,在图1.2所示的数据结构中,元素“春”所在的结点(简称为结点“春”,下同)为根结点,结点“冬”为终端结点;在图l-3所示的数据结构中,结点“父亲”为根结点,结点“儿子”与“女儿”均为终端结点;在图1.4所示的数据结构中,有两个根结点d1与d2,有三个终端结点d6、d7、d5、出。数据结构中除了根结点与终端结点外的其他结点一般称为内部结点。
通常,一个数据结构中的元素结点可能是在动态变化的。根据需要或在处理过程中,可以在一个数据结构中增加一个新结点(称为插入运算),也可以删除数据结构中的某个结点(称为删除运算)。插入与删除是对数据结构的两种基本运算。除此之外,对数据结构的运算还有查找、分类、合并、分解、复制和修改等。在对数据结构的处理过程中,不仅数据结构中的结点(即数据元素)个数在动态地变化,而且,各数据元素之间的关系也有可能在动态地变化。例如,一个无序表可以通过排序处理而变成有序表;一个数据结构中的根结点被删除后,它的某一个后件可能就变成了根结点;在一个数据结构中的终端结点后插入一个新结点后,则原来的那个终端结点就不再是终端结点而成为内部结点了。有关数据结构的基本运算将在后面讲到具体数据结构时再介绍。
1.2.3线性结构与非线性结构如果在一个数据结构中一个数据元素都没有,则称该数据结构为空的数据结构。在一个空的数据结构中插入一个新的元素后就变为非空;在只有一个数据元素的数据结构中,将该元素删除后就变为空的数据结构。
根据数据结构中各数据元素之间前后件关系的复杂程度,一般将数据结构分为两大类型:线性结构与非线性结构。
如果一个非空的数据结构满足下列两个条件:
①有且只有一个根结点;
②每一个结点最多有一个前件,也最多有一个后件。
则称该数据结构为线性结构。线性结构又称线性表。
由此可以看出,在线性结构中,各数据元素之间的前后件关系是很简单的。如例1.5中的一年四季这个数据结构,以及例1.7中的n维向量数据结构,它们都属于线性结构。
特别需要说明的是,在一个线性结构中插入或删除任何一个结点后还应是线性结构。根据这一点,如果一个数据结构满足上述两个条件,但当在此数据结构中插入或删除任何一个结点后就不满足这两个条件了,则该数据结构不能称为线性结构。例如,图1.5所示的数据结构显然是满足上述两个条件的,但它不属于线性结构这个类型,因为如果在这个数据结构中删除结点A后,就不满足上述的条件(1)。
如果一个数据结构不是线性结构,则称之为非线性结构。如例1.6中反映家庭成员间辈分关系的数据结构,以及例1.8中的数据结构,它们都不是线性结构,而是属于非线性结构。显然,在非线性结构中,各数据元素之间的前后件关系要比线性结构复杂,因此,对非线性结构的存储与处理比线性结构要复杂得多。
线性结构与非线性结构都可以是空的数据结构。一个空的数据结构究竟是属于线性结构还是属于非线性结构,这要根据具体情况来确定。如果对该数据结构的运算是按线性结构的规则来处理的,则属于线性结构;否则属于非线性结构。
1.3线性表及其顺序存储结构
1.3.1线性表的基本概念线性表(Linear List)是最简单、最常用的一种数据结构。
线性表由一组数据元素构成。数据元素的含义很广泛,在不同的具体情况下,它可以有不同的含义。例如,一个n维向量(x1,x2,…,xn)是一个长度为n的线性表,其中的每一个分量就是一个数据元素。又如,英文小写字母表(a,b,c,…,z)是一个长度为26的线性表,其中的每一个小写字母就是一个数据元素。再如,一年中的四个季节(春,夏,秋,冬)是一个长度为4的线性表,其中的每一个季节名就是一个数据元素。
矩阵也是一个线性表,只不过它是一个比较复杂的线性表。在矩阵中,既可以把每一行看成是一个数据元素(即一个行向量为一个数据元素),也可以把每一列看成是一个数据元素(即一个列向量为一个数据元素)。其中每一个数据元素(一个行向量或一个列向量)实际上又是一个简单的线性表。
数据元素可以是简单项(如上述例子中的数、字母、季节名等)。在稍微复杂的线性表中,一个数据元素还可以由若干个数据项组成。例如,某班的学生情况登记表是一个复杂的线性表,表中每一个学生的情况就组成了线性表中的每一个元素,每一个数据元素包括姓名、学号、性别、年龄和健康状况5个数据项,如表1.6所示。在这种复杂的线性表中,由若干数据项组成的数据元素称为记录(record),而由多个记录构成的线性表又称为文件(file)。因此,上述学生情况登记表就是一个文件,其中每一个学生的情况就是一个记录。
表1.6学生情况登记表姓 名
学 号
性 别
年 龄
健康状况
王 强刘建平赵 军葛文华
…
800356
800357
800361
800367
…
男男女男
…
19
20
19
2l
…
良好一般良好较差
…
综上所述,线性表是由n(n≥0)个数据元素a1,a2,…,an组成的一个有限序列,表中的每一个数据元素,除了第一个外,有且只有一个前件,除了最后一个外,有且只有一个后件。即线性表或是一个空表,或可以表示为
其中ai(i=l,2,…,n)是属于数据对象的元素,通常也称其为线性表中的一个结点。
显然,线性表是一种线性结构。数据元素在线性表中的位置只取决于它们自己的序号,即数据元素之间的相对位置是线性的。
非空线性表有如下一些结构特征:
①有且只有一个根结点a1,它无前件;
②有且只有一个终端结点an,它无后件;
③除根结点与终端结点外,其他所有结点有且只有一个前件,也有且只有一个后件。线性表中结点的个数n称为线性表的长度。当n=0时,称为空表。
1.3.2线性表的顺序存储结构在计算机中存放线性表,一种最简单的方法是顺序存储,也称为顺序分配。
线性表的顺序存储结构具有以下两个基本特点:
①线性表中所有元素所占的存储空间是连续的;
②线性表中各数据元素在存储空间中是按逻辑顺序依次存放的。
由此可以看出,在线性表的顺序存储结构中,其前后件两个元素在存储空间中是紧邻的,且前件元素一定存储在后件元素的前面。
在线性表的顺序存储结构中,如果线性表中各数据元素所占的存储空间(字节数)相等,则要在该线性表中查找某一个元素是很方便的。
假设线性表中的第一个数据元素的存储地址(指第一个字节的地址,即首地址)为ADR(a1),每一个数据元素占k个字节,则线性表中第i个元素ai在计算机存储空间中的存储地址为
即在顺序存储结构中,线性表中每一个数据元素在计算机存储空间中的存储地址由该元素在线性表中的位置序号惟一确定。一般来说,长度为n的线性表
在计算机中的顺序存储结构如图1.6所示。
在程序设计语言中,通常定义一个一维数组来表示线性表的顺序存储空间。因为程序设计语言中的一维数组与计算机中实际的存储空间结构是类似的,这就便于用程序设计语言对线性表进行各种运算处理。
在用一维数组存放线性表时,该一维数组的长度通常要定义得比线性表的实际长度大一些,以便对线性表进行各种运算,特别是插入运算。在一般情况下,如果线性表的长度在处理过程中是动态变化的,则在开辟线性表的存储空间时要考虑到线性表在动态变化过程中可能达到的最大长度。如果开始时所开辟的存储空间太小,则在线性表动态增长时可能会出现存储空间不够而无法再插入新的元素;但如果开始时所开辟的存储空间太大,而实际上又用不着那么大的存储空间,则会造成存储空间的浪费。在实际应用中,可以根据线性表动态变化过程中的一般规模来决定开辟的存储空间量。
在线性表的顺序存储结构下,可以对线性表进行各种处理。主要的运算有以下几种:
①在线性表的指定位置处加入一个新的元素(即线性表的插入);
②在线性表中删除指定的元素(即线性表的删除);
③在线性表中查找某个(或某些)特定的元素(即线性表的查找);
④对线性表中的元素进行整序(即线性表的排序);
⑤按要求将一个线性表分解成多个线性表(即线性表的分解);
⑥按要求将多个线性表合并成一个线性表(即线性表的合并);
⑦复制一个线性表(即线性表的复制);
⑧逆转一个线性表(即线性表的逆转)等。
下面两小节主要讨论线性表在顺序存储结构下的插入与删除的问题。
1.3.3顺序表的插入运算首先举一个例子来说明如何在顺序存储结构的线性表中插入一个新元素。
例1.9 图1.7(a)为一个长度为8的线性表顺序存储在长度为10的存储空间中。现在要求在第2个元素(即18)之前插入一个新元素87。其插入过程如下:
首先从最后一个元素开始直到第2个元素,将其中的每一个元素均依次往后移动一个位置,然后将新元素87插入到第2个位置。
插入一个新元素后,线性表的长度变成了9,如图1.7(b)所示。
如果再要在线性表的第9个元素之前插入一个新元素14,则采用类似的方法:将第9个元素往后移动一个位置,然后将新元素插入到第9个位置。插入后,线性表的长度变成了10,如图1.7(c)所示。
现在,为线性表开辟的存储空间已经满了,不能再插入新的元素了。如果再要插入,则会造成称为“上溢”的错误。
一般来说,设长度为n的线性表为
现要在线性表的第i个元素ai之前插入一个新元素b,插入后得到长度为n+1的线性表为
则插入前后的两线性表中的元素满足如下关系
在一般情况下,要在第i(1≤i≤n)个元素之前插入一个新元素时,首先要从最后一个(即第n个)元素开始,直到第i个元素之间共n-i+1个元素依次向后移动一个位置,移动结束后,第i个位置就被空出,然后将新元素插入到第i项。插入结束后,线性表的长度就增加了1。
显然,在线性表采用顺序存储结构时,如果插入运算在线性表的末尾进行,即在第n个元素之后(可以认为是在第n+1个元素之前)插入新元素,则只要在表的末尾增加一个元素即可,不需要移动表中的元素;如果要在线性表的第1个元素之前插入一个新元素,则需要移动表中所有的元素。在一般情况下,如果插入运算在第i(1≤i≤n)个元素之前进行,则原来第i个元素之后(包括第i个元素)的所有元素都必须移动。在平均情况下,要插入一个新元素,需要移动表中一半的元素。因此,在线性表顺序存储的情况下,要插入一个新元素,其效率是很低的,特别是在线性表比较大的情况下更为突出,由于数据元素的移动而消耗较多的处理时间。
1.3.4顺序表的删除运算首先举一个例子来说明如何在顺序存储结构的线性表中删除一个元素。
例1.10图1.8(a)为一个长度为8的线性表顺序存储在长度为10的存储空间中。现在要求删除线性表中的第1个元素(即删除元素29)。其删除过程如下:
从第2个元素开始直到最后一个元素,将其中的每一个元素均依次往前移动一个位置。此时,线性表的长度变成了7,如图1.8(b)所示。
如果再要删除线性表中的第6个元素,则采用类似的方法:将第7个元素往前移动一个位置。此时,线性表的长度变成了6,如图1.8(c)所示。
一般来说,设长度为n的线性表为
现要删除第i个元素,删除后得到长度为n-1的线性表为
则删除前后的两线性表中的元素满足如下关系:
在一般情况下,要删除第i(1≤i≤n)个元素时,则要从第i+1个元素开始,直到第n个元素之间共n-i个元素依次向前移动一个位置。删除结束后,线性表的长度就减小了1。
显然,在线性表采用顺序存储结构时,如果删除运算在线性表的末尾进行,即删除第n个元素,则不需要移动表中的元素;如果要删除线性表中的第1个元素,则需要移动表中所有的元素。在一般情况下,如果要删除第i(1≤i≤n)个元素,则原来第i个元素之后的所有元素都必须依次往前移动一个位置。在平均情况下,要在线性表中删除一个元素,需要移动表中一半的元素。因此,在线性表顺序存储的情况下,要删除一个元素,其效率也是很低的,特别是在线性表比较大的情况下更为突出,由于数据元素的移动而消耗较多的处理时间。
由线性表在顺序存储结构下的插入与删除运算可以看出,线性表的顺序存储结构对于小线性表或者其中元素不常变动的线性表来说是合适的,因为顺序存储的结构比较简单。但这种顺序存储的方式对于元素经常需要变动的大线性表就不太合适了,因为插入与删除的效率比较低。
1.4 栈和队列
1.4.1栈及其基本运算
1.什么是栈栈实际上也是线性表,只不过是一种特殊的线性表。在这种特殊的线性表中,其插入与删除运算都只在线性表的一端进行。即在这种线性表的结构中,一端是封闭的,不允许进行插入与删除元素;另一端是开口的,允许插入与删除元素。在顺序存储结构下,对这种类型线性表的插入与删除运算是不需要移动表中其他数据元素的。这种线性表称为栈。
栈(stack)是限定在一端进行插入与删除的线性表。
在栈中,允许插入与删除的一端称为栈顶,而不允许插入与删除的另一端称为栈底。栈顶元素总是最后被插入的元素,从而也是最先能被删除的元素;栈底元素总是最先被插入的元素,从而也是最后才能被删除的元素。即栈是按照“先进后出”(FILO-First In Last Out)或“后进先出”(LIFO—Last In First Out)的原则组织数据的,因此,栈也被称为“先进后出”表或“后进先出”表。由此可以看出,栈具有记忆作用。
通常用指针top来指示栈顶的位置,用指针bottom指向栈底。
往栈中插入一个元素称为入栈运算,从栈中删除一个元素(即删除栈顶元素)称为退栈运算。栈顶指针top动态反映了栈中元素的变化情况。
图1.9是栈的示意图。
栈这种数据结构在日常生活中也是常见的。例如,子弹夹是一种栈的结构,最后压入的子弹总是最先被弹出,而最先压入的子弹最后才能被弹出。又如,在用一端为封闭另一端为开口的容器装物品时,也是遵循“先进后出”或“后进先出”原则的。
2.栈的顺序存储及其运算
与一般的线性表一样,在程序设计语言中,用一维数组S(1:m)作为栈的顺序存储空间,其中m为栈的最大容量。通常,栈底指针指向栈空间的低地址一端(即数组的起始地址这一端)。图1.10(a)是容量为10的栈顺序存储空间,栈中已有6个元素;图1.10(b)与图1.10(c)分别为入栈与退栈后的状态。
在栈的顺序存储空间S(1:m)中,S(bottom)通常为栈底元素(在栈非空的情况下),s(top)为栈顶元素。top=0表示栈空;top=m表示栈满。
栈的基本运算有三种:入栈、退栈与读栈顶元素。下面分别介绍在顺序存储结构下栈的这三种运算。
(1)入栈运算入栈运算是指在栈顶位置插入一个新元素。这个运算有两个基本操作:首先将栈顶指针进一(即top加1),然后将新元素插入到栈顶指针指向的位置。
当栈顶指针已经指向存储空间的最后一个位置时,说明栈空间已满,不可能再进行入栈操作。这种情况称为栈“上溢”错误。
(2)退栈运算退栈运算是指取出栈顶元素并赋给一个指定的变量。这个运算有两个基本操作:首先将栈顶元素(栈顶指针指向的元素)赋给一个指定的变量,然后将栈顶指针退一(即top减1)。
当栈顶指针为0时,说明栈空,不可能进行退栈操作。这种情况称为栈“下溢”错误。
(3)读栈顶元素读栈顶元素是指将栈顶元素赋给一个指定的变量。必须注意,这个运算不删除栈项元素,只是将它的值赋给一个变量,因此,在这个运算中,栈项指针不会改变。
当栈顶指针为0时,说明栈空,读不到栈顶元素。
1.4.2队列及其基本运算
1.什么是队列在计算机系统中,如果一次只能执行一个用户程序,则在多个用户程序需要执行时,这些用户程序必须先按照到来的顺序进行排队等待。这通常是由计算机操作系统来进行管理的。
在操作系统中,用一个线性表来组织管理用户程序的排队执行,原则是:
①初始时线性表为空;
②当有用户程序来到时,将该用户程序加入到线性表的末尾进行等待;
③当计算机系统执行完当前的用户程序后,就从线性表的头部取出一个用户程序执行。
由这个例子可以看出,在这种线性表中,需要加入的元素总是插入到线性表的末尾,并且又总是从线性表的头部取出(删除)元素。这种线性表称为队列。
队列(queue)是指允许在一端进行插入、而在另~端进行删除的线性表。允许插入的一端称为队尾,通常用一个称为尾指针(rear)的指针指向队尾元素,即尾指针总是指向最后被插入的元素;允许删除的一端称为排头(也称为队头),通常也用一个排头指针(front)指向排头元素的前一个位置。显然,在队列这种数据结构中,最先插入的元素将最先能够被删除,反之,最后插入的元素将最后才能被删除。因此,队列又称为“先进先出”(FIFO——First In First Out)或“后进后出”(LILO——Last In Last Out)的线性表,它体现了“先来先服务”的原则。在队列中,队尾指针rear与排头指针front共同反映了队列中元素动态变化的情况。图1.11是具有6个元素的队列示意图。
往队列的队尾插入一个元素称为入队运算,从队列的排头删除一个元素称为退队运算。
图1.12是在队列中进行插入与删除的示意图。由图1.12可以看出,在队列的末尾插入一个元素(入队运算)只涉及队尾指针rear的变化,而要删除队列中的排头元素(退队运算)只涉及排头指针front的变化。
与栈类似,在程序设计语言中,用一维数组作为队列的顺序存储空间。
2.循环队列及其运算在实际应用中,队列的顺序存储结构一般采用循环队列的形式。
所谓循环队列,就是将队列存储空间的最后一个位置绕到第一个位置,形成逻辑上的环状空间,供队列循环使用,如图1.13所示。在循环队列结构中,当存储空间的最后一个位置已被使用而再要进行入队运算时,只要存储空间的第一个位置空闲,便可将元素加入到第一个位置,即将存储空间的第一个位置作为队尾。
在循环队列中,用队尾指针real指向队列中的队尾元素,用排头指针front指向排头元素的前一个位置,因此,从排头指针front指向的后一个位置直到队尾指针rear指向的位置之间所有的元素均为队列中的元素。
循环队列的初始状态为空,即rear=front=m,如图1.13所示。
循环队列主要有两种基本运算:入队运算与退队运算。
每进行一次入队运算,队尾指针就进一。当队尾指针rear=m+1时,则置rear=l。
每进行一次退队运算,排头指针就进一。当排头指针front=m+1时,则置front=l。
图1.14(a)是一个容量为8的循环队列存储空间,且其中已有6个元素。图1.14(b)是在图1.14(a)的循环队列中又加入了2个元素后的状态。图1.14(c)是在1.14(b)的循环队列中退出了1个元素后的状态。
由图1.14中循环队列动态变化的过程可以看出,当循环队列满时有front=rear,而当循环队列空时也有front=rear。即在循环队列中,当front=rear时,不能确定是队列满还是队列空。在实际使用循环队列时,为了能区分队列满还是队列空,通常还需增加一个标志s,s值的定义如下:
由此可以得出队列空与队列满的条件如下:
队列空的条件为s=0;
队列满的条件为s=l且front=rear。
下面具体介绍循环队列入队与退队的运算。
假设循环队列的初始状态为空,即:s=0,且front=rear=m。
(1)入队运算入队运算是指在循环队列的队尾加入一个新元素。这个运算有两个基本操作:首先将队尾指针进一(即rear=rear+1),并当rear=m+l时置rear=1;然后将新元素插入到队尾指针指向的位置。
当循环队列非空(s=1)且队尾指针等于排头指针时,说明循环队列已满,不能进行入队运算,这种情况称为“上溢”。
(2)退队运算退队运算是指在循环队列的排头位置退出一个元素并赋给指定的变量。这个运算有两个基本操作:首先将排头指针进一(即front=front+1),并当front=m+l时置front=l;然后将排头指针指向的元素赋给指定的变量。
当循环队列为空(s=0)时,不能进行退队运算,这种情况称为“下溢”。
1.5 线性链表
1.5.1线性链表的基本概念前面主要讨论了线性表的顺序存储结构以及在顺序存储结构下的运算。线性表的顺序存储结构具有简单、运算方便等优点,特别是对于小线性表或长度固定的线性表,采用顺序存储结构的优越性更为突出。
但是,线性表的顺序存储结构在某些情况下就显得不那么方便,运算效率不那么高。实际上,线性表的顺序存储结构存在以下几方面的缺点:
①在一般情况下,要在顺序存储的线性表中插入一个新元素或删除一个元素时,为了保证插入或删除后的线性表仍然为顺序存储,则在插入或删除过程中需要移动大量的数据元素。在平均情况下,为了在顺序存储的线性表中插入或删除一个元素,需要移动线性表中约一半的元素;在最坏情况下,则需要移动线性表中所有的元素。因此,对于大的线性表,特别是元素的插入或删除很频繁的情况下,采用顺序存储结构是很不方便的,插入与删除运算的效率都很低。
②当为一个线性表分配顺序存储空间后,如果出现线性表的存储空间已满,但还需要插入新的元素时,就会发生“上溢”错误。在这种情况下,如果在原线性表的存储空间后找不到与之连续的可用空间,则会导致运算的失败或中断。显然,这种情况的出现对运算是很不利的。也就是说,在顺序存储结构下,线性表的存储空间不便于扩充。
③在实际应用中,往往是同时有多个线性表共享计算机的存储空间,例如,在一个处理中,可能要用到若干个线性表(包括栈与队列)。在这种情况下,存储空间的分配将是一个难题。如果将存储空间平均分配给各线性表,则有可能造成有的线性表的空间不够用,而有的线性表的空间根本用不着或用不满,这就使得在有的线性表空间无用而处于空闲的情况下,另外一些线性表的操作由于“上溢”而无法进行。这种情况实际上是计算机的存储空间得不到充分利用。如果多个线性表共享存储空间,对每一个线性表的存储空间进行动态分配,则为了保证每一个线性表的存储空间连续且顺序分配,会导致在对某个线性表进行动态分配存储空间时,必须要移动其他线性表中的数据元素。这就是说,线性表的顺序存储结构不便于对存储空间的动态分配。
由于线性表的顺序存储结构存在以上这些缺点,因此,对于大的线性表,特别是元素变动频繁的大线性表不宜采用顺序存储结构,而是采用下面要介绍的链式存储结构。
假设数据结构中的每一个数据结点对应于一个存储单元,这种存储单元称为存储结点,简称结点。
在链式存储方式中,要求每个结点由两部分组成:一部分用于存放数据元素值,称为数据域;另一部分用于存放指针,称为指针域。其中指针用于指向该结点的前一个或后一个结点(即前件或后件)。
在链式存储结构中,存储数据结构的存储空间可以不连续,各数据结点的存储顺序与数据元素之间的逻辑关系可以不一致,而数据元素之间的逻辑关系是由指针域来确定的。
链式存储方式既可用于表示线性结构,也可用于表示非线性结构。在用链式结构表示较复杂的非线性结构时,其指针域的个数要多一些。
1.线性链表线性表的链式存储结构称为线性链表。
为了适应线性表的链式存储结构,计算机存储空间被划分为一个一个小块,每一小块占若干字节,通常称这些小块为存储结点。
为了存储线性表中的每一个元素,一方面要存储数据元素的值,另一方面要存储各数据元素之间的前后件关系。为此目的,将存储空间中的每一个存储结点分为两部分:一部分用于存储数据元素的值,称为数据域;另一部分用于存放下一个数据元素的存储序号(即存储结点的地址),即指向后件结点,称为指针域。由此可知,在线性链表中,存储空间的结构如图1.15所示。
在线性链表中,用一个专门的指针HEAD指向线性链表中第一个数据元素的结点(即存放线性表中第一个数据元素的存储结点的序号)。线性表中最后一个元素没有后件,因此,线性链表中最后一个结点的指针域为空(用NULL或0表示),表示链表终止。线性链表的逻辑结构如图1.17所示。
线性链表中存储结点的结构如图1.16所示。
下面举一个例子来说明线性链表的存储结构。
设线性表为(a1,a2,a3,a4,a5),存储空间具有10个存储结点,该线性表在存储空间中的存储情况如图1.18(a)所示。为了直观地表示该线性链表中各元素之间的前后件关系,还可以用如图1.18(b)所示的逻辑状态来表示,其中每一个结点上面的数字表示该结点的存储序号(简称结点号)。
一般来说,在线性表的链式存储结构中,各数据结点的存储序号是不连续的,并且各结点在存储空间中的位置关系与逻辑关系也不一致。在线性链表中,各数据元素之间的前后件关系是由各结点的指针域来指示的,指向线性表中第一个结点的指针HEAD称为头指针,当HEAD=NULL(或0)时称为空表。
对于线性链表,可以从头指针开始,沿各结点的指针扫描到链表中的所有结点。下面的算法是从头指针开始,依次输出各结点值。
上面讨论的线性链表又称为线性单链表。在这种链表中,每一个结点只有一个指针域,由这个指针只能找到后件结点,但不能找到前件结点。因此,在这种线性链表中,只能顺指针向链尾方向进行扫描,这对于某些问题的处理会带来不便,因为在这种链接方式下,由某一个结点出发,只能找到它的后件,而为了找出它的前件,必须从头指针开始重新寻找。
为了弥补线性单链表的这个缺点,在某些应用中,对线性链表中的每个结点设置两个指针,一个称为左指针(Llink),用以指向其前件结点;另一个称为右指针(Rlink),用以指向其后件结点。这样的线性链幕称为双向链表,其逻辑状态如图1.19所示。
2.带链的栈栈也是线性表,也可以采用链式存储结构。图1.20是栈在链式存储时的逻辑状态示意图。
在实际应用中,带链的栈可以用来收集计算机存储空间中所有空闲的存储结点,这种带链的栈称为可利用栈。由于可利用栈链接了计算机存储空间中所有的空闲结点,因此,当计算机系统或用户程序需要存储结点时,就可以从中取出栈项结点,如图1.21(b)所示;当计算机系统或用户程序释放一个存储结点(该元素从表中删除)时,则要将该结点放回到可利用栈的栈顶,如图1.21(a)所示。由此可知,计算机中的所有可利用空间都可以以结点为单位链接在可利用栈中。随着其他线性链表中结点的插入与删除,可利用栈处于动态变化之中,即可利用栈经常要进行退栈与入栈操作。
3.带链的队列与栈类似,队列也是线性表,也可以采用链式存储结构。图1.22(a)是队列在链式存储时的逻辑状态示意图。图1.22(b)是将新结点p插入队列的示意图。图1.22(c)是将排头结点p退出队列的示意图。
1.5.2线性链表的基本运算线性链表的运算主要有以下几个:
①在线性链表中包含指定元素的结点之前插入一个新元素。
②在线性链表中删除包含指定元素的结点。
③将两个线性链表按要求合并成一个线性链表。
④将一个线性链表按要求进行分解。
⑤逆转线性链表。
⑥复制线性链表。
⑦线性链表的排序。
⑧线性链表的查找。
本小节主要讨论线性链表的插入与删除。
1.在线性链表中查找指定元素在对线性链表进行插入或删除的运算中,总是首先需要找到插入或删除的位置,这就需要对线性链表进行扫描查找,在线性链表中寻找包含指定元素值的前一个结点。当找到包含指定元素的前一个结点后,就可以在该结点后插入新结点或删除该结点后的一个结点。
在非空线性链表中寻找包含指定元素值x的前一个结点p的基本方法如下:
从头指针指向的结点开始往后沿指针进行扫描,直到后面已没有结点或下一个结点的数据域为x为止。因此,由这种方法找到的结点p有两种可能:当线性链表中存在包含元素x的结点时,则找到的p为第一次遇到的包含元素x的前一个结点序号;当线性链表中不存在包含元素x的结点时,则找到的p为线性链表中的最后一个结点号。
2.线性链表的插入线性链表的插入是指在链式存储结构下的线性表中插入一个新元素。
为了要在线性链表中插入一个新元素,首先要给该元素分配一个新结点,以便用于存储该元素的值。新结点可以从可利用栈中取得。然后将存放新元素值的结点链接到线性链表中指定的位置。
假设可利用栈与线性链表如图1.23(a)所示。现在要在线性链表中包含元素x的结点之前插入一个新元素b。其插入过程如下:
(1)从可利用栈取得一个结点,设该结点号为p(即取得结点的存储序号存放在变量p中),并置结点p的数据域为插入的元素值b。经过这一步后,可利用栈的状态如图1.23(b)所示。
(2)在线性链表中寻找包含元素x的前一个结点,设该结点的存储序号为q。线性链表如图1.23(b)所示。
(3)最后将结点p插入到结点q之后。为了实现这一步,只要改变以下两个结点的指针域内容:
①使结点p指向包含元素x的结点(即结点q的后件结点)。
②使结点q的指针域内容改为指向结点p。
这一步的结果如图1.23(c)所示。此时插入就完成。
由线性链表的插入过程可以看出,由于插入的新结点取自于可利用栈,因此,只要可利用栈不空,在线性链表插入时总能取到存储插入元素的新结点,不会发生“上溢”的情况。而且,由于可利用栈是公用的,多个线性链表可以共享它,从而很方便地实现了存储空间的动态分配。另外,线性链表在插入过程中不发生数据元素移动的现象,只需改变有关结点的指针即可,从而提高了插入的效率。
3.线性链表的删除线性链表的删除是指在链式存储结构下的线性表中删除包含指定元素的结点。
为了在线性链表中删除包含指定元素的结点,首先要在线性链表中找到这个结点,然后将要删除结点放回到可利用栈。
假设可利用栈与线性链表如图1.24(a)所示。现在要在线性链表中删除包含元素x的结点,其删除过程如下:
(1)在线性链表中寻找包含元素x的前一个结点,设该结点序号为q。
(2)将结点q后的结点p从线性链表中删除,即让结点q的指针指向包含元素x的结点p的指针指向的结点。
经过上述两步后,线性链表如图1.24(b)所示。
(3)将包含元素x的结点p送回可利用栈。经过这一步后,可利用栈的状态如图1.24(c)所示。此时,线性链表的删除运算完成。
从线性链表的删除过程可以看出,在线性链表中删除一个元素后,不需要移动表的数据元素,只需改变被删除元素所在结点的前一个结点的指针域即可。另外,由于可利用栈是用于收集计算机中所有的空闲结点,因此,当从线性链表中删除一个元素后,该元素的存储结点就变为空闲,应将该空闲结点送回到可利用栈。
1.5.3循环链表及其基本运算前面所讨论的线性链表中,其插入与删除的运算虽然比较方便,但还存在一个问题,在运算过程中对于空表和对第一个结点的处理必须单独考虑,使空表与非空表的运算不统一。为了克服线性链表的这个缺点,可以采用另一种链接方式,即循环链表(Circular Linked List)的结构。
循环链表的结构与前面所讨论的线性链表相比,具有以下两个特点:
(1)在循环链表中增加了一个表头结点,其数据域为任意或者根据需要来设置,指针域指向线性表的第一个元素的结点。循环链表的头指针指向表头结点。
(2)循环链表中最后一个结点的指针域不是空,而是指向表头结点。即在循环链表中,所有结点的指针构成了一个环状链。
图1.25是循环链表的示意图。其中图1.25(a)是一个非空的循环链表,图1.25(b)是一个空的循环链表。在此,所谓的空表与非空表是针对线性表中的元素而言。
在循环链表中,只要指出表中任何一个结点的位置,就可以从它出发访问到表中其他所有的结点,而线性单链表做不到这一点。
另外,由于在循环链表中设置了一个表头结点,因此,在任何情况下,循环链表中至少有一个结点存在,从而使空表与非空表的运算统一。
循环链表的插入和删除的方法与线性单链表基本相同。但由循环链表的特点可以看出,在对循环链表进行插入和删除的过程中,实现了空表与非空表的运算统一。
1.6 树与二叉树
1.6.1树的基本概念树(tree)是一种简单的非线性结构。在树这种数据结构中,所有数据元素之间的关系具有明显的层次特性。图1.26表示了一棵一般的树。由图1.26可以看出,在用图形表示树这种数据结构时,很像自然界中的树,只不过是一棵倒长的树,因此,这种数据结构就用“树”来命名。
在树的图形表示中,总是认为在用直线连起来的两端结点中,上端结点是前件,下端结点是后件,这样,表示前后件关系的箭头就可以省略。
在现实世界中,能用树这种数据结构表示的例子有很多。例如,图1.27中的树表示了学校行政关系结构;图1.28中的树反映了一本书的层次结构。由于树具有明显的层次关系,因此,具有层次关系的数据都可以用树这种数据结构来描述。在所有的层次关系中,人们最熟悉的是血缘关系,按血缘关系可以很直观地理解树结构中各数据元素结点之间的关系,因此,在描述树结构时,也经常使用血缘关系中的一些述语。
下面介绍树这种数据结构中的一些基本特征,同时介绍有关树结构的基本术语。
在树结构中,每一个结点只有一个前件,称为父结点,没有前件的结点只有一个,称为树的根结点,简称为树的根。例如,在图1.26中,结点R是树的根结点。
在树结构中,每一个结点可以有多个后件,它们都称为该结点的子结点。没有后件的结点称为叶子结点。例如,在图1.26中,结点C、M、F、E、X、G、S、L、Z、A均为叶子结点。
在树结构中,一个结点所拥有的后件个数称为该结点的度。例如,在图1.26中,根结点R的度为4;结点T的度为3;结点K、B、N、H的度为2;结点P、Q、D、O、Y、W的度为l。叶子结点的度为0。在树中,所有结点中的最大的度称为树的度。例如,图1.26所示的树的度为4。
前面已经说过,树结构具有明显的层次关系,即树是一种层次结构。在树结构中,_般按如下原则分层:
根结点在第1层。
同一层上所有结点的所有子结点都在下一层。例如,在图1.26中,根结点R在第1层;结点K、P、Q、D在第2层;结点B、E、N、O、T在第3层;结点C、H、X、Y、S、w、z、A在第4层;结点M、F、G、L在第5层。
树的最大层次称为树的深度。例如,图1.26所示的树的深度为5。
在树中,以某结点的一个子结点为根构成的树称为该结点的一棵子树。例如,在图1.26中:结点R有4棵子树,它们分别以K、P、Q、D为根结点;结点P有1棵子树,其根结点为N;结点T有3棵子树,它们分别以W、Z、A为根结点。
在树中,叶子结点没有子树。
在计算机中,可以用树结构来表示算术表达式。
在一个算术表达式中,有运算符和运算对象。一个运算符可以有若干个运算对象。例如,取正(+)与取负(-)运算符只有一个运算对象,称为单目运算符;加(+)、减(-)、乘(*)、除(/)、乘幂(**)运算符有两个运算对象,称为双目运算符;三元函数f(x,y,z)中的f为函数运算符,它有三个运算对象,称为三目运算符。一般来说,多元函数运算符有多个运算对象,称为多目运算符。算术表达式中的一个运算对象可以是子表达式,也可以是单变量(或单变数)。例如,在表达式a*b+c中,运算符“+”有两个运算对象,其中a*b为子表达式,c为单变量;而在子表达式a*b中,运算符“*”有两个运算对象a和b,它们都是单变量。
用树来表示算术表达式的原则如下:
①表达式中的每一个运算符在树中对应一个结点,称为运算符结点。
②运算符的每一个运算对象在树中为该运算符结点的子树(在树中的顺序为从左到右)。
③运算对象中的单变量均为叶子结点。
根据以上原则,可以将表达式
a*(b+c/d)+e*h-g*f(s,t,x+y)
用如图1.29所示的树来表示。表示表达式的树通常称为表达式树。由图1.29可以看出,表示一个表达式的表达式树是不惟一的,如上述表达式可以表示成如图1.29(a)和图1.29(b)两种表达式树。
树在计算机中通常用多重链表表示。多重链表中的每个结点描述了树中对应结点的信息,而每个结点中的链域(即指针域)个数将随树中该结点的度而定,其一般结构如图2.30所示。
在表示树的多重链表中,由于树中每个结点的度一般是不同的,因此,多重链表中各结点的链域个数也就不同,这将导致对树进行处理的算法很复杂。如果用定长的结点来表示树中的每个结点,即取树的度作为每个结点的链域个数,这就可以使对树的各种处理算法大大简化。但在这种情况下,容易造成存储空间的浪费,因为有可能在很多结点中存在空链域。后面将介绍用二叉树来表示一般的树,会给处理带来方便。
1.6.2二叉树及其基本性质
1.什么是二叉树二叉树(binary tree)是一种很有用的非线性结构。二叉树不同于前面介绍的树结构,但它与树结构很相似,并且,树结构的所有术语都可以用到二叉树这种数据结构上。
二叉树具有以下两个特点:
①非空二叉树只有一个根结点;
②每一个结点最多有两棵子树,且分别称为该结点的左子树与右子树。
由以上特点可以看出,在二叉树中,每一个结点的度最大为2,即所有子树(左子树或右子树)也均为二叉树,而树结构中的每一个结点的度可以是任意的。另外,二叉树中的每一个结点的子树被明显地分为左子树与右子树。在二叉树中,一个结点可以只有左子树而没有右子树,也可以只有右子树而没有左子树。当一个结点既没有左子树也没有右子树时,该结点即是叶子结点。
图1.31(a)是一棵只有根结点的二叉树,图1.31(b)是一棵深度为4的二叉树。
2.二叉树的基本性质二叉树具有以下几个性质:
性质1在二叉树的第k层上,最多有2k-1(k≥1)个结点。
根据二叉树的特点,这个性质是显然的。
性质2深度为m的二叉树最多有2m-1个结点。
深度为m的二叉树是指二叉树共有m层。
根据性质l,只要将第1层到第m层上的最大的结点数相加,就可以得到整个二叉树中结点数的最大值,即
21-1+22-1+…+2m-1=2m-1
性质3在任意一棵二叉树中,度为0的结点(即叶子结点)总是比度为2的结点多一个。
对于这个性质说明如下:
假设二叉树中有n0个叶子结点,n1个度为1的结点,n2个度为2的结点,则二叉树中总的结点数为
n=n0+nl+n2 (1)
由于在二叉树中除了根结点外,其余每一个结点都有惟一的一个分支进入。设二叉树中所有进入分支的总数为m,则二叉树中总的结点数为
n=m+1 (2)
又由于二叉树中这m个进入分支是分别由非叶子结点射出的。其中度为l的每个结点射出1个分支,度为2的每个结点射出2个分支。因此,二叉树中所有度为1与度为2的结点射出的分支总数为n1+2n2。而在二叉树中,总的射出分支数应与总的进入分支数相等,即
m=n1+2n2 (3)
将(3)代入(2)式有
n=n1+2n2+1 (4)
最后比较(1)式和(4)式有
n0+n1+n2=n1+2n2+1
化简后得
n0=n2+1
即:在二叉树中,度为0的结点(即叶子结点)总是比度为2的结点多一个。
例如,在图1.31(b)所示的二叉树中,有3个叶子结点,有2个度为2的结点,度为0的结点比度为2的结点多一个。
性质4具有n个结点的二叉树,其深度至少为[1og2n]+1,其中[log2n]表示取log2n的整数部分。
这个性质可以由性质2直接得到。
3.满二叉树与完全二叉树满二叉树与完全二叉树是两种特殊形态的二叉树。
(1)满二叉树所谓满二叉树是指这样的一种二叉树:除最后一层外,每一层上的所有结点都有两个子结点。这就是说,在满二叉树中,每一层上的结点数都达到最大值,即在满二叉树的第k层上有2k-1个结点,且深度为m的满二叉树有2m-1个结点。
图1.32(a)、1.32(b)、1.32(c)分别是深度为2、3、4的满二叉树。
(2)完全二叉树所谓完全二叉树是指这样的二叉树:除最后一层外,每一层上的结点数均达到最大值;在最后一层上只缺少右边的若干结点。
更确切地说,如果从根结点起,对二叉树的结点自上而下、自左至右用自然数进行连续编号,则深度为m、且有n个结点的二叉树,当且仅当其每一个结点都与深度为m的满二叉树中编号从1到n的结点一一对应时,称之为完全二叉树。
图1.33(a)、1.33(b)分别是深度为3、4的完全二叉树。
对于完全二叉树来说,叶子结点只可能在层次最大的两层上出现;对于任何一个结点,若其右分支下的子孙结点的最大层次为p,则其左分支下的子孙结点的最大层次或为p,或为p+l。
由满二叉树与完全二叉树的特点可以看出,满二叉树也是完全二叉树,而完全二叉树一般不是满二叉树。
完全二叉树还具有以下两个性质:
性质5具有n个结点的完全二叉树的深度为[10g2n]+1。
性质6设完全二叉树共有n个结点。如果从根结点开始,按层序(每一层从左到右)用自然数l,2,…,n给结点进行编号,则对于编号为k(k=1,2,…,n)的结点有以下结论:
①若k=l,则该结点为根结点,它没有父结点;若k>1,则该结点的父结点编号为INT(k/2)。
②若2k≤n,则编号为k的结点的左子结点编号为2k;否则该结点无左子结点(显然也没有右子结点)。
③若2k+l≤n,则编号为k的结点的右子结点编号为2k+l;否则该结点无右子结点。
根据完全二叉树的这个性质,如果按从上到下、从左到右顺序存储完全二叉树的各结点,则很容易确定每一个结点的父结点、左子结点和右子结点的位置。
1.6.3二叉树的存储结构在计算机中,二叉树通常采用链式存储结构。
与线性链表类似,用于存储二叉树中各元素的存储结点也由两部分组成:数据域与指针域。但在二叉树中,由于每一个元素可以有两个后件(即两个子结点),因此,用于存储二叉树的存储结点的指针域有两个:一个用于指向该结点的左子结点的存储地址,称为左指针域;另一个用于指向该结点的右子结点的存储地址,称为右指针域。图1.34为二叉树存储结点的示意图。其中:L(i)为结点i的左指针域,即L(i)为结点i的左予结点的存储地址;R(i)为结点i的右指针域,即R(i)为结点i的右子结点的存储地址;V(i)为数据域。
由于二叉树的存储结构中每一个存储结点有两个指针域,因此,二叉树的链式存储结构也称为二叉链表。图1.35(a)、1.35(b)、1.35(c)分别表示了一棵二叉树、二叉链表的逻辑状态、二叉链表的物理状态。其中BT称为二叉链表的头指针,用于指向二叉树根结点(即存放二叉树根结点的存储地址)。
对于满二叉树与完全二叉树来说,根据完全二叉树的性质6,可以按层序进行顺序存储,这样,不仅节省了存储空间,又能方便地确定每一个结点的父结点与左右子结点的位置,但顺序存储结构对于一般的二叉树不适用。
1.6.4二叉树的遍历二叉树的遍历是指不重复地访问二叉树中的所有结点。
由于二又树是一种非线性结构,因此,对二叉树的遍历要比遍历线性表复杂得多。在遍历二叉树的过程中,当访问到某个结点时,再往下访问可能有两个分支,那么先访问哪一个分支呢?对于二叉树来说,需要访问根结点、左子树上的所有结点、右子树上的所有结点,在这三者中,究竟先访问哪一个?也就是说,遍历二叉树的方法实际上是要确定访问各结点的顺序,以便不重不漏地访问到二叉树中的所有结点。
在遍历二叉树的过程中,一般先遍历左子树,然后再遍历右子树。在先左后右的原则下,根据访问根结点的次序,二叉树的遍历可以分为三种:前序遍历、中序遍历、后序遍历。下面分别介绍这三种遍历的方法。
1.前序遍历(DLR)
所谓前序遍历是指在访问根结点、遍历左子树与遍历右子树这三者中,首先访问根结点,然后遍历左子树,最后遍历右子树;并且,在遍历左、右子树时,仍然先访问根结点,然后遍历左子树,最后遍历右子树。因此,前序遍历二叉树的过程是一个递归的过程。
下面是二叉树前序遍历的简单描述:
若二叉树为空,则结束返回。
否则:(1)访问根结点;
(2)前序遍历左子树;
(3)前序遍历右子树。
在此特别要注意的是,在遍历左右子树时仍然采用前序遍历的方法。如果对图1.35(a)中的二叉树进行前序遍历,则遍历的结果为F,C,A,D,B,E,G,H,P(称为该二叉树的前序序列)。
2.中序遍历(LDR)
所谓中序遍历是指在访问根结点、遍历左子树与遍历右子树这三者中,首先遍历左子树,然后访问根结点,最后遍历右子树;并且,在遍历左、右子树时,仍然先遍历左子树,然后访问根结点,最后遍历右子树。因此,中序遍历二叉树的过程也是一个递归的过程。
下面是二叉树中序遍历的简单描述:
若二叉树为空,则结束返回。
否则:(1)中序遍历左子树;
(2)访问根结点;
(3)中序遍历左子树。
在此也要特别注意的是,在遍历左右子树时仍然采用中序遍历的方法。如果对图1.35(a)中的二叉树进行中序遍历,则遍历结果为A,C,B,D,F,E,H,G,P(称为该二叉树的中序序列)。
3.后序遍历(LRD)
所谓后序遍历是指在访问根结点、遍历左子树与遍历右子树这三者中,首先遍历左子树,然后遍历右子树,最后访问根结点,.并且,在遍历左、右子树时,仍然先遍历左子树,然后遍历右子树,最后访问根结点。因此,后序遍历二叉树的过程也是一个递归的过程。
下面是二叉树后序遍历的简单描述:
若二叉树为空,则结束返回。
否则:(1)后序遍历左子树;
(2)后序遍历右子树;
(3)访问根结点。
在此也要特别注意的是,在遍历左右子树时仍然采用后序遍历的方法。如果对图1.35(a)中的二叉树进行后序遍历,则遍历结果为A,B,D,C,H,P,G,E,F(称为该二叉树的后序序列)。
1.7 查找技术查找是数据处理领域中的一个重要内容,查找的效率将直接影响到数据处理的效率。
所谓查找是指在一个给定的数据结构中查找某个指定的元素。通常,根据不同的数据结构,应采用不同的查找方法。
1.7.1顺序查找顺序查找又称顺序搜索。顺序查找一般是指在线性表中查找指定的元素,其基本方法如下:
从线性表的第一个元素开始,依次将线性表中的元素与被查元素进行比较,若相等则表示找到(即查找成功);若线性表中所有的元素都与被查元素进行了比较但都不相等,则表示线性表中没有要找的元素(即查找失败)。
在进行顺序查找过程中,如果线性表中的第一个元素就是被查找元素,则只需做一次比较就查找成功,查找效率最高;但如果被查的元素是线性表中的最后一个元素,或者被查元素根本不在线性表中,则为了查找这个元素需要与线性表中所有的元素进行比较,这是顺序查找的最坏情况。在平均情况下,利用顺序查找法在线性表中查找一个元素,大约要与线性表中一半的元素进行比较。
由此可以看出,对于大的线性表来说,顺序查找的效率是很低的。虽然顺序查找的效率不高,但在下列两种情况下也只能采用顺序查找:
(1)如果线性表为无序表(即表中元素的排列是无序的),则不管是顺序存储结构还是链式存储结构,都只能用顺序查找。
(2)即使是有序线性表,如果采用链式存储结构,也只能用顺序查找。
1.7.2二分法查找二分法查找只适用于顺序存储的有序表。在此所说的有序表是指线性表中的元素按值非递减排列(即从小到大,但允许相邻元素值相等)。
设有序线性表的长度为n,被查元素为x,则对分查找的方法如下:
将x与线性表的中间项进行比较:
若中间项的值等于x,则说明查到,查找结束;
若x小于中间项的值,则在线性表的前半部分(即中间项以前的部分)以相同的方法进行查找;
若x大于中间项的值,则在线性表的后半部分(即中间项以后的部分)以相同的方法进行查找。
这个过程一直进行到查找成功或子表长度为0(说明线性表中没有这个元素)为止。
显然,当有序线性表为顺序存储时才能采用二分查找,并且,二分查找的效率要比顺序查找高得多。可以证明,对于长度为n的有序线陛表,在最坏情况下,二分查找只需要比较10g2n次,而顺序查找需要比较n次。
1.8 排序技术排序也是数据处理的重要内容。所谓排序是指将一个无序序列整理成按值非递减顺序排列的有序序列。排序的方法有很多,根据待排序序列的规模以及对数据处理的要求,可以采用不同的排序方法。本节主要介绍一些常用的排序方法。
排序可以在各种不同的存储结构上实现。在本节所介绍的排序方法中,其排序的对象一般认为是顺序存储的线性表,在程序设计语言中就是一维数组。
1.8.1交换类排序法所谓交换类排序法是指借助数据元素之间的互相交换进行排序的一种方法。冒泡排序法与快速排序法都属于交换类的排序方法。
1.冒泡排序法冒泡排序法是一种最简单的交换类排序方法,它是通过相邻数据元素的交换逐步将线性表变成有序。
冒泡排序法的基本过程如下:
首先,从表头开始往后扫描线性表,在扫描过程中逐次比较相邻两个元素的大小。若相邻两个元素中,前面的元素大于后面的元素,则将它们互换,称之为消去了一个逆序。显然,在扫描过程中,不断地将两相邻元素中的大者往后移动,最后就将线性表中的最大者换到了表的最后,这也是线性表中最大元素应有的位置。
然后,从后到前扫描剩下的线性表,同样,在扫描过程中逐次比较相邻两个元素的大小。若相邻两个元素中,后面的元素小于前面的元素,则将它们互换,这样就又消去了一个逆序。显然,在扫描过程中,不断地将两相邻元素中的小者往前移动,最后就将剩下线性表中的最小者换到了表的最前面,这也是线性表中最小元素应有的位置。
对剩下的线性表重复上述过程,直到剩下的线性表变空为止,此时的线性表已经变为有序。
在上述排序过程中,对线性表的每一次来回扫描后,都将其中的最大者沉到了表的底部,最小者象气泡一样冒到表的前头。冒泡排序由此而得名,且冒泡排序又称下沉排序。
假设线性表的长度为n,则在最坏情况下,冒泡排序需要经过n/2遍的从前往后的扫描和n/2遍的从后往前的扫描,需要的比较次数为n(n-1)/2。但这个工作量不是必需的,一般情况下要小于这个工作量。
图1.36是冒泡排序的示意图。图中有方框的元素位置表示扫描过程中最后一次发生交换的位置。由图1.36可以看出,整个排序实际上只用了2遍从前往后的扫描和2遍从后往前的扫描就完成。
2.快速排序法在前面所讨论的冒泡排序法中,由于在扫描过程中只对相邻两个元素进行比较,因此,在互换两个相邻元素时只能消除一个逆序。如果通过两个(不是相邻的)元素的交换,能够消除线性表中的多个逆序,就会大大加快排序的速度。显然,为了通过一次交换能消除多个逆序,就不能象冒泡排序法那样对相邻两个元素进行比较,因为这只能使相邻两个元素进行交换,从而只能消除一个逆序。下面介绍的快速排序法可以实现通过一次交换而消除多个逆序。
快速排序法也是一种互换类的排序方法,但由于它比冒泡排序法的速度快,因此称之为快速排序法。
快速排序法的基本思想如下:
从线性表中选取一个元素,设为T,将线性表后面小于T的元素移到前面,而前面大于T的元素移到后面,结果就将线性表分成了两部分(称为两个子表),T插入到其分界线的位置处,这个过程称为线性表的分割。通过对线性表的一次分割,就以T为分界线,将线性表分成了前后两个子表,且前面子表中的所有元素均不大于T,而后面子表中的所有元素均不小于T。
如果对分割后的各子表再按上述原则进行分割,并且,这种分割过程可以一直做下去,直到所有子表为空为止,则此时的线性表就变成了有序表。
由此可知,快速排序法的关键是对线性表进行分割,以及对各分割出的子表再进行分割,这个过程如图1.37所示。
在对线性表或子表进行实际分割时,可以按如下步骤进行:
首先,在表的第一个、中间一个与最后一个元素中选取中项,设为P(k),并将P(k)赋给T,再将表中的第一个元素移到P(k)的位置上。
然后设置两个指针i和j分别指向表的起始与最后的位置。反复操作以下两步:
(1)将j逐渐减小,并逐次比较P(j)与T,直到发现一个P(j)<T为止,将P(j)移到P(i)的位置上。
(2)将i逐渐增大,并逐次比较P(i)与T,直到发现一个P(i)>T为止,将P(i)移到P(j)的位置上。
上述两个操作交替进行,直到指针i与j指向同一个位置(即i=j)为止,此时将T移到P(i)的位置上。
在快速排序过程中,随着对各子表不断的进行分割,划分出的子表会越来越多,但一次又只能对一个子表进行再分割处理,需要将暂时不分割的子表记忆起来,这就要用一个栈来实现。在对某个子表进行分割后,可以将分割出的后一个子表的第一个元素与最后一个元素的位置压入栈中,而继续对前一个子表进行再分割;当分割出的子表为空时,可以从栈中退出一个子表(实际上只是该子表的第一个元素与最后一个元素的位置)进行分割。这个过程直到栈空为止,此时说明所有子表为空,没有子表再需要分割,排序就完成了。
1.8.2插入类排序法冒泡排序法与快速排序法本质上都是通过数据元素的交换来逐步消除线性表中的逆序。本小节讨论另一类排序的方法,即插入类排序法。
1.简单插入排序法所谓插入排序,是指将无序序列中的各元素依次插入到已经有序的线性表中。
我们可以想像,在线性表中,只包含第1个元素的子表显然可以看成是有序表。接下来的问题是,从线性表的第2个元素开始直到最后一个元素,逐次将其中的每一个元素插入到前面已经有序的子表中。一般来说,假设线性表中前j-1个元素已经有序,现在要将线性表中第j个元素插入到前面的有序子表中,插入过程如下:
首先将第j个元素放到一个变量T中,然后从有序子表的最后一个元素(即线性表中第j-1个元素)开始,往前逐个与T进行比较,将大于T的元素均依次向后移动一个位置,直到发现一个元素不大于T为止,此时就将T(即原线性表中的第j个元素)插入到刚移出的空位置上,有序子表的长度就变为j了。
图1.38给出了插入排序的示意图。图中画有方框的元素表示刚被插入到有序子表中。
在简单插入排序法中,每一次比较后最多移掉一个逆序,因此,这种排序方法的效率与冒泡排序法相同。在最坏情况下,简单插入排序需要n(n-1)/2次比较。
2.希尔排序法希尔排序法(Shell Sort)属于插入类排序,但它对简单插入排序做了较大的改进。
希尔排序法的基本思想如下:
将整个无序序列分割成若干小的子序列分别进行插入排序。
子序列的分割方法如下:
将相隔某个增量h的元素构成一个子序列。在排序过程中,逐次减小这个增量,最后当h减到1时,进行一次插入排序,排序就完成。
增量序列一般取ht=n/2k(k=l,2,…,[log2n]),其中n为待排序序列的长度。
图1.39为希尔排序法的示意图。
在希尔排序过程中,虽然对于每一个子表采用的仍是插入排序,但是,在子表中每进行一次比较就有可能移去整个线性表中的多个逆序,从而改善了整个排序过程的性能。
希尔排序的效率与所选取的增量序列有关。如果选取上述增量序列,则在最坏情况下,希尔排序所需要的比较次数为O(n1.5)。
1.8.3选择类排序法
1.简单选择排序法选择排序法的基本思想如下:
扫描整个线性表,从中选出最小的元素,将它交换到表的最前面(这是它应有的位置);然后对剩下的子表采用同样的方法,直到子表空为止。
对于长度为n的序列,选择排序需要扫描n-1遍,每一遍扫描均从剩下的子表中选出最小的元素,然后将该最小的元素与子表中的第一个元素进行交换。图1.40是这种排序法的示意图,图中有方框的元素是刚被选出来的最小元素。
简单选择排序法在最坏情况下需要比较n(n-l)/2次。
2.堆排序法堆排序法属于选择类的排序方法。
堆的定义如下:
具有n个元素的序列(h1,h2,…,hn),当且仅当满足
(i=1,2,…,n/2)时称之为堆。本节只讨论满足前者条件的堆。
由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项。
在实际处理中,可以用一维数组H(1:n)来存储堆序列中的元素,也可以用完全二叉树来直观地表示堆的结构。例如,序列(91,85,53,36,47,30,24,12)是一个堆,它所对应的完全二叉树如图1.41所示。由图1.41可以看出,在用完全二叉树表示堆时,树中所有非叶子结点值均不小于其左、右子树的根结点值,因此,堆顶(完全二叉树的根结点)元素必为序列的n个元素中的最大项。
在具体讨论堆排序法之前,先讨论这样一个问题:在一棵具有n个结点的完全二叉树[用一维数组H(1:n)表示]中,假设结点H(m)的左右子树均为堆,现要将以H(m)为根结点的子树也调整为堆。这是调整建堆的问题。
例如,假设图1.42(a)是某完全二叉树的一棵子树。显然,在这棵子树中,根结点47的左、右子树均为堆。现在为了将整个子树调整为堆,首先将根结点47与其左、右子树的根结点值进行比较,此时由于左子树根结点91大于右子树根结点53,且它又大于根结点47,因此,根据堆的条件,应将元素47与91交换,如图1.42(b)所示。经过这一次交换后,破坏了原来左子树的堆结构,需要对左子树再进行调整,将元素85与47进行交换,调整后的结果如图1.42(c)所示。
由这个例子可以看出,在调整建堆的过程中,总是将根结点值与左、右子树的根结点值进行比较,若不满足堆的条件,则将左、右子树根结点值中的大者与根结点值进行交换。这个调整过程一直做到所有子树均为堆为止。
有了调整建堆的算法后,就可以将一个无序序列建成为堆。
假设无序序列H(1:n)以完全二叉树表示。从完全二叉树的最后一个非叶子结点(即第n/2个元素)开始,直到根结点(即第一个元素)为止,对每一个结点进行调整建堆,最后就可以得到与该序列对应的堆。
根据堆的定义,可以得到堆排序的方法如下:
(1)首先将一个无序序列建成堆。
(2)然后将堆顶元素(序列中的最大项)与堆中最后一个元素交换(最大项应该在序列的最后)。不考虑已经换到最后的那个元素,只考虑前n-1个元素构成的子序列,显然,该子序列已不是堆,但左、右子树仍为堆,可以将该子序列调整为堆。反复做第(2)步,直到剩下的子序列为空为止。
堆排序的方法对于规模较小的线性表并不适合,但对于较大规模的线性表来说是很有效的。在最坏情况下,堆排序需要比较的次数为O(nlog2n)。
习 题 1
一、选择题
1.算法的时间复杂度是指
A)执行算法程序所需要的时间
B)算法程序的长度
c)算法执行过程中所需要的基本运算次数
D)算法程序中的指令条数
2.算法的空间复杂度是指
A)算法程序的长度
B)算法程序中的指令条数
C)算法程序所占的存储空间
D)算法执行过程中所需要的存储空间
3.下列叙述中正确的是
A)线性表是线性结构 B)栈与队列是非线性结构
c)线性链表是非线性结构D)二叉树是线性结构
4.数据的存储结构是指
A)数据所占的存储空间量 B)数据的逻辑结构在计算机中的表示
C)数据在计算机中的顺序存储方式 D)存储在外存中的数据
5.下列关于队列的叙述中正确的是
A)在队列中只能插入数据 B)在队列中只能删除数据
c)队列是先进先出的线性表 D)队列是先进后出的线性表
6.下列关于栈的叙述中正确的是
A)在栈中只能插入数据 B)在栈中只能删除数据
C)栈是先进先出的线性表 D)栈是先进后出的线性表
7.设有下列二叉树:
对此二叉树中序遍历的结果为
A)ABCDEF B)DBEAFC C)ABDECF D)DEBFCA
8.在深度为5的满二叉树中,叶子结点的个数为
A)32 B)31 C)16 D)15
9.对长度为n的线性表进行顺序查找,在最坏情况下所需要的比较次数为
A)n+l B)n C)(n+1)/2 D)n/2
10.设树T的度为4,其中度为1,2,3,4的结点个数分别为4,2,1,l。则T中的叶子结点数为
A)8 B)7 C)6 D)5
二、填空题
1.在长度为n的有序线性表中进行二分查找,需要的比较次数为________。
2.设一棵完全二叉树共有700个结点,则在该二叉树中有________个叶子结点。
3.设一棵二叉树的中序遍历结果为DBEAFC,前序遍历结果为ABDECF,则后序遍历结果为________。
4.在最坏情况下,冒泡排序的时间复杂度为________。
5.在一个容量为15的循环队列中,若头指针front=6,尾指针rear=9,则该循环队列中共有________个元素。
第2章 程序设计基础
2.1 程序设计方法与风格程序设计是一门技术,需要相应的理论、技术、方法和工具来支持。就程序设计方法和技术的发展而言,主要经过了结构化程序设计和面向对象的程序设计阶段。
除了好的程序设计方法和技术之外,程序设计风格也是很重要的。因为程序设计风格会深刻地影响软件的质量和可维护性,良好的程序设计风格可以使程序结构清晰合理,使程序代码便于维护,因此,程序设计风格对保证程序的质量是很重要的。
一般来讲,程序设计风格是指编写程序时所表现出的特点、习惯和逻辑思路。程序是由人来编写的,为了测试和维护程序,往往还要阅读和跟踪程序,因此程序设计的风格总体而言应该强调简单和清晰,程序必须是可以理解的。可以认为,著名的“清晰第一,效率第二”的论点已成为当今主导的程序设计风格。
要形成良好的程序设计风格,主要应注重和考虑下述一些因素。
1.源程序文档化源程序文档化应考虑如下几点:
(1)符号名的命名:符号名的命名应具有一定的实际含义,以便于对程序功能的理解。
(2)程序注释:正确的注释能够帮助读者理解程序。注释一般分为序言性注释和功能性注释。序言性注释通常位于每个程序的开头部分,它给出程序的整体说明,主要描述内容可以包括:程序标题、程序功能说明、主要算法、接口说明、程序位置、开发简历、程序设计者、复审者、复审日期、修改日期等。功能性注释的位置一般嵌在源程序体之中,主要描述其后的语句或程序做什么。
(3)视觉组织:为使程序的结构一目了然,可以在程序中利用空格、空行、缩进等技巧使程序层次清晰。
2.数据说明的方法在编写程序时,需要注意数据说明的风格,以便使程序中的数据说明更易于理解和维护。一般应注意如下几点:
(1)数据说明的次序规范化。鉴于程序理解、阅读和维护的需要,使数据说明次序固定,可以使数据的属性容易查找,也有利于测试、排错和维护。
(2)说明语句中变量安排有序化。当一个说明语句说明多个变量时,变量按照字母顺序排序为好。
(3)使用注释来说明复杂数据的结构。
3.语句的结构程序应该简单易懂,语句构造应该简单直接,不应该为提高效率而把语句复杂化。一般应注意如下:
(1)在一行内只写一条语句;
(2)程序编写应优先考虑清晰性;
(3)除非对效率有特殊要求,程序编写要做到清晰第一,效率第二;
(4)首先要保证程序正确,然后才要求提高速度;
(5)避免使用临时变量而使程序的可读性下降;
(6)避免不必要的转移;
(7)尽可能使用库函数;
(8)避免采用复杂的条件语句;
(9)尽量减少使用“否定”条件的条件语句;
(10)数据结构要有利于程序的简化;
(11)要模块化,使模块功能尽可能单一化;
(12)利用信息隐蔽,确保每一个模块的独立性;
(13)从数据出发去构造程序;
(14)不要修补不好的程序,要重新编写;
4.输入和输出输入和输出信息是用户直接关心的,输入和输出方式和格式应尽可能方便用户的使用,因为系统能否被用户接受,往往取决于输入和输出的风格。无论是批处理的输入和输出方式,还是交互式的输入和输出方式,在设计和编程时都应该考虑如下原则:
(1)对所有的输入数据都要检验数据的合法性;
(2)检查输入项的各种重要组合的合理性;
(3)输入格式要简单,以使得输入的步骤和操作尽可能简单;
(4)输入数据时,应允许使用自由格式;
(5)应允许缺省值;
(6)输入一批数据时,最好使用输入结束标志;
(7)在以交互式输入/输出方式进行输入时,要在屏幕上使用提示符明确提示输入的请求,同时在数据输入过程中和输入结束时,应在屏幕上给出状态信息;
(8)当程序设计语言对输入格式有严格要求时,应保持输入格式与输入语句的一致性;给所有的输出加注释,并设计输出报表格式。
2.2 结构化程序设针由于软件危机的出现,人们开始研究程序设计方法,其中最受关注的是结构化程序设计方法。20世纪70年代提出了“结构化程序设计(structured programming)”的思想和方法。结构化程序设计方法引入了工程思想和结构化思想,使大型软件的开发和编程都得到了极大的改善。
2.2.1结构化程序设计的原则结构化程序设计方法的主要原则可以概括为自顶向下,逐步求精,模块化,限制使用goto语句。
1.自顶向下:程序设计时,应先考虑总体,后考虑细节;先考虑全局目标,后考虑局部目标。不要一开始就过多追求众多的细节,先从最上层总目标开始设计,逐步使问题具体化。
2.逐步求精:对复杂问题,应设计一些子目标作过渡,逐步细化。
3.模块化:一个复杂问题,.肯定是由若干稍简单的问题构成。模块化是把程序要解决的总目标分解为分目标,再进一步分解为具体的小目标,把每个小目标称为一个模块。
4.限制使用goto语句实际上,结构化程序设计方法的起源来自对GOTO语句的认识和争论。肯定的结论是,在块和进程的非正常出口处往往需要用GOTO语句,使用GOTO语句会使程序执行效率较高;在合成程序目标时,GOTO语句往往是有用的,如返回语句用GOTO。否定的结论是,GOTO语句是有害的,是造成程序混乱的祸根,程序的质量与GOTO语句的数量成反比,应该在所有高级程序设计语言中取消GOTO语句。取消GOTO语句后,程序易理解、易排错、易维护,程序容易进行正确性证明。作为争论的结论,1974年Knuth发表了令人信服的总结,并证实了:
(1)滥用GOTO语句确实有害,应尽量避免;
(2)完全避免使用GOTO语句也并非是个明智的方法,有些地方使用GOTO语句,会使程序流程更清楚、效率更高;
(3)争论的焦点不应该放在是否取消GOTO语句,而应该放在用什么样的程序结构上。
其中最关键的是,肯定以提高程序清晰性为目标的结构化方法。
2.2.2结构化程序设计的基本结构和特点结构化程序设计方法是程序设计的先进方法和工具。采用结构化程序设计方法编写程序,可使程序结构良好、易读、易理解、易维护。1966年,Boehm和Jacopini证明了程序设计语言仅仅使用顺序、选择和重复三种基本控制结构就足以表达出各种其他形式结构的程序设计方法。
1.顺序结构:顺序结构是一种简单的程序设计,它是最基本、最常用的结构,如图2.1所示。顺序结构是顺序执行结构,所谓顺序执行,就是按照程序语句行的自然顺序,一条语句一条语句地执行程序。
2.选择结构:选择结构又称为分支结构,它包括简单选择和多分支选择结构,这种结构可以根据设定的条件,判断应该选择哪一条分支来执行相应的语句序列。图2.2列出了包含2个分支的简单选择结构。
3.重复结构:重复结构又称为循环结构,它根据给定的条件,判断是否需要重复执行某一相同的或类似的程序段,利用重复结构可简化大量的程序行。在程序设计语言中,重复结构对应两类循环语句,对先判断后执行循环体的称为当型循环结构,如图2.3所示。对先执行循环体后判断的称为直到型循环结构,如图2.4所示。
总之,遵循结构化程序的设计原则,按结构化程序设计方法设计出的程序具有明显的优点,其一,程序易于理解、使用和维护。程序员采用结构化编程方法,便于控制、降低程序的复杂性,因此容易编写程序。便于验证程序的正确性,结构化程序清晰易读,可理解性好,程序员能够进行逐步求精、程序证明和测试,以确保程序的正确性,程序容易阅读并被人理解,便于用户使用和维护。其二,提高了编程工作的效率,降低了软件开发成本。由于结构化编程方法能够把错误控制到最低限度,因此能够减少调试和查错时间。结构化程序是由一些为数不多的基本结构模块组成,这些模块甚至可以由机器自动生成,从而极大地减轻了编程工作量。
2.2.3结构化程序设计原则和方法的应用基于对结构化程序设计原则、方法以及结构化程序基本构成结构的掌握和了解,在结构化程序设计的具体实施中,要注意把握如下要素:
1.使用程序设计语言中的顺序、选择、循环等有限的控制结构表示程序的控制逻辑;
2.选用的控制结构只准许有一个入口和一个出口;
3.程序语句组成容易识别的块,每块只有一个入口和一个出口:
4.复杂结构应该用嵌套的基本控制结构进行组合嵌套来实现;
5.语言中所没有的控制结构,应该采用前后一致的方法来模拟;
6.严格控制GOTO语句的使用。其意思是指:
(1)用一个非结构化的程序设计语言去实现一个结构化的构造;
(2)若不使用GOTO语句会使功能模糊:
(3)在某种可以改善而不是损害程序可读性的情况下。
2.3面向对象的程序设计
2.3.1关于面向对象方法今天,面向对象(obiect oriented)方法已经发展成为主流的软件开发方法。面向对象方法的形成同结构化方法一样,起源于实现语言,首先对面向对象的程序设计语言开展研究,随之逐渐形成面向对象分析和设计方法。面向对象方法和技术历经30多年的研究和发展,已经越来越成熟和完善,应用也越来越深入和广泛。
面向对象的软件开发方法在20世纪60年代后期首次提出,以60年代末挪威奥斯陆大学和挪威计算中心共同研制的SIMULA语言为标志,面向对象方法的基本要点首次在SIMULA语言中得到了表达和实现。后来一些著名的面向对象语言(如Smalltalk、C++、Java、Eiffel)的设计者都曾从SIMULA得到启发。随着80年代美国加州的Xerox研究中心推出Smalltalk语言和环境,使面向对象程序设计方法得到比较完善的实现。Smalltalk-80等一系列描述能力较强、执行效率较高的面向对象编程语言的出现,标志着面向对象的方法与技术开始走向实用。
面向对象方法的本质,就是主张从客观世界固有的事物出发来构造系统,提倡用人类在现实生活中常用的思维方法来认识、理解和描述客观事物,强调最终建立的系统能够映射问题域,也就是说,系统中的对象以及对象之间的关系能够如实地反映问题域中固有事物及其关系。
面向对象方法之所以日益受到人们的重视和应用,成为流行的软件开发方法,是源于面向对象方法的以下主要优点。
1.与人类习惯的思维方法一致传统的程序设计方法是面向过程的,其核心方法是以算法为核心,把数据和过程作为相互独立的部分,数据代表问题空间中的客体,程序则用于处理这些数据,在计算机内部数据和程序是分开存放的,这样的做法往往会发生使用错误的数据调用正确的程序模块的情况。其原因是,传统的程序设计方法忽略了数据和操作之间的内在联系,用这种方法设计出来的软件系统其解空间与问题空间不一致,使人感到难于理解。实际上,用计算机解决的问题都是现实世界中的问题,这些问题无非由一些相互间存在一定联系的事物所组成,每个具体的事物都具有行为和属性两方面的特征。因此,把描述事物静态属性的数据结构和表示事物动态行为的操作放在一起构成一个整体,才能完整、自然地表示客观世界中的实体。
面向对象方法和技术以对象为核心。对象是由数据和容许的操作组成的封装体,与客观实体有直接的对应关系。对象之间通过传递消息互相联系,以模拟现实世界中不同事物彼此之间的联系。
面向对象的设计方法与传统的面向过程的方法有本质不同,这种方法的基本原理是,使用现实世界的概念抽象地思考问题从而自然地解决问题。它强调模拟现实世界中的概念而不强调算法,它鼓励开发者在软件开发的绝大部分过程中都用应用领域的概念去思考。
2.稳定性好面向对象方法基于构造问题领域的对象模型,以对象为中心构造软件系统。它的基本作法是用对象模拟问题领域中的实体,以对象间的联系刻画实体间的联系。因为面向对象的软件系统的结构是根据问题领域的模型建立起来的,而不是基于对系统应完成的功能的分解,所以,当对系统的功能需求变化时并不会引起软件结构的整体变化,往往仅需要作一些局部性的修改。由于现实世界中的实体是相对稳定的,因此,以对象为中心构造的软件系统也是比较稳定的。而传统的软件开发方法以算法为核心,开发过程基于功能分析和功能分解。用传统方法所建立起来的软件系统的结构紧密地依赖于系统所要完成的功能,当功能需求发生变化时将引起软件结构的整体修改。事实上,用户需求变化大部分是针对功能的,因此,这样的软件系统是不稳定的。
3.可重用性好软件重用是指在不同的软件开发过程中重复使用相同或相似软件元素的过程。重用是提高软件生产率的最主要的方法。
传统的软件重用技术是利用标准函数库,也就是试图用标准函数库中的函数作为“预制件”来建造新的软件系统。但是,标准函数缺乏必要的“柔性”,不能适应不同应用场合的不同需要,并不是理想的可重用的软件成分。实际的库函数往往仅提供最基本、最常用的功能,在开发一个新的软件系统时,通常多数函数是开发者自己编写的,甚至绝大多数函数都是新编的。
使用传统方法学开发软件时,人们强调的是功能抽象,认为具有功能内聚性的模块是理想的模块,也就是说,如果一个模块完成一个且只完成一个相对独立的子功能,那么这个模块就是理想的可重用模块,而且这样的模块也更容易维护。基于这种认识,通常尽量把标准函数库中的函数做成功能内聚的。但是,事实上具有功能内聚性的模块并不是自含的和独立的,相反,它必须在数据上运行。如果要重用这样的模块,则相应的数据也必须重用。如果新产品中的数据与最初产品中的数据不同,则要么修改数据要么修改这个模块。
事实上,离开了操作数据便无法处理,而脱离了数据的操作也是毫无意义的,我们应该对数据和操作同样重视。在面向对象方法中所使用的对象,其数据和操作是作为平等伙伴出现的。因此,对象具有很强的自含性,此外,对象所固有的封装性,使得对象的内部实现与外界隔离,具有较强的独立性。由此可见,对象提供了比较理想的模块化机制和比较理想的可重用的软件成分。
面向对象的软件开发技术在利用可重用的软件成分构造新的软件系统时,有很大的灵活性。有两种方法可以重复使用一个对象类:一种方法是创建该类的实例,从而直接使用它;另一种方法是从它派生出一个满足当前需要的新类。继承性机制使得子类不仅可以重用其父类的数据结构和程序代码,而且可以在父类代码的基础上方便地修改和扩充,这种修改并不影响对原有类的使用。可见,面向对象的软件开发技术所实现的可重用性是自然的和准确的。
4.易于开发大型软件产品当开发大型软件产品时,组织开发人员的方法不恰当往往是出现问题的主要原因。用面向对象范型开发软件时,可以把一个大型产品看作是一系列本质上相互独立的小产品来处理,这就不仅降低了开发的技术难度,而且也使得对开发工作的管理变得容易。这就是为什么对于大型软件产品来说,面向对象范型优于结构化范型的原因之一。许多软件开发公司的经验都表明,当把面向对象技术用于大型软件开发时,软件成本明显地降低了,软件的整体质量也提高了。
5.可维护性好用传统的开发方法和面向过程的方法开发出来的软件很难维护,是长期困扰人们的一个严重问题,是软件危机的突出表现。
由于下述因素的存在,使得用面向对象的方法开发的软件可维护性好。
(1)用面向对象的方法开发的软件稳定性比较好。
如前所述,当对软件的功能或性能的要求发生变化时,通常不会引起软件的整体变化,往往只需对局部作一些修改。由于对软件的改动较小且限于局部,自然比较容易实现。
(2)用面向对象的方法开发的软件比较容易修改。
在面向对象方法中,核心是类(对象),它具有理想的模块机制,独立性好,修改一个类通常很少会牵扯到其他类。如果仅修改一个类的内部实现部分(私有数据成员或成员函数的算法),而不修改该类的对外接口,则可以完全不影响软件的其他部分。
面向对象技术特有的继承机制,使得对所开发的软件的修改和扩充比较容易实现,通常只需从已有类派生出一些新类,无需修改软件原有成分。
面向对象技术的多态性机制,使得当扩充软件功能时对原有代码的修改进一步减少,需要增加的新代码也比较少。
(3)用面向对象的方法开发的软件比较容易理解。
在维护已有软件的时候,首先需要对原有软件与此次修改有关的部分有深入理解,才能正确地完成维护工作。传统软件之所以难于维护,在很大程度上是因为修改所涉及的部分分散在软件各个地方,需要了解的面很广,内容很多,而且传统软件的解空间与问题空间的结构很不一致,更增加了理解原有软件的难度和工作量。
面向对象的技术符合人们习惯的思维方式,用这种方法所建立的软件系统的结构与问题空间的结构基本一致。因此,面向对象的软件系统比较容易理解。
对面向对象软件系统进行修改和扩充,通常是通过在原有类的基础上派生出一些新类来实现。由于对象类有很强的独立性,当派生新类的时候通常不需要详细了解基类中操作的实现算法。因此,了解原有系统的工作量可以大幅度降低。
(4)易于测试和调试。
为了保证软件质量,对软件进行维护之后必须进行必要的测试,以确保要求修改或扩充的功能已正确地实现了,而且没有影响到软件未修改的部分。如果测试过程中发现了错误,还必须通过调试改正过来。显然,软件是否易于测试和调试,是影响软件可维护性的一个重要因素。
对用面向对象的方法开发的软件进行维护,往往是通过从已有类派生出一些新类来实现。因此,维护后的测试和调试工作也主要围绕这些新派生出来的类进行。类是独立性很强的模块,向类的实例发消息即可运行它,观察它是否能正确地完成相应的工作,因此对类的测试通常比较容易实现。
2.3.2面向对象方法的基本概念关于面向对象方法,对其概念有许多不同的看法和定义,但是都涵盖对象及对象属性与方法、类、继承、多态性几个基本要素。下面分别介绍面向对象方法中这几个重要的基本概念,这些概念是理解和使用面向对象方法的基础和关键。
1.对象(object)
对象是面向对象方法中最基本的概念。对象可以用来表示客观世界中的任何实体,也就是说,应用领域中有意义的、与所要解决的问题有关系的任何事物都可以作为对象,它既可以是具体的物理实体的抽象,也可以是人为的概念,或者是任何有明确边界和意义的东西。例如,一个人、一家公司、一个窗口、贷款和借款等,都可以作为一个对象。总之,对象是对问题域中某个实体的抽象,设立某个对象就反映了软件系统保存有关它的信息并具有与它进行交互的能力。
面向对象的程序设计方法中涉及的对象是系统中用来描述客观事物的一个实体,是构成系统的一个基本单位,它由一组表示其静态特征的属性和它可执行的一组操作组成。
例如,一辆汽车是一个对象,它包含了汽车的属性(如颜色、型号、载重量等)及其操作(如启动、刹车等)。一个窗口是一个对象,它包含了窗口的属性(如大小、颜色、位置等)及其操作(如打开、关闭等)。
客观世界中的实体通常都既具有静态的属性,又具有动态的行为,因此,面向对象方法学中的对象是由描述该对象属性的数据以及可以对这些数据施加的所有操作封装在一起构成的统一体。对象可以做的操作表示它的动态行为,在面向对象分析和面向对象设计中,通常把对象的操作也称为方法或服务。
属性即对象所包含的信息,它在设计对象时确定,一般只能通过执行对象的操作来改变。如对象Person的属性有姓名、年龄、体重等。不同对象的同一属性可以具有相同或不同的属性值。如张三的年龄为19,李四的年龄为20。张三、李四是两个不同的对象,他们共同的属性“年龄”的值不同。要注意的是,属性值应该指的是纯粹的数据值,而不能指对象。
操作描述了对象执行的功能,若通过消息传递,还可以为其他对象使用。操作的过程对外是封闭的,即用户只能看到这一操作实施后的结果。这相当于事先已经设计好的各种过程,只需要调用就可以了,用户不必去关心这一过程是如何编写的。事实上,这个过程已经封装在对象中,用户也看不到。对象的这一特性,即是对象的封装性。
对象有如下一些基本特点:
(1)标识惟一性。指对象是可区分的,并且由对象的内在本质来区分,而不是通过描述来区分。
(2)分类性。指可以将具有相同属性和操作的对象抽象成类。
(3)多态性。指同一个操作可以是不同对象的行为。
(4)封装性。从外面看只能看到对象的外部特性,即只需知道数据的取值范围和可以对该数据施加的操作,根本无需知道数据的具体结构以及实现操作的算法。对象的内部,即处理能力的实行和内部状态,对外是不可见的。从外面不能直接使用对象的处理能力,也不能直接修改其内部状态,对象的内部状态只能由其自身改变。
(5)模块独立性好。对象是面向对象的软件的基本模块,它是由数据及可以对这些数据施加的操作所组成的统一体,而且对象是以数据为中心的,操作围绕对其数据所需做的处理来设置,没有无关的操作。从模块的独立性考虑,对象内部各种元素彼此结合得很紧密,内聚性强。
2.类(Class)和实例(Instance)
将属性、操作相似的对象归为类,也就是说,类是具有共同属性、共同方法的对象的集合。所以,类是对象的抽象,它描述了属于该对象类型的所有对象的性质,而一个对象则是其对应类的一个实例。
要注意的是,当使用“对象”这个术语时,既可以指一个具体的对象,也可以泛指一般的对象,但是,当使用“实例”这个术语时,必然是指一个具体的对象。
例如:Integer是一个整数类,它描述了所有整数的性质。因此任何整数都是整数类的对象,而一个具体的整数“123”是类Integer的一个实例。
由类的定义可知,类是关于对象性质的描述,它同对象一样,包括一组数据属性和在数据上的一组合法操作。例如,一个面向对象的图形程序在屏幕左下角显示一个半径3cm的红颜色的圆,在屏幕中部显示一个半径4cm的绿颜色的圆,在屏幕右上角显示一个半径lcm的黄颜色的圆。这三个圆心位置、半径大小和颜色均不相同的圆,是三个不同的对象。但是,它们都有相同的属性(圆心坐标、半径、颜色)和相同的操作(显示自己、放大缩小半径、在屏幕上移动位置,等等)。因此,它们是同一类事物,可以用“Circle类”来定义。
3.消息(Message)
面向对象的世界是通过对象与对象间彼此的相互合作来推动的,对象间的这种相互合作需要一个机制协助进行,这样的机制称为“消息”。消息是一个实例与另一个实例之间传递的信息,它请求对象执行某一处理或回答某一要求的信息,它统一了数据流和控制流。消息的使用类似于函数调用,消息中指定了某一个实例,一个操作名和一个参数表(可空)。接收消息的实例执行消息中指定的操作,并将形式参数与参数表中相应的值结合起来。消息传递过程中,由发送消息的对象(发送对象)的触发操作产生输出结果,作为消息传送至接受消息的对象(接受对象),引发接受消息的对象一系列的操作。所传送的消息实质上是接受对象所具有的操作/方法名称,有时还包括相应参数,图2.5表示了消息传递的概念。
消息中只包含传递者的要求,它告诉接受者需要做哪些处理,但并不指示接受者应该怎样完成这些处理。消息完全由接受者解释,接受者独立决定采用什么方式完成所需的处理,发送者对接受者不起任何控制作用。一个对象能够接受不同形式、不同内容的多个消息;相同形式的消息可以送往不同的对象,不同的对象对于形式相同的消息可以有不同的解释,能够做出不同的反映。一个对象可以同时往多个对象传递信息,两个对象也可以同时向某个对象传递消息。
例如,一个汽车对象具有“行驶”这项操作,那么要让汽车以时速50公里行驶的话,需传递给汽车对象“行驶”及“时速50公里”的消息。
通常,一个消息由下述三部分组成:
①接收消息的对象的名称;
②消息标识符(也称为消息名);
③零个或多个参数。
例如,MyCircle是一个半径4cm、圆心位于(100,200)的Circle类的对象,也就是Circle类的一个实例,当要求它以绿颜色在屏幕上显示自己时,在C++语言中应该向它发下列消息:
MyCircle.Show(GREEN);
其中,MyCircle是接收消息的对象的名字,Show是消息名,Green是消息的参数。
4.继承(Inheritance)
继承是面向对象的方法的一个主要特征。继承是使用已有的类定义作为基础建立新类的定义技术。已有的类可当作基类来引用,则新类相应地可当作派生类来引用。
广义地说,继承是指能够直接获得已有的性质和特征,而不必重复定义它们。
面向对象软件技术的许多强有力的功能和突出的优点,都来源于把类组成一个层次结构的系统:一个类的上层可以有父类,下层可以有子类。这种层次结构系统的一个重要性质是继承性,一个类直接继承其父类的描述(数据和操作)或特性,子类自动地共享基类中定义的数据和方法。
为了更深入、具体地理解继承性的含义,图2.6示出了实现继承机制的原理。
图中以A、B两个类为例,其中类B是从类A派生出来的子类,它除了具有自己定义的特性(数据和操作)之外,还从父类A继承特性。当创建类A的实例al的时候,a1以类A为样板建立实例变量。
当创建类B的实例b1的时候,b1既要以类B为样板建立实例变量,又要以类A为样板建立实例变量,bl所能执行的操作既有类B中定义的方法,又有类A中定义的方法,这就是继承。
继承具有传递性,如果类C继承类B,类B继承类A,则类C继承类A。因此,一个类实际上继承了它上层的全部基类的特性,也就是说,属于某类的对象除了具有该类所定义的特性外,还具有该类上层全部基类定义的特性。
继承分为单继承与多重继承。单继承是指,一个类只允许有一个父类,即类等级为树形结构。多重继承是指,一个类允许有多个父类。多重继承的类可以组合多个父类的性质构成所需要的性质。因此,功能更强,使用更方便;但是,使用多重继承时要注意避免二义性。继承性的优点是,相似的对象可以共享程序代码和数据结构,从而大大减少了程序中的冗余信息,提高软件的可重用性,便于软件修改维护。另外,继承性使得用户在开发新的应用系统时不必完全从零开始,可以继承原有的相似系统的功能或者从类库中选取需要的类,再派生出新的类以实现所需要的功能。
5.多态性(Polymorphism)
对象根据所接受的消息而做出动作,同样的消息被不同的对象接受时可导致完全不同的行动,该现象称为多态性。在面向对象的软件技术中,多态性是指子类对象可以像父类对象那样使用,同样的消息既可以发送给父类对象也可以发送给子类对象。
例如,在两个类Male(男性)和Female(女性)都有一项属性为Friend。一个人的朋友必须属于类Male或Female,这是一个多态性的情况。因为,Friend指向两个类之一的实例。如果Tom的朋友或者是Mary或者是John,类Male就不知道Friend应该与哪个类关联。这里参照量Friend必须是多态的,多态意味着可以关联不同的实例,而实例可以属于不同的类。
多态性机制不仅增加了面向对象软件系统的灵活性,进一步减少了信息冗余,而且显著地提高了软件的可重用性和可扩充性。当扩充系统功能增加新的实体类型时,只需派生出与新实体类相应的新的子类,完全无需修改原有的程序代码,甚至不需要重新编译原有的程序。利用多态性,用户能够发送一般形式的消息,而将所有的实现细节都留给接受消息的对象。
习 题 2
一、选择题
1.结构化程序设计主要强调的是
A)程序的规模 B)程序的易读性
c)程序的执行效率 D)程序的可移植性
2.对建立良好的程序设计风格,下面描述正确的是
A)程序应简单、清晰、可读性好
B)符号名的命名只要符合语法
c)充分考虑程序的执行效率
D)程序的注释可有可无
3.在面向对象方法中,一个对象请求另一对象为其服务的方式是通过发送
A)调用语句 B)命令 c)口令 D)消息
4.信息隐蔽的概念与下述哪一种概念直接相关?
A)软件结构定义 B)模块独立性 C)模块类型划分 D)模块耦合度
5.下面对对象概念描述错误的是
A)任何对象都必须有继承性
B)对象是属性和方法的封装体
c)对象间的通讯靠消息传递
D)操作是对象的动态属性二、填空题
1.结构化程序设计的三种基本逻辑结构为顺序、选择和________。
2.源程序文档化要求程序应加注释。注释一般分为序言性注释和________。
3.在面向对象方法中,信息隐蔽是通过对象的________性来实现的。
4.类是一个支持集成的抽象数据类型,而对象是类的________。
5.在面向对象方法中,类之间共享属性和操作的机制称为________。
第3章 软件工程基础
3.1 软件工程基本概念
3.1.1软件定义与软件特点计算机软件(software)是计算机系统中与硬件相互依存的另一部分,是包括程序、数据及相关文档的完整集合。其中,程序是软件开发人员根据用户需求开发的、用程序设计语言描述的、适合计算机执行的指令(语句)序列。数据是使程序能正常操纵信息的数据结构。文档是与程序开发、维护和使用有关的图文资料。可见软件由两部分组成:一是机器可执行的程序和数据;二是机器不可执行的,与软件开发、运行、维护、使用等有关的文档。
国标(GB)中对计算机软件的定义为:与计算机系统的操作有关的计算机程序、规程、规则,以及可能有的文件、文档及数据。
软件在开发、生产、维护和使用等方面与计算机硬件相比存在明显的差异。深入理解软件的定义需要了解软件的特点:
①软件是一种逻辑实体,而不是物理实体,具有抽象性。软件的这个特点使它与其他工程对象有着明显的差异。人们可以把它记录在纸上或存储介质上,但却无法看到软件本身的形态,必须通过观察、分析、思考、判断,才能了解它的功能、性能等特性。
②软件的生产与硬件不同,它没有明显的制作过程。一旦研制开发成功,可以大量拷贝同一内容的副本。所以对软件的质量控制,必须着重在软件开发方面下功夫。
③软件在运行、使用期间不存在磨损、老化问题。软件虽然在生存周期后期不会因为磨损而老化,但为了适应硬件、环境以及需求的变化要进行修改,而这些修改又会不可避免的引入错误,导致软件失效率升高,从而使得软件退化。
④软件的开发、运行对计算机系统具有依赖性,受计算机系统的限制,这导致了软件移植的问题。
⑤软件复杂性高,成本昂贵。软件是人类有史以来生产的复杂度最高的工业产品。软件涉及人类社会的各行各业、方方面面,软件开发常常涉及其他领域的专门知识。软件开发需要投入大量、高强度的脑力劳动,成本高,风险大。
⑥软件开发涉及诸多的社会因素。许多软件的开发和运行涉及软件用户的机构设置,体制问题以及管理方式等,甚至涉及到人们的观念和心理,软件知识产权及法律等问题。
软件根据应用目标的不同,是多种多样的。软件按功能可以分为:应用软件、系统软件、支撑软件(或工具软件)。应用软件是为解决特定领域的应用而开发的软件。例如,事务处理软件,工程与科学计算软件,实时处理软件,嵌入式软件,人工智能软件等应用性质不同的各种软件。系统软件是计算机管理自身资源,提高计算机使用效率并为计算机用户提供各种服务的软件。如操作系统,编译程序,汇编程序,网络软件,数据库管理系统等。支撑软件是介于系统软件和应用软件之间,协助用户开发软件的工具性软件,包括辅助和支持开发和维护应用软件的工具软件,如需求分析工具软件,设计工具软件,编码工具软件,测试工具软件,维护工具软件等,也包括辅助管理人员控制开发进程和项目管理的工具软件,如,计划进度管理工具软件,过程控制工具软件,质量管理及配置管理工具软件等。
3.1.2软件危机与软件工程软件工程概念的出现源自软件危机。
20世纪60年代末以后,“软件危机”这个词频繁出现。所谓软件危机是泛指在计算机软件的开发和维护过程中所遇到的一系列严重问题。实际上,几乎所有的软件都不同程度地存在这些问题。
随着计算机技术的发展和应用领域的扩大,计算机硬件性能/价格比和质量稳步提高,软件规模越来越大,复杂程度不断增加,软件成本逐年上升,质量没有可靠的保证,软件己成为计算机科学发展的“瓶颈”。
具体地说,在软件开发和维护过程中,软件危机主要表现在:
①软件需求的增长得不到满足。用户对系统不满意的情况经常发生。
②软件开发成本和进度无法控制。开发成本超出预算,开发周期大大超过规定日期的情况经常发生。
③软件质量难以保证。
④软件不可维护或维护程度非常低。
⑤软件的成本不断提高。
⑥软件开发生产率的提高赶不上硬件的发展和应用需求的增长。
总之,可以将软件危机归结为成本、质量、生产率等问题。
分析带来软件危机的原因,宏观方面是由于软件日益深入社会生活的各个层面,对软件需求的增长速度大大超过了技术进步所能带来的软件生产率的提高。而就每一项具体的工程任务来看,许多困难来源于软件工程所面临的任务和其他工程之间的差异以及软件和其他工业产品的不同。
在软件开发和维护过程中,之所以存在这些严重的问题,一方面与软件本身的特点有关,例如,在软件运行前,软件开发过程的进展难衡量,质量难以评价,因此管理和控制软件开发过程相当困难;在软件运行过程中,软件维护意味着改正或修改原来的设计;另外,软件的显著特点是规模庞大,复杂度超线性增长,在开发大型软件时,要保证高质量,极端复杂困难,不仅涉及技术问题(如分析方法、设计方法、版本控制),更重要的是必须有严格而科学的管理。另一方面与软件开发和维护方法不正确有关,这是主要原因。
为了消除软件危机,通过认真研究解决软件危机的方法,认识到软件工程是使计算机软件走向工程科学的途径,逐步形成了软件工程的概念,开辟了工程学的新兴领域——软件工程学。软件工程就是试图用工程、科学和数学的原理与方法研制、维护计算机软件的有关技术及管理方法。
关于软件工程的定义,国标(GB)中指出,软件工程是应用于计算机软件的定义、开发和维护的一整套方法、工具、文档、实践标准和工序。
1968年在北大西洋公约组织会议(NATO会议)上,讨论摆脱软件危机的办法,软件工程(software engineering)作为一个概念首次被提出,这在软件技术发展史上是一件大事。其后的几十年里,各种有关软件工程的技术、思想、方法和概念不断地被提出,软件工程逐步发展成为一门独立的科学。在会议上,德国人Fritz Bauer认为:“软件工程是建立并使用完善的工程化原则,以较经济的手段获得能在实际机器上有效运行的可靠软件的一系列方法”。
1993年,IEEE(Institute of Electrical&Electronic Engineers,电气和电子工程师学会)给出了一个更加综合的定义:“将系统化的、规范的、可度量的方法应用于软件的开发,运行和维护的过程,即将工程化应用于软件中”。
这些主要思想都是强调在软件开发过程中需要应用工程化原则。
软件工程包括3个要素,即方法、工具和过程。方法是完成软件工程项目的技术手段;工具支持软件的开发、管理、文档生成;过程支持软件开发的各个环节的控制、管理。
软件工程的进步是近几十年软件产业迅速发展的重要原动力。从根本上来说,其目的是研究软件的开发技术,软件工程的名称意味着用工业化的开发方法来替代小作坊式的开发模式。但是,几十年的软件开发和软件发展的实践证明,软件开发是既不同于其他工业工程,也不同于科学研究。软件不是自然界的有形物体,它作为人类智慧的产物有其本身的特点,所以软件工程的方法、概念、目标等都在发展,有的与最初的想法有了一定的差距。但是认识和学习过去和现在的发展演变,真正掌握软件开发技术的成就,并为进一步发展软件开发技术,以适应时代对软件的更高期望是有极大意义的。
软件工程的核心思想是把软件产品(就像其他工业产品一样)看作是一个工程产品来处理。把需求计划、可行性研究、工程审核、质量监督等工程化的概念引入到软件生产当中,以期达到工程项目的三个基本要素:进度、经费和质量的目标。同时,软件工程也注重研究不同于其他工业产品生产的一些独特特性,并针对软件的特点提出了许多有别于一般工业工程技术的一些技术方法。代表性的有结构化的方法、面向对象方法和软件开发模型及软件开发过程等。
特别地,从经济学的意义上来说,考虑到软件庞大的维护费用远比软件开发费用要高,因而开发软件不能只考虑开发期间的费用,而且应考虑软件生命周期内的全部费用。因此,软件生命周期的概念就变得特别重要。在考虑软件费用时,不仅仅要降低开发成本,更要降低整个软件生命周期的总成本。
3.1.3软件工程过程与软件生命周期
1.软件工程过程(Software Engineering Process)
ISO 9000定义:软件工程过程是把输入转化为输出的一组彼此相关的资源和活动。
定义支持了软件工程过程的两方面内涵。
其一,软件工程过程是指为获得软件产品,在软件工具支持下由软件工程师完成的一系列软件工程活动。基于这个方面,软件工程过程通常包含4种基本活动:
①P(Plan)——软件规格说明。规定软件的功能及其运行时的限制。
②D(Do)——软件开发。产生满足规格说明的软件。
③C(Check)——软件确认。确认软件能够满足客户提出的要求。
④A(Action)——软件演进。为满足客户的变更要求,软件必须在使用的过程中演进。
事实上,软件工程过程是一个软件开发机构针对某类软件产品为自己规定的工作步骤,它应当是科学的、合理的,否则必将影响软件产品的质量。
通常把用户的要求转变成软件产品的过程也叫做软件开发过程。此过程包括对用户的要求进行分析,解释成软件需求,把需求变换成设计,把设计用代码来实现并进行代码测试,有些软件还需要进行代码安装和交付运行。
其二,从软件开发的观点看,它就是使用适当的资源(包括人员、硬软件工具、时间等),为开发软件进行的一组开发活动,在过程结束时将输入(用户要求)转化为输出(软件产品)。
所以,软件工程的过程是将软件工程的方法和工具综合起来,以达到合理、及时地进行计算机软件开发的目的。软件工程过程应确定方法使用的顺序、要求交付的文档资料、为保证质量和适应变化所需要的管理、软件开发各个阶段完成的任务。
2.软件生命周期(Software Life Cycle)
通常,将软件产品从提出、实现、使用维护到停止使用退役的过程称为软件生命周期。也就是说,软件产品从考虑其概念开始,到该软件产品不能使用为止的整个时期都属于软件生命周期。一般包括可行性研究与需求分析、设计、实现、测试、交付使用以及维护等活动,如图3.1所示。这些活动可以有重复,执行时也可以有迭代。
还可以将软件生命周期分为如图3.1所示的软件定义、软件开发及软件运行维护三个阶段。图3.1所示的软件生命周期的主要活动阶段是:
①可行性研究与计划制定。确定待开发软件系统的开发目标和总的要求,给出它的功能、性能、可靠性以及接口等方面的可能方案,制定完成开发任务的实施计划。
②需求分析。对待开发软件提出的需求进行分析并给出详细定义。编写软件规格说明书及初步的用户手册,提交评审。
⑧软件设计。系统设计人员和程序设计人员应该在反复理解软件需求的基础上,给出软件的结构、模块的划分、功能的分配以及处理流程。在系统比较复杂的情况下,设计阶段可分解成概要设计阶段和详细设计阶段。编写概要设计说明书、详细设计说明书和测试计划初稿,提交评审。
④软件实现。把软件设计转换成计算机可以接受的程序代码。即完成源程序的编码,编写用户手册、操作手册等面向用户的文档,编写单元测试计划。
⑤软件测试。在设计测试用例的基础上,检验软件的各个组成部分。编写测试分析报告。
⑥运行和维护。将已交付的软件投入运行,并在运行使用中不断地维护,根据新提出的需求进行必要而且可能的扩充和删改。
3.1.4软件工程的目标与原则
1.软件工程的目标软件工程的目标是,在给定成本、进度的前提下,开发出具有有效性、可靠性、可理解性、可维护性、可重用性、可适应性、可移植性、可追踪性和可互操作性且满足用户需求的产品。
软件工程需要达到的基本目标应是:付出较低的开发成本;达到要求的软件功能;取得较好的软件性能;开发的软件易于移植;需要较低的维护费用;能按时完成开发,及时交付使用。
基于软件工程的目标,软件工程的理论和技术性研究的内容主要包括:软件开发技术和软件工程管理。
(1)软件开发技术软件开发技术包括:软件开发方法学、开发过程、开发工具和软件工程环境,其主体内容是软件开发方法学。软件开发方法学是根据不同的软件类型,按不同的观点和原则,对软件开发中应遵循的策略、原则、步骤和必须产生的文档资料都做出规定,从而使软件的开发能够进入规范化和工程化的阶段,以克服早期的手工方法生产中的随意性和非规范性做法。
(2)软件工程管理软件工程管理包括:软件管理学、软件工程经济学、软件一心理学等内容。
软件工程管理是软件按工程化生产时的重要环节,它要求按照预先制定的计划、进度和预算执行,以实现预期的经济效益和社会效益。统计数据表明,多数软件开发项目的失败,并不是由于软件开发技术方面的原因,它们的失败是由于不适当的管理造成的。因此人们对软件项目管理重要性的认识有待提高。软件管理学包括人员组织、进度安排、质量保证、配置管理、项目计划等。
软件工程经济学是研究软件开发中成本的估算、成本效益分析的方法和技术,用经济学的基本原理来研究软件工程开发中的经济效益问题。
软件心理学是软件工程领域具有挑战性的一个全新的研究视角,它是从个体心理、人类行为、组织行为和企业文化等角度来研究软件管理和软件工程的。
2.软件工程的原则为了达到上述的软件工程目标,在软件开发过程中,必须遵循软件工程的基本原则。这些原则适用于所有的软件项目。这些基本原则包括抽象、信息隐蔽、模块化、局部化、确定性、一致性、完备性和可验证性。
①抽象。抽取事物最基本的特性和行为,忽略非本质细节。采用分层次抽象,自顶向下,逐层细化的办法控制软件开发过程的复杂性。
②信息隐蔽。采用封装技术,将程序模块的实现细节隐藏起来,使模块接口尽量简单。
③模块化。模块是程序中相对独立的成分,一个独立的编程单位,应有良好的接口定义。模块的大小要适中,模块过大会使模块内部的复杂性增加,不利于对模块的理解和修改,也不利于模块的调试和重用。模块太小会导致整个系统表示过于复杂,不利于控制系统的复杂性。
④局部化。要求在一个物理模块内集中逻辑上相互关联的计算资源,保证模块间具有松散的耦合关系,模块内部有较强的内聚性,这有助于控制解的复杂性。
⑤确定性。软件开发过程中所有概念的表达应是确定的、无歧义且规范的。这有助于人与人的交互不会产生误解和遗漏,以保证整个开发工作的协调一致。
⑥一致性。包括程序、数据和文档的整个软件系统的各模块应使用己知的概念、符号和术语;程序内外部接口应保持一致,系统规格说明与系统行为应保持一致。
⑦完备性。软件系统不丢失任何重要成分,完全实现系统所需的功能。
⑧可验证性。开发大型软件系统需要对系统自顶向下,逐层分解。系统分解应遵循容易检查、测评、评审的原则,以确保系统的正确性。
3.1.5软件开发工具与软件开发环境现代软件工程方法之所以得以实施,其重要的保证是软件开发工具和环境的保证,使软件在开发效率、工程质量等多方面得到改善。软件工程鼓励研制和采用各种先进的软件开发方法、工具和环境。工具和环境的使用进一步提高了软件的开发效率、维护效率和软件质量。
1.软件开发工具早期的软件开发除了一般的程序设计语言外,尚缺少工具的支持,致使编程工作量大,质量和进度难以保证,导致人们将很多的精力和时间花费在程序的编制和调试上,而在更重要的软件的需求和设计上反而得不到必要的精力和时间投入。软件开发工具的完善和发展将促进软件开发方法的进步和完善,促进软件开发的高速度和高质量。软件开发工具的发展是从单项工具的开发逐步向集成工具发展的,软件开发工具为软件工程方法提供了自动的或半自动的软件支撑环境。同时,软件开发方法的有效应用也必须得到相应工具的支持,否则方法将难以有效的实施。
2.软件开发环境软件开发环境或称软件工程环境是全面支持软件开发全过程的软件工具集合。这些软件工具按照一定的方法或模式组合起来,支持软件生命周期内的各个阶段和各项任务的完成。
计算机辅助软件工程(CASE,Computer Aided Software Engineering)是当前软件开发环境中富有特色的研究工作和发展方向。CASE将各种软件工具、开发机器和一个存放开发过程信息的中心数据库组合起来,形成软件工程环境。CASE的成功产品将最大限度地降低软件开发的技术难度并使软件开发的质量得到保证。
3.2 结构化分析方法软件开发方法是软件开发过程所遵循的方法和步骤,其目的在于有效地得到一些工作产品,即程序和文档,并且满足质量要求。 软件开发方法包括分析方法、设计方法和程序设计方法。
结构化方法经过30多年的发展,已经成为系统、成熟的软件开发方法之一。结构化方法包括已经形成了配套的结构化分析方法、结构化设计方法和结构化编程方法,其核心和基础是结构化程序设计理论。
3.2.1 需求分析与需求分析方法
1.需求分析软件需求是指用户对目标软件系统在功能、行为、性能、设计约束等方面的期望。需求分析的任务是发现需求、求精、建模和定义需求的过程。需求分析将创建所需的数据模型、功能模型和控制模型。
(1)需求分析的定义
1997年IEEE软件工程标准词汇表对需求分析定义如下:
①用户解决问题或达到目标所需的条件或权能;
②系统或系统部件要满足合同、标准、规范或其他正式规定文档所需具有的条件或权能;
③一种反映①或②所描述的条件或权能的文档说明。
由需求分析的定义可知,需求分析的内容包括:提炼、分析和仔细审查已收集到的需求;确保所有利益相关者都明白其含义并找出其中的错误、遗漏或其他不足的地方;从用户最初的非形式化需求到满足用户对软件产品的要求的映射;对用户意图不断进行提示和判断。
(2)需求分析阶段的工作需求分析阶段的工作,可以概括为四个方面:
①需求获取 需求获取的目的是确定对目标系统的各方面需求。涉及到的主要任务是建立获取用户需求的方法框架,并支持和监控需求获取的过程。
需求获取涉及的关键问题有:对问题空间的理解;人与人之间的通信;不断变化的需求。
需求获取是在同用户的交流过程中不断收集、积累用户的各种信息,并且通过认真理解用户的各项要求,澄清那些模糊的需求,排除不合理的,从而较全面地提炼系统的功能性与非功能性需求。一般功能性与非功能性需求包括系统功能、物理环境、用户界面、用户因素、资源、安全性、质量保证及其他约束。
要特别注意的是,在需求获取过程中,容易产生诸如与用户存在交流障碍,相互误解,缺乏共同语言,理解不完整,忽视需求变化,混淆目标和需求等问题,这些问题都将直接影响到需求分析和系统后续开发的成败。
②需求分析对获取的需求进行分析和综合,最终给出系统的解决方案和目标系统的逻辑模型。
③编写需求规格说明书 需求规格说明书作为需求分析的阶段成果,可以为用户、分析人员和设计人员之间的交流提供方便,可以直接支持目标软件系统的确认,又可以作为控制软件开发进程的依据。
④需求评审在需求分析的最后一步,对需求分析阶段的工作进行复审,验证需求文档的一致性、可行性、完整性和有效性。
2.需求分析方法常见的需求分析方法有:
①结构化分析方法。主要包括:面向数据流的结构化分析方法(SA--Structured analysis),面向数据结构的Jackson方法(JSD-Jackson system development method),面向数据结构的结构化数据系统开发方法(DSSD-Data structured system development method)。
②面向对象的分析方法(OOA-Object-Oriented method)。
从需求分析建立的模型的特性来分,需求分析方法又分为静态分析方法和动态分析方法。
3.2.2结构化分析方法
1.关于结构化分析方法结构化分析方法是结构化程序设计理论在软件需求分析阶段的运用。它是20世纪70年代中期倡导的基于功能分解的分析方法,其目的是帮助弄清用户对软件的需求。
对于面向数据流的结构化分析方法,按照DeMarco的定义,“结构化分析就是使用数据流图(DFD)、数据字典(DD)、结构化英语、判定表和判定树等工具,来建立一种新的、称为结构化规格说明的目标文档。”
结构化分析方法的实质是着眼于数据流,自顶向下,逐层分解,建立系统的处理流程,以数据流图和数据字典为主要工具,建立系统的逻辑模型。
结构化分析的步骤如下:
①通过对用户的调查,以软件的需求为线索,获得当前系统的具体模型;
②去掉具体模型中非本质因素,抽象出当前系统的逻辑模型;
③根据计算机的特点分析当前系统与目标系统的差别,建立目标系统的逻辑模型;
④完善目标系统并补充细节,写出目标系统的软件需求规格说明;
⑤评审直到确认完全符合用户对软件的需求。
2.结构化分析的常用工具
(1)数据流图(DFD--Data Flow Diagram)
数据流图是描述数据处理过程的工具,是需求理解的逻辑模型的图形表示,它直接支持系统的功能建模。
数据流图从数据传递和加工的角度,来刻画数据流从输入到输出的移动变换过程。数据流图中的主要图形元素与说明如下:
加工(转换)。输入数据经加工变换产生输出。
数据流。沿箭头方向传送数据的通道,一般在旁边标注数据流名。
存储文件(数据源)。表示处理过程中存放各种数据的文件。
源,潭。表示系统和环境的接口,属系统之外的实体。
一般通过对实际系统的了解和分析后,使用数据流图为系统建立逻辑模型。建立数据流图的步骤如下:
第1步:由外向里:先画系统的输入输出,然后画系统的内部。
第2步:自顶向下:顺序完成顶层、中间层、底层数据流图。
第3步:逐层分解。
数据流图的建立从顶层开始,顶层的数据流图形式如图3.2所示。顶层数据流图应该包含所有相关外部实体,以及外部实体与软件中间的数据流,其作用主要是描述软件的作用范围,对总体功能、输入、输出进行抽象描述,并反映软件和系统、环境的关系。
对复杂系统的表达应采用控制复杂度策略,需要按照问题的层次结构逐步分解细化,使用分层的数据流图表达这种结构关系,分层的数据流图的形式如图3.3所示。
为保证构造的数据流图表达完整、准确、规范,应遵循以下数据流图的构造规则和注意事项:
①对加工处理建立唯一、层次性的编号,且每个加工处理通常要求既有输入又有输出;
②数据存储之间不应该有数据流;
③数据流图的一致性。它包括数据守恒和数据存储文件的使用,即某个处理用以产生输出的数据没有输入,即出现遗漏,另一种是一个处理的某些输入并没有在处理中使用以产生输出;数据存储(文件)应被数据流图中的处理读和写,而不是仅读不写、或仅写不读;
④父图、子图关系与平衡规则。相邻两层DFD之间具有父、子关系,子图代表了父图中某个加工的详细描述,父图表示了子图间的接口。子图个数不大于父图中的处理个数。所有子图的输入、输出数据流和父图中相应处理的输入、输出数据流必须一致。
图3.4是银行取款业务的数据流图。
(2)数据字典(DD—Data Dictionary)
数据字典是结构化分析方法的核心。数据字典是对所有与系统相关的数据元素的一个有组织的列表,以及精确的、严格的定义,使得用户和系统分析员对于输入、输出、存储成分和中间计算结果有共同的理解。数据字典把不同的需求文档和分析模型紧密地结合在一起,与各模型的图形表示配合,能清楚地表达数据处理的要求。
概括地说,数据字典的作用是对DFD中出现的被命名的图形元素的确切解释。通常数据字典包含的信息有:名称、别名、何处使用/如何使用、内容描述、补充信息等。例如,对加工的描述应包括:加工名、反映该加工层次的加工编号、加工逻辑及功能简述、输入/输出数据流等。
在数据字典的编制过程中,常使用定义式方式描述数据结构。表3.1给出了常用的定义式符号。
表3.1数据字典定义式方式中出现的符号符 号
含 义
=
表示“等于”,“定义为”,“由什么构成”
[…|…]
表示“或”,即选择括号中用“|”号分隔的各项中的某一项
+
表示“与”,“和”
n{}m
表示“重复”,即括号中的项要重复若干次,n,m是重复次数的上下限
(…)
表示“可选”,即括号中的项可以没有
**
表示“注释”
..
连接符
例如,银行取款业务的数据流图中,存储文件“存折”的DD定义如下:
存折=户名+所号+账户+开户日+性质+(印密)+1{存取行)50
户名=2{字母}24
所号=“001”..“999”
账号=“00000001”..“99999999”
开户日=年+月+日
性质=“1”..“6”
印密=“0”
存取行=日期+(摘要)+支出+存入+余额+操作+复核
日期=年+月+日
年=“00”..“99”
月=“01”..“12”
日=“0l”..“31”
摘要=1{字母}4
支出=金额
金额=“0000000.01”..“9999999.99”
操作=“00001”..“99999”
(3)判定树使用判定树进行描述时,应先从问题定义的文字描述中分清哪些是判定的条件,哪些是判定的结论,根据描述材料中的连接词找出判定条件之间的从属关系、并列关系、选择关系,根据它们构造判定树。
例如,某货物托运管理系统中,对发货情况的处理要依赖检查发货单,检查发货单受货物托运金额、欠款等条件的约束,可以使用类似分段函数的形式来描述这些约束和处理。对这种约束条件的描述,如果使用自然语言,表达易出现不准确和不清晰。如果使用如图3.5所示的判定树来描述,则简捷清晰。
(4)判定表判定表与判定树相似,当数据流图中的加工要依赖于多个逻辑条件的取值,即完成该加工的一组动作是由于某一组条件取值的组合而引发的,使用判定表描述比较适宜。
判定表由四部分组成,如图3.6所示。其中标识为①的左上部称条件项,列出了各种可能的条件。标识为②的右上部称条件项,它列出了各种可能的条件组合。标识为③的左下部称基本动作项,它列出了所有的操作。标识为④的右下部称动作项,它列出在对应的条件组合下所选的操作。
图3.7为“检查发货单”判定表,其中“√”表示满足对应条件项时执行的操作。
判定表或判定树是以图形形式描述数据流图的加工逻辑,它结构简单,易读易懂。尤其遇到组合条件的判定,利用判定表或判定树可以使问题的描述清晰,而且便于直接映射到程序代码。在表达一个加工逻辑时,判定树、判定表都是好的描述工具,根据需要还可以交叉使用。
l
2
3
4
条件
发货单金额
>$500
>$500
≤$500
≤$500
赊欠情况
>60天
≤60天
>60天
≤60天
操作
不发出批准书
√
发出批准书
√
√
√
发出发货单
√
√
√
发出赊欠报告
√
图3.7“检查发货单”判定表
3.2.3软件需求规格说明书软件需求规格说明书(SRS,Software Requirement Specification)是需求分析阶段的最后成果,是软件开发中的重要文档之一。
1.软件需求规格说明书的作用软件需求规格说明书的作用是:
①便于用户、开发人员进行理解和交流。
②反映出用户问题的结构,可以作为软件开发工作的基础和依据。
③作为确认测试和验收的依据。
2.软件需求规格说明书的内容软件需求规格说明书是作为需求分析的一部分而制定的可交付文档。该说明把在软件计划中确定的软件范围加以展开,制定出完整的信息描述、详细的功能说明、恰当的检验标准以及其他与要求有关的数据。
软件需求规格说明书所包括的内容和书写框架如下:
一、概述
二、数据描述
·数据流图
·数据字典
·系统接口说明
·内部接口
三、功能描述
·功能
·处理说明
·设计的限制
四、性能描述
·性能参数
·测试种类
·预期的软件响应,
·应考虑的特殊问题
五、参考文献目录
六、附录其中,概述是从系统的角度描述软件的目标和任务。
数据描述是对软件系统所必须解决的问题作出的详细说明。
功能描述中描述了为解决用户问题所需要的每一项功能的过程细节。对每一项功能要给出处理说明和在设计时需要考虑的限制条件。
在性能描述中说明系统应达到的性能和应该满足的限制条件,检测的方法和标准,预期的软件响应和可能需要考虑的特殊问题。
参考文献目录中应包括与该软件有关的全部参考文献,其中包括前期的其他文档、技术参考资料、产品目录手册以及标准等。
附录部分包括一些补充资料。如列表数据、算法的详细说明、框图、图表和其他材料。
3.软件需求规格说明书的特点软件需求规格说明书是确保软件质量的有力措施,衡量软件需求规格说明书质量好坏的标准、标准的优先级及标准的内涵是:
①正确性。体现待开发系统的真实要求。
②无歧义性。对每一个需求只有一种解释,其陈述具有惟一性。
⑧完整性。包括全部有意义的需求,功能的、性能的、设计的、约束的,属性或外部接口等方面的需求。
④可验证性。描述的每一个需求都是可以验证的,即存在有限代价的有效过程验证确认。
⑤一致性。各个需求的描述不矛盾。
⑥可理解性。需求说明书必须简明易懂,尽量少包含计算机的概念和术语,以便用户和软件人员都能接受它。
⑦可修改性。SRS的结构风格在需求有必要改变时是易于实现的。
⑧可追踪性。每一个需求的来源、流向是清晰的,当产生和改变文件编制时,可以方便地引证每一个需求。
软件需求规格说明书是一份在软件生命周期中至关重要的文件,它在开发早期就为尚未诞生的软件系统建立了一个可见的逻辑模型,它可以保证开发工作的顺利进行,因而应及时地建立并保证它的质量。
作为设计的基础和验收的依据,软件需求规格说明书应该是精确而无二义性的,需求说明书越精确,则以后出现错误、混淆、反复的可能性越小。用户能看懂需求说明书,并且发现和指出其中的错误是保证软件系统质量的关键,因而需求说明书必须简明易懂,尽量少包含计算机的概念和术语,以便用户和软件人员双方都能接受它。
3.3 结构化设计方法
3.3.1软件设计的基本概念
1.软件设计的基础软件设计是软件工程的重要阶段,是一个把软件需求转换为软件表示的过程。软件设计的基本目标是用比较抽象概括的方式确定目标系统如何完成预定的任务,即软件设计是确定系统的物理模型。
软件设计的重要性和地位概括为以下几点:
①软件开发阶段(设计、编码、测试)占据软件项目开发总成本绝大部分,是在软件开发中形成质量的关键环节;
②软件设计是开发阶段最重要的步骤,是将需求准确地转化为完整的软件产品或系统的惟一途径;
③软件设计作出的决策,最终影响软件实现的成败;
④设计是软件工程和软件维护的基础。
从技术观点来看,软件设计包括软件结构设计、数据设计、接口设计、过程设计。其中,结构设计是定义软件系统各主要部件之间的关系;数据设计是将分析时创建的模型转化为数据结构的定义;接口设计是描述软件内部、软件和协作系统之间以及软件与人之间如何通信;过程设计则是把系统结构部件转换成软件的过程性描述。
从工程管理角度来看,软件设计分两步完成:概要设计和详细设计。概要设计(又称结构设计)将软件需求转化为软件体系结构、确定系统级接口、全局数据结构或数据库模式;详细设计确立每个模块的实现算法和局部数据结构,用适当方法表示算法和数据结构的细节。
软件设计的一般过程是:软件设计是一个迭代的过程;先进行高层次的结构设计;后进行低层次的过程设计;穿插进行数据设计和接口设计。
2.软件设计的基本原理软件设计遵循软件工程的基本目标和原则,建立了适用于在软件设计中应该遵循的基本原理和与软肿设计有关的概念。
(1)抽象抽象是一种思维工具,就是把事物本质的共同特性提取出来而不考虑其他细节。软件设计中考虑模块化解决方案时,可以定出多个抽象级别。抽象的层次从概要设计到详细设计逐步降低。在软件概要设计中的模块分层也是由抽象到具体逐步分析和构造出来的。
(2)模块化模块是指把一个待开发的软件分解成若干小的简单的部分。如高级语言中的过程、函数、子程序等。每个模块可以完成一个特定的子功能,各个模块可以按一定的方法组装起来成为一个整体,从而实现整个系统的功能。
模块化是指解决一个复杂问题时自顶向下逐层把软件系统划分成若干模块的过程。
为了解决复杂的问题,在软件设计中必须把整个问题进行分解来降低复杂性,这样就可以减少开发工作量并降低开发成本和提高软件生产率。但是划分模块并不是越多越好,因为这会增加模块之间接口的工作量,所以划分模块的层次和数量应该避免过多或过少。
(3)信息隐蔽信息隐蔽是指,在一个模块内包含的信息(过程或数据),对于不需要这些信息的其他模块来说是不能访问的。
(4)模块独立性模块独立性是指,每个模块只完成系统要求的独立的子功能,并且与其他模块的联系最少且接口简单。
模块的独立程度是评价设计好坏的重要度量标准。衡量软件的模块独立性使用耦合性和内聚性两个定性的度量标准。
①内聚性:内聚性是一个模块内部各个元素间彼此结合的紧密程度的度量。内聚是从功能角度来度量模块内的联系。
内聚有如下的种类,它们之间的内聚性由弱到强排列为:
偶然内聚:指一个模块内的各处理元素之间没有任何联系。
逻辑内聚:指模块内执行几个逻辑上相关的功能,通过参数确定该模块完成哪一个功能。
时间内聚:把需要同时或顺序执行的动作组合在一起形成的模块为时间内聚模块。比如初始化模块,它顺序为变量置初值。
过程内聚:如果一个模块内的处理元素是相关的,而且必须以特定次序执行则称为过程内聚。
通信内聚:指模块内所有处理功能都通过使用公用数据而发生关系。这种内聚也具有过程内聚的特点。
顺序内聚:指一个模块中各个处理元素和同一个功能密切相关,而且这些处理必须顺序执行,通常前一个处理元素的输出就是下一个处理元素的输入。
功能内聚:指模块内所有元素共同完成一个功能,缺一不可,模块已不可再分。这是最强的内聚。
内聚性是信息隐蔽和局部化概念的自然扩展。一个模块的内聚性越强则该模块的模块独立性越强。作为软件结构设计的设计原则,要求每一个模块的内部都具有很强的内聚性,它的各个组成部分彼此都密切相关。
②耦合性:耦合性是模块间互相连接的紧密程度的度量。
耦合性取决于各个模块之间接口的复杂度、调用方式以及哪些信息通过接口。耦合可以分为下列几种,它们之间的耦合度由高到低排列为:
内容耦合:如一个模块直接访问另一模块的内容,则这两个模块称为内容耦合。
公共耦合:若一组模块都访问同一全局数据结构,则它们之间的耦合称之为公共耦合。
外部耦合:一组模块都访问同一全局简单变量(而不是同一全局数据结构),且不通过参数表传递该全局变量的信息,则称为外部耦合。
控制耦合:若一模块明显地把开关量、名字等信息送入另一模块,控制另一模块的功能,则为控制耦合。
标记耦合:若两个以上的模块都需要其余某一数据结构子结构时,不使用其余全局变量的方式而是用记录传递的方式,即两模块间通过数据结构交换信息,这样的耦合称为标记耦合。
数据耦合:若一个模块访问另一个模块,被访问模块的输入和输出都是数据项参数,即两模块间通过数据参数交换信息,则这两个模块为数据耦合。
非直接耦合:若两个模块没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的,则称这两个模块为非直接耦合。非直接耦合独立性最强。
上面仅是对耦合机制进行的一个分类。可见,一个模块与其他模块的耦合性越强则该模块的模块独立性越弱。原则上讲,模块化设计总是希望模块之间的耦合表现为非直接耦合方式。但是,由于问题所固有的复杂性和结构化设计的原则,非直接耦合往往是不存在的。
耦合性与内聚性是模块独立性的两个定性标准,耦合与内聚是相互关联的。在程序结构中,各模块的内聚性越强,则耦合性越弱。一般较优秀的软件设计,应尽量做到高内聚,低耦合,即减弱模块之间的耦合性和提高模块内的内聚性,有利于提高模块的独立性。
3.结构化设计方法与结构化需求分析方法相对应的是结构化设计方法。结构化设计就是采用最佳的可能方法设计系统的各个组成部分以及各成分之间的内部联系的技术。也就是说,结构化设计是这样一个过程,它决定用哪些方法把哪些部分联系起来,才能解决好某个具体有清楚定义的问题。
结构化设计方法的基本思想是将软件设计成由相对独立、单一功能的模块组成的结构。下面重点以面向数据流的结构化方法为例讨论结构化设计方法。
3.3.2概要设计
1.概要设计的任务软件概要设计的基本任务是:
(1)设计软件系统结构在需求分析阶段,已经把系统分解成层次结构,而在概要设计阶段,需要进一步分解,划分为模块以及模块的层次结构。划分的具体过程是:
①采用某种设计方法,将一个复杂的系统按功能划分成模块。
②确定每个模块的功能。
③确定模块之间的调用关系。
④确定模块之间的接口,即模块之间传递的信息。
⑤评价模块结构的质量。
(2)数据结构及数据库设计数据设计是实现需求定义和规格说明过程中提出的数据对象的逻辑表示。数据设计的具体任务是:确定输入、输出文件的详细数据结构;结合算法设计,确定算法所必需的逻辑数据结构及其操作;确定对逻辑数据结构所必须的那些操作的程序模块,限制和确定各个数据设计决策的影响范围;需要与操作系统或调度程序接口所必需的控制表进行数据交换时,确定其详细的数据结构和使用规则;数据的保护性设计:防卫性、一致性、冗余性设计。
数据设计中应注意掌握以下设计原则:
①用于功能和行为的系统分析原则也应用于数据。
②应该标识所有的数据结构以及其上的操作。
③应当建立数据字典,并用于数据设计和程序设计。
④低层的设计决策应该推迟到设计过程的后期。
⑤只有那些需要直接使用数据结构、内部数据的模块才能看到该数据的表示。
⑥应该开发一个由有用的数据结构和应用于其上的操作组成的库。
⑦软件设计和程序设计语言应该支持抽象数据类型的规格说明和实现。
(3)编写概要设计文档。在概要设计阶段,需要编写的文档有,概要设计说明书、数据库设计说明书、集成测试计划等。
(4)概要设计文档评审。在概要设计中,对设计部分是否完整地实现了需求中规定的功能、性能等要求,设计方案的可行性,关键的处理及内外部接口定义正确性、有效性,各部分之间的一致性等都要进行评审,以免在以后的设计中出现大的问题而返工。
常用的软件结构设计工具是结构图(SC Structure Chart),也称程序结构图。使用结构图描述软件系统的层次和分块结构关系,它反映了整个系统的功能实现以及模块与模块之间的联系与通讯,是未来程序中的控制层次体系。
结构图是描述软件结构的图形工具。结构图的基本图符如图3.8所示。
模块用一个矩形表示,矩形内注明模块的功能和名字;箭头表示模块间的调用关系。在结构图中还可以用带注释的箭头表示模块调用过程中来回传递的信息。如果希望进一步标明传递的信息是数据还是控制信息,则可用带实心圆的箭头表示传递的是控制信息,用带空心圆的箭心表示传递的是数据。
根据结构化设计思想,结构图构成的基本形式如图3.9所示。
经常使用的结构图有四种模块类型:传入模块,传出模块、变换模块和协调模块。其表示形式和含义如图3.10所示。
下面通过图3.11进一步了解程序结构图的有关术语。
深度:表示控制的层数。
上级模块、从属模块:上、下两层模块a和b,且有a调用b,则a是上级模块,b是从属模块。
宽度:整体控制跨度(最大模块数的层)的表示。
扇入:调用一个给定模块的模块个数。
扇出:一个模块直接调用的其他模块数。
原子模块:树中位于叶子结点的模块。
2.面向数据流的设计方法在需求分析阶段,主要是分析信息在系统中加工和流动的情况。面向数据流的设计方法定义了一些不同的映射方法,利用这些映射方法可以把数据流图变换成结构图表示的软件结构。首先需要了解数据流图表示的数据处理的类型,然后针对不同类型分别进行分析处理。
(1)数据流类型典型的数据流类型有两种:变换型和事务型。
①变换型。变换型是指信息沿输入通路进入系统,同时由外部形式变换成内部形式,进入系统的信息通过变换中心,经加工处理以后再沿输出通路变换成外部形式离开软件系统。变换型数据处理问题的工作过程大致分为三步,即取得数据、变换数据和输出数据,如图3.12所示。相应于取得数据、变换数据、输出数据的过程,变换型系统结构图由输入、中心变换和输出等三部分组成,如图3.13所示。
变换型数据流图映射的结构图如图3.14所示。
②事务型。在很多软件应用中,存在某种作业数据流,它可以引发一个或多个处理,这些处理能够完成该作业要求的功能,这种数据流就叫做事务。事务型数据流的特点是接受一项事务,根据事务处理的特点和性质,选择分派一个适当的处理单元(事务处理中心),然后给出结果。这类数据流归为特殊的一类,称为事务型数据流,如图3.15所示。在一个事务型数据流中,事务中心接收数据,分析每个事务以确定它的类型,根据事务类型选取一条活动通路。
事务型数据流图映射的结构图如图3.16所示。
在事务型数据流系统结构图中,事务中心模块按所接受的事务类型,选择某一事务处理模块执行,各事务处理模块并列。每个事务处理模块可能要调用若干个操作模块,而操作模块又可能调用若干个细节模块。
(2)面向数据流设计方法的实施要点与设计过程面向数据流的结构设计过程和步骤是:
第1步:分析、确认数据流图的类型,区分是事务型还是变换型。
第2步:说明数据流的边界。
第3步:把数据流图映射为程序结构。对于事务流区分事务中心和数据接收通路,将它映射成事务结构。对于变换流,区分输出和输入分支,并将其映射成变换结构。
第4步:根据设计准则对产生的结构进行细化和求精。
下面分别讨论变换型和事务型数据流图转换成程序结构图的实施步骤。
①变换型将变换型映射成结构图,又称为变换分析。其步骤如下:
第1步:确定数据流图是否具有变换特性。一般地说,一个系统中所有的信息流都可以认为是变换流,但是,当遇有明显的事务特性的信息流时,建议采用事务分析方法进行设计。在这时应该观察在整个数据流图中哪种属性占优势,先确定数据流的全局特性。此外还应把具有全局特性的不同特点的局部区域孤立出来,根据这些子数据流的特点作部分的处理。
第2步:确定输入流和输出流的边界,划分出输入、变换和输出,独立出变换中心。
第3步:进行第一级分解,将变换型映射成软件结构(参见图3.15),其中输入数据处理模块协调对所有输入数据的接收:变换中心控制模块管理对内部形式的数据的所有操作;输出数据处理控制模块协调输出信息的产生过程。
第4步:按上述步骤如出现事务流也可按事务流的映射方式对各个子流进行逐级分解,直至分解到基本功能。
第5步:对每个模块写一个简要说明,内容包括该模块的接口描述、模块内部的信息、过程陈述、包括的主要判定点及任务等。
第6步:利用软件结构的设计原则对软件结构进一步转化。
②事务型将事务型映射成结构图,又称为事务分析。其步骤如下:
事务分析的设计步骤与变换分析设计步骤大致类似,主要差别仅在于由数据流图到软件结构的映射方法不同(参见图3.14和3.16)。它是将事务中心映射成为软件结构中发送分支的调度模块,将接收通路映射成软件结构的接收分支。
3.设计的准则大量软件设计的实践证明,以下的设计准则是可以借鉴为设计的指导和对软件结构图进行优化。这些准则是:
①提高模块独立性。对软件结构应着眼于改善模块的独立性,依据降低耦合提高内聚的原则,通过把一些模块取消或合并来修改程序结构。
②模块规模适中。经验表明,当模块增大时,模块的可理解性迅速下降。但是当对大的模块分解时,不应降低模块的独立性。因为,当对一个大的模块分解时,有可能会增加模块间的依赖。
③深度、宽度、扇出和扇入适当。如果深度过大,则说明有的控制模块可能简单了。如果宽度过大,则说明系统的控制过于集中。而扇出过大则意味模块过分复杂,需要控制和协调过多的下级模块,这时应适当增加中间层次。扇出太小则可以把下级模块进一步分解成若干个子功能模块,或者合并到上级模块中去。扇入越大则共享该模块的上级模块数目越多。
经验表明,好的软件设计结构通常顶层高扇出,中间扇出较少,底层高扇入。
④使模块的作用域在该模块的控制域内。模块的作用域是指模块内一个判定的作用范围,凡是受这个判定影响的所有模块都属于这个判定的作用域。模块的控制域是指这个模块本身以及所有直接或间接从属于它的模块的集合。在一个设计得很好的系统中,所有受某个判定影响的模块应该都从属于做出判定的那个模块,最好局限于做出判定的那个模块本身及它的直属下级模块。对于那些不满足这一条件的软件结构,修改的办法是:将判定点上移或者将那些在作用范围内但是不在控制范围内的模块移到控制范围以内。
⑤应减少模块的接口和界面的复杂性。模块的接口复杂是软件容易发生错误的一个主要原因。应该仔细设计模块接口,使得信息传递简单并且和模块的功能一致。
⑥设计成单入口、单出口的模块。
⑦设计功能可预测的模块。如果一个模块可以当作一个“黑盒”,也就是不考虑模块的内部结构和处理过程,则这个模块的功能就是可以预测的。
3.3.3详细设计详细设计的任务,是为软件结构图中的每一个模块确定实现算法和局部数据结构,用某种选定的表达工具表示算法和数据结构的细节。表达工具可以由设计人员自由选择,但它应该具有描述过程细节的能力,而且能够使程序员在编程时便于直接翻译成程序设计语言的源程序。本节重点对过程设计进行讨论。
在过程设计阶段,要对每个模块规定的功能以及算法的设计,给出适当的算法描述,即确定模块内部的详细执行过程,包括局部数据组织、控制流、每一步具体处理要求和各种实现细节等。其目的是确定应该怎样来具体实现所要求的系统。
常见的过程设计工具有:
图形工具:程序流程图,N-S,PAD,HIPO。
表格工具:判定表。
语言工具:PDL(伪码)。
下面讨论其中几种主要的工具:
1.程序流程图程序流程图是一种传统的、应用广泛的软件过程设计表示工具,通常也称为程序框图。程序流程图表达直观、清晰,易于学习掌握,且独立于任何一种程序设计语言。
构成程序流程图的最基本图符及含义如图3.17所示。
按照结构化程序设计的要求,程序流程图构成的任何程序描述限制为如图3.18所示的5种控制结构。
图3.18所示的程序流程图构成的5种控制结构的含义是:
顺序型:几个连续的加工步骤依次排列构成;
选择型:由某个逻辑判断式的取值决定选择两个加工中的一个;
先判断重复型:先判断循环控制条件是否成立,成立则执行循环体语句;
后判断重复型:重复执行某些特定的加工,直到控制条件成立;
多分支选择型:列举多种加工情况,根据控制变量的取值,选择执行其中之一。
通过把程序流程图的5种基本控制结构相互组合或嵌套,可以构成任何复杂的程序流程图。
例如,下面是简单托运货物运费计算的问题。
设货物重量x,客户信息y,输入x、y后,计算运费的具体要求是:
如果O<x≤15(设为条件1),则用公式1计算后,循环3次完成同样的“记账”和“输出”操作,然后程序结束;
如果x>15(设为条件2),则用公式2计算后,循环3次完成同样的“记账”和“输出”操作,然后程序结束。
该问题程序的程序流程图描述如图3.19。
程序流程图虽然简单易学,但是若程序员不受任何约束,随意转移控制,会破坏结构化设计的原则,而且程序流程图不易表示数据结构。
2.N-S图为了避免流程图在描述程序逻辑时的随意性与灵活性,1973年Nossi和Shneiderman发表了题为“结构化程序的流程图技术”的文章,提出了用方框图来代替传统的程序流程图,通常也把这种图称为N-S图。
N-S图的基本图符及表示的5种基本控制结构如图3.20所示。
例如,上述问题程序的N-S图描述如图3.21所示。
N-S图有以下特征:
(1)每个构件具有明确的功能域;
(2)控制转移必须遵守结构化设计要求;
(3)易于确定局部数据和(或)全局数据的作用域;
(4)易于表达嵌套关系和模块的层次结构;
3.PAD图
PAD图是问题分析图(Problem Analysis Diagram)的英文缩写。它是继程序流程图和方框图之后,提出的又一种主要用于描述软件详细设计的图形表示工具。
PAD图的基本图符及表示的5种基本控制结构,如图3.22所示。
例如,上述问题程序的PAD图描述如图3.23所示。
PAD图有以下特征:
①结构清晰,结构化程度高;
②易于阅读;
③最左端的纵线是程序主干线,对应程序的第一层结构;每增加一层PAD图向右扩展一条纵线,故程序的纵线数等于程序层次数。
④程序执行:从PAD图最左主干线上端结点开始,自上而下、自左向右依次执行,程序终止于最左主干线。
4.PDL(Procedure Design Lauguage)
过程设计语言(PDL)也称为结构化的英语和伪码,它是一种混合语言,采用英语的词汇和结构化程序设计语言的语法,类似编程语言。
用PDL表示的基本控制结构的常用词汇如下:
顺序:
条件:IF/THEN/ELSE/ENDIF
循环:DO WHILE/ENDDO
循环:REPEAT UNTIL/ENDREPEAT
分支:CASE_OF/WHEN/SELECT/WHEN/SELECT/ENDCASE
例如,上述问题程序的描述如下,它是类似C语言的PDL。
/*计算运费*/
count();
{输入x;输入y;
if (0<x≤15)条件1{公式1计算;call sub;}
else if(x>15){公式2计算;call sub;}
}
sub();
{for(i=1,3,i++) do{记账;输出;}
}
PDL可以由编程语言转换得到,也可以是专门为过程描述而设计的。但应具备以下特征:
①有为结构化构成元素、数据说明和模块化特征提供的关键词语法;
②处理部分的描述采用自然语言语法:
③可以说明简单和复杂的数据结构;
④支持各种接口描述的子程序定义和调用技术。
3.4 软件测试随着计算机软、硬件技术的发展,计算机的应用领域越来越广泛,方方面面的应用对软件的功能要求也就越来越强,而且软件的复杂程度也就越来越高。但是,如何才能确保软件的质量并保证软件的高度可靠性呢?无疑,通过对软件产品进行必要的测试是非常重要的一个环节。软件测试也是在软件投入运行前对软件需求、设计、编码的最后审核。
软件测试的投入,包括人员和资金投入是巨大的,通常其工作量、成本占软件开发总工作量、总成本的40%以上,而且具有很高的组织管理和技术难度。
软件测试是保证软件质量的重要手段,其主要过程涵盖了整个软件生命期的过程,包括需求定义阶段的需求测试、编码阶段的单元测试、集成测试以及后期的确认测试、系统测试,验证软件是否合格、能否交付用户使用等。
3.4.1软件测试的目的
1983年IEEE将软件测试定义为:使用人工或自动手段来运行或测定某个系统的过程,其目的在于检验它是否满足规定的需求或是弄清预期结果与实际结果之间的差别。
关于软件测试的目的,Grenford J.Myers在《The ArtofSoftwareTesting》一书中给出了更深刻的阐述:
软件测试是为了发现错误而执行程序的过程;
一个好的测试用例是指很可能找到迄今为止尚未发现的错误的用例;
一个成功的测试是发现了至今尚未发现的错误的测试。
Myers的观点告诉人们测试要以查找错误为中心,而不是为了演示软件的正确功能。
3.4.2软件测试的准则鉴于软件测试的重要性,要做好软件测试,设计出有效的测试方案和好的测试用例,软件测试人员需要充分理解和运用软件测试的一些基本准则:
1.所有测试都应追溯到需求软件测试的目的是发现错误,而最严重的错误不外乎是导致程序无法满足用户需求的错误。
2.严格执行测试计划,排除测试的随意性软件测试应当制定明确的测试计划并按照计划执行。测试计划应包括:所测软件的功能、输入和输出、测试内容、各项测试的目的和进度安排、测试资料、测试工具、测试用例的选择、资源要求、测试的控制方式和过程等。
3.充分注意测试中的群集现象经验表明,程序中存在错误的概率与该程序中已发现的错误数成正比。这一现象说明,为了提高测试效率,测试人员应该集中对付那些错误群集的程序。
4.程序员应避免检查自己的程序为了达到好的测试效果,应该由独立的第三方来构造测试。因为从心理学角度讲,程序人员或设计方在测试自己的程序时,要采取客观的态度是程度不同地存在障碍的。
5.穷举测试不可能所谓穷举测试是指把程序所有可能的执行路径都进行检查的测试。但是,即使规模较小的程序,其路径排列数也是相当大的,在实际测试过程中不可能穷尽每一种组合。这说明,测试只能证明程序中有错误,不能证明程序中没有错误。
6.妥善保存测试计划、测试用例、出错统计和最终分析报告,为维护提供方便。
3.4.3软件测试技术与方法综述软件测试的方法和技术是多种多样的。对于软件测试方法和技术,可以从不同的角度加以分类。
若从是否需要执行被测软件的角度,可以分为静态测试和动态测试方法。若按照功能划分可以分为白盒测试和黑盒测试方法。
1.静态测试与动态测试
(1)静态测试静态测试包括代码检查、静态结构分析、代码质量度量等。静态测试可以由人工进行,充分发挥人的逻辑思维优势,也可以借助软件工具自动进行。经验表明,使用人工测试能够有效地发现30%到70%的逻辑设计和编码错误。
代码检查主要检查代码和设计的一致性,包括代码的逻辑表达的正确性,代码结构的合理性等方面。这项工作可以发现违背程序编写标准的问题,程序中不安全、不明确和模糊的部分,找出程序中不可移植部分、违背程序编程风格的问题,包括变量检查、命名和类型审查、程序逻辑审查、程序语法检查和程序结构检查等内容。代码检查包括代码审查、代码走查、桌面检查、静态分析等具体方式。
代码审查:小组集体阅读、讨论检查代码。
代码走查:小组成员通过用“脑”研究、执行程序来检查代码。
桌面检查:由程序员自己检查自己编写的程序。程序员在程序通过编译之后,进行单元测试之前,对源代码进行分析、检验,并补充相关文档,目的是发现程序的错误。
静态分析:对代码的机械性、程式化的特性分析方法,包括控制流分析、数据流分析、接口分析、表达式分析。
(2)动态测试静态测试不实际运行软件,主要通过人工进行。动态测试是基于计算机的测试,是为了发现错误而执行程序的过程。或者说,是根据软件开发各阶段的规格说明和程序的内部结构而精心设计一批测试用例(即输入数据及其预期的输出结果),并利用这些测试用例去运行程序,以发现程序错误的过程。
设计高效、合理的测试用例是动态测试的关键。测试用例(Test Case)是为测试设计的数据。测试用例由测试输入数据和与之对应的预期输出结果两部分组成。测试用例的格式为:
[(输入值集),(输出值集)]
下面重点讨论动态的白盒测试方法和黑盒测试方法。
2.白盒测试方法与测试用例设计白盒测试方法也称结构测试或逻辑驱动测试。它是根据软件产品的内部工作过程,检查内部成分,以确认每种内部操作符合设计规格要求。白盒测试把测试对象看作一个打开的盒子,允许测试人员利用程序内部的逻辑结构及有关信息来设计或选择测试用例,对程序所有的逻辑路径进行测试。通过在不同点检查程序的状态来了解实际的运行状态是否与预期的一致。所以,白盒测试是在程序内部进行,主要用于完成软件内部操作的验证。
白盒测试的基本原则是:保证所测模块中每一独立路径至少执行一次;保证所测模块所有判断的每一分支至少执行一次;保证所测模块每一循环都在边界条件和一般条件下至少各执行一次:验证所有内部数据结构的有效性。
按照白盒测试的基本原则,“白盒”法是穷举路径测试。在使用这一方案时,测试者必须检查程序的内部结构,从检查程序的逻辑着手,得出测试数据。贯穿程序的独立路径数是天文数字,但即使每条路径都测试了仍然可能有错误。第一,穷举路径测试决不能查出程序是否违反了设计规范,即程序本身是个错误的程序;第二,穷举路径测试不可能查出程序中因遗漏路径而出错;第三,穷举路径测试可能发现不了一些与数据相关的错误。
白盒测试的主要方法有逻辑覆盖、基本路径测试等。
(1)逻辑覆盖测试逻辑覆盖是泛指一系列以程序内部的逻辑结构为基础的测试用例设计技术。通常所指的程序中的逻辑表示有判断、分支、条件等几种表示方式。
①语句覆盖。选择足够的测试用例,使得程序中每个语句至少都能被执行一次。
例3.1设有程序流程图表示的程序如图3.24。
按照语句覆盖的测试要求,对图3.24的程序设计如下测试用例1和测试用例2。
语句覆盖是逻辑覆盖中基本的覆盖,尤其对单元测试来说。但是语句覆盖往往没有关注判断中的条件有可能隐含的错误。
②路径覆盖。执行足够的测试用例,使程序中所有可能的路径都至少经历一次。
例3.2设有程序流程图表示的程序如图3.25。
对图3.25的程序设计如表3.2列出的一组测试用例,就可以覆盖该程序的全部4条路径:ace,abd,abe,acd。
表3.2
测试用例
通过路径
测试用例
通过路径
[(A=2,B=0,X=3),(输出略)]
(ace)
[(A=2,B=1,X=1),(输出略)]
(abe)
[(A=1,B=0,X=1),(输出略)]
(abd)
[(A=3,B=0,X=1),(输出略)]
(acd)
③判定覆盖。使设计的测试用例保证程序中每个判断的每个取值分支(T或F)至少经历一次。
根据判定覆盖的要求,对如图3.26所示的程序,如果其中包含条件i≥j的判断为真值(即为“T”)和为假值(即为“F”)的程序执行路径至少经历一次,仍然可以使用例3.1的测试用例1和测试用例2。
程序每个判断中若存在多个联立条件,仅保证判断的真假值往往会导致某些单个条件的错误不能被发现。例如,某判断是“x<l或y>5”,其中只要一个条件取值为真,无论另一个条件是否错误,判断的结果都为真。这说明,仅有判断覆盖还无法保证能查出在判断的条件中的错误,需要更强的逻辑覆盖。
④条件覆盖。设计的测试用例保证程序中每个判断的每个条件的可能取值至少执行一次。
例3.3设有程序流程图表示的程序如图3.26。
按照条件覆盖的测试要求,对图3.26的程序判断框中的条件i≥j和条件j<5设计如下测试用例1和测试用例2,就能保证该条件取真值和取假值的情况至少执行一次。
条件覆盖深入到判断中的每个条件,但是可能会忽略全面的判断覆盖的要求。有必要考虑判断一条件覆盖。
⑤判断一条件覆盖。设计足够的测试用例,使判断中每个条件的所有可能取值至少执行一次,同时每个判断的所有可能取值分支至少执行一次。
例3.4设有程序流程图表示的程序如图3.27。
按照判断一条件覆盖的测试要求,对图3.27程序的两个判断框的每个取值分支至少经历一次,同时两个判断框中的三个条件的所有可能取值至少执行一次,设计如下测试用例1、测试用例2和测试用例3,就能保证满足判断一条件覆盖。
判断一条件覆盖也有缺陷,对质量要求高的软件单元,可根据情况提出多重条件组合覆盖以及其他更高的覆盖要求。
(2)基本路径测试基本路径测试的思想和步骤是,根据软件过程性描述中的控制流程确定程序的环路复杂性度量,用此度量定义基本路径集合,并由此导出一组测试用例对每一条独立执行路径进行测试。
例3.5设有程序流程图表示的程序如图3.28。
对图3.28的程序流程图确定程序的环路复杂度,方法是:
环路复杂度=程序流程图中的判断框个数+1则环路复杂度的值即为要设计测试用例的基本路径数,图3.28所示的程序环路复杂度为3,设计如表3.3列出的一组测试用例,覆盖的基本路径是:abf,acef.acdf。
表3.3
测试用例
通过路径
[(A=-2,B=0),(输出略)]
(abf)
[(A=5,B=0),(输出略)]
(acef)
[(A=5,B=5),(输出略)]
(acdf)
3.黑盒测试方法与测试用例设计黑盒测试方法也称功能测试或数据驱动测试。黑盒测试是对软件已经实现的功能是否满足需求进行测试和验证。黑盒测试完全不考虑程序内部的逻辑结构和内部特性,只依据程序的需求和功能规格说明,检查程序的功能是否符合它的功能说明。所以,黑盒测试是在软件接口处进行,完成功能验证。黑盒测试只检查程序功能是否棼照需求规格说明书的规定正常使用,程序是否能适当地接收输入数据而产生正确的输出信息,并且保持外部信息(如数据库或文件)的完整性。
黑盒测试主要诊断功能不对或遗漏、界面错误、数据结构或外部数据库访问错误、性能错误、初始化和终止条件错。
黑盒测试方法主要有等价类划分法、边界值分析法、错误推测法、因果图等,主要用于软件确认测试。
(1)等价类划分法等价类划分法是一种典型的黑盒测试方法。它是将程序的所有可能的输入数据划分成若干部分(及若干等价类),然后从每个等价类中选取数据作为测试用例。对每一个等价类,各个输入数据对发现程序中的错误的几率都是等效的,因此只需从每个等价类中选取一些有代表性的测试用例进行测试而发现错误。
使用等价类划分法设计测试方案,首先需要划分输入集合的等价类。等价类包括:
①有效等价类:合理、有意义的输入数据构成的集合。可以检验程序中符合规定的功能、性能。
②无效等价类:不合理、无意义的输入数据构成的集合。可以检验程序中不符合规定的功能、性能。
为此,需要研究程序的功能说明,从而确定输入数据的有效等价类和无效等价类。
等价类划分法实施步骤分为两步:
第l步:划分等价类;
第2步:根据等价类选取相应的测试用例。
例3.6程序实现输入3个边长(设为a,b,c),判断能否构成三角形。对该程序考虑等价类划分法。
满足测试三角形构成条件程序的等价类划分如表3.4所示。
表3.4
输入条件
有效等价类
无效等价类
①边长a,b,c限制
a>0或b>0或c>0
a<=0或b<=0或c<=0
②边长关系限制
a+b>c或b+c>a或a+c>b
a+b<=c或b+c<=a或a+c<=b
根据表3.4划分的等价类,可以设计以下的测试用例:
对满足输入条件①和②的有效等价类设计的测试用例:
[(a=3,b=4,c=5),(符合三角形构成条件)]
对满足输入条件①的无效等价类设计的测试用例:
[(a=-3,b=4,c=5),(无效输入)]
对满足输入条件②的无效等价类设计的测试用例:
[(a=3,b=4,c=8),(无效输入)]
划分等价类常用的几条原则是:
若输入条件规定了确切的取值范围,则可划分出一个有效等价类和两个无效等价类;
若输入条件规定了输入值的集合(或有“必须如何”的条件),可确定一个有效等价类和一个无效等价类;
若输入条件是一个布尔量,则可确定一个有效等价类和一个无效等价类;
若输入数据是一组值,且程序要对每个值分别处理。可为每个输入值确定一个有效等价类和一个无效等价类;
若规定了输入数据必须遵守一定规则,则可确定一个有效等价类和若干个无效等价类;
若已划分的等价类中各元素在程序中处理方式不同,须将该等价类进一步划分(更小的等价类)。
(2)边界值分析法边界值分析法是对各种输入、输出范围的边界情况设计测试用例的方法。
经验表明,程序错误最容易出现在输入或输出范围的边界处,而不是在输入范围的内部。因此针对各种边界情况设计测试用例,可以查出更多的错误。
使用边界值分析方法设计测试用例,确定边界情况应考虑选取正好等于,刚刚大于,或刚刚小于边界的值作为测试数据,这样发现程序中错误的概率较大。
边界值分析方法的使用要注意以下几点:
①如果输入条件规定了取值范围或数据个数,则可选择正好等于边界值、刚刚在边界范围内和刚刚超越边界外的值进行测试;
②针对规格说明的每个输入条件,使用上述原则;
③对于有序数列,选择第一个和最后一个作为测试数据。
例如,对例3.6中的判断三角形构成的程序,如果在等价类划分法中加入边界值分析的思想,即选取该等价类的边界值,则会使等价类划分法更有效。考虑等价类划分法加入边界值分析的例3.6的测试用例可以设计如下:
对满足输入条件①的无效等价类设计的测试用例:
[(a=0,b=4,c=5),(无效输入)]
或[(a=3,b=0,c=5),(无效输入)]
或[(a=3,b=4,c=0),(无效输入)]
对满足输入条件②的无效等价类设计的测试用例:
[(a=3,b=4,c=7),(无效输入)]
或[(a=9,b=4,c=5),(无效输入)]
或[(a=3,b=8,c=5),(无效输入)]
一般多用边界值分析法来补充等价类划分方法。
(3)错误推测法人们可以靠经验和直觉推测程序中可能存在的各种错误,从而有针对性地编写检查这些错误的例子,这就是错误推测法。
错误推测法的基本想法是:列举出程序中所有可能有的错误和容易发生错误的特殊情况,根据它们选择测试用例。错误推测法针对性强,可以直接切入可能的错误,直接定位,是一种非常实用、有效的方法。但是它需要丰富的经验和专业知识。
错误推测法的实施步骤一般是,对被测软件首先列出所有可能有的错误和易错情况表,然后基于该表设计测试用例。
例如,一般程序中输入为“0”或输出为“0”的情形是易错情况,测试者可以设计输入值为0的测试情况,以及使输出强迫为0的测试情况。
例如,要测试一个排序子程序,特别需要检查的情况是:输入表为空;输入表只含有一个元素;输入表的所有元素的值都相同;输入表已经排过序。这些隋况都是在程序设计时可能忽略的特殊情况。
实际上,无论是使用白盒测试方法还是黑盒测试方法,、或是其他测试方法,针对一种方法设计的测试用例,仅仅是易于发现某种类型的错误,对其他类型的错误不易发现。所以没有一种用例设计方法能适应全部的测试方案,而是各有所长。综合使用各种方法来确定合适的测试方案,应该考虑在测试成本和测试效果之间的一个合理折中。
3.4.4软件测试的实施软件测试是保证软件质量的重要手段,软件测试是一个过程,其测试流程是该过程规定的程序,目的是使软件测试工作系统化。
软件测试过程一般按4个步骤进行,即单元测试、集成测试、验收测试(确认测试)和系统测试。通过这些步骤的实施来验证软件是否合格,能否交付用户使用。
1.单元测试单元测试是对软件设计的最小单位——模块(程序单元)进行正确性检验的测试。单元测试的目的是发现各模块内部可能存在的各种错误。
单元测试的依据是详细设计说明书和源程序。
单元测试的技术可以采用静态分析和动态测试。对动态测试通常以白盒动态测试为主,辅之以黑盒测试。
单元测试主要针对模块的下列5个基本特性进行:
①模块接口测试——测试通过模块的数据流。例如,检查模块的输入参数和输出参数、全局量、文件属性与操作等都属于模块接口测试的内容。
②局部数据结构测试。例如,检查局部数据说明的一致性,数据的初始化,数据类型的一致以及数据的下溢、上溢等。
③重要的执行路径的检查。
④出错处理测试。检查模块的错误处理功能。
⑤影响以上各点及其他相关点的边界条件测试。
单元测试是针对某个模块,这样的模块通常并不是一个独立的程序,因此模块自己不能运行,而要靠辅助其他模块调用或驱动。同时,模块自身也会作为驱动模块去调用其他模块,也就是说,单元测试要考虑它和外界的联系,必须在一定的环境下进行,这些环境可以是真实的也可以是模拟的。模拟环境是单元测试常用的。
所谓模拟环境就是在单元测试中,用一些辅助模块去模拟与被测模块的相联系的其他模块,即为被测模块设计和搭建驱动模块和桩模块,如图3.29所示。
其中,驱动(Driver)模块相当于被测模块的主程序。它接收测试数据,并传给被测模块,输出实际测试结果。桩(Stub)模块通常用于代替被测模块调用的其他模块,其作用仅做少量的数据操作,是一个模拟子程序,不必将子模块的所有功能带入。
2.集成测试集成测试是测试和组装软件的过程。它是把模块在按照设计要求组装起来的同时进行测试,主要目的是发现与接口有关的错误。集成测试的依据是概要设计说明书。
集成测试所涉及的内容包括:软件单元的接口测试、全局数据结构测试、边界条件和非法输入的测试等。
集成测试时将模块组装成程序通常采用两种方式:非增量方式组装与增量方式组装。
非增量方式也称为一次性组装方式。将测试好的每一个软件单元一次组装在一起再进行整体测试。
增量方式是将已经测试好的模块逐步组装成较大系统,在组装过程中边连接边测试,以发现连接过程中产生的问题。最后通过增殖,逐步组装到所要求的软件系统。
增量方式包括自顶向下、自底向上、自顶向下与自底向上相结合的混合增量方法。
(1)自顶向下的增量方式将模块按系统程序结构,从主控模块(主程序)开始,沿控制层次自项向下地逐个把模块连接起来。自顶向下的增量方式在测试过程中能较早地验证主要的控制和判断点。
自项向下集成的过程与步骤如下:
①主控模块作为测试驱动器。直接附属于主控模块的各模块全都用桩模块代替。
②按照一定的组装次序,每次用一个真模块取代一个附属的桩模块。
③当装入每个真模块时都要进行测试。
④做完每一组测试后再用一个真模块代替另一个桩模块。
⑤可以进行回归测试(即重新再做过去做过的全部或部分测试),以便确定没有新的错误发生。
例3.7对图3.30(a)所示程序结构进行自顶向下的增量方式组装测试。
自顶向下的增量方式的组装过程如图3.30(b)~(f)所示。
(2)自底向上的增量方式自底向上集成测试方法是从软件结构中最底层的、最基本的软件单元开始进行集成和测试。在模块的测试过程中需要从子模块得到的信息可以直接运行子模块得到。由于在逐步向上组装过程中下层模块总是存在的,因此不再需要桩模块,但是需要调用这些模块的驱动模块。
自底向上集成的过程与步骤如下:
①低层的模块组成簇,以执行某个特定的软件子功能。
②编写一个驱动模块作为测试的控制程序,和被测试的簇连在一起,负责安排测试用例的输入及输出。
⑧对簇进行测试。
④拆去各个小簇的驱动模块,把几个小簇合并成大簇,再重复做②、③及④步。 这样在软件结构上逐步向上组装。
例3.8对图3.31(a)所示程序结构进行自底向上的增量方式的组装测试。
自底向上的增量方式的组装过程如图3.31(b)至(d)所示。
(3)混合增量方式自顶向下增量的方式和自底向上增量的方式各有优缺点,一种方式的优点是另一种方式的缺点。
自顶向下测试的主要优点是能较早显示出整个程序的轮廓,主要缺点是,当测试上层模块时使用桩模块较多,很难模拟出真实模块的全部功能,使部分测试内容被迫推迟,直至换上真实模块后再补充测试。
自底向上测试从下层模块开始,设计测试用例比较容易,但是在测试的早期不能显示出程序的轮廓。
针对自顶向下、自底向上方法各自的优点和不足,人们提出了自顶向下和自底向上相结合、从两头向中间逼近的混合式组装方法,被形象称之为“三明治”方法。这种方式,结合考虑软件总体结构的良好设计原则,在程序结构的高层使用自顶向下方式,在程序结构的低层使用自底向上方式。
3.确认测试确认测试的任务是验证软件的功能和性能及其他特性是否满足了需求规格说明中确定的各种需求,以及软件配置是否完全、正确。
确认测试的实施首先运用黑盒测试方法,对软件进行有效性测试,即验证被测软件是否满足需求规格说明确认的标准。复审的目的在于保证软件配置齐全、分类有序,以及软件配置所有成分的完备性、一致性、准确性和可操作性,并且包括软件维护所必需的细节。
4.系统测试系统测试是将通过测试确认的软件,作为整个基于计算机系统的一个元素,与计算机硬件、外设、支持软件、数据和人员等其他系统元素组合在一起,在实际运行(使用)环境下对计算机系统进行一系列的集成测试和确认测试。由此可知,系统测试必须在目标环境下运行,其功用在于评估系统环境下软件的性能,发现和捕捉软件中潜在的错误。
系统测试的目的是在真实的系统工作环境下检验软件是否能与系统正确连接,发现软件与系统需求不一致的地方。
系统测试的具体实施一般包括:功能测试、性能测试、操作测试、配置测试、外部接口测试、安全性测试等。
3.5程序的调试
3.5.1基本概念在对程序进行了成功的测试之后将进入程序调试(通常称Debug,即排错)。程序调试的任务是诊断和改正程序中的错误。它与软件测试不同,软件测试是尽可能多地发现软件中的错误。先要发现软件的错误,然后借助于一定的调试工具去执行找出软件错误的具体位置。软件测试贯穿整个软件生命期,调试主要在开发阶段。
由程序调试的概念可知,程序调试活动由两部分组成,其一是根据错误的迹象确定程序中错误的确切性质、原因和位置。其二,对程序进行修改,排除这个错误。
1.程序调试的基本步骤
(1)错误定位从错误的外部表现形式入手,研究有关部分的程序,确定程序中出错位置,找出错误的内在原因。确定错误位置占据了软件调试绝大部分的工作量。
从技术角度来看,错误的特征和查找错误的难度在于:
①现象与原因所处的位置可能相距很远。就是说,现象可能出现在程序的一个部位,而原因可能在离此很远的另一个位置。高耦合的程序结构中这种情况更为明显。
②当纠正其他错误时,这一错误所表现出的现象可能会消失或暂时性消失,但并未实际排除。
⑧现象可能并不是由错误引起的(如舍入误差)。
④现象可能是由于一些不容易发现的人为错误引起的。
⑤错误现象可能时有时无。,
⑥现象是由于难于再现的输入状态(例如实时应用中输入顺序不确定)引起。
⑦现象可能是周期出现的。如在软件、硬件结合的嵌入式系统中常常遇到。
(2)修改设计和代码,以排除错误排错是软件开发过程中一项艰苦的工作,这也决定了调试工作是一个具有很强技术性和技巧性的工作。软件工程人员在分析测试结果的时候会发现,软件运行失效或出现问题,往往只是潜在错误的外部表现,而外部表现与内在原因之间常常没有明显的联系。如果要找出真正的原因,排除潜在的错误,不是一件易事。因此可以说,调试是通过现象,找出原因的一个思维分析的过程。
(3)进行回归测试,防止引进新的错误因为修改程序可能带来新的错误,重复进行暴露这个错误的原始测试或某些有关测试,以确认该错误是否被排除、是否引进了新的错误。如果所做的修正无效,则撤销这次改动,重复上述过程,直到找到一个有效的解决办法为止。
2.程序调试的原则在软件调试方面,许多原则实际上是心理学方面的问题。因为调试活动由对程序中错误的定性、定位和排错两部分组成,因此调试原则也从以下两个方面考虑。
(1)确定错误的性质和位置时的注意事项:
①分析思考与错误征兆有关的信息。
②避开死胡同。如果程序调试人员在调试中陷入困境,最好暂时把问题抛开,留到后面适当的时间再去考虑,或者向其他人讲解这个问题,去寻求新的解决思路。
③只把调试工具当做辅助手段来使用。利用调试工具,可以帮助思考,但不能代替思考。因为调试工具给人提供的是一种无规律的调试方法。
④避免用试探法,最多只能把它当做最后手段。这是一种碰运气的盲目的动作,它的成功机率很小,而且还常把新的错误带到问题中来。
(2)修改错误的原则
①在出现错误的地方,很可能还有别的错误。经验表明,错误有群集现象,当在某一程序段发现有错误时,在该程序段中还存在别的错误的概率也很高。因此,在修改一个错误时,还要观察和检查相关的代码,看是否还有别的错误。
②修改错误的一个常见失误是只修改了这个错误的征兆或这个错误的表现,而没有修改错误本身。如果提出的修改不能解释与这个错误有关的全部现象,那就表明了只修改了错误的一部分。
③注意修正一个错误的同时有可能会引入新的错误。人们不仅需要注意不正确的修改,而且还要注意看起来是正确的修改可能会带来的副作用,即引进新的错误。因此在修改了错误之后,必须进行回归测试。
④修改错误的过程将迫使人们暂时回到程序设计阶段。修改错误也是程序设计的一种形式。一般说来,在程序设计阶段所使用的任何方法都可以应用到错误修正的过程中来。
⑤修改源代码程序,不要改变目标代码。
3.5.2软件调试方法调试的关键在于推断程序内部的错误位置及原因。从是否跟踪和执行程序的角度,类似于软件测试,软件调试可以分为静态调试和动态调试。软件测试中讨论的静态分析方法同样适用静态调试。静态调试主要指通过人的思维来分析源程序代码和排错,是主要的调试手段,而动态调试是辅助静态调试的。主要的调试方法可以采用:
1.强行排错法作为传统的调试方法,其过程可概括为,设置断点、程序暂停、观察程序状态、继续运行程序。这是目前使用较多、效率较低的调试方法。涉及的调试技术主要是设置断点和监视表达式。例如:
①通过内存全部打印来排错。
②在程序特定部位设置打印语句——即断点法。输出存储器内容,就是在程序执行到某一行的时候,计算机自动停止运行,并保留这时各变量的状态,方便检查,校对。
③自动调试工具。可供利用的典型的语言功能有打印出语句执行的追踪信息、追踪子程序调用,以及指定变量的变化情况。自动调试工具的功能是设置断点,当程序执行到某个特定的语句或某个特定的变量值改变时,程序暂停执行。程序员可在终端上观察程序此时的状态。
应用以上任何一种技术之前,都应当对错误的征兆进行全面彻底的分析,得出对出错位置及错误性质的推测,再使用一种适当的排错方法来检验推测的正确性。
2.回溯法该方法适合于小规模程序的排错。即一旦发现了错误,先分析错误征兆,确定最先发现“症状”的位置。然后,从发现“症状”的地方开始,沿程序的控制流程,逆向跟踪源程序代码,直到找到错误根源或确定错误产生的范围。
回溯法对于小程序很有效,往往能把错误范围缩小到程序中的一小段代码,仔细分析这段代码不难确定出错的准确位置。但随着源代码行数的增加,潜在的回溯路径数目很多,回溯会变得很困难,而且实现这种回溯的开销大。
3.原因排除法原因排除法是通过演绎和归纳,以及二分法来实现的。
演绎法是一种从一般原理或前提出发,经过排除和精化的过程来推导出结论的思考方法。演绎法排错是测试人员首先根据已有的测试用例,设想及枚举出所有可能出错的原因作为假设。然后再用原始测试数据或新的测试,从中逐个排除不可能正确的假设。最后,再用测试数据验证余下的假设确定出错的原因。
归纳法是一种从特殊推断出一般的系统化思考方法。其基本思想是从一些线索(错误征兆或与错误发生有关的数据)着手,通过分析寻找到潜在的原因,从而找出错误。
二分法实现的基本思想是,如果已知每个变量在程序中若干个关键点的正确值,则可以使用定值语句(如赋值语句、输入语句等)在程序中的某点附近给这些变量赋正确值,然后运行程序并检查程序的输出。如果输出结果是正确的,则错误原因在程序的前半部分;反之,错误原因在程序的后半部分。对错误原因所在的部分重复使用这种方法,直到将出错范围缩小到容易诊断的程度为止。
上面的每一种方法都可以使用调试工具来辅助完成。例如,可以使用带调试功能的编译器、动态调试器、自动测试用例生成器以及交叉引用工具等。
需要注意的一个实际问题是,调试的成果是排错,为了修改程序中错误,往往会采用“补丁程序”来实现,而这种做法会引起整个程序质量的下降,但是从目前程序设计发展的状况看,对大规模的程序的修改和质量保证,又不失为一种可行的方法。
习 题 3
一、选择题
1.在软件生命周期中,能准确地确定软件系统必须做什么和必须具备哪些功能的阶段是
A)概要设计 B)详细设计 c)可行性分析 D)需求分析
2.下面不属于软件工程的3个要素的是
A)工具 B)过程 c)方法 D)环境
3.检查软件产品是否符合需求定义的过程称为
A)确认测试 B)集成测试 C)验证测试 D)验收测试
4.数据流图用于抽象描述一个软件的逻辑模型,数据流图由一些特定的图符构成。下列图符名标识的图符不属于数据流图合法图符的是
A)控制流 B)加工 C)数据存储 D)源和潭
5.下面不属于软件设计原则的是
A)抽象 B)模块化 c)自底向上 D)信息隐蔽
6.程序流程图(PFD)中的箭头代表的是
A)数据流 B)控制流 c)调用关系 D)组成关系
7.下列T且中为需求分析常用T且的县
A)PAD B)PFD C)N-S D)DFD
8.在结构化方法中,软件功能分解属于下列软件开发中的阶段是
A)详细设计 B)需求分析 c)总体设计 D)编程调试
9.软件调试的目的是
A)发现错误 B)改正错误 c)改善软件的性能D)挖掘软件的潜能
10.软件需求分析阶段的工作,可以分为四个方面:需求获取,需求分析,编写需求规格说明书,以及
A)阶段性报告 B)需求评审 C)总结 D)都不正确二、填空题
1.软件是程序、数据和________的集合。
2.Jackson方法是一种面向________的结构化方法。
3.软件工程研究的内容主要包括:________技术和软件工程管理。
4.数据流图的类型有________和事务型。
5.软件开发环境是全面支持软件开发全过程的________集合
第4章 数据库设计基础数据库技术是计算机领域的一个重要分支。在计算机应用的三大领域(科学计算、数据处理和过程控制)中,数据处理约占其中的70%,而数据库技术就是作为一门数据处理技术发展起来的。随着计算机应用的普及和深入,数据库技术变得越来越重要了,而了解、掌握数据库系统的基本概念和基本技术是应用数据库技术的前提。本章首先介绍数据库系统的基础知识,然后对基本数据模型进行讨论,特别是其中的E-R模型和关系模型;之后再介绍关系代数及其在关系数据库中的应用,并对关系的规范化理论作了简单说明;最后,较为详细地讨论了数据库的设计过程。
4.1 数据厍系统的基本概念计算机科学与技术的发展,计算机应用的深入与拓展,使得数据库在计算机应用中的地位与作用日益重要,它在商业中、事务处理中占有主导地位。近年来在统计领域、在多媒体领域以及智能化应用领域中的地位与作用也变得十分重要。随着网络应用的普及,它在网络中的应用也日渐重要。因此,数据库已成为构成一个计算机应用系统的重要的支持性软件。
4.1.1数据、数据库、数据库管理系统
1.数据数据(Data)实际上就是描述事物的符号记录。
计算机中的数据一般分为两部分,其中一部分与程序仅有短时间的交互关系,随着程序的结束而消亡,它们称为临时性(Transient)数据,这类数据一般存放于计算机内存中;而另一部分数据则对系统起着长期持久的作用,它们称为持久性(Persistent)数据。数据库系统中处理的就是这种持久性数据。
软件中的数据是有一定结构的。首先,数据有型(type)与值(Value)之分,数据的型给出了数据表示的类型,如整型、实型、字符型等,而数据的值给出了符合给定型的值,如整型值15。随着应用需求的扩大,数据的型有了进一步的扩大,它包括了将多种相关数据以一定结构方式组合构成特定的数据框架,这样的数据框架称为数据结构(Data structure),数据库中在特定条件下称之为数据模式(Data schema)。
在过去的软件系统中是以程序为主体,而数据则以私有形式从属于程序,此时数据在系统中是分散、凌乱的,这也造成了数据管理的混乱,如数据冗余度高,数据一致性差以及数据的安全性差等多种弊病。近10多年来,数据在软件系统中的地位产生了变化,在数据库系统及数据库应用系统中数据已占有主体地位,而程序已退居附属地位。在数据库系统中需要对数据进行集中、统一的管理,以达到数据被多个应用程序共享的目标。
2.数据库数据库(Database,简称DB)是数据的集合,它具有统一的结构形式并存放于统一的存储介质内,是多种应用数据的集成,并可被各个应用程序所共享。
数据库存放数据是按数据所提供的数据模式存放的,它能构造复杂的数据结构以建立数据间内在联系与复杂的关系,从而构成数据的全局结构模式。
数据库中的数据具有“集成”、“共享”之特点,亦即是数据库集中了各种应用的数据,进行统一的构造与存储,而使它们可被不同应用程序所使用。
3.数据库管理系统数据库管理系统(Database Management System,简称DBMS)是数据库的机构,它是一种系统软件,负责数据库中的数据组织、数据操纵、数据维护、控制及保护和数据服务等。数据库中的数据是具有海量级的数据,并且其结构复杂,因此需要提供管理工具。数据库管理系统是数据库系统的核心,它主要有如下几方面的具体功能:
(1)数据模式定义。数据库管理系统负责为数据库构建模式,也就是为数据库构建其数据框架。
(2)数据存取的物理构建。数据库管理系统负责为数据模式的物理存取及构建提供有效的存取方法与手段。
(3)数据操纵。数据库管理系统为用户使用数据库中的数据提供方便,它一般提供查询、插入、修改以及删除数据的功能。此外,它自身还具有做简单算术运算及统计的能力,而且还可以与某些过程性语言结合,使其具有强大的过程性操作能力。
(4)数据的完整性、安全性定义与检查。数据库中的数据具有内在语义上的关联性与一致性,它们构成了数据的完整性,数据的完整性是保证数据库中数据正确的必要条件,因此必须经常检查以维护数据的正确。
数据库中的数据具有共享性,而数据共享可能会引发数据的非法使用,因此必须要对数据正确使用作出必要的规定,并在使用时做检查,这就是数据的安全性。
数据完整性与安全性的维护是数据库管理系统的基本功能。
(5)数据库的并发控制与故障恢复。数据库是一个集成、共享的数据集合体,它能为多个应用程序服务,所以就存在着多个应用程序对数据库的并发操作。在并发操作中如果不加控制和管理,多个应用程序间就会相互干扰,从而对数据库中的数据造成破坏。因此,数据库管理系统必须对多个应用程序的并发操作做必要的控制以保证数据不受破坏,这就是数据库的并发控制。
数据库中的数据一旦遭受破坏,数据库管理系统必须有能力及时进行恢复,这就是数据库的故障恢复。
(6)数据的服务。数据库管理系统提供对数据库中数据的多种服务功能,如数据拷贝、转存、重组、性能监测、分析等。
为完成以上六个功能,数据库管理系统一般提供相应的数据语言(Data Language),它们是:
数据定义语言(Data Definition Language简称DDL)。该语言负责数据的模式定义与数据的物理存取构建。
数据操纵语言(Data Manipulation Language简称DML)。该语言负责数据的操纵,包括查询及增、删、改等操作。
数据控制语言(Data Control Language简称DCL)。该语言负责数据完整性、安全性的定义与检查以及并发控制、故障恢复等功能,包括系统初启程序、文件读写与维护程序、存取路径管理程序、缓冲区管理程序、安全性控制程序、完整性检查程序、并发控制程序、事务管理程序、运行日志管理程序、数据库恢复程序等。
上述数据语言按其使用方式具有两种结构形式:
交互式命令语言。它的语言简单,能在终端上即时操作,它又称为自含型或自主型语言。
宿主型语言。它一般可嵌入某些宿主语言(Host Language)中,如C,C++和COBOL等高级过程性语言中。
此外,数据库管理系统还有为用户提供服务的服务性(Utility)程序,包括数据初始装入程序、数据转存程序、性能监测程序、数据库再组织程序、数据转换程序、通信程序等。
目前流行的DBMS均为关系数据库系统,比如ORACLE、sybase的PowerBuilder及IBM的DB2、微软的SQLServer等,它们均为严格意义上的DBMS系统。另外有一些小型的数据库,如微软的Vasual Foxpro和Access等,它们只具备数据库管理系统的一些简单功能。
4.数据库管理员由于数据库的共享性,因此对数据库的规划、设计、维护、监视等需要有专人管理,称他们为数据库管理员(Database Administrator简称DBA)。其主要工作如下:
(1)数据库设计(Database Design)。DBA的主要任务之一是做数据库设计,具体的说是进行数据模式的设计。由于数据库的集成与共享性,因此需要有专门人员(即DBA)对多个应用的数据需求作全面的规划、设计与集成。
(2)数据库维护。DBA必须对数据库中的数据安全性、完整性、并发控制及系统恢复、数据定期转存等进行实施与维护。
(3)改善系统性能,提高系统效率。DBA必须随时监视数据库运行状态,不断调整内部结构,使系统保持最佳状态与最高效率。当效率下降时,DBA需采取适当的措施,如进行数据库的重组、重构等。
5.数据库系统数据库系统(Database System简称DBS)由如下几部分组成:数据库(数据)、数据库管理系统(软件)、数据库管理员(人员)、系统平台之一——硬件平台(硬件)、系统平台之二——软件平台(软件)。这五个部分构成了一个以数据库为核心的完整的运行实体,称为数据库系统。
在数据库系统中,硬件平台包括:
计算机:它是系统中硬件的基础平台,目前常用的有微型机、小型机、中型机、大型机及巨型机。
网络:过去数据库系统一般建立在单机上,但是近年来它较多的建立在网络上,从目前形势看,数据库系统今后将以建立在网络上为主,而其结构形式又以客户/服务器(C/S)方式与浏览器/服务器(B/S)方式为主。
在数据库系统中,软件平台包括:
操作系统:它是系统的基础软件平台,目前常用的有各种UNIX(包括LINUX)与WINDOWS两种。
数据库系统开发工具:为开发数据库应用程序所提供的工具,它包括过程性程序设计语言如C,C++等,也包括可视化开发工具VB、PB、Delphi等,它还包括近期与INTERNET有关的HTML及XML等以及一些专用开发工具。
接口软件:在网络环境下数据库系统中数据库与应用程序,数据库与网络间存在着多种接口,它们需要用接口软件进行联接,否则数据库系统整体就无法运作,这些接口软件包括ODBC,JDBC,OLEDB,CORBA,COM,DCOM等。
6.数据库应用系统(Database Application System简称DBAS)
利用数据库系统进行应用开发可构成一个数据库应用系统,数据库应用系统是数据库系统再加上应用软件及应用界面这三者所组成,具体包括:数据库、数据库管理系统、数据库管理员,硬件平台、软件平台、应用软件、应用界面。其中应用软件是由数据库系统所提供的数据库管理系统(软件)及数据库系统开发工具所书写而成,而应用界面大多由相关的可视化工具开发而成。
数据库应用系统的7个部分以一定的逻辑层次结构方式组成一个有机的整体。如果不计数据库管理员(人员)并将应用软件与应用界面记成应用系统,则数据库应用系统的结构如图4.1所示。
下面以一个用户读取某数据记录为例,展示在数据库系统中访问数据的具体执行过程,该过程如图4.2所示,对其各个步骤简单说明如下:
①用户程序中有一条读数据库记录的DML语句,当计算机执行到该语句时,即向DBMS发出读取相应记录的命令。
②DBMS接到该命令后,首先访问该用户对应的子模式,检查该操作是否在合法授权范围内及欲读记录的正确性、有效性,若不合法则拒绝执行,并向应用程序状态返回区发出回答状态信息;反之执行下一步。
③DBMS读取模式描述并从子模式映像到全局模式,从而确定所需的逻辑记录类型。
④DBMS从逻辑模式映像到存储模式,从而确定读入哪些物理记录以及具体的地址信息。
⑤DBMS向操作系统发出从指定地址读取记录的命令。
⑥操作系统执行读命令,按指定地址从数据库中把记录读入系统缓冲区,并在操作结束后向DBMS作出回答。
⑦DBMS按照模式将读入系统缓冲区中的内容映像成用户要求读取的逻辑记录。
⑧DBMS将导出的逻辑记录送入用户工作区,并将操作执行情况的状态信息返回给用户。
⑨DBMS将已执行的操作载入运行日志。
⑩应用程序根据返回的状态信息决定是否利用该数据进行操作等。
如果用户是更新一个记录内容,则执行过程类似。首先读出目标记录,并在用户工作区中进行修改,然后向DBMS发出“写回修改数据”的数据库指令即可。
4.1.2数据库系统的发展数据管理发展至今已经历了三个阶段:人工管理阶段、文件系统阶段和数据库系统阶段。人工管理阶段是在20世纪50年代中期以前,主要用于科学计算,硬件无磁盘,直接存取,软件没有操作系统。20世纪50年代后期到20世纪60年代中期,进入文件系统阶段。20世纪60年代之后,数据管理进入数据库系统阶段。随着计算机应用领域不断扩大,数据库系统的功能和应用范围也愈来愈广,到目前已成为计算机系统的基本及主要的支撑软件。
1.文件系统阶段文件系统是数据库系统发展的初级阶段,它提供了简单的数据共享与数据管理能力,但是它无法提供完整的、统一的、管理和数据共享的能力。由于它的功能简单,因此它附属于操作系统而不成为独立的软件,目前一般将其看成仅是数据库系统的雏形,而不是真正的数据库系统。
2.层次数据库与网状数据库系统阶段从20世纪60年代末期起,真正的数据库系统——层次数据库与网状数据库开始发展,它们为统一管理与共享数据提供了有力支撑,这个时期数据库系统蓬勃发展形成了有名的“数据库时代”。但是这两种系统也存在不足,主要是它们脱胎于文件系统,受文件的物理影响较大,对数据库使用带来诸多不便,同时,此类系统的数据模式构造烦琐不宜于推广使用。
3.关系数据库系统阶段关系数据库系统出现于20世纪70年代,在80年代得到蓬勃发展,并逐渐取代前两种系统。关系数据库系统结构简单,使用方便,逻辑性强物理性少,因此在80年代以后一直占据数据库领域的主导地位。但是由于此系统来源于商业应用,适合于事务处理领域而对非事务处理领域应用受到限制,因此在80年代末期兴起与应用技术相结合的各种专用数据库系统。
工程数据库系统:是数据库与工程领域的结合;
图形数据库系统:是数据库与图形应用的结合:
图像数据库系统:是数据库与图像应用的结合;
统计数据库系统:是数据库与工程应用的结合;
知识库系统:是数据库与人工智能应用领域的结合;
分布式数据库系统:是数据库与网络应用的结合;
并行数据库系统:是数据库与多机并行应用的结合;
面向对象数据库系统:是数据库与面向对象方法的结合。
关于数据管理三个阶段中的软硬件背景及处理特点,简单概括在表4.1中。
表4.1数据管理三个阶段的比较
人工管理
文件系统
数据库系统
背景
应用背景
科学计算
科学计算、管理
大规模管理
硬件背景
无直接存取设备
磁盘、磁鼓
大容量磁盘
软件背景
没有操作系统
有文件系统
有数据库管理系统
处理方式
批处理
联机实时处理批处理
联机实时处理分布处理批处理
特点
数据管理者
人
文件系统
数据库管理系统
数据面向对象
某个应用程序
某个应用程序
现实世界
数据共享程度
无共享冗余度大
共享性差冗余度大
共享性大冗余度小
数据独立性
不独立,完全依赖于程序
独立性差
具有高度的物理独立性和一定的逻辑独立性
数据结构化
无结构
记录内有结构整体无结构
整体结构化,用数据模型描述
数据控制能力
应用程序自己控制
应用程序自己控制
由DBMS提供数据安全性、完整性、并发控制和恢复
目前,数据库技术也与其他信息技术一样在迅速发展之中,计算机处理能力的增强和越来越广泛的应用是促进数据库技术发展的重要动力。一般认为,未来的数据库系统应支持数据管理、对象管理和知识管理,应该具有面向对象的基本特征。在关于数据库的诸多新技术中,下面三种是比较重要的:
面向对象数据库系统:用面向对象方法构筑面向对象数据模型使其具有比关系数据库系统更为通用的能力;
知识库系统:用人工智能中的方法特别是用谓词逻辑知识表示方法构筑数据模型,使其模型具有特别通用的能力;
关系数据库系统的扩充:利用关系数据库作进一步扩展,使其在模型的表达能力与功能上有进一步的加强,如与网络技术相结合的Web数据库、数据仓库及嵌入式数据库等。
4.1.3数据库系统的基本特点数据库技术是在文件系统基础上发展产生的,两者都以数据文件的形式组织数据,但由于数据库系统在文件系统之上加入了DBMS对数据进行管理,从而使得数据库系统具有以下特点:
1.数据的集成性数据库系统的数据集成性主要表现在如下几个方面:
(1)在数据库系统中采用统一的数据结构方式,如在关系数据库中采用二维表作为统一结构方式。
(2)在数据库系统中按照多个应用的需要组织全局的统一的数据结构(即数据模式),数据模式不仅可以建立全局的数据结构,还可以建立数据间的语义联系从而构成一个内在紧密联系的数据整体。
(3)数据库系统中的数据模式是多个应用共同的、全局的数据结构,而每个应用的数据则是全局结构中的一部分,称为局部结构(即视图),这种全局与局部的结构模式构成了数据库系统数据集成性的主要特征。
2.数据的高共享性与低冗余性由于数据的集成性使得数据可为多个应用所共享,特别是在网络发达的今天,数据库与网络的结合扩大了数据关系的应用范围。数据的共享自身又可极大地减少数据冗余性,不仅减少了不必要的存储空间,更为重要的是可以避免数据的不一致性。所谓数据的一致性是指在系统中同一数据的不同出现应保持相同的值,而数据的不一致性指的是同一数据在系统的不同拷贝处有不同的值。因此,减少冗余性以避免数据的不同出现是保证系统一致性的基础。
3.数据独立性数据独立性是数据与程序间的互不依赖性,即数据库中数据独立于应用程序而不依赖于应用程序。也就是说,数据的逻辑结构、存储结构与存取方式的改变不会影响应用程序。
数据独立性一般分为物理独立性与逻辑独立性两级。
(1)物理独立性:物理独立性即是数据的物理结构(包括存储结构、存取方式等)的改变,如存储设备的更换、物理存储的更换、存取方式改变等都不影响数据库的逻辑结构,从而不致引起应用程序的变化。
(2)逻辑独立性:数据库总体逻辑结构的改变,如修改数据模式、增加新的数据类型、改变数据间联系等,不需要相应修改应用程序,这就是数据的逻辑独立性。
4.数据统一管理与控制数据库系统不仅为数据提供高度集成环境,同时它还为数据提供统一管理的手段,这主要包含以下三个方面:
(1)数据的完整性检查:检查数据库中数据的正确性以保证数据的正确。
(2)数据的安全性保护:检查数据库访问者以防止非法访问。
(3)并发控制:控制多个应用的并发访问所产生的相互干扰以保证其正确性。
4.1.4数据库系统的内部结构体系数据库系统在其内部具有三级模式及二级映射,三级模式分别是概念级模式、内部级模式与外部级模式,二级映射则分别是概念级到内部级的映射以及外部级到概念级的映射。这种三级模式与二级映射构成了数据库系统内部的抽象结构体系,如图4.3所示。
1.数据库系统的三级模式.
数据模式是数据库系统中数据结构的一种表示形式,它具有不同的层次与结构方式。
(1)概念模式。概念模式(Conceptual Schema)是数据库系统中全局数据逻辑结构的描述,是全体用户(应用)公共数据视图。此种描述是一种抽象的描述,它不涉及具体的硬件环境与平台,也与具体的软件环境无关。
概念模式主要描述数据的概念记录类型以及它们间的关系,它还包括一些数据间的语义约束,对它的描述可用DBMS中的DDL语言定义。
(2)外模式。外模式(External Schema)也称子模式(Subschema)或用户模式(User's schema)。它是用户的数据视图,也就是用户所见到的数据模式,它由概念模式推导而出。概念模式给出了系统全局的数据描述而外模式则给出每个用户的局部数据描述。一个概念模式可以有若干个外模式,每个用户只关心与它有关的模式,这样不仅可以屏蔽大量无关信息而且有利于数据保护。在一般的DBMS中都提供有相关的外模式描述语言(外模式DDL)。
(3)内模式。内模式(Internal Schema)又称物理模式(Physical Schema),它给出了数据库物理存储结构与物理存取方法,如数据存储的文件结构、索引、集簇及hash等存取方式与存取路径,内模式的物理性主要体现在操作系统及文件级上,它还未深入到设备级上(如磁盘及磁盘操作)。内模式对一般用户是透明的,但它的设计直接影响数据库的性能。DBMS一般提供相关的内模式描述语言(内模式DDL)。
数据模式给出了数据库的数据框架结构,数据是数据库中的真正的实体,但这些数据必须按框架所描述的结构组织,以概念模式为框架所组成的数据库叫概念数据库(Conceptual Database),以外模式为框架所组成的数据库叫用户数据库(user's Database),以内模式为框架所组成的数据库叫物理数据库(Physical Database)。这三种数据库中只有物理数据库是真实存在于计算机外存中,其他两种数据库并不真正存在于计算机中,而是通过两种映射由物理数据库映射而成。
模式的三个级别层次反映了模式的三个不同环境以及它们的不同要求,其中内模式处于最底层,它反映了数据在计算机物理结构中的实际存储形式,概念模式处于中层,它反映了设计者的数据全局逻辑要求,而外模式处于最外层,它反映了用户对数据的要求。
2.数据库系统的两级映射数据库系统的三级模式是对数据的三个级别抽象,它把数据的具体物理实现留给物理模式,使用户与全局设计者不必关心数据库的具体实现与物理背景;同时,它通过两级映射建立了模式间的联系与转换,使得概念模式与外模式虽然并不具备物理存在,但是也能通过映射而获得其实体。此外,两级映射也保证了数据库系统中数据的独立性,亦即数据的物理组织改变与逻辑概念级改变相互独立,使得只要调整映射方式而不必改变用户模式。
(1)概念模式到内模式的映射。该映射给出了概念模式中数据的全局逻辑结构到数据的物理存储结构间的对应关系,此种映射一般由DBMS实现。
(2)外模式到概念模式的映射。概念模式是一个全局模式而外模式是用户的局部模式。一个概念模式中可以定义多个外模式,而每个外模式是概念模式的一个基本视图。外模式到概念模式的映射给出了外模式与概念模式的对应关系,这种映射一般也是由DBMS来实现的。
4.2 数据模型
4.2.1数据模型的基本概念数据库中的数据模型可以将复杂的现实世界要求反映到计算机数据库中的物理世界,这种反映是一个逐步转化的过程,它分为两个阶段:由现实世界开始,经历信息世界而至计算机世界,从而完成整个转化。
现实世界(real world):用户为了某种需要,需将现实世界中的部分需求用数据库实现,这样,我们所见到的是客观世界中的划定边界的一个部分环境,它称为现实世界。
信息世界(information world):通过抽象对现实世界进行数据库级上的刻画所构成的逻辑模型叫信息世界。信息世界与数据库的具体模型有关,如层次、网状、关系模型等。
计算机世界(computer world):在信息世界基础上致力于其在计算机物理结构上的描述,从而形成的物理模型叫计算机世界。现实世界的要求只有在计算机世界中才得到真正的物理实现,而这种实现是通过信息世界逐步转化得到的。
数据是现实世界符号的抽象,而数据模型(data model)则是数据特征的抽象,它从抽象层次上描述了系统的静态特征、动态行为和约束条件,为数据库系统的信息表示与操作提供一个抽象的框架。数据模型所描述的内容有三个部分,它们是数据结构、数据操作与数据约束。
(1)数据结构。数据模型中的数据结构主要描述数据的类型、内容、性质以及数据间的联系等。数据结构是数据模型的基础,数据操作与约束均建立在数据结构上。不同数据结构有不同的操作与约束,因此,一般数据模型的分类均以数据结构的不同而分。
(2)数据操作。数据模型中的数据操作主要描述在相应数据结构上的操作类型与操作方式。
(3)数据约束。数据模型中的数据约束主要描述数据结构内数据间的语法、语义联系,它们之间的制约与依存关系,以及数据动态变化的规则,以保证数据的正确、有效与相容。
数据模型按不同的应用层次分成三种类型,它们是概念数据模型(conceptual data model)、逻辑数据模型(logic data model)、物理数据模型(physical data model)。
概念数据模型简称概念模型,它是一种面向客观世界、面向用户的模型;它与具体的数据库管理系统无关,与具体的计算机平台无关。概念模型着重于对客观世界复杂事物的结构描述及它们之间的内在联系的刻画。概念模型是整个数据模型的基础。目前,较为有名的概念模型有E-R模型、扩充的E-R模型、面向对象模型及谓词模型等。
逻辑数据模型又称数据模型,它是一种面向数据库系统的模型,该模型着重于在数据库系统一级的实现。概念模型只有在转换成数据模型后才能在数据库中得以表示。目前,逻辑数据模型也有很多种,较为成熟并先后被人们大量使用过的有:层次模型、网状模型、关系模型、面向对象模型等。
物理数据模型又称物理模型,它是一种面向计算机物理表示的模型,此模型给出了数据模型在计算机上物理结构的表示。
4.2.2E-R模型概念模型是面向现实世界的,它的出发点是有效和自然地模拟现实世界,给出数据的概念化结构。长期以来被广泛使用的概念模型是E-R模型(entity-relationship model)(或实体联系模型),它于1976年由Peter Chen首先提出。该模型将现实世界的要求转化成实体、联系、属性等几个基本概念,以及它们间的两种基本联接关系,并且可以用一种图非常直观地表示出来。
1.E-R模型的基本概念
(1)实体现实世界中的事物可以抽象成为实体,实体是概念世界中的基本单位,它们是客观存在的且又能相互区别的事物。凡是有共性的实体可组成一个集合称为实体集(entity set)。如小赵、小李是实体,他们又均是学生而组成一个实体集。
(2)属性现实世界中事物均有一些特性,这些特性可以用属性来表示。属性刻画了实体的特征。一个实体往往可以有若干个属性。每个属性可以有值,一个属性的取值范围称为该属性的值域(value domain)或值集(value set)。如小赵年龄取值为17,小李为19。
(3)联系现实世界中事物间的关联称为联系。在概念世界中联系反映了实体集间的一定关系,如工人与设备之间的操作关系,上、下级间的领导关系,生产者与消费者之间的供求关系。
实体集间的联系有多种,就实体集的个数而言有:
①两个实体集间的联系。两个实体集间的联系是一种最为常见的联系,前面举的例子均属两个实体集间的联系。
②多个实体集间的联系。这种联系包括三个实体集间的联系以及三个以上实体集间的联系。如工厂、产品、用户这三个实体集间存在着工厂提供产品为用户服务的联系。
③一个实体集内部的联系。一个实体集内有若干个实体,它们之间的联系称实体集内部联系。如某公司职工这个实体集内部可以有上、下级联系。
实体集间联系的个数可以是单个也可以是多个。如工人与设备之间有操作联系,另外还可以有维修联系。两个实体集间的联系实际上是实体集间的函数关系,这种函数关系可以有下面几种:
一对一(one t0 one)的联系,简记为1:1。这种函数关系是常见的函数关系之一,如学校与校长间的联系,一个学校与一个校长间相互一一对应。
一对多(one to many)或多对一(many to one)联系,简记为l:M(1:m)或M:1(m:1)。这两种函数关系实际上是一种函数关系,如学生与其宿舍房间的联系是多对一的联系(反之,则为一对多联系),即多个学生对应一个房间。
多对多(many to many)联系,简记为M:N或m:n。这是一种较为复杂的函数关系,如教师与学生这两个实体集间的教与学的联系是多对多的,因为一个教师可以教授多个学生,而一个学生又可以受教于多个教师。
2.E-R模型三个基本概念之间的联接关系
E-R模型由上面三个基本概念组成。由实体、联系、属性三者结合起来才能表示现实世界。
(1)实体集(联系)与属性间的联接关系实体是概念世界中的基本单位,属性附属于实体,它本身并不构成独立单位。一个实体可以有若干个属性,实体以及它的所有属性构成了实体的一个完整描述。因此实体与属性间有一定的联接关系。如在人事档案中每个人(实体).可以有:编号、姓名、性别、年龄、籍贯、政治面貌等若干属性,它们组成了一个有关人(实体)的完整描述。
属性有属性域,每个实体可取属性域内的值。一个实体的所有属性取值组成了一个值集叫元组(tuple)。在概念世界中,可以用元组表示实体,也可用它区别不同的实体。如在人事档案简表4.2中,每一行表示一个实体,这个实体可以用一组属性值表示。比如:(101,赵英俊,男,18,浙江,团员),(102,王平,男,21,江苏,党员),这两个元组分别表示两个不同的实体。
实体有型与值之别,一个实体的所有属性构成了这个实体的型,如人事档案中的实体,它的型是由编号、姓名、性别、年龄、籍贯、政治面貌等属性组成,而实体中属性值的集合(即元组)则构成了这个实体的值。
相同型的实体构成了实体集。如表4.2中的每一行是一个实体,它们均有相同的型,因此表内诸实体构成了一个实体集。
表4.2人事档案简表编 号
姓 名
性 别
年 龄
籍 贯
政治面貌
101
赵英俊
男
18
浙江
团员
102
王 平
男
21
江苏
党员
103
吴亦奇
女
20
辽宁
群众
104
刘 过
男
21
陕西
群众
105
李美丽
女
18
安徽
团员
联系也可以附有属性,联系和它的所有属性构成了联系的一个完整描述,因此,联系与属性间也有联接关系。如有教师与学生两个实体集间的教与学的联系,该联系尚可附有属性“教室号”。
(2)实体(集)与联系实体集间可通过联系建立联接关系,一般而言,实体集间无法建立直接关系,它只能通过联系才能建立起联接关系。如教师与学生之间无法直接建立关系,只有通过“教与学”的联系才能在相互之间建立关系。
在E-R模型中有三个基本概念以及它们之间的两种基本联接关系。它们将现实世界中的错综复杂的现象抽象成简单明了的几个概念与关系,具有极强的概括性和表达能力。因此,E—R模型目前已成为表示概念世界的有力工具。
3.E-R模型的图示法
E-R模型可以用一种非常直观的图的形式表示,这种图称为E-R图(entity-relationship diagram)。在E-R图中我们分别用下面不同的几何图形表示E-R模型中的三个概念与两个联接关系。
(1)实体集表示法在E-R图中用矩形表示实体集,在矩形内写上该实体集的名字。如实体集学生(student)、课程(course)可用图4.4表示。
(2)属性表示法在E-R图中用椭圆形表示属性,在椭圆形内写上该属性的名称。如学生有属性:学号(S#)、姓名(Sn)及年龄(Sa),它们可以用图4.5表示。
(3)联系表示法在E-R图中用菱形(内写上联系名)表示联系。如学生与课程间的联系SC,用图4.6表示。
三个基本概念分别用三种几何图形表示。它们之间的联接关系也可用图形表示。
(4)实体集(联系)与属性间的联接关系属性依附于实体集,因此,它们之间有联接关系。在E-R图中这种关系可用联接这两个图形间的无向线段表示(一般情况下可用直线)。如实体集student有属性S孝(学号)、Sn(学生姓名)及Sa(学生年龄);实体集course有属性C孝(课程号)、Cn(课程名)及P孝(预修课号),此时它们可用图4.7联接。
属性也依附于联系,它们之间也有联接关系,因此也可用无向线段表示。如联系SC可与学生的课程成绩属性G建立联接并可用图4.8表示。
(5)实体集与联系间的联接关系在E-R图中实体集与联系间的联接关系可用联接这两个图形间的无向线段表示。如实体集student与联系SC间有联接关系,实体集course与联系SC间也有联接关系,因此它们之间可用无向线段相联,构成一个如图4.9所示的图。
有时为了进一步刻画实体间的函数关系,还可在线段边上注明其对应函数关系,如l:1,1:n,n:m等,如student与course间有多对多联系,此时在图中可以用图4.10所示的形式表示。
实体集与联系间的联接可以有多种,上面所举例子均是两个实体集间联系叫二元联系,也可以是多个实体集间联系,叫多元联系。如工厂、产品与用户间的联系FPU是一种三元联系,此种联接关系可用图4.11表示。
一个实体集内部可以有联系。如某公司职工(employee)间上、下级管理(manage)的联系,此时,其联接关系可用图4.12(a)表示。
实体集间可有多种联系。如教师(T)与学生(S)之间可以有教学(E)联系也可有管理(M)联系,此种联接关系可用图4.12(b)表示。
由矩形、椭圆形、菱形以及按一定要求相互间联接的线段构成了一个完整的E-R图。
例4.1 由前面所述的实体集student,course以及附属于它们的属性和它们间的联系SC以及附属于SC的属性G构成了一个学生课程联系的概念模型,可用图4.13的E-R图表示。
在概念上,E-R模型中的实体、属性与联系是三个有明显区别的不同概念。但是在分析客观世界的具体事物时,对某个具体数据对象,究竟它是实体,还是属性或联系,则是相对的,所做的分析设计与实际应用的背景以及设计人员的理解有关。这是工程实践中构造E-R模型的难点之一。
4.2.3层次模型层次模型是最早发展起来的数据库模型。层次模型(hierarchical model)的基本结构是树形结构,这种结构方式在现实世界中很普遍,如家族结构、行政组织机构,它们自项向下、层次分明。图4.14给出了一个学校行政机构图的简化E-R图,略去了其中的属性。
由图论中树的性质可知,任一树结构均有下列特性:
(1)每棵树有且仅有一个无双亲结点,称为根(root)。
(2)树中除根外所有结点有且仅有一个双亲。因此,树结构是受到一定限制的,从E-R模型观点看,它对于联系也加上了许多限制。
层次数据模型支持的操作主要有查询、插入、删除和更新。在对层次模型进行插入、删除、更新操作时,要满足层次模型的完整性约束条件:进行插入操作时,如果没有相应的双亲结点值就不能插入子女结点值;进行删除操作时,如果删除双亲结点值,则相应的子女结点值也被同时删除;进行更新操作时,应更新所有相应记录,以保证数据的一致性。
层次模型的数据结构比较简单,操作简单;对于实体间联系是固定的、且预先定义好的应用系统,层次模型有较高的性能;同时,层次模型还可以提供良好的完整性支持。但由于层次模型形成早,受文件系统影响大,模型受限制多,物理成分复杂,操作与使用均不甚理想,它不适合于表示非层次性的联系;对于插入和删除操作的限制比较多;此外,查询子女结点必须通过双亲结点。
4.2.4网状模型网状模型(Network model)的出现略晚于层次模型。从图论观点看,网状模型是一个不加任何条件限制的无向图。网状模型在结构上较层次模型好,不像层次模型那样要满足严格的条件。图4.15是学校行政机构图中学校与学生联系的简化E-R图。
在实现中,网状模型将通用的网络拓扑结构分成一些基本结构。一般采用的分解方法是将一个网络分成若干个二级树,即只有两个层次的树。换句话说,这种树是由一个根及若干个叶所组成。为实现的方便,一般规定根结点与任一叶子结点间的联系均是一对多的联系(包含一对一联系)。
在网状模型的DBTG(data base task group)标准中,基本结构简单二级树叫系(set),系的基本数据单位是记录(record),它相当于E-R模型中的实体(集);记录又可由若干数据项(data item)组成,它相当于E-R模型中的属性。系有一个首记录(owner record),它相当于简单二级树的根;系同时有若干个成员记录(member record),它相当于简单二级树中的叶;首记录与成员记录之间的联系用有向线段表示(线段方向仅表示由首记录至成员记录的方向,而并不表示搜索方向),在系中首记录与成员记录间是一对多联系(包括一对一联系)。图4.16给出了一个系的实例。
一般地,现实世界的一个实体结构往往可以由若干个系组成。在网状模型的数据库管理系统中,一般提供DDL语言,用它可以构造系。网状模型中的基本操作是简单二级树中的操作,它包括查询、增加、删除、修改等操作,对于这些操作,不仅需要说明做什么,还需要说明怎么做。比如,在进行查询时,不但要说明查找对象,而且还要规定存取的路径。在DBTG报告中,提供了在系上进行操纵的DML语言,它们有包括打开(OPEN)、关闭(CLOSE)、定位(FIND)、取(GET)、删除(DELETE)、存储(STORE)等在内的许多操作。
网状模型明显优于层次模型,不管是数据表示或数据操纵均显示了更高的效率、更为成熟。但是,网状模型数据库系统也有一定的不足,在使用时涉及到系统内部的物理因素较多,用户操作使用并不方便,其数据模式与系统实现也均不甚理想。
4.2.5关系模型
1.关系的数据结构关系模型采用二维表来表示,简称表。二维表由表框架(Frame)及表的元组(Tuple)组成。表框架由n个命名的属性(Attribute)组成,n称为属性元数(Arity)。每个属性有一个取值范围称为值域(Domain)。表框架对应了关系的模式,即类型的概念。
在表框架中按行可以存放数据,每行数据称为元组,实际上,一个元组是由n个元组分量所组成,每个元组分量是表框架中每个属性的投影值。一个表框架可以存放m个元组,m称为表的基数(Cardinality)。
一个n元表框架及框架内m个元组构成了一个完整的二维表。表4.3给出了有关学生(s)二维表的一个实例。
二维表一般满足下面7个性质:
①二维表中元组个数是有限的——元组个数有限性。
②二维表中元组均不相同——元组的惟一性。
表4.3二维表的一个实例
S群
Sn
Sd
Sa
2001001
2001002
2001003
2001004
张浩然李一明王 伟赵坚强
EE
EE
EE
EE
18
19
18
20
③二维表中元组的次序可以任意交换——元组的次序无关性。
④二维表中元组的分量是不可分割的基本数据项——元组分量的原子性。
⑤二维表中属性名各不相同——属性名惟一性。
⑥二维表中属性与次序无关,可任意交换——属性的次序无关性。
⑦二维表属性的分量具有与该属性相同的值域——分量值域的同一性。
满足以上7个性质的二维表称为关系(Relation),以二维表为基本结构所建立的模型称为关系模型。
关系模型中的一个重要概念是键(Key)或码。键具有标识元组、建立元组间联系等重要作用。
在二维表中凡能惟一标识元组的最小属性集称为该表的键或码。
二维表中可能有若干个键,它们称为该表的候选码或候选键(Candidata Key)。
从二维表的所有候选键中选取一个作为用户使用的键称为主键(Primary key)或主码,一般主键也简称键或码。
表A中的某属性集是某表B的键,则称该属性集为A的外键(Foreign Key)或外码。
表中一定要有键,因为如果表中所有属性的子集均不是键,则表中属性的全集必为键(称为全键),因此也一定有主键。
在关系元组的分量中允许出现空值(Null Value)以表示信息的空缺。空值用于表示未知的值或不可能出现的值,一般用NULL表示。一般关系数据库系统都支持空值,但是有两个限制,即关系的主键中不允许出现空值,因为如主键为空值则失去了其元组标识的作用;需要定义有关空值的运算。
关系框架与关系元组构成了一个关系。一个语义相关的关系集合构成一个关系数据库(Relational Database)。关系的框架称为关系模式,而语义相关的关系模式集合构成了关系数据库模式(Relational Database Schema)。
关系模式支持子模式,关系子模式是关系数据库模式中用户所见到的那部分数据模式描述。关系子模式也是二维表结构,关系子模式对应用户数据库称视图(View)。
2.关系操纵关系模型的数据操纵即是建立在关系上的数据操纵,一般有查询、增加、删除及修改四种操作。
(1)数据查询用户可以查询关系数据库中的数据,它包括一个关系内的查询以及多个关系间的查询。
①对一个关系内查询的基本单位是元组分量,其基本过程是先定位后操作。所谓定位包括纵向定位与横向定位两部分,纵向定位即是指定关系中的一些属性(称列指定),横向定位即是选择满足某些逻辑条件的元组(称行选择)。通过纵向与横向定位后一个关系中的元组分量即可确定了。在定位后即可进行查询操作,就是将定位的数据从关系数据库中取出并放入至指定内存。
②对多个关系间的数据查询则可分为三步:第一步,将多个关系合并成一个关系;第二步,对合并后的一个关系作定位;第三步,操作。其中第二步与第三步为对一个关系的查询。对多个关系的合并可分解成两个关系的逐步合并,如有三个关系R1,R2与R3,合并过程是先将R1与R2合并成R4,然后再将R4与R3合并成最终结果R5。
因此,对关系数据库的查询可以分解成一个关系内的属性指定、一个关系内的元组选择、两个关系的合并三个基本定位操作以及一个查询操作。
(2)数据删除数据删除的基本单位是一个关系内的元组,它的功能是将指定关系内的指定元组删除。它也分为定位与操作两部分,其中定位部分只需要横向定位而无需纵向定位,定位后即执行删除操作。因此数据删除可以分解为一个关系内的元组选择与关系中元组删除两个基本操作。
(3)数据插入数据插入仅对一个关系而言,在指定关系中插入一个或多个元组。在数据插入中不需定位,仅需做关系中元组插入操作,因此数据插入只有一个基本操作。
(4)数据修改数据修改是在一个关系中修改指定的元组与属性。数据修改不是一个基本操作,它可以分解为删除需修改的元组与插入修改后的元组两个更基本的操作。
以上四种操作的对象都是关系,而操作结果也是关系,因此都是建立在关系上的操作。这四种操作可以分解成六种基本操作,称为关系模型的基本操作:
①关系的属性指定;
②关系的元组选择;
③两个关系合并;
④一个或多个关系的查询;
⑤关系中元组的插入;
⑥关系中元组的删除。
3.关系中的数据约束关系模型允许定义三类数据约束,它们是实体完整性约束、参照完整性约束以及用户定义的完整性约束,其中前两种完整性约束由关系数据库系统自动支持。对于用户定义的完整性约束,则由关系数据库系统提供完整性约束语言,用户利用该语言写出约束条件,运行时由系统自动检查。
(1)实体完整性约束(Entity Integrity Constraint)
该约束要求关系的主键中属性值不能为空值,这是数据库完整性的最基本要求,因为主键是惟一决定元组的,如为空值则其惟一性就成为不可能的了。
(2)参照完整性约束(Reference Integrity Constraint)
该约束是关系之间相关联的基本约束,它不允许关系引用不存在的元组:即在关系中的外键要么是所关联关系中实际存在的元组,要么就为空值。比如在关系s(S#、SN、SD、SA)与SC(s#、C#、G)中,SC中主键为(S#,C#)而外键为S#,SC与S通过S#相关联,参照完整性约束要求SC中的S#的值必在S中有相应元组值,如有SC(S13,C8,70),则必在S中存在S(S13,…)。
(3)用户定义的完整性约束(User defined Integrity Constraint)
这是针对具体数据环境与应用环境由用户具体设置的约束,它反映了具体应用中数据的语义要求。
实体完整性约束和参照完整性约束是关系数据库所必需遵守的规则,在任何一个关系数据库管理系统(RDBMS)中均由系统自动支持。
4.3 关系代数关系数据库系统的特点之一是它建立在数学理论的基础之上,有很多数学理论可以表示关系模型的数据操作,其中最为著名的是关系代数(relational algebra)与关系演算(relational calculus)。数学上已经证明两者在功能上是等价的。下面将介绍关于关系数据库的理论——关系代数。
1.关系模型的基本操作关系是由若干个不同的元组所组成,因此关系可视为元组的集合。n元关系是一个n元有序组的集合。
设有一个n元关系R,它有n个域,分别是D1,D2,…,Dn,此时,它们的笛卡尔积是:
该集合的每个元素都是具有如下形式的n元有序组:
该集合与n元关系R有如下联系:
即n元关系R是n元有序组的集合,是它的域的笛卡尔积的子集。
关系模型有插入、删除、修改和查询四种操作,它们又可以进一步分解成六种基本操作:
①关系的属性指定。指定一个关系内的某些属性,用它确定关系这个二维表中的列,它主要用于检索或定位。
②关系的元组的选择。用一个逻辑表达式给出关系中所满足此表达式的元组,用它确定关系这个二维表的行,它主要用于检索或定位。
用上述两种操作即可确定一张二维表内满足一定行、列要求的数据。
③两个关系的合并。将两个关系合并成一个关系。用此操作可以不断合并从而可以将若干个关系合并成一个关系,以建立多个关系间的检索与定位。
用上述三个操作可以进行多个关系的定位。
④关系的查询。在一个关系或多个关系间做查询,查询的结果也为关系。
⑤关系元组的插入。在关系中增添一些元组,用它完成插入与修改。
⑥关系元组的删除。在关系中删除一些元组,用它完成删除与修改。
2.关系模型的基本运算由于操作是对关系的运算,而关系是有序组的集合,因此,可以将操作看成是集合的运算。
(1)插入设有关系R需插入若干元组,要插入的元组组成关系,则插入可用集合并运算表示为:
(2)删除设有关系R需删除一些元组,要删除的元组组成关系,则删除可用集合差运算表示为:
(3)修改修改关系R内的元组内容可用下面的方法实现:
①设需修改的元组构成关系,则先做删除得:
②设修改后的元组构成关系R”,此时将其插入即得到结果:
(4)查询用于查询的三个操作无法用传统的集合运算表示,需要引入一些新的运算。
①投影(Projection)运算对于关系内的域指定可引入新的运算叫投影运算。投影运算是一个一元运算,一个关系通过投影运算(并由该运算给出所指定的属性)后仍为一个关系。是这样一个关系,它是R中投影运算所指出的那些域的列所组成的关系。设R有n个域:,则在R上对域的投影可表示成为下面的一元运算:
②选择(selection)运算选择运算也是一个一元运算,关系R通过选择运算(并由该运算给出所选择的逻辑条件)后仍为一个关系。这个关系是由R中那些满足逻辑条件的元组所组成。设关系的逻辑条件为F,则R满足F的选择运算可写成为:
逻辑条件F是一个逻辑表达式,它由下面的规则组成。
它可以具有的形式,其中,是域(变量)或常量,但,又不能同为常量,是比较符,它可以是<,>,≤,≥,=及≠。叫基本逻辑条件。
由若干个基本逻辑条件经逻辑运算得到,逻辑运算为∧(并且)、∨(或者)及~(否)构成,称为复合逻辑条件。
有了上述两个运算后,我们对一个关系内的任意行、列的数据都可以方便地找到。
③笛卡尔积(Cartesian Product)运算对于两个关系的合并操作可以用笛卡尔积表示。设有n元关系R及m元关系S,它们分别有p、q个元组,则关系R与S经笛卡尔积记为R×S,该关系是一个n+m元关系,元组个数是p×q,由R与S的有序组组合而成。
表4.4给出了两个关系R、S的实例以及R与S的笛卡尔积T=R×S。
3.关系代数中的扩充运算关系代数中除了上述几个最基本的运算外,为操纵方便还需增添一些运算,这些运算均可由基本运算导出。常用的扩充运算有交、除、连接及自然连接等。
(1)交(intersection)运算关系R与S经交运算后所得到的关系是由那些既在R内又在S内的有序组所组成,记为R∩S。
表4.5给出了两个关系R与S及它们经交运算后得到的关系T。
交运算可由基本运算推导而得:
(2)除(division)运算如果将笛卡尔积运算看作乘运算的话,那么除运算就是它的逆运算。当关系T=R×s时,则可将除运算写成为:
S称为T除以R的商(quotient)。
由于除是采用的逆运算,因此除运算的执行是需要满足一定条件的。设有关系T、R,T能被除的充分必要条件是:T中的域包含R中的所有属性;T中有一些域不出现在R中。
在除运算中s的域由T中那些不出现在R中的域所组成,对于s中任一有序组,由它与关系R中每个有序组所构成的有序组均出现在关系T中。
表4.6给出了关系R及一组S,对这一组不同的S给出了经除法运算后的商R/S,从中可以清楚地看出除法的含义及商的内容。
除法运算不是基本运算,它可以由基本运算推导而出。设关系R有域,关系S有域,此时有:
除法的定义虽然较复杂,但在实际中,除法的意义还是比较容易理解的。
例4.2设关系R给出了学生修读课程的情况,关系S给出了所有课程号,分别如表4.7(a)、(b)所示。试找出修读所有课程的学生号。
解:修读所有课程的学生号可用T=R/S表示,结果如表4.7(c)所示。
(3)连接(join)与自然连接(naturaljoin)运算在数学上,可以用笛卡尔积建立两个关系间的连接,但这样得到的关系庞大,而且数据大量冗余。在实际应用中一般两个相互连接的关系往往须满足一些条件,所得到的结果也较为简单。这样就引入了连接运算与自然连接运算。
连接运算又可称为θ—连接运算,这是一种二元运算,通过它可以将两个关系合并成一个大关系。设有关系R、S以及比较式iθj,其中i为R中的域,j为s中的域,θ含义同前。则可以将R、S在域i,j上的θ连接记为:
它的含义可用下式定义:
即R与S的θ连接是由R与S的笛卡尔积中满足限制iθj的元组构成的关系,一般其元组的数目远远少于R×S的数目。应当注意的是,在θ连接中,i与j需具有相同域,否则无法作比较。
在θ连接中如果θ为“=”,就称此连接为等值连接,否则称为不等值连接;如θ为“<”时称为小于连接;如θ为“>”时称为大于连接。
例4.3设有关系R、S分别如表4.8(a)、(b)所示,则为表4.8(c)所示的关系,而为如表4.8(d)所示的关系。
在实际应用中最常用的连接是一个叫自然连接的特例。它满足下面的条件:
①两关系间有公共域;
②通过公共域的相等值进行连接。
设有关系R、S,R有域,S有域,并且,与分别为相同域,此时它们自然连接可记为:
自然连接的含义可用下式表示:
例4.4设关系R、S如表4.9(a)、(b)所示,则如表4.9(c)所示。
在以上运算中最常用的是投影运算、选择运算、自然连接运算、并运算及差运算。
4.关系代数的应用实例关系代数虽然形式简单,但它已经足以表达对表的查询、插入、删除及修改等要求。在所有这些操作中,查询是最复杂的操作。在20世纪70年代,关系数据库系统始终无法走向商品化,最主要的原因就是它的查询效率低下。关系数据库的查询语言一般是非过程语言,即仅仅说明要查询的要求,而不说明如何去进行查询。最终,通过查询优化技术解决了此问题,而对于查询语句(即代数表达式)本身的优化即代数优化是最基本的技术。下面通过一个例子来体会一下如何将关系代数应用于查询。
例4.5建立一个学生选课的关系数据库,它由下面三个关系模式组成:
S(S#,Sn,Sd,Sa);
C(C#,Cn,P#);
SC(S#,C#,G)。
其中S#,C#,Sn,Sd,Sa,Cn,P#,G分别表示学号、课程号、学生姓名、学生系别、学生年龄、课程名、预修课程号、成绩,而S,C,SC则分别表示学生、课程、学生选课关系。
写出对关系模式S、C和SC中的下述查询表达式:
(1)检索学生所有情况:
(2)检索学生年龄大于等于20岁的学生姓名:
(3)检索预修课号为C2的课程的课程号:
(4)检索课程号为C,且成绩为A的所有学生姓名:
注意:这是一个涉及到两个关系的检索,此时需用连接运算。
(5)检索s1所修读的所有课程名及其预修课号:
(6)检索年龄为23岁的学生所修读的课程名:
注意:这是涉及到三个关系的检索。
(7)检索至少修读S,所修读的一门课的学生姓名。
这个例子比较复杂,需作一些分析。将问题分以下三步解决:
第1步:取得S,修读的课程号,它可以表示为:
第2步:取得至少修读为S,修读的一门课的学号:
第3步:最后结果为:
分别将R、w代入后即得检索要求之表达式:
对于一般较为复杂的查询,都是通过这样多步来解决的。注意到该过程中会产生一些中间的表,而查询优化中一般应尽可能使这些中间表比较小。
4.4 数据厍设计与管理数据库设计是数据库应用的核心。本节讨论数据库设计的任务特点、基本步骤和方法,重点介绍数据库的需求分析、概念设计及逻辑设计三个阶段,并用实际例子说明如何进行相关的设计。此外本节还简单讨论数据库管理的内容及DBA的工作。
4.4.1数据库设计概述在数据库应用系统中的一个核心问题就是设计一个能满足用户要求,性能良好的数据库,这就是数据库设计(Database design)。
数据库设计的基本任务是根据用户对象的信息需求、处理需求和数据库的支持环境(包括硬件、操作系统与DBMS)设计出数据模式。所谓信息需求主要是指用户对象的数据及其结构,它反映了数据库的静态要求;所谓处理需求则表示用户对象的行为和动作,它反映了数据库的动态要求。数据库设计中有一定的制约条件,它们是系统设计平台,包括系统软件、工具软件以及设备、网络等硬件。因此,数据库设计即是在一定平台制约下,根据信息需求与处理需求设计出性能良好的数据模式。
在数据库设计中有两种方法,一种是以信息需求为主,兼顾处理需求,称为面向数据的方法(data-oriented approach);另一种方法是以处理需求为主,兼顾信息需求,称为面向过程的方法(process-oriented approach)。这两种方法目前都有使用,在早期由于应用系统中处理多于数据,因此以面向过程的方法使用较多,而近期由于大型系统中数据结构复杂、数据量庞大,而相应处理流程趋于简单,因此用面向数据的方法较多。由于数据在系统中稳定性高,数据己成为系统的核心,因此面向数据的设计方法已成为主流方法。
数据库设计目前一般采用生命周期(1ife cycle)法,即将整个数据库应用系统的开发分解成目标独立的若干阶段。它们是:需求分析阶段、概念设计阶段、逻辑设计阶段、物理设计阶段、编码阶段、测试阶段、运行阶段、进一步修改阶段。在数据库设计中采用上面几个阶段中的前四个阶段,并且重点以数据结构与模型的设计为主线,如图4.17所示。
4.4.2数据库设计的需求分析需求收集和分析是数据库设计的第一阶段,这一阶段收集到的基础数据和一组数据流图(Data Flow Diagram简记为DFD)是下一步设计概念结构的基础。概念结构是整个组织中所有用户关心的信息结构,对整个数据库设计具有深刻影响。而要设计好概念结构,就必须在需求分析阶段用系统的观点来考虑问题、收集和分析数据及其处理。
需求分析阶段的任务是通过详细调查现实世界要处理的对象(组织、部门、企业等),充分了解原系统的工作概况,明确用户的各种需求,然后在此基础上确定新系统的功能。新系统必须充分考虑今后可能的扩充和改变,不能仅按当前应用需求来设计数据库。
调查的重点是“数据”和“处理”,通过调查要从中获得每个用户对数据库的如下要求:
①信息要求。指用户需要从数据库中获得信息的内容与性质。由信息要求可以导出数据要求,即在数据库中需存储哪些数据。
②处理要求。指用户要完成什么处理功能,对处理的响应时间有何要求,处理的方式是批处理还是联机处理。
③安全性和完整性的要求。
为了很好地完成调查的任务,设计人员必须不断地与用户交流,与用户达成共识,以便逐步确定用户的实际需求,然后分析和表达这些需求。需求分析是整个设计活动的基础,也是最困难、最花时间的一步。需求分析人员既要懂得数据库技术,又要对应用环境的业务比较熟悉。
分析和表达用户的需求,经常采用的方法有结构化分析方法和面向对象的方法。结构化分析(structured Analysis,简称SA方法)方法用自顶向下、逐层分解的方式分析系统。用数据流图表达了数据和处理过程的关系,数据字典对系统中数据的详尽描述,是各类数据属性的清单。对数据库设计来讲,数据字典是进行详细的数据收集和数据分析所获得的主要结果。
数据字典是各类数据描述的集合,它通常包括5个部分,即数据项,是数据的最小单位;数据结构,是若干数据项有意义的集合;数据流,可以是数据项,也可以是数据结构,表示某一处理过程的输入或输出;数据存储,处理过程中存取的数据,常常是手工凭证、手工文档或计算机文件;处理过程。
数据字典是在需求分析阶段建立,在数据库设计过程中不断修改、充实、完善的。
在实际开展需求分析工作时有两点需要特别注意:
第一,在需求分析阶段一个重要而困难的任务是收集将来应用所涉及的数据。若设计人员仅仅按当前应用来设计数据库,新数据的加入不仅会影响数据库的概念结构,而且将影响逻辑结构和物理结构,因此设计人员应充分考虑到可能的扩充和改变,使设计易于更动。
第二,必须强调用户的参与,这是数据库应用系统设计的特点。数据库应用系统和广泛的用户有密切的联系,其设计和建立又可能对更多人的工作环境产生重要影响。因而,设计人员应该和用户充分合作进行设计,并对设计工作的最后结果承担共同的责任。
4.4.3数据库概念设计
1.数据厍概念设计概述数据库概念设计的目的是分析数据间内在语义关联,在此基础上建立一个数据的抽象模型。数据库概念设计的方法有以下两种:
(1)集中式模式设计法这是一种统一的模式设计方法,它根据需求由一个统一机构或人员设计一个综合的全局模式。这种方法设计简单方便,它强调统一与一致,适用于小型或并不复杂的单位或部门,而对大型的或语义关联复杂的单位则并不适合。
(2)视图集成设计法这种方法是将一个单位分解成若干个部分,先对每个部分作局部模式设计,建立各个部分的视图,然后以各视图为基础进行集成。在集成过程中可能会出现一些冲突,这是由于视图设计的分散性形成的不一致所造成的,因此需对视图作修正,最终形成全局模式。
视图集成设计法是一种由分散到集中的方法,它的设计过程复杂但它能较好地反映需求,适合于大型与复杂的单位,避免设计的粗糙与不周到,目前此种方法使用较多。
2.数据库概念设计的过程使用E-R模型与视图集成法进行设计时,需要按以下步骤进行:首先选择局部应用,再进行局部视图设计,最后对局部视图进行集成得到概念模式。
(1)选择局部应用根据系统的具体情况,在多层的数据流图中选择一个适当层次的数据流图,让这组图中每一部分对应一个局部应用,以这一层次的数据流图为出发点,设计分E-R图。
(2)视图设计视图设计一般有三种设计次序,它们是:
①自顶向下。这种方法是先从抽象级别高且普遍性强的对象开始逐步细化、具体化与特殊化,如学生这个视图可先从一般学生开始,再分成大学生、研究生等,进一步再由大学生细化为大学本科与专科,研究生细化为硕士生与博士生等,还可以再细化成学生姓名、年龄、专业等细节。
②由底向上。这种设计方法是先从具体的对象开始,逐步抽象,普遍化与一般化,最后形成一个完整的视图设计。
③由内向外。这种设计方法是先从最基本与最明显的对象着手逐步扩充至非基本、不明显的其他对象,如学生视图可从最基本的学生开始逐步扩展至学生所读的课程、上课的教室与任课的教师等其他对象。
上面3种方法为视图设计提供了具体的操作方法,设计者可根据实际情况灵活掌握,可以单独使用也可混合使用。有某些共同特性和行为的对象可以抽象为一个实体。对象的组成成分可以抽象为实体的属性。
在进行设计时,实体与属性是相对而言的。同一事物,在一种应用环境中作为“属性”,在另一种应用环境中就必须作为“实体”。但是,在给定的应用环境中,属性必须是不可分的数据项,属性不能与其他实体发生联系,联系只发生在实体之间。
例4.6学籍管理局部应用中主要涉及的实体包括学生、宿舍、档案材料、班级、班主任。这些实体之间的联系有:
①一个宿舍可以住多个学生,一个学生只能住在一个宿舍中,因此宿舍与学生之间是l:N的联系。
②一个班有若干名学生,一个学生只能属于一个班级,因此班级与学生之间也是1:N的联系。
③班主任与学生之间是1:N的联系。
④学生和他自己的档案材料之间是1:1的联系。
⑤班级与班主任之间都是1:1的联系。
于是,省略了实体的属性后学籍管理的E-R图如图4.18所示。
对应于各实体的属性分别为:
学生:{学号,姓名,出生日期,所在系,何时入学,平均成绩}
档案材料:{档案号,…}
班级:{班级号,学生人数}
班主任:{职工号,姓名,性别,是否为优秀班主任}
宿舍:{宿舍编号,地址,人数}
教室:{教室编号,地址,容量}
其中有下划线的属性为实体的码。
例4.7课程管理局部视图的设计:在这一视图中共有五个实体,分别是学生、课程、教室、教师及教科书,描述这些实体的属性分别为:
学生:{学号,姓名,年龄,性别,入学时间}
课程:{课程号,课程名,学时数}
选修:{学号,课程号,成绩}
教科书:{书号,书名,ISBN,作者,出版时间,关键字}
教室:{教室编号,地址,容量}
同样,省略了实体的属性后课程管理的E-R图如图4.19所示。
(3)视图集成视图集成的实质是将所有的局部视图统一与合并成一个完整的数据模式。在进行视图集成时,最重要的工作便是解决局部设计中的冲突。在集成过程中由于每个局部视图在设计时的不一致性因而会产生矛盾,引起冲突,常见冲突有下列几种:
①命名冲突。命名冲突有同名异义和同义异名两种。如上面的实例中学生属性“何时入学”与“入学时间”属同义异名。
②概念冲突。同一概念在一处为实体而在另一处为属性或联系。
③域冲突。相同的属性在不同视图中有不同的域,如学号在某视图中的域为字符串而在另一个视图中可为整数,有些属性采用不同度量单位也属域冲突。
④约束冲突。不同的视图可能有不同的约束。
视图经过合并生成的是初步E-R图,其中可能存在冗余的数据和冗余的实体间联系。冗余数据和冗余联系容易破坏数据库的完整性,给数据库维护增加困难。因此,对于视图集成后所形成的整体的数据库概念结构还必须进行进一步验证,确保它能够满足下列条件:
整体概念结构内部必须具有一致性,即不能存在互相矛盾的表达;
整体概念结构能准确地反映原来的每个视图结构,包括属性、实体及实体间的联系;
整体概念结构能满足需求分析阶段所确定的所有要求;
整体概念结构最终还应该提交给用户,征求用户和有关人员的意见,进行评审、修改和优化,然后把它确定下来,作为数据库的概念结构,作为进一步设计数据库的依据。
例4.8学籍管理局部视图与课程管理局部视图的集成。
根据上面所述的方法,集成过程可按下面步骤进行:
①消除冲突这两个子E-R图存在着多方面的冲突:
1)班主任也属于教师,学籍管理中的班主任实体与课程管理中的教师实体属于异名同义,可以统一称为教师。
2)将班主任改为教师后,教师与学生之间呈现两种不同类型的联系:指导联系和教学联系。由于指导联系实际上可以包含在教学联系之中,因此可以将这两种联系综合为教学联系。
3)调整学生实体属性组成及次序,调整结果可为:
学生:{学号,姓名,出生日期,年龄,所在系,年级,平均成绩}
②消除冗余
1)学生实体中的年龄可以由出生日期推算出来,属于冗余数据。
学生:{学号,姓名,出生日期,所在系,年级,平均成绩}
2)教室实体与班级实体之间的上课联系可以由教室与课程之间的开设联系、课程与学生之间的选修联系、学生与班级之间的组成联系三者推导出来,因此属于冗余联系。
3)学生实体中的平均成绩可以从选修联系中的成绩属性中推算出来。
如果需要经常查询学生的平均成绩,可以考虑保留该冗余数据,提高效率。但是为了维护数据一致性,应采用一定的机制以保持数据的一致。
这样,集成这两个子E-R图后的学生管理予系统的E-R图如图4.20所示。
学生管理子系统的基本E-R图还必须进一步和教师管理子系统以及后勤管理子系统等的基本E-R图合并,才能生成整个学校管理系统的基本E-R图。
4.4.4数据库的逻辑设计
1.从E-R图向关系模式转换数据库的逻辑设计主要工作是将E-R图转换成指定RDBMS中的关系模式。首先,从E-R图到关系模式的转换是比较直接的,实体与联系都可以表示成关系,E-R图中属性也可以转换成关系的属性。实体集也可以转换成关系。E-R模型与关系间的转换如表4.10所示。
表4.10 E—R模型与关系间的比较表
E-R模型
关 系
E-R模型
关 系
属性实体
属性元组
实体集联系
关系关系
下面讨论由E-R图转换成关系模式时会遇到的一些转换问题。
(1)命名与属性域的处理关系模式中的命名可以用E-R图中原有命名,也可另行命名,但是应尽量避免重名,RDBMS一般只支持有限种数据类型而E-R中的属性域则不受此限制,如出现有RDBMS不支持的数据类型时则要进行类型转换。
(2)非原子属性处理
E-R图中允许出现非原子属性,但在关系模式中一般不允许出现非原子属性,非原子属性主要有集合型和元组型。如出现此种情况时可以进行转换,其转换办法是集合属性纵向展开而元组属性则横向展开。
例4.9学生实体有学号、学生姓名及选读课程,其中前两个为原子属性而后一个为集合型非原子属性,因为一个学生可选读若干课程,设有学生S1307,王承志,他修读Database,Operating System及Computer Network三门课,此时可将其纵向展开用关系形式如表4.11所示。
表4.11学生实体学 号
学生姓名
选读课程
S1307
S1307
S1307
王承志王承志王承志
Database
Operating System
Computer Network
(3)联系的转换在一般情况下联系可用关系表示,但是在有些情况下联系可归并到相关联的实体中。
2.逻辑模式规范化及调整、实现
(1)规范化在逻辑设计中还需对关系做规范化验证。
(2)RDBMS
对逻辑模式进行调整以满足RDBMS的性能、存储空间等要求,同时对模式做适应RDBMS限制条件的修改,它们包括如下内容:
①调整性能以减少连接运算;
②调整关系大小,使每个关系数量保持在合理水平,从而可以提高存取效率;
③尽量采用快照(snapshot),因在应用中经常仅需某固定时刻的值,此时可用快照将某时刻值固定,并定期更换,此种方式可以显著提高查询速度。
3.关系视图设计逻辑设计的另一个重要内容是关系视图的设计,它又称为外模式设计。关系视图是在关系模式基础上所设计的直接面向操作用户的视图,它可以根据用户需求随时创建,一般RDBMS均提供关系视图的功能。
关系视图的作用大致有如下几点:
(1)提供数据逻辑独立性:使应用程序不受逻辑模式变化的影响。数据的逻辑模式会随着应用的发展而不断变化,逻辑模式的变化必然会影响到应用程序的变化,这就会产生极为麻烦的维护工作。关系视图则起了逻辑模式与应用程序之间的隔离墙作用,有了关系视图后建立在其上的应用程序就不会随逻辑模式修改而产生变化,此时变动的仅是关系视图的定义。
(2)能适应用户对数据的不同需求:每个数据库有一个非常庞大的结构,而每个数据库用户则希望只知道他们自己所关心的那部分结构,不必知道数据的全局结构以减轻用户在此方面的负担。此时,可用关系视图屏蔽用户所不需要的模式,而仅将用户感兴趣的部分呈现出来。
(3)有一定数据保密功能:关系视图为每个用户划定了访问数据的范围,从而在应用的各用户间起了一定的保密隔离作用。
4.4.5数据库的物理设计数据库物理设计的主要目标是对数据库内部物理结构作调整并选择合理的存取路径,以提高数据库访问速度及有效利用存储空间。在现代关系数据库中已大量屏蔽了内部物理结构,因此留给用户参与物理设计的余地并不多,一般的RDBMS中留给用户参与物理设计的内容大致有如下几种:索引设计、集簇设计和分区设计。
4.4.6数据库管理数据库是一种共享资源,它需要维护与管理,这种工作称为数据库管理(Database administration),而实施此项管理的人则称为数据库管理员(Database administrator)简称DBA。数据库管理一般包含如下一些内容:数据库的建立、数据库的调整、数据库的重组、数据库的安全性控制与完整性控制、数据库的故障恢复和数据库的监控。下面对这些管理内容作简单讨论。
1.数据库的建立数据库的建立包括两部分内容,数据模式的建立及数据加载。
(1)数据模式建立数据模式由DBA负责建立,DBA利用RDBMS中的DDL语言定义数据库名,定义表及相应属性,定义主关键字、索引、集簇、完整性约束、用户访问权限,申请空间资源,定义分区等,此外还需定义视图。
(2)数据加载在数据模式定义后即可加载数据,DBA可以编制加载程序将外界数据加载至数据模式内,从而完成数据库的建立。
2.数据库的调整在数据库建立并经一段时间运行后往往会产生一些不适应的情况,此时需要对其作调整,数据库的调整一般由DBA完成,调整包括下面一些内容:
(1)调整关系模式与视图使之更能适应用户的需求;
(2)调整索引与集簇使数据库性能与效率更佳;
(3)调整分区、数据库缓冲区大小以及并发度使数据库物理性能更好。
3.数据库的重组数据库在经过一定时间运行后,其性能会逐步下降,下降的原因主要是由于不断的修改、删除与插入所造成的。由于不断的删除而造成盘区内废块的增多而影响I/O速度,由于不断的删除与插入而造成集簇的性能下降,同时也造成了存储空间分配的零散化,使得一个完整表的空间分散,从而造成存取效率下降。基于这些原因需要对数据库进行重新整理,重新调整存贮空间,此种工作叫数据库重组。一般数据库重组需花大量时间,并做大量的数据搬迁工作。实际中,往往是先做数据卸载,然后再重新加载从而达到数据重组的目的。目前一般RDBMS都提供一定手段,以实现数据重组功能。
4.数据库安全性控制与完整性控制数据库是一个单位的重要资源,它的安全性是极端重要的,DBA应采取措施保证数据不受非法盗用与破坏。此外,为保证数据的正确性,使录入库内的数据均能保持正确,需要有数据库的完整性控制。
5.数据库的故障校复一旦数据库中的数据遭受破坏,需要及时进行恢复,RDBMS一般都提供此种功能,并由DBA负责执行故障恢复功能。
6.数据库监控
DBA需随时观察数据库的动态变化,并在发生错误、故障或产生不适应情况时随时采取措施,如数据库死锁、对数据库的误操作等;同时还需监视数据库的性能变化,在必要时对数据库作调整。
习 题 4
一、选择题
1.在数据管理技术的发展过程中,经历了人工管理阶段、文件系统阶段和数据库系统阶段。其中数据独立性最高的阶段是
A)数据库系统 B)文件系统 c)人工管理 D)数据项管理
2.下述关于数据库系统的叙述中正确的是
A)数据库系统减少了数据冗余
B)数据库系统避免了一切冗余
C)数据库系统中数据的一致性是指数据类型一致
D)数据库系统比文件系统能管理更多的数据
3.数据库系统的核心是
A)数据库 B)数据库管理系统
C)数据模型 D)软件工具
4.用树形结构来表示实体之间联系的模型称为
A)关系模型 B)层次模型 c)网状模型 D)数据模型
5.关系表中的每一横行称为一个
A)元组 B)字段 c)属性 D)码
6.按条件f对关系R进行选择,其关系代数表达式是
A) B) C) D)
7.关系数据库管理系统能实现的专门关系运算包括
A)排序、索引、统计 B)选择、投影、连接
c)关联、更新、排序 D)显示、打印、制表
8.在关系数据库中,用来表示实体之间联系的是
A)树结构 B)网结构 C)线性表D)--维表
9.数据库设计包括两个方面的设计内容,它们是
A)概念设计和逻辑设计 B)模式设计和内模式设计
C)内模式设计和物理设计 D)结构特性设计和行为特性设计
10.将E-R图转换到关系模式时,实体与联系都可以表示成
A)属性 B)关系 C)键 D)域二、填空题
1.一个项目具有一个项目主管,一个项目主管可管理多个项目,则实体“项目主管”与实体“项目”的联系属于________的联系。
2.数据独立性分为逻辑独立性与物理独立性。当数据的存储结构改变时,其逻辑结构可以不变,因此,基于逻辑结构的应用程序不必修改,称为________。
3.数据库系统中实现各种数据管理功能的核心软件称为________。
4.关系模型的完整性规则是对关系的某种约束条件,包括实体完整性、________和自定义完整性。
5.在关系模型中,把数据看成一个二维表,每一个二维表称为一个________。
习题参考答案习题1参考答案一、选择题
1.C) 2.D) 3.A) 4.B) 5.C) 6.D) 7.B) 8.B) 9.B) 10.A)
二、填空题
1.log2n 2.350 3.DEBFCA 4.n(n-1)/2 5.3
习题2参考答案一、选择题
1.B) 2.A) 3.D) 4.B) 5,A)
二、填空题
1.重复(或循环) 2.功能性 3.封装 4.实例 5.继承
习题3参考答案一、选择题
1.D) 2.D) 3.A) 4.A) 5.C) 6.B) 7.D) 8.C) 9.B) 10.B)
二、填空题
1.文档 2.数据流 3.软件开发 4.变换型 5.软件工具
习题4参考答案一、选择题
1.A) 2.A) 3.B) 4.B) 5.A) 6.C) 7.B) 8.D) 9.A) 10.B)
二、填空题
1.一对多(或1:N) 2.逻辑独立性 3.数据库管理系统 4.参照完整性 5.关系