数据结构-链表

吐槽一下:

        在我第一次看到链表这个东西的时候,我觉得数据结构好难啊,怎么这么难理解啊,这是什么玩意啊,结果慢慢的我才发现,链表是除了顺序表最简单的一个数据结构了;我以为我学完了链表,我就很牛了,后面才发现我连数据结构的门坎都没没摸到。下面来看下链表到底是个什么玩意。

链表:

        需要提示一下,对于C语言中的指针不熟悉的小伙伴,我会在出一个专门讲指针的一篇文章,因为数据结构中,对于指针的使用是非常多的。

它的数据结构:

        在顺序表的那篇文章中,我提过数据结构 = 结构定义 + 结构操作;

        那么链表的结构定义是什么?他的物理结构和逻辑结构分别是什么?在学习一个新的数结构之前需要这要问一下自己,这样你的思路会更清晰,也不会混乱;

        物理结构:他是由节点组成的,然后每个节点里面,有一个值域,有一个指针域;

        值域是什么,用来存数据的,就像数组每个位置存的值一样;

        指针域存的是什么,用来存他下一个节点的地址的,已能通过该节点找到下一个节点;这样就这个把每个结点串联起来,形成一个链状;大概就是图1的样子 

图1      

        那么他的结构定义用代码实现是怎么样的,来看下面的代码:

        

typedef struct Node {//结点定义void val;//值域,可以是int,char等等类型struct Node *next;//指针域,用来存下一个结点的地址的
} Node;

        可能会有小伙伴会问,不是链表吗?怎么就定义结点?其实只要有头节点,是不是可以找到链表的每一个结点,因为每个结点中有存着下一个结点的地址,假如现在在头结点,是不是可找到头结点的下一个结点,同样的道理可以找到下下个结点,直到找到末尾;他是通过像图2的方式来找寻下个结点的;每个矩形上方代表值域,下方代表指针域:

        

         为了让你们更好理解后面的结构操作,我还是把链表的结构定义写出来:

          

typedef struct List {//链表int len;//结点个数Node *head;//头结点
} List;

        逻辑结构:链表在我们的思维中,也就是我们的脑子中以为他是连成一串的,就像图1的样子一样,但是在计算机中他的样子可能是图3的样子:

图3

         而图3中的样子才是链表在计算机中的真实样子;

它的结构操作:

        了解完它的结构定义之后,一定要把结构定义刻在脑子里,就像你忘不了你的昨天晚上那场王者荣耀你mvp被0-14的队友嘲讽的那场比赛一样;

        对于数据的操作,无非就是增删改查嘛,下面我会实现增删两个操作,改查可以自己去摸索一下自己尝试后,你就会发现这**博主讲东西只讲一半;

        下面是代码实现:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>typedef struct Node {//结构定义结点int val;struct Node *next;
} Node;typedef struct List {//结构定义链表int len;Node head;
} List;Node *getNewNode(int val) {//初始化,获取新节点Node *n = (Node *)malloc(sizeof(Node));n->val = val;n->next = NULL;return n;
}List *getNewList() {//初始化,获取新链表List *list = (List *)malloc(sizeof(List));list->len = 0;//为什么为0,因为链表结构中这个头结点是虚拟的不纯在的list->head.next = NULL;return list;
}int insertNode(List *l, int val, int ind) {//插入结点,从虚拟头结点开始数位置从0开始到l->len的位置中间插入结点if (!l) return 0;if (ind < 0 || ind > l->len) return 0;//如果插入位置小于0,或者大于了l->len的值,说明插入位置不在链表中Node *p = &(l->head), *n = getNewNode(val);while (ind--) p = p->next;n->next = p->next;p->next = n;l->len++;return 1;
}int eraseNode(List *l, int ind) {//删除结点if (!l) return 0;if (ind < 0 || ind >= l->len) return 0;//l->len位置是没有结点的,插入的时候就是插入在l->len的位置 Node *p = &(l->head);while (ind--) p = p->next;Node *temp = p->next;p->next = p->next->next;free(temp);l->len--;return 1;
}void clearNode(Node *n) {//删除结点,借了计算机的就要还回去if (!n) return ;clearNode(n->next);free(n);return ;
}void clearList(List *list) {//删除链表if (!list) return ;clearNode(list->head.next); free(list);return ;
}void output(List *list) {//打印链表printf("List(%d) = ", list->len);Node *p = list->head.next;while (p) {printf("%d--->", p->val);p = p->next;}printf("NULL\n");return ;
}int main() {srand(time(0));int op, val, ind;List *list = getNewList();for (int i = 0; i < 20; i++) {op = rand() % 4;val = rand() % 100;ind = rand() % (list->len + 2) - 1;switch (op) {case 0:case 1:case 2: {printf("%d insert in List %d is %d\n", val, ind, insertNode(list, val, ind));} break;case 3: {printf("erase in List %d is %d\n", ind, eraseNode(list, ind));} break;}output(list);}clearList(list);//记得还给计算机return 0;
}

        整段代码以及实现了,现在会有小伙伴,理解不了删除和插入结点的情况,下面我讲解一下:

        插入结点:

         也就是增加结点,指定了一个位置

        上面讲了,head是虚拟头结点,它是我们思想中的一个结点,它的值域是无效的,也就是说他的值域是没用的,然后为什么需要这个虚拟头结点,因为在增加删结点时,我们需要找它对应的前一个结点的位置,才能更方便的去增删;现在是增加结点,你需要怎么去做上面操作?是不是需要把它的上一个结点的指针域改为增加结点的地址,然后增加结点的指针域改为上一个结点之前存的地址,这样链表就链接起来了;这两步我说反了,因为如果你先去覆盖了上一个结点的指针域,也就是把新结点的地址去覆盖了之前的值,那么你就会丢失后面链表;就像这样

         你会发现,你找不到后面结点了,所以需要先把后面的结点接在结点的指针域上,在去覆盖前一个结点的指针域,这样才是正确的顺序插入结点;现在把代码拿过来看一下:

        

