数据结构·单链表

        不可否认的是,前几节我们讲解的顺序表存在一下几点问题:

        1. 中间、头部的插入和删除,需要移动一整串数据,时间复杂度O(N)

        2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗

        3. 增容一般是2倍的增长,这势必会造成空间的浪费

        那如何解决这些问题呢,此时,链表出现了

1. 链表的概念和结构

        我们之前说过,线性表的特点就是逻辑上是连续的,物理上不一定连续。顺序表是逻辑上是连续的,物理上也是连续的。而今天的链表就是逻辑上是连续的,但是物理上是不连续的

        最简单的链表是由节点们串在一起组成的,每个节点包含了两个内容:

                1. 要存入的有效数据

                2. 下一个节点的地址

        可以看出,每个节点在物理上都是独立的,不连续的。但是每个节点在逻辑上又有关联,每个节点都知道下一个节点的指针,要找到下一个节点就访问那个指针就好了

        具体来讲就是把 plist 当成一个钥匙,最开始保存的是第一个节点的地址,用完第一个节点的数据之后,把第一个节点中存储的地址再给到plist,这样plist就可以开第二个节点了,以此类推···

        下面我们依照上面的图片做一个简单的节点结构

                                

        到这里,链表的地基就学会了,下面我们尝试实现一下

2. 单链表(Single linked list)的实现

        跟顺序表一样,先是把准备工作,三个文件准备好

                                

        再把链表节点写出来

                                 ​​​​​​

        在这里我要重点声明一下,接下来如果从主函数前去访问链表时用的都是plist参数,从主函数中给子函数传参也是plist,就像这样

        ​​​​​​​        ​​​​​​​                

        然后就正式进入链表实现啦,大家伙坐稳喽

2.1 链表的打印

        打印的逻辑就是先拿到第一个节点的地址 pcur,把这个地址访问到 data 打印出来,然后将pcur的内容变成下一个要打印的节点的地址,直到pcur的内容是NULL为止

        ​​​​​​​        ​​​​​​​        

2.2 链表的插入

        因为插入就一定需要申请一个新的节点,所以我们先把这个功能封装好

        向堆区申请一块空间用来存放节点,记录这个节点的地址

        当然,如果你想把newnode的类型改成 SLTNode 也可以,不过后面要用到节点地址的时候就要取地址一下,很麻烦,所以我们干脆直接返回节点的地址

2.2.1 尾插

        在链表的尾端插入一个数据。

        因为如果链表为空(没有节点)的时候要修改 plist 的内容,让它指向我们新添加的第一个节点,所以我们传参的时候要传 &plist ,因此函数参数要用二级指针来接收这个可能会被修改的plist

        如果链表不为空,就去找尾节点,把为节点的next成员内容从NULL变成我们新添加的节点地址,可以这么理解:

        这个图里有一点不恰当,就是这个 pphead 要解引用一次 (*pphead) 才能找到第一个节点的地址

        ​​​​​​​

        接下来我们运行一下看看效果

2.2.2 头插

        头插比尾插好理解一点,直接上思路图(画的太丑了QAQ)

        

        很明显,链表是否为空对于需要的操作是没有影响的,上代码:

        

        最后运行一下看结果:

        

        因为每次都是把节点插到最前面,所以反着打出来是对的

2.3 链表的删除

2.3.1 尾删

        尾删的逻辑就是找到最后一个节点 ptail 和倒数第二个节点 prev ,把倒数第二个节点的next成员置为空指针,释放掉最后一个节点。当然,如果链表为空,也就是说没有节点的话就不能执行删除操作,用assert断言报错

        ​​​​​​​        ​​​​​​​        

        上代码:

        

2.3.2 头删

        头删也是需要两个指针控制,要注意的就是要先释放掉*pphead也就是第一个节点,然后再把*pphead的内容改成第二个节点的地址,接上第二个节点

        ​​​​​​​              ​​​​​​​

        代码如下:

        ​​​​​​​        

2.4 查找

        链表的查找很简单,就是遍历链表,找到了就返回节点地址,没找到就返回空指针

        

2.5 在任意位置插入数据

2.5.1 在指定位置前插入数据

        可以用SLTFind找到要被前插的节点的地址pos,在这个节点前面插入节点,还需要直到它前面那个节点的地址prev

        ​​​​​​​        ​​​​​​​        

        在实现这个功能的时候我们要注意,当pos是头节点的情况:

        

        下面使用一下

        

2.5.2 在指定位置后插入数据

        这个比较简单,但是要注意给地址的顺序,要先把后面那个节点的地址给到新节点,再把指定位置pos节点的地址成员改成新节点的地址,否则就会导致后面那个节点地址的丢失,没办法接到新节点后面了

        还有就是我们不需要知道链表的头节点是什么了,只需要关注pos就行了

        ​​​​​​​                ​​​​​​​

        

2.6 在任意位置删除节点

2.6.1 删除pos节点

        删除pos节点要先知道它前面的那个节点prev,然后把prev跟pos后面那个节点先连起来,最后再把pos释放掉。还有要注意的一点就是当pos就是链表头节点的时候要特殊处理一下

        ​​​​​​​        ​​​​​​​          

        

