目录
1.概念
双向带头循环链表(下面统称该链表)和普通链表区别,很明显:
1、双向。 是指该链表有两个指针域,一个和普通链表一样,指向后一个节点(next),另一个指向前一个节点(prev)。
2、带头。 是指该链表有哨兵位,该位置不存储任何数据,只是为了帮我们定位该链表。
3、循环。 是指该链表头尾连接。
如下,是该链表每一个节点的定义,以及图示。
typedef int LTDataType;
typedef struct LinkListNode
{
LTDataType data;
struct LinkListNode* prev;
struct LinkListNode* next;
}LTNode;
2.示例
如下图,是一个双向带头循环链表的示例,可以看出,哨兵位头节点的prev指针指向该链表的尾节点,尾节点的next指针指向该链表的哨兵位头节点 :
那么,该链表为空的时候,其结构如何呢? 无论该链表是否为空,哨兵位都存在,那么,哨兵位既是头节点,又是尾节点,所以其prev指针指向自己,而next指针也指向自己。如下图,实际上,该链表为空即该链表初始化。
3.实现
3.1接口
如下,其各个接口和普通链表类似,涉及头插、尾插、头删、尾删、任意节点的插入和删除等等。
3.2生成一个节点
生成一个节点,该接口只需要传入 生成的节点的数据域的值 即可,两个指针指向NULL,然后返回指向该节点的指针。
LTNode* BuyLinkListData(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
3.3初始化
初始化即生成一个哨兵位头节点,和示例中一样,prev指针和next指针都指向自己,然后返回指向该哨兵位的指针。
LTNode* LinkListInit()
{
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
phead->next = phead;
phead->prev = phead;
return phead;
}
3.4插入和删除的特点
如图,由于该链表是循环的,所以可以这样画出来。无论哪个位置是哨兵位,只要非空,那么删除和插入操作都没有太大的区别,任一节点的前后节点都是LTNode类型的数据,要插入数据,就和某个位置的前后节点有关;要删除数据,就和删除节点的前后节点有关。(本质上是前一个节点的next指针 和 后一个节点的prev指针)。
3.5删除
首先要考虑是该链表是否为空,为空时上文已经说过,就是只有哨兵位头节点,且其两个指针都指向自己,所以如果 phead->next == phead; 那么 这个双向带头循环链表就是空的,就不能进行头删操作。这个判断用 assert(phead->next != phead); 即可。
不为空时,如下图,要删除头节点cur,直接暴力一点,把该节点拿出来。下图中可以看出,和该节点关联的指针有四个,其中 ①和②(紫色)都是该节点的指针,所以不用去管(反正都要被删除),那么之只剩下cur节点的前一个节点(记为Prev)的指针 和 cur节点的后一个节点(记为Next)的指针,只要处理好这两个节点的指针,那么头删就算是完成。看到图中最下面的部分,只需要Prev节点的next指针指向Next,Next节点的prev指针指向Prev即可。(注意区分大小写)
头删
void LinkListPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* pphead = phead;
LTNode* Nnext = phead->next->next;
LTNode* next = phead->next;
//删除操作
pphead->next = Nnext;
Nnext->prev = pphead;
free(next);
}
尾删
void LinkListPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* Tail = phead->prev;
LTNode* NewTail = Tail->prev;
//删除操作
NewTail->next = phead;
phead->prev = NewTail;
free(Tail);
}
删除pos位置的数据
//删除pos位置的数据
void LinkListErase(LTNode* phead, LTNode* pos)
{
assert(phead);
assert(pos);
assert(phead->next != phead);
LTNode* Prev = pos->prev;
LTNode* Next = pos->next;
Prev->next = Next;
Next->prev = Prev;
}
3.6插入
相比删除,插入操作就不需要考虑 该链表是否为空。如下图,要在pos节点之前插入一个节点。
首先要获得pos节点前面一个节点的指针,并记为Prev。那么我们现在所拥有的,就是pos、Prev和新节点newnode。对于Prev和pos节点,需要更改的就是 Prev的next指针 和 pos的prev指针(绿色部分),而newnode节点有两个指针,一个prev指针要指向Prev,另一个next指针要只想pos(紫色部分)。
在这里可以不用考虑这几个指针先后使用顺序,因为我们已经有了Prev、pos、newnode,需要更改的是Prev->next 、pos->prev 、 newnode->prev 、newnode->next ,这几个指针要指向的位置也只是在Prev、pos、newnode这三个里面的,所以其先后更改顺序无所谓。
头插
void LinkListPushFront(LTNode* phead, LTDataType x)
{
//断言
assert(phead);
LTNode* newnode = BuyLinkListData(x);
LTNode* next = phead->next;
//插入操作
phead->next = newnode;
newnode->prev = phead;
newnode->next = next;
next->prev = newnode;
}
尾插
void LinkListPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyLinkListData(x);
LTNode* Tail = phead->prev;
//插入操作
Tail->next = newnode;
newnode->prev = Tail;
newnode->next = phead;
phead->prev = newnode;
}
pos位置之前插入
//在pos前面插入数据
void LinkListInsert(LTNode* phead, LTNode* pos, LTDataType x)
{
assert(phead);
assert(pos);
LTNode* newnode = BuyLinkListData(x);
LTNode* Prev = pos->prev;
Prev->next = newnode;
newnode->prev = Prev;
newnode->next = pos;
pos->prev = newnode;
}
3.7打印数据
遍历每个节点并打印数据域的数据即可,注意循环结束条件,当指针回到指向哨兵位,那么就是遍历完了。
void LinkListPrint(LTNode* phead)
{
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
}
3.8销毁
同理打印,遍历然后free,区别就是,销毁要先存好下一个节点的指针。
void LinkListDestory(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* temp = cur->next;
free(cur);
cur = temp;
}
//free(phead);
}
关于双向带头循环链表的介绍就到这里,希望多多支持!!!