首先,我们创建链表时,要先明白这个链表的结构,它有前指针和后指针,和存放数据的区域,所以我们应该如下创建:
typedef int LTDataType;//类型重命名,方便修改数据类型
typedef struct ListNode
{
struct ListNode *next;//前指针
struct ListNode *prev;//后指针
LTDataType data;
}LTNode;//链表重命名
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#include
LTNode* BuyListNode(LTDataType x);//创建结点
LTNode* ListInit();//初始化
void ListPrint(LTNode *phead);//打印
void ListPushBack(LTNode *phead, LTDataType x);//尾插
void ListPushFront(LTNode *phead, LTDataType x);//头插
void ListPopBack(LTNode *phead);//尾删
void ListPopFront(LTNode *phead);//头删
bool ListEmpty(LTNode* phead);//判断链表是否为空
// 在pos位置之前插入x
void ListInsert(LTNode* pos, LTDataType x);
// 删除pos位置的节点
void ListErase(LTNode* pos);
int ListSize(LTNode* phead);//求链表的长度
void ListDestroy(LTNode* phead);//链表的销毁:
LTNode* BuyListNode(LTDataType x)//创建结点
{
//开辟新的节点
LTNode * newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc fail");//开辟失败结束程序
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
首先我们使用返回值的方式,不像单链表一样使用二级指针的形式,是因为双向带头循环链表的头结点不会改变,所以没必要使用二级指针。只使用返回值就可以
LTNode* ListInit()//初始化
{
//创建头结点,赋值可以随意。
LTNode *phead = BuyListNode(-1);
phead->next = phead;//把前后指针都指向自己
phead->prev = phead;
return phead;//返回头结点,初始化完成。
}
在链表初始化时,我们其实只是创建了一个头结点,把它的前后指针都指向自己,进而完成后续的操作,这里保证链表的整体统一,所以也使用了返回值的形式。
void ListPrint(LTNode *phead)//打印
{
assert(phead);//断言节点不为空
//保存下一个结点,进行遍历
LTNode *cur = phead->next;
//从头结点的下一个开始遍历,到头结点结束
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
在链表的打印中,我们要注意的是,这个链表是循环的链表,所以不可以指直接遍历,否则会导致链表死循环,所以我们从链表的头结点的下一个结点进行遍历,到链表的头结点结束,刚好把链表遍历一遍。
void ListPushBack(LTNode *phead, LTDataType x)//尾插
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->prev;//找到尾节点
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
链表的尾插,其实就是找到链表的头结点的后指针,进而找到尾节点,在创建一个新节点,然后把他们连接起来
void ListPushFront(LTNode *phead, LTDataType x)//头插
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->next;//找到第一个节点
phead->next = newnode;
newnode->prev = phead;
newnode->next = tail;
tail->prev = newnode;
}
头插和尾插大同小异,头插我们要先找出第一个节点,就是头结点的后指针,然后我们进行插入连接,与尾插类似。
void ListPopBack(LTNode *phead)//尾删
{
assert(phead);
assert(!ListEmpty(phead));//判断链表不为空
LTNode* tail = phead->prev;//保存尾节点
tail->prev->next = phead;
phead->prev = tail->prev;
free(tail);
tail = NULL;
}
链表的尾删也不难,我们先找到尾节点,接着我们对节点进行修改,把尾节点的前一个的后指针连接到头结点,接着把头结点的前指针,连接到尾节点的前一个,最后free掉尾节点,把尾节点置NULL。
void ListPopFront(LTNode *phead)//头删
{
assert(phead);
assert(!ListEmpty(phead));
LTNode* tail = phead->next;
tail->next->prev = phead;
phead->next = tail->next;
free(tail);
tail = NULL;
}
头删和尾删基本一样,就删除的结点不一样,我们只需要找到第一个结点就可以啦。
bool ListEmpty(LTNode* phead)//判断链表是否为空
{
assert(phead);
return phead->next == phead;//判断是否为空
}
我们一般判断链表为空时,我们采用的是 if 语句条件判断,看是否phead->next == phead这样进行判断,我们可以直接用 phead->next == phead 这个表达式的结果进行判断。
int ListSize(LTNode* phead)//求链表的长度
{
int size = 0;
LTNode *cur = phead->next;
while (cur != phead)
{
size++;
cur = cur->next;
}
return size;
}
这个求链表的长度和打印链表的思路一致,我们不能直接遍历,所以我们保存头结点的下一个,然后进行遍历,一直到链表的头结点处遍历结束,最后我们返回size
void ListDestroy(LTNode* phead)//链表的销毁
{
assert(phead);
LTNode *cur = phead->next;
while (cur != phead)
{
//头删前,要先保存下一个的结点。
LTNode* next = cur->next;
ListPopFront(cur);
cur = next;
}
free(phead);
}
链表的销毁中最值得注意的就是,我们在删除链表时必须要把删除前的下一个进行保存,这样不会导致访问不到,删除完成后,我们还要删除链表的头结点。
void ListInsert(LTNode* pos, LTDataType x)
{
assert(pos);//断言pos不为NULL
LTNode* tail = pos->prev;//找到pos结点的前一个。
LTNode* newnode = BuyListNode(x);
tail->next = newnode;
newnode->prev = tail;
newnode->next = pos;
pos->prev = newnode;
}
我们先判断pos不为NULL,然后找到pos结点的前一个,创建一个新节点,然后把这三个结点(pos pos->prev newnode )进行连接。
void ListErase(LTNode* pos)
{
assert(pos);
LTNode* tailnext = pos->next;//找到pos的下一个结点
LTNode* tailprev = pos->prev;//找到pos的前一个结点
tailprev->next = tailnext;
tailnext->prev = tailprev;
free(pos);
pos = NULL;
}
我们先找到pos的前 后 结点,接着我们进行连接这两个结点(pos->next pos->prev),最后free 掉pos结点,把pos结点处置为NULL。