数据结构之链表篇

8a6bc8c8433f4ff4aa919cec1573965d.png

今天我们讲我们数据结构的另一个重要的线性结-----链表,

什么是链表

链表是一种在 物理存储上不连续,但是在逻辑结构上通过指针链接下一个节点的形成一个连续的结构。

他和我们的火车相似,我们的元素是可以类比成车厢,需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。

这就挺现了一个好处:就是我们在插入删除的时候不用将其他的元素进行移动,我们只需要将指针的指向进行改变就行。所以在我们考虑使用顺序表还链表的时候,就可以考虑我们是否需要频繁的插入和删除我们的元素。

链表的在机内的存储

我们的链表在机内存储是不连续的,是分散的,我们要想找到我们的下一个节点就需要一个指针指向我们的下一个节点,这样来考虑的话我们的每个节点就需要一个数据域和一个指针域,用一个结构体。

为什么还需要指针变量来保存下⼀个节点的位置? 链表中每个节点都是独⽴申请的(即需要插⼊数据时才去申请⼀块节点的空间),我们需要通过指针 变量来保存下⼀个节点位置才能从当前节点找到下⼀个节点。 

108dfbe5a2db49b3973bab3d0d4177d2.png

 那我们的节点的结构体就可以写出来了:

struct SListNode
{int data; //节点数据 struct SListNode* next; //指针变量⽤保存下⼀个节点的地址 
};

 链表的特点

由于链表在存储空间是上不是连续的,我们在来插入删除的时候就不需要去移动,使我们的时间复杂度降低。

而且我们的链表是用一个节点申请一个节点,不会出现节点空间不够和空间浪费的现象。

 但是这不方便我们经常访问元素,因为我们要从头节点一个一个的去遍历,每次的访问都需要我们从头开始这就导致我们的访问不方便,是顺序存取。

用链表来实现接口

我们的来拿表有很多种分类,有这几种特性结合在一起:带不带头(就是我们的哨兵位),循不循环,是双向的还是单向的;

f073b0a743c84179b02772299c5df403.png

我们这里使用的是不带头的单链表。

我们用链表来实现一些接口:增删查改等。

我们先来完成我们的链表的插入:头插和尾插;

我们有一个功能就是我们需要去频繁地申请空间,这样我们可以去包装成一个函数:

创建新的节点

创建新的节点其实就是使用我们动态申请空间函数的应用了,malloc和calloc。

在我们使用完这个函数之后,我们还需要去判断一下是否申请成功。如果空间申请不成功我们可以报个错。

代码:

//申请新的节点
SLTNode* SLBuyNode(SLTDataType x)
{SLTNode* newNode = (SLTNode*)malloc (sizeof(SLTNode));if (newNode==NULL){perror("malloc:");exit(1);}newNode->data = x;newNode->next = NULL;return newNode;
}

头插:

我们的头插是在我们我们的第一个节点的前面插入,如果我们有哨兵位的话就是在 哨兵位的后面进行插入,我们这里实现的是不带哨兵位的。

那我们这里就有2个问题:就是当我们的链表为空时,我们应该怎么办,和我们的形参参应该是一级指针还是二级指针?

其实当我们的链表为空时,和我们不为空时是一样的处理方法,先创建一个新的节点,使这个新节点的下一个节点指向我们的刚开始的头节点,如果为空,新节点的next指针就指向空

而对于我们的传参,我们应该知道的是,我们在主函数中创建的是一个ListNode*的结构体指针变量phead,我们如果传一级指针的话,那我们的形参就是我们实参的一个拷贝,我们形参的改变并不会改变影响到实参,但是我们在进行头插的时候我们是需要改变我们的实参phead的指向的,所以我们就需要传我们的地址,而我们的一级指针的地址,需要用二级指针来接收。

所以我们这两个问题解决了就可以两我们的代码写出来了。

原来的链表:

插入之后

代码:

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newNode = NULL;newNode = SLBuyNode(x);newNode->next = *pphead;*pphead= newNode;
}

