【链表】双向链表的介绍和基本操作(C语言实现)【保姆级别详细教学】
双向链表


文章目录
前言
先赞后看好习惯 打字不容易,这都是很用心做的,希望得到支持你 大家的点赞和支持对于我来说是一种非常重要的动力 看完之后别忘记关注我哦!️️️
作者: @小小Programmer
这是我的主页:@小小Programmer
在食用这篇博客之前,博主在这里介绍一下其它高质量的编程学习栏目:
数据结构专栏:数据结构 这里包含了博主很多的数据结构学习上的总结,每一篇都是超级用心编写的,有兴趣的伙伴们都支持一下吧!
算法专栏:算法 这里可以说是博主的刷题历程,里面总结了一些经典的力扣上的题目,和算法实现的总结,对考试和竞赛都是很有帮助的!
力扣刷题专栏:Leetcode想要冲击ACM、蓝桥杯或者大学生程序设计竞赛的伙伴,这里面都是博主的刷题记录,希望对你们有帮助!
干货满满~ 强烈建议本篇收藏后再食用~
看完本篇,相信你会对双向链表有一个比较深入的了解,而且会深深地体会到这一种链表相比于之前的单链表结构上的优越性、使用更为方便的特点。
双向链表的基本介绍
一些链表的分类
在链表这一数据结构的模块里,我们可以通过它的结构,做出以下分类。
| 单向 | 双向 |
| 循环 | 非循环 |
| 带头 | 不带头 |
| tips:带头即:带有哨兵位的头结点的链表,哨兵位头结点不存储数据。 |
最常用的两种链表即:
1.无头单向非循环链表
这一种单链表,若单独使用,缺陷较多,不常用
1)因此,单链表经常在OJ题里面出现。
2)另外,它是很多复杂数据结构的子结构,如图、哈希表等
2.带头双向循环链表
1)常用。
2)STL里面的list就是这种链表
因此,这两种链表都是我们必须掌握的知识点
如果对无头单向非循环链表不太了解的伙伴,可以翻看我之前的博客,先做了解,再食用本篇【数据结构】单链表的介绍和基本操作(C语言实现)【保姆级别详细教学】
带头双向循环链表的基本结构
带头双向循环链表在以下简称为双向链表,无头单向非循环链表简称单链表。
以下就是双向链表的基本结构
这种链表的结点里面,相比于单链表,多了一个prev指针,指向前一个结点。
head头结点-不存储数据,作为哨兵位使用
head头结点的prev指向链表尾
链表尾的next指向head
特殊的:链表为空的时候,并不是一个结点都没有,而是只有头结点。此时head的next和prev均指向它自己。
有了这些铺垫,我们就可以开始实现我们的双向链表了。
双向链表的实现
同样,实现这个链表需要3个源文件,这样可以使我们的程序可读性更高,更为清晰。对此不明白的伙伴可以翻看博主之前关于单链表或者扫雷游戏的作品,里面有讲解~
test.c:用于测试
List.c:用于实现接口
List.h:存放接口的声明
结点的定义、头指针的创建
学习过单链表的伙伴应该已经对这一步骤很熟悉了。
在.h文件里创建结点
typedef int LTDataType;
typedef struct ListNode {
//指针域
struct ListNode* next;
struct ListNode* prev;
//数据域
LTDataType data;
}ListNode;
然后在.c的main()里创建头指针
void TestList1() {
ListNode* phead = NULL;
}
int main() {
TestList1();
return 0;
}
这个时候,我们还需要初始化一些我们的头结点,记住只有头结点链表为空。头结点初始化完之后,在后续操作中,头结点是不动的。
这里非常关键,我们知道,实现没有头的链表的时候,做头插,头删这些操作的时候,因为头结点会有可能改变,因此每次我们从
main()里传参,都要传头指针的地址,也就是一个二级指针,但是
**带头的就不同!**头指针永不变,这表明,除了初始化头指针这个接口之外,别的接口,都不需要传二级指针!
因此,我们现在需要创建一个头指针了!
此时需要一个开辟结点的接口,我们先写这个开辟结点的接口。
开辟结点接口
ListNode* BuyListNode(LTDataType x);//.h的声明
ListNode* BuyListNode(LTDataType x) {
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
node->next = NULL;//前后的指针先置空,到了操作接口里面,我们再操作这些指针
node->prev = NULL;
node->data = x;//数据置为传入的x
return node;
}
有了这个开辟结点的接口,我们可以初始化头结点了
初始化头结点接口
void ListInit(ListNode** pphead);//刚才解释过了,要传入二级指针。
void ListInit(ListNode** pphead) {//初始化的时候要定义头结点,所以要二级指针
*pphead = BuyListNode(0);
(*pphead)->next = *pphead;
(*pphead)->prev = *pphead;
//->优先级和*相同,所以括号一下
}
需要注意的点:
1.因为我们的头结点不存储数据,所以传0给开辟结点接口
2.只有头结点的时候链表为空,head的两个指针都要指向自己
接下来,我们可以先把打印接口写一写
打印接口
//打印接口
void ListPrint(ListNode* phead);
void ListPrint(ListNode* phead) {
//这里的打印和单链表就不一样了
//1.不能直接遍历
//2.哨兵位的头结点不要打印
assert(phead);//头结点肯定不能为空
ListNode* cur = phead->next;//跳过头结点
while (cur != phead) {
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
注意:
1.和单链表的遍历不一样,这里的遍历是到cur回到phead为结束标志,因为它是一个循环接口,这个很好理解
2.另外,与单链表不同,这里所有的接口都要assert(),因为头结点永远都在。
3.打印的时候要跳过头结点。
尾插接口
尾插:即在链表尾部插入一个新结点
从现在开始,我们将会深深地体会到这一种链表结构上的优越性,它们的代码实现实在是比单链表简单太多了,以致于有些地方根本不需要多解释,伙伴们都能够明白
尾插
首先,在单链表中,我们的第一步是遍历找尾,在这里我们需要这样吗?
phead的prev就是尾,根本就不用找。
因此,我们所需要做的,就是定义一个tail,让tail,phead,newnode之间的连接关系搞好,就大功告成了。
其次,在单链表中,我们重新调整结点之间连接关系的时候,常常需要临时指针储存我们的结点,为什么:怕丢,我们调整一个指针的时候,可能就会丢掉原来那个,为什么这么容易丢:因为每个结点只有一个指针指着。
而在这里,我们需要这样做吗?很明显不需要!我们每个结点都有多个指针指着,我们美美地调整连接关系就可以了。
其三:在单链表中,我们常常要在操作的时候分情况,链表为空吗,链表只有一个结点还是多个?在这里统统不需要,因为我们有带哨兵位的头结点。不明白的伙伴画个图就明白了。
我们直接上代码:
void ListPushBack(ListNode* phead, LTDataType x) {
//这种链表的尾插非常简单
//不用找尾
//头结点的prev就是尾
//而且不用判断链表是否为空,因为这是带头结点的链表
assert(phead);
ListNode* tail = phead->prev;//找到尾了
ListNode* newnode = BuyListNode(x);
//phead ...tail..newnode
//处理tail和newnode的关系
tail->next = newnode;
newnode->prev = tail;
//处理head和newnode的关系
newnode->next = phead;
phead->prev = newnode;
}
我们可以测试一下
test.c
void TestList1() {
ListNode* phead = NULL;
//初始化
ListInit(&phead);//不用二级指针,用返回的方式得到栈上开辟的新指针也是可以的
//尾插
ListPushBack(phead, 1);
ListPushBack(phead, 2);
ListPushBack(phead, 3);
ListPushBack(phead, 4);
ListPrint(phead);
}
int main() {
TestList1();
return 0;
}

其实看到这里,伙伴们应该都具有独立写出后面那些接口的能力了,可以先尝试自己写,其实真的比单链表简单很多,写完在继续食用下面的接口
尾删接口
尾删:在链表末尾删除一个结点
void ListPopBack(ListNode* phead) {
assert(phead);
//这里要稍微注意一下,链表不能为空,空了就把头结点删了,删完还要删就崩了
assert(phead->next != phead);
ListNode* tail = phead->prev;
phead->prev = tail->prev;
phead->prev->next = phead;//画个图就能明白
//这一句看不明白的可以画图,或者定义一个tailPrev也是可以的,这样更清晰
//以后尽量少写
//解决方法:定义一个tailPrev即可
free(tail);
tail = NULL;//别忘了这一句,养成好习惯
}
注意:删除结点的接口要多一个细节:判断链表是否为空,因此加多一句assert()即可。
assert(phead->next != phead);
头插接口
头插:在链表头插入一个新结点
头插其实就是在头结点和第一个结点之间插入一个新结点
很简单:定义一个first结点表示第一个结点,然后调整newnode,phead和first三者关系即可。
void ListPushFront(ListNode* phead,LTDataType x) {
//比较简单,但是要判断一下链表为空的情况
ListNode* first = phead->next;
ListNode* newnode = BuyListNode(x);
//调整关系
phead->next = newnode;
newnode->prev = phead;
newnode->next = first;
first->prev = newnode;
}
写链表要细心,对特殊情况要敏感一些,考虑链表为空的情况。
我们用同样的逻辑套一下那个空链表的特殊情况,发现上面那段代码是符合的,first就是phead自己,完全没问题
这就是双向链表的优势
头删接口
头删:删除第一个结点
定义一个first指向第一个结点,second指向第二个结点,删除first,重新调整phead和second的关系即可。
void ListPopFront(ListNode* phead) {
assert(phead);
assert(phead->next != phead);
//同样,非常简单,phead-first-second 把first free掉就可以了
ListNode* first = phead->next;
ListNode* second = first->next;
phead->next = second;
second->prev = phead;
free(first);
first = NULL;
//写到这里真的是感受到了双向带头链表的优越性,毫无死角,操作非常简单
}
同样:删除的接口要有
assert(phead->next != phead);
查找接口
查找接口通常和在任意位置插入结点,在任意位置删除结点,修改结点,这些功能结合在一起,因为我们找到以后,想改可以改,想插入可以插入,想删除可以删除了。
//查找
ListNode* ListFind(ListNode* phead, LTDataType x) {
assert(phead);
ListNode* cur = phead->next;
while (cur != phead) {
if (cur->data == x) {
return cur;
}
cur = cur->next;
}
return NULL;//找了一圈都没有找到,返回空
}
插入接口
在pos位置前插入一个结点
关键还是调整结点之间的链接关系
思路非常简单,不赘述了。不明白的小伙伴可以私信留言
//插入
void ListInsert(ListNode* pos, LTDataType x) {
//在pos前面插入x
assert(pos);
ListNode* posPrev = pos->prev;
ListNode* newnode = BuyListNode(x);
//posPrev newnode pos 的链接关系
posPrev->next = newnode;
newnode->prev = posPrev;
newnode->next = pos;
pos->prev = newnode;
}
删除接口
删除pos位置的结点
//删除
void ListErase(ListNode* pos) {
assert(pos);
//注意:pos不能是phead
//assert(pos != phead);
ListNode* posPrev = pos->prev;
ListNode* posNext = pos->next;
free(pos);
pos = NULL;
posPrev->next = posNext;
posNext->prev = posPrev;
}
我们可以测试一下最后这三个接口
void TestList2() {
ListNode* phead = NULL;
ListInit(&phead);
//先尾插一些数据进去先
ListPushBack(phead, 1);
ListPushBack(phead, 2);
ListPushBack(phead, 3);
ListPushBack(phead, 4);
ListPrint(phead);
//想要在3前面插入30
ListNode* pos = ListFind(phead, 3);
ListInsert(pos, 30);
ListPrint(phead);
//删除3
pos = ListFind(phead,3);
ListErase(pos);
ListPrint(phead);
//30改300
pos = ListFind(phead, 30);
pos->data = 300;
ListPrint(phead);
}
int main() {
//TestList1();
TestList2();
return 0;
}

在本篇中博主并没有展示所有接口的测试,但是我们自己写的时候,我们每写完一个都要测试,这是一个编程的好习惯,而且测试成功也会给我们自己更多的自信。
测试代码和头文件代码的完整展示
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include"List.h"
#if 1
void TestList1() {
ListNode* phead = NULL;
ListInit(&phead);//不用二级指针,用返回的方式得到栈上开辟的新指针也是可以的
//测试尾插
ListPushBack(phead, 1);
ListPushBack(phead, 2);
ListPushBack(phead, 3);
ListPushBack(phead, 4);
ListPrint(phead);
//测试尾删
ListPopBack(phead);
ListPopBack(phead);
ListPopBack(phead);
ListPrint(phead);
//测试头插
ListPushFront(phead, 0);
ListPushFront(phead, -1);
ListPushFront(phead, -2);
ListPushFront(phead, -3);
ListPrint(phead);
//测试头删
ListPopFront(phead);
ListPopFront(phead);
ListPopFront(phead);
ListPopFront(phead);
ListPrint(phead);
}
void TestList2() {
ListNode* phead = NULL;
ListInit(&phead);
ListPushBack(phead, 1);
ListPushBack(phead, 2);
ListPushBack(phead, 3);
ListPushBack(phead, 4);
ListPrint(phead);
//想要在3前面插入30
ListNode* pos = ListFind(phead, 3);
ListInsert(pos, 30);
ListPrint(phead);
//删除3
pos = ListFind(phead,3);
ListErase(pos);
ListPrint(phead);
//30改300
pos = ListFind(phead, 30);
pos->data = 300;
ListPrint(phead);
}
int main() {
TestList1();
TestList2();
return 0;
}
List.h
#pragma once
#include<stdio.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode {
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}ListNode;
//初始化
void ListInit(ListNode** pphead);
//开辟新结点接口
ListNode* BuyListNode(LTDataType x);
//打印接口
void ListPrint(ListNode* phead);
//尾插
void ListPushBack(ListNode* phead, LTDataType x);
//尾删
void ListPopBack(ListNode* phead);
//头插
void ListPushFront(ListNode* phead, LTDataType x);
//头删
void ListPopFront(ListNode* phead);
//查找(修改)
ListNode* ListFind(ListNode* phead,LTDataType x);
//插入
void ListInsert(ListNode* pos, LTDataType x);
//删除
void ListErase(ListNode* pos);
尾声
看到这里,相信伙伴们已经对带头双向循环链表已经有了比较深入的了解,掌握了基本的操作接口实现方法。相信我们已经深深感受到了这种结构的厉害之处。
如果看到这里的你感觉这篇博客对你有帮助,不要忘了收藏,点赞,转发,关注哦。
【链表】双向链表的介绍和基本操作(C语言实现)【保姆级别详细教学】的更多相关文章
- R语言-Knitr包的详细使用说明
R语言-Knitr包的详细使用说明 by 扬眉剑 来自数盟[总舵] 群:321311420 1.相关资料 1:自动化报告-谢益辉 https://github.com/yihui/r-ninja/bl ...
- 「C语言」单链表/双向链表的建立/遍历/插入/删除
最近临近期末的C语言课程设计比平时练习作业一下难了不止一个档次,第一次接触到了C语言的框架开发,了解了View(界面层).Service(业务逻辑层).Persistence(持久化层)的分离和耦合, ...
- 详解双向链表的基本操作(C语言)
@ 目录 1.双向链表的定义 2.双向链表的创建 3.双向链表的插入 4.双向链表的删除 5.双向链表更改节点数据 6.双向链表的查找 7.双向链表的打印 8.测试函数及结果 1.双向链表的定义 上一 ...
- [数据结构]单向链表及其基本操作(C语言)
单向链表 什么是单向链表 链表是一种物理储存单元上非连续.非顺序的储存结构.它由一系列结点(链表中每一个元素称为结点)组成,结点可动态生成.每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存 ...
- MySQL数据库 介绍,安装,基本操作
- 数据库介绍: 1.随意存放在一个文件中的数据,数据的格式千差万别 tank|123 jason:123 sean~123 2.软件开发目录规范 - Project: - conf - bin - ...
- mariadb_1 数据库介绍及基本操作
数据库介绍 1.什么是数据库? 简单的说,数据库就是一个存放数据的仓库,这个仓库是按照一定的数据结构(数据结构是指数据的组织形式或数据之间的联系)来组织,存储的,我们可以通过数据库提供的多种方法来管理 ...
- 二叉树的基本操作(C语言版)
今天走进数据结构之二叉树 二叉树的基本操作(C 语言版) 1 二叉树的定义 二叉树的图长这样: 二叉树是每个结点最多有两个子树的树结构,常被用于实现二叉查找树和二叉堆.二叉树是链式存储结构,用的是二叉 ...
- cassandra简单介绍与基本操作
项目中用到了cassandra,用来存储海量数据,且要有高效的查询:本博客就进行简单的介绍和进行一些基本的操作 一.使用场景: 是一款分布式的结构化数据存储方案(NoSql数据库),存储结构比Key- ...
- hibernate框架学习第一天:hibernate介绍及基本操作
框架辅助开发者进行开发,半成品软件,开发者与框架进行合作开发 Hibernate3Hibernate是一种基于Java的轻量级的ORM框架 基于Java:底层实现是Java语言,可以脱离WEB,在纯J ...
- 顺序栈的基本操作(C语言)
由于现在只学了C语言所以就写这个C语言版的栈的基本操作 这里说一下 :网上和书上都有这种写法 int InitStack(SqStack &p) &p是取地址 但是这种用法好像C并不 ...
随机推荐
- Serverless Devs 重大更新,基于 Serverless 架构的 CI/CD 框架:Serverless-cd
近日,Serverless 开发者平台 Serverless Devs 重磅发布基于 Serverless 架构的轻量级 CI/CD 框架--Serverless-cd.Serverless-cd 是 ...
- uniapp picker组件实现二级联动
https://blog.csdn.net/hxh_csdn/article/details/111504951 https://www.cnblogs.com/jstll/p/14149600.ht ...
- Qt大型工程开发技术选型PartFinal:CLR调用COM组件
Qt大型工程开发技术选型PartFinal:CLR调用COM组件 这里其实没什么内容了,直接上代码吧,如下文所示: #pragma once #using <mscorlib.dll> u ...
- python爬虫-豆瓣电影top250
一.python爬虫简介1.什么是爬虫:网络爬虫,是一种按照一定规则,自动抓取互联网信息的程序或者脚本.由于互联网数据的多样性和资源的有限性,根据用户需求定向抓取相关网页并分析已成为如今主流的爬取策略 ...
- python之HtmlTestRunner(一)生成测试报告
一.下载安装 windows10,cmd环境通过如下命令
- Java 如何将Excel转换为TXT文本格式
TXT文件是一种非常简单.通用且易于处理的文本格式.在处理大规模数据时,将Excel转为TXT纯文本文件可以提高处理效率.此外,许多编程语言和数据处理工具都有内置的函数和库来读取和处理TXT文件,因此 ...
- Angular系列教程之依赖注入详解
.markdown-body { line-height: 1.75; font-weight: 400; font-size: 16px; overflow-x: hidden; color: rg ...
- APP Inventor的tcp连接扩展插件ClientSocketAl2Ext
参考原文:https://www.cnblogs.com/bemfa/p/13390251.html 下载地址:https://wwtl.lanzoum.com/ik0Ky1clu41a B站关联学习 ...
- JMS微服务开发示例(四)把配置文件appsettings.json 部署在网关,共享给其他相同的微服务
通常,多个相同的微服务器,它们的appsettings.json配置文件的内容都是一样的,如果,每次修改配置文件,都要逐个替换,那就太繁琐了,我们可以利用网关的文件共享功能,实现配置文件的统一更新. ...
- [转帖]linux audit审计(7-1)--读懂audit日志
https://www.cnblogs.com/xingmuxin/p/8807774.html auid=0 auid记录Audit user ID,that is the loginuid.当我 ...
