数据结构 -- 双向链表

谁说我没有死过? 出生以前, 太阳已无数次起落. 悠久的时光被悠久的虚无吞没, 又以我生日的名义卷土重来. --史铁生

目录

  • 1. 前言
  • 2. 双向链表的结构
  • 3. 双向链表的实现
  • 4. 完整代码
  • 5. 总结


正文开始

1. 前言

双向链表是一种常见的数据结构,它与单向链表相比,在存储元素的同时还记录了元素的前驱节点。双向链表可以实现双向遍历,不仅可以从头到尾遍历元素,还可以从尾到头遍历。这种特性使得双向链表在某些场景下更加方便和高效。

在双向链表中,每个节点都有两个指针,一个指向前驱节点,一个指向后继节点。这样,我们可以通过前驱指针和后继指针,方便地进行插入、删除、查找等操作。

在接下来的文章中,我们将详细讨论双向链表的实现和常见操作。我们将逐步介绍双向链表的构造、插入、删除、查找等操作,并给出相应的代码示例。通过学习和理解这些操作,我们可以更好地理解和应用双向链表,提升自己的编程能力。让我们开始吧!

更多知识点击: 酷酷学!!!
更多精彩 不要忘了关注哟~~


2. 双向链表的结构

双向链表与顺序表的区别:

在这里插入图片描述

注意:

这里的“带头”跟前面我们说的“头节点”是两个概念,实际前面的在单链表阶段称呼不严谨,但是为了更好的理解就直接称为单链表的头节点。带头链表里的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这里“放哨的”.

“哨兵位”存在的意义:

遍历循环链表避免死循环。

在这里插入图片描述

链表的分类可以分为八种, 但最常见的无非两种:

单链表和双向带头循环链表

在这里插入图片描述

在这里插入图片描述

  1. 无头单向非循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。
  2. 带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带来很多优势,实现反⽽简单了,下面我们就来实现带头双向循环链表.

3. 双向链表的实现

第一步:
在头文件中定义和声明需要使用的双向链表结构体和函数的声明, 我们分别创建三个文件, LIst.h 文件主要用来进行类型和函数的声明, List.c文件主要进行函数的实现, test.c用来进行测试文件 :

List.h:

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int DataType;typedef struct ListNode
{DataType data;struct ListNode* next;struct ListNode* prev;
}LTNode;//初始化
void LTInit(LTNode** pphead);
//LTNode* LTInit();//尾插
void LTpushBack(LTNode* phead,DataType x);//打印
void LTprint(LTNode* phead);//头插void PushFront(LTNode* phead, DataType x);//尾删
void PopBack(LTNode* phead);
//头删
void PopFront(LTNode* phead);//查找
LTNode* LTFind(LTNode* phead, DataType x);//在指定位置之后插入数据
void LTInsert(LTNode* pos, DataType x);
//删除指定位置数据
void Erase(LTNode* pos);void LTDestory(LTNode* phead);

第二步: 进行链表的初始化, 首先一个双向带头循环链表需要有一个哨兵位, 也就是头结点, 并且需要循环 , 所以可以在test.c中创建一个指针指向这个头结点, 也可以使用函数返回一个指针指向这个函数. 如下:

LTNode* LTBuyNode(DataType 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;
}void LTInit(LTNode** pphead)
{*pphead = LTBuyNode(-1);
}//或者LTNode* LTInit()
{LTNode* phead = LTBuyNode(-1);return phead;
}

第三步: 实现链表的其他方法:
尾插:
这里不需要传递二级指针, 因为phead指向的位置只能是哨兵位, 不可以发生改变, 申请新的结点, 为了不影响原链表, 先改变新节点的前驱指针和后继指针, 在改变受影响的结点.让新节点next指向头结点, prev指向头结点的前驱结点, 在改变d3, 最后改变头结点.

如图:

在这里插入图片描述

void LTpushBack(LTNode* phead,DataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->next = phead;newnode->prev = phead->prev;phead->prev->next = newnode;//注意这样写不能改变与下一行交换位置phead->prev = newnode;
}

可以封装一个打印函数来显示我们的链表, 注意结束条件是遍历到phead前停止

void LTprint(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){printf("%d -> ", pcur->data);pcur = pcur->next;}printf("\n");
}

测试一下:

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"void test01()
{LTNode* plist = NULL;LTInit(&plist);LTpushBack(plist,100);LTprint(plist);}int main()
{	test01();return 0;
}

运行结果:

在这里插入图片描述

