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