int insertNode(List *l, int val, int ind) {if (!l) return 0;if (ind < 0 || ind > l->len) return 0;Node *p = &(l->head), *n = getNewNode(val);//n为新添加的结点while (ind--) p = p->next;//while循环后,p现在是插入结点的前一个结点//假如ind为0,那么p是不是虚拟头结点的位置,那么就可以在头结点的位置插入结点,也就是位置0//可以发现虚拟头结点的好处就是不用去判断是否在链表的头尾还是中间插入结点,减少了代码量n->next = p->next;//新节点的指针域去存前一个结点的下一个结点的地址,也就是接上后面的结点p->next = n;//前一个结点在接上新的结点,完成了插入结点的操作l->len++;return 1;
}
n->next = p->next;
p->next = n;

       这两行代码就是下面3张图片的操作:

 

删除结点:

        其实和插入结点差不了太多,同样和增加结点一样,找到删除结点前一个结点,然后通过前一个结点来获取到前一个结点的下下个结点的地址,然后来覆盖前一个的指针域,现在删除的结点成为了单独的结点,然后free掉还给计算机,老子不借了;

        先来看代码:

int eraseNode(List *l, int ind) {if (!l) return 0;if (ind < 0 || ind >= l->len) return 0;Node *p = &(l->head);while (ind--) p = p->next;//获取到删除结点的前一个结点Node *temp = p->next;//用一个指针指向,删除结点,防止丢失;如果你没记录下来丢了,那你就去找吧,反正我没有办法找到p->next = p->next->next;//p->next现在表示的是前一个结点的指针域,然后用下下个结点的地址来覆盖掉,也就是接上下下个结点free(temp);//给删除的结点还给电脑,老子不用了l->len--;//最后链表的长度记得减1return 1;
}

        理解不了代码的小伙伴来看图:

 OK完成删除操作,非常完美

        看到这儿的小伙伴我觉得应该都差不多理解了,现在直接来一道leetcode题目,练练手直接起飞!

        leetcode19:删除倒数第N个结点,我们刚刚删的是正数的,现在是倒数的,嗯,看到脑壳痛,刚刚正的都没搞清楚,现在又来反的了;

        这个题需要用到双指针,也就是两个指针变量,一个指针为f快指针,一个为s慢指针;f先走N + 1步,假如N为2,结点个数为5

         这里只是巧合f到了删除结点的前一个结点,然后f,s同时往前移动,直到f到达NULL:

         s到大删除结点的前一个,那么就和上面删除结点的操作一样了;

        现在来说为什么成立f总共走了len + 1步,len为结点个数,为什么+1,因为它走到了NULL的位置还有一步;        

        那么s走了多少步 len + 1 - (N + 1);len + 1为f走的步减去,它之前先走的步数那么最终s走了len - N步;

        那么步就得到了正着数的步数了嘛,那不就简单了嘛,和我们实现的删除操作是一样的拉嘛;

        现在来看代码

