介绍了单链表,提到了单链表的一些限制,基于单链表,本文介绍循环链表和双向链表,可以解决一些对于单链表来说比较复杂的问题,对单链表不熟的可以去看上一篇文章
循环链表
尾结点的指针域指向头结点
使用头指针不方便,多使用尾指针
带尾指针的循环链表的合并
-
存表头结点
-
Tb表头连接到Ta表尾
-
释放Tb表头空间
-
修改指针
❓疑惑:第一步先将Tb的next域指向Ta的头结点不可以吗?这样可以省去存头结点的步骤
算法描述
LinkList Connect(LinkList Ta,LinkList Tb){
p=Ta->next;
Ta->next=Tb->next->next;
delete Tb->next;
Tb->next=p;
}
时间复杂度:O(1)
双向链表
双向链表有两个指针域
双向链表的结构定义
typedef struct DuLNode{
Elemtype data;
struct DuLNode *prior,*next;
}DuLNode,*DuLinkList;
双向循环链表
- 头结点的前驱指针指向尾结点
- 尾结点的后继指针指向头结点
- 空表的前驱指针和后继指针都指向自身
双向链表结构具有对称性,p->next->prior=p->prior->next
基本操作算法
获取表长、取值、查找算法和单链表的相同
插入
共需要操作四个指针:
- s的前驱
- p的前驱的后继(即a的后继,但是没有a的指针)
- s的后继
- p的前驱
p的前驱在最后修改,防止找不到p的前驱
算法描述
void ListInsert_DuL(DuLLinkList &L;int i,Elemype e){
if(!(p=GetElemP_DuL(L,i))) //用已有方法将p指向第i个位置,若该位置不合法,返回error
return ERROR;
//创建新结点
s=new DuLNOde;
s->data=e;
//插入新结点
s->prior=p->prior;
p->prior->next=s;
s->next=p;
p->prior=s;
return OK;
}//ListInsert_DuL
时间复杂度😮(n)
删除
删除p结点,共需操作两个指针:
- p的前驱的后继
- p的后继的前驱
算法描述
void ListDelete_DuL(DuLink &L,int i,ElemType &e){ //删除带头结点的双向循环链表L的第i个元素,并用e返回
if(!(p=GetElemtP_DuL(L,i)))
return ERROR;
e=p->data;
//删除
p->prior->next=p->next;
p->next->prior=p->prior;
delete p;
return OK;
}//ListDelete_DuL
时间复杂度😮(n)
时间效率对比
查找表头结点(首元结点) | 查找表尾结点 | 查找结点*p的前驱结点 | |
---|---|---|---|
带头结点的单链表 | L->next O(1) | 从L->next向后遍历 O(n) | 通过p->next找不到前驱结点 |
带头结点仅设头指针的循环单链表 | L->next O(1) | 从L->next向后遍历 O(n) | 能找到前驱结点 O(n) |
带头结点仅设尾指针的循环单链表 | R->next O(1) | R O(1) | 能找到前驱结点 O(n) |
带头结点的双向循环链表 | L->next O(1) | L->prior O(1) | p->prior O(1) |
顺序表和链表的比较
链式存储结构的优点:
- 结点空间可以动态申请和释放
- 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素
链式存储结构的缺点:
- 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大
存储密度 = 结点数据本身占用的空间 结点占用的空间总量 存储密度=\frac{结点数据本身占用的空间}{结点占用的空间总量} 存储密度=结点占用的空间总量结点数据本身占用的空间
一般的,存储密度越大,存储空间的利用率就越高。顺序表的存储密度为1,链表的存储密度小于1
- 链式存储结构是非随机存取结构。对任意结点的操作都要从头指针依次查找该结点,增加了算法的复杂度