专题 1 数据结构分类与抽象数据类型 1.1 数据结构分类 数据结构讨论现实世界和计算机世界中的数据及其相互之间的联系,这体现在逻辑和 存储两个层面上,相应称之为逻辑结构和存储结构。也就是说,在现实世界中讨论的数据 结构是指逻辑结构,在计算机世界中讨论的数据结构是指存储结构,又称为物理结构。 数据的逻辑结构总体上分为 4 种类型:集合结构、线性结构、树结构和图结构。数据 的存储结构总体上也分为 4 种类型:顺序结构、链接结构、索引结构和散列结构。原则上, 一种逻辑结构可以采用任一种存储结构来存储(表示)。 对于现实世界中的同一种数据,根据研究问题的角度不同,将会选用不同的逻辑结构; 对于一种逻辑结构,根据处理问题的要求不同,将会选用不同的存储结构。 对于复杂的数据结构,不论从逻辑层面上还是从存储层面上看,都可能包含有多个嵌 套层次。如假定一种数据结构包含有两个层次,第一层(顶层)的逻辑结构可能是树结构, 存储结构可能是链接结构;第二层(底层)的逻辑结构可能是线性结构,存储结构可能是 顺序结构。第一层结构就是数据的总体结构,第二层结构就是第一层中数据元素的结构。 数据的逻辑结构通常采用二元组来描述,其中一元为数据元素的集合,另一元为元素 之间逻辑关系的集合,每一个逻辑关系是元素序偶的集合,如就是一个序偶,其中 x 为前驱,y 为后继。当数据的逻辑结构存在着多个逻辑关系时,通常对每个关系分别进行 讨论。 逻辑结构的另一种描述方法是图形表示,图中每个结点表示元素,每条带箭头的连线 表示元素之间的前驱与后继的关系,其箭头一端为后继元素,另一端为前驱元素。 数据的存储结构通常采用一种计算机语言中的数据类型来描述,通过建立数据存储结 构的算法来具体实现。 数据的逻辑结构或存储结构也时常被简称为数据结构,读者可根据上下文来理解。 下面通过例子来说明数据的逻辑结构。 假定某校教务处的职员简表如表 1.1 所示。该表中共有 10 条记录,每条记录都由 6 个 数据项组成。此表整体上被看为一个数据,每个记录是这个数据中的数据元素。由于每条 记录的职工号各不相同,所以可把职工号作为记录的关键字,在下面构成的各种数据结构 中,将用记录的关键字代表整个记录。 2 数据结构辅导与提高 表 1.1 职 工 01 02 03 04 05 06 07 08 09 10 号 姓 赵 张 名 宁 利 性 男 男 女 女 男 女 女 男 男 男 别 教务处职员简表 出 生 日 期 1962.03.20 1968.06.14 1964.12.07 1972.08.05 1959.08.15 1975.04.01 1972.06.28 1967.03.17 1975.10.12 1976.07.05 职 处长 科长 科长 主任 科员 科员 科员 科员 科员 科员 教材科 考务科 办公室 教材科 教材科 考务科 考务科 考务科 办公室 务 部 门 万明华 赵书芳 刘永年 王明理 王 张 敏 才 马立仁 邢怀常 【例 1.1】一种数据结构的二元组表示为 set=(K,R),其中 K={01,02,03,04,05,06,07,08,09,10} R={} 在数据结构 set 中,只存在有元素的集合,不存在有关系,或者说关系为空。这表明只 考虑表中的每条记录,不考虑它们之间的任何关系。把具有此种特点的数据结构称为集合 结构。 集合结构中的元素可以任意排列,无任何次序。 【例 1.2】一种数据结构的二元组表示为 linearity=(K,R),其中 K={01,02,03,04,05,06,07,08,09,10} R={<05,01>,<01,03>,<03,08>,<08,02>,<02,07>,<07,04>, <04,06>,<06,09>,<09,10>} 对应的图形表示如图 1.1 所示。 图 1.1 数据的线性结构示意图 结合表 1.1,细心的读者不难看出:R 是按职员年龄从大到小排列的关系。 在数据结构 linearity 中,数据元素之间是有序的,每个数据元素有且仅有一个直接前 驱元素(除结构中第一个元素 05 外),有且仅有一个直接后继元素(除结构中最后一个元 素 10 外)。这种数据结构的特点是数据元素之间的 1 对 1(1∶1)联系,即线性关系。我 们把具有这种特点的数据结构叫做线性结构。 专题 1 数据结构分类与抽象数据类型 3 【例 1.3】一种数据结构的二元组表示为 tree=(K,R),其中 K={01,02,03,04,05,06,07,08,09,10} R={<01,02>,<01,03>,<01,04>,<02,05>,<02,06>,<03,07>, <03,08>,<03,09>,<04,10>} 对应的图形表示如图 1.2 所示。 图 1.2 数据的树结构示意图 结合表 1.1,细心的读者不难看出:R 是职员之间领导与被领导的关系。 图 1.2 像倒着画的一棵树,在这棵树中,最上面的一个没有前驱只有后继的结点叫做 树根结点,最下面一层的只有前驱没有后继的结点叫做树叶结点,除树根和树叶之外的结 点叫做树枝结点。 在一棵树中,每个结点有且只有一个前驱结点(除树根结点外),但可以有任意多个 后继结点(树叶结点可看作为含 0 个后继结点)。这种数据结构的特点是数据元素之间的 1 对 N(1∶N)联系(N≥0),即层次关系,我们把具有这种特点的数据结构叫做树结构, 简称树。 【例 1.4】一种数据结构的二元组表示为 graph=(K,R),其中 K={01,02,03,04,05,06,07} R={<01,02>,<02,01>,<01,04>,<04,01>,<02,03>,<03,02>, <02,06>,<06,02>,<02,07>,<07,02>,<03,07>,<07,03>, <04,06>,<06,04>,<05,07>,<07,05>} 对应的图形表示如图 1.3 所示。 从图 1.3 可以看出,R 是 K 上的对称关系。为了简化起见,我们把〈x,y〉和〈y,x〉这 两个对称序偶用一个无序对(x,y)或(y,x)来代替;在示意图中,我们把 x 结点和 y 结点 之间两条相反的有向边用一条无向边来代替。这样 R 关系可改写为: R={(01,02),(01,04),(02,03),(02,06),(02,07), (03,07),(04,06),(05,07)} 对应的图形表示如图 1.4 所示。 如果说 R 中每个序偶里的两个元素所代表的职员是好友的话,那么 R 关系就是人员之 4 数据结构辅导与提高 间的好友关系。 图 1.3 数据的图结构示意图 图 1.4 图 1.3 的等价表示 从图 1.3 或 1.4 可以看出,结点之间的联系是 M 对 N(M∶N)联系(M≥0,N≥0), 即网状关系。也就是说,每个结点可以有任意多个前驱结点和任意多个后继结点。我们把 具有这种特点的数据结构叫做图结构,简称图。 从图结构、树结构和线性结构的定义可知,树结构是图结构的特殊情况(即 M=1 的情 况),线性结构是树结构的特殊情况(即 N=1 的情况)。为了区别于线性结构,我们把树 结构和图结构统称为非线性结构。 集合结构是整个数据结构中的一种特殊情况,其元素之间不存在任何关系。 【例 1.5】一种数据结构的二元组表示为 B=(K,R),其中 K={k1,k2,k3,k4,k5,k6} R={R1,R2} R1={,,,,} R2={,,,,} 若用实线表示关系 R1,虚线表示关系 R2,则对应的图形表示如图 1.5 所示。 图 1.5 带有两个关系的一种数据结构示意图 从图 1.5 可以看出:数据结构 B 是图结构。但是,若只考虑关系 R1 则为树结构,若只 考虑关系 R2 则为线性结构。 下面简要讨论数据的存储结构。 存储数据不仅要存储数据中的每个数据元素,而且要存储元素之间的逻辑关系。具体 地说,若数据是集合结构,则只需要存储所有数据元素,不需要存储它们之间的任何关系; 专题 1 数据结构分类与抽象数据类型 5 若数据是线性结构、树结构或图结构,则除了要存储所有数据元素外,还要相应存储元素 之间的线性关系、层次关系或网状关系。 数据的存储结构分为顺序、链接、索引和散列 4 种。 顺序存储对应一块连续的存储空间,该空间的大小要大于等于存储所有元素需占有的 存储空间的大小,存储元素之间的联系(即逻辑结构)通常不需要附加空间,而是通过元 素下标之间的对应关系反映出来,只要简单的计算就可以得到一个元素的前驱或后继元素 的下标。顺序存储空间一般需要通过定义数组类型和数组对象来实现。 在链接存储结构中,元素之间的逻辑关系通过存储结点之间的链接关系反映出来,每 个存储结点对应存储一个元素,同时存储该元素的前驱和后继元素所在结点的存储位置, 或者说同时存储指向其前驱元素结点和后继元素结点的指针,通过这些指针能够直接访问 到其前驱元素和后继元素。链接存储空间通过定义元素的存储结点类型和对象来实现,所 有存储结点可以占用连续的存储空间(即数组空间),也可以占用不连续的存储空间,此 空间是由动态分配的每个结点的空间形成的。 索引存储是首先把所有数据元素按照一定的函数关系划分成若干个子表,每个子表对 应一个索引项,然后采用一种存储结构存储所有子表的索引项和采用另一种存储结构存储 所有子表中的元素。如存储汉字字典时,需要采用索引存储,首先按偏旁部首划分所存汉 字为若干子表,得到偏旁部首表,对于每个部首再按所属汉字的笔画多少划分子表,得到 检字表,检字表中的每个汉字对应汉字解释表(即字典主体)中的一个条目;然后再分别 存储部首表、检字表和汉字解释表。这里检字表是汉字解释表的索引,而偏旁部首表又是 检字表的索引,它是汉字解释表的二级索引。当存储的数据量很大时,通常都需要采用索 引存储,并且时常使用多级索引。 在索引存储中,各级索引表和主表(即数据元素表)通常都以文件的形式保存在外存 磁盘上,访问任一数据元素时,都要根据该数据元素的特征依次访问各级索引表和最后访 问主表,存取外存的次数至少等于建立索引的级数加 1。 散列存储方法是按照数据元素的关键字通过一种函数变换直接得到该元素存储地址的 方法,该存储地址为相应数组空间中的下标位置。用于散列存储所有数据元素的相应数组 空间称为散列表。通过定义用于计算散列存储地址的函数和定义存储数据元素的散列表能 够实现散列存储结构。 以上简要叙述了数据结构的有关概念,在以后的各专题中将会做深入和具体的讨论。 1.2 抽象数据类型 抽象数据类型(Abstract Data Type,ADT)由一种数据结构和在该数据结构上的一组 操作所组成。抽象数据类型包含一般数据类型的概念,但含义比一般数据类型更广、更抽 象。一般数据类型由具体语言系统内部定义,直接提供给编程者定义用户数据,因此称它 6 数据结构辅导与提高 们为预定义数据类型。抽象数据类型通常由编程者定义,包括定义它所使用的数据、数据 结构以及所进行的操作。在定义抽象数据类型中的数据部分(含数据结构在内)和操作部 分时,可以只定义数据的逻辑结构和操作说明,不考虑具体的存储结构和操作的具体实现, 这样能够为用户提供一个简明的使用接口,然后再另外给出具体的存储结构和操作的具体 实现,使得操作声明与实现分开,从而符合面向对象的程序设计思想。 抽象数据类型在 C++语言中是通过类类型来描述的,其数据部分通常定义为类的私有 或保护的数据成员,它只允许该类或派生类直接使用,操作部分通常定义为类的公共的成 员函数,它既可以提供给该类或派生类使用也可以提供给外部定义的类和函数使用。 在本书中,为了便于叙述和分析数据结构和算法,使读者容易理解和接受,所以在实 现所定义的抽象数据类型时,把数据部分用一种已知的数据类型(如结构或数组等)来实 现,把操作部分中的每个操作用普通函数来实现,这样能够同读者熟悉的 C 语言、C++语 言,甚至其他计算机语言很好地兼容起来。 一种抽象数据类型的定义将采用如下书写格式: ADT <抽象数据类型名> is Data: <数据描述> Operations: <操作声明> end <抽象数据类型名> 【例 1.6】假定把矩形定义为一种抽象数据类型,其数据部分包括矩形的长度和宽度, 操作部分包括初始化矩形的尺寸、求矩形的周长和求矩形的面积。 假定该抽象数据类型名用 RECtangle 矩形) ( 表示, 定义矩形长度和宽度的数据用 length 和 width 表示,并假定其类型为浮点(float)型,初始化矩形数据的函数名用 InitRectangle 表示,求矩形周长的函数名用 Circumference(周长)表示,求矩形面积的函数名用 Area (面积)表示,则矩形的 ADT(抽象数据类型)描述如下: ADT RECtangle is Data: 一个矩形 r,其长度和宽度分别用 length 和 width 表示 Operations: //初始化矩形 r 的长度和宽度值为 len 和 wid void InitRectangle(Rectangle& r, float len, float wid); //求矩形 r 的周长并返回 float Circumference(Rectangle& r); //求矩形 r 的面积并返回 float Area(Rectangle& r); end RECtangle 专题 1 数据结构分类与抽象数据类型 7 这里假定数据部分的矩形 r 是类型名为 Rectangle 的一个结构对象,该类型的具体定义 如下: struct Rectangle{ float length, width; }; 下面给出每个操作的具体实现。 (1)初始化矩形尺寸 void InitRectangle(Rectangle& r, float len, float wid) { r.length=len; //把 len 值赋给 r 的 length 域 r.width=wid; //把 wid 值赋给 r 的 width 域 } 该函数把两个值参 len 和 wid 的值分别赋给引用参数 r 的 length 域和 width 域, 实现对 一个矩形 r 的初始化。 (2)求矩形周长 float Circumference(Rectangle& r) { return 2*(r.length+r.width); } (3)求矩形面积 float Area(Rectangle& r) { return r.length*r.width; } 求矩形周长和面积的函数分别使用一个矩形引用参数,当然也可以改为值参。 【例 1.7】把二次多项式 ax2+bx+c 设计成一种抽象数据类型,假定起名为 QUAdratic, 该类型的数据部分为三个系数项 a,b 和 c,操作部分为: (1)初始化 a,b 和 c 的值,假定它们的默认值均为 0; (2)做两个多项式加法,返回它们的和; (3)根据给定 x 的值计算多项式的值并返回; (4)计算方程 ax2+bx+c=0 的两个实数根,对于有实根、无实根和不是二次方程(即 a==0)这三种情况都要返回不同的整数值,以便返回后做不同的处理; (5)按照 ax**2+bx+c 的格式输出二次多项式,在输出时要注意去掉系数为 0 的项, 并且当 b 和 c 的值为负时,其前不能出现加号。 8 数据结构辅导与提高 该抽象数据类型可具体定义如下: ADT QUAdratic is Data: 一个二次多项式 q,其二次项、一次项和常数项的系数分别用 a,b,c 表示 Operations: //初始化二项式,用于赋给 a,b,c 值的每个对应参数的默认值设为 0 void Init(Quadratic& q, float aa=0, float bb=0, float cc=0); //两个二项式相加 Quadratic Add(Quadratic& q1, Quadratic& q2); //二项式求值 float Eval( Quadratic& q, float x); //求二项式方程的根,两个实根由引用参数 r1 和 r2 带回 int Root(Quadratic& q, float& r1, float& r2); //输出二项式 void Print(Quadratic& q); end QUAdratic 这里假定数据部分的二次多项式 q 是类型名为 Quadratic 的一个结构对象, 该类型的具 体定义如下: struct Quadratic { float a,b,c; }; 下面给出每个操作的具体实现。 (1)初始化 a,b 和 c 的值 void Init(Quadratic& q, float aa, float bb, float cc) { q.a=aa; q.b=bb; q.c=cc; } (2)做两个多项式加法,返回它们的和 Quadratic Add(Quadratic& q1, Quadratic& q2) { Quadratic q; q.a=q1.a+q2.a; q.b=q1.b+q2.b; q.c=q1.c+q2.c; 专题 1 return q; } 数据结构分类与抽象数据类型 9 (3)根据给定 x 的值计算多项式的值并返回 float Eval(Quadratic& q, float x) { return (q.a*x*x+q.b*x+q.c); } (4)求二项式方程的根,两个实根由引用参数 r1 和 r2 带回 int Root(Quadratic& q, float& r1, float& r2) { if(q.a==0) return -1; if(x>=0){ r1=float(-q.b+sqrt(x))/(2*q.a); //sqrt(x)为计算 x 的平方根,该函数存在于系统头文件 math.h 中 r2=float(-q.b-sqrt(x))/(2*q.a); return 1; } else return 0; } //无实根返回 0 //有实根返回 1 //不是二次方程返回-1 float x=q.b*q.b-4*q.a*q.c; (5)输出二项式 void Print(Quadratic& q) { if (q.a) cout<0) cout<<"+"<0) cout<<"+"< #include struct Quadratic { float a,b,c; }; //初始化二项式 数据结构辅导与提高 void Init(Quadratic& q, float aa=0, float bb=0, float cc=0); //两个二项式相加 Quadratic Add(Quadratic& q1, Quadratic& q2); //二项式求值 float Eval( Quadratic& q, float x); //求二项式方程的根,两个实根又引用参数 r1 和 r2 带回 int Root(Quadratic& q, float& r1, float& r2); //输出二项式 void Print(Quadratic& q); //各函数定义已经在上面给出,这里从略 void main() { Quadratic a,b,c; Init(a,2,5); Print(a); cout<