struct ListNode* removeNthFromEnd(struct ListNode* head, int n){if (!head) return head;struct ListNode *s, *f, xuni_head;//没有虚拟头结点,那就自己创一个xuni_head.next = head;//虚拟头结点指向头结点s = &(xuni_head);f = head;//因为f要走n+1步那么我就先走一步while (n--) f = f->next;//f在走n步while (f) {//同时走,直到f到NULLf = f->next;s = s->next;}f = s->next;//f获取删除结点位置s->next = s->next->next;//链接上删除结点的后面结点位置free(f);return xuni_head.next;
}

谢谢观看,觉得如果对你有帮助,可以点个赞,也是对我的鼓励; 

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

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

相关文章

登录认证-登录校验-会话技术方案选择和对比(cookie、session和JWT令牌)

会话技术方案选择和对比 一、背景说明二、会话技术之 Cookie1、为什么说cookie是客户端会话技术2、cookie的优点和缺点 三、会话技术之 Session1、为什么说Session是服务端会话技术2、session的优点和缺点 四、令牌技术JWT1、JWT 的原理2、JWT的优点和缺点 一、背景说明 在开发…

科大讯飞笔试编程第二题(处理Scanner不能先输入数字再输入字符串问题)

问题&#xff1a; 在使用scanner的时候如果先读取一个数字&#xff0c;在读取一行带有空格的字符串&#xff0c;势必会出错或者字符串读不到 public static void main(String[] args) {Scanner scanner new Scanner(System.in);int x scanner.nextInt();String s scanner.n…

【C++杂货铺】探索vector的底层实现

文章目录 一、STL1.1 什么是STL?1.2 STL的版本1.3 STL的六大组件 二、vector的介绍及使用2.1 vector的介绍2.2 vector的使用2.2.1 vector的定义2.2.2 vector iterator2.2.3 vector空间增长问题2.2.4 vector增删查改 2.3 vector\<char\> 可以替代 string 嘛&#xff1f; …

指针-C语言(初阶)

目录 一、什么是指针 二、指针和指针类型 2.1 指针-整数 2.2 指针的解引用 三、野指针 3.1 野指针形成原因 3.2 如何规避野指针 四、指针运算 4.1 指针-整数 4.2 指针-指针 4.3 指针的关系运算 五、指针和数组 六、二级指针 七、指针数组 一、什么是指针 指针是内存中一个…

【八股】2023秋招八股复习笔记4(MySQL Redis等)

文章目录 目录1、MySQLmysql索引实现mysql索引优化mysql索引失效的情况mysql 千万数据优化mysql 事务隔离级别 & 实现原理mysql MVCC版本链&#xff08;undo log&#xff09;mysql数据同步机制 & 主从复制 &#xff08;binlog&#xff09;mysql 日志&数据恢复&…

5G NR:RACH流程-- Msg1之生成PRACH Preamble

随机接入流程中的Msg1&#xff0c;即在PRACH信道上发送random access preamble。涉及到两个问题&#xff1a; 一个是如何产生preamble&#xff1f;一个是如何选择正确的PRACH时频资源发送所选的preamble? 一、PRACH Preamble是什么 PRACH Preamble从数学上来讲是一个长度为…

MyBatis与Spring的集成整合加优化分页功能

目录 一.为什么要将MyBatis和Spring整合&#xff1f;&#xff1f;&#xff1f; 二.配置环境 2.1 pom文件 2.2 xml文件 三.演示举例 四.Aop整合pageHelper 分页插件 今天的分享就到这啦&#xff01;&#xff01;&#xff01; 一.为什么要将MyBatis和Spring整合&#xff1f…

自动驾驶感知传感器标定安装说明

1. 概述 本标定程序为整合现开发的高速车所有标定模块,可实现相机内参标定和激光、相机、前向毫米波 至车辆后轴中心标定,标定参数串联传递并提供可视化工具验证各个模块标定精度。整体标定流程如下,标定顺序为下图前标0-->1-->2-->3,相同编号标定顺序没有强制要求…

【业务功能篇83】微服务SpringCloud-ElasticSearch-Kibanan-docke安装-应用层实战

五、ElasticSearch应用 1.ES 的Java API两种方式 Elasticsearch 的API 分为 REST Client API&#xff08;http请求形式&#xff09;以及 transportClient API两种。相比来说transportClient API效率更高&#xff0c;transportClient 是通过Elasticsearch内部RPC的形式进行请求…

共享内存 windows和linux

