算法工程师重生之第三天( 链表理论基础 移除链表元素 设计链表 反转链表 )

ops/2024/12/22 9:32:32/

参考文献 代码随想录

一、 链表理论基础

什么是链表链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。

链表的入口节点称为链表的头结点也就是head。

如图所示: 

<a class=链表1" height="360" src="https://img-blog.csdnimg.cn/img_convert/b650f0707faafca136b1f90f2a708552.png" width="1118" />

链表的类型">#链表的类型

接下来说一下链表的几种类型:

链表">#单链表

刚刚说的就是单链表

链表">#双链表

链表中的指针域只能指向节点的下一个节点。

链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。

链表 既可以向前查询也可以向后查询。

如图所示: 

<a class=链表2" height="276" src="https://img-blog.csdnimg.cn/img_convert/13448d51fdf8ba6003405906a3a5399c.png" width="1192" />

链表">#循环链表

循环链表,顾名思义,就是链表首尾相连。

循环链表可以用来解决约瑟夫环问题。

<a class=链表4" height="440" src="https://img-blog.csdnimg.cn/img_convert/a277f4c866ed4e5b91d60bbe8580de52.png" width="514" />

链表的存储方式">#链表的存储方式

了解完链表的类型,再来说一说链表在内存中的存储方式。

数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。

链表是通过指针域的指针链接在内存中各个节点。

所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。

如图所示:

<a class=链表3" height="348" src="https://img-blog.csdnimg.cn/img_convert/78d6c9e69a215b51d0056ad8d3aab5bf.png" width="525" />

这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。

链表的操作">链表的操作

#删除节点

删除D节点,如图所示:

<a class=链表-删除节点" height="308" src="https://img-blog.csdnimg.cn/img_convert/943aa425f1a7fa195d70e57db6af9338.png" width="1132" />

只要将C节点的next指针 指向E节点就可以了。

那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。

是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。

其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。

#添加节点

如图所示:

<a class=链表-添加节点" height="420" src="https://img-blog.csdnimg.cn/img_convert/7131c739b0212f48742f34dd26f3e888.png" width="1122" />

可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。

但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。

#性能分析

再把链表的特性和数组的特性进行一个对比,如图所示:

<a class=链表-链表与数据性能对比" height="400" src="https://img-blog.csdnimg.cn/img_convert/b7cccc73fd29df4f0e9e2a8e68e60ac8.png" width="984" />

数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。

链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。

二、移除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

示例 1:

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

示例 2:

输入:head = [], val = 1
输出:[]

示例 3:

输入:head = [7,7,7,7], val = 7
输出:[]

提示:

  • 列表中的节点数目在范围 [0, 104] 内
  • 1 <= Node.val <= 50
  • 0 <= val <= 50

问题分析

        注意事项

                1. 不能直接操作虚拟头结点,如果操作,那么头节点会不断的被更改掉,必须定义一个临时的变量指向虚拟头结点

                2. 使用while判断时,要保证,操作的指针不能为空,那为什么这里可以使用if呢,首先有虚拟头头结点的存在,其次,if和else只要满足一个,那么他就会回到while里,所以也可以使用if,相当于这样的链表的时候[7,7,7],存在了虚拟头结点,那么此时链表就变成了[0,7,7,7],如果cur.next.val == val,则需要删除,那么就不会执行else,直接到上一层的while循环,再次判断cur.next.val == val是否相等,所以可以使用if判断。

while版本

           

# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution(object):def removeElements(self, head, val):""":type head: ListNode:type val: int:rtype: ListNode"""# 不为空,寻找目标值# 使用虚拟头结点,便于操作(针对于头节点等于val)dummy_head  = ListNode(next = head)cur = dummy_head  # 不能直接操作dummy_head, 如果你操作这个,那么原理的结构就会发生改变while cur != None and cur.next != None:  # 如果下一个元素存在,那么就要往下,遍历,寻找等于val的值,并删除while cur.next != None and cur.next.val == val :cur.next = cur.next.nextelse:cur = cur.nextreturn dummy_head.next

if版本

# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution(object):def removeElements(self, head, val):""":type head: ListNode:type val: int:rtype: ListNode"""# 不为空,寻找目标值# 使用虚拟头结点,便于操作(针对于头节点等于val)dummy_head  = ListNode(next = head)cur = dummy_head  # 不能直接操作dummy_head, 如果你操作这个,那么原理的结构就会发生改变while cur.next:  # 如果下一个元素存在,那么就要往下,遍历,寻找等于val的值,并删除if cur.next.val == val :cur.next = cur.next.nextelse:cur = cur.nextreturn dummy_head.next

