数据结构--单链表
前言
上一章,我们讲了数据结构--动态顺序表,我们会发现有以下问题:
1.当我们要头部或者插入或删除时,都需要进行位置挪动,腾出某一个位置,时间复杂度为0(N);
2.增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3.增容会有一定的浪费空间;例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
下面我们来看看单链表这种线性结构;
链表
概念与结构
链表是一种常见的数据结构,用于存储和组织数据。它由一系列节点组成,每个节点包含两部分:数据和指向下一个节点的指针。
链表中的节点在内存中可以分布在任意位置,不像数组那样需要连续的存储空间。每个节点都包含了存储的数据以及指向下一个节点的指针。通过这种方式,链表可以灵活地分配和管理内存空间。就像一节节连动的火车车厢;

在数据结构中,呈现:
逻辑图中,呈现:

在逻辑图中,链式结构是连续性的,但实际上不一样连续;从数据结构中看出,链表是通过地址来联系在一起的,不需要地址的连续性;在我们要解决链表相关问题时,只需要画出逻辑图即可;
注意:
结点的空间在堆区中开辟;堆区中申请出的空间,会按照一定的策略进行分配,两次申请的空间可能连续,可能不连续;
链表的分类
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向或者双向
2. 带头或者不带头

3. 循环或者非循环

可以通过一定的组合达成不同种类的链表;
这里我们比较常用的是有两种结构:
在这里,我们将先实现无头单向非循环链表,这是链表中结构最为简单的;简称单链表。
单链表的接口实现
先写一下它的结构:
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>typedef int SLTDataType;
typedef struct SLTNode
{SLTDataType data;struct SListNode* next;}SLTNode;
结构体中放入一个数据存储类型和一个结构体指针;结构体指针存放下一个结点的地址;
单链表打印
void SLTrint(SLTNode* phead)
{SLTNode* cur = phead;while (cur){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}
将链表从头到尾遍历一遍,用一个cur指针来进行移动,在while循环中不断遍历打印出结果;打印完就进入下一个结点;
增加链表结点
SLTNode* BuySListNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("mallco fail");exit(-1);}newnode->data = x;newnode->next = NULL;return newnode;
}
用动态内存分配进行扩容,同时对data和next进行初始化;最后返回结点;
尾插
void SLPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = BuySListNode(x);if (* pphead == NULL){* pphead = newnode;}else{SLTNode* tail = * pphead;while (tail->next != NULL){tail = tail->next;}tail->next = newnode;}}
这里要注意,我们的形参用到了二级指针,因为当结构体指针为空时,我们就需要对结构体指针进行改变,用二级指针接收结构体指针的地址,能够有效的访问,否则将会报错;当结构体指针不为空时,就利用结构体指针通过循环访问到尾结点,然后在尾结点进行连接;
验证:
void Test3()
{SLTNode* plist = NULL;SLPushBack(&plist, 1);SLPushBack(&plist, 2);SLPushBack(&plist, 3);SLPushBack(&plist, 4);SLTrint(plist);}
int main()
{Test3();return 0;
}

尾删
void SLPopBack(SLTNode** pphead)
{assert(pphead);//判空assert(*pphead);//一个节点if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}//其他else{SLTNode* tailPrev = NULL;SLTNode* tail = *pphead;while (tail->next){tailPrev = tail;tail = tail->next;}free(tail);tailPrev->next = NULL;}
}
当删除的是第一个结点,将会改变结构体指针的地址,所以形参要引用二级指针;其他情况就先找到尾结点,然后进行删除;
验证:
void Test3()
{SLTNode* plist = NULL;SLPushBack(&plist, 1);SLPushBack(&plist, 2);SLPushBack(&plist, 3);SLPushBack(&plist, 4);SLTrint(plist);SLPopBack(&plist);SLTrint(plist);}
int main()
{Test3();return 0;
}

