代码随想录第三天 | 链表理论基础,203.移除链表元素,707.设计链表,206.反转链表

链表理论基础

链表是一种通过指针串联在一起的线性结构,包含储存数值的数据域,和指向节点的指针域。

链表种类

单链表

单链表每一个节点由两部分组成,一个是数据域,一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。链表的入口节点称为链表的头结点也就是head。

在这里插入图片描述

双链表

单链表中的指针域只能指向节点的下一个节点。而双链表的每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。因此双链表既可以向前查询也可以向后查询。

在这里插入图片描述

循环链表

链表首尾相连,A-B-C-D-再回到A,循环链表可以用来解决约瑟夫环问题。
在这里插入图片描述

存储方式

数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。链表是通过指针域的指针链接在内存中各个节点。所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
在这里插入图片描述这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。

链表的定义

通过class类,来定义链表节点

class ListNode:
    def __init__(self, val, next=None):
        self.val = val
        self.next = next

链表操作

删除节点

在这里插入图片描述
通过将C的next指针指向E节点来删除D节点。

在C++里D节点依然存留在内存里,只不过是没有在这个链表里而已,所以最好是再手动释放这个D节点,释放这块内存。其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了

添加节点

在这里插入图片描述
可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。

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

数组 V.S. 链表

在这里插入图片描述
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。

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


203. 移除链表元素

Leetcode 203 原题链接

思路

在原有链表操作

当我们检索到存在和val相同数值的节点时,我们需要把该节点之前和之后的两个节点连接起来。但是由于单链表中只有一个指向下一个数据的指针,所以应当是来看遍历指针current的next是否是应该删除的节点,如果是,就将current和current.next.next相连,即current.next=current.next.next (将current的指针指向current.next.next)。

在这个过程中,由于会涉及到current,current.next和current.next.next这三个位置:

  • 如果current.next和current.next.next都是None,那么便会报错。当current.next是None时,说明current是最后一个数据,如果其数值=val,我们应当将其删掉,但是Nonetype has no attribute of next and val,也就无法指向current.next.next;更何况删除末尾元素应当把倒数第二个元素指针指向Null,所以应当用current.next来判断下一个指针是否满足条件,如果是非空,且满足数值条件,删除它就直接把当前指针指向新的下一个节点。 这也能解决链表只有一个数字的情况。
  • 更不用说当current=None时,说明已经越界了,应当跳出循环。
  • 因此我们得到了循环条件应当是,current和current.next都不是None

既然我们会考虑current和current.next两个数据,那么当head是应该删除的元素我们又该怎么办?如果是采用上述在原来的链表来进行删除操作的方法,我们只能将head作为特殊情况处理。

移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。特殊情况包括:

  • head.val==val,那么只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点(直接将head.next作为新的head,即head=head.next)
  • 整个链表都是None,也就不存在head。那么直接返回[ ] (return head)。

因此针对head的判删除条件,应当是head is not None and head.val==val。由于可能存在链表开头存在多个相同数值的元素,因此设置while来循环判断。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
        while head is not None and head.val==val:
        # 链表非空,且head是要删除的数时,进入循环
        # 可能存在链表开头存在多个相同数值的元素,设置while来循环判断
            head=head.next # 更新head.next为新的head头节点
        cur=head # 设置遍历链表的节点cur,使其从head开始遍历
        while cur is not None and cur.next is not None:
        # 确保cur=末位元素或者链表只有一个元素情况时跳出循环
            if cur.next.val==val:
                cur.next=cur.next.next # 将cur的下一个元素更新为cur.next.next
            else: cur=cur.next # 数值不匹配,就直接cur更新为下一个节点继续遍历
        return head # 返回新的头节点

虚拟头结点

给链表添加一个虚拟头结点为新的头结点,这样我们可以看做头节点前也有一个节点,同样可以将头节点通过前一个节点来移除。
在这里插入图片描述
最后呢在题目中,return 头结点的时候,别忘了 return dummyNode->next;, 这才是新的头结点,头节点可能会因为删除改变,这时候用dummyNode->next表示head更准确

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
        dummyhead=ListNode(0) # 设立一个虚拟结点
        dummyhead.next=head # 将虚拟节点的下一节点设为头节点,安排在head前
        current=dummyhead # 设置遍历链表的current,使其从虚拟头结点开始遍历
        while current.next:
        # 虚拟头结点默认链表有一个隐形元素
        # 当实际链表只有一个元素时,不会出现current.next是None而报错
        # 当current为末位元素刚好跳出循环
            if current.next.val==val:
                current.next=current.next.next # 移除节点
            else:
                current=current.next # 节点指向下一个节点去判断
        return dummyhead.next # 返回虚拟头结点后的节点,那才是新的头节点

