【基本数据结构】链表

文章目录

  • 前言
  • 链表
    • 简介
      • 头节点与尾节点
      • 特性
    • 分类
      • 单向链表
      • 双向链表
      • 循环链表
    • 单链表基本操作
      • 定义并初始化单链表
      • 读取节点
      • 插入节点
      • 删除节点
      • 修改节点
  • 参考资料
  • 写在最后

前言

本系列专注更新基本数据结构,现有以下文章:

【算法与数据结构】数组.

【算法与数据结构】链表.

【算法与数据结构】哈希表.


链表

简介

链表是一种线性结构,但不同于数组在内存中占据一块连续的内存,链表使用的是内存中一组任意的存储单元来存储具有相同的数据类型的元素。这组任意的存储单元可以是连续的,也可以不是连续的。

以单链表为例,链表的存储方式如下图所示。

链表-链表.drawio

链表将一组任意的存储单元串联在一起。每一个存储单元被称为链表的一个节点,节点是一个结构体。结构体内存储两个变量,一个是节点的值,另一个是指向链表下一个节点的指针。一个节点值为整型的链表结构体可以这样定义:

struct ListNode {int val;ListNode* next;
}

头节点与尾节点

链表的头节点指的是链表的第一个节点(有的资料中将第一个元素之前的节点称为头节点,就是我们会面要讲到的呀节点),通常给一个链表的头节点,我们就可以通过遍历得到链表中的每一个节点。这里需要区分一下头节点与头指针。

头指针是指向链表第一个节点的指针。在单向链表中,头指针指向链表的头节点,就是后面会提到的呀节点的next指针,即 dummy->next。在双向链表中,头指针同样指向链表的头节点。

链表的尾节点是指链表中最后一个节点。在单向链表中,尾节点的 next 指针通常指向空指针 nullptr,表示链表的末尾。在双向链表中,尾节点的 next 指针同样指向空指针 nullptr,而 prev 指针则指向倒数第二个节点,表示双向链表的末尾。

特性

链表不需要实现事先分配内存,在需要存储空间时可以临时申请。因为链表不需要内存中一块连续的存储空间,所以相比数组可以更好的利用内存中零散的空间。相比于数组,使用链表执行数据的插入、删除以及移动效率会高一点。但是空间开销相比数组会大一点,因为链表的每一个节点需要存储两个变量,而数组的每个位置只需要存储一个变量。


分类

单向链表

定义

单向链表指的是链表的每一个节点里的指针都会指向下一个节点的链表。

单向链表节点类设计如下所示, C++11 \texttt{C++11} C++11 的标准库中虽然也定义了 forward_list \texttt{forward\_list} forward_list 单向链表,但因为单向链表的定义与操作相对简单,所有我们通常自己定义节点。

struct ListNode {int val;ListNode* next;ListNode() : val(0), next(nullptr) {}ListNode(int x) : val(x), next(nullptr) {}ListNode(int x, ListNode* _next) : val(x), next(_next) {}};

结构图

链表-单向链表.drawio

双向链表

定义

双向链表是对单向链表的升级,除了具备单向链表的 next 节点之外,还有一个 prev 指针,该指针指向当前节点的上一个节点。头节点的上一个节点为 nullptr 节点,尾节点的下一个节点为 nullptr

双向链表的节点类设计如下所示。 C++11 \texttt{C++11} C++11 的标准库中虽然也定义了 list \texttt{list} list 双向链表.

struct ListNode {int val;ListNode* next;ListNode* prev;ListNode() : val(0), next(nullptr), prev(nullptr) {}ListNode(int x) : val(x), next(nullptr), prev(nullptr) {}ListNode(int x, ListNode* _next, ListNode* _prev) : val(x), next(_next), prev(_prev) {}};

结构图

链表-双向链表.drawio

循环链表

定义

循环链表有两种,一种是在单向链表中将尾节点的 next 指针指向由空指针改为指向头节点形成的单向循环链表,另一种指的是双向循环链表。

