目录
一. 循环链表
定义:是一种头尾相接的链表。表中最后一个结点的指针域指向头结点,整个链表形成一个环。
优点:从表中任意结点出发都可以找到其他结点。
注意:由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p->next是否为空,而是判断它们是否等于头指针。
尾指针:指向最后一个结点的指针,使用尾指针可以减少寻找头,尾结点的时间复杂度。(尾指针寻找头尾结点的时间复杂度都是O(1),头指针找最后一个结点的时间复杂度为O(n))
算法:带尾指针的两个循环链表的合并
算法步骤:(1)p存Ta表头结点(合并后的表头结点就是Ta的表头结点)(p=Ta->next);(2)Tb表头连接到Ta表尾(Ta->next=Tb->next->next);(3)释放Tb表头结点(delete Tb->next);(4)修改指针把Tb最后一个结点连接到Ta头结点(Tb->next=p);
LinkList Connect(LinkList Ta,LinkList Tb){ //带尾指针循环链表的合并
p=Ta->next;
Ta->next=Tb->next->next;
delete Tb->next;
Tb->next=p;
return Tb; //新链表的尾指针还是Tb
}
此算法的时间复杂度是O(1)。
二. 双向链表
单链表的结点→有指示后继的指针域→找后继结点方便;即:查找某结点的后继结点的执行时间为O(1))。无指示前驱的指针域→找前驱结点难:从表头出发查找。即:查找某结点的前驱结点的执行时间为O(n)。所以,在单链表的每个结点里再增加一个指向其直接前驱的指针域prior,这样链表中就形成了有两个方向不同的链,故称为双向链表。
数据结构类型可以表示如下:
typedef struct DuLNode //声明结点类型和指向结点的指针类型
{
ElemType data; //结点的数据域
struct DuLNode *prior,*next; //结点的指针域,包含前后两个指针
}DuLNode,*DuLinkList;
类似的也可以定义双向循环链表:让头结点的前驱指针指向链表的最后一个结点,让最后一个结点的后继指针指向头结点。
双向链表的指针满足对称性:
p->prior->next=p=p->next->prior
在双向链表中有些操作(如:ListLength、GetElem等),因仅涉及一个方向的指针,故它们的算法与线性链表的相同。但在插入、删除时,则需同时修改两个方向上的指针,两者的操作的时间复杂度均为O(n)。
双向链表的插入操作
主要分以下四步进行:
void ListInsert_DuL(DuLinkList &L,int i,ElemType e){
//在带头结点的双向循环链表L中第i个位置插入元素e
if(!(p=GetElemP_DuL(L,i))) return ERROR; //GetElemP_DuL查找函数,找不到报错
s=new DuLNode;
s->data=e; //建立新结点并把e放入
//此时指针p指向第i个结点,在第i个结点之前插入
s->prior=p->prior;
p->prior->next=s;
s->next=p;
p->prior=s;
return OK;
}
双向链表的删除操作
分两步进行:(1)修改a的next域指向c;(2)修改c的prior域指向a;
void ListDelete_DuL(DuLinkList &L,int i,ElemType e){
//删除带头结点的双向循环链表L中第i个元素,数据用e返回
if(!(p=GetElemP_DuL(L,i))) return ERROR; //GetElemP_DuL查找函数,找不到报错
e=p->data; //此时p指针指向第i个元素
p->prior->next=p->next; //a的next域指向c
p->next->prior=p->prior; //c的prior域指向a
free(p);
return OK;
}
三. 几种链表的比较
四. 顺序表和链表的比较
链式存储结构的优点:(1)结点空间可以动态申请和释放;(2)数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素。
链式存储结构的缺点:(1)存储密度小(存储密度是指结点数据本身所占的存储量和整个结点结构中所占的存储量之比,显然顺序表的存储密度是1),每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大;(2)链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,这增加了算法的复杂度。
五. 线性表的应用
(1)线性表的合并
问题描述:假设利用两个线性表La和Lb分别表示两个集合A和B,现要求一个新的集合A=AUB。eg.给定La=(7,5,3,11)和Lb=(2,6,3),合并后La=(7,5,3,11,2,6)
算法步骤:依次取出Lb中的每个元素,重复下面两步:1.在La中查找该元素;2.如果找不到,将其插入到La的最后;此算法的时间复杂度是O(La_len*Lb_len)
void Union(List &a,List b){
La_len=ListLength(La);
Lb_len=ListLength(Lb);
for(i=1,i<=Lb_len,i++){
GetElem(Lb,i,e);
if(!LocateElem(La,e)) ListInsert(&La,++La_len,e)
}
}
(2)有序表的合并
问题描述:已知线性表La和Lb中的数据元素按值非递减有序排列,现要求将La和Lb归并为一个新的线性表Lc,且Lc中的数据元素仍按值非递减有序排列。eg.给定La=(1,7,8)和Lb=(2,4,6,8,10,11),合并后Lc=(1,2,4,6,7,8,8,10,11)
算法步骤:(1)创建一个空表Lc;(2)依次从La或Lb中“摘取”元素值较小的结点插入到Lc表的最后,直至其中一个表变空为止;(3)继续将La或Lb其中一个表的剩余结点插入在Lc表的最后;
a. 顺序表实现
void MergeList_Sq(SqList La,SqList Lb,SqList &Lc){ //顺序表实现
pa=La.elem;
pb=Lb.elem; //指针pa和pb的初值分别指向La,Lb第一个元素
Lc.length=La.length+Lb.length; //新表长等于两个原表长之和
Lc.elem=new ElemType[Lc.length]; //为新表分配空间
pc=Lc.elem; //指针pc指向Lc第一个元素
pa_last=La.elem+(La.length-1);
pb_last=Lb.elem+(Lb.length-1); //这两个指针指向列表最后一个元素,表示结束
while(pa<=pa_last&&pb<=pb_last){
if(*pa<*pb) *pc++=*pa++; //判断pa指向的值是否小于pb指向的值
//将pa指向的值赋给pc指向的位置,并且将pc的值增加1
//这里的*pc++表示先取pc指向的值,然后将pc指针向后移动一个位置
else *pc++=*pb++;
}
while(pa<=pa_last) *pc++=*pa++; //Lb表已到表尾,将La表其余部分加到Lc最后
while(pb<=pb_last) *pc++=*pb++; //La表已到表尾,将Lb表其余部分加到Lc最后
}
注意:*pc++=*pa++的意思就是pa赋值pc然后pa和pc自增,指向后一位。之所以可以指针++,是因为顺序表中元素在物理上相邻,在链表中就不可以用指针++来使指针指向下一个数据元素。
此算法的时间和空间复杂度都是O(ListLength(La)+ListLength(Lb))。
b. 链表实现
此时pa->data>pb->data:
void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc){
pa=La->next;
pb=Lb->next; //此时pa,pb指向首元结点
pc=Lc=La; //La的头结点作为Lc的头结点
while(pa&&pb){
if(pa->data<pb->data){
pc->next=pa; //pa较小,当前结点的next域指向pa结点
pc=pa; //pc移动到pa结点处
pa=pa->next; //pa指向La下一个结点处
}
else{pc->next=pb;pc=pb;pb=pb->next;}
}
pc->next=pa?pa:pb; //相当于一个判断,加入剩余链表
delete pb; //删除pb头结点
}
六. 几个案例分析与实现
(1)一元线性多项式的运算
加,减,乘(操作对应位即可)
(2)稀疏多项式的运算
对稀疏多项式,构建线性表
,以求和运算为例:
若采用顺序存储:(1)创建一个新数组c;(2)分别从头遍历比较a和b的每一项。若指数相同,对应系数相加,若其和不为零,则在c中增加一个新项;若指数不相同,则将指数较小的项复制到c中;(3)当一个多项式已遍历完毕时,将另一个剩余项依次复制到c中即可。
采用顺序存储存在的问题:内存分配不灵活,所以推荐采用链式存储:
采用链式存储的算法步骤:
(1)指针p1和p2初始化,分别指向Pa和Pb的首元结点。
(2)p3指向和多项式的当前结点,初值为Pa的头结点。
(3)当指针p1和p2均未到达相应表尾时,则循环比较p1和p2所指结点对应的指数值
(p1->expn与p2->expn),有下列3种情况:
a.当p1->expn=p2->expn时,则将两个结点中的系数相加。若和不为零,则修改p1所指结点的系数值,同时删除p2所指结点;若和为零,则删除p1和p2所指结点;
b.当p1->expn<p2->expn时,则应摘取p1所指结点插入到“和多项式”链表中去;
c.当p1->expn>p2->expn时,则应摘取p2所指结点插入到“和多项式”链表中去
(4)将非空多项式的剩余段插入到p3所指结点之后。
(5)释放Pb的头结点。