本文干货满满,你准备好了吗?
目录
前面我们了解到链表总共有八种类型,我们已经学习了其中的一种,即不带头单向不循环链表,也就是单链表。那么今天我们又将学习一种链表的结构,即带头双向循环链表,简称双向链表。
接下来让我们一起进入双向链表的学习吧! \color{green}{接下来让我们一起进入双向链表的学习吧!} 接下来让我们一起进入双向链表的学习吧!
一、什么是双向链表?
带头双向循环链表简称双向链表,是链表结构的一种,其结构如下图所示:
我们可以看到,双向链表与单链表不同的是双向链表的第一个节点之前有一个head节点,这个就是头节点,同时双向链表每个节点有两个指针,一个指向前一个节点(prev),一个指向后一个节点(next),它可以从头到尾,也可以从尾到头双向遍历,所以它是循环的,这大大增加了双向链表的灵活性。
二、单链表和双向链表的不同点
在我们实际运用中,最常用的即单链表和双向链表
那么这两种结构具体有什么不同点呢?下面为大家总结一下。
节点指针
单链表:每个节点只有一个指向下一个节点的指针。这意味着你只能从当前节点访问下一个节点,而无法直接访问前一个节点。
双向链表:每个节点有两个指针,一个指向前一个节点,一个指向后一个节点。这使得你可以从当前节点访问前一个节点和后一个节点。
遍历方向
单链表:由于每个节点只有一个指向下一个节点的指针,因此只能从头到尾单向遍历。
双向链表:由于每个节点有两个指针,因此可以从头到尾,也可以从尾到头双向遍历。
增删节点复杂度
单链表:增加和删除节点相对简单,只需调整一个指针。例如,删除一个节点时,只需将前一个节点的指针指向被删除节点的下一个节点。
双向链表:增加和删除节点较为复杂,需要调整两个指针。例如,删除一个节点时,需要将前一个节点的后指针指向被删除节点的后一个节点,并将后一个节点的前指针指向被删除节点的前一个节点。
内存占用
单链表:内存占用较少,因为每个节点只包含一个指针。
双向链表:内存占用较多,因为每个节点包含两个指针。
使用场景
单链表:适用于节点的增加和删除操作频繁的场景,因为这些操作相对简单。
双向链表:适用于需要双向查找节点值的场景,因为可以从两个方向遍历链表,提供了更大的灵活性。
三、双向链表的实现
1、定义节点结构
双向链表的节点也是结构体结构,别忘了双向链表每个节点有prev和next两个指针
注意
双向链表为空时,还剩下一个头结点,头结点的prev和next指针都指向自己,如下图所示:
双向链表头结点不存储有效数据
//定义节点结构
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* next;//指向下一节点的指针
struct ListNode* prev;//指向前一个节点的指针
LTDataType data;//存储的数据
}LTNode;
2、双向链表要实现的功能
对于双向链表,我们要实现以下功能:
//初始化
//void LTInit(LTNode** pphead);
LTNode* LTInit();
//申请节点
LTNode* LTBuyNode(LTDataType x);
//销毁
void LTDesTroy(LTNode* phead);
//双向链表打印
void LTPrint(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//删除pos节点
void LTErase(LTNode* pos);
//查找指定数据
LTNode* LTFind(LTNode* phead, LTDataType x);
3、双向链表功能的实现
3.1、申请节点
//申请节点
LTNode* LTBuyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if(node == NULL)
{
perror("malloc fail!");
exit(1);
}
node->data = x;
node->next = node->prev = node;
return node;
}
3.2、初始化链表
双向链表需要初始化,就是给双向链表创建一个头节点(哨兵位).
//初始化
LTNode* LTInit()
{
//给双向链表创建一个哨兵位
LTNode* phead = BuyListNode(-1);//哨兵位不存储有效的值,这里传参传-1(也可以传其他值)
return phead;
}
3.3、打印链表
//打印
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
3.4、双向链表尾插
在双向链表尾插过程中,哨兵位节点(即头结点)不能被删除,节点的地址也不能发生改变(哨兵位不能被修改),所以在传形参的时候传一级指针。
//尾插
void LTPushBack(LTNode* phead,LTDataType x)
{
assert(phead);//哨兵位不能为空
LTNode* newnode = LTBuyNode(x);
newnode->prev = phead->prev;
newnode->next = phead;
phead->prev->next = newnode;/1
phead->prev = newnode;//2
//1和2不能交换位置
}
3.5、双向链表头插
头插不是往哨兵位前面去插入,而是往第一个有效的节点前面去插入新节点。
//头插
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;
}
3.6、双向链表尾删
要注意链表必须有效且链表不能为空,链表为空即链表只有一个哨兵位。
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead && phead->next != phead);//链表有效且不为空
LTNode* del = phead->prev;
del->prev->next = phead;
phead->prev = del->prev;
free(del);//删除del节点
del = NULL;
}
3.7、双向链表头删
//头删
void LTPopFront(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
free(del);//删除del节点
del = NULL;
}
3.8、查找指定数据
//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//没有找到
return NULL;
}
3.9、在指定位置之后插入数据
//在指定位置之后插入数据
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;
}
3.10、删除pos节点
//删除pos节点
void LTErase(LTNode* pos)
{
assert(pos);
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
3.11、销毁链表
//销毁链表
void LTDesTroy(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while(pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
phead = NULL;
}
LTErase和LTDestroy参数理论上要传二级指针,因为我们需要让形参的改变影响到实参,但是为了保持接口的一致性才传的一级指针,传一级指针存在的问题是:当形参phead置为NULL后,实参plist不会被修改为NULL,因此解决办法是:调用完方法后手动将实参置为NULL。
四、总代码
1、List.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LTDataType;
//定义双向链表节点的结构
typedef struct ListNode
{
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
//初始化
//void LTInit(LTNode** pphead);
LTNode* LTInit();
//申请节点
LTNode* LTBuyNode(LTDataType x);
//销毁
void LTDesTroy(LTNode* phead);
//双向链表打印
void LTPrint(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//删除pos节点
void LTErase(LTNode* pos);
//查找指定数据
LTNode* LTFind(LTNode* phead, LTDataType x);
2、List.c
#include"List.h"
//申请节点
LTNode* LTBuyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if(node == NULL)
{
perror("malloc fail!");
exit(1);
}
node->data = x;
node->next = node->prev = node;
return node;
}
//初始化
LTNode* LTInit()
{
//给双向链表创建一个哨兵位
LTNode* phead = BuyListNode(-1);//哨兵位不存储有效的值,这里传参传-1(也可以传其他值)
return phead;
}
//打印
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
//尾插
void LTPushBack(LTNode* phead,LTDataType x)
{
assert(phead);//哨兵位不能为空
LTNode* newnode = LTBuyNode(x);
newnode->prev = phead->prev;
newnode->next = phead;
phead->prev->next = newnode;/1
phead->prev = newnode;//2
//1和2不能交换位置
}
//头插
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;
}
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead && phead->next != phead);//链表有效且不为空
LTNode* del = phead->prev;
del->prev->next = phead;
phead->prev = del->prev;
free(del);//删除del节点
del = NULL;
}
//头删
void LTPopFront(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
free(del);//删除del节点
del = NULL;
}
//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//没有找到
return NULL;
}
//在指定位置之后插入数据
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;
}
//删除pos节点
void LTErase(LTNode* pos)
{
assert(pos);
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
//销毁链表
void LTDesTroy(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while(pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
phead = NULL;
}
3、test.c
#include"List.h"
void ListTest01()
{
//LTNode* plist = NULL;
//LTInit(&plist);
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPrint(plist);
LTNode* find = LTFind(plist, 3);
//LTInsert(find, 66);
LTErase(find);
find = NULL;
LTPrint(plist);
LTDesTroy(plist);
//测试尾删
LTPopBack(plist);
LTPrint(plist);
LTPopBack(plist);
LTPrint(plist);
LTPopBack(plist);
LTPrint(plist);
}
int main()
{
ListTest01();
return 0;
}
那么到这里就结束啦!感谢您的阅读,点个小赞再走吧。后面将持续为您更新哦!