当前位置: 首页 > article >正文

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

参考文献 代码随想录

一、 链表理论基础

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

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

如图所示: 

链表1

#链表的类型

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

#单链表

刚刚说的就是单链表。

#双链表

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

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

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

如图所示: 

链表2

#循环链表

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

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

链表4

#链表的存储方式

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

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

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

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

如图所示:

链表3

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

链表的操作

#删除节点

删除D节点,如图所示:

链表-删除节点

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

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

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

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

#添加节点

如图所示:

链表-添加节点

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

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

#性能分析

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

链表-链表与数据性能对比

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

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

二、移除链表元素

给你一个链表的头节点 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.next
            else:
                cur = cur.next
        return 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.next
            else:
                cur = cur.next
        return 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 。

问题分析

删除链表节点: 

链表-删除节点

添加链表节点: 

链表-添加节点

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

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

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

链表操作的两种方式:

  1. 直接使用原来的链表来进行操作。
  2. 设置一个虚拟头结点在进行操作。
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

class 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 -1
        cur = self.dummy_head.next
        for i in range(index):  # 举例一个极端,那么添加到下标为0时候是否为正确
            cur = cur.next
        return cur.val


    def addAtHead(self, val):
        """
        :type val: int
        :rtype: None
        """
        self.dummy_head.next = ListNode(val, self.dummy_head.next)
        self.size += 1

    def addAtTail(self, val):
        """
        :type val: int
        :rtype: None
        """
        cur = self.dummy_head
        while cur.next:
            cur = cur.next
        cur.next = ListNode(val)
        self.size += 1

    def addAtIndex(self, index, val):
        """
        :type index: int
        :type val: int
        :rtype: None
        """
        if index < 0 or index > self.size:
            return
        cur = self.dummy_head
        for i in range(index):
            cur = cur.next
        cur.next = ListNode(val, cur.next)
        self.size += 1

    def deleteAtIndex(self, index):
        """
        :type index: int
        :rtype: None
        """
        if index < 0 or index >= self.size:
            return
        cur = self.dummy_head
        for i in range(index):
            cur = cur.next
        cur.next = cur.next.next
        self.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_反转链表

之前链表的头节点是元素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 = head
        while cur:
            tmp = cur.next 
            cur.next = prev
            prev = cur
            cur = tmp
        return 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 pre
        temp = cur.next
        cur.next = pre
        return self.reverse(temp, cur)


http://www.kler.cn/news/299149.html

相关文章:

  • 【PostgreSQL教程】PostgreSQL 高级篇之子查询
  • Linux——redis主从复制、集群模式、哨兵模式
  • 漫谈设计模式 [10]:享元模式
  • 机器学习和深度学习的区别是什么?
  • 插槽slot
  • Linux环境常用的一些网络相关的命令
  • 【Linux】循序进阶学运维-服务篇-tomcat入门
  • Linux:从入门到放弃
  • mysql5.7安装
  • ros2中使用launch.xml启动时,怎么在命令行里设置参数,或者加载参数文件(params.yaml)
  • 版权与开源协议:一场创新与自由的边界之争
  • 关于武汉芯景科技有限公司的IIC电平转换芯片XJ4300开发指南(兼容LTC4300)
  • 大模型备案重难点最详细说明【评估测试题+附件】
  • Linux开发工具的使用
  • DBeaver 24.2 发布下载,新增功能概览
  • HalconDotNet中的图像视频采集
  • 社交媒体的未来:Facebook如何通过AI技术引领潮流
  • 2024/9/10黑马头条跟学笔记(六)
  • 利用AI大语言模型和Langchain开发智能车算法训练知识库(上篇)
  • 计算机毕业设计PySpark+Django考研分数线预测 考研院校推荐系统 考研推荐系统 考研爬虫 考研大数据 Hadoop 大数据毕设 机器学习 深度学习