链表
1、环形链表找环起始结点
-
使用快慢指针检测环:
- 初始化两个指针
slow
和fast
,都指向链表的头节点。 slow
每次移动一步,fast
每次移动两步。- 如果
fast
和slow
相遇(即fast == slow
),说明链表中存在环。否则,如果fast
遇到链表的末尾(fast
为None
或fast.next
为None
),则链表无环。
- 初始化两个指针
-
找到环的起始节点:
- 一旦快慢指针相遇,就说明链表中存在环。
- 此时,将其中一个指针(例如
slow
)重新指向链表的头节点,另一个指针(例如fast
)保持在相遇点。然后两个指针都每次移动一步,它们最终会在环的起始节点相遇。 - 返回相遇的节点作为环的起始节点。
-
没有环的情况:
- 如果快慢指针没有相遇,则返回
None
,表示链表中没有环。
- 如果快慢指针没有相遇,则返回
第一阶段,soft和fast在b点相遇,soft走过的距离是x+y,fast走过的距离是2(x+y)。fast比soft多走的距离x+y=r+y。可以得到x=r。
第二阶段,找环起点a,soft重定向到头结点head,fast从相遇点b出发。soft走到a的距离为x,fast回到环起点经过的距离为r,x=r,所以两者会在a处相遇。
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow=fast=head
while fast and fast.next:
slow=slow.next
fast=fast.next.next
if fast==slow:
flag=True
break
else:
return None
slow=head
while slow!=fast:
slow=slow.next
fast=fast.next
return slow
2、合并两个有序链表
-
创建虚拟头节点:
- 为了方便操作和返回结果,创建一个虚拟头节点
prehead
,并初始化一个指针prev
指向prehead
。 prehead
的作用是简化边界情况的处理,例如当list1
或list2
一开始就是None
的情况。
- 为了方便操作和返回结果,创建一个虚拟头节点
-
遍历两个链表:
- 进入
while
循环,只要list1
和list2
都不为空,就比较它们的当前节点值。 - 如果
list1
当前节点的值小于或等于list2
的当前节点值,则将list1
的当前节点连接到prev
后面,并移动list1
指针到下一个节点。 - 如果
list2
当前节点的值小于list1
,则将list2
的当前节点连接到prev
后面,并移动list2
指针到下一个节点。 - 无论哪种情况,
prev
都需要前进到下一个位置,即移动prev = prev.next
。
- 进入
-
处理剩余节点:当
while
循环结束时,说明至少有一个链表已经遍历完毕,但另一个链表可能还有剩余节点。直接将剩余的链表连接到prev.next
,因为剩下的节点一定比已经处理的所有节点都大,所以直接附加到链表末尾。 -
返回合并后的链表:最后返回
prehead.next
,即合并后链表的头节点。prehead
只是一个虚拟头节点,不包含在最终结果中。
class Solution:
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
prehead=ListNode(-1)
prev=prehead
while list1 and list2:
if list1.val<=list2.val:
prev.next=list1
list1=list1.next
else:
prev.next=list2
list2=list2.next
prev=prev.next
prev.next=list1 if list1 else list2
return prehead.next
时间复杂度:O(n+m),其中 n 和 m 分别为两个链表的长度。空间复杂度:O(1)。
3、链表两数相加
-
创建虚拟头节点:
- 使用
prehead
作为虚拟头节点,它帮助我们简化结果链表的构建过程。 pre
指针用于遍历和构建最终的结果链表。
- 使用
-
初始化进位:
carry
用于存储每次相加后的进位(即两数相加超过10的情况)。
-
遍历两个链表:
- 使用
while
循环,条件为l1
、l2
或carry
不为0
。这意味着即使一个链表已经处理完毕,我们仍然需要处理另一个链表的剩余部分以及进位。
- 使用
-
按位相加:
res
用于存储当前节点的值,首先加上进位carry
。- 如果
l1
不为空,将l1
的当前节点值加到res
中,然后将l1
移动到下一个节点。 - 如果
l2
不为空,将l2
的当前节点值加到res
中,然后将l2
移动到下一个节点。
-
处理进位:
- 计算新的
carry
,即res
除以10
的结果。 - 计算当前节点值
res
对10
取模后的结果,并创建一个新的节点new
,将其连接到pre
后面。
- 计算新的
-
更新指针:将
pre
移动到新创建的节点new
。 -
返回结果链表:
- 循环结束后,
prehead.next
指向的链表就是结果链表的头节点,返回该节点即可。
- 循环结束后,
class Solution:
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
prehead=ListNode(-1)
pre=prehead
carry=0
while l1 or l2 or carry:
res=carry
if l1:
res+=l1.val
l1=l1.next
if l2:
res+=l2.val
l2=l2.next
carry=res//10
res%=10;new=ListNode(res)
pre.next=new;pre=pre.next
return prehead.next
4、删除链表的倒数第N个数
为了统一处理头结点和其他结点的删除操作,创建一个哑结点dummy,它的next指针指向头结点。
这道题主要利用双指针的思想,一个指针first来遍历链表,一个指针second指向需要删除的节点的前一个结点。初始化first指向头结点,second指向dummy结点。
主要分成两步解决问题:
第一步,first遍历链表前移n步,指向第n+1个结点。second此时仍指向dummy结点。因此second和first之间有n个结点。
第二步,first遍历链表直到指向链表尾NULL,second随着first遍历直到停止,两者之间始终有n个结点,second指向的是倒数第n个结点的前一个结点。
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
dummy=ListNode(0,head)
first=head
second=dummy
for _ in range(n):
first=first.next
while first:
first=first.next
second=second.next
second.next=second.next.next
return dummy.next
5、两两交换链表中的节点
两两结对交换节点,1和2交换,3和4交换。
设置一个哑结点dummy方便后面处理头部结点。
在交换过程中主要有三个结点比较关键:
- 要交换的对子之前的节点node0。
- 要交换的对子中靠前的节点node1。
- 要交换的对子中靠后的节点node2。
实现交换要建立的指针关系分别有以下三种:
- node0->node2
- node1->node2.next
- node2.next=node1
要遍历更新的结点:node0=node1
-
创建虚拟头节点:
- 使用
dummy
作为虚拟头节点,它指向链表的头节点head
。这样做的目的是处理链表头部交换时的边界情况,并方便返回结果。
- 使用
-
初始化指针:
node0
指针初始化为dummy
,用于遍历链表,并进行节点交换操作。
-
遍历链表并交换节点:
- 进入
while
循环,条件是node0.next
和node0.next.next
都不为空。这保证了至少有两个节点可以进行交换。 node1
指向node0.next
,即待交换的第一个节点。node2
指向node0.next.next
,即待交换的第二个节点。- 进行节点交换:
- 将
node0.next
指向node2
,使node0
直接指向node2
。 - 将
node1.next
指向node2.next
,使node1
连接到交换后的下一个节点。 - 将
node2.next
指向node1
,完成交换。
- 将
- 将
node0
移动到node1
,准备处理下一对节点。
- 进入
-
返回结果链表:
- 最后返回
dummy.next
,即交换后的链表头节点。
- 最后返回
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy=ListNode(0,head)
node0=dummy
while node0.next and node0.next.next:
node1=node0.next
node2=node0.next.next
node0.next=node2
node1.next=node2.next
node2.next=node1
node0=node1
return dummy.next
6、K个一组翻转链表
主要思路如下:可以将链表划分为三部分,已翻转部分+待翻转部分+未翻转部分
- 首先,为了统一处理操作,为链表设置一个哑结点,next指向头结点。
- 其次,每次翻转链表,都是以组的形式进行操作的,一组节点的数量为k。使用pre指向一组节点的前序节点,使用start指向一组的起始节点,使用end指向一组的尾部节点,使用next_group指向未翻转部分的第一个节点。其中,三者的关系可以描述为:start=pre.next,而end的位置是从pre.next开始遍历k个节点获取的。需要注意的是,当一个组内节点个数不足k个的时候(也就是end没遍历k次就到链表尽头了),直接返回处理过的链表即可(return dummy.next)。
- 因此根据上面的信息,为了处理第一组节点,pre设置为dummy,end也设置为dummy。
- 再每次翻转完链表后,需要将链表与前后两部分衔接起来,因此pre.next=当前翻转后的部分。当前翻转后的部分中,start指向的是该部分的最后一个节点,因此start.next=next_group。
- 更新pre和end,pre更新为start,end也更新为start。
class Solution:
def reverseList(self,head):
pre=None
curr=head
while curr:
nex=curr.next
curr.next=pre
pre=curr
curr=nex
return pre
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
dummy=ListNode(0,head)
pre=end=dummy
while end.next:
for _ in range(k):
end=end.next
if not end:
return dummy.next
start=pre.next
next_group=end.next
end.next=None
pre.next=self.reverseList(start)
start.next=next_group
pre=start
end=pre
return dummy.next
7、随机链表的复制
主要分两步走:第一步,遍历链表,创建每个节点对应的新节点,此时创建的新节点值与原结点相同,但next和random关系没有建立。第二步,遍历链表,构建新节点的random和next关系。
- 创建一个哈希表:用来存储原链表节点与新链表节点之间的映射关系。
- 复制所有节点:遍历原链表,为每个节点创建一个新节点,这些新节点具有相同的值但
next
和random
指针先不处理。 - 设置指针:再次遍历原链表,根据哈希表中的映射关系,设置新链表的
next
和random
指针。 - 返回新链表的头节点:使用哈希表找到与原链表头节点对应的新链表头节点,返回此节点作为结果。
"""
# Definition for a Node.
class Node:
def __init__(self, x: int, next: 'Node' = None, random: 'Node' = None):
self.val = int(x)
self.next = next
self.random = random
"""
class Solution:
def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
if not head:
return None
d=dict()
pre=head
while pre:
new=Node(pre.val,None,None)
d[pre]=new
pre=pre.next
pre=head
while pre:
if pre.next:
d[pre].next=d[pre.next]
if pre.random:
d[pre].random=d[pre.random]
pre=pre.next
return d[head]
8、排序链表
主要是利用归并排序的思想来实现的,在链表的迭代归并排序中,逐渐增加排序的子段长度(从1开始,然后是2、4、8等),每次迭代合并相邻的两个已排序的子链表。
主要通过以下两个步骤实现:
- 提取子链表:对于每个子链表,遍历链表直到达到所需长度或链表结束。
- 合并子链表:比较 h1 和 h2 的节点值,将较小的节点链接到 pre,并更新 pre 和选中的子链表的头节点。如果一个子链表先遍历完毕,将剩余的另一个子链表直接链接到已排序部分。
-
初始化:
- 创建一个哑节点dummy,其
next
指向链表头部。 - 使用变量
h
遍历整个链表以计算其长度length
。 - 设置
intv
作为每次需要排序和合并的子链表的长度。
- 创建一个哑节点dummy,其
-
外层循环:当
intv
小于length
时,进行合并操作,每次循环结束后将intv
的值翻倍。 -
内层循环:
- 每次循环开始前设置
pre
和h
指向dummy.next,不能直接使用head,因为在排序的过程中head可能已经发生变化。
- 提取两个长度为
intv
的子链表h1
和h2
进行合并。
- 每次循环开始前设置
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def splitList(self,head,size):
prev=None
cur=head
count=0
while cur and count<size:
prev=cur
cur=cur.next
count+=1
if prev:
prev.next=None
return head,cur
def mergeList(self,h1,h2,pre):
while h1 and h2:
if h1.val<h2.val:
pre.next=h1
h1=h1.next
else:
pre.next=h2
h2=h2.next
pre=pre.next
pre.next=h1 if h1 else h2
while pre.next:
pre=pre.next
return pre
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
p=head;length=0
dummy=ListNode(0,head)
while p:
length+=1
p=p.next
intv=1
while intv<length:
pre=dummy
h=dummy.next
while h:
h1,h=self.splitList(h,intv)
h2,h=self.splitList(h,intv)
if not h2:
pre.next=h1
break
pre=self.mergeList(h1,h2,pre)
intv*=2
return dummy.next
9、合并K个升序链表
从题意可以得到,一个list里面存放着若干链表,要想合并K个升序链表,可以使用小根堆来存放结点(利用了小根堆的堆顶一定是val最小的结点)。
首先,遍历列表中的每个链表,将每个链表的头结点存放到小根堆中。为了避免后面多个相同最小值pop报错的情况(假如只存放(val,Node)元组,当val相同时,小根堆去比较Node,会发生type error),使用index作为索引来标识不同链表,小根堆存放的元素就变成了(val,index,node)。heappop时比较完val,就会比较列表索引index,然后弹出一个节点。
接下来,在小根堆非空的情况下,每次循环pop出最小结点,链接到当前链表的尾部。同时,如果当前pop出的最小结点所在的链表还没有遍历结束,就将该结点在原链表中next指针指向的结点放入小根堆中。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
import heapq
class Solution:
def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
min_heap=[];index=0
for l in lists:
if l:
heapq.heappush(min_heap,(l.val,index,l))
index+=1
dummy=ListNode(0)
curr=dummy
while min_heap:
val,index,node=heapq.heappop(min_heap)
curr.next=node
curr=curr.next
if node.next:
heapq.heappush(min_heap,(node.next.val,index,node.next))
return dummy.next
10、LRU缓存
首先,解读题意:
- LRU(最近最少使用)的规则是:就当下而言,最久没有访问过的被淘汰。所以可以用双向链表这一数据结构来实现,最新访问过的被放在链表的头部位置,最久没有访问过的处于链表的尾部,淘汰时只需要删除尾部节点。
- get是获取指定Key所对应的value,对于LRU缓存来说相当于访问一次key-value。每访问一次,被访问的结点就被移动到链表的头部。
- put是在缓存中没有相应的key时,创建新节点插入链表头部;当缓存中有相应的key时,更改它对应的value,并将其移动到链表头部。需要注意的是,当创建新节点插入链表头部时,可能超出了LRU指定的capacity,这个时候就需要移除链表尾部的节点。
class DNode:
def __init__(self,key=0,value=0,prev=None,next=None):
self.key=key
self.value=value
self.prev=prev
self.next=next
class LRUCache:
def __init__(self, capacity: int):
self.cache=dict()
self.head=DNode()
self.tail=DNode()
self.head.next=self.tail
self.tail.prev=self.head
self.capacity=capacity
self.size=0
def get(self, key: int) -> int:
if key not in self.cache:
return -1
node=self.cache[key]
self.moveToHead(node)
return node.value
def put(self, key: int, value: int) -> None:
if key not in self.cache:
if self.capacity==self.size:
removed=self.removeTail()
self.cache.pop(removed.key)
self.size-=1
node=DNode(key,value)
self.cache[key]=node
self.addToHead(node)
self.size+=1
else:
node=self.cache[key]
node.value=value
self.moveToHead(node)
def addToHead(self,node):
node.prev=self.head
node.next=self.head.next
self.head.next.prev=node
self.head.next=node
def removeNode(self,node):
node.next.prev=node.prev
node.prev.next=node.next
def moveToHead(self,node):
self.removeNode(node)
self.addToHead(node)
def removeTail(self):
node=self.tail.prev
self.removeNode(node)
return node
# Your LRUCache object will be instantiated and called as such:
# obj = LRUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)