头插:
将新节点插入到头结点之前是尾插, 插入到头结点之后即尾插, 同样先改变新结点, 在改变受到影响的结点.

画图:
在这里插入图片描述

void PushFront(LTNode* phead, DataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;//这样写也不能和下一行交换位置phead->next = newnode;
}

尾删:
为了方便理解, 以及减少代码复杂性, 我们先将要删除的结点保存在del中, 然后将del的前一个结点指向del的下一个结点, 也即头结点, 并且改变头结点的前一个结点指向del的前一个结点

画图:
在这里插入图片描述

void PopBack(LTNode* phead)
{assert(phead && phead->next != phead);LTNode* del = phead->prev;del->prev->next = phead;phead->prev = del->prev;free(del);del = NULL;
}

头删:
头删就是删除头结点的下一个结点, 先将要删除的结点保存到del中, 改变del的下一个结点的前驱结点指向头结点, 并且改变头结点的下一个结点指向del的下一个结点.

画图:
在这里插入图片描述

void PopFront(LTNode* phead)
{assert(phead && phead->next != phead);LTNode* del = phead->next;phead->next = del->next;del->next->prev = phead;free(del);del = NULL;
}

查找:
下面将实现一个查找函数, 以便于我们找到指定位置, 用作后续的指定位置的插入和删除, 其实和打印函数的思想基本一致, 只是遍历的途中需要与x值进行对比, 找到了就返回该地址.

LTNode* LTFind(LTNode* phead, DataType x)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}

在指定位置的插入:
此时我们并不需要头结点了, 根据pos的指针域就可以找到要插入的位置, 并且申请新节点, 改变指针域就可以了,代码也非常简单, 逻辑也非常简单

画图:
在这里插入图片描述

void LTInsert(LTNode* pos, DataType x)
{assert(pos);LTNode* newnode = LTBuyNode(x);newnode->next = pos->next;newnode->prev = pos;pos->next->prev = newnode;pos->next = newnode;
}

删除指定位置:
和前面的删除思想基本一致, 改变受到影响的结点之后, 销毁此节点.

画图:
在这里插入图片描述

void Erase(LTNode* pos)
{assert(pos);pos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);pos = NULL;
}

最后一步: 销毁结点, 既然动态开辟了内存, 我们就需要手动释放, 因为我们这传递的是一级指针, 所以形参不会影响实参, 在测试代码中需要再次将phead的值置为NULL, 但是我们为了代码的一致性, 传递了一级指针, 遍历链表, 保存下一个结点, 以此销毁, 最后销毁头结点.

在这里插入图片描述

在这里插入图片描述

void LTDestory(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}free(phead);phead = NULL;
}

4. 完整代码

List.c

#define _CRT_SECURE_NO_WARNINGS 1#include"List.h"LTNode* LTBuyNode(DataType 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;
}void LTInit(LTNode** pphead)
{*pphead = LTBuyNode(-1);
}void LTpushBack(LTNode* phead,DataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->next = phead;newnode->prev = phead->prev;phead->prev->next = newnode;phead->prev = newnode;
}void LTprint(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){printf("%d -> ", pcur->data);pcur = pcur->next;}printf("\n");
}void PushFront(LTNode* phead, DataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;phead->next = newnode;
}void PopBack(LTNode* phead)
{assert(phead && phead->next != phead);LTNode* del = phead->prev;del->prev->next = phead;phead->prev = del->prev;free(del);del = NULL;
}void PopFront(LTNode* phead)
{assert(phead && phead->next != phead);LTNode* del = phead->next;phead->next = del->next;del->next->prev = phead;free(del);del = NULL;
}LTNode* LTFind(LTNode* phead, DataType x)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}
void LTInsert(LTNode* pos, DataType x)
{assert(pos);LTNode* newnode = LTBuyNode(x);newnode->next = pos->next;newnode->prev = pos;pos->next->prev = newnode;pos->next = newnode;
}void Erase(LTNode* pos)
{assert(pos);pos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);pos = NULL;
}void LTDestory(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}free(phead);phead = NULL;
}

