前引:今天学习的双链表属于链表结构中最复杂的一种(带头双向循环链表),按照安排,我们会先进行复习,如何实现双链表,如基本的头插、头删、尾删、尾插,掌握每个细节,随后进行例题练习,帮助我们了解它的实际挑战,前面的实现只是了解它结构的入门,当然只有打好基础才是最重要的,小编会仔细讲解它的各个环节,正文开始~
目录
知识点速览
双链表的实现
今天咱们来复习一下最复杂的链表结构——带头双向循环链表。根据字面意思我们大概可以详细想象出来它的特点,首先有一个哨兵节点来充当头节点,其次是循环双向的,逻辑结构如下图:
它较与单链表的结构里面多了一个指针,指向它的前一个节点,以此达到双向。针对哨兵节点:
哨兵节点一般不存储任何数据, 不仅使用方便,可以简化插入删除的操作(不用传二级指针),所有操作无需去处理哨兵节点,逻辑统一,最后可以减少空指针异常
这里的循环是根据节点的空间结构而来的:头节点->prev=尾节点,尾节点->next=头节点
节点结构
较单链表而言只多加了一个指针,用来指向自身的前一个节点。这里小编用 tpedef 重定义了一下数据类型,方便以后进行维护。
typedef int Plastic;
typedef struct DoubleList
{
//数据域
Plastic data;
//前指针域
struct DoubleList* prev;
//后指针域
struct DoubleList* next;
}DoubleList;
设置哨兵节点
哨兵节点的数据域一般不存储数据,但是它之后的链表节点需要给一个数据作为参数开辟节点,所以小编在这里将二者分开,给哨兵节点单独设置一个函数来开辟空间。开始时头指针指向哨兵节点,哨兵节点前后指针应该指向自己,已达到双向循环结构
//设置哨兵节点
DoubleList* pphead = Sentry();
//设置哨兵节点
DoubleList* Sentry()
{
//开辟节点
DoubleList* newnode = (DoubleList*)malloc(sizeof(DoubleList));
//判断空间有效性
if (newnode == NULL)
{
printf("哨兵节点开辟失败\n");
return NULL;
}
//初始化
newnode->next = newnode;
newnode->prev = newnode;
return newnode;
}
开辟节点
开辟节点还是和单链表一样,传一个数据给它就行了
这里需要注意:初始化开辟的节点时应该是前后指向自己的,如下图:
//新增节点
DoubleList* Newnode(Plastic data)
{
//开辟节点
DoubleList* newnode = (DoubleList*)malloc(sizeof(DoubleList));
//判断空间有效性
if (newnode == NULL)
{
printf("节点开辟失败\n");
return NULL;
}
//初始化
newnode->next = newnode;
newnode->prev = newnode;
newnode->data = data;
return newnode;
}
尾插
尾插需要先找尾,对于双向循环链表而言,头节点的前一个节点就是它的尾。然后再插入新增的节点,连接 next 与 prev 的关系即可
注意:尾插不用判断链表是否存在啊,因为我们这里有哨兵节点,直接找尾、插入即可
//尾插
void Tail_insert(DoubleList* pphead, Plastic data)
{
//找尾
DoubleList* tail = pphead->prev;
//开辟节点
DoubleList* newnode = Newnode(data);
//连接
pphead->prev = newnode;
newnode->next = pphead;
tail->next = newnode;
newnode->prev = tail;
}
下面我们通过打印函数来看一下尾插的效果如何:
//打印
void List_Print(DoubleList* pphead)
{
//如果只有哨兵节点
if (pphead->next == pphead)
{
printf("无元素可以打印\n");
return;
}
//因为如果只有哨兵节点,pphead->next就越界了
DoubleList* first = pphead->next;
while (first != pphead)
{
printf("%d -> ", first->data);
first = first->next;
}
printf("pphead\n");
}
尾删
尾删需要先找尾,然后将头节点与尾的前一个节点进行连接,再释放之前标记的尾,思维上并不难
//尾删
void Tail_deletion(DoubleList* pphead)
{
//如果只有头节点无法删除
if (pphead->prev == pphead)
{
printf("无法删除\n");
return;
}
//找尾
DoubleList* tail = pphead->prev;
//找倒数第二个节点
DoubleList* cur = pphead->prev->prev;
//更新关系,重新连接
pphead->prev = cur;
cur->next = pphead;
free(tail);
tail = NULL;
}
头插
先标记头节点的下一个节点,然后在头节点与这个标记的节点中间插入即可,最后更新连接关系
//头插
void Head_insert(DoubleList* pphead, int data)
{
//标记头节点的下一个节点
DoubleList* first = pphead->next;
DoubleList* newnode = Newnode(5);
//更新连接关系
pphead->next = newnode;
newnode->next = first;
first->prev = newnode;
newnode->prev = pphead;
}
头删
对于只有哨兵节点的双链表是无法头删的,因此需要先进行判断。其次是标记链表的第一个节点、第二个节点,重新确立头节点和第二个节点的关系,再释放掉第一个节点
//头删
void Head_deletion(DoubleList* pphead)
{
//判断是否只有哨兵节点
if (pphead->prev == pphead)
{
printf("无法删除\n");
return;
}
//标记第一个节点
DoubleList* first = pphead->next;
//标记第二个节点
DoubleList* second = first->next;
//重新确立头节点和第二个节点的关系
pphead->next = second;
second->prev = first;
//释放第一个节点
free(first);
first = NULL;
}
在目标节点前面插入
我们先找到目标节点,然后再标记目标节点前面的一个节点,再确立三者之间的 next 与 prev 的关系,如下图:
//在目标节点前面插入
void Before_target(DoubleList* pphead, int data)
{
//找目标节点
DoubleList* cur = pphead->next;
while (cur->data != data)
{
if (cur == pphead)
{
printf("没有找到\n");
return;
}
cur = cur->next;
}
//标记cur前面的节点
DoubleList* prev = cur->prev;
DoubleList* newnode = Newnode(6);
//将三者进行连接
prev->next = newnode;
newnode->prev = prev;
newnode->next = cur;
cur->prev = newnode;
}
在目标节点后面插入
先找到目标节点,然后标记目标节点后面的一个节点,再确立新增节点、目标节点、标记节点的 next prev 的关系,与“在目标节点前面插入”很类似
//在目标节点后面插入
void Behind_target(DoubleList* pphead, int data)
{
//找目标节点
DoubleList* cur = pphead->next;
while (cur->data != data)
{
if (cur == pphead)
{
printf("没有找到\n");
return;
}
cur = cur->next;
}
//标记cur后面的节点
DoubleList* next = cur->next;
DoubleList* newnode = Newnode(7);
//将三者进行连接
cur->next = newnode;
newnode->prev = cur;
newnode->next = next;
next->prev = newnode;
}
练习题说明
一般链表题,比如考研、面试、校招会以单链表居多,大家可以看我的上一篇文章来学习链表题目