头插头删
void SLPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = BuySListNode(x);newnode->next = *pphead;*pphead = newnode;}void SLPopFront(SLTNode** pphead)
{ assert(pphead);//判空assert(*pphead);//其他SLTNode* newhead = (*pphead)->next;free(*pphead);*pphead = newhead;
}
头插相对尾插来说比较容易,因为有头指针,所以不用遍历循环来找到尾结点;并且无论头节点是否为空,操作程序都保持一致;
头删只要找到头结点的下一个结点,那么就可以删除了;
验证:
void Test2()
{SLTNode* plist = NULL;SLPushBack(&plist, 1);SLPushBack(&plist, 2);SLPushBack(&plist, 3);SLPushBack(&plist, 4);SLPushBack(&plist, 5);SLTrint(plist);SLPushFront(&plist, 6);SLPushFront(&plist, 7);SLPushFront(&plist, 8);SLPushFront(&plist, 9);SLTrint(plist);SLPopFront(&plist);SLTrint(plist);}int main()
{Test2();return 0;
}

查找与插入
SLTNode* SLFind(SLTNode* phead, SLTDataType x)
{//判空assert(phead);SLTNode* cur = phead;while (cur){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = BuySListNode(x);newnode->next = pos->next;pos->next = newnode;}
查找:在循环里面通过结点的data与x进行匹配,找到就返回该结点,找不到返回空;如果有多个结点的data与x一致,返回链表最接近头指针的;
插入:是在pos后面进行插入,这样插入比较方便,不用考虑头指针是否为空的问题;
验证:
void Test3()
{SLTNode* plist = NULL;SLPushBack(&plist, 1);SLPushBack(&plist, 2);SLPushBack(&plist, 3);SLPushBack(&plist, 4);SLTrint(plist);SLTNode* pos = SLFind(plist, 3);SLTInsertAfter(pos, 88);SLTrint(plist);}
int main()
{Test3();return 0;
}

删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pos);if (pos == *pphead){SLPopFront(pphead);}else{SLTNode* perv = *pphead;while (perv->next != pos){perv = perv->next;}perv->next = pos->next;free(pos);}
}void SLTEraseAfter(SLTNode* pos)
{assert(pos);//检查尾节点assert(pos->next);SLTNode* posNext = pos->next;pos->next = posNext->next;free(posNext);}
第一种删除是删除pos结点,但需要判断该结点是否为首结点;而且需要遍历找到pos结点的前一个结点;比较麻烦;
第二种删除是删除pos结点后一个结点,只需要通过pos结点连接到下下一个结点即可,最后free掉pos的下一个结点;
验证:
void Test3()
{SLTNode* plist = NULL;SLPushBack(&plist, 1);SLPushBack(&plist, 2);SLPushBack(&plist, 3);SLPushBack(&plist, 4);SLTrint(plist);SLTNode* pos = SLFind(plist, 3);SLTInsertAfter(pos, 88);SLTrint(plist);SLTErase(&plist, pos);SLTrint(plist);}
int main()
{Test3();return 0;
}

void Test3()
{SLTNode* plist = NULL;SLPushBack(&plist, 1);SLPushBack(&plist, 2);SLPushBack(&plist, 3);SLPushBack(&plist, 4);SLTrint(plist);SLTNode* pos = SLFind(plist, 3);SLTInsertAfter(pos, 88);SLTrint(plist);SLTEraseAfter(pos);SLTrint(plist);}
int main()
{Test3();return 0;
}

摧毁
void SLTDestroy(SLTNode** pphead)
{assert(pphead);SLTNode* cur = *pphead;while (cur){SLTNode* prev = cur;cur = cur->next;free(prev);}*pphead = NULL;
}
通过记住头结点的下一个结点,free掉头节点,然后头节点的下一个结点成为新的头节点;
验证:
void Test3()
{SLTNode* plist = NULL;SLPushBack(&plist, 1);SLPushBack(&plist, 2);SLPushBack(&plist, 3);SLPushBack(&plist, 4);SLTrint(plist);SLTNode* pos = SLFind(plist, 3);SLTInsertAfter(pos, 88);SLTrint(plist);SLTDestroy(&plist);SLTrint(plist);
}
int main()
{Test3();return 0;
}

完整代码
slist.h
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>typedef int SLTDataType;
typedef struct SLTNode
{SLTDataType data;struct SListNode* next;}SLTNode;void SLTrint(SLTNode* phead);
SLTNode* BuySListNode(SLTDataType x);
void SLPushBack(SLTNode** pphead, SLTDataType x);
void SLPushFront(SLTNode** pphead, SLTDataType x);
void SLPopBack(SLTNode** pphead);
void SLPopFront(SLTNode** pphead);
SLTNode* SLFind(SLTNode* phead, SLTDataType x);
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
void SLTErase(SLTNode** pphead, SLTNode* pos);
void SLTEraseAfter(SLTNode* pos);
void SLTDestroy(SLTNode** phead);
slist.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Slist.h"void SLTrint(SLTNode* phead)
{SLTNode* cur = phead;while (cur){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}SLTNode* BuySListNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("mallco fail");exit(-1);}newnode->data = x;newnode->next = NULL;return newnode;
}void SLPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = BuySListNode(x);if (* pphead == NULL){* pphead = newnode;}else{SLTNode* tail = * pphead;while (tail->next != NULL){tail = tail->next;}tail->next = newnode;}}void SLPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = BuySListNode(x);newnode->next = *pphead;*pphead = newnode;}void SLPopBack(SLTNode** pphead)
{assert(pphead);//判空assert(*pphead);//一个节点if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}//其他else{SLTNode* tailPrev = NULL;SLTNode* tail = *pphead;while (tail->next){tailPrev = tail;tail = tail->next;}free(tail);tailPrev->next = NULL;}
}
void SLPopFront(SLTNode** pphead)
{ assert(pphead);//判空assert(*pphead);//其他SLTNode* newhead = (*pphead)->next;free(*pphead);*pphead = newhead;
}SLTNode* SLFind(SLTNode* phead, SLTDataType x)
{//判空assert(phead);SLTNode* cur = phead;while (cur){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = BuySListNode(x);newnode->next = pos->next;pos->next = newnode;}void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pos);if (pos == *pphead){SLPopFront(pphead);}else{SLTNode* perv = *pphead;while (perv->next != pos){perv = perv->next;}perv->next = pos->next;free(pos);}
}void SLTEraseAfter(SLTNode* pos)
{assert(pos);//检查尾节点assert(pos->next);SLTNode* posNext = pos->next;pos->next = posNext->next;free(posNext);}void SLTDestroy(SLTNode** pphead)
{assert(pphead);SLTNode* cur = *pphead;while (cur){SLTNode* prev = cur;cur = cur->next;free(prev);}*pphead = NULL;
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Slist.h"void Test1()
{int n;SLTNode* plist = NULL;printf("请输入链表长度");scanf("%d", &n);printf("请输入值");for (int i = 0; i < n; i++){int val;scanf("%d", &val);SLTNode* newnode = BuySListNode(val);newnode->next = plist;plist = newnode;}SLTrint(plist);
}void Test2()
{SLTNode* plist = NULL;SLPushBack(&plist, 1);SLPushBack(&plist, 2);SLPushBack(&plist, 3);SLPushBack(&plist, 4);SLPushBack(&plist, 5);SLTrint(plist);SLPushFront(&plist, 6);SLPushFront(&plist, 7);SLPushFront(&plist, 8);SLPushFront(&plist, 9);SLTrint(plist);SLPopFront(&plist);SLTrint(plist);}void Test3()
{SLTNode* plist = NULL;SLPushBack(&plist, 1);SLPushBack(&plist, 2);SLPushBack(&plist, 3);SLPushBack(&plist, 4);SLTrint(plist);SLTNode* pos = SLFind(plist, 3);SLTInsertAfter(pos, 88);SLTrint(plist);SLTDestroy(&plist);SLTrint(plist);
}
int main()
{Test3();return 0;
}
相关文章:
数据结构--单链表
前言 上一章,我们讲了数据结构--动态顺序表,我们会发现有以下问题: 1.当我们要头部或者插入或删除时,都需要进行位置挪动,腾出某一个位置,时间复杂度为0(N); 2.增容需要申请新空间,…...
过程:从虚拟机上添加 git 并成功提交到 GitLab 的全过程
Ⅰ、准备工作: 1、Git 查看: 其一、命令:git --version // 此时就能在虚拟机环境下看到 git 的版本为: git version 2.41.0 其二、如何在虚拟机上安装 git : A、命令 : sudo apt-get install git B、然后再输入虚…...
机器学习笔记之优化算法(九)收敛速度的简单认识
机器学习笔记之优化算法——收敛速度的简单认识 引言收敛速度的判别标准 Q \mathcal Q Q-收敛速度 R \mathcal R R-收敛速度关于算法复杂度与收敛速度 引言 本节对收敛速度简单介绍。 收敛速度的判别标准 我们之前几节介绍了线搜索方法 ( Line Search Method ) (\text{Line …...
FPGA学习——Altera IP核调用之PLL篇
文章目录 一、IP核1.1 IP核简介1.2 FPGA中IP核的分类1.3 IP核的缺陷 二、PLL简介2.1 什么是PLL2.2 PLL结构图2.3 C4开发板上PLL的位置 三、IP核调用步骤四、编写测试代码五、总结 一、IP核 1.1 IP核简介 IP核(知识产权核),是在集成电路的可…...
经纬度坐标工具
LngLatUtil :用于计算里程数 import cn.hutool.core.util.ArrayUtil; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import lombok.Getter; import lombok.Setter;import java.io.FileInputStream; import java.io.Serializable; import java.t…...
如何使用伪元素::before和::after?
伪元素(::before和::after)是CSS中非常有用的特性,它们允许你在元素的内容之前或之后插入额外的内容,并且不需要在HTML结构中添加额外的标记。这样可以方便地在页面上添加装饰性元素、图标、或者样式效果。以下是使用伪元素的基本方法: 1、创…...
Visual Studio Code中对打开的脚本格式统一
什么是Language Server Protocol (LSP)? Language Server Protocol(语言服务器协议,简称LSP)是微软在2016年提出的一套统一的通讯协议方案。LSP定义了一套编辑器或者IDE与语言服务器(Language Server)之间使用的协议&…...
补充JDK源码-IDEA集成工具
在阅读JDK8源码的时候发现,只有一小部分常用包是存在源码及其注释的,而很多内部包是没有源码,class文件在阅读的时候对阅读者十分不友好。在网上搜集了很多资料都没有解决问题。 解决问题办法:参考文档。本文主要是根据这篇文章记…...
Git Submodule 更新子库失败 fatal: Unable to fetch in submodule path
编辑本地目录 .git/config 文件 在 [submodule “Assets/CommonModule”] 项下 加入 fetch refs/heads/:refs/remotes/origin/...
Springboot切面打印日志
切面打印完整日志,以下代码用于扫描RestController 注解修饰的接口,并打印相关日志 import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; impor…...
ubuntu上回环设备/dev/loop0占用100%清理
查看磁盘占用情况时: df -h/dev/loopn这些设备在Linux下被称为回环设备。 终端输入: sudo apt autoremove --purge snapd再次查看:...
List list=new ArrayList()抛出的ArrayIndexOutOfBoundsException异常
1.应用场景,今天生产日志监控到一下ArrayList 进行add 异常,具体日志如下: eptionHandler.handler(178): TXXYBUSSINESS|执行异常 java.util.concurrent.CompletionException: java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bo…...
桶排序算法
桶排序算法 算法思想概述:桶排序的主要步骤如下: 算法goland实现:图解演示: 算法思想概述: 桶排序(Bucket Sort)是一种非比较性的排序算法,它将待排序的元素分到有限数量的桶&#…...
P8604 [蓝桥杯 2013 国 C] 危险系数
题目背景 抗日战争时期,冀中平原的地道战曾发挥重要作用。 题目描述 地道的多个站点间有通道连接,形成了庞大的网络。但也有隐患,当敌人发现了某个站点后,其它站点间可能因此会失去联系。 我们来定义一个危险系数 DF(x,y)&…...
Excel·VBA表格横向、纵向相互转换
如图:对图中区域 A1:M6 横向表格,转换成区域 A1:C20 纵向表格,即 B:M 列转换成每2列一组按行写入,并删除空行。同理,反向操作就是纵向表格转换成横向表格 目录 横向转纵向实现方法1转换结果 实现方法2转换结果 纵向转横…...
Leetcode-每日一题【剑指 Offer 06. 从尾到头打印链表】
题目 输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。 示例 1: 输入:head [1,3,2]输出:[2,3,1] 限制: 0 < 链表长度 < 10000 解题思路 1.题目要求我们从尾到头反过…...
LeetCode--HOT100题(22)
目录 题目描述:160. 相交链表(简单)题目接口解题思路代码 PS: 题目描述:160. 相交链表(简单) 给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表…...
产品体系架构202308版
1.前言 当我们不断向前奔跑时,需要回头压实走过的路。不断扩张的同时把相应的内容沉淀下来,为后续的发展铺垫基石。 不知从何时起,产品的架构就面向了微服务/中台化/前后端分离/低代码化/分布式/智能化/运行可观测化的综合体,让…...
Linux systemctl 简单介绍与使用
在Linux下,systemctl是一个管理系统服务的命令。它提供了对systemd服务的控制和管理。 在系统中使用systemctl命令,您可以执行以下操作: 启动服务:systemctl start servicename停止服务:systemctl stop servicename重…...
恺英网络宣布:与华为鸿蒙系统展开合作,将开发多款手游
8月5日消息,恺英网络宣布旗下子公司盛和网络参加了华为开发者大会(HDC.Together)游戏服务论坛,并在华为鸿蒙生态游戏先锋合作启动仪式上进行了亮相。恺英网络表示,将逐步在HarmonyOS上开发多款游戏,利用Har…...
【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型
摘要 拍照搜题系统采用“三层管道(多模态 OCR → 语义检索 → 答案渲染)、两级检索(倒排 BM25 向量 HNSW)并以大语言模型兜底”的整体框架: 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后,分别用…...
【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...
ES6从入门到精通:前言
ES6简介 ES6(ECMAScript 2015)是JavaScript语言的重大更新,引入了许多新特性,包括语法糖、新数据类型、模块化支持等,显著提升了开发效率和代码可维护性。 核心知识点概览 变量声明 let 和 const 取代 var…...
从零实现富文本编辑器#5-编辑器选区模型的状态结构表达
先前我们总结了浏览器选区模型的交互策略,并且实现了基本的选区操作,还调研了自绘选区的实现。那么相对的,我们还需要设计编辑器的选区表达,也可以称为模型选区。编辑器中应用变更时的操作范围,就是以模型选区为基准来…...
基于当前项目通过npm包形式暴露公共组件
1.package.sjon文件配置 其中xh-flowable就是暴露出去的npm包名 2.创建tpyes文件夹,并新增内容 3.创建package文件夹...
C# 类和继承(抽象类)
抽象类 抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。 不能创建抽象类的实例。抽象类使用abstract修饰符声明。 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带 实现的成员的任意组合。抽象类自己可以派生自另一个抽象类。例…...
Python 训练营打卡 Day 47
注意力热力图可视化 在day 46代码的基础上,对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...
Spring Security 认证流程——补充
一、认证流程概述 Spring Security 的认证流程基于 过滤器链(Filter Chain),核心组件包括 UsernamePasswordAuthenticationFilter、AuthenticationManager、UserDetailsService 等。整个流程可分为以下步骤: 用户提交登录请求拦…...
Xela矩阵三轴触觉传感器的工作原理解析与应用场景
Xela矩阵三轴触觉传感器通过先进技术模拟人类触觉感知,帮助设备实现精确的力测量与位移监测。其核心功能基于磁性三维力测量与空间位移测量,能够捕捉多维触觉信息。该传感器的设计不仅提升了触觉感知的精度,还为机器人、医疗设备和制造业的智…...
MySQL的pymysql操作
本章是MySQL的最后一章,MySQL到此完结,下一站Hadoop!!! 这章很简单,完整代码在最后,详细讲解之前python课程里面也有,感兴趣的可以往前找一下 一、查询操作 我们需要打开pycharm …...