双向循环循环链表是在双向链表的基础上,将链表的头节点和尾节点连接在一起,即将头节点的 prev 指针指向尾节点,尾节点的 next 指针指向头节点。通过这样的操作可以实现从循环链表的任何一个节点出发都能找到其他的任意节点。

循环链表的节点类设计与双向链表的节点类设计一致。

结构图

链表-循环链表.drawio

单链表基本操作

链表是一种具有增、删、改、查这四种基本操作的基本数据结构。单链表作为一种形式最简单的链表自然也具备这四种操作。本节会介绍定义并初始化单链表以及提到的四种基本操作,中间还会穿插介绍如何计算链表的长度。

在这单向链表、双向链表和循环链表中,单向链表最为基础,并且是算法类面试题中链表这一块的考察重点,需要重点掌握。

定义并初始化单链表

// 定义节点
struct ListNode {int val;ListNode* next;ListNode() : val(0), next(nullptr) {}ListNode(int x) : val(x), next(nullptr) {}ListNode(int x, ListNode* _next) : val(x), next(_next) {}};// 定义链表头节点
ListNode* head = new ListNode(0);

在此例子中,我们首先定义了链表的节点类,然后定义了一个节点 head 作为链表的头节点,头节点的 next 指针指向一个空节点。

读取节点

在数组这种顺序结构中,我们计算任意一个元素的存储位置是很容易的(C/C++ 中虽然是通过下标进行索引的,但其底层是通过数组的首地址与下标之间的计算获得对应位置的地址,再取地址中的元素)。但是在单链表中我们无法像数组那样通过索引得知第 N 个节点是什么,只能从头节点开始一个节点一个节点的查找。

获得链表的第 N 个节点(N >= 1 )的算法思路:

  • 在查找链表的第 N 个节点之前需要先统计链表中的节点总数,如果总数 cnt < N,则直接返回 nullptr,否则接着执行以下步骤。
  • 声明一个指向链表头节点的节点 cur,使用 for 循环或者 while 循环(迭代),将节点向后移动 N-1 次(将 cur 更新为 cur->next)。
  • 循环结束后,返回 cur 即为需要查找的节点。
// 计算以 head 为头节点的链表的节点数
int getN(ListNode* head) {int cnt = 0;while (head != nullptr) {++cnt;head = head->next;}return cnt;
}ListNode* getNthNode(ListNode* head, int N) {int cnt = getN(head);if (N > cnt) {return nullptr;}ListNode* cur = head;while (N > 1) {cur = cur->next;}return cur;
}

在此例子中,我们使用函数 getN 计算链表的长度(节点的数量)。我们从链表的头节点开始遍历链表,只要当前的链表不为空(nullptr),就更新 cnt = cnt + 1,并更改 head 为下一个节点。

插入节点

在给定链表中的指定位置插入一个节点,需要考虑以下几个问题:

  • (1)给定的链表是否为空;
  • (2)指定位置是否越界;
  • (3)指定的位置位于链表的头部、中间还是尾部。

如果给定的链表为空,则直接返回新插入的节点;如果指定的位置越界,直接返回给定链表的头节点即可。对于问题(3)中的三种情况,我们逐条进行分析。

在链表中间插入元素

顾名思义,插入节点的位置位于链表的中间位置,在链表第 i 个位置(头节点被称为第一个位置)之前插入值为 val 的链节点,通常:

  • (1)遍历链表找到第 i-1 个节点 preNode
  • (2)新建需要插入的节点 newNode
  • (3)将节点 newNode 的 next 指针连接到(指向)preNode 节点的下一个节点;
  • (4)将节点 preNode 的 next 指针连接到 newNode 节点;
  • (5)最后返回头节点 head

一图胜千言,上述变换过程见下图所示:

链表-插入节点.drawio (1)

在链表头部插入节点

