图片出处:The world's biggest drone photo and video sharing platform | SkyPixel.com
在上一篇博文中,我详细地讲解了无头单向不循环链表的实现,也提到了链表有八种分类,那么难道每种分类都要去学一遍吗?并不是的!我们只需要学习其中最常用的两种链表即可,即是上篇的无头单向不循环链表与本篇的带头双向循环链表,简称单链表与双链表,当我们将这两种链表掌握之后,自然就能独立实现其它链表了。话不多说,直接开始进入正文吧!
目录
前言
双链表的概念:
(1)带头
(2)循环
(3)双链表
双链表的结构
双链表的实现
(1)头文件实现
1))创建单个双链表节点结构
2))接口声明
(2)源文件
1))初始化链表
2)) 打印链表
3))尾插
1/开辟新节点
2/尾插
4))头插
5))尾删
6))头删
7)) 删除pos位置节点
1/查找特定值
2/删除pos
8))在pos之后插入节点
9))销毁链表
写在最后的话
头文件源码
源文件源码
注意,以下所有双链表均是指带头双向不循环链表。
在开始实现之前,我们需要理解什么是带头、什么是循环、什么是双链表。当将此种数据结构内涵了解后,实现起来就会事半功倍。
我们此处的“带头”与上篇博文单链表实现中的“头节点”是两种概念,带头链表的头节点实际为“哨兵位”,哨兵位不存储任何有效元素,只作为形式上的头节点存在。为了有所区分,在下文中,我会将不存储任何节点的哨兵位称作哨兵位,将哨兵位后一个存储有效元素的节点称之为头节点。
在上篇单链表实现的博文中,我们是将其尾节点的next指针置为NULL,此为不循环链表的实现方式。在循环链表中,则是将尾节点的next指针指向 头节点(带头链表则是指向哨兵位),使其首尾相连形成闭环,此为循环链表的实现方式。
我们已知单链表就是链表的每个节点都会包含一个指针变量指向后一个节点的地址,那么双链表顾名思义就是:双链表的每个节点中都会有两个指针变量,分别指向该节点上一个节点的地址与该节点下一个节点的地址。
双链表的结构大概如下图所示:
按照惯例,在正式实现前,我们需要理清自己本次需要实现哪些接口,在头文件中将预先设定的接口声明,后根据已声明的接口再去一一实现并根据实现过程中遇见的问题进行修改。
本次待实现的接口如下:
打印链表尾插
头插
尾删
头删
删除pos位置节点
在pos之后插入节点
初始化链表
销毁链表
同样的,头文件的实现分三步,分别是:
- 包含需要的头文件
- 创建单个双链表节点结构
- 接口声明
包含头文件省略。
以下为预先设想待实现的接口声明,形参部分为初步设想,具体最终实现效果为文末源码。
我们在源文件具体实现之前,需包含上文中所设定的头文件,如下:
当我们需要对一个链表进行初始化时,有两种方式:
- 自己创建一个哨兵位传给初始化接口完成初始化
- 调用初始化接口返回一个已初始化的哨兵位
这两种方式都是可行的,但是由于我在单链表实现的章节中使用的是第一种方式,故在此使用第二次初始化方式实现初始化接口。
为了更好地观察接口的增删改,故先实现打印链表接口。
在实现尾插之前,我先讲解一下尾插的原理以及实现方式。
在此之前,我们需要先理解一个点:因为带头双向循环链表中头尾节点相连,故head->next为头节点,head->prev为尾节点(head是哨兵位),如下图:
既然head->prev指向尾节点,那么在head前头插一个节点就相当于在该链表中尾插一个节点,捋清楚这一点,接下来再说尾插的基本原理,如下图:
可以清晰地看到,较之单链表,双链表各个接口的实现都比较简单,只需调整各节点的指针指向即可。
在准备插入时,我们又会遇到一个问题,既然是尾插,那么就需要开辟节点,且在后续的头插及pos位置之后插入都需要开辟节点,那么我们就可将此类需要频繁使用到的代码封装成函数,后续直接调用即可。
在尾插部分,我们讲到过,head->next为头节点,head->prev为尾节点,已知在head之前插入为尾插,那么同样的不难理解,在head之后插入为头插。
而头插也是分为两步:
一:先调整待插入节点的指针指向
二:调整受影响的节点指针指向
尾删即是将哨兵位前一个节点删除,在双链表中删除节点也是调整被影响的节点指针指向,逻辑如下:
代码实现如下:
头删与尾删同理,只是将删除哨兵位上一个节点改为删除哨兵位下一个节点。
既然要删除pos位置节点,那么首先我们需要找到pos节点,故在此之前,我先实现在链表中查找特定值。
以该代码实现方式,其功能准确来说应当是遍历数组,返回第一个节点中值 == x 的节点。
在本文中,为了更好地观察,我将指定节点交由主函数提供,需要在调用接口时先调用查找接口,再将其返回值传参。
删除逻辑如下:
代码实现如下:
在pos位置之后插入节点原理与头插相同,只是一个是在哨兵位后插入节点,一个是在pos节点后插入节点,只需处理好新节点的指针指向,与受影响的指针指向即可。
由于前面所有接口都是传递一级指针,故为了保持接口的一致性,在销毁链表接口中也传递的是一级指针,这就导致其无法在接口中对哨兵位置空,需要在调用该接口后手动置空。
当本文写到这的时候,基本上就已经完成了,通过上述实现的过程中不难看出,双链表的接口实现较之单链表要简单很多,主要是因为通过双链表哨兵位可以快速找到链表中的头节点和尾节点,无需再去找尾;并且在双链表中,一个节点可以同时指向前后两个节点,故也无需去遍历链表查找上一个节点。希望通过本篇文章能让你有所收获!
下面附上本文实现的双链表源码。
List.h
#pragma once
#include
#include
#include
//创建单个双链表节点结构
//重定义存储数据类型
typedef int LNDataType;
typedef struct LNode
{
LNDataType data;//储存的数据
struct LNode* prev;//保存上一个节点的地址
struct LNode* next;//保存下一个节点的地址
}LNode;
//初始化
LNode* SLInit();
//打印链表
void SLPrint(LNode* phead);
//尾插
void SLPushBack(LNode* phead,LNDataType x);
//头插
void SLPushFront(LNode* phead, LNDataType x);
//尾删
void SLPopBack(LNode* phead);
//头删
void SLPopFront(LNode* phead);
//删除pos位置节点
void SLErase(LNode* pos);
//在pos之后插入节点
void SLInsert(LNode* pos, LNDataType x);
//销毁链表
void SLDestroy(LNode* phead);
//查找指定值
LNode* SLFind(LNode* phead, LNDataType x);
List.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"
//初始化
LNode* SLInit()
{
LNode* head = (LNode*)malloc(sizeof(LNode));
if (head == NULL)
{
perror("malloc error");
return NULL;
}
//初始化prev、next指针指向本身,实现循环
head->prev = head->next = head;
return head;
}
//打印链表
void SLPrint(LNode* phead)
{
assert(phead);
LNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
//开辟新节点
LNode* SLByNode(LNDataType x)
{
LNode* node = malloc(sizeof(LNode));
if (node == NULL)
{
perror("malloc error");
return NULL;
}
node->data = x;
node->next = node->prev = node;
return node;
}
//尾插
void SLPushBack(LNode* phead, LNDataType x)
{
assert(phead);
LNode* node = SLByNode(x);
//先调整新节点指针指向
node->prev = phead->prev;
node->next = phead;
//调整被影响的节点指针指向
phead->prev = node;
node->prev->next = node;
}
//头插
void SLPushFront(LNode* phead, LNDataType x)
{
assert(phead);
LNode* node = SLByNode(x);
//先调整新节点的指针指向
node->prev = phead;
node->next = phead->next;
//调整被影响的节点指针指向
phead->next = node;
node->next->prev = node;
}
//尾删
void SLPopBack(LNode* phead)
{
//链表不能为空
assert(phead && phead->next!=phead);
//保存尾节点,方便删除
LNode* del = phead->prev;
//调整受影响节点的指针
phead->prev = del->prev;
del->prev->next = phead;
free(del);
del = NULL;
}
//头删
void SLPopFront(LNode* phead)
{
assert(phead && phead->next != phead);
//先保存待删除的头节点
LNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
free(del);
del = NULL;
}
//查找指定值
LNode* SLFind(LNode* phead, LNDataType x)
{
assert(phead && phead->next != phead);
LNode* pcur = phead->next;
while (pcur != phead)
{
if(pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
//删除pos位置节点
void SLErase(LNode* pos)
{
assert(pos);
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
//在pos之后插入节点
void SLInsert(LNode* pos, LNDataType x)
{
assert(pos);
LNode* node = SLByNode(x);
//调整node指针指向
node->prev = pos;
node->next = pos->next;
//调整受影响的节点指针指向
pos->next = node;
node->next->prev = node;
}
//销毁链表
void SLDestroy(LNode* phead)
{
assert(phead);
LNode* pcur = phead->next;
LNode* next;
while (pcur != phead)
{
next = pcur->next;
free(pcur);
pcur = next;
}
pcur = next = NULL;
free(phead);
//需要手动将phead置为NULL
}