目录:
前言
我们可以根据前面的博客得知,链表分为带头和不带头,双向和单向,循环和非循环链表,两两组合之后,共有8种链表结构。其中最常用的是单向不带头非循环链表和双向带头循环链表。前者在之前已经实现过了,大家可以点击前面的链接直达,而今天我们主要介绍的是双向带头循环链表的实现。
一、双向链表的概念
双向带头循环链表是一种特殊的双向链表,具有以下特点:
- 双向链表: 每个节点有两个指针,一个指向前驱节点(
prev
),一个指向后继节点(next
)。因此,从任意节点可以沿链表向前或向后遍历。 - 带头节点: 链表中有一个特殊的头节点(通常不存储实际数据,称为哨兵节点)。头节点的主要作用是简化操作,如插入和删除。它提供统一的起始点,使得链表的头尾操作更加方便。
- 循环链表: 链表的最后一个节点的
next
指向头节点,头节点的prev
指向最后一个节点。因此,链表就呈环状连接起来(构成循环),没有明确的开始和结束,方便从任意节点进行循环遍历。
1. 结构特点:
- 头节点作为链表的起始和终止标记,解决了普通链表操作时需要判断空链表或链表尾部的问题。
- 空链表时,头节点的
next
和prev
都指向自己。
2. 优点:
- 统一操作: 无论链表是否为空,头节点总是存在,插入和删除操作不需要特殊处理空链表或只有一个节点的情况。
- 双向遍历: 可以从任意节点向前或向后遍历,非常灵活。
- 循环结构: 链表没有明确的尾部,方便处理循环场景。
双向带头循环链表常用于需要频繁插入和删除操作的场景,能够高效地处理这些操作。
二、双向链表的实现
1.双向链表的结构
typedef int LTDataType;
typedef struct ListNode
{
LTDataType date;
struct ListNode* prev;
struct ListNode* next;
}LTNode;//重命名简化,后续代码不用再繁琐
双向链表有三个数据,保存它自身数据,他的前驱指针,以及他的后继指针。
2. 初始化
//双向链表初始化
void LTInit(LTNode** pphead);
因为初始化要创造头节点,而且为了方便后续插入操作创造节点,我们要先写出创造节点的方法
创造出的节点本身自己也必须是双向循环的才可以,他的prev以及next都指向他自己,自身成环。
LTNode* LTBuyNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
newnode->date = x;
newnode->next = newnode;
newnode->prev = newnode;
return newnode;
}
//双向链表初始化
void LTInit(LTNode** pphead)
{
*pphead = LTBuyNode(-1);
}
3. 销毁
//双向链表销毁
void LTDestory(LTNode** pphead);
销毁的时候要遍历链表,将链表中的每一个节点都销毁,这里我们传的是二级指针,头节点会被会被销毁,但后面为了统一接口需要手动释放。
//双向链表销毁
void LTDestory(LTNode** pphead)
{
assert(pphead && *pphead);
LTNode* pcur = (*pphead)->next;
while (pcur != *pphead)
{
LTNode* Next = pcur->next;
free(pcur);
pcur = Next;
}
free(*pphead);
*pphead = NULL;
pcur = NULL;
}
4. 打印
//双向链表打印
void LTPrint(LTNode* phead);
和单链表一样,打印链表就是遍历链表.
注意,当时我们的循环条件是pcur != NULL,但是双链表是循环的,还这样写的话就成了死循环,因此需要让循环条件变成 pcur != phead
//双向链表打印
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d -> ", pcur->date);
pcur = pcur->next;
}
printf("\n");
}
5. 尾插
//双向链表尾插
void LTPushBack(LTNode* phead, LTDataType x);
通过下面这张图,就可以看出来尾插需要涉及到哪些节点。
//双向链表尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead;
newnode->prev = phead->prev;
phead->prev->next = newnode;
phead->prev = newnode;
}
测试功能如下:
6.头插
//双向链表头插
void LTPushFront(LTNode* phead, LTDataType x);
通过下面这张图,看一下头插需要涉及哪些节点。
//双向链表头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
测试功能如下:
7. 尾删
//双向链表尾删
void LTPopBack(LTNode* phead);
删除操作要判断当前链表是不是空链表,如果为空就不进行删除。
为空就是链表只有头节点。
//双向链表是否为空
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
//双向链表尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* del = phead->prev;
LTNode* prev = del->prev;
prev->next = phead;
phead->prev = prev;
free(del);
del = NULL;
}
测试功能如下:
8. 头删
//双向链表头删
void LTPopFront(LTNode* phead);
//双向链表头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
free(del);
del = NULL;
}
测试功能如下:
9. 查找
//双向链表查找
LTNode* LTFind(LTNode* phead, LTDataType x);
查找遍历链表,返回结点。
//双向链表查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->date == x)
{
//找到了
return pcur;
}
pcur = pcur->next;
}
//没找到
return NULL;
}
10. 双向链表指定位置插入
//双向链表指定位置插入
void LTInsert(LTNode* pos, LTDataType x);
//双向链表指定位置插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
11. 双向链表指定位置删除
//双向链表指定位置删除
void LTErase(LTNode* pos);
//双向链表指定位置删除
void LTErase(LTNode* pos)
{
assert(pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
三、双线链表优化
因为有哨兵位的存在,我们不用改变头结点,因此就可以传一级指针
1. 初始化优化接口
将接口改为一级指针,因为形参无法影响实参,因此需要手动接收所创造出来的结点。
//优化让接口统一为 LTNode* phead
LTNode* LTInit()
{
LTNode* phead = LTBuyNode(-1);
return phead;
}
这样来接收~
LTNode* plist = LTInit();
2. 销毁优化接口
将接口改为一级指针,因为形参无法影响实参,因此创造的plist需要手动释放,将plist放置为NULL。
//优化接口统一为 LTNode* phead,但是需要手动释放让 *plist=NULL;!!!
void LTDestory2(LTNode* phead)
{
assert(phead);
LTNode* pcur = (phead)->next;
while (pcur != phead)
{
LTNode* Next = pcur->next;
free(pcur);
pcur = Next;
}
free(phead);
phead = NULL;
pcur = NULL;
}
手动在外面置空
//LTDestroy(&plist);
LTDestroy2(plist);
plist = NULL;
总结
双向带头循环链表是一种灵活且高效的数据结构,适用于需要频繁插入和删除操作的场景。通过掌握双向带头链表的创建、插入、删除、查找等操作,我们可以更好地理解和应用这一数据结构,从而提高程序的效率和可靠性。