在链表头部插入节点更加简单:

  • 新建需要插入的节点 newNode
  • 将节点 newNode 的 next 指针连接到(指向)head 节点,newNode 作为链表新的头节点。

在链表尾部插入元素

遍历找到链表的最后一个节点,将该节点的 next 指针指向新建的节点 newNode 即可。

总结

下面就是一个往给定链表中指定位置插入一个元素的示例:

ListNode* insertNode(ListNode* head, int pos, int newVal) {// 问题(1)if (head == nullptr) {	return new ListNode(newVal);}// 问题(2)int N = getN(head);	// 获取链表长度if (pos < 0 || pos > N+1) { // 注意这里的 大于 N+1 是考虑到要在尾部插入节点return head;}// 问题(3)ListNode* cur = head;int i = 1;while (i < pos-1) {		// 找到第 pos 个节点后退出循环cur = cur->next;++i;}ListNode* newNode = new ListNode(newVal);// 在链表头部插入节点if (cur == head) { cur->next = head;return newNode;}// 在链表中间或尾部插入节点newNode->next = cur->next;	// 当在在链表尾部插入节点时,此时 cur->next = nullptrcur->next = newNode;return head;
}

Note:以上代码中 在链表中间插入元素 是在链表的第 i 个位置之前插入节点,如果是在第 i 个位置之后插入节点,代码会有细微的变换,请读者注意。

删除节点

删除链表中的节点与插入节点操作一样都需要考虑一下三个情况:

  • 待删除的链表为空;
  • 删除的节点是非法的(越界);
  • 删除的节点分别位于链表的头部、中间位置或者尾部。

以下以图解的形式对第三种请款进行说明。前两种情况比较简单,将直接在代码中进行展示。

链表-删除节点.drawio

