C——双向链表

一.链表的概念及结构

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。什么意思呢?意思就是链表在物理结构上不一定是连续的,但在逻辑结构上一定是连续的。链表是由一个一个的节点连接而成的。

我们借助这个图来理解链表的物理结构上的不连续和逻辑结构上的连续。这上面的6个节点在内存空间的地址不是连续的,但是他们在逻辑上却是连续的,1->2->3->4->5->6。

与链表相似的还有顺序表,顺序表与链表相同都是线性表的一种。而顺序表的底层其实就是数组,所以顺序表在物理结构上是连续的,在逻辑结构上也是连续的。 

二.链表的分类

我们从上图可以得知,链表一共有2*2*2种。

 分别为:

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

而在这么多种的链表中,最常用的只有单向不带头不循环链表(也称单链表),以及双向带头循环链表(也称双向链表)。我们今天来了解这两种之一的双向链表。

三.双向链表的结构

双向链表全称为:双向带头循环链表。怎么理解这里面的每一个修饰词呢?我们先来看一下双向链表的结构。

四.实现双向链表 

我们在实现双向链表的时候可以将所有的链表所需的函数的声明都放到一个List.h中,将函数的定义放到一个List.c中,我们还需要一个test.c用来测试我们的双向链表中的方法。

4.1链表的元素——节点的创建

节点是链表的组成元素,而对于双向链表来说,每一个节点不仅要存储数据还要存储前一个节点的地址和后一个节点的地址,没有哪一种内置类型可以同时包含这三种,所以我们节点的创建要用到自定义类型——结构体。

struct ListNode
{int val;struct ListNode* prev;struct ListNode* next;
};

这样的结构体就可以表示一个节点了嘛?难道我们的节点只能存储整型嘛?当然不是,我们的节点可以存储任意数据,但是我们如果直接这样写的话,等到代码量大了,如果我们想要该链表存储字符型,我们到时候要修改的地方非常多。所以我们有一个一劳永逸的方法:

typedef int ListValType;

我们可以给int类型利用typedef关键字起一个新名字ListValType,我们结构体内部定义 int类型的成员时不再使用int a;而使用ListValType a;这两种的效果是一样的。以后我们想修改链表存储数据的类型的时候只需要将最前面的重命名语句中的int类型改为其他类型即可。

我们在创建节点的时候要写struct ListNode这么长一串,我们也可以利用typedef关键字给该结构体类型起一个新名字,避免了结构体名太长的问题。

所以我们节点的定义最终为:

typedef int ListValType;typedef struct ListNode
{ListValType val;struct ListNode* prev;struct ListNode* next;
}ListNode;

4.2双向链表的初始化

双向链表是带头链表,而这个头就是头节点(哨兵位)。所以双向链表的初始化其实就是创建一个头节点。头节点也是节点,所以双向链表的初始化其实就是创建一个节点,只不过这个节点没有有效的值。

//创建节点
ListNode* Buynode(ListValType x)
{ListNode* node = (ListNode*)malloc(sizeof(ListNode));if (node == NULL){perror("malloc");exit(-1);}node->val = x;node->next = NULL;node->prev = NULL;return node;
}//双向链表的初始化
ListNode* ListInit()
{//创建一个头节点(哨兵位)ListNode* phead = Buynode(-1);return phead;
}

上面的代码可以完成双向链表的初始化嘛?不行!

修改后的代码为: 

 我们来写一个测试函数,来判断我们的链表的初始化是否正确。

我们调试看到,头节点的next指针和prev指针都指向了他自己,并且val = -1,说明我们的初始化没有问题。

4.3尾插 

我们创建好了新节点后想要将该节点插入到链表的尾部,怎么插入呢?插入的时候我们要注意指针指向的改变。我们来画图分析尾插的过程。

第一步:先将新节点连接到链表中

第二步:改变链表中指针的指向 

我们发现,将newnode作为新节点插入到链表中后,原链表中有的指针的指向需要改变。我们继续来画图分析哪些改变了,要怎么修改?

通过上面两幅图的分析,我们已经了解了尾插的规则,现在我们来实现双向链表的尾插方法:

//尾插
void ListPushBack(ListNode* phead,ListValType x)
{assert(phead);//判断该双向链表是否有效ListNode* newnode = Buynode(x);//head head->prev newnodenewnode->prev = phead->prev;newnode->next = phead;phead->prev->next = newnode;phead->prev = newnode;
}

