【基本数据结构】链表

文章目录

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

前言

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

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

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

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


链表

简介

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

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

链表-链表.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.hqwc.cn/news/701537.html

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

相关文章

初识C语言——第二十天

do while ()循环 do 循环语句; while(表达式); 句式结构&#xff1a; 执行过程&#xff1a; do while循环的特点&#xff1a; 代码练习&#xff1a; 二分法算法&#xff1a; int main() {int arr[] { 0,1,2,3,4,5,6,7,8,9};int k 7;//查找数字7&#xff0c;在arr这个数组…

【iOS】——RunLoop学习

文章目录 一、RunLoop简介1.RunLoop介绍2.RunLoop功能3.RunLoop使用场景4.Run Loop 与线程5.RunLoop源代码和模型图 二、RunLoop Mode1.CFRunLoopModeRef2.RunLoop Mode的五种模式3.RunLoop Mode使用 三、RunLoop Source1.CFRunLoopSourceRefsourc0&#xff1a;source1: 2.CFRu…

利用AI创建MYsol存储过程

DDLDML CREATE TABLE student (id INT AUTO_INCREMENT PRIMARY KEY,createDate DATETIME NOT NULL,userName VARCHAR(255) NOT NULL,phone VARCHAR(20) NOT NULL,age INT NOT NULL,sex ENUM(男, 女, 其他) NOT NULL,introduce TEXT ); INSERT INTO student (createDate, userN…

JavaScript异步编程——11-异常处理方案【万字长文,感谢支持】

异常处理方案 在JS开发中&#xff0c;处理异常包括两步&#xff1a;先抛出异常&#xff0c;然后捕获异常。 为什么要做异常处理 异常处理非常重要&#xff0c;至少有以下几个原因&#xff1a; 防止程序报错甚至停止运行&#xff1a;当代码执行过程中发生错误或异常时&#x…

冷风机厂家电话,为什么车间降温要用冷风机?

冷风机厂家电话&#xff0c;为什么车间降温要用冷风机&#xff1f; 夏季车间高温很多工厂想降温&#xff0c;采用原来的传统空调费用高耗能高&#xff0c;很多工厂是用不起的&#xff0c;那么采用蒸发市冷风机怎么样&#xff1f; 冷风机是很过对于为什么冷风机比空调更适合车…

默认成员函数:析构、深浅拷贝

析构函数 析构函数&#xff1a;与构造函数功能相反&#xff0c;析构函数不是完成对对象本身的销毁&#xff0c;局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数&#xff0c;完成对象中资源的清理工作。 特性 析构函数名时在类名前面加上字符~ class D…

【微命令】git config如何配置全局的用户和邮箱?(--global user.name、user.email;git config --help)

虽然经常用&#xff0c;也经常忘记&#xff0c;特此记录。 命令 git config --global user.name "myname" git config --global user.email test163.com另外一种方式 help git config --help |grep email | grep name直接help查看

ICode国际青少年编程竞赛- Python-5级训练场-多参数函数

ICode国际青少年编程竞赛- Python-5级训练场-多参数函数 1、 def go(a, b):Spaceship.step(2)Dev.step(a)Spaceship.step(b)Dev.turnRight()Dev.step(b)Dev.turnLeft()Dev.step(-a) Dev.turnLeft() Dev.step(3) Dev.step(-3) go(3, 2) go(6, 1) go(5, 2) go(4, 3)2、 def go(…

【已解决】力扣打不开

表现&#xff1a; 1.访问国内其他网站都没有问题 2.访问github也能成功 3.wifi没有问题 4.连接同网络的其他主机能打开 唯独力扣打不开&#xff0c;可能是DNS解析错误 》自己网络配置问题 解决办法【亲测可行】 找可用的hosts 打开站长之家&#xff0c;进行DNS查询&#xff…

树莓派|串口通信协议

1、串口通信原理 串口通讯(Serial Communication)&#xff0c;是指外设和计算机间&#xff0c;通过数据信号线、地线等&#xff0c;按位进行传输数据的一种通讯方式。串口是一种接口标准&#xff0c;它规定了接口的电气标准&#xff0c;没有规定接口插件电缆以及使用的协议。串…

C++基础与深度解析 | 表达式 | 操作符

文章目录 一、表达式基础1.表达式的值类别2.表达式的类型转换 二、表达式详述1.算术操作符2.逻辑与关系操作符3.位操作符4.赋值操作符5.自增与自减运算符6.其他操作符三、C17对表达式的求值顺序的限定 一、表达式基础 表达式由一到多个操作数组成&#xff0c;可以求值并 ( 通常…

CKA-Ubuntu18.04安装Kubernetes集群

文档整理参考:虫之教育唐老师 文章目录 K8S是什么修改静态ip环境准备修改更新源安装Docker安装K8S-master1.安装kubeadm, kubelet, kubectl2.初始化3.创建kubeadm-config.yaml4.查看是否安装成功运行集群环境报错排查问题安装网络安装K8S-node1,2步参考master3.查看是否安装成…