707. 设计链表

Leetcode 707 原题链接

单链表

两个构造函数:def __init__(self, val=0, next=None) & def __init__(self)。前者接受两个参数:val和next,并将它们分别用于初始化节点的值和下一个节点的引用;后者没有接受任何参数,而是在构造函数内部直接初始化了dummy_head和size这两个类属性。dummyhead是一个ListNode类的实例,它用作链表的虚拟头节点。size是一个整数,用于表示链表的大小。

val和next是用于初始化链表节点的属性,而dummyhead和size是用于初始化整个链表的属性。self.dummy_head是一个虚拟头结点,它的作用是充当一个占位符节点。它的next指针指向链表的实际头结点,起到了统一起点的作用。新的链表输入后会直接在dummyhead之后,dummyhead成为虚拟头结点。

# class ListNode
    # def __init__(self, val=0, next=None):
        # self.val = val
        # self.next = next
        
class MyLinkedList:

    def __init__(self):
        self.dummyhead=ListNode() # 返回ListNode函数,创造一个虚拟结点
        self.size=0 # 初始化整个链表的属性,因为不是节点参数,而是设置了整个链表的属性(长度)
     
    def get(self, index: int) -> int:
        if index<0 or index>=self.size: 
        # 超出边界的index属于无效下标
        # 当index=self.size,链表最大的在索引为index-1
            return -1
        current=self.dummyhead # 从虚拟头结点开始遍历
        for _ in range(index):
            current=current.next # 当current索引为index-1,跳出函数
        return current.next.val # 返回索引为index的数的值
        
    def addAtHead(self, val: int) -> None:
        self.dummyhead.next=ListNode(val,self.dummyhead.next)
        # 简便写法:
        # newhead=ListNode(val)
        # newhead.next=self.dummyhead.next 使新头节点指向原来的头节点
        # self.dummyhead.next=newhead 使虚拟头结点指向新头节点
        self.size+=1
        
    def addAtTail(self, val: int) -> None:
        current=self.dummyhead # 用current指针从虚拟头结点开始遍历链表
        while current.next: # 使current遍历链表找到最后一个元素,跳出循环时current=最后一个元素
            current=current.next 
        current.next=ListNode(val)
        # newtail=ListNode(val)
        # newtail.next=None 末位元素指向None,此行可省
        # current.next=newtail
        self.size+=1
        
    def addAtIndex(self, index: int, val: int) -> None:
        if index<0 or index>self.size:
            return # 啥也不干
        current=self.dummyhead
        for _ in range(index): # 当current索引为index-1,跳出函数
            current=current.next 
        current.next=ListNode(val,current.next)
        # newnode=ListNode(val)
        # newnode.next=current.next 新节点指向原来索引为index的数
        # current.next=newnode 索引index-1的数指向新节点
        self.size+=1
        
    def deleteAtIndex(self, index: int) -> None:
        if index <0 or index >=self.size:
            return # 啥也不干
        current=self.dummyhead
        for _ in range(index):
            current=current.next
        current.next=current.next.next # index-1的数指向index+1的数
        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和tail。self.head和self.tail是用来方便引用链表的头部和尾部节点的实例变量。它们只是存储了对链表中实际头部和尾部节点的引用,并没有像虚拟头结点一样起到占位符的作用。通过self.head和self.tail,你可以方便地访问链表的头部和尾部节点,而不需要遍历整个链表来找到它们。

简单来说,它们只是指向链表中的节点对象,并不是直接存储链表本身。 当你输入一个链表后,将链表的头部节点赋值给self.head,将链表的尾部节点赋值给self.tail,这样你就可以通过这两个实例变量来访问链表的头部和尾部

class ListNode:
    def __init__(self,val=0,prev=None,next=None):
        self.val=val
        self.prev=prev
        self.next=next