我们通过调试来判断一下我们的尾插是否正确。 观察上图,我们的尾插已经实现了。但是这样并不好观察,我们可以先实现双向链表的打印方法,这样就可以明显的看出尾插是否正确了。

4.4双向链表的打印

 双向链表的打印也就是遍历该链表就行了,我们只需要注意遍历时的起始位置和结束条件就行了。

//双向链表的打印
void ListPrint(ListNode* phead)
{assert(phead);ListNode* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->val);pcur = pcur->next;}printf("\n");
}

我们现在来利用打印方法来测试尾插方法: 我们看到,尾插和打印方法都没有问题。

4.5头插

头插往哪插呢?头节点的前面吗?头插插的地方是头节点后面的位置。

头插的分析与尾插的分析相同,我们先将newnode连接到链表中,在判断那些指针的指向需要改变。

第一步:先将newnode连接到链表中

第二步:改变链表中指针的指向 

头插代码为: 

//头插
void ListPushFront(ListNode* phead, ListValType x)
{assert(phead);ListNode* newnode = Buynode(x);//phead newnode phead->nextnewnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;phead->next = newnode;
}

我们测试一下头插代码: 

经过测试,我们看到头插方法没有问题。

4.6尾删 

尾删就是删除该链表中的最后一个节点,即head->prev。删除该节点后,链表中有的指针指向就要发生改变。

//尾删
void ListPopBack(ListNode* phead)
{assert(phead && phead->next != phead);//删除的链表必须是有效链表,即不能只包含头节点ListNode* del = phead->prev;//要删除的尾节点//phead del->prev deldel->prev->next = phead;phead->prev = del->prev;free(del);del = NULL;
}

我们利用测试代码进行测试: 我们删除了4次,所以最后一次删除链表已经为空链表了,而头节点是一个没有值的节点,所以打印出来就是空白。

 4.7头删

我们已经知道了尾删方法,头删方法的分析方式与尾删相似,我们依旧先找到要需要改变指向的指针。我们借助图来分析:

 

//头删
void ListPopFront(ListNode* phead)
{assert(phead && phead->next != phead);ListNode* del = phead->next;//phead del del->nextdel->prev = phead;phead->next = del->next;free(del);del = NULL;
}

写完一个方法之后依旧通过测试方法来判断方法是否正确: 

走到这里,我们头删的方法也是正确的。

4.8在指定位置之后插入数据 

在指定位置之后插入数据,我们首先要保证这个指定的位置是存在的,要不然找不到怎么在它的后面插入呢?所以在插入数据之前我们得先查找这个数据在链表中的位置。

4.8.1查找节点

查找节点我们只需要遍历我们的链表就行了。如果遍历途中找到了就返回该节点,如果遍历完了链表还没有找到该节点,那就说明该链表只能中没有该节点。

//查找节点
ListNode* Find(ListNode* phead, ListValType x)
{ListNode* pcur = phead->next;//遍历链表while (pcur != phead){if (pcur->val == x){return pcur;}pcur = pcur->next;}return NULL;
}

测试代码: 

4.8.2找到节点后插入数据 

//在指定位置之后插入数据
void ListInsert(ListNode* pos, ListValType x)
{assert(pos);ListNode* newnode = Buynode(x);//pos newnode pos->nextnewnode->next = pos->next;newnode->prev = pos;pos->next->prev = newnode;pos->next = newnode;
}

测试代码:

4.8.3在指定位置之后插入与尾插的区别  

4.9删除pos节点

删除pos节点也需要查找该节点是否在链表中,只有该节点在链表中我们才能对其删除。

//删除pos节点
void ListErase(ListNode* pos)
{assert(pos);//pos->prev pos pos->nextpos->prev->next = pos->next;pos->next->prev = pos->prev;free(pos);pos = NULL;
}

测试代码: 我们看到,我们调用完该方法后,我又手动将find置为了NULL,为什么要这样呢?在该方法内部不是已经置为NULL了嘛?

因为我们传的参数是一级指针,接收的形参也是一级指针,我们虽然已经将该空间释放掉了也将形参置为了空,但是这种传递方式是值传递,形参的改变不会影响实参,所以我们出了函数之后,最好将find也手动置为空,要不然会有野指针的风险。

4.10销毁链表

我们创建的链表是由一个一个的节点连接起来的,而节点是我们利用动态内存管理申请的空间,我们用完了之后就得还给操作系统,所以我们在使用完链表之后,也要将链表销毁。