尾插:

我们的尾插就是在我们的链表中的尾部插入,而我们的尾插比头插麻烦的是,我们寻找我们的链表的尾部,因为我们的链表在机内的存储是分开的,我们并不知道我们的尾节点在哪,只能从头开始遍历,找到我们的尾节点,而我们的尾节点是next指针为空的节点,我们通过一个while循环即可,

但是这里又有一个不同的是,当我们的链表为空时,和我们不为空时的是不同的,为空时我们需要将我们的phead指针指向我们的新节点,而不为空时就需要我们去遍历找我们的尾节点,这也告诉我们这里需要传地址,用二级指针接收,

原来的链表:

插入之后 

代码:

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newNode = SLBuyNode(x);SLTNode* p = *pphead;//空链表if (*pphead==NULL){*pphead = newNode;}else {//找尾节点while (p->next!=NULL){p = p->next;}p->next = newNode;}}

再来实现我们的删除操作:头删和尾删

头删:

我们的头删和我们的头插刚刚相反,是在我们的头部删除。

我们的删除不是简简单单的的改变phead指针的位置,我们需要量将我们节点空间释放,否则会导致内存泄露,因为你向我们的我们的栈区申请的,如果我们的申请了而不将我们的空间还给我们的系统,我们的空间是有限的,当我们很多申请的空间都没有归还,那我们的可能会出现一系列严重的问题。

所以我们在改变我们头指针的位置是还需要将我们删除的那个节点空间释放。

当我们的链表为空时,我们不能进行删除操作,需要断言一下,

代码:

//头删
void SLTPopFront(SLTNode** pphead)
{assert(pphead);assert(*pphead);if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else {SLTNode* p = (*pphead)->next;;free(*pphead);*pphead = p;}
}

尾删:

尾删和我们的尾插相反,是删除我们的尾部节点,他和我们的尾插一样,需要去寻找我们的尾部节点。

当我们链表为空时我们不可以去删除,我们在这里需要断言一下。

代码

