数据结构───链表

花费一个周时间学完了链表(的一部分),简单总结一下。

 

链表的学习离不开画图,将其抽象成一种逻辑模型,可以减少思考时间,方便理解。

链表大致分为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;
}

 

个人总结 

链表的操作虽然学会了,但放在题目还是不会,还是算法基础太薄弱了,已经准备买书买咖啡早起学算法了,希望终有一天我也能成为一个算法大佬。

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

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

相关文章

ubuntu无网络连接,没有网络标识,快速解决方法

在这里插入代码片当我们装虚拟机的时候&#xff0c;需要用到网络时发现没有网络连接&#xff0c;且右上角没有网络标识符&#xff0c;这时只需要简单的输入一下三个命令即可 sudo nmcli networking offsudo nmcli networking onsudo service network-manager restart然后重启客…

Vue源码-各实现原理简单总结

1&#xff0c;双向数据绑定的实现 简单总结为&#xff1a;通过js的层层封装&#xff0c;互相调用&#xff0c;实际就是利用js的Object.defineProperty()方法&#xff0c;然后实现了一个发布订阅模式。 整体逻辑是在vue初始化的时候&#xff0c;通过Object.defineProperty()重写…

(二)FreeRTOS任务控制(1)

1.任务延时vTaskDelay //task. hvoid vTaskDelay( const TickType_t xTicksToDelay ); 必须将 INCLUDE_vTaskDelay 定义为 1&#xff0c;此函数才可用。按给定的 tick 数延迟任务。任务保持阻塞的实际时间取决于 tick 频率。 常量 portTICK_PERIOD_MS 配合 tick 周期分辨率可用…

问题 D: 免费馅饼(类数塔问题)

免费馅饼 都说天上不会掉馅饼&#xff0c;但有一天gameboy正走在回家的小径上&#xff0c;忽然天上掉下大把大把的馅饼。说来gameboy的人品实在是太好了&#xff0c;这馅饼别处都不掉&#xff0c;就掉落在他身旁的10米范围内。馅饼如果掉在了地上当然就不能吃了&#xff0c;所以…

【DriveGPT学习笔记】自动驾驶汽车Autonomous Vehicle Planning

原文地址&#xff1a;DriveGPT - Lei Maos Log Book 自动驾驶汽车的核心软件组件是感知、规划和控制。规划是指在给定场景或一系列场景的情况下为自动驾驶汽车制定行动计划的过程&#xff0c;以实现安全和理想的自动驾驶。 用于规划的场景是从感知软件组件获得的。计划的行动将…

深度学习——炼丹

学习率调整策略 自定义学习率调整策略 简单版 net MyNet()optimoptim.Adam(net.parameters(),lr0.05) for param_group in optim.param_groups: param_group["lr"] param_group["lr"]*0.5print(param_group["lr"]) #0.25复杂版&#…

(新手)vue git下载后无法在本地启动项目

npm run serve> xxxxxx0.1.0 serve > vue-cli-service serve vue-cli-service 不是内部或外部命令&#xff0c;也不是可运行的程序 或批处理文件。 原因:缺少依赖 npm i 是 npm install 的简写形式&#xff0c;是用于安装 JavaScript 包和模块的 npm 命令。 npm …

centos 7 kafka2.6单机安装及动态认证SASL SCRAM配置

目录 1.kfaka安装篇 1.1 安装jdk 1.2安装kafka 2.安全篇 2.1 kafka安全涉及3部份&#xff1a; 2.2 Kafka权限控制认证方式 2.3 SASL/SCRAM-SHA-256 配置实例 2.3.1 创建用户 2.3.2 创建 JAAS 文件及配置 3.测试 3.1 创建测试用户 3.2 配置JAAS 文件 3.2.1 生产者配…

arcgis删除细长图斑的方法

1、有一张图斑数据如下&#xff1a; 如上图&#xff0c;有很多细长的面要素&#xff0c;需要保留的仅是图中的块状要素。 2、首先要将被合并的要素进行拆分&#xff0c;具体拆分步骤如下&#xff1a; 将所有要素选中&#xff0c;点击高级编辑中的拆分按钮。 3、拆分后图斑就…

信号、进程、线程、I/O介绍