//链表的销毁
void ListDestory(ListNode* phead)
{assert(phead);ListNode* pcur = phead->next;ListNode* next = pcur->next;while (pcur != phead){free(pcur);pcur = next;next = pcur->next;}//到这里,所有的有效节点已经删除了,现在只需要删除头节点free(phead);phead = NULL;
}

到这里,我们双向链表的全部功能就已经实现了。

五.完整代码

5.1双链表头文件

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>typedef int ListValType;typedef struct ListNode
{ListValType val;struct ListNode* prev;struct ListNode* next;
}ListNode;//双向链表的初始化
ListNode* ListInit();//双向链表的打印
void ListPrint(ListNode* phead);//尾插
void ListPushBack(ListNode* phead,ListValType x);//头插
void ListPushFront(ListNode* phead, ListValType x);//尾删
void ListPopBack(ListNode* phead);//头删
void ListPopFront(ListNode* phead);//查找节点
ListNode* Find(ListNode* phead , ListValType x);//在指定位置之后插入数据
void ListInsert(ListNode* pos, ListValType x);//删除pos节点
void ListErase(ListNode* pos);//链表的销毁
void ListDestory(ListNode* phead);

5.2双链表源文件

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"//创建节点
ListNode* Buynode(ListValType x)
{ListNode* node = (ListNode*)malloc(sizeof(ListNode));if (node == NULL){perror("malloc");exit(-1);}node->val = x;node->next = node;node->prev = node;return node;
}//双向链表的初始化
ListNode* ListInit()
{//创建一个头节点(哨兵位)ListNode* phead = Buynode(-1);return phead;
}//双向链表的打印
void ListPrint(ListNode* phead)
{assert(phead);ListNode* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->val);pcur = pcur->next;}printf("\n");
}//尾插
void ListPushBack(ListNode* phead,ListValType x)
{assert(phead);//判断该双向链表是否有效ListNode* newnode = Buynode(x);//phead head->prev newnodenewnode->prev = phead->prev;newnode->next = phead;phead->prev->next = newnode;phead->prev = newnode;
}//头插
void ListPushFront(ListNode* phead, ListValType x)
{assert(phead);ListNode* newnode = Buynode(x);//phead newnode phead->nextnewnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;phead->next = newnode;
}//尾删
void ListPopBack(ListNode* phead)
{assert(phead && phead->next != phead);//删除的链表必须是有效链表,即不能只包含头节点ListNode* del = phead->prev;//要删除的尾节点//phead del->prev deldel->prev->next = phead;phead->prev = del->prev;free(del);del = NULL;
}//头删
void ListPopFront(ListNode* phead)
{assert(phead && phead->next != phead);ListNode* del = phead->next;//phead del del->nextdel->prev = phead;phead->next = del->next;free(del);del = NULL;
}//查找节点
ListNode* Find(ListNode* phead, ListValType x)
{ListNode* pcur = phead->next;//遍历链表while (pcur != phead){if (pcur->val == x){return pcur;}pcur = pcur->next;}return NULL;
}//在指定位置之后插入数据
void ListInsert(ListNode* pos, ListValType x)
{assert(pos);ListNode* newnode = Buynode(x);//pos newnode pos->nextnewnode->next = pos->next;newnode->prev = pos;pos->next->prev = newnode;pos->next = newnode;
}//删除pos节点
void ListErase(ListNode* pos)
{assert(pos);//pos->prev pos pos->nextpos->prev->next = pos->next;pos->next->prev = pos->prev;free(pos);pos = NULL;
}//链表的销毁
void ListDestory(ListNode* phead)
{assert(phead);ListNode* pcur = phead->next;ListNode* next = pcur->next;while (pcur != phead){free(pcur);pcur = next;next = pcur->next;}//到这里,所有的有效节点已经删除了,现在只需要删除头节点free(phead);phead = NULL;
}

