删除链表的倒数第 N 个结点
力扣19
方法一:暴力解法。先遍历一遍链表得到链表长度 L ,第二遍指针遍历到第 L-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(0, head)
L = 0
cur = head
while cur:
L += 1
cur = cur.next
cur = dummy_head
for _ in range(L - n):
cur = cur.next
cur.next = cur.next.next
return dummy_head.next
时间复杂度:O(L);空间复杂度:O(1)。
方法二:栈。先依次入栈,弹出的第 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(0, head)
stack = []
cur = dummy_head
while cur: #依次入栈,虚拟头结点也要入栈,方便处理头结点
stack.append(cur)
cur = cur.next
for _ in range(n): #弹出n个节点
stack.pop()
prev = stack[-1] #得到栈顶元素即待删除节点的前一个节点
prev.next = prev.next.next
return dummy_head.next
时间复杂度:O(L);空间复杂度:O(L)(栈的开销)。
方法三:双指针法(使用一趟扫描实现)。让 fast 指向头结点,先移动 n 步,slow 指向虚拟头结点,此时 fast 比 slow 超前 n+1 个节点,然后 fast 和 slow 同时移动,直到 fast 指向链表末尾,此时 slow 指向待删除节点的前一个节点,执行删除操作即可。
# 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(0, head)
slow, fast = dummy_head, head
for _ in range(n): #初始定义就差一步,多走n步即可方便删除操作
fast = fast.next
while fast:
slow = slow.next
fast = fast.next
slow.next = slow.next.next
return dummy_head.next
时间复杂度:O(L);空间复杂度:O(1)。
相交链表
力扣160
求相交起始节点的指针,交点不是数值相等,而是指针相等。链表A长度为m,链表B长度为n。
方法一:暴力解法。把链表A的每个节点都加入哈希集合中,然后遍历链表B,判断节点是否在哈希集合中。
# 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]:
hashtable = set()
curA, curB = headA, headB
while curA: #链表 A 全部加入哈希集合
hashtable.add(curA)
curA = curA.next
while curB: #遍历链表 B 判断节点是否在哈希集合中
if curB in hashtable:
return curB
curB = curB.next
return None
时间复杂度:O(m+n);空间复杂度:O(m)。
方法二:栈。由于相交后的节点都相同,所以从后往前找不同即可。
# 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]:
la, lb = [], []
curA, curB = headA, headB
while curA: #链表 A 全部入栈
la.append(curA)
curA = curA.next
while curB: #链表 B 全部入栈
lb.append(curB)
curB = curB.next
ans = None
i, j = len(la) - 1, len(lb) - 1
while i >= 0 and j >= 0 and la[i] == lb[j]: #注意边界条件
ans = la[i]
i -= 1
j -= 1
return ans
时间复杂度:O(m+n);空间复杂度:O(m+n)。
方法三:双指针法。遍历两个链表得到长度差,将长链表对齐至短链表头结点的位置,同时向后移动,并且在过程中找到第一个相同的节点。
# 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]:
m, n = 0, 0
curA, curB = headA, headB
while curA:
m += 1
curA = curA.next
while curB:
n += 1
curB = curB.next
curA, curB = headA, headB
if m > n: #固定 B 指向较长的链表
m, n = n, m
curA, curB = curB, curA
for _ in range(n - m): #将长链表对齐至短链表
curB = curB.next
while curA:
if curA == curB:
return curA
curA = curA.next
curB = curB.next
return curA
时间复杂度:O(m+n);空间复杂度:O(1)。
方法四:双指针法。指针A和指针B分别指向两个链表的头结点,同时移动两个指针,如果指针不为空则移到下一个节点,如果指针A为空则指向链表B的头结点,如果指针B为空则指向链表A的头结点,在此过程中,如果两个指针指向同一个节点或者都为空时,返回指向的节点。
证明:记相交后相同的节点长度为 c,链表A相交前节点数为 m-c,链表B相交前节点数为 n-c。
如果 m = n,m-c=n-c,相交时两个指针会同时走到交点,否则同时走到 None。
如果 m ≠ n,指针A先遍历完链表A,再遍历链表B直到交点时走过了 m+(n-c) 个节点,指针B先遍历完链表B,再遍历链表A直到交点时走过了 n+(m-c) 个节点,由于 m+(n-c) = n+(m-c),此时两个指针指向同一个节点,如果相交(c>0)指向要求的交点交点,如果不相交(c=0)指向 None 节点。
# 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]:
if (not headA) or (not headB):
return None
curA, curB = headA, headB
while (curA != curB): #两个指针指向不同节点时继续按规则遍历
curA = curA.next if curA else headB
curB = curB.next if curB else headA
return curA
时间复杂度:O(m+n);空间复杂度:O(1)。
环形链表 II
力扣142
方法一:哈希表暴力解法。遍历每个节点,如果不在哈希表中就添加,如果在就找到了环的入口。
# 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]:
hashtable = set()
cur = head
while cur:
if cur in hashtable:
return cur
hashtable.add(cur)
cur =cur.next
return cur
时间复杂度:O(n);空间复杂度:O(n)。
方法二:双指针法。分两步,第一步判断是否有环,第二步如果有环如何找到环的入口。
是否有环?方法:分别定义 fast 和 slow 指针,从头结点出发,fast 指针每次移动两个节点,slow 指针每次移动一个节点,如果 fast 和 slow 指针在途中相遇 ,说明这个链表有环。证明:fast 指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇。fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。
如果有环如何找到环的入口?方法:从头结点和相遇节点开始同时遍历,一次一个节点,再次相遇的节点即为入环点。
证明:(包含左不包含右)假设从头结点到环形入口节点的节点数为x,环形入口节点到fast指针与slow指针相遇节点的节点数为y,从相遇节点再到环形入口节点的节点数为 z,也就是说环的长度L=y+z。
当slow指针刚进入环时,要走 L 步才能走完一圈,slow指针刚进入环时fast指针一定已经在环内了,且两个指针最多差L-1步,以slow指针为参考系,fast指针在以每次一节点的速度靠近slow指针,最多需要L-1次操作就能追上slow指针,所以两个指针相遇时slow指针一定在第一圈,一共只走了x+y步。
两个指针相遇时,slow指针走过节点数为x+y,fast指针走过节点数为x+y+nL,时间相同可以得到,化简得到
,于是可知再额外使用一个指针p指向头结点,与slow指针同时移动,都是每次一个节点,当slow多转了 n-1 圈时,两个指针会在入环节点相遇。
# 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, head
while fast and fast.next:
# fast 一次移动两个节点,如果有 None,说明无环结构
fast = fast.next.next
slow = slow.next
if fast == slow: #指针相遇,说明存在环结构
p = head
while p != slow: #再次寻找相遇节点,此相遇节点即为入环点
p = p.next
slow = slow.next
return p
return None
时间复杂度:O(n+n) → O(n);空间复杂度:O(1)。
学习自【代码随想录】