24. 两两交换链表中的节点
题目:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
解题思路
我们可以使用递归的方法来解决这个问题。递归的思路是:
- 1 首先反转前面两个节点。
- 2 然后递归地反转第三个节点开始的链表。
- 3 最后连接第一个节点和反转后的第三个节点。。
- 时间复杂度:O(n)
- 空间复杂度:O(n)
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 确保有两个节点
if head is None or head.next is None:
return head
prev = head
cur = head.next
nxt = head.next.next
# 反转前两个
cur.next = prev
prev.next = self.swapPairs(nxt)
return cur
总结
通过递归的方法,我们可以较为简洁地实现链表中两两交换节点的问题。递归的核心在于理解每一层递归的作用,以及如何通过递归处理剩余部分的链表。指针操作是实现过程中需要特别注意的地方,确保每一步操作都正确无误。
25. K 个一组翻转链表
题目:给你链表的头节点 head ,每 k **个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k **的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
解题思路
本题可以使用递归的方法,首先反转以head开头的一组,然后按照定义用reverseKGroup反转K+1节点开始的整个链表,再拼起来:
- 1 首先判断链表是否为空或只有一个节点,如果是,直接返回头节点。
- 2 找到第一组需要反转的节点范围,即从头节点开始,找到第 k 个节点。如果不足 k 个节点,说明无法完成反转,直接返回头节点。
- 3 反转第一组节点,调用
reverse函数完成反转,并获得反转后的头节点。 - 4 递归调用
reverseKGroup函数,处理剩余的节点。将第一组反转后的头节点与剩余部分链表连接起来。
- 时间复杂度:O(n)
- 空间复杂度:O(n/k),即递归调用的深度。
class Solution:
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
if head is None or head.next is None:
return head
# 找到第一组的范围
left, right = head, head
for i in range(k):
if right is None: # 不满k个节点,不反转,直接返回head
return head
right = right.next
# 反转第一组
newHead = self.reverse(left, right)
# 反转后面的组并拼接
left.next = self.reverseKGroup(right, k)
return newHead
def reverse(self, a:ListNode, b:ListNode)->ListNode:
''' 反转[a,b)区间内的链表 '''
prev = None
cur = a
while cur != b:
nxt = cur.next
cur.next = prev
prev = cur
cur = nxt
return prev # cur=b了,cur=b-1
总结
通过递归的方式,我们能够高效地对链表进行分组反转。该方法不仅清晰易懂,而且具有较好的时间和空间复杂度表现。需要注意reverse反转区间[left,right)的节点,不包括right。
19. 删除链表的倒数第 N 个结点
题目:给你一个链表,删除链表的倒数第 n **个结点,并且返回链表的头结点。
解题思路
本题首先使用快慢指针找到倒数n+1个节点,然后删除倒数第n个节点,注意需要用一个虚拟节点dummy来防止一个元素时候找倒数第2个元素失败。
- 时间复杂度:O(n)
- 空间复杂度:O(1)
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
# base case sz=1不用管
if head is None:
return head
# 删除
dummy = ListNode(next = head)
prev = self.findFromEnd(dummy, n+1) # 避免一个节点时候,找倒数第2个
prev.next = prev.next.next
return dummy.next
def findFromEnd(self,head:ListNode, n:int)->ListNode:
''' 找到链表倒数第n个节点 '''
# fast先走k步
slow, fast = head,head
for i in range(n):
fast = fast.next
# 同速前进,直到fast为空
while fast != None:
slow = slow.next
fast = fast.next
return slow
总结
该题的关键在于理解快慢指针的思路,并正确地使用指针来找到和删除链表的倒数第 N 个节点,快指针先走N步到空时,慢指针刚好是倒数第N个节点 。通过快慢指针的方法,我们可以高效地解决链表问题。
面试题 02.07. 链表相交
题目:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。图示两个链表在节点 c1 开始相交:
解题思路
通过让两个指针在两个链表之间来回移动,最终它们一定会在交点处相遇,或者指向null。
- 1 初始化两个指针
p1和p2,分别指向链表headA和headB的头节点。 - 2 让
p1和p2同时前进,当p1到达链表headA的末尾时,将其移动到链表headB的头节点;当p2到达链表headB的末尾时,将其移动到链表headA的头节点。 - 3 重复步骤2,直到
p1和p2指向同一个节点。这个节点就是两个链表的交点。如果两个链表没有交点,那么p1和p2最终会同时指向null,此时返回null。
- 时间复杂度:O(n + m)
- 空间复杂度:O(1)
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
# base case
p1, p2 = headA, headB
while p1 != p2:
p1 = headB if p1 is None else p1.next
# p2 = headA if p2 is None else p2.next # 不要复制
p2 = headA if p2 is None else p2.next
return p1
总结
解决此问题的关键是理解如何有效地遍历两个链表,通过交替移动指针,确保至少有一个指针始终跟随相交部分的路径。此外,注意在更新指针时避免错误地使它们指向同一节点。通过这种方式,我们可以在 O(n + m) 的时间内找到两个链表相交的第一个节点。
142. 环形链表 II
题目:给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
解题思路
快慢指针,slow走k步,fast走2k步,到一个相遇点后,把slow放回head,然后同速而行,再走k-m步就相遇(m为相交点到相遇点的距离)
- 时间复杂度: O(n),快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个index指针走的次数也小于链表长度,总体为走的次数小于 2n
- 空间复杂度: O(1)
#补充
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head is None:
return head
slow, fast = head, head
while fast.next != None:
slow = slow.next
fast = fast.next.next
# 相遇,跳出循环
if slow == fast:
break
# fast 遇到空指针说明没有环
if fast is None or fast.next is None:
return None
# 重新指向头结点,快慢指针同步前进,相交点就是环起点
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow
总结
这道题的难点主要在于理解快慢指针的原理,以及如何利用这个原理来找到环的入口节点。如果不理解这个原理,很难想到这种解法。另外,还需要仔细推导整个过程,确保每一步都是正确的。