数据结构───链表
花费一个周时间学完了链表(的一部分),简单总结一下。
链表的学习离不开画图,将其抽象成一种逻辑模型,可以减少思考时间,方便理解。
链表大致分为8种结构,自己学习并实现了两种结构,也是两种最经典的结构。一种是单向不带头非循环链表,另一种是双向带头循环链表。
无头单向非循环链表
无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
以下就是该链表的实现:
1.链表的创建
定义一个结构体,包含存储的数据和指向后继节点的指针。
typedef int MyType;//单向不带头非循环链表
typedef struct SingleLinklist{MyType data;struct SingleLinklist* next;
}SLNode;
2.链表功能实现
由于是不带头链表,增删改功能需要修改链表的内容,所以需要传头节点的地址,功能函数用二级指针来接收,亦或者选择用返回值的方式。下面是采取传地址的方式。
先封装一个创建新节点的函数,方便以后多次使用:
SLNode* BuyNewNode(MyType x) {SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));newnode->data = x;newnode->next = NULL;return newnode;
}
链表的尾插:
要实现单向链表的尾插,需要先判断是否头节点为空,然后遍历链表找到链表的最后一个结点。
//尾插
void SLinkListPushBack(SLNode** pphead, MyType x) {SLNode* newnode = BuyNewNode(x);if (*pphead == NULL) {*pphead = newnode;}else {SLNode* tail = *pphead;while (tail->next!=NULL) {tail = tail->next;}tail->next = newnode;}
}
链表的头插:
链表的头插也大相径庭,也先判断头节点是否为空,头插完成后将新节点置成头。
//头插
void SLinkListPushPront(SLNode** pphead, MyType x) {SLNode* newnode = BuyNewNode(x);newnode->next = *pphead;*pphead = newnode;
}
链表的尾删:
单链表的麻烦之处可能就是,尾插之时,不好找上一个位置。这样就需要另外一个变量来保存上有个节点。
//尾删
void SLinkListPopBack(SLNode** pphead) {//当链表为空,直接报错assert(pphead);//只有一个结点if((*pphead)->next==NULL) {free(*pphead);*pphead = NULL;}//两个结点及以上else {SLNode* tail = *pphead;while (tail->next->next) {tail = tail->next;}free(tail->next);tail->next = NULL;}
}
链表的头删:
头删就相对容易了,可以不用考虑一个还是多个情况,因为即使一个,它的下一个空节点为新的头节点也不受影响。
//头删
void SLinkListPopFront(SLNode** pphead) {//链表为空assert(*pphead);SLNode* newphead = (*pphead)->next;free(*pphead);*pphead = newphead;
}
链表的查找、插入:
查找的话遍历一遍链表就好啦。插入分为在前插入和在后插入。在前插入相对麻烦,因为单向链表的前一个节点需要再找一遍,所以需要重新定义一个变量,如果插入的位置是头节点之前的话,又就变成头插了(可以直接调用头插函数)。
//查找
SLNode* SLinkListFind(SLNode* phead, MyType x) {SLNode* cur = phead;while (cur) {if (cur->data == x) {return cur;}else{cur = cur->next;}}return NULL;
}
//在前插入
void SLinkListInterFormer(SLNode** pphead, SLNode*pos,MyType x) {assert(pphead);assert(pos);SLNode* newnode = BuyNewNode(x);if (*pphead ==pos) {newnode->next = *pphead;*pphead = newnode;}else {SLNode* Prev = *pphead;while (Prev->next != pos) {Prev = Prev->next;}Prev->next = newnode;newnode->next = pos;}}
//在后一个位置插入
void SLinkListInterAfter(SLNode* pos, MyType x) {assert(pos);SLNode* newnode = BuyNewNode(x);newnode->next = pos->next;pos->next = newnode;}
链表销毁:
由于传的的地址,直接一个函数就可以销毁了。
//摧毁链表
void SLinkListDestory(SLNode** pphead) {assert(pphead);SLNode* cur = *pphead;while (cur) {SLNode* next = cur->next;free(cur);cur = next;}*pphead = NULL;
}
双向带头循环链表
单向链表实现了,下面看一下双向带头循环链表,这种结构可以说是非常牛逼的一种链表结构。
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。
1.链表的创建
由于是双向,所以一个前继指针,一个后继指针。
typedef int MyType;//双向循环带头链表
typedef struct DoubleLinklist {MyType val;struct DoubleLinklist* next;struct DoubleLinklist* prev;
}DLNode;
2.链表功能实现
上面的单向链表不带头所以不需要初始化,直接phead=NULL;就可以开始创建链表。而这种结构的好处就是不用传地值了,因为它修改的是结构体里的内容,不过要先创建一个哨兵位节点—站岗用的。
链表初始化:
//初始化链表
DLNode* DLNodeInit() {DLNode* phead = (DLNode*)malloc(sizeof(DLNode));//哨兵位 不存储有效数据phead->next = phead;phead->prev = phead;return phead;
}
malloc新节点:
//创建新节点
DLNode* CeateNewnode(MyType x) {DLNode* newhead = (DLNode*)malloc(sizeof(DLNode));newhead->val = x;newhead->next = NULL;newhead->prev = NULL;return newhead;
}
链表的尾插:
尾插只需要找到head->prev就行。没有节点时,head->prev就是它自己。
所以尾插就可以轻松实现。
//尾插
void DLNodePushBack(DLNode* phead, MyType x) {//因为只改变phead指向的结构体的东西,并不改变Phead,所以只传一级指针assert(phead);//malloc新节点DLNode* newnode = CeateNewnode(x);//pehad->prev==tailDLNode* tail = phead->prev;//连接新节点tail->next = newnode;newnode->prev = tail;//首尾相连,形成循环phead->prev = newnode;newnode->next = phead;}
链表的头插:
链表的头插注意的是插入到head->next也就是head的下一个节点。因为链表遍历是从head的下一个节点开始。这也是我一开始打错的原因。
//打印链表
void DLNodePrint(DLNode* phead) {assert(phead);//phead里没有存有效数据,所以从phead的下一个开始DLNode* cur = phead->next;while (cur != phead) {printf("%d->", cur->val);cur = cur->next;}printf("\n");
}
//头插
void DLNodePushFront(DLNode* phead, MyType x) {assert(phead);DLNode* newnode = CeateNewnode(x);DLNode* next = phead->next;newnode->prev = phead;phead->next = newnode;next->prev = newnode;newnode->next = next;//可以调用插入函数,因为头插就是phead后结点插入/*DLNodeInterBack(phead, x);*/
}
链表的尾删:
尾节点不需要遍历就能找到,而且它的前一个节点也可以找到,这样就减少了消耗。不过需要注意的是,得先判断是不是没有节点,准确来说是不是只有一个哨兵位节点。
//尾删
void DLNodePopBack(DLNode* phead) {assert(phead);if(phead->next != phead) {DLNode* tail = phead->prev;tail->prev->next = phead;phead->prev = tail->prev;free(tail);tail = NULL;}
}
链表的头删:
头删也大相径庭,跟尾删一样要先判断是不是只有一个头节点。
//头删
void DLNodePopFront(DLNode* phead) {assert(phead);if (phead->next != phead) {DLNode* del = phead->next;phead->next = del->next;del->next->prev = phead;free(del);del = NULL;}
}
链表的查找、插入和删除:
为什么上个链表没有删除,因为忘记实现了。除了删除,只要查找到位置,修改,插入,删除其实都是很容易的事情。当然在这个完美的链表结构就更容易实现啦。
因为无论前插后插还是删除,直接就可以找前一个结点和后一个节点。
//查找
DLNode* DLNodeFind(DLNode* phead, MyType x) {assert(phead);DLNode* cur = phead->next;while (cur != phead) {if (cur->val == x) {return cur;}cur = cur->next;}return NULL;
}//结点前插入
void DLNodeInterFront(DLNode* pos, MyType x) {assert(pos);DLNode* newnode = CeateNewnode(x);DLNode* former = pos->prev;former->next = newnode;newnode->prev = former;newnode->next = pos;pos->prev = newnode;}
//结点后插入
void DLNodeInterBack(DLNode* pos, MyType x) {assert(pos);DLNode* newnode = CeateNewnode(x);DLNode* latter = pos->next;pos->next = newnode;newnode->prev = pos;newnode->next = latter;latter->prev = newnode;
}
//删除节点
void DLNodeErase(DLNode* pos) {assert(pos);DLNode* former = pos->prev, * latter = pos->next;former->next = latter;latter->prev = former;free(pos);pos = NULL;}
链表销毁:
需要注意的是,这里传的是一级指针,所传指针需要在函数外置空。
//摧毁链表
void DLNodeDestroy(DLNode* phead) {assert(phead);DLNode* cur = phead->next;while (cur!=phead) {DLNode* next = cur->next;free(cur);cur = next;}free(phead);phead = NULL;
}
链表基础题
链表的实现完成了,学了知识就需要接下来不断巩固。
刷题就是最好的方式,下面是几道关于链表的简单题目。
203. 移除链表元素 - 力扣(LeetCode)
题解:可以看到,移除所给值的节点,直接遍历就好啦,不过需要注意的是是否为头节点,如果为头节点就需要将头指针转移,就是头删。单向链表删除节点,需要遍历出上一个节点,如果每次删除都要遍历一遍,不如只遍历一遍将节点保存一下,再继续往下走,删除时就只需要将保存的前继节点连接到后一个节点就行。也是双指针问题。
struct ListNode* removeElements(struct ListNode* head, int val){if(head==NULL){return NULL;}struct ListNode* cur=head;struct ListNode* prev=NULL;while(cur){if(cur->val==val){//头删if(cur==head){head=head->next;free(cur);cur=head;}else{prev->next=cur->next;free(cur);cur=prev->next; } }else{prev=cur;cur=cur->next;}}return head;
}
面试题 02.04. 分割链表 - 力扣(LeetCode)
这道题的思路是遍历一遍链表,将小于目标值的节点合成一个链表,大于目标值的节点合成一个链表,然后将两个链表尾首相连即可。需要注意的是如果连接后的链表的最后一个节点的后继指针不为空,需要置空,否则他依旧指向某个节点,这样一来就形成了死循环了。
还有种情况就是没有小于目标值的节点,这样直接返回大于目标值的节点就好啦。
struct ListNode* partition(struct ListNode* head, int x){struct ListNode*cur=head;struct ListNode*samllerHead=NULL;struct ListNode*samallerTail=NULL;struct ListNode*greaterHead=NULL;struct ListNode*gearterTail=NULL;while(cur){if(cur->val<x){if(samllerHead==NULL){samllerHead=samallerTail=cur;}else{samallerTail->next=cur;samallerTail=samallerTail->next;}}else{if(greaterHead==NULL){greaterHead=gearterTail=cur;}else{gearterTail->next=cur;gearterTail=gearterTail->next;}}cur=cur->next;}if(samallerTail){samallerTail->next=greaterHead;}else{samallerTail=greaterHead;}if(gearterTail){gearterTail->next=NULL;}if(samllerHead==NULL){samllerHead=greaterHead;}return samllerHead;
}
面试题 02.02. 返回倒数第 k 个节点 - 力扣(LeetCode)
这道题解法也是双指针,可以用到双指针中的快慢指针。快指针先走k步,然后再跟慢指针一起走,当快指针为空时,此时慢指针就是倒数第k个节点。
int kthToLast(struct ListNode* head, int k){struct ListNode* fast=head,*slow=head;//先让fast走k步while(k--){fast=fast->next;}//再一起走,当fast为NULL时,此时slow就是第k的位置while(fast){slow=slow->next;fast=fast->next;}return slow->val;
}
个人总结
链表的操作虽然学会了,但放在题目还是不会,还是算法基础太薄弱了,已经准备买书买咖啡早起学算法了,希望终有一天我也能成为一个算法大佬。
相关文章:

数据结构───链表
花费一个周时间学完了链表(的一部分),简单总结一下。 链表的学习离不开画图,将其抽象成一种逻辑模型,可以减少思考时间,方便理解。 链表大致分为8种结构,自己学习并实现了两种结构,也…...
SQLAlchemy删除所有重复的用户|Counter类运用
Python标准库中的collections模块中的Counter类。Counter类用于计算可迭代对象中元素的出现次数,并以字典的形式返回结果,其中键是元素,值是该元素的出现次数。 for name, count in Counter(names).items() 是一个循环语句,它用于…...

Lec11 Thread switching (Robert)
线程的概念 线程就是单个串行执行代码的单元,它只占用一个CPU并且以普通的方式一个接一个的执行指令。 线程还具有状态,我们可以随时保存线程的状态并暂停线程的运行,并在之后通过恢复状态来恢复线程的运行。 程序计数器(Progr…...

前端的简单介绍
前端核心的分析 CSS语法不够强大,比如无法嵌套书写,倒是模块化开发中需要书写很多重复的选择器 没有变量和合理的样式复用机制,使逻辑上相关的属性值必须字面量的心事重复的输出,导致难以维护 CSS预处理器,减少代码的笨重&#…...

云服务器 centos 部署 code-server 并配置 c/c++ 环境
将你的云服务器改为 centos 8 为什么要将云服务器的操作系统改成 centos 8 呢?原因就是 centos 7 里面的配置满足不了 code-server 的需求。如果你使用的是 centos 7 那么就需要你升级一些东西,这个过程比较麻烦。我在 centos 7 上面运行 code-server 的…...
Ubuntu 22.04 安装 Terraform
Ubuntu 22.04 安装 Terraform 安装 Terraform 安装 Terraform sudo apt updatesudo apt install software-properties-common gnupg2 curlcurl https://apt.releases.hashicorp.com/gpg | gpg --dearmor > hashicorp.gpgsudo install -o root -g root -m 644 hashicorp.gpg…...

MLF - 麻辣粉
MLF全称中期借贷便利(Medium-term lending Facility),理解为央行向商业银行、政策银行发放的贷款,但需要符合一定要求才可向央行申请。银行通过MLF向央行借款的时候,需要提供担保品。一般为国债、央行票据、政策性金融债、地方债、…...

Flutter三棵树的创建流程
一、Flutter常见的家族成员 Widget常见的家族成员 Element常见的家族成员 Render常见的家族成员 二、示例代码对应的Flutter Inspector树 示例代码:MyApp->MyHomePage->ErrorWidget,包含了StatelessWidget、StatefulWidget、LeafRenderObjectWid…...
思维训练第二课 独立主格
系列文章目录 文章目录 系列文章目录前言一、独立主格特点 二、独立主格的构成1.名词/人称代词主格现在分词2. 名词/人称代词主格过去分词3. 名词/人称代词主格形容词/副词4. 名词/人称代词主格不定式5. 名词/人称代词主格介词短语6.介词复合宾语 三、独立主格结构的句法功能1、…...

一致性哈希揭秘,深入解析其工作原理
前言 在进行一致性哈希介绍前,先思考2个问题: 什么是Hash一致性Hash和Hash的关系是什么 对于第一个问题Hash的定义 Hash也成散列,基本原理就是把任意长度的输入,通过hash算法变成固定长度的输出。 对于第二个问题,…...

前端环境的安装 Node npm yarn
一 node npm 1.下载NodeJS安装包 下载地址:Download | Node.js 2.开始安装 打开安装包后,一直Next即可。当然,建议还是修改一下安装位置,NodeJS默认安装位置为 C:\Program Files 3.验证是否安装成功 打开DOS命令界面&#…...

基于机器视觉的银行卡识别系统 - opencv python 计算机竞赛
1 前言 🔥 优质竞赛项目系列,今天要分享的是 基于深度学习的银行卡识别算法设计 该项目较为新颖,适合作为竞赛课题方向,学长非常推荐! 🧿 更多资料, 项目分享: https://gitee.com/dancheng…...
大数据工具-kafkaUi-lite
1、kafkaUI-lite v1.0 已经发布,此版本更新内容包括: 可以实现 kafak/zookooper/redis 的界面化操作 kafka: 多环境管理、生产消息、消费消息、创建 topic、删除 topiczookeeper: 多环境管理、查看节点、查看节点数据redis: 多环境管理、查询数据2、kafkaUI-lite 介绍 史上…...
Vdue之模版语法指令过滤器计算属性监听属性
模板语法 Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器和 HTML 解析器解析。vue将模板编译成虚拟dom, 结合响应系统,V…...

Mysql权限控制语句
1.创建用户 create user ky32localhost IDENTIFIED by 123456 create user:创建用户开头 ky32:用户名 localhost 新建的用户可以在哪些主机上登录 即可以使用ip地址,网段主机名 ky32localhost ky32192.168.233.22 ky32192.168.233.0/2…...

小程序如何导入配送账号
为了提高配送效率和用户体验,可以导入配送账号(包括电子面单快递物流账号、同城外卖配送账号)到小程序中。导入后,可以实现一键发货,无需手动回填单号。而且在小程序中可以查看到物流状态,对于同城配送&…...

ubuntu(18.04) 安装 blast 并在php中调用
1、下载 https://ftp.ncbi.nlm.nih.gov/blast/executables/blast/LATEST/2、解压,配置环境变量 tar zvxf ncbi-blast-2.14.1-x64-linux.tar.gz解压后改名为 blast 配置环境变量,可以不配置 使用的时候直接绝对路径使用(本次使用绝对路径&am…...

UML—时序图是什么
目录 前言: 什么是时序图: 时序图的组成元素: 1. 角色(Actor) 2. 对象(Object) 3. 生命线(LifeLine) 4. 激活期(Activation) 5. 消息类型(Message) 6.组合片段(Combined fragment) 时序图的绘制规则: 绘制时序图的3步: 1.划清边界…...
【每日一题Day364】LC2003每棵子树内缺失的最小基因值 | dfs
每棵子树内缺失的最小基因值【LC2003】 有一棵根节点为 0 的 家族树 ,总共包含 n 个节点,节点编号为 0 到 n - 1 。给你一个下标从 0 开始的整数数组 parents ,其中 parents[i] 是节点 i 的父节点。由于节点 0 是 根 ,所以 parent…...
调试记录 单片机GD32F103C8T6(兆易创新) 程序烧写完成但是没有现象 (自己做的板子)
1. 单片机GD32F103C8T6 的资料 CPU内核:ARM Cortex-M3 CPU最大主频:108MHz 工作电压范围:2.6V~3.6V 程序存储容量:64KB 程序存储器类型:FLASH RAM, 总容量:20KB GPIO端口数量:37 最…...
Java 8 Stream API 入门到实践详解
一、告别 for 循环! 传统痛点: Java 8 之前,集合操作离不开冗长的 for 循环和匿名类。例如,过滤列表中的偶数: List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...

【JVM】- 内存结构
引言 JVM:Java Virtual Machine 定义:Java虚拟机,Java二进制字节码的运行环境好处: 一次编写,到处运行自动内存管理,垃圾回收的功能数组下标越界检查(会抛异常,不会覆盖到其他代码…...
使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装
以下是基于 vant-ui(适配 Vue2 版本 )实现截图中照片上传预览、删除功能,并封装成可复用组件的完整代码,包含样式和逻辑实现,可直接在 Vue2 项目中使用: 1. 封装的图片上传组件 ImageUploader.vue <te…...

Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)
引言:为什么 Eureka 依然是存量系统的核心? 尽管 Nacos 等新注册中心崛起,但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制,是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...

SpringCloudGateway 自定义局部过滤器
场景: 将所有请求转化为同一路径请求(方便穿网配置)在请求头内标识原来路径,然后在将请求分发给不同服务 AllToOneGatewayFilterFactory import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; impor…...

dify打造数据可视化图表
一、概述 在日常工作和学习中,我们经常需要和数据打交道。无论是分析报告、项目展示,还是简单的数据洞察,一个清晰直观的图表,往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server,由蚂蚁集团 AntV 团队…...
Redis的发布订阅模式与专业的 MQ(如 Kafka, RabbitMQ)相比,优缺点是什么?适用于哪些场景?
Redis 的发布订阅(Pub/Sub)模式与专业的 MQ(Message Queue)如 Kafka、RabbitMQ 进行比较,核心的权衡点在于:简单与速度 vs. 可靠与功能。 下面我们详细展开对比。 Redis Pub/Sub 的核心特点 它是一个发后…...

论文笔记——相干体技术在裂缝预测中的应用研究
目录 相关地震知识补充地震数据的认识地震几何属性 相干体算法定义基本原理第一代相干体技术:基于互相关的相干体技术(Correlation)第二代相干体技术:基于相似的相干体技术(Semblance)基于多道相似的相干体…...
Java求职者面试指南:计算机基础与源码原理深度解析
Java求职者面试指南:计算机基础与源码原理深度解析 第一轮提问:基础概念问题 1. 请解释什么是进程和线程的区别? 面试官:进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位;而线程是进程中的…...