【数据结构】顺序表和链表——链表(包含大量经典链表算法题)

ops/2024/9/23 2:27:19/

文章目录

1. 单链表

1.1 概念与结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

在这里插入图片描述

淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只需要将火车里的某节车厢去掉/加上,不会影响其他车厢,每节车厢都是独立存在的。

链表里,每节“车厢”是什么样的呢?

在这里插入图片描述

1.1.1 结点

与顺序表不同的是,链表里的每节"车厢"都是独立申请下来的空间,我们称之为“结点/节点

结点的组成主要有两个部分:当前结点要保存的数据和保存下一个结点的地址(指针变量)

图中指针变量plist保存的是第一个结点的地址,我们称plist此时“指向”第一个结点,如果我们希望plist“指向”第二个结点时,只需要修改plist保存的内容为0x0012FFA0即可。

链表中每个结点都是独立申请的(即需要插入数据时才去申请一块结点的空间),我们需要通过指针变量来保存下一个结点位置才能从当前结点找到下一个结点。

1.1.2 链表的性质

1、链式结构在逻辑上是连续的,在物理结构上不⼀定连续

2、结点一般是从上申请的

3、从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能连续,可能不连续

结合前面学到的结构体知识,我们可以给出每个结点对应的结构体代码:

假设当前保存的结点为整型:

struct SListNode
{int data; //结点数据struct SListNode* next; //指针变量⽤保存下⼀个结点的地址
};

当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要保存整型数据,也需要保存下⼀个结点的地址(当下⼀个结点为空时保存的地址为空)。

当我们想要从第一个结点走到最后一个结点时,只需要在当前结点拿上下一个结点的地址就可以了。

1.1.3 链表的打印

给定的链表结构中,如何实现结点从头到尾的打印?

在这里插入图片描述

思考:当我们想保存的数据类型为字符型、浮点型或者其他⾃定义的类型时,该如何修改?

1.2 实现单链表

SList.h

typedef int SLTDataType;
typedef struct SListNode
{SLTDataType data; //结点数据struct SListNode* next; //指针保存下一个结点的地址
}SLTNode;void SLTPrint(SLTNode* phead);
//头部插入删除/尾部插入删除
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);//查找
SLTNode * SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插⼊数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDestroy(SLTNode** pphead);

上面这部分是链表实现所需要的节点结构和一些方法,其中具体的方法由读者自己先来尝试实现,如有不会的可以在讨论区询问,将会由作者或者其它积极的读者来解答❤️❤️❤️

1.3 链表的分类

链表的结构非常多样,以下情况组合起来就有8种(2 x 2 x 2)链表结构:

在这里插入图片描述

链表说明:

