链表理论基础
链表是一种通过指针串联在一起的线性结构,包含储存数值的数据域,和指向节点的指针域。
链表种类
单链表
单链表每一个节点由两部分组成,一个是数据域,一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向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. 移除链表元素
思路
在原有链表操作
当我们检索到存在和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. 设计链表
单链表
两个构造函数: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. 反转链表
反转
只需要改变链表的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.next
和current.next=prev
是两个主要的转向操作,剩下的都是赋值操作。因此,可以利用递归反复赋值。