数据结构(C语言版)严蔚敏 吴伟民 编著 第1章 绪论
数据结构(C语言版)严蔚敏 吴伟民 编著 第1章 绪论
1.1 什么是数据结构?
用计算机解决一个具体问题时,大致需要经过下列几个步骤:首先要从具体问题抽象出一个适当的数学模型,然后设计一个解此数学模型的算法,最后编出程序,进行调试,调整直至得到最终解答。寻找数学模型的实质是分析问题,从中提取操作的对象,并找出这些操作对象之间含有的关系,然后用数学的语言加以描述。描述这类非数值问题的数学模型不再是数学方程,而是诸如表、树和图之类的数据结构。因此,简单来说,数据结构是一门研究非数值计算的程序设计问题中计算机的操作对象以及它们之间的关系和操作等的学科。
“数据结构”的研究不仅涉及到计算机硬件(特别是编码理论、存储装置和读取方法等)的研究范围,而且和计算机软件的研究有着更密切的关系,无论是编译程序还是操作系统,都涉及到数据元素在存储器中的分配问题。在研究信息检索时也必须考虑如何组织数据,以便查找和存取数据元素更为方便。因此,可以认为“数据结构”是介于数学、计算机硬件和计算机软件三者之间的一门核心课程。在计算机科学中,“数据结构”不仅是一般程序设计(特别是非数值计算的程序设计)的基础,而且是设计和实现编译程序、操作系统、数据库系统及其他系统程序和大型应用程序的重要基础。
1.2 基本概念和术语
- 数据
在计算机科学中是指所有能输入到计算机中并被计算机程序处理的符号的总称。它是计算机程序加工的“原料”。 - 数据元素
是数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理。有时,一个数据元素可由若干个数据项组成。 - 数据对象
是性质相同的数据元素的集合,是数据的一个子集。 - 数据结构
是相互之间存在一种或多种特定关系的数据元素的集合。
根据元素之间关系的不同特性,通常有以下4类基本结构:
(1)集合
结构中数据元素之间除了“同属于一个集合”关系外,别无其他关系
(2)线性结构
结构中数据元素之间存在一个对一个的关系
(3)树形结构
结构中的数据元素之间存在一个对多个的关系
(4)图状结构或网状结构
结构中的数据元素之间存在多个对多个的关系
数据结构的形式定义为:数据结构是一个二元组
Data_Structure=(D,S)
其中:D是数据元素的有限集,S是D上关系的有限集。
上述数据结构的定义仅是对操作对象的一种数学描述,换句话说,是从操作对象抽象出来的数学模型。结构定义中的“关系”描述的是数据元素之间的逻辑关系,因此又称为数据的逻辑结构,然而讨论数据结构的目的是为了在计算机中实现对它的操作,因此还需研究如何在计算机中表示它。
数据结构在计算机中的表示(又称映像)称为数据的物理结构,又称存储结构。它包括数据元素的表示和关系的表示。在计算机中表示信息的最小单位是二进制数的一位,叫做位(bit)。在计算机中,我们可以用一个由若干位组合起来形成的一个位串表示数据元素(如用一个字长的位串表示一个整数,用8位二进制数表示一个字符串等),通常这个位串为元素或节点。当数据元素由若干数据项组成时,位串中对应于各个数据项的子位串称为数据域。因此,元素或节点可看成是数据元素在计算机中的映像。
数据元素之间的关系在计算机中有两种不同的表示方法:顺序映像和非顺序映像,并由此得到两种不同的存储结构:顺序存储结构和链式存储结构。顺序映像的特点是借助元素在存储器中的相对位置来表示元素之间的逻辑关系。非顺序映像的特点是借助指示元素存储地址的指针来表示数据元素之间的逻辑关系。数据的逻辑结构和物理结构是密切相关的两个方面,任何一个算法的设计取决于选定的数据(逻辑)结构,而算法的实现依赖于采用的存储结构。
如何描述存储结构呢?虽然存储结构涉及数据元素及其关系在存储器中的物理位置,但由于本书是在高级程序语言的层次上讨论数据结构的操作,因此不能如上那样直接以内存地址来描述存储结构,但我们可以借用高级程序语言中提供的“数据类型”来描述它。我们把C语言看成是一个执行C指令和C数据类型的虚拟存储器,那么本书中讨论的存储结构是数据结构在C虚拟处理中的表示,不妨称它为虚拟存储结构。
数据类型是和数据结构密切相关的一个概念,它最早出现在高级程序语言中,用以刻画(程序)操作对象的特性。在用高级程序语言编写的程序中,每个变量、常量或表达式都有一个它所属的确定的数据类型。类型明显或隐含地规定了在程序执行期间变量或表达式所有可能取值的范围,以及在这些值上允许进行的操作。因此数据类型是一个值得集合和定义在这个值集上的一组操作的总称。如C语言中的整型变量,其值集为某个区间上的整数(区间大小依赖于不同的机器),定义在其上的操作为加、减、乘、除和取模等算术运算。
按“值”的不同特性,高级程序语言中数据类型可分为两类:一类是非结构的原子类型。原子类型的值是不可分解的,例如C语言中的基本类型(整型、实型、字符型和枚举类型)、指针类型和空类型。另一类是结构类型。结构类型的值是由若干成分按某种结构组成的,因此是可以分解的,并且它的成分可以是非结构的,也可以是结构的。
实际上,在计算机中,数据类型的概念并非局限于高级语言中,每个处理器(包括计算机硬件系统、操作系统、高级语言、数据库等)都提供了一组原子类型或结构类型。如一个计算机硬件系统通常含有“位”、“字节”、“字”等原子类型,它们的操作通过计算机设计的一套指令系统直接由电路系统完成,而高级程序语言提供的数据类型,其操作需要通过编译器或解释器转化为低层,即汇编语言或机器语言的数据类型来实现。引入“数据类型”的目的,从硬件的角度看,是作为解释计算机内存中信息含义的一种手段,而对使用数据类型的用户来说,实现了信息的隐蔽,即将一切用户不必了解的细节都封装在类型中。
抽象数据类型(Abstract Data Type)是指一个数学模型以及定义在该模型上的一组操作。抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关,即不论其内部结构如何变化,只要它的数学特性不变,都不影响其外部的使用。抽象数据类型和数据类型实质上是一个概念,抽象数据类型不再局限于前述各处理器中已定义并实现的数据类型,还包括用户在设计软件系统时自己定义的数据类型。为了提高软件的复用率,在近代程序设计方法学中指出,一个软件系统的框架应建立在数据之上,而不是建立在操作之上(后者是传统的软件设计方法所为)。即在构成软件系统的每个相对独立的模块上,定义一组数据和施于这些数据上的一组操作,并在模块内部给出这些数据的表示及其操作的细节,而在模块外部使用的只是抽象的数据和抽象的操作。一个含抽象数据类型的软件模块通常包含定义、表示和实现3个部分。
如前所述,抽象数据类型的定义由一个值域和定义在该值域上的一组操作组成。若按其值得不同特性,可细分为下列3种类型:
-
原子类型
属原子类型的变量的值是不可分解的。这类抽象数据类型较少,因为一般情况下,已有的固有数据类型足以满足需求。但有时也有必要定义新的原子数据类型。 -
固定聚合类型
属该类型的变量,其值由确定数目的成分按某种结构组成。 -
可变聚合类型
和固定聚合类型相比较,构成可变聚合类型“值”的成分的数目不确定。
后两种类型可统称为结构类型。
和数据结构的形式定义相对应,抽象数据类型可用以下三元组表示:
(D,S,P)
其中D是数据对象,S是D上的关系集,P是对D的基本操作集。本书采用以下格式定义抽象数据类型:
ADT 抽象数据类型名{
数据对象:<数据对象的定义>
数据关系:<数据关系的定义>
基本操作:<基本操作的定义>
}ADT 抽象数据类型名
其中,数据对象和数据关系的定义用伪码描述,基本操作的定义格式为:
基本操作名(参数表)
初始条件:<初始条件描述>
操作结果:<操作结果描述>
基本操作有两种参数:赋值参数只为操作提供输入值;引用参数以&大有,除可提供输入值外,还将返回操作结果。“初始条件”描述了操作执行之前数据结构和参数应满足的条件,若不满足,则操作失败,并返回相应出错信息。“操作结果”说明了操作正常完成之后,数据结构的变化状况和应返回的结果,若初始条件为空,则省略之。
多形数据类型(polymorphic data type)是指其值成分不确定的数据类型。从抽象数据类型的角度看,具有相同的数学抽象特性,故称为多形数据类型。显然,需要借助面向对象的程序设计语言如C++等实现之。本书中讨论的各种数据类型大多是多行数据类型,限于本书采用类C语言作为描述工具,故只讨论含有确定成分的数据元素的情况。
1.3 抽象数据类型的表示与实现
抽象数据类型可通过固有数据类型来表示和实现,即利用处理器中已存在的数据类型来说明新的结构,用已经实现的操作来组合新的操作。由于本书在高级程序设计语言的虚拟层次上讨论抽象数据类型的表示和实现,并且讨论的数据结构及算法主要是面向读者,故采用介于伪码和C语言之间的类C语言作为描述工具,有时也用伪码描述一些只含抽象操作的抽象算法。
本书采用的类C语言精选了C语言的一个核心子集,同时做了若干扩充修改,增强了语言的描述功能,以下对其作简要说明:
(1)预定义常量和类型:
// 函数结果状态代码
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
// Status 是函数的类型,其值是函数结果状态代码
typedef int Status;
(2) 数据结构的表示(存储结构)用类型定义(typedef)描述。数据元素类型约定为ElemType,由用户在使用该数据类型时自行定义。
(3) 基本操作的算法都用以下形式的函数描述:
函数类型 函数名(函数参数表){
// 算法说明
语句序列
} // 函数名
除了函数的参数需要说明类型外,算法中使用的辅助变量可以不做变量说明,必要时对其作用给予注释。一般而言,a、b、c、d、e等用作数据元素名,i、j、k、l、m、n等用作整型变量名,p、q、r等用作指针变量名。当函数返回值为函数结果状态代码时,函数定义为Status类型。为了便于算法描述,除了值调用方式外,增添了C++语言的引用调用的参数传递方式。在形参表中,以&打头的参数即为引用参数。
(4)赋值语句有
简单赋值 变量名=表达式;
串联赋值 变量名1=变量名2=...=变量名k=表达式;
成组赋值 (变量名1,...,变量名k)=(表达式1,...,表达式k);
结构名=结构名;
结构名=(值1,...,值k);
变量名[]=表达式;
变量名[起始下标..终止下标]=变量名[起始下标..终止下标];
交换赋值 变量名<-->变量名
条件赋值 变量名 = 条件表达式? 表达式T:表达式F;
(5)选择语句有
条件语句1 if(表达式) 语句;
条件语句2 if(表达式) 语句;
else 语句;
开关语句1 switch(表达式){
case值1: 语句序列1;break;
...
case值n: 语句序列n;break;
default:语句序列n+1;
}
开关语句2 switch{
case条件1: 语句序列1;break;
...
case条件n: 语句序列n;break;
default:语句序列n+1;
}
(6) 循环语句有
for语句 for(赋初值表达式序列;条件;修改表达式序列) 语句;
while语句 while(条件) 语句;
do-while语句 do{
语句序列;
}while(条件);
(7) 结束语句有
函数结束语句 return 表达式;
return;
case结束语句 break;
异常结束语句 exit(异常代码)
(8) 输入和输出语句有
输入语句 scanf([格式串],变量1,...,变量n);
输出语句 printf([格式串],表达式1,...,表达式n);
(9) 注释
单行注释 // 文本序列
(10) 基本函数有
求最大值 max(表达式1,...,表达式n);
求最小值 min(表达式1,...,表达式n);
求绝对值 abs(表达式)
求不足整数值 floor(表达式)
求进位整数值 ceil(表达式)
判定文件结束 eof(文本变量) 或 eof
判定行结束 eoln(文本变量) 或 eoln
(11) 逻辑运算约定
与运算&&
或运算||
1.4 算法和算法分析
1.4.1 算法
算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中一条指令表示一个或多个操作;此外,一个算法还具有下列5个重要特性:
-
有穷性
一个算法必须总是(对任何合法的输入值)在执行有穷步之后结束,且每一步都可在有穷时间内完成。 -
确定性
算法中每一条指令必须有确定的含义,读者理解时不会产生二义性。并且,在任何条件下,算法只有唯一的一条执行路径,即对于相同的输入只能得到相同的输出。 -
可行性
一个算法是能行的,即算法中描述的操作都是可以通过已经实现的基础运算执行有限次来实现的。 -
输入
一个算法有零个或多个的输入,这些输入取自于某个特定的对象的集合。 -
输出
一个算法有一个或多个的输出,这些输出是同输入有着某些特定关系的量。
1.4.2 算法设计的要求
-
正确性
算法应当满足具体问题的需求。 -
可读性
算法主要是为了人的阅读与交流,其次才是机器执行。可读性好有助于人对算法的理解,晦涩难懂的程序易于隐藏较多错误,难以调试和修改。 -
健壮性
当输入数据非法时,算法也能适当地作出反应或进行处理,而不会产生莫名其妙的输出结果。 -
效率与低存储量需求
通俗的说,效率指的是算法执行的时间。对于同一个问题如果有多个算法可以解决,执行时间短的算法效率高。存储量需求指算法执行过程中所需要的最大存储空间。效率与低存储量需求这两者都与问题的规模有关。
1.4.3 算法效率的度量
算法执行时间需通过依据该算法编制的程序在计算机上运行时所消耗的时间来度量。而度量一个程序的执行时间通常有两种方法。
(1)事后统计的方法
不同算法的程序可通过一组或若干组相同的统计数据以分辨优劣。但这种方法有两种缺陷:一是必须先运行依据算法编制的程序,二是所得时间的统计量依赖于计算机的硬件、软件等环境因素,有时容易掩盖算法本身的优劣。
(2)事前分析估算的方法
一个用高级程序语言编写的程序在计算机上运行时所消耗的时间取决与下列因素:
- 依据的算法选用何种策略
- 问题的规模
- 书写程序的语言
- 编译程序所产生的机器代码的质量
- 机器执行指令的速度
使用绝对的时间单位衡量算法的效率是不合适的。撇开这些与计算机硬件、软件有关的因素,可以认为一个特定算法“运行工作量”的大小,只依赖于问题的规模(通常用整数n表示),或者说,它是问题规模的函数。
一个算法是由控制结构(顺序、分支和循环3种)和原操作(指固有数据类型的操作)构成的,则算法时间取决于两者的综合效果。为了便于比较同一个问题的不同算法,通常的做法是,从算法中选取一种对于所研究的问题(或算法类型)来说是基本操作的原操作,以该基本操作重复执行的次数作为算法的时间量度。
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数f(n),算法的时间量度记作:
T(n) = O(f(n))
它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称时间复杂度。
有的情况下,算法中基本操作重复执行的次数还随着问题的输入数据集不同而不同。对这类算法的分析,一种解决的办法是计算它的平均值,即考虑它对所有可能的输入集的期望值,此时相应的时间复杂度为算法的平均时间复杂度。然而,在很多情况下,各种输入数据集出现的概率难以确定,算法的平均时间复杂度也就难以确定。因此,另一种更可行也更常用的办法是讨论算法在最坏情况下的时间复杂度,即分析最坏情况以估算算法执行时间的一个上界。在本书以后各章中讨论的时间复杂度,除特别指明外,均指最坏情况下的时间复杂度。
1.4.4 算法的存储空间需求
类似于算法的时间复杂度,本书以空间复杂度作为算法所需存储空间的量度,记作:
S(n)=O(f(n))
其中n为问题的规模,一个上机执行的程序除了需要存储空间来寄存本身所用的指令、常数、变量和输入数据外,也需要一些对数据进行操作的工作单元和存储一些为实现计算所需信息的辅助空间。若输入数据所占空间只取决于问题本身,和算法无关,则只需要分析除输入和程序之外的额外空间,否则应同时考虑输入本身所需空间(和输入数据的表示形式有关)。若额外空间相对于输入数据量来说是常数,则称此算法为原地工作,如果所占空间量依赖于特定的输入,则除特别指明外,均按最坏情况来分析。