服务端&#xff0c;即写入端 #include <iostream> #include <string.h> #define BUF_SIZE 1024 #ifdef _WIN32 #include <windows.h> #define SHARENAME L"shareMemory" HANDLE g_MapFIle; LPVOID g_baseBuffer; #else #define SHARENAME "sh…

使用通信顺序进程(CSP)模型的 Go 语言通道

在并发编程中&#xff0c;许多编程语言采用共享内存/状态模型。然而&#xff0c;Go 通过实现 通信顺序进程&#xff08;CSP&#xff09;模型来区别于众多。在CSP中&#xff0c;程序由不共享状态的并行进程组成&#xff1b;相反&#xff0c;它们通过通道进行通信和同步操作。因此…

wireshark抓包

Wireshark是非常流行的网络封包分析软件&#xff0c;可以截取各种网络数据包&#xff0c;并显示数据包详细信息。常用于开发测试过程各种问题定位。本文主要内容包括&#xff1a; 1、Wireshark软件下载和安装以及Wireshark主界面介绍。 2、WireShark简单抓包示例。通过该例子学…

最新绕过目标域名CDN进行信息收集技术

绕过目标域名CDN进行信息收集 1&#xff0e;CDN简介及工作流程 CDN&#xff08;Content Delivery Network&#xff0c;内容分发网络&#xff09;的目的是通过在现有的网络架构中增加一层新的Cache&#xff08;缓存&#xff09;层&#xff0c;将网站的内容发布到最接近用户的网…

ubuntu下自启动设置,为了开机自启动launch文件

1、书写sh脚本文件 每隔5秒钟启动一个launch文件&#xff0c;也可以直接在一个launch文件中启动多个&#xff0c;这里为了确保启动顺利&#xff0c;添加了一些延时 #! /bin/bash ### BEGIN INIT sleep 5 gnome-terminal -- bash -c "source /opt/ros/melodic/setup.bash…

uniapp - 全平台兼容实现上传图片带进度条功能,用户上传图像到服务器时显示上传进度条效果功能(一键复制源码,开箱即用)

效果图 uniapp小程序/h5网页/app实现上传图片并监听上传进度,显示进度条完整功能示例代码 一键复制,改下样式即可。 全部代码 记得改下样式,或直接

MyBatis的基本入门及Idea搭建MyBatis坏境且如何一步骤实现增删改查(CRUD)---详细介绍

一&#xff0c;MaBatis是什么&#xff1f; 首先是一个开源的Java持久化框架&#xff0c;它可以帮助开发人员简化数据库访问的过程并提供了一种将SQL语句与Java代码进行解耦的方式&#xff0c;使得开发人员可以更加灵活地进行数据库操作。 1.1 Mabatis 受欢迎的点 MyBatis不仅是…

使用CSS的@media screen 规则为不同的屏幕尺寸设置不同的样式(响应式图片布局)

当你想要在不同的屏幕尺寸或设备上应用不同的CSS样式时&#xff0c;可以使用 media 规则&#xff0c;特别是 media screen 规则。这允许你根据不同的屏幕特性&#xff0c;如宽度、高度、方向等&#xff0c;为不同的屏幕尺寸设置不同的样式。 具体来说&#xff0c;media screen…

【Spring MVC】

目录 &#x1f36e;1 什么是 MVC &#xff1f; &#x1f381;2 Spring MVC 的连接 &#x1f358;2.1 RequestMapping 实现 POST 和 GET 请求 &#x1f963;2.2 GetMapping 只支持 GET 请求 &#x1fad6;2.3 PostMapping 只支持 POST 请求 &#x1f36c;3 Spring MVC 获取参数的…

Spring复习:(56)PropertySourcePlaceholderConfigurer的工作原理

PropertySourcePlaceholderConfigurer的用途&#xff1a;通过配置文件&#xff08;比如.properties文件&#xff09;给bean设置属性&#xff0c;替代属性占位符 示例&#xff1a; 属性配置文件 spring.datasource.urljdbc:mysql://xxx.xxx.xxx.xxx/test spring.datasource.us…

【数仓建设系列之三】数仓建模方式及如何评估数仓完善性

【数仓建设系列之三】数仓建模方式及如何评估数仓完善性 上篇文章我们对数仓的分层架构及核心概念做了简单介绍&#xff0c;同时也指明DW层是数仓建模的核心层。本篇文章&#xff0c;将详细从常见的维度模型建设手段及如何评估数仓建设的完善性展开讨论。 一、数仓维度建模 ​…