目录
1、双向循环链表的结构
2、双向循环链表的结构体创建
3、双向循环链表的初始化
3.1 双向链表的打印
4、双向循环链表的头插
5、双向循环链表的尾插
6、双向循环链表的删除
6.1 尾删
6.2 头删
6.3 小节结论
7、查找
8、在pos位置前插入数据
9、删除pos位置的数据
10、释放双向循环链表
结语:
前言:
双向循环链表在实际应用中是一种非常广泛的数据结构,双向循环链表在结构上比单链表更复杂,比如单链表中的节点只有一个指针,而双向循环链表中有两个指针共同维护该节点,其中一个指针指向后一个节点,而另一个指针指向前面一个节点。虽然其结构较复杂,但是在实现增删查改的功能上却比单链表要便捷的多。
1、双向循环链表的结构
上图的第一个节点head称为该链表的头节点(也称为哨兵位的节点,他的作用就好比一个哨兵只负责站岗),该节点中与其他节点的结构一样,只是该头节点中的数据不具有有效性,也就是打印数据时除了头节点的数据不打印,其他节点的数据都要打印。
该头节点的优势在于无需更改pilst的指向,因为plist指针始终指向该节点,只需要对该节点内部成员的指针进行修改即可,同时也可以简化代码,具体如下文。
2、双向循环链表的结构体创建
节点的结构体代码如下:
typedef int DListDataType;//int类型重定义,方便使用其他类型时进行修改
typedef struct DoubleListNode
{
struct DoubleListNode* prev;//prev指向前一个节点的指针
struct DoubleListNode* next;//next指向后一个节点的指针
DListDataType data;//存储的数据
}DLNode;//重定义结构体类型
3、双向循环链表的初始化
初始化即生成一个头节点,即哨兵位节点,并且用一个指针指向其节点即可。因为是循环链表,因此初始化的时候要将头节点的两个指针都指向自己。
因此当链表为空的时候,头节点(哨兵位节点)依然是存在的,则在对链表进行删除操作时头节点不能被删除。
初始化代码如下:
DLNode* DLInit()//初始化
{
DLNode* phead = BuyNode(-1);//节点创建函数
phead->next = phead;//只有一个头节点的时候也是自己指向自己
phead->prev = phead;
return phead;//返回头节点的地址
}
int main()
{
DLNode* plist = DLInit();//外部有一个结构体指针来接收返回值
return 0;
}
3.1 双向链表的打印
打印双向链表,以便观察各个功能的结果:
void Print(DLNode* phead)//打印
{
assert(phead);
DLNode* cur = phead->next;
printf("哨兵位<=>");
while (cur != phead)
{
printf("%d<=>", cur->data);
cur = cur->next;
}
}
4、双向循环链表的头插
思路很简单,就是将newnode节点的next指向d1,并且d1的prev指向newnode。newnode的prev指向head,head的next指向newnode。但是这里涉及到节点的创建,每次插入节点的时候都需要创建节点,因此将其封装成一个函数如下:
DLNode* BuyNode(DListDataType x)
{
DLNode* newnode = (DLNode*)malloc(sizeof(DLNode));//malloc开辟空间
if (newnode == NULL)//判断malloc是否成功
{
perror("BuyNode");
return NULL;
}
newnode->data = x;//赋予创建节点的值
newnode->prev = NULL;//置空
newnode->next = NULL;//置空
return newnode;//返回节点的地址
}
头插代码:
void PushFront(DLNode* phead, DListDataType x)//头插
{
assert(phead);
DLNode* next = phead->next;//定义一个指针next,他指向phead的下一个节点
DLNode* newnode = BuyNode(x);//创建节点
phead->next = newnode;//phead下一个节点为newnode
newnode->prev = phead;//newnode前一个节点为phead
newnode->next = next;//newnode下一个节点为next
next->prev = newnode;//next前一个节点为newnode
}
5、双向循环链表的尾插
思路与头插差不多,但是尾插能体现出双向循环链表的优势在于无需遍历整个链表去找尾,因为head的prve指向的就是尾, 因此可以直接找到尾,然后再进行尾插操作。
尾插代码如下:
void PushBack(DLNode* phead, DListDataType x)//尾插
{
assert(phead);
DLNode* tail = phead->prev;//找到尾部节点
DLNode* newnode = BuyNode(x);//创建节点
tail->next = newnode;//让尾部的下一个节点为newnode
newnode->prev = tail;//让newnode的上一个节点为tail
phead->prev = newnode;//让phead的上一个节点为newnode
newnode->next = phead;//让newnode下一个节点是phead
}
6、双向循环链表的删除
6.1 尾删
思路:从上文可以得知,找到尾节点很简单,但是这里需要再定义一个指针,他指向tail前一个节点。然后将tail释放后,再把tailprev的next指向头节点,头节点的prev更新成指向tailprev节点即可完成尾删。
尾删代码如下:
bool Empty(DLNode* phead)//判断链表是否为空
{
assert(phead);
return phead->next == phead;//为空返回真
}
void PopBack(DLNode* phead)//尾删
{
assert(phead);
assert(!Empty(phead));//若返回真表示链表为空,则结果取反,断言不通过
DLNode* tail = phead->prev;//定义tail指针
DLNode* tailPrev = tail->prev;//定义tailPrev指针
tailPrev->next = phead;//tailPrev下一个节点为phead
phead->prev = tailPrev;//让phead的前一个节点为tailPrev
free(tail);//释放tail节点
}
6.2 头删
思路:将头节点与second指向的节点互相联系起来,然后将first释放即可。
头删代码如下:
void PopFront(DLNode* phead)//头删
{
assert(phead);
assert(!Empty(phead));//链表判空
DLNode* first = phead->next;//定义指针
DLNode* second = first->next;
phead->next = second;//将头节点于second节点相连
second->prev = phead;
free(first);//删除first节点
}
6.3 小节结论
这里可以体现出双向循环链表相对于单链表的一个优势:链表只有一个节点的时候,直接删除即可无需将plist指针置为空,因为plist指针始终指向哨兵节点。若是单链表进行删除则还要进行多一项的判断,还要考虑plist指针是否为空的情况。
但是其缺陷是空链表的时候若还进行删除会把哨兵位也删了,这时候plist就是野指针,对pilst进行解引用就会出现非法访问的问题。因此要断言链表是否为空,为空则不能进行删除。
7、查找
查找功能就相对简单点,只需要遍历链表,返回要查找节点的地址pos即可,只是这里需要注意一点,即:遍历链表的条件,因为头节点的数据没有有效性,因此从头节点的下一个节点开始遍历,直到遍历到头节点结束。
查找代码如下:
DLNode* Seach(DLNode* phead, DListDataType x)//搜索
{
assert(phead);
DLNode* cur = phead->next;//用cur代替phead去遍历
while (cur != phead)//cur指向头节点时结束循环
{
if (cur->data == x)
return cur;//如果等于返回cur地址
cur = cur->next;//遍历cur
}
return NULL;//链表中没有该数据则返回空
}
查找函数通常与中间插入、中间删除函数进行搭配,因为查找函数返回了一个地址pos,再将这个地址直接传到中间插入、中间删除函数内,就能很好的实现功能。
8、在pos位置前插入数据
思路:通过查找函数可以得到pos位置的地址,然后再定义一个指针prev,再将newnode节点与prev节点和pos节点联系起来即可。
pos前插代码如下:
void PosInsert(DLNode* pos, DListDataType x)//在pos前插入
{
assert(pos);
DLNode* posPrev = pos->prev;//定义posprev指针,指向pos前面一个节点
DLNode* newnode = BuyNode(x);//创建节点
newnode->prev = posPrev;//以下的操作就是把newnode节点与pos、posprev节点联系起来
posPrev->next = newnode;
newnode->next = pos;
pos->prev = newnode;
}
插入数据与查找数据搭配测试代码如下,随便测试之前的接口:
int main()
{
DLNode* plist = DLInit();//初始化
PushFront(plist, 4);//头插
PushFront(plist, 3);
PushFront(plist, 2);
PushFront(plist, 1);
PushBack(plist, 5);//尾插
PushBack(plist, 6);
/*PopFront(plist);
PopBack(plist);*/
Print(plist);//打印
DLNode* pos = Seach(plist, 1);//查找
if (pos)
{
PosInsert(pos, 20);//中间插入
}
printf("\n");//换行
Print(plist);//打印
DLNodeFree(plist);//释放链表
plist = NULL;//手动将plist置空
return 0;
}
运行结果:
9、删除pos位置的数据
思路:将posPrev与posNext这两个节点联系起来,然后释放pos即可。
删除pos位置代码如下:
void PosDestroy(DLNode* pos)//删除pos位置
{
assert(pos);
DLNode* posPrev = pos->prev;//定义两个指针,一个指向pos前面节点,一个指向pos后面节点
DLNode* posNext = pos->next;
posPrev->next = posNext;//将这两个指针指向的节点联系在一起
posNext->prev = posPrev;
free(pos);//释放pos节点
}
10、释放双向循环链表
因为链表中的各个节点(包括哨兵位节点)都是在堆上申请的,因此在使用完毕后应该对这些空间进行释放。
释放函数代码如下:
void DLNodeFree(DLNode* phead)//释放链表
{
assert(phead);//此处要断言,不然下面会对空指针解引用
DLNode* cur = phead->next;//用cur指针代替phead去遍历
while (cur != phead)
{
//Next指针的作用是记住位置,防止释放cur后找不到下一个节点
DLNode* Next = cur->next;
free(cur);//释放cur指向的节点
cur = Next;//赋予cur新的位置
}
free(phead);//最后释放哨兵位(头节点)
}
int main()
{
DLNode* plist = DLInit();
DLNodeFree(plist);
plist = NULL;//外部的plist此时的野指针,要手动置空
return 0;
}
这里注意的点:若在DLNodeFree函数内部进行对phead的置空,则不会影响外部plist的值,因为phead只是plist的一份临时拷贝,除非将pilst的地址传给DLNodeFree函数,用二级指针来操作,或者手动在外面将plist置空,两种方法都能将plist置空。
结语:
以上就是关于双向循环链表的实现与解析,如果本文对你起到了帮助,希望可以点赞👍+关注😎+收藏👌哦!如果有遗漏或者有误的地方欢迎大家在评论区补充~!!谢谢大家!!