在这里插入图片描述

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:链表和双向链表

  1. 无头单向非循环链表(俗称:单链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

  2. 带头双向循环链表(俗称:双向链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

1.4 单链表算法

1.4.1 移除链表元素

https://leetcode.cn/problems/remove-linked-list-elements/description/

OJ代码有bug怎么办?VS调试技能用起来

(1)将OJ代码复制粘贴到vs上

在这里插入图片描述

(2)创建测试方法,调用本次要调试的目标方法

在这里插入图片描述

(3)利用vs调试工具排查代码问题

在这里插入图片描述

1.4.2 反转链表

https://leetcode.cn/problems/reverse-linked-list/description/

1.4.3 链表的中间结点

https://leetcode.cn/problems/middle-of-the-linked-list/description/

1.4.4 合并两个有序链表

https://leetcode.cn/problems/merge-two-sorted-lists/description/

1.4.5 链表分割

https://www.nowcoder.com/practice/0e27e0b064de4eacac178676ef9c9d70

1.4.6 链表的回文结构

https://www.nowcoder.com/practice/d281619e4b3e4a60a2cc66ea32855bfa

1.4.7 相交链表

https://leetcode.cn/problems/intersection-of-two-linked-lists/description/

在这里插入图片描述

1.4.8 环形链表1

https://leetcode.cn/problems/linked-list-cycle/description/

💡 快慢指针

快慢指针,即慢指针一次走一步,快指针一次走两步,两个指针从链表起始位置开始运行,如果链表带环则一定会在环中相遇,否则快指针率先走到链表的未尾

思考1:为什么快指针每次走两步,慢指针走一步可以相遇,有没有可能遇不上,请推理证明!

在这里插入图片描述

slow一次走一步,fast一次走2步,fast先进环,假设slow也走完入环前的距离,准备进环,此时fast和slow之间的距离为N,接下来的追逐过程中,每追击一次,他们之间的距离缩小1步

追击过程中fast和slow之间的距离变化:

在这里插入图片描述

因此,在带环链表中慢指针走一步,快指针走两步最终一定会相遇。

思考2:快指针一次走3步,走4步,…n步行吗?

step1:

按照上面的分析,假设慢指针每次走一步,快指针每次走三步,此时快慢指针的最大距离为N,接下来的追逐过程中,每追击一次,他们之间的距离缩小2步

追击过程中fast和slow之间的距离变化:

在这里插入图片描述

分析:

(1)如果N是偶数,第一轮就追上了

(2)如果N是奇数,第一轮追不上,快追上,错过了,距离变成-1,即C-1,进入新的一轮追击

​ a、C-1如果是偶数,那么下一轮就追上了

​ b、C-1如果是奇数, 那么就永远都追不上

总结一下追不上的前提条件: N是奇数,C是偶数

但是你以为就是这样吗?请看下面进一步分析

step2:

在这里插入图片描述

假设:

环的周长为C,头结点到slow结点的长度为L,slow走一步,fast走三步,当slow指针入环后,

slow和fast指针在环中开始进行追逐,假设此时fast指针已经绕环x周。

在追逐过程中,快慢指针相遇时所走的路径长度:

fast: L + x ∗ C + C − N L+x*C+C-N L+xC+CN

slow: L L L

由于慢指针走一步,快指针要走三步,因此得出: 3 ∗ 慢指针路程 = 快指针路程 3 * 慢指针路程 = 快指针路程 3慢指针路程=快指针路程,即:

3 L = L + x ∗ C + C − N 3L = L + x*C + C − N 3L=L+xC+CN

化简得: 2 L = ( x + 1 ) C − N 2L = (x + 1)C − N 2L=(x+1)CN

对上述公式继续分析:由于偶数乘以任何数都为偶数,因此 2 L 2L 2L 一定为偶数,则可推导出这个可能的情况:

  • 情况1:偶数 = 偶数 - 偶数

  • 情况2:偶数 = 奇数 - 奇数

由step1中(1)得出的结论,如果N是偶数,则第一圈快慢指针就相遇了。

由step1中(2)得出的结论,如果N是奇数,则fast指针和slow指针在第一轮的时候套圈了,开始进行下一轮的追逐;当N是奇数,要满足以上的公式,则 ( x + 1 ) C (x+1)C (x+1)C 必须也要为奇数

  • 其中如果C为偶数,则这里的 ( x + 1 ) C (x+1)C (x+1)C 为偶数,条件不满足,则step1中(2)b的C-1为奇数不存在

  • 如果C为奇数,满足这里的 ( x + 1 ) C (x+1)C (x+1)C 为奇数且符合step1中(2)a的C-1为偶数的结论,则快慢指针会相遇

因此, step1中的 N是奇数,C是偶数不成立 ,既然不存在该情况,则快指针一次走3步最终一定也可以相遇。

快指针一次走4、5…步最终也会相遇,其证明方式同上。

typedef struct ListNode ListNode;
bool hasCycle(struct ListNode* head) {ListNode * slow, * fast;slow = fast = head;while (fast && fast->next){slow = slow->next;int n = 3; //fast每次⾛三步while (n--){if (fast->next)fast = fast->next;elsereturn false;}if (slow == fast){return true;}}return false;
}

💡 提示

虽然已经证明了快指针不论走多少步都可以满足在带环链表中相遇,但是在编写代码的时候选择其它的步数方式会有额外的步骤引入,所以涉及到快慢指针的算法题中通常习惯使用慢指针走一步快指针走两步的方式。

1.4.9 环形链表2

https://leetcode.cn/problems/linked-list-cycle-ii/description/

💡 结论

让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时快慢指针相遇点的位置开始绕环运行,两个指针都是每次均走一步,最终肯定会在入口点的位置相遇

证明以上结论

在这里插入图片描述

说明:

​ H为链表的起始点,E为环入口点,M与判环时候相遇点。

假设:

​ 环的长度为R,H到E的距离为L,E到M的距离为 X ,则:M到E的距离为 R-X。

在判环时,快慢指针相遇时所走的路径长度:

fast: L + X + n R L+X + nR L+X+nR

slow: L + X L+X L+X

注意:

1.当慢指针进入环时,快指针可能已经在环中绕了n圈了,n至少为1

​ 因为:快指针先进环走到M的位置,最后又在M的位置与慢指针相遇

2.慢指针进环之后,快指针肯定会在慢指针走一圈之内追上慢指针

​ 因为:慢指针进环后,快慢指针之间的距离最多就是环的长度,而两个指针在移动时,每次它们至少距离都缩减一步,因此在慢指针移动一圈之前快指针肯定是可以追上慢指针的,而快指针速度是慢指针的两倍,因此有如下关系是:

2 ∗ ( L + X ) = L + X + n R 2 * (L+X)=L+X+nR 2(L+X)=L+X+nR

—> L + X = n R L+X=nR L+X=nR

—> L = n R − X L=nR-X L=nRX

—> L = ( n − 1 ) R + ( R − X ) L = (n-1)R+(R-X) L=(n1)R+(RX)

(n为1,2,3,4…,n的大小取决于环的大小,环越小n越大)

极端情况下,假设n=1,此时: L=R-X

即:一个指针从链表起始位置运行,一个指针从相遇点位置绕环,每次都走一步,两个指针最终会在入口点的位置相遇。

1.4.10 随机链表的复制

https://leetcode.cn/problems/copy-list-with-random-pointer/description/

更多链表算法刷题入口:

牛客网:https://www.nowcoder.com/exam/oj

LeetCode:https://leetcode.cn/problems/copy-list-with-random-pointer/description/

2. 双向链表

2.1 概念与结构

在这里插入图片描述

💡注意

这里的“带头”跟前面我们说的“头结点”是两个概念,实际前面的在单链表阶段称呼不严谨,但是为了同学们更好的理解就直接称为单链表的头结点。

带头链表里的头结点,实际为“哨兵位”,哨兵位结点不存储任何有效元素,只是站在这里“放哨的”

2.2 实现双向链表

List.h

typedef int LTDataType;
typedef struct ListNode
{struct ListNode* next; //指针保存下⼀个结点的地址struct ListNode* prev; //指针保存前⼀个结点的地址LTDataType data;}LTNode;//void LTInit(LTNode** pphead);
LTNode * LTInit();
void LTDestroy(LTNode* phead);
void LTPrint(LTNode* phead);
bool LTEmpty(LTNode* phead);
void LTPushBack(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);
void LTPushFront(LTNode* phead, LTDataType x);
void LTPopFront(LTNode* phead);
//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x);
void LTErase(LTNode* pos);
LTNode* LTFind(LTNode* phead, LTDataType x);

上面这部分是链表实现所需要的节点结构和一些方法,其中具体的方法由读者自己先来尝试实现,如有不会的可以在讨论区询问,将会由作者或者其它积极的读者来解答❤️❤️❤️

3. 顺序表与链表的对比分析

不同点顺序表链表(单链表
存储空间上物理上一定连续逻辑上连续,但物理上不一定连续
随机访问支持:O(1)不支持:O(N)
任意位置插入或者删除元素可能需要搬移元素,效率低O(N)只需修改指针指向
插入动态顺序表,空间不够时需要扩容和空间浪费没有容量的概念,按需申请释放,不存在空间浪费
应用场景元素高效存储+频繁访问任意位置高效插入和删除

http://www.ppmy.cn/ops/105029.html

相关文章

算法的学习笔记—把数组排成最小的数(牛客JZ45)

😀前言 在编程面试中,经常会遇到需要将问题转化为排序问题的题目。这些问题看似复杂,但只要抓住核心思路,便能迅速解决。今天我们就来看一道这样的题目:如何将一个非负整数数组拼接成最小的数字。 🏠个人主…

RTC(实时时钟)/BKP(备份寄存器

1 unix时间戳 2 时间戳转换函数 3 BKP(备份寄存器) 1 TAMPER引脚侵入事件 2 RTC校准时间 3 RST闹钟脉冲和秒脉冲 可以输出出来为其他信号提供 4 校准时钟,寄存器加输出RTC校准时钟 5 总结:3个功能只能同时使用一个 4 BKP基本…

将python项目打包成一个可执行文件(包含需要的资源文件)

目标 项目源码是采用Python编写,代码中需要读取部分资源文件。现在需要将项目打包成一个exe文件,没有其他任何多余文件,仅1个exe文件。 打包 安装pyinstaller 在自己项目的虚拟环境中,安装pyinstaller。注意一定要是虚拟环境&…

每天一个数据分析题(五百一十五)- 朴素贝叶斯分类器

朴素贝叶斯分类器是一系列以假设特征之间强(朴素)独立下运用贝叶斯定理为基础的简单概率分类器。该分类器模型会给问题实例分配用特征值表示的类标签,类标签取自有限集合。下列选项不属于朴素贝叶斯分类器特点的是? A. 面对孤立的…

3.2 Browser -- useColorMode

3.2 Browser – useColorMode https://vueuse.org/core/useColorMode/ 作用 可以自动持久化的响应式颜色主题模式。 官方示例 import { useColorMode } from vueuse/coreconst mode useColorMode() // Ref<dark | light>默认情况下&#xff0c;它将使用usePreferre…

Spel注入漏洞分析

文章目录 SPEL注入SPEL漏洞案例CVE-2022-2297EXP SPEL注入 SPEL是Spring框架中的一种表达式语言&#xff0c;用于在Spring配置中动态计算值。它提供了一种简洁的语法&#xff0c;用于访问和操作对象的属性、调用方法、进行数学计算、逻辑运算等。允许开发者在Spring应用程序中…

Java基于微信小程序的超市购物管理系统

1 简介 Java基于微信小程序的超市购物管理系统&#xff0c;此超市购物系统利用当下成熟完善的springboot框架&#xff0c;使用跨平台的可开发大型商业网站的Java语言&#xff0c;以及最受欢迎的RDBMS应用软件之一的Mysql数据库进行程序开发。实现了收货地址管理、购物车管理、…

JVM(Java虚拟机)

Java的“一次编写&#xff0c;处处运行”主要得益于Java的设计理念和Java虚拟机&#xff08;JVM&#xff09;的存在。 JVM&#xff08;Java Virtual Machine&#xff09;是Java虚拟机的简称&#xff0c;它是运行所有Java程序的抽象计算机。也就是说&#xff0c;JVM是一个能够运…