一起来学数据结构——双向循环链表
双向带头循环链表是链表中的一个比较重要的链表了。因为它最为复杂的结构,也使得在某些方面显得更加的容易。
和简单的单链表但是操作复杂产生了鲜明的对比。
链表的结构
- 每一个节点都有两个节点指针,一个指向前,一个指向后。
- 带有一个指向空的哨兵节点。
- 尾的
next
指向头,头的prev
指向尾,构成一个循环
typedef struct ListNode
{
struct ListNode* prev;
struct ListNode* next;
LTDataType data;
}ListNode;
建立双向链表
万事开头难,所以链表是如何建立的是十分重要的。
对于这个双向链表,我们所需要的呢就是一个哨兵节点。就是这个哨兵节点将链表建立起来。
ListNode* plist = NULL;
初始化节点
只要有了哨兵节点,那我们就算是建立了链表,但是这个链表中没有任何的东西,我们应该先把他们初始化。
ListNode* ListInit()
{
//创建一个头节点
ListNode* phead = BuyListNode(0);
//让头和尾都指向它自己
//这个很重要
phead->next = phead;
phead->prev = phead;
return phead;
}
然后再让这个初始化后的节点等于我们的建立的节点
ListNode* plist = ListInit();
增删查改
下面我们就来看一看双向链表的增删查改吧
尾插
- 先是
malloc
出来一个节点 - 找到尾节点
- 尾节点和新节点进行连接
//找到尾
ListNode* ptail = phead->prev;
//新创建一个节点
ListNode* newnode = BuyListNode(x);
//链接
ptail->next = newnode;
newnode->prev = ptail;
phead->prev = newnode;
newnode->next = phead;
提问:
只有哨兵节点,没有其他的头节点怎么办,还可不可以使用上面的方法呢?
答案是可以的。
这就是双向节点的优势
头插
- 先
malloc
出来一个节点 - 找到第一个节点
- 进行链接
assert(phead);
//c创建一个新节点
ListNode* newnode = BuyListNode(x);
//链接
// phead newnode node 三个指针进行链接
ListNode* pnode = phead->next;
newnode->next = pnode;
pnode->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
//如果就只是有头节点也是满足的
尾删
- 利用
phead
找到尾节点 - 将他free掉
- 处理新的尾节点
assert(phead);
//不能出现只有头节点的情况
assert(phead->next != phead);
ListNode* ptail = phead->prev;
ListNode* pcur = ptail->prev;
// pcur ptail phead 三个进行解绑
pcur->next = phead;
phead->prev = pcur;
free(ptail);
//如果只有一个数据也满足
头删
- 利用
phead
找到头节点 - 将他free掉
- 处理新的头节点
assert(phead);
不能出现新的头节点的情况
assert(phead->next != phead);
ListNode* pcur = phead->next;
ListNode* pcurnext = pcur->next;
//phead pcur pcurnext三个进行解绑
phead->next = pcurnext;
pcurnext->prev = phead;
free(pcur);
//如果只有一个数据也满足
寻找节点
这个就只能去遍历了从哨兵节点的下一个开始,直到该节点为phead
,注意不是NULL哦
ListNode* ListFind(ListNode* phead, LTDataType x)
{
ListNode* pcur = phead->next;
while (pcur != phead)
{
//如果找到了就返回
if (x == pcur->data)
{
return pcur;
}
pcur = pcur->next;
}
//没找到就返回NULL
return NULL;
//如果只有一个数据也满足
}
在任意位置插入节点
在指定的pos
的前面插入节点
这个也是十分简单的。
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* pinsert = BuyListNode(x);
pinsert->next = pos;
pinsert->prev = prev;
prev->next = pinsert;
pos->prev = pinsert;
//如果只有一个节点也满足
}
删除该节点
删除在pos
位置上的节点
//删除该节点的值
void ListErase(ListNode* pos)
{
assert(pos);
ListNode* pfront = pos->prev;
ListNode* pbehind = pos->next;
pfront->next = pbehind;
pbehind->prev = pfront;
free(pos);
pos = NULL;
//如果只有一个节点也满足
销毁链表
将每一个节点并且包括头节点进行删除。后面的具体节点就正常删除就可以了。
但是对于哨兵节点来说,要特殊处理。
因为在我们函数传参的时候传的是哨兵节点的值,所以改变形参的值不会改变实参。
所以我们要在调用函数的主函数内手动再置为NULL
assert(phead);
ListNode* pcur = phead->next;
while (pcur != phead)
{
ListNode* pnext = pcur->next;
free(pcur);
pcur = pnext;
}
//不要忘记将phead也要释放
free(phead);
phead = NULL;
//但是这里是值传递,这样不起作用,所以要在test中
//手动置空
plist = NULL;
//这里为了保持接口一致,采用了值传递。
//在函数中的置空不起作用,所以要在这里手动置null
这就是我们的双向链表的实现。
传值实现or传址实现?
还有一个很值得去说的问题,那就是在上面的整个过程中,我们一直使用的都是哨兵节点的值,而没有去传指针。
因为这里传值就可以起到效果,不需要再去传址了。因为我们直接就是在进行链接工作,并不没有对
下面就可以解释:
![image-20210930155808332](https://gitee.com/luoxiangyu/picture-bed/raw/master/202110052110627.png)