数据结构之循环链表和双向链表
1.循环链表
循环链表是另一种形式的链式存储结构,其特点是单链表的最后一个结点的指针不为空,而是指向头结点(带头结点的链表)或第一个结点(不带头结点的链表),整个链表形成一个环。这样从链表任何一个位置出发,均可以找到表中的其他结点。循环链表根据指针域是一个还是多个,可以分为单循环链表和多重循环链表。
对于单循环链表而言,判断链表是否为空不再是panda头结点的指针是否为空,而是判断头结点的指针是否指向自身,如上图所示。循环链表基本操作的算法和单链表没什么区别,只是循环结束条件(表尾)不再是判断指针p
或p->next
是否为空,而是判断他们是否等于头结点。
有些时候,循环链表中设一个指向表尾的尾指针对某些应用更加方便。这样,尾指针访问头结点和尾结点都非常方便,相当于既指向了头结点,又指向了尾结点。如把由尾指针指引的单循环链表表示的线性表A和B进行合并(即将A和B合并成一个链表),实现起来非常方便。只需要修改两个指针即可,其运算时间为O(1)。代码如下:
q = B->next; // q指向B的头结点
B->next = A->next; // B的指针指向A的头结点
A->next = q->next; // A的为节点指针指向B的第一个结点,B链接到A上
A = B; // A指向B,作为合并后链表的尾指针
free(q); // 释放B的头结点
2.双向链表
在单链表中,对于任何一个结点通过其指向后继的指针,就可以直接访问其后继结点,其时间复杂度为O(1)。显然,在单链表中访问其前驱结点,必须要从表头结点顺链查找,其时间复杂度为O(n)。显然,在单链表中访问某个元素的前驱十分不便。如果在链表的结点结构中同时包含分别指向其前驱和后继结点的两个指针域,就很容易解决单链表中访问前驱困难的问题,这种链表称作双向链表。
如图(a)所示,双向链表的结点结构除了数据域之外,还包含两个指针域(用于指向前驱结点的前驱指针域和用于指向后继结点的后继指针域)。其结点结构定义如下:
typedef struct DNode{
ElemType data;
struct DNode *prior; // 指向前驱结点的指针域
struct DNode *next; // 指向后继结点的指针域
}DListNode,*DLinkList;
如图(b) (c)所示,对于带有头结点的双向链表,其头结点prior
域为空,尾结点的next
域为空;显然对于一个空的双向链表,两个指针域均为空。
双向链表也可以是循环链表,双向循环链表的头结点的前去指针prior指向表尾结点,尾结点的后继指针next
指向头结点;对于一个空表,头结点的两个指针均指向头结点自身。
在双向链表中,诸如计算表的长度定位元素和获取元素等操作,仅涉及一个方向的指针,这些操作与单链表的操作相同。由于需要修改两个方向上的指针,在双向链表中进行插入和删除等操作与单链表有较大的差异。
1.在双向链表中插入元素
在长度为n的双向链表的第 i(1≤i≤n+1)个位置上插入元素时,需要修改两个方向上的指针。在线性表的第i个位置插入元素e时,需要通过修改指针将含有的数据元素e的结点链接到元素ai-1和ai所在结点之间。
基本过如下:
- 找到第i个结点(插入位置),将指针p指向该结点,包含数据元素e的结点将链接到此结点的前驱和后继节点之间。注意,如果是在第i+1个位置上插入元素,则p指向头结点。
- 产生一个新结点s,并将数据元素e存放到该结点的数据域中
- 对指针进行操作,具体如代码所示
Status InsertList(DLinkList H, int i, ElemType e){
if(!H)
return Err_InvalidParam;
DListNode *p,*s;
int j = 1;
p = H->next;
while(p!=H && j<i){
p = p->next;
j++;
}
if(j>i || (p==H&&j<i))
return Err_IllegalPos;
s = (DListNode*)malloc(sizeof(DListNode));
if(!s)
return Err_Memory;
s->data = e;
s->prior = p->prior; // 将新结点s的前驱指针指向p的前驱结点
p->prior->next = s; // 将p的前驱的后继指针指向s
s->next = p; // 将s的后继指针指向p
p->prior = s; // 将p的前驱指针指向s
return OK;
}
注意,代码中对于指针的更新涉及到四步,前两步必须先更新插入结点s的前驱指针与p先驱结点的后继指针。因为如果先进行下两步的操作,将p的前驱指针指向s,则此时无法通过p的前驱指针得到p的前驱元素,从而无法将s链接到链表中。
2. 在双向链表中删除元素
与单链表相同,对于双向链表,删除线性表知道位置的数据元素,只需把指定位置的结点从双向链表中删除,并释放该结点空间即可。
算法基本过程如下:
Status DeleteList(DLinkList H, int i, ElemType *e){
DListNode *p;
if(!H)
return Err_InvalidParam;
DListNode *p;
p = H->next;
int j = 1;
while(j<i && p!=H){
p = p->next;
j++;
}
if(p==H || j>i)
return Err_IllegalPos;
*e = p->data;
p->prior->next = p->next; // 将p的前驱结点的后继指针指向p的后继结点
p->next->prior = p->prior; // 将p的后继结点的前去指针指向p的前驱
free(p);
return OK;
}
注意,对指针的更新涉及到的两步顺序可以不同