class MyLinkedList:

    def __init__(self):
        self.head=None
        self.tail=None
        self.size=0

    def get(self, index: int) -> int:
        if index<0 or index>=self.size:
            return -1
        if index<self.size//2: # 二分查找
            current=self.head
            for _ in range(index): # index是多少,跳出循环时current索引就是index,除非越界
                current=current.next
        else:
            current=self.tail
            for _ in range(self.size-index-1): # self.size-1才是末尾元素的索引,此时current索引为index
                current=current.prev
        return current.val

    def addAtHead(self, val: int) -> None:
        newnode=ListNode(val,None,self.head) # 创造新结点,新节点指向head
        # newnode=ListNode(val,None,None)
        # newnode.next=self.head 新节点就成为了新的头结点,并且它的后继节点是原来的头结点
        if self.head: # 非空链表情况 self.head is not None
            self.head.prev=newnode # 原来的头结点也指向新节点,双向
        else: # 空链表
            self.tail=newnode # 因为只有一个元素,head也是tail,将新节点 newnode 赋值给了self.tail,使self.tail指向新节点
        self.head=newnode # 使self.head访问新的头结点
        self.size+=1
        # 总结:先调整指向方向,再更新链表头部/尾部的访问位置
        
    def addAtTail(self, val: int) -> None:
        newnode=ListNode(val,self.tail,None) # 创造新结点,且前驱节点为原来的尾部节点,下一节点为None,不用管
        if self.tail: # 不是非空链表
            self.tail.next=newnode
        else: # 空链表
            self.head=newnode
        self.tail=newnode # 将newnode赋值给self.tail,使self.tail访问新结点
        self.size+=1

    def addAtIndex(self, index: int, val: int) -> None:
        if index<0 or index>self.size:
            return 
        if index==0:
            self.addAtHead(val) 
        elif index==self.size:
            self.addAtTail(val) # 当index=self.size,直接插入新节点作为tail
        else:
            if index<self.size//2:
                current=self.head
                for _ in range(index-1): # 此时current索引为index-1
                    current=current.next
            else:
                current=self.tail
                for _ in range(self.size-index): # 此时current索引为index-1
                    current=current.prev
            # 需要current来作为newnode的前驱节点,current.next作为newnode后继节点
            # 因此新节点index确认后,用的current是他之前,也就是index-1位置上
            newnode=ListNode(val,current,current.next)
            # newnode=ListNode(val)
            # newnode.prev=current
            # newnode.next=current.next

            # A B C,current为A,B为插入点,ListNode把A<-B和B->C定义了
            current.next.prev=newnode # (1)先定义了B<-C
            current.next=newnode # (2)再定义了A->B
            # 若(2)在前头,则会导致(1)中的current.next部分称为newnode,会出问题
            self.size+=1

    def deleteAtIndex(self, index: int) -> None:
        if index<0 or index>=self.size:
            return 
        # 由于没有虚拟节点,要考虑区间问题
        elif index==0:
            self.head=self.head.next
            # 不会因为是空链表而报错,因为Python中的变量赋值是一种引用传递的方式
            if self.head: # 非空链表
                self.head.prev=None # 新头结点之前更新为None,而非原来的头结点
            else: self.tail=None # 空链表
        elif index==self.size-1:
            self.tail=self.tail.prev
            if self.tail:
                self.tail.next=None
            else: self.head=None
        else:
            if index<self.size//2:
                current=self.head
                for _ in range(index): # 跳出循环时current索引为index,方便我们操作
                    current=current.next
            else: 
                current=self.tail
                for _ in range(self.size-1-index):
                    current=current.prev
            current.next.prev=current.prev
            current.prev.next=current.next
            # 连接current的前后两节点,调换不影响结果
        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)

206. 反转链表

Leetcode 206 原题链接

反转

只需要改变链表的next指针的指向,直接将链表反转。如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
在这里插入图片描述

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        current=head # 定义current指向头结点遍历链表
        prev=None # 定义prev来充当反转后的None
        while current:
            tmp=current.next # 储存值,以防转向后断链无法访问
            current.next=prev # 转向
            prev=current # 将current的值赋值给prev,等于将其右移一格,但没改变新的方向
            current=tmp # 将tmp储存的值赋值给current,等于将其右移一格,但没改变新的方向
        return prev

递归

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

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

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next

# 递归:在函数中调用自身的编程技巧或方法
class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        return self.reverse(head,None)
        # 同理,reverseList也是类的成员方法,需要加self.来调用

    def reverse(self,current: Optional[ListNode],prev: Optional[ListNode]) -> Optional[ListNode]:
        if current==None:
            return prev
        tmp=current.next
        current.next=prev
        return self.reverse(tmp,current)
        # 由于 reverse() 方法是类的成员方法,需要使用 self.reverse() 而不是直接使用 reverse() 调用该方法

我们可以看出,tmp=current.nextcurrent.next=prev是两个主要的转向操作,剩下的都是赋值操作。因此,可以利用递归反复赋值


  • 27
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值