2.6.2 删除pos后面的一个节点

        这个功能也是只需要关注pos后面的内容就行,所以只需要传pos一个参数。还要注意一点就是pos不能是链表中的最后一个节点,否则它后面没有节点了还删什么

        ​​​​​​​        

        ​​​​​​​        

2.7 链表的销毁

        两个变量,pcur记录当前要准备销毁的节点地址,next记录下一个节点地址,防止销毁上一个节点之后找不到下一个节点了。然后两个变量一直循环向后扫描销毁,直到pcur指向NULL

        ​​​​​​​                ​​​​​​​

                        

3. 链表的分类

        链表按带头或不带头,单向或双向,循环或不循环,排列组合有8种

        我们刚刚学的单链表全称就是:不带头单向不循环链表

        带头不带头是说链表有没有一个不存储有效数据的节点,放在第一个存放有效数据节点之前

        ​​​​​​​        ​​​​​​​        

        单向双向是说链表能通过后一项找到前一项就是双向的,如果只能根据前一项找到后一项链表就是单项的。或者说双向链表的节点中的两个存放地址的成员中,一个存下一个节点的地址,一个存上一个节点的地址。

        ​​​​​​​        ​​​​​​​        

        循环不循环是说最后一个节点指向第一个节点就是循环链表,要是最后一个节点指向NULL就是不循环链表

                        

        虽然链表的种类很多,但是常用的只有两种:

                1.单链表(不带头单向不循环链表)

        单链表结构简单,一般不会单独用来存贮数据,它一般作为其他数据结构的子结构出现

                2.双向链表(带头双向循环链表)

        双向链表结构最复杂,一般用来单独存储数据。它虽然复杂,但是之后实现它的实际就会发现它有很多优势,致使实现它反而变得简单了,后面会有实现它的章节的。

4. 本节代码

        SList.h

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>//链表是由节点构成的
typedef int SLTDataType;typedef struct SListNode 
{SLTDataType data;struct SListNode* next;
}SLTNode;//链表的打印
void SLTPrint(SLTNode* phead); //链表的插入
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);//链表的头删和尾删
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);//查找
SLTNode* SLTFind(SLTNode** pphead, SLTDataType x);//在任意位置插入数据
//在指定位置前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);//在任意位置删除节点
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos后面的一个节点
void SLTEraseAfter(SLTNode* pos);//销毁链表
void SLTDestory(SLTNode** pphead);

        SList.c

