640 likes | 941 Vues
数据结构. (C 语言版) 严蔚敏 吴伟民 编著. “ 数据结构 ” 学科 形成和发展的背景. 自1946年第一台计算机问世以来,计算机产业的飞速发展已远远超出人们对它的预料。如今,计算机已深入到人类社会的各个领域。计算机的应用已不再局限于科学计算,而更多地用于控制、管理及数据处理等非数值计算的处理工作。与此相应计算机加工处理的对象由纯粹的数值发展到字符、表格和图象等各种具有一定结构的数据,这就给程序设计带来一些新的问题。为了编写一些 “ 好 ” 的程序,必须分析待处理对象的特性以及各处理对象间的关系。这就是 “ 数据结构 ” 这门学科形成和发展的背景。.
E N D
数据结构 (C语言版) 严蔚敏 吴伟民 编著
“数据结构”学科形成和发展的背景 自1946年第一台计算机问世以来,计算机产业的飞速发展已远远超出人们对它的预料。如今,计算机已深入到人类社会的各个领域。计算机的应用已不再局限于科学计算,而更多地用于控制、管理及数据处理等非数值计算的处理工作。与此相应计算机加工处理的对象由纯粹的数值发展到字符、表格和图象等各种具有一定结构的数据,这就给程序设计带来一些新的问题。为了编写一些“好”的程序,必须分析待处理对象的特性以及各处理对象间的关系。这就是“数据结构”这门学科形成和发展的背景。
数据结构的研究目的和研究内容 数据结构的研究目的是寻求有效的组织和处理非数值数据的理论技术和方法,这类数据具有量大且具有一定内在逻辑关系的特点。 数据结构的研究内容包括三个方面:数据的逻辑结构,存储结构以及相应的基本操作运算的定义和实现。
第一章 绪论 • 1.1 什么是数据结构 • 1.2 基本概念和术语 • 1.3 抽象数据类型的表示与实现 • 1.4 算法与算法分析
1.1 什么是数据结构(1学时) 一般来说,用计算机解决一个具体问题时,大致需要经过下列几个步骤: 1)首先要从具体问题抽象出一个适当的数学模型; 2)然后设计一个解此数学模型的算法; 3)最后编出程序、进行测试、调整直至得到最终解答。 寻求数学模型的实质是分析问题,从中提取操作的对象,并找出这些操作对象之间含有的关系,然后用数学的语言加以描述。例如,求解梁架结构中应力的数学模型为线性方程组;预报人口增长情况的数学模型为微分方程。然而,更多的非数值计算问题无法用数学方程加以描述。下面请看三个例子。
例1-1 图书馆的书目检索系统自动化问题 由此,在书目检索系统中可以建立一张按登录号顺序排列的书目文件和三张分别按书名、作者名和分类号顺序排列的索引表。 001 高等数学 樊映川 S01 … 002 理论力学 罗远祥 L01 … 003 高等数学 华罗庚 S01 … 004 线形代数 栾汝书 S02 … ……………
由这四张表构成的文件便是书目自动检索的数学模型,计算机的主要操作便是按照某个特定要求(如给定书名)对书目文件进行查询。诸如此类的还有查号系统自动化、仓库账目管理等。由这四张表构成的文件便是书目自动检索的数学模型,计算机的主要操作便是按照某个特定要求(如给定书名)对书目文件进行查询。诸如此类的还有查号系统自动化、仓库账目管理等。 在这类文档管理的数学模型中,计算机处理的对象之间通常存在着的是一种最简单的线性关系,这类数学模型可称谓线性的数据结构。
例1-2 计算机和人对奕问题 在对奕问题中,计算机操作的对象是对奕过程中可能出现的棋盘状态——称为格局。例如下图(a)所示为井字棋的一 个格局,而格局之间的关系是由比赛规则决定的。通常,这个关系不是线性的,因为从一个棋盘格局可以派生出几个格局,例如左图(a)所示的格局可以派生出五个格局,如下图所示, 而从每一个新的格局又可派生出四个可能出现的格局。 (a) (a1) (a2) (a3) (a4) (a5)
因此,若将从对奕开始到结束的过程中所有可能出现的格局都画在一张图上,则可得到一棵倒长的“树”。“树根”是对奕开始之前的棋盘格局,而所有的“叶子”就是可能出现的结局,对奕的过程就是从树根沿树叉到某个叶子的过程。“树”可以是某些非数值计算问题的数学模型,它也是一种数据结构。因此,若将从对奕开始到结束的过程中所有可能出现的格局都画在一张图上,则可得到一棵倒长的“树”。“树根”是对奕开始之前的棋盘格局,而所有的“叶子”就是可能出现的结局,对奕的过程就是从树根沿树叉到某个叶子的过程。“树”可以是某些非数值计算问题的数学模型,它也是一种数据结构。
通常,在十字交叉路口只需设红、绿两色的交通灯便可保持正常的交通秩序,而在多交叉路口需设几种颜色的交通灯才能使车辆相互之间不碰撞,又能达到车辆的最大流通。 假设有一个如图1.3(a)所示的五叉路口,其中C和E为单行道。在路口有13条可行的通路,其中有的可以同时通行,如A—B和E—C,而有的不能同时通行,如E—B和A—D。那末,在路口应如何设置交通灯进行车辆的管理呢? C B D A E 1.3(a) 例 1-3 多叉路口交通灯的管理问题
通常,这类交通、道路问题的数学模型是一种称谓“图”的数据结构。例如在此例题中,可以用图中一个顶点表示一条通路,而通路之间互相矛盾的关系以两个顶点之间的连线表示。设置交通灯的问题等价为对图的顶点的染色问题,要求对图上的每个顶点染一种颜色,并且要求有线相连的两个顶点不能具有相同颜色,而总的颜色种类应尽可能地少。 综上三个例子可见,描述这类非数值问题的数学模型不再是数学方程,而是诸如表、树和图之类的数据结构。因此简单说来,数据结构是一门研究非数值计算的程序设计问题中计算机的操作对象以及它们之间的关系和操作等等的学科。
1.2 基本概念和术语(1学时) 数据(Data)是对客观事物的符号表示,在计算机科学中是指所有能输入到计算机中并被计算机程序处理的符号的总称。它是计算机程序加工的“原料”。例如,一个利用数值分析方法解代数方程的程序,其处理对象是整数和实数;一个编译程序或文字处理程序的处理对象是字符串。因此,对计算机科学而言,数据的含义极为广泛,如图象、声音等都可以通过编码而归之于数据的范畴。 数据元素(DataElement)是数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理。例如,例1-2中的“树”中的一个棋盘格局,例1-3中的“图”中的一个圆圈都被称为一个数据元素。有时,一个数据元素可由若干个数据项(Data ltem)组成,例如,例1-1中一本书的书目信息为一个数据元素,而书目信息中的每一项(如书名、作者名等)为一个数据项。数据项是数据的不可分割的最小单位。
数据对象(Data Object)是性质相同的数据元素的集合,是数据的一个子 集。例如,整数数据对象是集合N={0,±1,±2,…},字母字符数据对象是集合C={‘A’,‘B’,…,‘Z’}。 数据结构(Data Structure)是相互之间存在一种或多种特定关系的数据元素的集合。从1.1节中三个例子可以看到,在任何问题中,数据元素都不是孤立存在的,而是在它们之间存在着某种关系,这种数据元素相互之间的关系称为结构(Structure)。 数据结构的形式定义为:数据结构是一个二元组 Data_Structure=(D,S) (1-1) 其中:D是数据元素的有限集, S是D上关系的有限集。
根据数据元素之间关系的不同特性,通常有下列四类基本结构:(1)集合 结构中的数据元素之间除了“同属于一个集合”的关系外,别无其它关系;(2)线性结构 结构中的数据元素之间存在一个对一个的关系;(3)树形结构 结构中的数据元素之间存在一个对多个的关系;(4)图状结构或网状结构 结构中的数据元素之间存在多个对多个的关系。 例1-4 在计算机科学中,复数可取如下定义:复数是一种数据结构 Complex=(C,R) (1-2) 其中:C是含两个实数的集合{c1,c2};R={P},而P是定义在集合C上的一种关系 {<c1,c2>},其中有序偶<c1,c2>表示c1是复数的实部,c2是复数的虚部。
例1-5 假设我们需要编制一个事务管理的程序,管理学校科学研究课题小组的各项事务,则首先要为程序的操作对象——课题小组设计一个数据结构。假设每个小组由一位教师、一至三名研究生及一至六名本科生组成,小组成员之间的关系是:教师指导研究生,而由每位研究生指导一至两名本科生。则可以如下定义数据结构: Group = (P,R) (1—3) 其中: P = {T,G1,…,Gn,S11…Snm} 1<=n<=3,1<=m<=2, R = {R1, R2} R1 = {<T, Gi> | 1<=i<=n, 1<=n<=3 } R2 = {<Gi, Sij> | 1<=i<=n, 1<=j<=m, 1<=n<=3, 1<=m<=2}
数据结构包括逻辑结构和物理结构两个方面(或层次): 逻辑结构是对数据元素之间的逻辑关系的描述,它可以用一个数据元素的集合和定义在此集合上的若干关系来表示; 逻辑结构在计算机中的表示(又称映象)称为数据的物理结构,又称存储结构。它包括数据元素的表示和关系的表示。在计算机中表示信息的最小单位是二进制数的一位,叫做位(bit)。在计算机中,我们可以用一个由若干位组合起来形成的一个位串表示一个数据元素(如用一个字长的位串表示一个整数,用八位二进制数表示一个字符等),通常称这个位串为元素(Element)或结点(Node)。当数据元素由若干数据项组成时,位串中对应于各个数据项的子位串称为数据域(DataField)。因此,元素或结点可看成是数据元素在计算机中的映象。
数据元素之间的关系在计算机中有两种不同的表示方法:顺序映象和非顺序映象,并由此得到两种不同的存储结构:顺序存储结构和 链式存储结构。 顺序映象的特点是借助元素在存储器中的相对位置来表示数据元素之间的逻辑关系。例如,假设用两个字长的位串表示一个实数,则可以用地址相邻的四个字长的位串表示一个复数。 非顺序映象的特点是借助指示元素存储地址的指针(Pointer)表示数据元素之间的逻辑关系。 数据的逻辑结构和物理结构是密切相关的两个方面,以后大家会看到,任何一个算法的设计取决于选定的数据(逻辑)结构,而算法的实现依赖于采用的存储结构。
如何描述存储结构呢? 我们可以借用高级程序语言中提供的 “数据类型”来描述它,例如可以用所有高级程序语言中都有的“一维数组”类型来描述顺序存储结构,以C语言提供的“指针”来描述链式存 储结构。假如我们把C语言看成是一个执行C指令和C数据类型的虚拟处理器,那么讨论的存储结构是数据结构在C虚拟处理器中的表示,不妨称它为虚拟存储结构。
数据类型(DataType)是和数据结构密切相关的一个概念,它最早出现在高级程序语言中,用以刻画 (程序)操作对象的特性。在用高级程序语言编写的程序中,每个变量、常量或表达式都有一个它所属的确定的数据类型。类型明显或隐含地规定了在程序执行期间变量或表达式所有可能取值的范围,以及在这些值上允许进行的操作。因此数据类型是一个值的集合和定义在这个值集上的一组操作的总称。例如,C语言中的整型变量,其值集为某个区间上的整数(区间大小依赖于不同的机器),定义在其上的操作为:加、减、乘、除和取模等算术运算。
按“值”的不同特性,高级程序语言中的数据类型可分为两类:一类是非结构的原子类型。原子类型的值是不可分解的。如:C语言中的基本类型(整型、实型、字符型和枚举类型)、指针类型和空类型。另一类是结构类型。结构类型的值是由若干成分按某种结构组成的,因此是可以分解的,并且它的成分可以是非结构的,也可以是结构的。例如数组的值由若干分量组成,每个分量可以是整数,也可以是数组等。在某种意义上,数据结构可以看成是“一组具有相同结构的值”,则结构类型可以看成由一种数据结构和定义在其上的一组操作组成。按“值”的不同特性,高级程序语言中的数据类型可分为两类:一类是非结构的原子类型。原子类型的值是不可分解的。如:C语言中的基本类型(整型、实型、字符型和枚举类型)、指针类型和空类型。另一类是结构类型。结构类型的值是由若干成分按某种结构组成的,因此是可以分解的,并且它的成分可以是非结构的,也可以是结构的。例如数组的值由若干分量组成,每个分量可以是整数,也可以是数组等。在某种意义上,数据结构可以看成是“一组具有相同结构的值”,则结构类型可以看成由一种数据结构和定义在其上的一组操作组成。
抽象数据类型(Abstract Data Type简称ADT)是指一个数学模型以及定义在该模型上的一组操作,面向逻辑层次。抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关,即不论其内部结构如何变化,只要它的数学特性不变,都不影响其外部的使用。 抽象数据类型和数据类型实质上是一个概念。例如,各个计算机都拥有的“整数”类型是一个抽象数据类型,尽管它们在不同处理器上实现的方法可以不同,但由于其定义的数学特性相同,在用户看来都是相同的。因此,“抽象”的意义在于数据类型的数学抽象特性。
另一方面,抽象数据类型的范畴更广,它不再局限于前述各处理器中已定义并实现的数据类型(也可称这类数据类型为固有数据类型),还包括用户在设计软件系统时自己定义的数据类型。为了提高软件的复用率,在近代程序设计方法学中指出,一个软件系统的框架应建立在数据之上,而不是建立在操作之上(后者是传统的软件设计方法所为)。即在构成软件系统的每个相对独立的模块上,定义一组数据和施于这些数据上的一组操作,并在模块内部给出这些数据的表示及其操作的细节,而在模块外部使用的只是抽象的数据和抽象的操作。显然,所定义的数据类型的抽象层次越高,含有该抽象数据类型的软件模块的复用程度也就越高。另一方面,抽象数据类型的范畴更广,它不再局限于前述各处理器中已定义并实现的数据类型(也可称这类数据类型为固有数据类型),还包括用户在设计软件系统时自己定义的数据类型。为了提高软件的复用率,在近代程序设计方法学中指出,一个软件系统的框架应建立在数据之上,而不是建立在操作之上(后者是传统的软件设计方法所为)。即在构成软件系统的每个相对独立的模块上,定义一组数据和施于这些数据上的一组操作,并在模块内部给出这些数据的表示及其操作的细节,而在模块外部使用的只是抽象的数据和抽象的操作。显然,所定义的数据类型的抽象层次越高,含有该抽象数据类型的软件模块的复用程度也就越高。
如前所述,抽象数据类型的定义由一个值域和定义在该值域上的一组操作组成。若按其值的不同特性,可细分为下列三种类型: 原子类型(Atomic Data Type)属原子类型的变量的值是不可分解的。这类抽象数据类型较少,因为一般情况下,已有的固有数据类型足以满足需求。但有时也有必要定义新的原子数据类型,例如数位为100的整数。 固定聚合类型(Fixed-aggregate Data Type)属该类型的变量,其值由确定数目的成分按某种结构组成。例如,复数是由两个实数依确定的次序关系构成。 可变聚合类型(Variable-Aggregate Data Type)和固定聚合类型相比较,构成可变聚合类型“值”的成分的数目不确定。例如,可定义一个“有序整数序列”的抽象数据类型,其中序列的长度是可变的。 显然,后两种类型可统称为结构类型。
和数据结构的形式定义相对应,抽象数据类型可用以下三元组示 (D,S,P) (1—4) 其中,D是数据对象,S是D上的关系集,P是对D的基本操作集。 抽象数据类型定义格式: ADT抽象数据类型名{ 数据对象:<数据对象的定义> 数据关系:<数据关系的定义> 基本操作:<基本操作的定义> } ADT 抽象数据类型名 其中,数据对象和数据关系的定义用伪码描述,基本操作的定义格式为 基本操作名(参数表) 初始条件:<初始条件描述> 操作结果:<操作结果描述>
1.3 抽象数据类型的表示与实现 (1学时) 抽象数据类型可通过固有数据类型来表示和实现,即利用处理器中已存在的数据类型来说明新的结构,用已经实现的操作来组合新的操作。 我们采用介于伪码和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)描述。数据元素类型约定为Elem Type,由用户在使用该数据类型时自行定义。 (3)基本操作的算法都用以下形式的函数描述: 函数类型函数名(函数参数表){ //算法说明 语句序列 } //函数名 除了函数的参数需要说明类型外,算法中使用的辅助变量可以不作变量说明,必要时对其作用给予注释。一般而言,a、b、c、d、e等用作数据元素名,i、k、1、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);或 cin>>变量1>>…>>变量n; 输出语句 printf( [格式串],表达式1, …,表达式n);或 cout<<表达式1<<…<<表达式n; 通常省略格式串。 (9)注释 单行注释 //文字序列 (10)基本函数有 求最大值 max (表达式l,…,表达式n) 求最小值 min (表达式1,…,表达式n) 求绝对值 abs (表达式) 求不足整数值 floor(表达式) 求进位整数值 ceil(表达式) 判定文结束 eof(文件变量)或eof 判定行结束 eoln(文件变量)或eoln
例1—7 抽象数据类型三元组(Triplet)的表示和实现。 //—————采用动态分配的顺序存储结构————— typedef ElemType *Triplet; //—————基本操作的函数原型说明—————— Status InitTriplet (Triplet &T,ElemType vl, ElemType v2, ElemType v3); //操作结果:构造了三元组T,元素e1,e2和e3 //分别被赋以参数v1,v2和v3的值。 Status DestroyTriplet (Triplet &T); //操作结果:三元组T被销毁。 Status Get(Triplet T,int i,ElemType &e); //初始条件:三元组T已存在,1≤i≤3。 //操作结果:用e返回T的第i元的值。 Status Put(Triplet &T,int i,ElemType e ) //初始条件:三元组T已存在,1≤i≤3。 //操作结果:改变T的第i元的值为e
Status IsAscending (Triplet T); //初始条件:三元组T已存在。 //操作结果:如果T的三个元素按升序排列,则返回1 , // 否则返回0。 Status IsDescending (Triplet T); //初始条件:三元组T已存在。 //操作结果:如果T的三个元素按降序排列,则返回1 , // 否则返回0。 Status Max(Triplet T,ElemType &e); //初始条件:三元组T已存在。 //操作结果:用e返回T的三个元素中的最大值。 Status Min(Triplet T,ElemType &e); //初始条件:三元组T已存在。 //操作结果:用e返回T的三个元素中的最小值。
//———基本操作的实现——— Status InitTriplet(Triplet &T,ElemType vl,ElemType v2,ElemType v3){ //构造三元组T,依次置T的三个元素的初值为v1,v2和v3。 T = (ElemType *)malloc(3 * sizeof(ElemType)); //分配3个元素的存储空间 if ( !T ) exit (OVERFLOW); //分配存储空间失败 T[0] =v1; T[1] =v2; T[2] =v3; return OK; }//InitTriplet Status DestroyTriplet (Triplet &T){//销毁三元组T。 free(T); T = NULL; return OK; }// DestroyTriplet
Status Get ( Triplet T,int i,ElemType &e){ //1≤i≤3,用e返回T的第i元的值。 if(i<1 || i>3) return ERROR; e = T[i-1]; return OK; }//Get Status Put(Triplet &T,int i,ElemType e){ //1≤i≤3,置T的第i元的值为e。 if (i<1 || i>3) return ERROR; T[i-1] = e; return OK; }//Put
Status IsAscending (Triplet T){ //如果T的三个元素按升序排列,则返回1,否则返回0。 return(T[0]<=T[1])&&(T[1]<=T[2]); } //IsAscending Status IsDescending (Triplet T){ //如果T的三个元素按降序排列,则返回1,否则返回0。 return(T[0]>=T[1])&&(T[1]>=T[2]); } //IsDescending
Status Max( Triplet T,ElemType &e){ //用e返回指向T的最大元素的值。 e = (T[0] >=T[1]) ? ( (T[0] >=T[2])? T[0] : T[2]) : ( (T[1]>=T[2])? T[1] : T[2]) ; return OK; } //Max Status Min (Triplet T,ElemType &e){ //用e返回指向T的最小元素的值。 e = (T[0]<=T[1])? ( (T[0] <=T[2])? T[0] : T[2]) : ( (T[1]<=T[2])?T[1] : T[2]) ; return OK; } // Min
1.4 算法和算法分析(1学时) 1.4.1 算法 算法(Algorithm)是对特定问题求解步骤的一种描述,是为解决一个或一类问题给出的一个确定的、有限长的操作序列;此外,一个算法还具有下列五个重要特性: (1)有穷性 一个算法必须总是(对任何合法的输入值)在执行有穷步之后结束,且每一步都可在有穷时间内完成;
(2)确定性 算法中每一条指令必须有确切的含义,读者理解时不会产生二义性。并且,在任何条件下,算法只有唯一的一条执行路径,即对于相同的输入只能得出相同的输出。 (3)可行性 一个算法是能行的,即算法中描述的操作都是可以通过已经实现的基本运算执行有限次来实现的。 (4)输入 一个算法有零个或多个的输入,这些输入取自于某个特定的对象的集合。 (5)输出 一个算法有一个或多个的输出。这些输出是同输入有着某些特定关系的量。
1.4.2 算法设计的要求 通常设计一个“好”的算法应考虑达到以下目标: (1)正确性 (Correctness) 算法应当满足具体问题的需求。 “正确”一词的含义在通常的用法中有很大差别,大体可分为以下四个层次: a. 程序不含语法错误; b. 程序对于几组输入数据能够得出满足规格说明要求的结果; c. 程序对于精心选择的典型、苛刻而带有刁难性的几组输入数据能够得出满足规格说明要求的结果; d. 程序对于一切合法的输入数据都能产生满足规格说明要求的结果。
(2) 可读性(Readability) 算法主要是为了人的阅读与交流,其次才是机器执行。可读性好有助于人对算法的理解;晦涩难懂的程序易于隐藏较多错误难以调试和修改。 (3) 健壮性(Robustness) 当输入数据非法时,算法也能适当地作出反应或进行处理,而不会产生莫明其妙的输出结果。 (4) 效率与低存储量需求 通俗地说,效率指的是算法执行时间。对于同一个问题如果有多个算法可以解决,执行时间短的算法效率高。存储量需求指算法执行过程中所需要的最大存储空间。效率与低存储量需求这两者都与问题的规模有关。
1.4.3 算法效率的度量 算法执行时间需通过依据该算法编制的程序在计算机上运行时所消耗的时间来度量。而度量一个程序的执行时间通常有两种方法: (1)事后统计的方法。 因为很多计算机内部都有计时功能,有的甚至可精确到毫秒级,不同算法的程序可通过一组或若干组相同的统计数据以分辨优劣。但这种方法有两个缺陷:一是必须先运行依据算法编制的程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素,有时容易掩盖算法本身的优劣。因此人们常常采用另一种事前分析估算的方法。
(2)事前分析估算的方法。 一个用高级程序语言编写的程序在计算机上运行时所消耗的时间取决于下 列因素: a.依据的算法选用何种策略; b.问题的规模,例如求100以内还是1000以内的素数; c.书写程序的语言,对于同一个算法,实现语言的级别越高,执行 效率就越低; d.编译程序所产生的机器代码的质量; e.机器执行指令的速度。
一个算法是由控制结构(顺序、分支和循环三种)和原操作(指固有数据类型的操作)构成的,则算法时间取决于两者的综合效果。为了便于比较同一问题的不同算法,通常的做法是,从算法中选取一种对于所研究的问题(或算法类型)来说是基本操作的原操作,以该基本操作重复执行的次数作为算法的时间量度。 这种衡量效率的办法所得出的不是时间量,而是一种增长趋势的度量。它与软硬件环境无关,只暴露算法本身执行效率的优劣。
例如,在如下所示的两个N×N矩阵相乘的 算法中, for(i = 1;i <= n;++i) for(j = 1;j <= n;++j){ c[i][j] = 0; for(k = = 1;k <= n;++k) c[i][j] + = a[i][k] * b[k][i] } “乘法”运算是“矩阵相乘问题”的基本操作。整个算法的执行时间与 该基本操作(乘法)重复执行的次数 n3 成正比,记作T(n) = O(n3)。
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数f(n),算法的时间量度记作 T(n)=O(f(n)) (1—5) 它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度(Asymptotic Time Complexity),简称时间复杂度。 显然,被称作问题的基本操作的原操作应是其重复执行次数和算法的执行时间成正比的原操作,多数情况下它是最深层循环内的语句中的原操作,它的执行次数和包含它的语句的频度相同。
语句的频度(Frequency Count)指的是该语句重复执行的次数,例如:在下列三个程序段中, (a) {++x; s = 0;} (b) for (i = 1;i<=n;++i) { ++ x; s + = x;} (c) for (i = 1;i<=n;++i) for (k = 1;k<=n;++k) { ++ x;s + = x;} 含基本操作“x增1”的语句的频度分别为1,n和n2,则这三个程序段的时间复杂度分别为O(1),O(n)和O(n2),分别称为常量阶、线性阶和平方阶。算法还可能呈现的时间复杂度有:对数阶O(1og n),指数阶O(2n)等。
一般情况下,对一个问题(或类算法) 只需选择一种基本操作来讨论算法的时间 复杂度即可,有时也需要同时考虑几种基 本操作,甚至可以对不同的操作赋以不同 权值,以反映执行不同操作所需的相对时 间,这种做法便于综合比较解决同一问题的两种完全不同算法。 由于算法的时间复杂度考虑的只是对于问题规模n的增长率,则在难以精确计算基本操作执行次数(或语句频度)的情况下,只需求出它关于n的增长率或阶即可。例如,在下列程序段中 for (i = 2;i<=n;++i) for (j = 1;j<=i-1;++j){ ++ x;a[i][j] = x;} 语句++x的执行次数关于n的增长率为n2,它是语句频度表达式(n-1)(n-2)/2中增长最快的项。
有时,算法中基本操作重复执行的 次数还随问题的输入数据集不同而不同。 一种解决的办法是计算它的平均值,即考 虑它对所有可能的输入数据集的期望值, 此时相应的时间复杂度为算法的平均时间复杂度。另一种更可行也 更常用的办法是讨论算法在最坏情况下的时间复杂度,即分析最坏 情况以估算算法执行时间的一个上界。 实践中我们可以把事前估算和事后统计两种办法结合起来使用。以两个矩阵相乘为例,若上机运行两个10*10的矩阵相乘,执行时间为12ms,则由算法的时间复杂度T(n)=O(n3)可估算两个31*31的矩阵相乘所需时间大致为(31/10)3*12ms=358ms。