三、设计链表

你可以选择使用单链表或者双链表,设计并实现自己的链表

链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。

如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

实现 MyLinkedList 类:

  • MyLinkedList() 初始化 MyLinkedList 对象。
  • int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
  • void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
  • void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
  • void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
  • void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。

示例:

输入
["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
[[], [1], [3], [1, 2], [1], [1], [1]]
输出
[null, null, null, null, 2, null, 3]解释
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addAtHead(1);
myLinkedList.addAtTail(3);
myLinkedList.addAtIndex(1, 2);    // 链表变为 1->2->3
myLinkedList.get(1);              // 返回 2
myLinkedList.deleteAtIndex(1);    // 现在,链表变为 1->3
myLinkedList.get(1);              // 返回 3

提示:

  • 0 <= index, val <= 1000
  • 请不要使用内置的 LinkedList 库。
  • 调用 getaddAtHeadaddAtTailaddAtIndex 和 deleteAtIndex 的次数不超过 2000 。

问题分析

删除链表节点: 

<a class=链表-删除节点" height="308" src="https://img-blog.csdnimg.cn/img_convert/452b95e71e29f7370318469e321c5c45.png" width="1132" />

添加链表节点: 

<a class=链表-添加节点" height="420" src="https://img-blog.csdnimg.cn/img_convert/6416323c1ba909084a1397ad51399139.png" width="1122" />

这道题目设计链表的五个接口:

  • 获取链表第index个节点的数值
  • 链表的最前面插入一个节点
  • 链表的最后面插入一个节点
  • 链表第index个节点前面插入一个节点
  • 删除链表的第index个节点

可以说这五个接口,已经覆盖了链表的常见操作,是练习链表操作非常好的一道题目

链表操作的两种方式:

  1. 直接使用原来的链表来进行操作。
  2. 设置一个虚拟头结点在进行操作。
class ListNode:def __init__(self, val=0, next=None):self.val = valself.next = nextclass MyLinkedList(object):def __init__(self):self.dummy_head = ListNode()  # 初始化虚拟节点self.size = 0  # 记录链表的长度def get(self, index):""":type index: int:rtype: int"""if index < 0 or index >= self.size:return -1cur = self.dummy_head.nextfor i in range(index):  # 举例一个极端,那么添加到下标为0时候是否为正确cur = cur.nextreturn cur.valdef addAtHead(self, val):""":type val: int:rtype: None"""self.dummy_head.next = ListNode(val, self.dummy_head.next)self.size += 1def addAtTail(self, val):""":type val: int:rtype: None"""cur = self.dummy_headwhile cur.next:cur = cur.nextcur.next = ListNode(val)self.size += 1def addAtIndex(self, index, val):""":type index: int:type val: int:rtype: None"""if index < 0 or index > self.size:returncur = self.dummy_headfor i in range(index):cur = cur.nextcur.next = ListNode(val, cur.next)self.size += 1def deleteAtIndex(self, index):""":type index: int:rtype: None"""if index < 0 or index >= self.size:returncur = self.dummy_headfor i in range(index):cur = cur.nextcur.next = cur.next.nextself.size -= 1# Your MyLinkedList object will be instantiated and called as such:
# obj = MyLinkedList()
# param_1 = obj.get(index)
# obj.addAtHead(val)
# obj.addAtTail(val)
# obj.addAtIndex(index,val)
# obj.deleteAtIndex(index)

四、反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表

示例 1:

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例 2:

输入:head = [1,2]
输出:[2,1]

示例 3:

输入:head = []
输出:[]

提示:

  • 链表中节点的数目范围是 [0, 5000]
  • -5000 <= Node.val <= 5000

如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。

其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:

206_反转<a class=链表" height="398" src="https://img-blog.csdnimg.cn/img_convert/df463ca9d9d0fc4cdaa6cb945f7323a2.png" width="1028" />

之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改变next指针的方向。

那么接下来看一看是如何反转的呢?

我们拿有示例中的链表来举例,如动画所示:(纠正:动画应该是先移动pre,在移动cur)

首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。

然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。

为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。

接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。

最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。

双指针

        声明一个空指针,指向None,另外一个指针指向head,因为要使用cur来接受head,不能直接操作在head,所以需要判断是否为空指针,即while cur,翻转,那么就是每个节点的下一给都指向上一个节点,那么就需要存储,cur的下一个,如果你存,那么等下你,操作cur.next指向空指针prev的话,那么原来链表的元素就找不到了,所以需要使用一个临时变量存储,然后在做next方向的改变,然后就要移动这个2个指针,使prev指针指向cur, cur指向tmp。

class Solution(object):def reverseList(self, head):""":type head: ListNode:rtype: ListNode"""prev = None  # cur = headwhile cur:tmp = cur.next cur.next = prevprev = curcur = tmpreturn prev

迭代

递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。

关键是初始化的地方,可能有的同学会不理解, 可以看到双指针法中初始化 cur = head,pre = NULL,在递归法中可以从如下代码看出初始化的逻辑也是一样的,只不过写法变了。

class Solution:def reverseList(self, head: ListNode) -> ListNode:return self.reverse(head, None)def reverse(self, cur: ListNode, pre: ListNode) -> ListNode:if cur == None:return pretemp = cur.nextcur.next = prereturn self.reverse(temp, cur)


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

相关文章

SQL进阶技巧:如何利用SQL解决趣味赛马问题?| 非等值关联匹配问题

目录 0 问题描述 1 数据准备 2 问题分析 方法一:先分后合思想 方法2:非等值关联匹配 3 小结 0 问题描述 有一张赛马记录表,如下所示: create table RacingResults ( trace_id char(3) not null,race_date date not null, race_nbr int not null,win_name char(30) n…

Codeforces Round 969 (Div. 2)

文章目录 A. Doras Set题意&#xff1a;思路&#xff1a;代码 B. Index and Maximum Value题意&#xff1a;思路&#xff1a;代码&#xff1a; C. Dora and C题意&#xff1a;思路&#xff1a;代码&#xff1a; 题目链接 A. Dora’s Set 题意&#xff1a; 给你一个区间&#…

docker 构建最小镜像 - 2MB 不到

文章目录 1.编译 code2.编写 Dockerfile3.构建镜像4.运行起来5.总结 1.编译 code rootjn:/home/jn/Desktop/Docker# cat hello.go package mainimport ("fmt""time" )func main() {for {fmt.Println("hello~")time.Sleep(time.Second)} } rootj…

K8s中如何使用etcd进行集群信息的备份与恢复

这里写目录标题 ETCD是什么?1. **`etcd`(服务)**2. **`etcdctl`(客户端工具)**如何安装etcdctl(客户端工具)查看目前K8s自带etcd中的版本信息安装对应版本的etcdutl工具下载 `etcdutl` 3.5.7 版本配置环境变量创建备份文件验证一下备份的快照文件备份文件恢复的效果演示…

一些可能很有用的矩阵知识

一些可有可无的矩阵知识 酉矩阵 酉矩阵 一个服从正态分布的向量乘以一个酉矩阵&#xff0c;得到的向量仍然服从正态分布 酉矩阵是一个复数矩阵&#xff0c;满足其转置的共轭等于其逆矩阵。当一个向量通过一个酉矩阵进行线性变换时&#xff0c;它的模长保持不变&#xff0c;只是…

uniapp微信小程序开发踩坑日记:Pinia持久化报错Cannot read property ‘localStorage‘ of undefined

插件默认使用 localStorage 实现持久化&#xff0c;小程序端不兼容&#xff0c;需要替换持久化 API import { defineStore } from pinia export const useCommonStore defineStore(pack-store, {state: (): State > ({wwInfo: {},globalData: {},timerLock: false, //是…

TCP通信三次握手、四次挥手

前言 前面我说到了&#xff0c;UDP通信的实现&#xff0c;但我们经常说UDP通信不可靠&#xff0c;是因为他只会接收和发送&#xff0c;并不会去验证对方收到没有&#xff0c;那么我们说TCP通信可靠&#xff0c;就是因为他会进行验证接收端是否能够接收和发送&#xff0c;并且只…

【Kubernetes知识点问答题】监控与升级 / ETCD 备份与恢复

目录 1. 举例说明 K8s 中都有哪些常规的维护管理操作。 2. 如何升级 K8s 到新的版本&#xff1f;在升级过程中应该注意哪些事项&#xff1f; 3. 解释 ETCD 及其备份和恢复的过程。 1. 举例说明 K8s 中都有哪些常规的维护管理操作。 常见的维护管理操作有&#xff1a; ① 查看…