//尾删
void SLTPopBack(SLTNode** pphead)
{assert(pphead);assert(*pphead);//空链表SLTNode* p = *pphead;if ((*pphead)->next == NULL)//只有一个元素{free(*pphead);*pphead = NULL;}else {//找到尾部while (p->next->next != NULL){p = p->next;}free(p->next);p->next = NULL;}}

查找元素

我们的查找元素很简单,我们只需要去遍历我们的链表,去和我们要找的的元素进行比较,如果相等我们就返回这个节点的地址,如果遍历完整个链表还没有找到这个与之相等的节点,就说明链表中没有该节点,此时我们返回空(NULL),在这之前我们的进行判断我们的链表为不为空。

代码:

//查找(按照元素查找)
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* p = phead;if (phead==NULL){printf("该链表中没有元素\n");exit(0);}else {//找元素while (p != NULL && p->data != x){p = p->next;}if (p == NULL){return NULL;}else {return p;}}}

打印链表中的元素

我们打印链表中的元素也是将我们的链表遍历,叫我们的节点中的数据域的值给打印出来,当然我们在遍历之前也要判断一下链表尾部为空。

代码

//打印元素
void SLTPrint(SLTNode* phead)
{SLTNode* p = phead;while (p){printf("%d->", p->data);p = p->next;}printf("NULL\n");
}

在特点的节点插入和删除:

我们在指定的节点删除和插入,我们需要先在我们的链表中寻找一下有没有该节点,找到该节点后,如果我们是插入,我们只需要创建一个新的节点,我们先要将我们的新节点的next指针改成我们找到的节点的next,再去改变我们该节点的next指针,改成我们插入节点的指针,

对于删除,我们需要找到我们目标节点的前一个节点,因为我们的链表是单向的,当我们找到我们目标节点,但是我们却找不到他的前一个节点,所以我们是要找到目标节点的前一个节点,我们需要一个新的指针来帮我们记住我们需要删除节点,再来将我们目标节点的前一个节点的next改变成我们的目标节点的下一个节点,在将我们的目标节点的空间释放。

代码:

//查找(按照元素查找)
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* p = phead;if (phead==NULL){printf("该链表中没有元素\n");exit(0);}else {//找元素while (p != NULL && p->data != x){p = p->next;}if (p == NULL){return NULL;}else {return p;}}}

总代码:

我们需要将我们的文件管理好:我们的头文件和函数的声明就用一个头文件ListNode.h存放,我们实现的接口就用一个ListNode.c文件来存放。

ListNode.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include"Contact.h"
typedef struct person SLTDataType;
typedef struct SListNode
{//SLTDataType data;//放数据struct SListNode* next; //指向下一个节点
}SLTNode;//打印
void SLTPrint(SLTNode* phead);
SLTNode* SLBuyNode(SLTDataType x);
//头部插入删除/尾部插入删除
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pphead);

ListNode.c 

#define _CRT_SECURE_NO_WARNINGS 1
#include"SLQ.h"//打印元素
void SLTPrint(SLTNode* phead)
{SLTNode* p = phead;while (p){printf("%d->", p->data);p = p->next;}printf("NULL\n");
}//申请新的节点
SLTNode* SLBuyNode(SLTDataType x)
{SLTNode* newNode = (SLTNode*)malloc (sizeof(SLTNode));if (newNode==NULL){perror("malloc:");exit(1);}newNode->data = x;newNode->next = NULL;return newNode;
}//尾部插入删除
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newNode = SLBuyNode(x);SLTNode* p = *pphead;//空链表if (*pphead==NULL){*pphead = newNode;}else {//找尾节点while (p->next!=NULL){p = p->next;}p->next = newNode;}}
//销毁
void SListDesTroy(SLTNode** pphead)
{SLTNode* L = *pphead;while (*pphead){L = (*pphead)->next;free(*pphead);*pphead = L;}
}
//头部插入删除 
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newNode = NULL;newNode = SLBuyNode(x);newNode->next = *pphead;*pphead= newNode;
}
//尾删
void SLTPopBack(SLTNode** pphead)
{assert(pphead);assert(*pphead);//空链表SLTNode* p = *pphead;if ((*pphead)->next == NULL)//只有一个元素{free(*pphead);*pphead = NULL;}else {//找到尾部while (p->next->next != NULL){p = p->next;}free(p->next);p->next = NULL;}}
//头删
void SLTPopFront(SLTNode** pphead)
{assert(pphead);assert(*pphead);if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else {SLTNode* p = (*pphead)->next;;free(*pphead);*pphead = p;}
}
//查找(按照元素查找)
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* p = phead;if (phead==NULL){printf("该链表中没有元素\n");exit(0);}else {//找元素while (p != NULL && p->data != x){p = p->next;}if (p == NULL){return NULL;}else {return p;}}}
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(*pphead);//空链表的时候assert(pos);SLTNode* newNode = SLBuyNode(x);SLTNode* p = *pphead;if (pos == p)//说明在是头插也可以使用我们的头插函数(我这里未使用,自己写){newNode->next = *pphead;*pphead = newNode;}else {while (p->next != pos){p = p->next;}newNode->next = p->next;p->next = newNode;}}
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(pos && *pphead);SLTNode* p = *pphead;if (*pphead == pos)//头删{*pphead = (*pphead)->next;free(pos);}else {while (p->next != pos){p = p->next;}p->next =pos->next;free(pos);pos = NULL;}
}
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode*newNode=SLBuyNode(x);//增加新节点newNode->next = pos->next;pos->next = newNode;}
//删除pos之后的节点
void SLTEraseAfter(SLTNode * pos)
{assert(pos);if (pos->next == NULL){;}else {SLTNode* p = pos->next;pos->next = p->next;free(p);p = NULL;}
}

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

ubuntu22.04服务器docker-compose方式部署ldap服务

一&#xff1a;系统版本 二&#xff1a;部署环境 节点名称 IP 部署组件及版本 配置文件路径 机器CPU 机器内存 机器存储 Ldap 10.10.10.111 self-service-password:latest phpldapadmin:latest openldap:latest openldap:/data/openldap/config phpldapadmin&#x…

回炉重造java----双列集合(HashMap,TreeMap)

体系结构 ①基本操作: ②遍历方式: 第一种: 键找值&#xff0c;通过map.keySet()获取Map的键集合&#xff0c;通过键去匹配Map中的值 Set<String> strings map.keySet();for (String string : strings) {System.out.println(map.get(string));} 第二种: 键值对&…

【多模态】30、GPT4V_OCR | GPT4V 在 OCR 数据集上效果测评

文章目录 一、背景二、测评2.1 场景文本识别2.2 首先文本识别2.3 手写数学公式识别2.4 图表结构识别&#xff08;不考虑单元格中的文本内容&#xff09;2.5 从内容丰富的文档中抽取信息 三、讨论 论文&#xff1a;EXPLORING OCR CAPABILITIES OF GPT-4V(ISION) : A QUANTITATIV…

Android动态布局framelayout

功能说明 最近碰到一个需求&#xff0c;要求在网页端拖控件&#xff0c;动态配置app控件的模块&#xff0c;大小和位置&#xff0c;显示不同的功能&#xff0c;然后在app大屏展示。 技术难点&#xff1a; 1.动态控件位置和大小难调&#xff0c;会出现布局混乱&#xff0c;位置错…

2024-05-10 C语言使用开源的JPEG解码库libjpeg 读取JPEG文件并将其解码为RGB24格式的数据

一、可以使用开源的JPEG解码库&#xff0c;例如libjpeg库&#xff0c;来读取JPEG文件并将其解码为RGB24格式的数据。 二、在ubuntu上面进行测试。 2.1安装了libjpeg-dev包 sudo apt-get install libjpeg-dev 2.2 测试c源码 #include <stdio.h> #include <stdlib.h&…

虚拟化技术 分离虚拟机数据流量与ESXi的流量管理

一、实验内容 为ESXi主机添加网卡通过vClient查看已添加的网卡信息为ESXi添加网络&#xff0c;创建标准交换机修改网络配置&#xff0c;实现虚拟机数据流量与ESXi的管理流量分离 二、实验主要仪器设备及材料 安装有64位Windows操作系统的台式电脑或笔记本电脑&#xff0c;建…

Java入门基础学习笔记15——强制类型转换

大范围类型的变量是否可以赋值给小范围类型的变量呢&#xff1f; IDEA直接报错。直接报错&#xff0c;是提醒你有问题。但是我非常进行类型转换。 非要强行赋值呢&#xff1f; 强制类型转换&#xff0c;强行将类型范围大的变量&#xff0c;数据赋值给类型范围小的变量。 数据…

实用的Chrome命令 帮你打开Chrome浏览器的隐藏功能

前言 Chrome作为主力浏览器&#xff0c;支持相当丰富的第三方扩展&#xff0c;其实浏览器本身也内置了大量实用的命令。许多实用的功能并没有直接显示在Chrome的菜单上。在这篇文章中&#xff0c;我们将介绍几个实用的chrome:// commands。 通过下面整理的 Chrome 命令&#x…

霍金《时间简史 A Brief History of Time》书后索引(A--D)

图源&#xff1a;Wikipedia INDEX A Abacus Absolute position Absolute time Absolute zero Acceleration Age of the universe Air resistance Albrecht, Andreas Alpha Centauri Alpher, Ralph Anthropic principle Antigravity Antiparticles Aristotle Arrows of time …

权限束缚术--权限提升你需要知道这些

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文主要对渗透测试中权限提升的一些基础知识进行整理 并不包含权限提升的具体操作 适合要入门权限提升的朋友 提权的重要性 我们在渗透网站时&#xff0c;我们往往会拿到一些权限&#xff0c;但是我们的权限有…

全栈开发之路——前端篇(9)插槽、常用api和全局api

全栈开发一条龙——前端篇 第一篇&#xff1a;框架确定、ide设置与项目创建 第二篇&#xff1a;介绍项目文件意义、组件结构与导入以及setup的引入。 第三篇&#xff1a;setup语法&#xff0c;设置响应式数据。 第四篇&#xff1a;数据绑定、计算属性和watch监视 第五篇 : 组件…

【Java代码审计】代码审计的方法及常用工具

【Java代码审计】代码审计的方法及常用工具 代码审计的常用思路代码审计辅助工具代码编辑器测试工具反编译工具Java 代码静态扫描工具 代码审计的常用思路 1、接口排查&#xff08;“正向追踪”&#xff09;&#xff1a;先找出从外部接口接收的参数&#xff0c;并跟踪其传递过…

鸿蒙OpenHarmony开发板解析:【系统能力配置规则】

如何按需配置部件的系统能力 SysCap&#xff08;SystemCapability&#xff0c;系统能力&#xff09;是部件向开发者提供的接口的集合。 开发前请熟悉鸿蒙开发指导文档&#xff1a;gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md点击或者复制转到。 部件配置系统…

与队列和栈相关的【OJ题】

✨✨✨专栏&#xff1a;数据结构 &#x1f9d1;‍&#x1f393;个人主页&#xff1a;SWsunlight 目录 一、用队列实现栈&#xff1a; 1、2个队列的关联起来怎么由先进先出转变为先进后出&#xff1a;&#xff08;核心&#xff09; 2、认识各个函数干嘛用的&#xff1a; …

android进阶-Binder

参考&#xff1a;Android——Binder机制-CSDN博客 机制&#xff1a;Binder是一种进程间通信的机制 驱动&#xff1a;Binder是一个虚拟物理设备驱动 应用层&#xff1a;Binder是一个能发起进程间通信的JAVA类 Binder相对于传统的Socket方式&#xff0c;更加高效Binder数据拷贝…

C++ VScode: launch: program ...... dose not exist

VScode: launch: program … dose not exist 介绍 参考VS Code 配置 C/C 编程运行环境&#xff08;保姆级教程&#xff09;教程配置了VSCode。在配置launch.json适用多个.c 文件编译时&#xff0c;弹出下面错误。 原因和解决方法 是task.json 默认配置的问题。 默认的 cwd参…

LearnOpenGL(十一)之光源

一、投光物 将光投射(Cast)到物体的光源叫做投光物(Light Caster)。 二、平行光 当一个光源处于很远的地方时&#xff0c;来自光源的每条光线就会近似于互相平行&#xff0c;我们可以称这些光为平行光。当我们使用一个假设光源处于无限远处的模型时&#xff0c;它就被称为定向…

Acrobat Pro DC 2023 for Mac:PDF处理的终极解决方案

Acrobat Pro DC 2023 for Mac为Mac用户提供了PDF处理的终极解决方案。它具备强大的文档处理能力&#xff0c;无论是查看、编辑还是创建PDF文件&#xff0c;都能轻松胜任。在编辑功能方面&#xff0c;Acrobat Pro DC 2023支持对文本、图像进行精准的修改和调整&#xff0c;还能添…

【Web后端】Tomcat简介_安装_解决乱码_idea配置

1.1 简介 tomcat是在oracle公司的ISWDK(lavaServer Web DelevopmentKit)的基础上发展起来的一个优秀的开源的servlet容器tomcat使用java语言编写。运行稳定、可靠、效率高&#xff0c;可以和目前 主流web服务器一起工作(如IIS、Apache、 Nginx)tomcat是Apache软件基金会(Apach…

MySQL数据库的安装和部署

1.数据库的相关介绍 关系型数据库管理系统&#xff1a;&#xff08;英文简称&#xff1a;RDBMS&#xff09; 为我们提供了一种存储数据的特定格式&#xff0c;所谓的数据格式就是表&#xff0c; 在数据库中一张表就称为是一种关系. 在关系型数据库中表由两部分组成&#xf…