5.3测试源文件

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"void test01()
{ListNode* phead = ListInit();//测试尾插ListPushBack(phead,1);ListPrint(phead);ListPushBack(phead,2);ListPrint(phead);ListPushBack(phead,3);ListPrint(phead);ListPushBack(phead,4);ListPrint(phead);
}void test02()
{ListNode* phead = ListInit();//测试头插ListPushFront(phead, 5);ListPrint(phead);ListPushFront(phead, 6);ListPrint(phead);ListPushFront(phead, 7);ListPrint(phead);
}void test03()
{ListNode* phead = ListInit();//测试尾插ListPushBack(phead, 1);ListPushBack(phead, 2);ListPushBack(phead, 3);ListPushBack(phead, 4);ListPrint(phead);//链表的销毁ListDestory(phead);phead = NULL;ListPrint(phead);//ListNode* find = Find(phead, 1);测试删除pos节点//ListErase(find);//删除1节点//find = NULL;//ListPrint(phead);测试查找方法//ListNode * find = Find(phead, 1);if (find == NULL){printf("找不到!");}else{printf("找到了!");}//ListInsert(find,99);//在第一个节点之后插入99//ListPrint(phead);测试头删//ListPopFront(phead);//ListPrint(phead);//ListPopFront(phead);//ListPrint(phead);//ListPopFront(phead);//ListPrint(phead);//ListPopFront(phead);//ListPrint(phead);测试尾删//ListPopBack(phead);//ListPrint(phead);//ListPopBack(phead);//ListPrint(phead);//ListPopBack(phead);//ListPrint(phead);//ListPopBack(phead);//ListPrint(phead);}
int main()
{//test01();//test02();test03();return 0;
}

完!

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

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

相关文章

24五一杯ABC题完整思路+可执行代码+可视化图表(1)

2024五一赛A题保姆级建模思路20页1-3问可执行代码后续成品论文各类可视化图表&#xff08;完整版资料在文章最后&#xff09; 处理第一问的代码 其余各题的代码如下&#xff1a; A题保姆级建模思路20页&#xff1a;&#xff08;手把手教你如何建模&#xff09; 高清图片如下&am…

第12章 软件测试基础(第一部分)概念、质量保证、测试用例、测试执行过程

一、软件测试 &#xff08;一&#xff09;定义 动态验证计算机程序对有限的测试用例集是否可产生期望的结果的过程。测试计划是描述了要进行的测试活动的范围、方法、资源和进度的文档。编写测试计划目的&#xff1a;使测试工作顺利进行、使项目参与人员沟通更舒畅、使测试工…

面试常见 | 项目上没有亮点,如何包装?

很多技术人在公司用的老技术&#xff0c;而且很多都是搬业务代码且做枯燥乏味的CRUD&#xff0c;在面试提交简历或做自我介绍的时候并不突出&#xff0c;这种情况&#xff0c;如何破局&#xff1f; 首先不管你做的啥项目&#xff0c;全世界不可能只有你自己在做&#xff0c;比…

Tensorflow2.0笔记 - ResNet实践

本笔记记录使用ResNet18网络结构&#xff0c;进行CIFAR100数据集的训练和验证。由于参数较多&#xff0c;训练时间会比较长&#xff0c;因此只跑了10个epoch&#xff0c;准确率还没有提升上去。 import os import time import tensorflow as tf from tensorflow import keras …

附录6-5 黑马优购项目-我的与后端本地化

目录 1 我的 2 后端本地化 1 我的 tarbar我的 只有这两个页面 其中未登录页面中只有一键登录有用&#xff0c;其他都是写死的&#xff0c;一键登录的功能仅仅是切换到登录的页面 目前微信小程序和微信用户的信息是脱钩的&#xff08;之前的wx.getUserProfile与wx.getUs…

企业气候风险披露、报表词频、文本分析数据集合(2007-2022年)

01、数据介绍 企业气候风险披露是指企业通过一定的方式&#xff0c;将气候变化对其影响、自身采取的应对措施等信息披露出来。这有助于投资者更准确地评估企业价值&#xff0c;发现投资机会&#xff0c;规避投资风险。解企业在气候风险方面的关注度和披露情况。 可以帮助利益…

Django后台项目开发实战七

为后台管理系统换风格 第七阶段 安装皮肤包 pip install django-grappelli 在 setting.py 注册 INSTALLED_APPS [grappelli,django.contrib.admin,django.contrib.auth,django.contrib.contenttypes,django.contrib.sessions,django.contrib.messages,django.contrib.stat…

【yolov8】yolov8剪枝训练流程

yolov8剪枝训练流程 流程&#xff1a; 约束剪枝微调 一、正常训练 yolo train model./weights/yolov8s.pt datayolo_bvn.yaml epochs100 ampFalse projectprun nametrain二、约束训练 2.1 修改YOLOv8代码&#xff1a; ultralytics/yolo/engine/trainer.py 添加内容&#…

R语言4版本安装mvstats(纯新手)

首先下载mvstats.R文件 下载mvstats.R文件点此链接&#xff1a;https://download.csdn.net/download/m0_62110645/89251535 第一种方法 找到mvstats.R的文件安装位置&#xff08;R语言的工作路径&#xff09; getwd() 将mvstats.R保存到工作路径 在R中输入命令 source(&qu…

ctf web-部分

** web基础知识 ** *一.反序列化 在PHP中&#xff0c;反序列化通常是指将序列化后的字节转换回原始的PHP对象或数据结构的过程。PHP中的序列化和反序列化通过serialize()和unserialize()函数实现。 1.序列化serialize() 序列化说通俗点就是把一个对象变成可以传输的字符串…

创新指南|如何通过用户研究打造更好的人工智能产品

每个人都对人工智能感到兴奋&#xff0c;但对错过机会 (FOMO) 的恐惧正在驱使公司将人工智能嵌入到每个产品功能中。这可能会导致以技术为中心的方法&#xff0c;从而掩盖产品开发的基本目标&#xff1a;创建真正解决用户问题并满足他们需求的解决方案。本文将介绍通过用户研究…

HawkEye—高效、细粒度的大页管理算法

文章目录 HawkEye—高效、细粒度的大页管理算法1.作者简介2.文章简介与摘要3.简介(1).当时的SOTA系统概述LinuxFreeBSDIngensHawkEye 4.动机(1).地址翻译开销与内存膨胀(2).缺页中断延迟与缺页中断次数(3).多处理器大页面分配(4).如何测算地址翻译开销&#xff1f; 5.设计与实现…

大长案例 - 通用的三方接口调用方案设计

文章目录 引言身份验证防止重复提交数据完整性和加密回调地址安全事件响应可用性 设计方案概述1. API密钥生成2. 接口鉴权3. 回调地址设置4. 接口API设计 权限划分权限划分概述1. 应用ID&#xff08;AppID&#xff09;2. 应用公钥&#xff08;AppKey&#xff09;【&#xff08;…

安装VMware Tools报错处理(SP1)

一、添加共享文件 因为没有VMware Tools&#xff0c;所以补丁只能通过共享文件夹进行传输了。直接在虚拟机的浏览器下载的话&#xff0c;自带的IE浏览器太老了&#xff0c;网站打不开&#xff0c;共享文件夹会方便一点&#xff0c;大家也可以用自己的方法&#xff0c;能顺利上…

【Go语言快速上手(六)】管道, 网络编程,反射,用法讲解

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:Go语言专栏⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习更多Go语言知识   &#x1f51d;&#x1f51d; GO快速上手 1. 前言2. 初识管道3. 管…

清新优雅、功能强大的后台管理模板 | 开源日报 No.238

soybeanjs/soybean-admin Stars: 7.0k License: MIT soybean-admin 是一个基于 Vue3、Vite5、TypeScript、Pinia、NaiveUI 和 UnoCSS 的清新优雅且功能强大的后台管理模板。 使用最新流行的技术栈&#xff0c;如 Vue3、Vite5 和 TypeScript。采用清晰的项目架构&#xff0c;易…

Mac M2 本地下载 Xinference

想要在Mac M2 上部署一个本地的模型。看到了Xinference 这个工具 一、Xorbits Inference 是什么 Xorbits Inference&#xff08;Xinference&#xff09;是一个性能强大且功能全面的分布式推理框架。可用于大语言模型&#xff08;LLM&#xff09;&#xff0c;语音识别模型&…

Kubernetes 弃用Docker后 Kubelet切换到Containerd

containerd 是一个高级容器运行时&#xff0c;又名 容器管理器。简单来说&#xff0c;它是一个守护进程&#xff0c;在单个主机上管理完整的容器生命周期&#xff1a;创建、启动、停止容器、拉取和存储镜像、配置挂载、网络等。 containerd 旨在轻松嵌入到更大的系统中。Docke…

screen服务使用解析

一、为什么要使用screen服务 当我们在进行一些常见的远程操作时&#xff0c;通常首先会先进行远程ssh登录 或者telnet连接到远程服务器上&#xff0c;然后执行相关操作&#xff0c;或程序启动等。 1、程序所需的执行时间过长&#xff0c;可能需要挂载几天的那种&#xff0c;可…

Linux(ubuntu)—— 用户管理user 用户组group

一、用户 1.1、查看所有用户 cat /etc/passwd 1.2、新增用户 useradd 命令&#xff0c;我这里用的是2.4的命令。 然后&#xff0c;需要设置密码 passwd student 只有root用户才能用passwd命令设置其他用户的密码&#xff0c;普通用户只能够设置自己的密码 二、组 2.1查看…