目录
线性结构的基本特点是除第一个元素无直接前驱,最后一个元素无直接后继之外,其他每个数据元素都有一个前驱和后继。线性表是最基本且最常用的一种线性结构,同时也是其他数据结构的基础,尤其单链表,是贯穿整个数据结构课程的基本技术。本章将讨论线性表的逻辑结构、存储结构和相关运算,以及线性表的应用实例。
2.1 线性表的定义和特点
在日常生活中,线性表的例子比比皆是。例如,26个英文字母的字母表:(A,B,C,...,Z) 是一个线性表,表中的数据元素是单个字母。在稍复杂的线性表中,一个数据元素可以包含若干个数据项。例如在第一章中提到的学生基本信息表,每个学生为一个数据元素,包括学号、姓名、性别、籍贯、专业等数据项。
由以上示例可以看出,它们的数据元素虽然不同,但同一线性表中的元素必定具有相同的特性,即属于同一数据对象,相邻数据元素之间存在着序偶关系。
诸如此类由n(n>=0)个数据特性相同的元素构成的有限序列称为线性表。
线性表中元素的个数n(n>=0)定义为线性表的长度,n=0时称为空表。
对于非空的线性表或线性结构,其特点是:
1)存在唯一的一个被称作“第一个”的数据元素;
2)存在唯一的一个被称作“最后一个”的数据元素;
3)除第一个之外,结构中的每个数据元素均只有一个前驱;
4)除最后一个之外,结构中的每个数据元素均只有一个后继。
2.2 案例引入
案例2.1:一元多项式的运算。
在数学上,一个一元多项式可按升幂写成:
要求:实现两个一元多项式的相加、相减和相乘的运算。
实现两个多项式相关运算的前提是如何在计算机中有效地表示一元多项式,进而在此基础上设计相关运算的算法?这个问题看似很复杂,我们通过学习本章中线性表的表示及其相关运算便可以完成。
可以看出,一个一元多项式可由n+1个系数唯一确定,因此,可以将一元多项式抽象为一个由n+1个元素组成的有序序列,可用一个线性表P来表示:
。这时,每一项的指数i隐含在其系数
的序号中。
假设是一元m次多项式,同样可以哟弄个线性表Q来表示:
。不失一般性,设m
n,则两个多项式相加的结果
可用线性表R表示:
在后面的叙述中将看到,对此类多项式的线性表只需要用数组表示的顺序存储结构便很容易实现上述运算。
然而,在通常的应用中,多项式的次数可能很高且变化很大,这种所谓的稀疏多项式如果采用上述表示方法,将使得线性表中出现很多零元素。下面给出稀疏多项式的例子。
案例2.2:稀疏多项式的运算。
例如,在处理形如的多项式时,就要用一个长度为20001的线性表来表示,而表中仅有三个非零元素,此时将会造成存储空间的很大浪费,这种对空间的浪费是应当避免的。由于线性表的元素可以包含多个数据项,由此可改变元素设定,对多项式的每一项,可用(系数,指数)唯一确定。
一般情况下的一元n次多项式可写成:。其中,
是指数为
的项的非零系数,且满足
。
若用一个长度为m且每个元素有两个数据项(系数项和指数项)的线性表便可唯一确定多项式
。在最坏情况下,n+1(=m)个系数都不为零,则比只存储每项系数的方案要多存储一倍的数据。但是对于类似S(x)的稀疏多项式,这种表示将大大节省空间。
由上述讨论可以看出,如果多项式属于非稀疏多项式,且只对多项式进行“求值”等不改变多项式的系数和指数的运算,可采用数组表示的顺序存储结构,如果多项式属于稀疏多项式,显然可以采用数组表示法,但这种顺序存储结构的存储空间分配不够灵活。因为事先无法确定多项式的非零项数,所以需要根据预期估计可能的最大值定义数组的大小,这种分配方式可能会带来两种问题:一种是实际非零项数比较小,浪费了大量存储空间;另一种是实际非零项式超过了最大值,存储空间不够。另外在实现多项式的相加运算时,还需要开辟一个新的数组保存结果多项式,导致算法的空间复杂度较高。改进方案是利用链式存储结构表示多项式的有序序列,这样灵活性更大。
2.3 线性表的类型定义
线性表是一个相当灵活的数据结构,其长度可根据需要增长或缩短,即对线性表的数据元素不仅可以进行访问,而且可以进行插入和删除等操作。为不失一般性,本章采用1.2节抽象数据结构类型格式对各种数据进行描述。下面给出线性表的抽象数据类型定义:
ADT List{
数据对象:D={}
数据关系:R={}
基本操作:
InitList(&L)
操作结果:构造一个空的线性表L。
DestroyList(&L)
初始条件:线性表L已存在。
操作结果:销毁线性表L。
ClearList(&L)
初始条件:线性表已存在。
操作结果:将L重置为空表。
ListEmpty(L)
初始条件:线性表L已存在。
操作结果:若L为空表,则返回true,否则返回false。
ListLength(L)
初始条件:线性表L已存在。
操作结果:返回L中数据元素的个数。
GetElem(L,i,&e)
初始条件:线性表L已存在,且。
操作结果:用e返回L中第i个数据元素的值。
LocateElem(L,e)
初始条件:线性表L已存在。
操作结果:返回L中第1个值与e相同的元素在L中的位置。若这样的数据元素不存在,则返回值为0.
PriorElem(L,cur_e,&pre_e)
初始条件:线性表L已存在。
操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回其前驱,否则操作失败,pre_e无定义。
ListInsert(&L,i,e)
初始条件:线性表L已存在。且。
操作结果:在L中第i个位置之前插入新的数据元素e,L的长度增加1。
ListDelete(&L,i)
初始条件:线性表L已存在,且。
操作结果:删除L的第i个数据元素,L的长度减1.
TraverseList(L)
初始条件:线性表L已存在。
操作结果:对线性表L进行遍历,在遍历过程中对L的每个结点访问一次。
}ADT List
说明:
1)抽象数据类型仅是一个模型的定义,并不涉及模型的具体实现,因此这里描述中所涉及的参数不必考虑具体数据类型。在实际应用中,数据元素可能有多种类型,到时可根据具体需要选择使用不同的数据类型。
2)上述抽象数据类型中给出的操作知识基本操作,由这些基本操作可以构成其他较复杂的操作。
3)对于不同的应用,基本操作的接口可能不同。
4)由抽象数据类型定义的线性表,可以根据实际所采用的存储结构形式,进行具体的表示和实现。
2.4 线性表的顺序表示和实现
2.4.1线性表的顺序存储表示
线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素,这种表示也称做线性表的顺序存储结构或顺序映像。通常,称这种存储结构的线性表为顺序表(SequenialList)。其特点是,逻辑上相邻的数据元素,其物理次序也是相邻的。
假设线性表的每个元素需占用l个存储单元,并以所占的第一个单元的存储地址作为数据元素的存储起始位置。则线性表中第i+1个数据元素的存储位置和第i个数据元素的存储位置
之间满足如下关系:
只要确定了存储线性表的起始位置,线性表中任一数据元素都可随机存取,所以线性表的顺序存储结构是一种随机存储的存储结构。
由于高级程序设计语言中的数组类型也有随机存取的特性,因此,通常都用数组来描述数据结构中的存储结构。因此,由于线性表的长度可变,且所需最大存储空间随问题不同而不同,则在C语言中可用动态分配的一维数组表示线性表,描述如下:
#define MAXSIZE 100 //顺序表可能达到的最大长度
typedef struct
{
ElemType *elem; //存储空间的基地址
int length; //当前长度
}Sqlist; //顺序表的结构类型为SqList
说明:
1)数组空间通过后面相关算法初始化动态分配得到,初始化完成后,数组指针elem指示顺序表的基地址,数组空间大小为MAXSIZE。
2)元素类型定义中的 ElemType 数据类型是为了描述同一而自定的,在实际应用中,用户可根据实际需要具体定义表中数据元素的数据类型,既可以是进本数据类型,如int、float、char等,也可以是构造数据类型,如struct结构体类型。
3)length表示顺序表中当前数据元素的个数。因为C语言数组的下标是从0开始的,而位置序号是从1开始的,所以要注意区分元素的位置序号和该元素在数组中的下标位置之间的对应关系,数据元素a1,a2,...,an依次存放在数组elem[0]、elem[1]、...、elem[length-1]中。
用顺序表存储案例2.2的稀疏多项式数据时,其顺序存储分配情况如下图所示。多项式的顺序存储结构的类型定义如下:
#define MAXSIZE 100 //多项式可能达到的最大长度
typedef struct
{
float coef; //系数
int expn; //指数
}Polynomial;
typedef strcut
{
Polynomial *elem; //存储空间的基地址
int length; //多项式中当前项的个数
}SqList; //多项式的顺序存储结构类型为SqList
2.4.2 顺序表中基本操作的实现
可以看出,当线性表以上上述定义的顺序表表示时,某些操作很容易实现。因为表的长度是顺序表的一个“属性”,所以可以通过返回length的值实现求表长的操作,通过判断length的值是否为0判断表是否为空,这些操作算法的时间复杂度都是O(1)。下面讨论顺序表其他几个主要操作的实现。
1.顺序表的初始化操作就是构造一个空的顺序表。
算法2.1 顺序表的初始化
【算法步骤】
1)为顺序表L动态分配一个预定义大小的数组空间,使elem指向这段空间的基地址。
2)将表的当前长度设为0。
【算法描述】
Status InitList(SqList &L)
{//构建一个空的顺序表L
L.elem=new ElemType[MAXSIZE]; //为顺序表分配一个大小为MAXSIZE的数组空间
if(!L.elme) exit(OVERFLOW); //存储分配失败退出
L.length=0; //空表长度为0
return Ok;
}
动态分配线性表的存储区域可以更有效地利用系统的资源,当不需要该线性表时,可以使用销毁操作及时释放占用的存储空间。
2.取值
取值操作是根据指定的位置序号i,获取顺序表中第i个数据元素的值。
由于顺序存储结构具有随机存取的特点,可以直接通过数组下标定位得到,elem[i-1]单元存储第i个数据元素。
算法2.2 顺序表的取值
【算法步骤】
1)判断指定的位置序号i值是否合理,若不合理,则返回ERROR。
2)若i值合理,则将第i个数据元素L.elem[i-1]赋给参数e,通过e返回第i个数据元素的传值。
【算法描述】
Status GetElem(SpList L,int i,ElemType &e)
{
if(i<1||i>L.length) //判断i是否合理,若不合理,返回ERROR
return ERROR;
e=L.elem[i-1]; //elem[i-1]单元存储第i个数据元素
return OK;
}
【算法分析】显然,顺序表取值算法的时间复杂度为O(1)。
3.查找
查找操作时根据指定的元素值e,查找顺序表中第1个与e相等的元素。若查找成功,返回该元素的序号;若查找失败,则返回0。
算法2.3 顺序表的查找
【算法步骤】
1)从第一个元素起,依次和e相比较,若找到与e相等的元素L.elem[i],则查找成功,返回该元素的序号i+1(因为序号是从0开始的)。
2)若查遍整个顺序表都没有找到,则查找失败,返回0.
【算法描述】
int LocateElem(SqList L,ElemType e)
{//在顺序表L中查找值为e的数据元素,返回其序号
for(i=0;i<L.length;i++)
if(L.elem[i]==e)
return i+1;
return 0;
}
【算法分析】
当在顺序表中查找一个数据元素时,其时间主要耗费在数据的比较上,而比较的次数取决于被查找元素在线性表中的位置。
在查找时,为确定元素在顺序表中的位置,需和给定值进行比较的数据元素个数的期望值称为查找算法在查找成功时的平均查找长度(Average Search Length,ASL)。
假设是查找第i个元素的概率,
为找到表中其关键字与给定值相等的第i个记录时,和给定值已进行过比较的关键字个数,则在长度为n的线性表中,查找成功时的平均查找长度为
。
从顺序表查找的过程可见,取决于 所查元素在表中的位置。例如,查找表中第一个记录时,仅需比较一次;而查找表中最后一个记录时,则需比较n次。一般情况下
等于i。
假设每个元素的查找概率相等,即,则上述ASL公式可简化为
。由此可见,顺序表按值查找算法的平均时间复杂度为O(n)。
4.插入
线性表的插入操作是指在表的第i个位置插入一个新的数据元素e,使长度为n的线性表变成长度为n+1的线性表。
数据元素与
之间的逻辑关系发生了变化。在线性表的顺序存储结构中,由于逻辑上相邻的数据元素在物理位置上也是相邻的,因此,除非i=n+1,否则必须移动元素才能反映这个逻辑关系的变化。
例如,上图为一个线性表在插入前后数据元素在存储空间中的位置变化。为了在线性表的第5个位置上插入一个值为25的数据元素,则需将第5个至第8个数据元素依次向后移动一个位置。
一般情况下,在第个位置插入一个元素时,需从最后一个元素即第n个元素开始,依次向后移动一个位置,直至第i个元素。
算法2.4 顺序表的插入
【算法步骤】
1)判断插入位置i是否合法(i值的合法范围是),若不合法则返回ERROR。
2)判断顺序表的存储空间是否已满,若满则返回ERROR。
3)将第n个至第i个位置的元素依次向后移动一个位置,空出第i个位置(i=n+1时无需移动)。
4)将要插入的新元素e放入第i个位置。
5)表长加1.
【算法描述】
Status ListInsert(SQList &L,int i,ELemType e)
{//在顺序表L中第i个位置插入新的元素e,i值得合法范围是1<=i<=L.length+1
if((i<1)||(i>L.length+1))
return ERROR;
if(L.length==MAXSIZE)
return ERROR;
for(j=L.length-1;j>=i-1;j--)
L.elem[j-1]=L.elem[j]; //插入位置及之后的元素后移
L.elem[i-1]=e; //将新元素e放入第i个位置
++L.length;
return OK;
}
上述算法没有处理表的动态扩充,因此当表长已经达到预设的最大空间时,则不能再插入元素。
【算法分析】
当在顺序表中某个位置上插入一个数据元素时,其时间主要耗费在移动元素上,而移动元素的个数取决于插入元素的位置。
假设是在第i个元素之前插入一个元素的概率,
为在长度为n的线性表中插入一个元素时所需移动元素次数的期望值(平均次数),则有
。不失一般性,可以假定在线性表的任何位置插入元素都是等概率的,即
,则
,由此可见,顺序表插入算法的平均时间复杂度为O(n)。
5.删除
线性表的删除操作是指将表的第i个元素删去,将长度为n的线性表变成一个长度为n-1的线性表。
数据元素、
和
之间的逻辑发生了变化,为了在存储结构上反映这个变化,同样需要移动元素。如上图所示,为了删除第4个元素,必须将第5个至第8个元素都依次向前移动一个位置。
算法2.5 顺序表的删除
【算法步骤】
1)判断删除位置i是否合法,若不合法则返回ERROR。
2)将第i+1个至第n个元素依次向前移动一个位置(i=n时无需移动)。
3)表长减1。
【算法描述】
Status ListDelete(SqList &L,int i)
{//在顺序表L中删除第i个元素,i值的合法值范围是1<=i<=L.length
if((i<1)||(i>L.length))
return ERROR;
for(j=i;j<=L.length;j++)
L.elem[j-1]=L.elem[j];
--L.length
return OK;
}
【算法分析】
当在顺序表中某个位置上删除一个数据元素时,其时间主要耗费在移动元素上,而移动元素的个数取决于删除元素的位置。
假设是删除第i个元素的概率,
为在长度为n的线性表中删除一个元素时所需移动元素次数的期望值,则有
。不失一般性,可以假定在线性表的任何位置上删除元素都是等概率的,即
,则
,由此可见,顺序表删除算法的平均时间复杂度为O(n)。
顺序表可以随机存取表中任一元素,其存储位置可用一个简单、直观的公式来表示。然而,从另一方面来看,这个特点也造成了这种存储结构的缺点:在做插入或删除操作的时候,需移动大量元素。另外由于数组有长度相对固定的静态特性,当表中数据元素个数较多且变化较大时,操作过程相对复杂,必然导致存储空间的浪费。所有这些问题,都可以通过线性表的另一种表示方法——链式存储结构来解决。
总结:本章主要讲了顺序表的定义及其相关操作。顺序表就好比数据库中的一个关系表,顺序表中的一个元素就相当于关系表中的一个元组,不过顺序表对于元素的属性本身和属性之间的关系要求不严格。顺序表的应用相对比较广泛,例如学生成绩、商品信息(单价、售价等)等合理的数据都可以使用顺序表表示。顺序表中的“顺序”着重点在于其存储空间的顺序连续(物理和逻辑上),而不是顺序表中数据一定是要有一定先后顺序的。对于顺序表的操作而言,需要注意查找、插入和删除的操作都要先判断其查找、插入和删除的位置是否恰当,若其位置不在线性表的范围内,则毫无意义。线性表缺点就是当对其进行改动时,需要大幅度移动表内元素,所以对于经常需要修改的数据来说使用线性表并不合适。相对于此,线性表更适用于存储历史数据、修改不频繁的数据。