1.什么是双向带头循环链表?
双向带头循环链表是数据结构里众多链表中的一种,这种类型的链表是一种非常常用的链表。他的整体结构是首先在最开始是有一个哨兵位来当作一个头节点,这个头节点是不需要存放数据的(当然想存放数据也是可以的)。在我们使用这种类型的链表的时候,由于有这个哨兵位头节点的存在,因此我们就不需要来考虑链表是否为空的问题,在使用的过程中更加方便;其次,对于这种链表,他每一个节点都是有一个前驱指针和后继指针,这里我就把前驱指针叫做prve,后继指针叫做next。他的prve指针指向的是前一个节点的地址,next指针指向的是下一个节点的地址。但是双向带头循环链表中,由于要实现他"循环"的性质,因此头节点的prve指针指向的是链表中最后一个节点,而最后一个节点的next指针指向的是头节点
这就构成了双向带头循环链表。
2.双向带头循环链表的实现
1.链表的初始化
要完成这个链表的初始化,首先在对于节点的定义上我们就比单链表多了一个前驱指针prve指针。
那对于初始化,既然要实现一个双向带头循环的,那首先就得把这个头节点给创建出来,然后再让他的next和prve指针都指向自己。
大概就如上图所示。
那代码的实现就非常简单了。
DL* DLInit()
{
DL* phead = DListCreatBuyNode(-1);
phead->next = phead;
phead->prve = phead;
phead->val = 0;
return phead;
}
这段代码的思路就是首先创建一个头节点,这个头节点我把-1存放到了里面,然后让头节点的next指向自己,让prve指向自己。而创建节点的代码可以把他写到一个函数里面,如下。
DL* DListCreatBuyNode(DListDataType val)
{
DL* node = (DL*)malloc(sizeof(DL));
if (node == NULL)
{
perror("newnode malloc fail!");
return;
}
node->val = val;
node->next = NULL;
node->prve = NULL;
return node;//最后返回malloc出来的节点
}
2.链表的尾插
对于链表的尾插,我们首先要想清楚他的逻辑,如果想要实现尾插的话,首先得找到最后一个节点,俗称找尾,找到了尾节点之后,再进行尾插,在这里的尾插和在单链表上的尾插不一样,因为要时刻保证他是一种双向带头循环的结构,要保证他是循环的,因此在尾插的时候还是有比较多需要注意的地方。
首先来说一下他尾插的整体思路:
如果想实现一个双向链表的尾插,就首先是需要找到该链表的尾节点
那该怎么找到这个尾节点?在双向循环链表中,整个链表都是循环的,因此不能像单链表那样把寻找尾的判断条件变成tail->next==NULL,这样在双向循环链表中,这个循环是永远都出不来了的。因此,我们可以考虑把条件改成如果tail->next!=head的话,就继续寻找链表中的尾节点,如果找到了尾节点,就用尾节点的next指向插入的节点,然后用新插入节点的prve指向尾节点,新节点的next指向哨兵位,哨兵位的prve指向新插入的节点。这个就是尾插的大体思路。
void DLPushBack(DL* phead, DListDataType val)
{
DL* newnode = DListCreatBuyNode(val);
DL* newhead = phead;
DL* Tail = phead->prve;
Tail->next = newnode;
newnode->prve = Tail;
newnode->next = newhead;
newhead->prve = newnode;
}
3.链表的头插
那对于链表的头插来说,如果在这里我们没有用哨兵位头节点的话,就得判断链表是否为空的状态。但在这里是已经有了头节点,那么这个链表里面就不存在链表为空的情况,是一直都有东西的。因此我们直接把一个新的节点插入到头节点哨兵位的后面就可以了。
那总体思路就是: 创建一个新节点,让新节点node->next指向phead->next,phead->next->prve指向node,phead->next指向node,node->prve指向phead。ok打完收工!
void DLPushFornt(DL* phead, DListDataType val)
{
DL* newnode = DListCreatBuyNode(val);
DL* cur = phead->next;
newnode->next = cur;
cur->prve = newnode;
phead->next = newnode;
newnode->prve = phead;
}
在这里以防混淆,我把phead的next用cur来代替~
4.链表的尾删
而双向链表的尾删,大体思路则是首先找到尾节点,让尾节点前一个的next指向哨兵位头节点,让哨兵位头节点的next指向tail->prve。最后再把尾节点给释放掉。
void DLPopBack(DL* phead)
{
assert(phead->next!=phead);
DL* Tail = phead->prve;
DL* newprve = Tail->prve;
DL* newhead = phead;
newprve->next = newhead;
newhead->prve = newprve;
free(Tail);
}
5.链表的头删
而对于链表的头删,就是把哨兵位头节点的下一个节点释放掉。
以这张图为例,如果我想把cur节点给释放掉,那就得让phead->next指向curnext,curnext->prve指向phead,最后把cur节点给释放掉~
void DLPopFront(DL* phead)
{
assert(phead->next != phead);
DL* cur = phead->next;
DL* next = cur->next;
DL* newhead = phead;
newhead->next = next;
next->prve = newhead;
free(cur);
}
6.在指定节点位置插入一个新的节点
其实这个在指定位置插入节点的逻辑跟头插是差不多的,只不过从改动phead变成了改动链表中节点的链接关系。而由于链表是malloc出来的,每一个节点的地址都是随机的,因此不能像数组那样对下标操作,从而进行准确的插入,在链表中,只能根据值来进行插入,因而在"指定位置插入"其实并不完全指定位置,只能是一个比较模糊的概念。
就比如上面这张图,如果我想把node节点插入到curnext这个位置的话,那么我就可以先让cur->next指向node,node->next指向curnext,curnext->prve指向node,node->prve指向cur。而在传参的时候,我们可以传第一个是链表地址,第二个是想修改的节点的值,第三个则是新节点。
代码如下:
void DLInsert(DL* phead, DListDataType pos, DListDataType val)
{
assert(phead != NULL);
DL* pHead = phead->next;
DL* pTail = phead->next;
DL* InsertNode = DListCreatBuyNode(val);
DL* FindNode = DSLTFindNode(phead, pos);
while (pTail != phead)
{
if (pTail == FindNode)
{
break;
}
pHead = pTail;
pTail = pTail->next;
}
InsertNode->next = pTail;
pHead->next = InsertNode;
pTail->prve = InsertNode;
InsertNode->prve = pHead;
}
7.删除指定节点
比如我想删除node节点,那么在删除node节点的时候,我们可以把curnext->next指向tail,tail的prve指向node,node->next 指向tail,node->prve指向curnext。最后再把node节点给free掉就行了
void DLPop(DL* phead, DListDataType pos)
{
assert(phead->next != phead);
DL* pHead = phead->next;
DL* Del = DSLTFindNode(phead, pos);
DL* pTail = phead->next;
//DL* cur = phead->next;
if (phead->next == Del)
{
DLPopFront(phead);
return;
}
while (pTail != phead)
{
if (pTail == Del)
{
break;
}
pHead = pTail;
pTail = pTail->next;
}
pHead->next = pTail->next;
pTail->next->prve = pHead;
}