#include"SList.h"//链表的打印	
void SLTPrint(SLTNode* phead)
{SLTNode* pcur = phead;while (pcur){printf("%d -> ", pcur->data);pcur = pcur->next;}printf("NULL\n");
}//申请一个新节点
SLTNode* SLTBuyNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));newnode->data = x;newnode->next = NULL;return newnode;
}//链表的插入
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);//申请一个新节点SLTNode* newnode = SLTBuyNode(x);//链表为空,新节点作为头if (*pphead == NULL){*pphead = newnode;return;}//链表不为空,找尾节点SLTNode* ptail = *pphead;while (ptail->next){ptail = ptail->next;}//找到尾节点了ptail->next = newnode;
}//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);//申请一个新节点SLTNode* newnode = SLTBuyNode(x);//链表为不为空,操作都一样//斩断第一个连接,再把新节点接进去newnode->next = *pphead;*pphead = newnode;
}//链表的头删和尾删
//尾删
void SLTPopBack(SLTNode** pphead)
{assert(pphead);//链表不能为空assert(*pphead);//链表不为空//链表只有一个节点if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;return;}//链表有多个节点SLTNode* ptail = *pphead;SLTNode* prev = NULL;//找到尾节点while (ptail->next){prev = ptail;ptail = ptail->next;}prev->next = NULL;free(ptail);ptail = NULL;}//头删
void SLTPopFront(SLTNode** pphead)
{assert(pphead);//链表不能为空assert(*pphead);//让第二个节点变成新的头节点//释放旧的头节点SLTNode* sec = (*pphead)->next;free(*pphead);*pphead = sec;
}//查找
SLTNode* SLTFind(SLTNode** pphead, SLTDataType x)
{assert(pphead);//遍历链表SLTNode* pcur = *pphead;while (pcur){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}//在任意位置插入数据
//在指定位置前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(pos);//这里断言*pphead不能为空//因为指定节点的地址pos不能为空,所以这个链表也不能为空assert(*pphead);//当pos是头节点,执行头插if (pos == *pphead){SLTPushFront(pphead, x);return;}//当pos不是头节点//申请一个新节点SLTNode* newnode = SLTBuyNode(x);SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = newnode;newnode->next = pos;
}//在指定位置后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);//申请一个新节点SLTNode* newnode = SLTBuyNode(x);newnode->next = pos->next;pos->next = newnode;
}//在任意位置删除节点
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(pos);assert(*pphead);//如果pos指向头节点,执行头删if (*pphead == pos){SLTPopFront(pphead);return;}SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;
}//删除pos后面的一个节点
void SLTEraseAfter(SLTNode* pos)
{assert(pos);//pos->next不能为空//就是说pos不能是最后一个节点assert(pos->next);SLTNode* del = pos->next;pos->next = pos->next->next;free(del);del = NULL;
}//销毁链表
void SLTDestory(SLTNode** pphead)
{assert(pphead);assert(*pphead);SLTNode* pcur = *pphead;SLTNode* next = NULL;while (pcur){next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}

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

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

相关文章

Linux——理解文件系统

目录 1、inode 2、硬链接、软链接 理解硬链接 软链接 3、静态库、动态库 静态库与动态库 生成静态库 生成动态库 使用动态库 运行动态库 使用外部库 库文件名称和引入库的名称 1、inode 使用ls -l命令不仅显示出了文件名&#xff0c;也可以显示出文件元数据 一行…

Spring 的 IOC 和 AOP

题目 Spring 的 IOC 和 AOP 推荐解析 IOC 是什么&#xff1f; IoC&#xff08;Inversion of Control&#xff09; 控制反转&#xff0c;是一种常见的设计思想&#xff0c;主要就是将手动创建对象的控制权&#xff0c;交给 Spring 框架来管理。 为什么需要存在一个容器&…

MVC模式

Model-View-Controller : 模型-视图-控制器模式&#xff0c;用于应用程序的分层开发。 Model(模型)&#xff1a;代表一个存取数据的对象。也可以带有逻辑&#xff0c;在数据变化时更新控制器。 View(视图)&#xff1a;代表模型包含的数据的可视化。 Controller(控制器)&#xf…

Nuxt3:/_nuxt/xxx.mjs请求502问题引发的_nuxt实际指向探究

一、问题描述 今天后台的小伙伴给我发了一张图&#xff1a; 根据图中提示&#xff0c;应该是在请求某个/_nuxt/xxx.mjs的时候报错了&#xff0c;如果单独在浏览器访问该文件路径也是能访问到的&#xff0c;那有可能就是访问量比较大&#xff0c;服务器响应不过来导致的&#x…

数组A[m+n]中存放了两个线性表(a1,a2,.....am)和(b1,b2.....bn),将数组中的两个线性表的位置互换,要求空间复杂度为1

要求空间复杂度为O(1)&#xff0c;那么不可以借助辅助数组来完成此操作 算法思路&#xff1a;可先将此数组逆置变成bn,......b1,am,....,a1&#xff0c;然后分别逆转两个线性表的数据元素 算法实现 1、定义一个函数&#xff0c;该函数的功能是可以对一个数组的任意连续的部分进…

JS之打地鼠案例

需要素材的同学可以私信我 效果图&#xff1a; 上代码&#xff1a; <!DOCTYPE html> <html> <head><meta charset"utf-8"><title></title><style>* {margin: 0;padding: 0;}.box {position: relative;width: 320px;heigh…

DAY08_SpringBoot—整合Mybatis-Plus

目录 1 MybatisPlus1.1 MP介绍1.2 MP的特点1.3 MybatisPlus入门案例1.3.1 导入jar包1.3.2 编辑POJO对象1.3.3 编辑Mapper接口1.3.4 编译YML配置文件1.3.5 编辑测试案例 1.4 MP核心原理1.4.1 需求1.4.2 原理说明1.4.3 对象转化Sql原理 1.5 MP常规操作1.5.1 添加日志打印1.5.2 测…

搭建Android开发环境—— 熟悉Android开发工具,掌握Android移动端开发环境的搭建、项目导入,并能够将项目部署到模拟器和真机进行测试。

搭建Android开发环境 一、实验目的 熟悉Android开发工具&#xff0c;掌握Android移动端开发环境的搭建、项目导入&#xff0c;并能够将项目部署到模拟器和真机进行测试。 二、实验设备及器件 1、JDK1.8安装包 2、Android Studio安装包 三、实验内容 完成JDK和Android Stud…

WPF多值转换器

背景&#xff1a;实现Slider拖动可以调整rgb 单转换器&#xff1a;WPF中数据绑定转换器Converter-CSDN博客 在View中&#xff1a; <StackPanel Orientation"Vertical"><Slider x:Name"slider_R" Minimum"0" Maximum"255" Wi…

MySQL视图特性

视图是一个虚拟表&#xff0c;其内容由查询定义。同真实的表一样&#xff0c;视图包含一系列带有名称的列和行数据。视图的数据变化会影响到基表&#xff0c;基表的数据变化也会影响到视图。 1.基本使用 创建视图 create view 视图名 as select 语句&#xff1b; 案例&…

微信双开bat脚本中出现中文目录导致报错运行不了

在微信双开bat脚本的教程中看到一个评论&#xff1a; 可以是可以&#xff0c;写完脚本另存为的时候改变编码就行了&#xff0c;选ANSI

Socket实现服务器和客户端

Socket 编程是一种用于在网络上进行通信的编程方法&#xff0c;以下代码可以实现在不同主机之间传输数据。 Socket 编程中服务器端和客户端的基本步骤&#xff1a;服务器端步骤&#xff1a; 1.创建 Socket&#xff1a; int serverSocket socket(AF_INET, SOCK_STREAM, 0);…