链表的双指针技巧
- 环形链表 I
单链表中的双指针技巧通常是快慢指针。即设置一快一慢的指针。
对于此题,判断链表中是否有环。我们设置快慢指针,如果有环,我们发现快指针会首先进入环的循环,然后迟早与慢指针相遇。如果无欢,快指针则会在与慢指针相遇前便到达尾结点。
计算复杂度: O(N)
空间复杂度: O(1)
class Solution:
def hasCycle(self, head: ListNode) -> bool:
if not head or not head.next:
return False
# 设置快慢指针
slow = head
fast = head.next
# 让慢指针追逐快指针直至相遇或者快指针慢指针到头
while fast:
# 相遇
if slow == fast:
return True
# 到头
if not fast.next:
return False
# 指针前移
slow = slow.next
fast = fast.next.next
return False
- 环形链表 II
这一题与上一题不同的点在于,我们不仅需要找到图中是否有环,还需要找到确切的分叉点。上一题中我们知道只要有环,快慢指针就一定会相遇,但是相遇点却不一定是分叉点,那我们可以尝试计算一下分叉点的位置。
假设每次循环slow指针走一步,fast走两步,设链表从开头到分叉点的距离为a,环的长度为b。那第一次相遇时,fast指针一定是比slow多走了n个环的长度的。所以我们有:
f a s t = 2 × s l o w f a s t = s l o w + n b n = 0 , 1 , 2 , 3... fast = 2\times slow \\ fast = slow + nb \\ n = 0,1,2,3... fast=2×slowfast=slow+nbn=0,1,2,3...
即 f a s t = 2 n b , s l o w = n b fast = 2nb, slow = nb fast=2nb,slow=nb
那我们就可以将fast指回head,并且以slow一样的速度前进,最终相遇时两者:
f
a
s
t
=
a
s
l
o
w
=
n
b
+
a
fast = a\\ slow = nb + a
fast=aslow=nb+a
就可以很方便得出a的长度,即环的分叉口
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
if not head or not head.next:
return
# 快慢指针直到第一次相遇
slow, fast = head, head
while True:
# 如果陷入末尾则无环
if not fast or not fast.next:
return
# fast 走两步,slow 走一步
slow = slow.next
fast = fast.next.next
if slow == fast:
break
# 第一次相遇后,slow走了nb,fast走了a + nb
# fast置于head,每次走一步,经过a步便可以与slow相遇与交点
fast = head
while slow != fast:
fast = fast.next
slow = slow.next
return fast
- 相交链表
给定两个长短不一的链表,他们有着共同的尾部,我们需要求尾部的起点,即他们的交点。
首先很明显,我们仍然使用双指针技巧,每个链表都被交点分成了两个部分,我们不妨设第一个链表长度为a + c,第二个为 b + c,c是尾部的长度。
判断是否是交点的条件便是上下指针第一次重合,但是如果都从头出发,除非a == b,不然由于从头到交点的路程不一样,他们只会先后到达交点。所以我们需要使得上下两个指针的起点到交点的距离相等。
所以思路明确,假设 a > b,我们可以让a的指针向前进 (b-a)步,以此作为起点,这样他们便可以前进相遇在交点了。
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
if not headA or not headB:
return
# 首先计算两个链表的长度 a + c , b + c
count_a = 0
count_b = 0
cur_a = headA
cur_b = headB
while cur_a.next or cur_b.next:
if cur_a.next:
cur_a = cur_a.next
count_a += 1
if cur_b.next:
cur_b = cur_b.next
count_b += 1
# 更长的那一条链表的指针先行
gap = count_a - count_b
cur_a = headA
cur_b = headB
if gap <= 0:
for _ in range(-gap):
cur_b = cur_b.next
else:
for _ in range(gap):
cur_a = cur_a.next
# 起点相同,最终相遇在交点,如果未相遇,则两条链表一定没有相交
while cur_a and cur_b:
if cur_a == cur_b:
return cur_a
cur_a = cur_a.next
cur_b = cur_b.next
return
- 删除链表的倒数第N个节点
依然是双指针,设置一前一后的指针,右边的指针触底时,左边的指针的next便是我们要删除的结点
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
if not head:
return
# 设置双指针,left起始位置为head, right起始位置为left后n位
dummy = ListNode(0, head)
left = dummy
right = head
for _ in range(n-1):
right = right.next
# 向右直至right触底
while right.next:
left = left.next
right = right.next
# 删除倒数第n个点
# left即倒数第n+1个点,其next指向倒数第n-1个点
left.next = left.next.next
return dummy.next
双指针总结
- 调用next字段之前,一定要检查结点是否为空
- 循环的结束条件一定要明确