目录
🥕前言🥕:
🌽一、双向链表概述🌽:
1.双向链表概念:
2.双向链表结构:
🍆二、双向链表接口实现🍆:
1.工程文件建立:
2.接口实现(本文重点):
Ⅰ.双向链表初始化:
Ⅱ.打印双向链表:
Ⅲ.申请新节点:
Ⅳ.双向链表尾插:
Ⅴ.双向链表尾删:
Ⅵ.双向链表头插:
Ⅶ.双向链表头删:
Ⅷ.双向链表查找:
Ⅸ.双向链表给定节点前插:
Ⅹ.双向链表给定节点后插:
ⅩⅠ.双向链表删除给定节点:
ⅩⅡ.双向链表销毁:
🍄三、完整接口实现代码🍄:
1.List.h:
2.List.c:
3.test.c:
🌶️四、顺序表与链表对比🌶️:
1.两者差异:
2.存储器层次结构(辅图):
🥬总结🥬:
🛰️博客主页:✈️銮同学的干货分享基地
🛰️欢迎关注:👍点赞🙌收藏✍️留言
🛰️系列专栏:🎈 数据结构
🎈【进阶】C语言学习
🎈 C语言学习
🛰️代码仓库:🎉数据结构仓库
🎉VS2022_C语言仓库
家人们更新不易,你们的👍点赞👍和⭐关注⭐真的对我真重要,各位路过的友友麻烦多多点赞关注,欢迎你们的私信提问,感谢你们的转发!
关注我,关注我,关注我,你们将会看到更多的优质内容!!
🏡🏡 本文重点 🏡🏡:
🚅 双向链表 🚃 顺序表与链表对比 🚏🚏
🥕前言🥕:
上节课中我们完整的实现了无头单向链表的各个功能接口,但是我们也注意到,由于单向链表只保存指向下一节点的指针,于是我们在进行前插等操作时,还需要遍历链表来找到前一个节点,效率不高且步骤繁琐。于是为了克服类似这样的问题,我们更多的会使用另一种数据结构,即带头双向循环链表,本文就将带领各位小伙伴们一起实现带头双向循环链表的各接口功能。
🌽一、双向链表概述🌽:
1.双向链表概念:
双向链表也叫双链表,是链表的一种,它的每个数据节点中都有两个指针,分别指向直接后继和直接前驱。所以从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
并且在上节课中我们就说过:
- 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
- 带头双向循环链表:结构最复杂,一般用在单独存储数据。虽然它结构复杂,但在实际使用中使用代码实现后,优秀的结构会带来很多优势,实现反而更加简单。并且在我们的实际中所使用的链表数据结构,一般都是带头双向循环链表。
所以今天我们就主要研究带头双向循环链表的各个接口功能的实现。
2.双向链表结构:
🍆二、双向链表接口实现🍆:
1.工程文件建立:
我们仍使用模块化开发格式,使用 List.h 、List.c 、test.c 三个文件进行代码书写:
- List.h:存放函数声明、包含其他头文件、定义宏。
- List.c:书写函数定义,书写函数实现。
- test.c:书写程序整体执行逻辑。
这其中,我们的接口实现主要研究的是函数实现文件 List.c 中的内容,对 test.c 文件中的内容分不关心。
2.接口实现(本文重点):
这里是本文重点中的重点,即 List.c 文件中的接口具体实现:
Ⅰ.双向链表初始化:
- 双向链表初始化:
- 动态申请首节点,并使两个指向直接前置节点与直接后继节点的指针均指向自己,形成循环结构。
- 最后返回初始化完成的头节点。
LNode* LInit() { //哨兵位头节点: LNode* phead = (LNode*)molloc(sizeof(LNode)); phead->next = phead; phead->prev = phead; return phead; }
Ⅱ.打印双向链表:
- 执行操作前需对传入指针进行非空判断,防止对空指针进行操作。
- 双向链表的打印方式与单链表相似,采用方式均为由头节点开始,通过节点指针指向寻找下一节点的方式循环进行遍历打印。
- 不同点是,由于双向循环链表首尾相连,形成闭环,因此终止循环打印条件将不再是执行至空指针而是执行回到头节点(即完成整个循环)。
void LPrint(LNode* phead) { if (phead == NULL) { return; } LNode* cur = phead->next; while (cur != phead) { printf("%d ", cur->data); cur = cur->next; } printf("NULL\n"); }
Ⅲ.申请新节点:
- 新节点的申请与单链表基本相同,动态申请新节点后,再对新节点进行操作。
- 不同点是双向链表多出一个指向直接前驱节点的指针,因此该指针也需要在使用前进行置空操作,防止造成野指针错误。
LNode* BuyListNode(LDataType x) { LNode* newnode = (LNode*)malloc(sizeof(LNode)); newnode->data = x; newnode->next = NULL; newnode->prev = NULL; return newnode; }
Ⅳ.双向链表尾插:
- 执行操作前应当进行非空判断,防止传入空指针。
- 在进行尾插操作前应当首先找到尾节点,采用的方式是,通过双向循环链表中头节点的前驱指针指向来找到尾节点。
- 接着动态申请新节点。
- 最后执行尾插操作。首先使前面找到的尾节点的后继指针指向新节点,并使新节点的前驱指针也指向尾节点;接着使头节点的前驱指针指向新节点,并使新节点的后继指针指向头节点。
void LPushBack(LNode* phead, LDataType x) { if (phead == NULL) { return; } LNode* tail = phead->prev; //找到尾节点 LNode* newnode = BuyListNode(x); //新尾互指: tail->next = newnode; newnode->prev = tail; //新头互指: phead->prev = newnode; newnode->next = phead; }
- 测试尾插接口功能实现:
Ⅴ.双向链表尾删:
- 在双向链表进行尾删时,不仅要防止传入空指针,同时也要注意避免链表为空(只含有哨兵节点)的情况发生。
- 首先找到尾节点及尾节点的前驱节点,接着使该前驱节点与头节点跳过尾节点互指,最后释放原尾节点并置空即可。
void LPopBack(LNode* phead) { if ((phead == NULL) || (phead->next == phead)) //排除为空的情况 { return; } LNode* tail = phead->prev; //找到尾 LNode* tailprev = tail->prev; //找到尾的前驱 tailprev->next = phead; //尾前驱节点的后继指针指向头 phead->prev = tail->prev; //头的前驱指针指向尾的前驱 free(tail); tail = NULL; }
- 测试尾删接口功能实现:
Ⅵ.双向链表头插:
- 在执行操作前应当对传入指针进行判断,防止传入空指针。
- 在改变指向前,应当首先保存哨兵节点的后继节点,因为当我们插入新节点后链表结构将会发生改变,再想要找到该节点将变得麻烦。
- 进行插入操作时,使哨兵节点的后继指针指向新节点,再使新节点的前驱指针指向哨兵节点,接着使用同样的操作使新节点与哨兵节点的原后继节点互指。
void LPushFront(LNode* phead, LDataType x) { if (phead == NULL) { return; } LNode* newnode = BuyListNode(x); LNode* next = phead->next; //保存哨兵节点的后继节点 phead->next = newnode; //哨兵点的后继指针指向新节点 newnode->prev = phead; //新节点的前驱指针指向哨兵节点 newnode->next = next; //新节点的后继指针指向哨兵节点的原后继节点 next->prev = newnode; //哨兵节点的原后继节点前驱指针指向新节点 }
- 测试头插接口功能实现:
Ⅶ.双向链表头删:
- 首先进行非空判断,并排除链表为空(只含有哨兵节点)的情况。
- 开始进行头删操作前找到并保存头节点,防止改变指向后链表结构发生变化难以找到原本的头节点,以便于最后进行释放。
- 并且应当找到并保存原头节点的后继节点,防止防止改变指向后链表结构发生变化而难以找到。
- 具体操作便是使哨兵节点与头节点的后继节点跳过头节点互指,再将要删除的头节点释放并置空即可。
void LPopFront(LNode* phead) { if ((phead == NULL) || (phead->next == phead)) //排除为空的情况 { return; } LNode* next = phead->next; //保存头节点,便于释放 LNode* nextNext = next->next; //保存头节点的后继节点 phead->next = nextNext; nextNext->prev = phead; free(next); next = NULL; }
- 测试头删接口功能实现:
Ⅷ.双向链表查找:
- 执行操作前需进行非空判断,防止对空指针进行操作。
- 查找逻辑很简单,遍历整个链表,直至链表完整循环一遍,若比对存在匹配元素返回该节点,否则返回空。
LNode* LFind(LNode* phead, LDataType x) { if (phead == NULL) { return; } LNode* cur = phead->next; while (cur != phead) { if (cur->data == x) { return cur; } else { cur = cur->next; } } return NULL; }
- 测试查找接口功能实现:
Ⅸ.双向链表给定节点前插:
- 执行操作前需进行非空判断,防止对空指针进行操作。
- 执行逻辑很简单,将数据存入通过动态申请的来的新节点中后,找到目标节点,使目标节点的前驱节点与新节点互指,再使新节点与目标节点互指接即可。
void LInsert(LNode* pos, LDataType x) { if (pos == NULL) { return; } LNode* posPrev = pos->prev; LNode* newnode = BuyListNode(x); posPrev->next = newnode; newnode->prev = posPrev; newnode->next = pos; pos->prev = newnode; }
- 测试前插接口功能实现:
Ⅹ.双向链表给定节点后插:
- 执行操作前需进行非空判断,防止对空指针进行操作。
- 执行逻辑与前插高度类似,不同的是使目标节点的后继节点与新节点互指,再使新节点与目标节点互指。
void LInsertBack(LNode* pos, LDataType x) { if (pos == NULL) { return; } LNode* posPrev = pos->next; LNode* newnode = BuyListNode(x); posPrev->prev = newnode; newnode->next = posPrev; newnode->prev = pos; pos->next = newnode; }
- 测试后插接口功能实现:
ⅩⅠ.双向链表删除给定节点:
- 执行操作前需进行非空判断,防止操作空指针。
void LErase(LNode* pos) { if (pos == NULL) { return; } LNode* posPrev = pos->prev; LNode* posNext = pos->next; posPrev->next = posNext; posNext->prev = posPrev; free(pos); pos = NULL; }
- 测试删除接口功能实现:
ⅩⅡ.双向链表销毁:
- 若哨兵节点为空,即表示链表内没有有效数据节点,则无需进行销毁、释放与置空操作。
- 含有有效节点则遍历所有节点,将每一个节点均进行释放,特别注意所有数据节点释放完毕之后,不要忘记释放哨兵节点。
void LDestroy(LNode* phead) { if(phead==NULL) { return; } LNode* cur = phead->next; while (cur != phead); { LNode* next = cur->next; free(cur); cur = next; } free(phead); phead = NULL; }
🍄三、完整接口实现代码🍄:
1.List.h:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//自定数据类型LDataType:
typedef int LDataType;
//双向链表节点结构:
typedef struct ListNode
{
LDataType data;
struct LNode* next;
struct LNode* prev;
}LNode;
LNode* LInit(); //初始化双向循环链表
void LPrint(LNode* phead); //打印双向循环链表
LNode* BuyListNode(LDataType x);//双向循环链表新节点申请
void LPushBack(LNode* phead, LDataType x); //双向循环链表尾插
void LPopBack(LNode* phead); //双向循环链表尾删
void LPushFront(LNode* phead, LDataType x); //双向循环链表头插
void LPopFront(LNode* phead); //双向循环链表头删
LNode* LFind(LNode* phead, LDataType x); //双向循环链表查找
void LInsertFront(LNode* pos, LDataType x); //双向循环链表给定节点前插
void LInsertBack(LNode* pos, LDataType x); //双向循环链表给定节点后插
void LErase(LNode* pos); //双向循环链表给定节点删除
void LDestroy(LNode* phead); //双向循环链表的销毁
2.List.c:
#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
//初始化双向循环链表初始化:
LNode* LInit()
{
//哨兵位头节点:
LNode* phead = (LNode*)malloc(sizeof(LNode));
phead->next = phead;
phead->prev = phead;
return phead;
}
//打印双向循环链表
void LPrint(LNode* phead)
{
if (phead == NULL)
{
return;
}
LNode* cur = phead->next;
while (cur != phead)
{
printf("%d -> ", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
//双向循环链表申请新节点:
LNode* BuyListNode(LDataType x)
{
LNode* newnode = (LNode*)malloc(sizeof(LNode));
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
//双向循环链表尾插:
void LPushBack(LNode* phead, LDataType x)
{
if (phead == NULL)
{
return;
}
LNode* tail = phead->prev; //找到尾节点
LNode* newnode = BuyListNode(x);
//新尾互指:
tail->next = newnode;
newnode->prev = tail;
//新头互指:
phead->prev = newnode;
newnode->next = phead;
}
//双向循环链表尾删:
void LPopBack(LNode* phead)
{
if ((phead == NULL) || (phead->next == phead)) //排除为空的情况
{
return;
}
LNode* tail = phead->prev; //找到尾
LNode* tailprev = tail->prev; //找到尾的前驱
tailprev->next = phead; //尾前驱节点的后继指针指向头
phead->prev = tail->prev; //头的前驱指针指向尾的前驱
free(tail);
tail = NULL;
}
//双向循环链表头插:
void LPushFront(LNode* phead, LDataType x)
{
if (phead == NULL)
{
return;
}
LNode* newnode = BuyListNode(x);
LNode* next = phead->next; //保存哨兵节点的后继节点
phead->next = newnode; //哨兵点的后继指针指向新节点
newnode->prev = phead; //新节点的前驱指针指向哨兵节点
newnode->next = next; //新节点的后继指针指向哨兵节点的原后继节点
next->prev = newnode; //哨兵节点的原后继节点前驱指针指向新节点
}
//双向循环链表头删:
void LPopFront(LNode* phead)
{
if ((phead == NULL) || (phead->next == phead)) //排除为空的情况
{
return;
}
LNode* next = phead->next; //保存头节点,便于释放
LNode* nextNext = next->next; //保存头节点的后继节点
phead->next = nextNext;
nextNext->prev = phead;
free(next);
next = NULL;
}
//双向循环链表查找:
LNode* LFind(LNode* phead, LDataType x)
{
if (phead == NULL)
{
return;
}
LNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;
}
//双向循环链表给定节点前插:
void LInsert(LNode* pos, LDataType x)
{
if (pos == NULL)
{
return;
}
LNode* posPrev = pos->prev;
LNode* newnode = BuyListNode(x);
posPrev->next = newnode;
newnode->prev = posPrev;
newnode->next = pos;
pos->prev = newnode;
}
//双向循环链表给定节点前插:
void LInsertFront(LNode* pos, LDataType x)
{
if (pos == NULL)
{
return;
}
LNode* posPrev = pos->prev;
LNode* newnode = BuyListNode(x);
posPrev->next = newnode;
newnode->prev = posPrev;
newnode->next = pos;
pos->prev = newnode;
}
//双向循环链表给定节点后插:
void LInsertBack(LNode* pos, LDataType x)
{
if (pos == NULL)
{
return;
}
LNode* posPrev = pos->next;
LNode* newnode = BuyListNode(x);
posPrev->prev = newnode;
newnode->next = posPrev;
newnode->prev = pos;
pos->next = newnode;
}
//双向循环链表给定节点删除:
void LErase(LNode* pos)
{
if (pos == NULL)
{
return;
}
LNode* posPrev = pos->prev;
LNode* posNext = pos->next;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
pos = NULL;
}
//双向循环链表销毁:
void LDestroy(LNode* phead)
{
if(phead==NULL)
{
return;
}
LNode* cur = phead->next;
while (cur != phead);
{
LNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
phead = NULL;
}
3.test.c:
#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
void LTest()
{
LNode* plist = LInit();
//双向循环链表尾插:
LPushBack(plist, 1);
LPushBack(plist, 2);
LPushBack(plist, 3);
LPushBack(plist, 4);
LPrint(plist); //打印链表
LNode* ret = LFind(plist, 3);
LErase(ret);
LPrint(plist); //打印链表
}
int main()
{
LTest();
return 0;
}
//双向循环链表尾插与尾删:
//LNode* plist = LInit();
双向循环链表尾插:
//LPushBack(plist, 1);
//LPushBack(plist, 2);
//LPushBack(plist, 3);
//LPushBack(plist, 4);
//LPrint(plist); //打印链表
双向循环链表尾删:
//LPopBack(plist);
//LPopBack(plist);
//LPopBack(plist);
//LPrint(plist); //打印链表
//双向循环链表头插与头删:
//LNode* plist = LInit();
//双向循环链表头插:
//LPushFront(plist, 1);
//LPushFront(plist, 2);
//LPushFront(plist, 3);
//LPushFront(plist, 4);
//LPrint(plist); //打印链表
双向循环链表头删:
//LPopFront(plist);
//LPopFront(plist);
//LPopFront(plist);
//LPrint(plist); //打印链表
//双向循环链表查找:
//LNode* plist = LInit();
//双向循环链表尾插:
//LPushBack(plist, 1);
//LPushBack(plist, 2);
//LPushBack(plist, 3);
//LPushBack(plist, 4);
//LPrint(plist); //打印链表
//LNode* ret = LFind(plist, 3);
//双向循环链表给定位置前插:
//LNode* plist = LInit();
双向循环链表尾插:
//LPushBack(plist, 1);
//LPushBack(plist, 2);
//LPushBack(plist, 3);
//LPushBack(plist, 4);
//LPrint(plist); //打印链表
//LNode* ret = LFind(plist, 3);
//LInsertFront(ret, 5);
//LPrint(plist); //打印链表
//双向循环链表给定位置后插:
//LNode* plist = LInit();
双向循环链表尾插:
//LPushBack(plist, 1);
//LPushBack(plist, 2);
//LPushBack(plist, 3);
//LPushBack(plist, 4);
//LPrint(plist); //打印链表
//LNode* ret = LFind(plist, 3);
//LInsertBack(ret, 5);
//LPrint(plist); //打印链表
//双向循环链表给定节点删除:
//LNode* plist = LInit();
双向循环链表尾插:
//LPushBack(plist, 1);
//LPushBack(plist, 2);
//LPushBack(plist, 3);
//LPushBack(plist, 4);
//LPrint(plist); //打印链表
//LNode* ret = LFind(plist, 3);
//LErase(ret);
//LPrint(plist); //打印链表
🌶️四、顺序表与链表对比🌶️:
1.两者差异:
不同点 | 顺序表 | 链 表 |
---|---|---|
连续性 | 物理上一定连续 | 逻辑上一定连续 |
是否支持随机访问 | 是 | 否 |
任意位置节点修改 | 可能需要搬移元素,效率低 | 只需改变指针指向,效率高 |
插入方式 | 动态顺序表,使用时需扩容 | 无容量概念 |
应用场景 | 数据高效存储+频繁访问 | 节点及数据修改频繁 |
缓存利用率 | 高 | 低 |
2.存储器层次结构(部分差异成因辅图):
🥬总结🥬:
至此我们关于带头双向循环链表,乃至链表全部内容的学习和讲解就到此为止喽~不知道各位小伙伴们掌握了多少呢?希望各位小伙伴们下去以后多加练习,夯实基础,牢固掌握链表与顺序表的相关接口实现与使用,为后面的 C++ 高阶数据结构学习打好坚实的基础!!!
🔥🔥相信自己拥有无限的潜力,只要你有一刻渴望成长,它就会支撑着你开花结果🔥🔥
更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!