目录
一、带头双向循环链表的原理
二、带头双向循环链表的实现
1.带头双向循环链表的定义
2.带头双向循环链表的初始化
3.带头双向循环链表节点的创建
4.打印链表
5.判断链表是否为空
6.双向链表在pos的前面进行插入
7.双向链表在头部/尾部插入数据
8.双向链表删除pos处的节点
9.双向链表在头部/尾部删除数据
10.销毁链表
三、完整代码及演示
总结
我们知道链表有带头和不带头,循环与不循环,单向与双向,这几种区别排列组合链表一共有8种,前面我们实现了最简单的同时也是缺陷最多的链表——单链表,那么今天我们来实现带头双向循环链表,这种最完美的链表也是日常生活中常用的链表。
我们观察它的细节,一个节点还是由两个部分组成,指针域和数据域,
指针域有next指针,用来指向当前节点的下一个节点的指针
prev指针 ,用来指向当前节点的上一个节点的指针
同时有一个哨兵位的头节点head,方便头插,head的prev指向的就是尾
尾的next指向head,这样就可以快速找到链表的头和尾,这种链表弥补了单向不带头不循环链表的缺点,使用起来十分方便。
话不多说我们就进入实现的阶段
// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
LTDataType _data;
struct ListNode* _next;
struct ListNode* _prev;
}ListNode;
这种链表的定义想必我们是比较熟悉了,大体结构不变,我们只需要更改链表的指针域就可以了
ListNode* ListCreate()
{
ListNode* phead = BuyListNode(-1);
phead->_next = phead;
phead->_prev = phead;
return phead;
}
初始化的部分注意一个点,我们让哨兵位的头节点指向它自己,这个在后面会有大用处,同时这个哨兵位的头结点的数据域的数据不用更改,只要是随机值或者-1就可以
ListNode* BuyListNode(LTDataType x)
{
ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
if (newNode == NULL)
{
perror("BuyListNode malloc");
exit(-1);
}
newNode->_data = x;
newNode->_next = NULL;
newNode->_prev = NULL;
return newNode;
}
这里要注意,每次用完malloc函数一定要判断指针是否为空,有可能有开辟空间失败的情况
我们这次循环结束的条件就不能是cur指针走向空了,参考带头双向循环链表的结构图,我们不难发现当cur指针走向phead时一次循环就结束了,切记循环结束条件不能让cur走向空,这会出现死循环。
void ListPrint(ListNode* pHead)
{
assert(pHead);
assert(!ListEmpty(pHead));
ListNode* cur = pHead->_next;
while (cur != pHead)
{
printf("%d ", cur->_data);
cur = cur->_next;
}
}
bool ListEmpty(ListNode* phead)
{
assert(phead);
return phead->_next == phead;
}
这个比较简单类似于上面的打印
我们在插入数据时一定要保存pos节点的prev,如果不保存,我们将新节点与pos链接后无法找到pos的prev,所以要保存,如果不保存也可以,一定要注意节点的链接顺序,不能出现上述情况。
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* prev = pos->_prev;
ListNode* newNode = BuyListNode(x);
prev->_next = newNode;
newNode->_prev = prev;
newNode->_next = pos;
pos->_prev = newNode;
}
我们前面已经写过了在pos之前插入数据了
在头部插入数据就是在链表的第一个节点之前插入数据
在尾部插入数据就是在尾部之前插入数据,链表的尾部的prev不就是phead的next吗?
综上所述我们就可以这样来写代码
//尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListInsert(pHead, x);
}
//头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListInsert(pHead->_next, x);
}
还记得前面埋下的伏笔吗?
将哨兵位的头节点的prev和next指向自己,我们如果想在头部或尾部插入数据,我们走读代码
发现它竟然在逻辑上说得通,这就是将哨兵位头节点指向自己的优势,同时在删除头尾节点时它的优势又可以展现
在删除节点时我们必须明确,链表为空就不能删除了
所以一开始我们就要判断链表是否为空,然后保存pos的prev和next可避免节点连接的先后顺序问题。
void ListErase(ListNode* pos)
{
assert(pos);
assert(!ListEmpty(pos));
ListNode* prev = pos->_prev;
ListNode* next = pos->_next;
free(pos);
prev->_next = next;
next->_prev = prev;
}
这个操作和双向链表在头部/尾部插入数据类似,我们直接复用上面代码
//尾删
void ListPopBack(ListNode* pHead)
{
assert(pHead);
ListErase(pHead->_prev);
}
//头删
void ListPopFront(ListNode* pHead)
{
assert(pHead);
ListErase(pHead->_next);
}
我们整个链表都是malloc出来的而C\C++是没有自动内存回收机制的,所以我们需要手动实现free掉整个链表的操作
void ListDestory(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->_next;
while (cur != pHead)
{
ListNode* next = cur->_next;
free(cur);
cur = NULL;
cur = next;
}
}
//ListNode.h
#include
#include
#include
#include
// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
LTDataType _data;
struct ListNode* _next;
struct ListNode* _prev;
}ListNode;
//判断链表为空
bool ListEmpty(ListNode* phead);
//创建新节点
ListNode* BuyListNode(LTDataType x);
// 创建返回链表的头结点.
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
//ListNode.c
#include"ListNode.h"
ListNode* ListCreate()
{
ListNode* phead = BuyListNode(-1);
phead->_next = phead;
phead->_prev = phead;
return phead;
}
ListNode* BuyListNode(LTDataType x)
{
ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
if (newNode == NULL)
{
perror("BuyListNode malloc");
exit(-1);
}
newNode->_data = x;
newNode->_next = NULL;
newNode->_prev = NULL;
return newNode;
}
bool ListEmpty(ListNode* phead)
{
assert(phead);
return phead->_next == phead;
}
void ListPrint(ListNode* pHead)
{
assert(pHead);
assert(!ListEmpty(pHead));
ListNode* cur = pHead->_next;
while (cur != pHead)
{
printf("%d ", cur->_data);
cur = cur->_next;
}
}
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* prev = pos->_prev;
ListNode* newNode = BuyListNode(x);
prev->_next = newNode;
newNode->_prev = prev;
newNode->_next = pos;
pos->_prev = newNode;
}
//尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListInsert(pHead, x);
}
//头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListInsert(pHead->_next, x);
}
void ListErase(ListNode* pos)
{
assert(pos);
assert(!ListEmpty(pos));
ListNode* prev = pos->_prev;
ListNode* next = pos->_next;
free(pos);
prev->_next = next;
next->_prev = prev;
}
//尾删
void ListPopBack(ListNode* pHead)
{
assert(pHead);
ListErase(pHead->_prev);
}
//头删
void ListPopFront(ListNode* pHead)
{
assert(pHead);
ListErase(pHead->_next);
}
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* cur = pHead->_next;
while (cur!=pHead)
{
if (cur->_data == x)
{
return cur;
}
cur = cur->_next;
}
return NULL;
}
void ListDestory(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->_next;
while (cur != pHead)
{
ListNode* next = cur->_next;
free(cur);
cur = NULL;
cur = next;
}
}
//test.c
#include"ListNode.h"
void test1()
{
ListNode* L = ListCreate();
ListPushBack(L, 1);
ListPushBack(L, 2);
ListPushBack(L, 3);
ListPushBack(L, 4);
ListPushBack(L, 5);
ListInsert(L, 1);
ListPushFront(L, 100);
ListPopBack(L);
ListPopBack(L);
ListPopBack(L);
ListPopFront(L);
ListPrint(L);
ListNode*Find=ListFind(L, 100);
if (Find == NULL)
{
printf("无法找到\n");
}
else
{
printf("%d %p %p\n", Find->_data, Find->_next, Find->_prev);
}
ListDestory(L);
}
int main()
{
test1();
return 0;
}
完整代码可以从此处获得:data-structure--/Double-ListNode/Double-ListNode at main · 01294268442ww/data-structure-- (github.com)