链表的结构分为8中,其实搞懂了单链表和双向哨兵位循环链表,这部分的知识也就掌握的差不多了。双向哨兵位循环链表的结构如下:
下面我从0构建一个双向哨兵位循环链表。
1、准备工作
构建节点结构体,双向循环链表的每一个节点内需要有两个指针变量,一个指针变量指向前一个节点,另一个指针变量指向后一个节点。这里将指向前一个节点的指针变量命名为prev,指向后一个节点的指针变量命名为next,那么节点的结构可以定义为:
typedef int LDataType;
typedef struct LNode
{
LDataType data;
struct LNode* prev;
struct LNode* next;
}LNode;
存储的数据类型为LDataType类型。
2、链表的初始化与销毁
在链表的初始化部分,我们需要创建一个哨兵位节点用于后面链表的搭建。它的结构可以定义为下面的形式:
哨兵位节点的prev和next指针均指向自己本身从而形成循环,代码可以写成如下的形式:
LNode* LNodeCreat()
{
LNode* pHead = (LNode*)malloc(sizeof(LNode));
if (pHead == NULL)
{
perror("malloc fail");
return NULL;
}
pHead->next = pHead->prev = pHead;
return pHead;
}
链表的销毁部分应该考虑到后续插入来的节点,应该遍历整个链表将每一个节点进行释放并置空,最后再销毁哨兵位的头节点。
void LNodeDestroy(LNode* pHead)
{
assert(pHead);
LNode* cur = pHead->next;
while (cur != pHead)
{
LNode* next = cur->next;
free(cur);
cur = next;
}
free(pHead);
}
3、链表的末尾插入
这里将创建一个新的节点封装成一个函数,为了方便后面继续使用。创建的新节点的prev和next均指向NULL,新节点内包含的数据为用户需要插入的数据val.下面是创建节点的函数实现:
LNode* BuyNewNode(LDataType val)
{
LNode* newNode = (LNode*)malloc(sizeof(LNode));
if (newNode == NULL)
{
perror("BuyNewNode malloc fail");
return NULL;
}
newNode->prev = newNode->next = NULL;
newNode->data = val;
}
创建的新节点的逻辑结构可以想象成下面的形式:
我们知道,链表的尾插是需要找到尾节点的,在这里可以很方便的找到链表的尾节点tail,因为哨兵位的头节点指向的就是链表的尾节点,所以说不管原链表中是否存在节点在这里均可视为一种情况,插入的逻辑图为:
代码的实现如下:
void LNodePushBack(LNode* pHead, LDataType val)
{
assert(pHead);
LNode* newNode = BuyNewNode(val);
LNode* tail = pHead->prev;
tail->next = newNode;
newNode->prev = tail;
pHead->prev = newNode;
newNode->next = pHead;
}
4、链表的打印
上面实现了链表的尾插,为了验证上述代码的正确性,这里先来写链表的打印,以便及时发现存在的问题。这里的思想就是遍历整个链表打印节点中的数据。
void LNodePrint(LNode* pHead)
{
assert(pHead);
LNode* cur = pHead->next;
while (cur != pHead)
{
printf("%d <=>", cur->data);
LNode* next = cur->next;
cur = next;
}
printf("\n");
}
这里先来测试一下上述代码能否正常实现双向带头链表的功能。
//测试代码
void test()
{
LNode* pList = LNodeCreat();
LNodePushBack(pList, 1);
LNodePushBack(pList, 2);
LNodePushBack(pList, 3);
LNodePushBack(pList, 4);
LNodePrint(pList);
LNodeDestroy(pList);
}
int main()
{
test();
return 0;
}
输出结果:
5、链表的头部插入
头插的操作是十分简单的,在插入之前记录一下第一个节点的位置用于和新创建的节点链接。逻辑结构可以化成如下的形式:
代码实现如下:
void LNodePushFront(LNode* pHead, LDataType val)
{
assert(pHead);
LNode* newNode = BuyNewNode(val);
LNode* first = pHead->next;
newNode->prev = pHead;
pHead->next = newNode;
newNode->next = first;
first->prev = newNode;
}
6、链表的尾删
逻辑结构如下图所示:
代码实现如下:
void LNodePopBack(LNode* pHead)
{
assert(pHead);
assert(pHead->prev != pHead);
LNode* tail = pHead->prev;
LNode* last = tail->prev;
pHead->prev = last;
last->next = pHead;
free(tail);
tail = NULL;
}
7、链表的头删
逻辑结构如下:
代码实现:
void LNodePopFront(LNode* pHead)
{
assert(pHead);
assert(pHead->prev != pHead);
LNode* del = pHead->next;
LNode* first = del->next;
pHead->next = first;
first->prev = pHead;
free(del);
del = NULL;
}
8、链表节点的查找
查找到目标节点,返回目标节点的地址,用于后续的操作。代码实现:
LNode* FindNode(LNode* pHead, LDataType val)
{
assert(pHead);
LNode* cur = pHead->next;
while (cur != pHead)
{
if (cur->data == val)
return cur;
cur = cur->next;
}
return NULL;
}
9、链表在pos前插入
配合上面的查找函数进行使用,先查找到pos的位置,再进行插入操作。
代码实现:
//在pos的前面插入
void LNodeInsert(LNode* pos, LDataType val)
{
assert(pos);
LNode* posPrev = pos->prev;
LNode* newNode = BuyNewNode(val);
posPrev->next = newNode;
newNode->prev = posPrev;
newNode->next = pos;
pos->prev = newNode;
}
10、删除pos位置的节点
这个函数还是要配合查找函数进行使用,逻辑图如下:
代码实现:
void LNodeErase(LNode* pos)
{
assert(pos);
LNode* posPrev = pos->prev;
LNode* posNext = pos->next;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
pos = NULL;
}
至此,哨兵位头节点的双向循环链表已全部实现。希望能帮到读者。