目录
顺序表和链表的比较:
前面两节介绍了线性表的两种存储结构:顺序表和链表。
在实际应用中,不能笼统地说哪种存储结构更好,由于它们各有优缺点,选用哪种存储结构,应根据具体问题作具体分析;
通常从空间性能和时间性能两个方面作比较分析。
空间性能的比较:
(1)存储空间的分配:
顺序表的存储空间必须预先分配,元素个数有一定限制,易造成存储空间浪费或空间溢出现象;
而链表不需要为其预先分配空间,只要内存空间允许,链表中的元素个数就没有限制。
基于此,当线性表的长度变化较大,难以预估存储规模时,宜采用链表作为存储结构。
(2)存储密度的大小:
链表的每个结点除了设置数据域用来存储数据元素外,还要额外设置指针域,用来存储指示元素之间逻辑关系的指针;
从存储密度上来讲,这是不经济的。
所谓存储密度是指数据元素本身所占用的存储量和整个结点结构所占用的存储量之比,即:
存储密度=数据元素本身占用的存储量 / 结点结构占用的存储量
存储密度越大,存储空间的利用率就越高。显然,顺序表的存储密度为1,而链表的存储密度小于1。如果每个元素数据域占据的空间较小,则指针的结构性开销就占用了整个结点的大部分空间,这样存储密度较小。例如,若单链表的结点数据均为整数,指针所占用的空间和整型量所占用的相同,则单链表的存储密度为0.S。因此,如果不考虑顺序表中的空闲区,则顺序表的存储空间利用率为100%,而单链表的存储空间利用率仅为50%。
基于此,当线性表的长度变化不大,易于事先确定其大小时,为了节约存储空间,宜采用顺序表作为存储结构。
时间性能的比较:
(1)存取元素的效率:
顺序表是由数组实现的,它是一种随机存取结构,指定任意一个位置序号,都可以在O(1)时间内直按存取该位置上的元素,即取值操作的效率高;
而链表是一种顺序存取结构,按位置访问链表中第i个元素时,只能从表头开始依次向后遍历链表,直到找到第位置上的元素,时间复杂度为O(n),即取值操作的效率低。
基于此,若线性表的主要操作是和元素位置紧密相关的一类取值操作,很少做插入或删除时,宜采用顺序表作为存储结构。
(2)插入和删除操作的效率:
对于链表,在确定插入或删除的位置后,插入或删除操作无须移动数据,只需要修改指针,时间复杂度为O(1)。
而对于顺序表,进行插入或删除时,平均要移动表中近一半的结点,时间复杂度为O(n)。尤其是当每个结点的信息量较大时,移动结点的时间开销就相当可观。
基于此,对于频繁进行插入或删除操作的线性表,宜采用链表作为存储结构。
线性表的应用:
线性表的合并:
求解一般集合的并集问题。
【问题描述】:
已知两个集合A和B,现要求一个新的集合A=AUB。
例如,设:A=(7,5,3,11),B=(2,63)
合并后:A=(7,5,3,11,2,6)
【问题分析】:
可以利用两个线性表LA和LB分别表示集合A和B(线性表中的数据元素为集合中的成员),这样只需扩大线性表LA,将存在于LB中而不存在于LA中的数据元素插入LA中。只要从LB中依次取得每个数据元素,并依值在LA中进行查访,若不存在,则插入之。
上述操作过程可用算法2.15来描述。具体实现时既可采用顺序形式,也可采用链表形式。
void MergeList(List &LA,List &LB)
{将所有在线性表LB中但不在LA中的数据元素插入LA中
m=ListLength(LA);n=ListLength(LB); 求线性表的长度
for(int i=1;i<=n;i++)
{
GetElem(LB,i,e); 取LB中第i个数据元素赋给e
if(!(LocateElem(LA,e))) LA中不存在和e相同的数据元素
ListInsert(LA,++m,e); 将e插在LA的最后
}
}
【算法分析】:
上述算法的时间复杂度取决于抽象数据类型List定义中基本操作的执行时间,假设LA和LB的表长分别为m和n,循环执行n次,则:
①当采用顺序存储结构时,在每次循环中,GetElem和ListInsert这两个操作的执行时间和表长无关LocateElem的执行时间和表长m成正比,因此,算法2.15的时间复杂度为O(m×n);
②当采用链式存储结构时,在每次循环中,GetElem的执行时间和表长n成正比,而LocateElem和ListInsert这两个操作的执行时间和表长m成正比,因此,若假设m大于n,算法2.15的时间复杂度也为O(m×n)。
有序表的合并:
顺序有序表的合并:
void MergeList_Sq(SqList LA,SqList LB,SqList &LC)
{已知顺序表LA和LB的元素按值非递减排列
归并LA和LB得到新的顺序表LC,LC的元素也按值非递减排列
LC.length=LA.length+LB.length; 新表长度为待合并两表的长度之和
LC.elem=new ElemType[LC.length]; 为合并的新表分配一个数组空间
pc=LC.elem; 指针pc指向新表的第一个元素
pa=LA.elem; 指针pa指向新表的第一个元素
pb=LB.elem; 指针pb指向新表的第一个元素
pa_last=LA.elem+LA.length-1; 指针pa_last指向新表的最后一个元素
pb_last=LB.elem+LB.length-1; 指针pb_last指向新表的最后一个元素
while((pa<pa_last)&&(pb<pb_last)) 未达到LA和LB的表尾
{
if(*pa<*pb) *pc++=*pa++; 依次摘取两表中值较小的结点插入LC的最后
else *pc++=*pb++;
}
while(pa<=pa_last) *pc++=*pa++; 已达到LB表尾,依次将LA的剩余元素插入LC的最后
while(pb<=pb_last) *pc++=*pb++; 已达到LB表尾,依次将LA的剩余元素插入LC的最后
}
若对算法2.16中第一个循环语句的循环体进行如下修改:分出元素比较的第三种情况,当*pa==*pb时,只将两者之一插入LC,则该算法完成的操作和算法2.15相同,但时间复杂度却不同。
在算法2.16中,由于LA和LB中元素依值非递减,则对LB中的每个元素,不需要在LA中从表头至表尾进行全程搜索。如果两个表长分别记为m和n,则算法2.16循环最多执行的总次数为m+n。所以算法的时间复杂度为O(m+n)。
链式顺序表的合并:
假设头指针为LA和LB的单链表分别为线性表LA和LB的存储结构,现要归并LA和LB得到单链表LC。
因为链表结点之间的关系是通过指针指向建立起来的,所以用链表进行合并不需要另外开辟存储空间,可以直接利用原来两个表的存储空间,合并过程中只需把LA和LB两个表中的结点重新进行链接即可。
按照例2.2给出的合并思想,需设立3个指针pa、pb和pc,其中pa和pb分别指向LA和LB中当前待比较插入的结点,而pe指向LC中当前最后一个结点(LC的表头结点设为LA的表头结点)。指针的初值为:pa和pb分别指向LA和LB表中的第一个结点,pc指向空表LC中的头结点。
同算法2.16一样,通过比较指针pa和pb所指向的元素的值,依次从LA或LB中摘取元素值较小的结点插入到LC的最后,当其中一个表变空时,只要将另一个表的剩余段链接在pc所指结点
之后即可。
void MergeList_L(LinkList &LA,LinkList &LB,LinkList &LC)
{已知单链表LA和LB的元素按值非递减排列
归并LA和LB得到新的单链表LC,LC的元素也按值非递减排列
pa=LA->next; pb=LB->next; pa和pb的指针初值分别指向两个表的第一个结点
LC=LA; 用LA的头结点作为LC的头结点
pc=LC; pc的指针初值指向LC的头结点
while(pa&&pb)
{LA和LB均未到达表尾,依次“摘取”两表中值较小的结点插入到LC的最后
if(pa->data<=pb->data) 摘取pa所指结点
{
pc->next=pa; 将pa所指结点链接到pc所指结点之后
pc=pa; pc指向pa
pa=pa->next; pa指向下一结点
}
else
{
pc->next=pb; 将pb所指结点链接到pc所指结点之后
pc=pb; pc指向pb
pb=pb->next; pb指向下一结点
}
}
pc->next=pa?pa:pb; 将非空表的剩余段插入到pc所指结点之后
delete LB; 释放LB的头结点
}
可以看出,算法2.17的时间复杂度和算法2.16相同,但空间复杂度不同。在归并两个链表为一个链表时,不需要另建新表的结点空间,而只需将原来两个链表中结点之间的关系解除,重新按元素值非递减的关系将所有结点链接成一个链表即可,所以空间复杂度为O(1)。
案例分析与实现:
在2.2节我们通过3个典型案例引入了线性表这种数据结构,本节结合线性表的基本操作对
这3个案例进行进一步的分析,然后给出案例中有关算法的具体实现。
案例1.1:一元多项式的运算
【案例分析】
由2.2节的讨论我们已知,一元多项式可以抽象成一个线性表。在计算机中,我们可以采用数组来表示一元多项式的线性表。
利用数组p表示:数组中每个分量p[i]表示多项式每项的系数p(下标i),数组分量的下标i即对应每项的指数。数组中非零的分量个数即多项式的项数。
例如,多项式P(x)=10+5x的4次-4x的平方+3x的立方+2x的4次可以用表2.1所示的数组表示。
显然,利用上述方法表示一元多项式,多项式相加的算法很容易实现,只要把两个数组对应的分量项相加就可以了。
案例1.2:稀疏多项式的运算
【案例分析】
由2.2节的讨论我们已知,稀疏多项式也可以抽象成一个线性表。结合2.7节介绍的两个有序表的归并方法,可以看出,稀疏多项式的相加过程和归并两个有序表的过程极其类似,
不同之处仅在于,后者在比较数据元素时只出现两种情况(小于等于、大于),而多项式的相加过程在比较两个多项式指数时要考虑3种情况(等于、小于、大于)。
因此,多项式相加的过程可以根据算法2.16和算法2.17改进而成。
和顺序存储结构相比,利用链式存储结构更加灵活,更适合表示一般的多项式,合并过程的空间复杂度为O(1),所以较为常用。
本节将给出如何利用单链表的基本操作来实现多项式的相加运算。
例如,图2.22所示两个链表分别表示多项式A(x)=7+3x+9x的8次+5x的17次和多项式B(x)=8x+22x的7次-9x的8次。从图中可见,每个结点表示多项式中的一项。
如何实现用这种单链表表示的多项式的加法运算呢?
根据多项式相加的运算规则:对于两个多项式中所有指数相同的项,对应系数相加,若其和不为0,则作为“和多项式”中的一项插入“和多项式”链表中;
对于两个多项式中指数不相同的项,则将指数值较小的项插入“和多项式”链表中。“和多项式”链表中的结点无须生成,而应该从两个多项式的链表中摘取。
图2.22所示的两个多项式相加得到的和多项式如图2.23所示,图中的长方框表示已被释放的结点。
【案例实现】
用链表表示多项式时,每个链表结点存储多项式中的一个非零项,包括系数(coef)和指数(expn)两个数据域以及一个指针域(next)。对应的数据结构定义为:
typedef struct PNode
{
floatcoef; 系数
int expn; 指数
struct PNode*next; 指针域
}PNode,*Polynomial;
一个多项式可以表示成由这些结点链接起来的单链表,要实现多项式的相加运算,首先需要创建多项式链表。
多项式的创建:
多项式的创建方法类似于链表的创建方法,区别在于多项式链表是一个有序表,每项的位置要经过比较才能确定。
首先初始化一个空链表用来表示多项式,然后逐个输入各项,通过比较,找到第一个大于该输入项指数的项,将输入项插到此项的前面,这样即可保证多项式链表的有序性。
【算法描述】
void CreatePolyn (Polynomial &P,int n)
{输入n项的系数和指数,建立表示多项式的有序链表P
P=new PNode;
P->next=NULL; 先建立一个带头结点的单链表
for(i=1;i<=n;++i) 依次输入n个非零项
{
s=new PNode; 生成新结点
cin>>s->coef>>s->expn; 输入系数和指数
pre=P; pre用于保存q的前驱,初值为头结点
q=P->next; q初始化,指向首元结点
while(q&&q->expn<s->expn) 通过比较指数找到第一个大于输入项指数的项*q
{
pre=q;
q=q->next;
} //while
s->next=q; 将输入项s插入★q和其前驱结点pre之间
pre->next=s;
} //for
}
【算法分析】
创建一个项数为n的有序多项式链表,需要执行n次循环逐个输入各项,而每次循环又都要从前向后比较输入项与各项的指数。在最坏情况下,第n次循环需要进行n次比较,因此,间复杂度为O(n平方)
多项式的相加:
创建两个多项式链表后,便可以进行多项式的加法运算了。
假设头指针为Pa和Pb的单链表分别为多项式A和B的存储结构,指针pl和p2分别指向A和B中当前进行比较的某个结点,
则逐一比较两个结点中的指数项,对于指数相同的项,对应系数相加,若其和不为0,则插入“和多项式”链表中;
对于指数不相同的项,则通过比较,将指数值较小的项插入“和多项式”链表中。
【算法描述】
void AddPolyn(Polynomial&Pa, Polynomial &Pb)
{多项式加法:Pa=Pa+Pb,利用两个多项式的结点构成“和多项式”
pl=Pa->next;p2=Pb->next; p1和p2初始时分别指向Pa和Pb的首元结点
p3=Pa; p3指向和多项式的当前结点,初值为Pa
while(p1&sp2) p1和p2均非空
{
if(p1->expn==p2->expn) 指数相等
{
sum=p1->coef+p2->coef; sum保存两项的系数和
if(sum!=0) 系数和不为 0
{
p1->coef=sum; 修改Pa当前结点的系数值为两项系数的和
p3->next=p1;p3=p1; 将修改后的Pa当前结点链接在p3之后,p3指向p1
p1=p1->next; p1指向后一项
r=p2;p2=p2->next;delete r; 删除Pb当前结点,p2指向后一项
}
else 系数和为0
{
r=pl;pl=p1->next; delete r; 删除Pa当前结点,p1指向后一项
r=p2;p2=p2->next; delete r; 删除Pb当前结点,p2指向后一项
}
}
else if(pl->expn<p2->expn) Pa当前结点的指数值小
{
p3->next=pl; 将p1链接在p3之后
p3=p1; p3指向pl
p1=p1->next; p1指向后一项
}
else Pb当前结点的指数值小
{
p3->next=p2; 将p2链接在p3之后
p3=p2; p3指向p2
p2=p2->next; p2指向后一项
}
} //while
p3->next=p1?pl:p2; 插入非空多项式的剩余段
delete Pb; 释放Pb的头结点
}
【算法分析】
假设两个多项式的项数分别为m和n,则同算法2.17一样,该算法的时间复杂度为O(m+n),空间复杂度为O(1)。
对于两个一元多项式减法和乘法的运算,都可以利用多项式加法的算法来实现。
减法运算比较简单,只需要先对要减的多项式的每项系数进行取反,再调用加法运算AddPolyn即可。
多项式的乘法运算可以分解为一系列的加法运算。
假设A(x)和B(x)为式(2-1)的多项式,则:
其中,每一项都是一个一元多项式。
多项式相加的例子说明,对于一些有规律的数学运算,借助链表实现是一种解决问题的途径。
小结:
线性表是整个数据结构课程的重要基础,本章主要内容如下。
(1) 线性表的逻辑结构特性是指数据元素之间存在着线性关系,在计算机中表示这种关系的两类不同的存储结构是顺序存储结构(顺序表)和链式存储结构(链表)。
(2) 对于顺序表,元素存储的相邻位置反映出其逻辑上的线性关系,可借助数组来表示。给定数组的下标,便可以存取相应的元素,可称为随机存取结构。
而对于链表,其是依靠指针来反映其线性逻辑关系的,链表结点的存取都要从头指针开始,顺链而行,所以不属于随机存取结构,可称之为顺序存取结构。不同的特点使得顺序表和链表有不同的适用情况。
表2.2分别从空间、时间和适用情况3个方面对二者进行了比较:
(3) 对于链表,除了常用的单链表外,在本章还讨论了两种不同形式的链表,即循环链表和双向链表,它们有不同的应用场合。
表2.3对三者的几项有差别的基本操作进行了比较:
学习完本章后,读者应熟练掌握顺序表和链表的查找、插入和删除算法,链表的创建算法,并能够设计出线性表应用的常用算法,比如线性表的合并等;能够从时间和空间复杂度的角度比较两种存储结构的不同特点及其适用场合,明确它们各自的优缺点。