代码随想录算法训练营第4天|链表|24.两两交换链表中的节点19.删除链表的倒数第N个节点160.相交链表142.环形链表II
一、24.两两交换链表中的节点
文档链接:代码随想录
题目链接:24.两两交换链表中的节点
视频讲解:视频讲解
题目描述:
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例 2:
输入:head = []
输出:[]
示例 3:
输入:head = [1]
输出:[1]
提示:
- 链表中节点的数目在范围
[0, 100]
内 0 <= Node.val <= 100
代码:
- 递归法
思路:通过递归和指针操作来逐个处理链表中的节点对,并将它们的位置交换。
- 边界情况处理:首先检查链表是否为空或只有一个节点。这是递归终止的条件,因为在这种情况下,没有节点需要交换。
- 初始化指针:创建三个指针,
pre
、cur
和next
。pre
指向当前需要交换的前一个节点,cur
指向当前需要交换的第一个节点,next
指向当前需要交换的第二个节点之后的节点。 - 交换节点:通过改变节点的
next
指针来交换cur
和pre
的位置。这样,cur
节点就移动到了它原本位置的后面。 - 递归处理:递归地调用
swapPairs
函数来处理被交换的第二个节点之后的链表部分。这个递归调用是为了处理可能存在的下一个节点对。 - 返回结果:最后返回交换后的新头节点,即原本的第二个节点。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 如果链表为空或只有一个节点,则直接返回原链表头节点
if head is None or head.next is None:
return head
# 初始化三个指针:pre指向当前需要交换的前一个节点,cur指向当前需要交换的第一个节点,next指向当前需要交换的第二个节点之后的节点
pre = head # pre 用于暂存当前对的前一个节点
cur = head.next # cur 用于暂存当前对的第一个节点
next = head.next.next # next 用于暂存当前对的第二个节点之后的节点
# 进行节点交换,将cur和pre交换位置
# 注意这里并没有真的交换节点内部的值,而是通过改变节点的next指针来交换节点的位置
cur.next = pre # 将cur的next指向pre,完成第一个节点的交换
pre.next = self.swapPairs(next) # 递归地交换后面的节点对,并将结果链接到pre后面
# 返回交换后的新头节点(即原本的第二个节点)
return cur
- 虚拟头节点法
思路:
- 首先,创建一个虚拟头节点(dummy_head),并将其next指针指向原链表的头节点。这样做是为了简化对头节点的处理,因为在交换过程中,头节点可能会被改变位置。
- 定义一个指针current,指向虚拟头节点。这样可以在循环中方便地移动到下一个节点。
- 使用while循环来遍历链表,直到没有更多的连续节点为止。在每次循环中,执行以下操作:
- 临时存储当前需要交换的两个节点(temp和temp1)。
- 将当前节点的下一个节点指向下下个节点,即将current.next指向current.next.next。
- 将下下个节点指向原先的下一个节点,即将current.next.next指向temp。
- 将原先的下一个节点指向原下下个节点的下一个节点,即将temp.next指向temp1。
- 将指针current移动到第二个节点之后的位置,即将current更新为current.next.next。
- 最后,返回虚拟头节点的下一个节点作为交换后的新链表头节点。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy_head = ListNode(next=head) # 创建一个虚拟节点,作为新的链表头部
current = dummy_head # 定义一个指针current,初始指向虚拟头节点
# 当当前节点的下一个节点和下下个节点都存在时,进行交换操作
while current.next and current.next.next:
# 定义临时变量,暂存当前节点的下一个节点和下下个节点的下一个节点
temp = current.next # 第一个节点
temp1 = current.next.next.next # 第二个节点之后的节点
# 交换两个节点
current.next = current.next.next # 使当前节点的下一个节点指向下下个节点
current.next.next = temp # 使下下个节点指向原先的下一个节点
temp.next = temp1 # 使原先的下一个节点指向原下下个节点的下一个节点
current = current.next.next # 当指针current移动到下一对节点的起始节点
# 返回交换后的新链表头节点(即虚拟头节点的下一个节点)
return dummy_head.next
二、19.删除链表的倒数第N个节点
文档链接:代码随想录
题目链接:19.删除链表的倒数第N个节点
视频讲解:视频讲解
题目描述:
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1
输出:[]
示例 3:
输入:head = [1,2], n = 1
输出:[1]
提示:
- 链表中结点的数目为
sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
进阶: 你能尝试使用一趟扫描实现吗?
代码:
- 快慢指针法
思路:由于链表只能从头到尾遍历,不能直接访问倒数第n
个节点。需要采用快慢指针
的方法。
快慢指针: 开始时”快指针“和”慢指针“都指向虚拟头节点
,然后快指针先向前移动n
步。此时,快指针和慢指针之间相隔n
个节点。接下来,我们同时移动快指针和慢指针,直到快指针到达链表的末尾(即快指针的next
指针为null)。同时慢指针会指向倒数第n+1
个节点。
删除节点: 当快指针到达链表的末尾时,我们可以通过修改慢指针的next
指针来删除倒数第n
个节点。将慢指针的next
指针指向倒数第n
个节点的下一个节点。
**返回头节点:**返回虚拟头节点的next
指针,它现在指向修改后的链表的头节点。
注意事项:
- 如果链表为空或
n
小于等于 0,则直接返回原链表。 - 快指针先走
n
步,如果在这个过程中快指针变成了null
,说明链表长度小于n
,此时也直接返回原链表。 - 在移动快慢指针时,要确保快指针不会越界(即快指针的
next
不为null
时才移动)。 - 在删除节点时,要确保慢指针的
next
不为null
,以防止空指针异常。
通过这种方法,我们可以在一次遍历中删除链表的倒数第 n
个节点,而不需要知道链表的具体长度。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
dummy_head = ListNode(next=head) # 创建一个虚拟节点,作为新的链表头部
# 初始快慢指针,都指向虚拟头节点
fast = slow = dummy_head
# 快指针先走n步
for i in range(n):
fast = fast.next
# 如果快指针已经到达链表末尾,说明链表长度小于n
if not fast:
return head
# 快慢指针同时前进,直到快指针到达链表末尾,移动 n-1 步
while fast.next:
fast = fast.next
slow = slow.next
# 删除倒数第n个节点
slow.next = slow.next.next # slow指向倒数第n+1个节点
# 返回虚拟头节点的下一个节点
return dummy_head.next
三、160.相交链表
文档链接:代码随想录
题目链接:160.相交链表
题目描述:
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
图示两个链表在节点 c1
开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
自定义评测:
评测系统 的输入如下(你设计的程序 不适用 此输入):
intersectVal
- 相交的起始节点的值。如果不存在相交节点,这一值为0
listA
- 第一个链表listB
- 第二个链表skipA
- 在listA
中(从头节点开始)跳到交叉节点的节点数skipB
- 在listB
中(从头节点开始)跳到交叉节点的节点数
评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA
和 headB
传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
— 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。
示例 2:
输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [1,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
提示:
listA
中节点数目为m
listB
中节点数目为n
1 <= m, n <= 3 * 104
1 <= Node.val <= 105
0 <= skipA <= m
0 <= skipB <= n
- 如果
listA
和listB
没有交点,intersectVal
为0
- 如果
listA
和listB
有交点,intersectVal == listA[skipA] == listB[skipB]
进阶: 你能否设计一个时间复杂度 O(m + n)
、仅用 O(1)
内存的解决方案?
代码:
- 双指针法
思路:如果两个链表相交,那么从相交点开始,它们的后续节点是相同的。如果两个链表不相交,那么它们的尾节点一定不同。想办法让两个链表长度相同,pA遍历(A+B)拼接的链表,pB遍历(B+A)拼接的链表。当两个链表相交时,pA和pB最终会同时到达相交的节点。
- 初始化两个指针
pA
和pB
分别指向链表headA
和headB
的头节点。 - 先遍历链表
headA
,同时使用pA
记录当前节点。当遍历完headA
时,我们将pA
重新定位到headB
的头节点。 - 同时,另一个指针
pB
从headB
开始遍历。当遍历完headB
时,我们将pB
重新定位到headA
的头节点。 - 由于两个链表的长度可能不同,较长的链表会先遍历完自己的节点,然后开始遍历另一个链表。较短的链表在遍历完自己的节点后,也会开始遍历另一个链表。
- 当两个指针都进入相交部分时,由于相交部分在两个链表中是共享的,所以两个指针会以相同的速度移动,并且最终会在相交的起始节点相遇。
- 当
pA
和pB
相遇时,它们要么都指向相交的起始节点,要么都指向null
(如果两个链表不相交)。 - 返回相遇点作为相交的起始节点。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
# pA指向链表headA的头节点,pB指向链表headB的头节点
pA,pB = headA,headB
# 当pA不等于pB时,继续循环
while pA != pB:
# 如果pA不为None,则将pA移动到下一个节点;如果pA到达尾部,将pA重新定位到headB的头节点
pA = pA.next if pA else headB
# 如果pB不为None,则将pB移动到下一个节点;如果pB到达尾部,将pB重新定位到headA的头节点
pB = pB.next if pB else headA
return pA # pA和pB相遇的点就是相交的起始节点,或者它们都是null(不相交)
四、142.环形链表II
文档链接:代码随想录
题目链接:142.环形链表II
视频讲解:视频讲解
题目描述:
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
提示:
- 链表中节点的数目范围在范围
[0, 104]
内 -105 <= Node.val <= 105
pos
的值为-1
或者链表中的一个有效索引
进阶: 你是否可以使用 O(1)
空间解决此题?
代码:
- 快慢指针法
思路:使用快慢指针法,定义两个指针,快指针每次移动两步,慢指针每次移动一步。如果链表中存在换,那么快慢指针最终会在环内的某个节点相遇。想办法让快慢指针在环开始的位置相遇。当相遇时,将其中一个指针指向头节点,然后让它们以相同的速度(每次一步)移动快慢指针,直到它们再次相遇。再次相遇时所在的节点位置就是环开始的位置。
-
初始化快慢指针:
- 将快慢指针
slow
和fast
都初始化为链表的头节点head
。
- 将快慢指针
-
检测环:
- 如果链表中没有环,
fast
指针将先于slow
。
- 如果链表中没有环,
-
判断链表是否有环:
-
同时移动快慢指针,快指针
fast
每次移动两步,慢指针slow
每次移动一步。 -
如果链表中有环,快指针和慢指针最终会在环内的某个节点相遇(即
slow == fast
)。 -
如果指针到达链表的尾部(即遇到
None
或null
)。 -
如果链表中有环,由于
fast
指针的移动速度是slow
指针的两倍,它们最终会在环内的某个节点上相遇。快指针到达链表尾部(即fast
或fast.next
为null
),说明链表无环,返回null
。
-
-
确定环的起始节点:
- 当快慢指针相遇时,我们已经知道链表中有环。
-
找到环的起始节点:
-
当快慢指针相遇时,说明链表有环。此时,将慢指针
slow
重新指向链表的头节点head
。 -
为了找到环的起始节点,我们将
slow
指针重新设置为链表的头节点,而fast
指针保持不变。 -
然后,两个指针都以相同的速度(每次一步)移动,直到它们再次相遇。
-
保持快指针
fast
不动(仍然在相遇点),然后同时以相同的速度(每次一步)移动快慢指针,直到它们再次相遇。 -
它们再次相遇的节点就是环的起始节点。
-
快慢指针再次相遇的节点就是环的起始节点。
-
原理:
-
当快慢指针在环内相遇时,慢指针已经走了
k
步(其中k
是环外因为从环的起始节点到相遇点的距离,与从头节点到相遇点的距离是相同的(这可以通过快指针走过的距离是慢指针的两倍链表的长度加上环内某段距离)。- k = x + y k = x + y k=x+y
-
快指针走的距离是慢指针的两倍,也就是
2k
步。-
2 k = x + y + y + z 2k = x + y + y + z 2k=x+y+y+z
-
z = x z=x z=x
-
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 定义快慢指针,起始时都指向头节点
fast=slow=head
# 使用while循环来移动指针,直到快指针到达链表的末尾或遇到环
while fast and fast.next:
fast = fast.next.next # 快指针每次移动两步
slow = slow.next # 慢指针每次移动一步
# 如果快慢指针相遇,说明链表有环
if slow == fast:
# 慢指针重新指向头节点,快指针保持在相遇点
slow = head
# 快慢指针再次同时移动,直到找到环的起始节点
while slow != fast:
slow = slow.next
fast = fast.next
# 返回环的起始节点
return slow
# 如果不存在环,返回None
return None