(1)删除链表中间位置的节点需要先找到被删除节点的上一个节点 `prevNode;

(2)将 prevNode 的 next 指针指向 prev->next->next

(3)最后得到删除的链表。

示例代码

ListNode* removeNode(ListNode* head, int pos) {// 链表为空if (head == nullptr) {	return nullptr;}// 删除的节点是非法的int N = getN(head);	// 获取链表长度if (pos < 0 || pos > N) {return head;}// 情况三ListNode* preNode = head;int i = 1;while (i < pos-1) {		// 找到第 pos 个节点后提出循环preNode = preNode->next;++i;}// 删除头节点if (preNode == head) { return preNode->next;}// 删除链表中间或尾部的节点preNode->next = preNode->next->next;return head;
}

修改节点

修改主要指的是修改节点的值。比如将第 i 个节点的值修改为指定值 val。思路清晰直接看代码:

void modifyNthVal(ListNode* head, int n, int val) {if (head == nullptr) {return;}int N = getN(head);	// 获取链表长度if (n < 0 || n > N) {	// 越界return;}ListNode* cur = head;while (--n) {cur = cur->next;	}cur->val = val;
}

参考资料

【书籍】大话数据结构

【文章】一文讲透链表操作,看完你也能轻松写出正确的链表代码

【文章】链表基础知识


写在最后

如果您发现文章有任何错误或者对文章有任何疑问,欢迎私信博主或者在评论区指出 💬💬💬。

如果大家觉得有些地方需要补充,欢迎评论区交流。

next;
}
cur->val = val;
}


# 参考资料【书籍】大话数据结构【文章】[一文讲透链表操作,看完你也能轻松写出正确的链表代码](https://www.cnblogs.com/lonely-wolf/p/15761239.html)【文章】[链表基础知识](https://algo.itcharge.cn/02.Linked-List/01.Linked-List-Basic/01.Linked-List-Basic/)---# 写在最后如果您发现文章有任何错误或者对文章有任何疑问,欢迎私信博主或者在评论区指出 💬💬💬。如果大家觉得有些地方需要补充,欢迎评论区交流。最后,感谢您的阅读,如果有所收获的话可以给我点一个 👍 哦。

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

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

相关文章

互联网搞钱大变天,这有几条活路

互联网搞钱大变天&#xff0c;这有几条活路 靠互联网营生的各位同胞&#xff0c;你们有没有想过这样一个问题&#xff1a;假如有一天你的自媒体账号全被封了&#xff0c;你手上的操作项目全都黄了&#xff0c;你会怎么办&#xff1f; 就封号这事在这几年相信大家都不会陌生&a…

【LLM第五篇】名词解释:prompt

1.是什么 提示工程&#xff08;Prompt Engineering&#xff09;是一门较新的学科&#xff0c;关注提示词开发和优化&#xff0c;帮助用户将大语言模型&#xff08;Large Language Model, LLM&#xff09;用于各场景和研究领域。 掌握了提示工程相关技能将有助于用户更好地了解…

深入了解 npm:Node.js 包管理工具详解

文章目录 一、npm 基本概念1.1 什么是 npm&#xff1f;1.2 package.json 文件 二、npm 常用命令2.1 初始化项目2.2 安装依赖2.2.1 安装单个包2.2.2 全局安装包2.2.3 安装开发依赖 2.3 移除依赖2.4 更新依赖2.5 查看已安装的包2.6 发布包 三、npm 高级用法3.1 使用 npm scripts3…

JETBRAINS IDES 分享一个2099通用试用码!CLion 2024 版 ,支持一键升级

文章目录 废话不多说上教程&#xff1a;&#xff08;动画教程 图文教程&#xff09;一、动画教程激活 与 升级&#xff08;至最新版本&#xff09; 二、图文教程 &#xff08;推荐&#xff09;Stage 1.下载安装 toolbox-app&#xff08;全家桶管理工具&#xff09;Stage 2 : 下…

nestJs中跨库查询

app.module.ts中配置 模块的module中 注意实体类在写的时候和数据库中的表名一样 service中使用一下

【JS】call和 apply函数的详解

JavaScript 中 call() 和 apply() 函数的详解 在JavaScript中&#xff0c;call()和apply()都是非常重要的方法&#xff0c;用于调用函数时指定函数体内的this的值&#xff0c;从而实现不同对象之间的方法共享。尽管它们的功能非常相似&#xff0c;但在实际使用中各有其优势和特…

生产环境磁盘变更方案

datanode磁盘扩容(1人天) 扩容磁盘 1.1 扩容前检查、新盘初始化 确定block副本统计正常,无丢块,无under-replicated 块 后台执行命令检查 hdfs dfs fsck / 无异常可进行以下步骤进行磁盘扩容 新盘初始化 ##格式化新盘,命令示例: mkfs.xfs /dev/sdc##挂载新盘,命令示…

【AI学习】聊两句昨夜OpenAI的GPT-4o

蹭个热点&#xff0c;聊两句昨夜的大事件——OpenAI发布GPT-4o&#xff0c;我看到和想到的一点东西。 首先是端到端方法&#xff0c;前面关于深度学习的文章&#xff0c;对端到端的重要性做了一些学习&#xff0c;对端到端这个概念有了一些理解。正如Richard Sutton在《苦涩的…

大数据Scala教程从入门到精通第六篇:Scala源文件编写和运行

一&#xff1a;Scala源文件编写和运行 1&#xff1a;源代码比较 public class HelloJava{public static void main(String[] args){System.out.println("hello scala")} } object HelloScala{//用于声明方法 入参是一个String类型的数组。返回值类型为空def main…

鸿蒙内核源码分析(gn应用篇) | gn语法及在鸿蒙的使用

gn是什么? gn 存在的意义是为了生成 ninja,如果熟悉前端开发,二者关系很像 Sass和CSS的关系. 为什么会有gn,说是有个叫even的谷歌负责构建系统的工程师在使用传统的makefile构建chrome时觉得太麻烦,不高效,所以设计了一套更简单,更高效新的构建工具gnninja,然后就被广泛的使用…

【docker】SpringBoot应用容器镜像日志挂载

启动镜像时候使用 -v 挂载 首先得在宿主机创建目录&#xff1a;/workspace/java/demo/logs mkdir -pv /workspace/java/demo/logs 启动镜像 docker run -p 8080:8080 -itd -v /workspace/java/demo/logs/:/logs/ 192.168.2.1:5000/demo:0.0.1-SNAPSHOT -v /workspace/ja…

【Shell脚本】Shell编程之数组

目录 一.数组 1.基本概念 2.定义数组的方法 2.1.方法一 2.2.方法二 2.3.方法三 2.4.方法四 2.5.查看数组长度 2.6.查看数组元素下标 3.数组分片 4.数组字符替换 4.1.临时替换 4.2.永久替换 5.数组删除 5.1.删除某个下标 5.2.删除整组 6.数组遍历和重新定义 7…

SpringBoot自动装配(二)

近日&#xff0c;余溺于先贤古哲之文无法自拔。虽未明其中真意&#xff0c;但总觉有理。遂抄录一篇以供诸君品鉴——公孙鞅曰&#xff1a;“臣闻之&#xff1a;‘疑行无名&#xff0c;疑事无功。’君亟定变法之虑&#xff0c;殆无顾天下之议之也。且夫有高人之行者&#xff0c;…

【备忘】Unable to stop the ctxapinject driver services

【问题】一台通过Autopilot部署的Windows11系统&#xff0c;在公司门户里看到 策略推送的Citrix workspace不成功&#xff0c;尝试手动安装&#xff0c;报错&#xff1a; 【解决】网上看了半天没找出个所以然来&#xff0c;尝试sc queryex ctxapinject 查到对应的进程ID是0&…

【JavaWeb】Day77.Spring——SpringBoot原理(一)

SpringBoot原理 Spring是目前世界上最流行的Java框架&#xff0c;它可以帮助我们更加快速、更加容易的来构建Java项目。而在Spring家族当中提供了很多优秀的框架&#xff0c;而所有的框架都是基于一个基础框架的SpringFramework(也就是Spring框架)。而如果我们直接基于Spring框…

MySQL企业级开发重点之事物和索引

事物 -- 解散学工部 delete from tb_dept where id 1;-- 删除部门下的员工 delete from tb_emp where dept_id 1; 介绍和操作 我们应该将两个语句写成一个语句 -- 开启事物 start transaction ;-- 解散学工部 delete from tb_dept where id 3;-- 删除部门下的员工 delete fr…

Ajax 学习

文章目录 1. 前置知识1.1 ajax 介绍1.2 XML 简介 2. AJAX 学习2.1 AJAX基础学习&#xff08;1&#xff09;AJAX的特点&#xff08;2&#xff09;AJAX 初体验&#xff08;3&#xff09;服务端响应json 数据 2.2 IE 缓存问题2.3 请求超时和网络异常2.4 手动取消请求2.5 重复请求2…

流畅的python-学习笔记_序列修改+散列+切片

vector第一版 reprlib.repr用于选取有限长度较长变量 vector第二版切片 注意切片还有indices属性&#xff0c;它可以入参一个序列长度&#xff0c;根据此序列长度&#xff0c;转化不规矩的start stop stride&#xff0c; vector第三版动态存取属性 obj.attra时&#xff0c;先…

Day 46 139.单词拆分

单词拆分 给定一个非空字符串 s 和一个包含非空单词的列表 wordDict&#xff0c;判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。 说明&#xff1a; 拆分时可以重复使用字典中的单词。 你可以假设字典中没有重复的单词。 示例 1&#xff1a; 输入: s “leet…

【35分钟掌握金融风控策略26】定价策略

目录 定价策略 定价策略的开发、部署、监控和调优 定价策略开发 定价策略部署 定价策略监控 定价策略调优 定价策略 定价是对授信审批通过的客户给予合适利率的过程。如何定价、定价多少是由定价策略来决定的。定价策略的制订要遵循“收益覆盖风险”原则&#xff0c;对于…