List.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int DataType;typedef struct ListNode
{DataType data;struct ListNode* next;struct ListNode* prev;
}LTNode;//初始化
void LTInit(LTNode** pphead);//尾插
void LTpushBack(LTNode* phead,DataType x);//打印
void LTprint(LTNode* phead);//头插void PushFront(LTNode* phead, DataType x);//尾删
void PopBack(LTNode* phead);
//头删
void PopFront(LTNode* phead);//查找
LTNode* LTFind(LTNode* phead, DataType x);//在指定位置之后插入数据
void LTInsert(LTNode* pos, DataType x);
//删除指定位置数据
void Erase(LTNode* pos);void LTDestory(LTNode* phead);

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"void test01()
{LTNode* plist = NULL;LTInit(&plist);LTpushBack(plist,100);
//	PushFront(plist, 200);
//	PushFront(plist, 300);
//
//	LTNode* find = LTFind(plist, 100);
	PopBack(plist);
//	LTInsert(find, 888);
//
//	Erase(find);
//	find = NULL;//LTDestory(plist);//plist = NULL;//plist = NULL;LTprint(plist);}int main()
{	test01();return 0;
}

5. 总结

是不是发现双向链表的实现更加容易了呢, 这是因为我们不需要像单链表一样每次都遍历结点, 总的来说,双向链表是一种功能强大的数据结构,拥有插入、删除和反向遍历等额外功能。然而,它也增加了额外的复杂性和内存开销。在选择数据结构时,需要根据具体的场景和需求来权衡使用双向链表的优缺点。


完 , 感谢铁子们的支持 别忘了 关注+点赞 ~~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/4850.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java+playwright+testNG实现UI自动化测试

今天来讲讲使用Java结合最新的playwright来做UI自动化测试 目前网上大部分都是关于使用Python做自动化的教程&#xff0c;Java的比较少一些&#xff0c;但是我认为使用Java做自动化还是有优点的&#xff0c;性能就好一点&#xff0c;当然大家根据实际需求来。 一、 普通UI测试 …

【python】去除水印的几种方式

1、python调用FFMEPG的delogo函数去除水印 要使用Python调用FFmpeg的delogo filter去除视频水印&#xff0c;你需要使用subprocess模块运行FFmpeg命令。以下是一个简单的Python脚本示例&#xff1a; import subprocessdef remove_watermark(input_video, output_video, logo_…

MacPro(M1,M2芯片)Java开发和常用工具开源软件合集

目录 Java开发软件1 IDE1.1 idea1.2 Vs Code 2 开发工具2.1 数据库数据库模型管理数据库连接客户端 2.2 SSH/Telnet/Serial/Shell/Sftp客户端2.3 MarkDown编辑器2.3 代码片段管理粘贴 3小工具3.1 截图贴图3.2 Mac下修改hosts文件的图形化界面软件 Java开发软件 1 IDE 1.1 ide…

如果把软路由的网段更换成169.254.0.0/16会咋样?

前言 这几天有小伙伴在折腾软路由系统&#xff0c;然后问题就来了。 他咨询的是&#xff1a;为啥电脑连接软路由之后&#xff0c;无法访问软路由的管理页&#xff1f; 嗯。。。确实不是什么大事。但不注意看&#xff0c;还以为软路由没有正常获取到ip。 熟悉网络的小伙伴们都…

教程推荐:手机应用自动化

手机应用程序的自动化通常涉及使用专门设计的自动化框架和工具。对于Android和iOS平台&#xff0c;以下是一些常用的自动化工具&#xff1a; Android: Espresso: Espresso是谷歌官方支持的自动化测试框架。它适用于写UI测试来模拟用户对Android应用的交云。Espresso工作在应用…

Python中的map()和filter()函数:深入解析与使用场景

Python中的map()和filter()函数&#xff1a;深入解析与使用场景 在Python编程中&#xff0c;map()和filter()是两个非常实用的内置函数&#xff0c;它们可以帮助我们更高效地处理数据。这两个函数都是高阶函数&#xff0c;可以接受一个函数作为参数&#xff0c;并应用于序列&a…

实例解释:溢出和进位是咋回事?不能胡来吧!

有学生给我一段程序&#xff0c;就在运行中标志位的“怪异”表现提出问题。   程序不难懂&#xff1a; assume cs:codesg codesg segment start:mov al,0fchadd al,05h ;结果不溢出mov al,0f5hadd al,87h ;结果溢出mov ax,4c00hint 21h codesg ends end start难懂的是&a…

设计模式- 访问者模式(Visitor Pattern)结构|原理|优缺点|场景|示例

设计模式&#xff08;分类&#xff09; 设计模式&#xff08;六大原则&#xff09; 创建型&#xff08;5种&#xff09; 工厂方法 抽象工厂模式 单例模式 建造者模式 原型模式 结构型&#xff08;7种&#xff09; 适配器…

leetcode-有效括号序列-94

题目要求 思路 1.使用栈的先进后出的思路&#xff0c;存储前括号&#xff0c;如果st中有对应的后括号与之匹配就说明没问题 2.有两个特殊情况就是字符串第一个就是后括号&#xff0c;这个情况本身就是不匹配的&#xff0c;还有一种是前面的n个字符串本身是匹配的&#xff0c;这…

Python中format的常见用法

一、填充 1、按默认顺序填充 name "Alice" age 25 print("My name is {} and I am {} years old.".format(name, age))输出&#xff1a;My name is Alice and I am 25 years old. 2、指定位置 name "Bob" age 30 print("My name is…

与Apollo共创生态:我们携手远航

目录 小程一言会议记录 回望7年发展展望未来小程有感 小程一言 4月22日&#xff0c;百度Apollo在北京车展前夕举办了以“破晓•拥抱智变时刻”为主题的智能汽车产品发布会。我在观看后也是很是触动 作为在校大学生的我&#xff0c;从大一开始知道Apollo开始&#xff0c;Apollo…

2024.04.09 校招 实习 内推 面经

绿*泡*泡VX&#xff1a; neituijunsir 交流*裙 &#xff0c;内推/实习/校招汇总表格 1、校招&实习 | 佑驾创新Minieye 春招补录实习&#xff08;内推&#xff09; 校招&实习 | 佑驾创新Minieye 春招补录实习&#xff08;内推&#xff09; 2、校招 | 上海复旦微电子…

STM32中I2C通信的完整C语言代码范例

在嵌入式系统开发中&#xff0c;STM32芯片是一种广泛应用的微控制器&#xff0c;具有强大的性能和丰富的外设功能。其中&#xff0c;I2C&#xff08;Inter-Integrated Circuit&#xff09;是一种常用的串行通信协议&#xff0c;用于在微控制器之间或者微控制器与外设之间进行数…

高并发实现高效内存管理

高并发下传统方式的弊端 void *malloc(size_t size);在内存的动态存储区中分配一块长度为size字节的连续区域返回该区域的首地址. void *calloc(size_t nmemb, size_t size);与malloc相似&#xff0c;参数size为申请地址的单位元素长度&#xff0c;nmemb为元素个数&#xff0…

开发中git的常用操作

Git 是一款分布式版本控制系统&#xff0c;广泛应用于软件开发中&#xff0c;用于管理项目的版本和修改历史。在开发过程中&#xff0c;有许多常用的 Git 操作可以帮助团队协作、版本管理和代码管理。下面将详细讲解常用的 Git 操作&#xff0c;并通过举例说明它们的用法和作用…

软考高级 | 系统架构设计师笔记(一)

一. 系统规划 1.1 项目的提出与选择 该步骤生成” 产品/项目建议书”. 1.2 可行性研究与效益分析 包括经济可行性/技术可行性/法律可行性/执行可行性/方案选择 5 个部分. 该步骤生 成”可行性研究报告”. 1.3 方案的制订和改进 包括确定软件架构/确定关键性要素?/确定计算…

spring-boot控制bean的创建顺序

1、order注解&#xff08;不一定有效&#xff09; org.springframework.core.annotation.Order 2、dependsOn注解&#xff08;有效&#xff09; org.springframework.context.annotation.DependsOn 3、提前将bean注册为BeanDefinition 1、实现BeanDefinitionRegistryPostP…

商城数据库88张表结构(八)

DDL 29.商品规格表 CREATE TABLE wang_goods_specs (id int(11) NOT NULL AUTO_INCREMENT COMMENT 自增ID,shopId int(11) NOT NULL DEFAULT 0 COMMENT 店铺ID,goodsid int(11) NOT NULL DEFAULT 0 COMMENT 商品ID,productNo varchar(20) NOT NULL COMMENT 商品货号,specids v…

Python 自定义日志输出

Python 有着内置的日志输出模块&#xff1a;logging 使用也很方便&#xff0c;但我们今天不说这个&#xff0c;我们用文件读写模块&#xff0c;实现自己的日志输出模块&#xff1b;这样在项目中&#xff0c;可以存在更高的自由度及更高的扩展性&#xff1b; 先来看看日志输出…

TDengine高可用架构之TDengine+Keepalived

之前在《TDengine高可用探讨》提到过&#xff0c;TDengine通过多副本和多节点能够保证数据库集群的高可用。单对于应用端来说&#xff0c;如果使用原生连接方式&#xff08;taosc&#xff09;还好&#xff0c;当一个节点下线&#xff0c;应用不会受到影响&#xff1b;但如果使用…