文章目录 信号进程进程通信线程可/不可重入函数线程同步互斥锁条件变量自旋锁读写锁 I/O操作阻塞/非阻塞I/OI/O多路复用存储映射I/O 信号 信号是事件发生时对进程的通知机制&#xff0c;可以看做软件中断。信号与硬件中断的相似之处在于其能够打断程序当前执行的正常流程。大多…

C语言每日一题(23)兔子的序列

牛客网 BC159 兔子的序列 题目描述 描述 兔子发现了一个数字序列&#xff0c;于是开始研究这个序列。兔子觉得一个序列应该需要有一个命名&#xff0c;命名应该要与这个序列有关。由于兔子十分讨厌完全平方数&#xff0c;所以兔子开创了一个新的命名方式&#xff1a;这个序列…

深度学习数据集大合集—疾病、植物、汽车等

最近又收集了一大批深度学习数据集&#xff0c;今天分享给大家&#xff01;废话不多说&#xff0c;直接上数据&#xff01; 1、招聘欺诈数据集 招聘欺诈数据集&#xff1a;共收集了 200,000 条数据&#xff0c;来自三个网站。 该数据集共收集了 200.000 条数据&#xff0c;分别…

算法-小红的ABC(最短回文子串)- [简单]

直通牛客-小红的ABC 题目描述 小红拿到了一个只包含 a , b , c 三种字符的字符串。 小红想知道&#xff0c;这个字符串最短的、长度超过 1 的回文子串的长度是多少&#xff1f; 子串定义&#xff1a;字符串取一段连续的区间。例如"abcca"的子串有"ab"、&…

Java程序设计2023-第五次上机测试

8-1使用按钮选择绘制不同图形 编写一个程序&#xff0c;实现如下的界面&#xff0c;当点击不同按钮时绘制相应的图形。点击“椭圆”&#xff0c;绘制一个椭圆形&#xff1b;点击“矩形”&#xff0c;绘制一个矩形&#xff1b;点击“直线”&#xff0c;绘制一条直线。实现图形绘…

短视频账号矩阵系统saas源码搭建/技术

一、短视频矩阵系统建模----技术api接口--获取用户授权 技术文档分享&#xff1a; 本系统采用MySQL数据库进行存储&#xff0c;数据库设计如下&#xff1a; 1.用户表&#xff08;user&#xff09;&#xff1a; - 用户ID&#xff08;user_id&#xff09; - 用户名&#xff08…

纯前端实现图片验证码

前言 之前业务系统中验证码一直是由后端返回base64与一个验证码的字符串来实现的&#xff0c;想了下&#xff0c;前端其实可以直接canvas实现&#xff0c;减轻服务器压力。 实现 子组件&#xff0c;允许自定义图片尺寸(默认尺寸为100 * 40)与验证码刷新时间(默认时间为60秒)…

MySQL - 系统库之 information_schema

information_schema 用于存储数据库元数据&#xff08;metadata&#xff09;。包含了有关数据库服务器、数据库、表、列、索引、权限、存储引擎和其他数据库对象的信息。information_schema 允许用户查询和检索数据库结构和对象的信息&#xff0c;而不需要直接访问系统表或文件…

天体学爱好者基础知识-太阳系//未完待续,业余者的学习

难过的时候&#xff0c;仰望天空吧&#xff0c;人类有时候&#xff0c;做的事情真的太愚昧且无聊了&#xff0c;渺小的尘埃&#xff0c;也可以飘际宇宙。 太阳系-八大行星 卫星围绕着恒星公转。行星必须围绕着恒星公转。 什么是行星&#xff1f;行星和恒星、卫星有什么区别&am…

Fourier分析导论——第2章——Fourier级数的基本属性(E.M. Stein R. Shakarchi)

第 2 章 Fourier级数的基本属性(Basic Properties of Fourier Series) Nearly fifty years had passed without any progress on the question of analytic representation of an arbitrary function, when an assertion of Fourier threw new light on the subject. Thus…

yolov7模型轻量化改进之MobileOne骨干替换

本文在之前文章yolov7分割训练的基础上进行改进: https://blog.csdn.net/qq_41920323/article/details/129464115?spm=1001.2014.3001.5502 具体GitHub工程下载,环境安装配置,数据准备等,请借鉴之前的文章,此处只介绍如何进行改进。 MobileOne地址:https://github.com…