24. 两两交换链表中的节点
用虚拟头结点,这样会方便很多。
题目:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
思路:使用虚拟头结点会方便很多,要不然每次针对头结点(没有前一个指针指向头结点),还要单独处理。接下来就是交换相邻两个元素了,此时一定要画图,不画图,操作多个指针很容易乱,而且要操作的先后顺序需要搞清楚。
# 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: ListNode) -> ListNode:
dummy_head = ListNode(next=head)
current = dummy_head
# 必须有cur的下一个和下下个才能交换,否则说明已经交换结束了
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
return dummy_head.next
19.删除链表的倒数第N个节点
双指针的操作,要注意,删除第N个节点,那么当前遍历的指针一定要指向第N个节点的前一个节点。
题目:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
尝试使用一趟扫描实现
思路:双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。
算法:
- 使用虚拟头结点,这样方便处理删除实际头结点的逻辑
- 定义fast指针和slow指针,初始值为虚拟头结点
- fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作)
- 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)
cur=cur1=dummy_head
count=0
#计算链表的长度
while cur.next:
count+=1
cur=cur.next
# print(count)
#删除链表,找到要删除节点的前一个节点
while count-n:
cur1 = cur1.next
count-=1
# 删除节点
cur1.next=cur1.next.next
return dummy_head.next
# 第二种方式
# # 扫描一遍
# # 创建一个虚拟节点,并将其下一个指针设置为链表的头部
# dummy_head = ListNode(0, head)
# # 创建两个指针,慢指针和快指针,并将它们初始化为虚拟节点
# slow = fast =dummy_head
# # 快指针比慢指针快 n+1 步
# for i in range(n+1):
# fast = fast.next
# # 移动两个指针,直到快速指针到达链表的末尾
# while fast:
# slow=slow.next
# fast=fast.next
# ##注意:当fast指向空指针时,slow应该在倒数n+1的地方
# slow.next=slow.next.next
# return dummy_head.next
面试题 02.07. 链表相交
数值相同,不代表指针相同
题目:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。图示两个链表在节点 c1 开始相交:
思想:
- 相交意味着两个链表从某个节点开始共享相同的节点引用,而不仅仅是节点的值相同。这意味着链表中的相交点是指向同一个内存地址的节点对象。
- 简单来说,就是两个链表点的指针交点,不是数值相等,而是指针相等。看两个链表,目前curA 指向链表A的头点,curB指向链表B的头点,求出两个链表的长度,并求出两个链表长度的差值,然后让curA对齐,和curB空格的位置,如图
- 此时我们可以比较curA和curB是否相同,如果不相同,则同时向后移动curA和curB,如果遇到curA == curB,则找到交点。
- 否则循环退出空值。
# 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) -> ListNode:
# 解法1:求长度,同时出发
lenA,lenB=0,0
#求A的长度
cur=headA
while cur:
lenA+=1
cur=cur.next
#求B的长度
cur=headB
while cur:
lenB+=1
cur=cur.next
#下面是A更长时运行的代码,就算B更长,也让其变换一下,使得A最长
curA,curB=headA,headB
if lenB>lenA:
curA, curB = curB, curA
lenA, lenB = lenB, lenA
# 将A和B的末端对齐
for i in range(lenA-lenB) :
curA=curA.next
# 进行判断
for i in range(lenB):
if curA!=curB:
curA=curA.next
curB=curB.next
else:
return curA
return None
142.环形链表II
比较有难度的题目,确定环和找环入口。
题目:给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。不允许修改链表。
思路:
-
判断链表是否有环
- 小学经典问题,可以使用快慢指针,如果有环,快指针一定可以追赶上慢指针。
- 具体来说,分别定义快和慢指针,从头结点出发,快指针每次移动两个节点,慢指针每次移动一个节点,如果快和慢指针在途中相遇,说明这个链表有环。
-
如果有环,如何找到这个环的入口
- 假设从头结点到环形入口节点的节点数为x。 环形入口节点到快指针与慢指针相遇节点数为y。 从相遇节点再到环形入口节点数为z。 以下是所示:
- 相遇时:slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为一圈内节点的个数A。
- 因为快指针是一步走两个节点,慢指针一步走一个节点,所以快指针走过的节点数 = 慢指针走过的节点数 * 2:
- (x + y) * 2 = x + y + n (y + z),故x + y = n (y + z)
- 因为要找环形的入口,那么要求的是x,因为x表示头结点到环形入口节点的距离。所以要求x ,将x单独放在左面:x = n (y + z) - y
- 整理公式之后为如下公式:x = (n - 1) (y + z) + z注意这里n一定大于等于1的,因为快指针至少要多走一圈才能碰到慢指针。
当n=1时,x = z,慢指针和快指针同时从头节点出发,慢指针步长为1,快指针步长为2,两者在B处相遇,想要到达A处,还需要z步,刚好x=z,让慢指针回到头节点,同时快指针的步长设为1,两者同时出发,同时走z步之后在A处相遇。
n>1时,快指针在环形转n圈之后才遇到慢指针。其实周期和n为1的时候效果都是相同的,一样可以通过这个方法找到循环的入口节点,只不过,index1指向环里多转了(n-1)圈,通常会遇到index2,相遇点依然是循环的入口节点。
- 假设从头结点到环形入口节点的节点数为x。 环形入口节点到快指针与慢指针相遇节点数为y。 从相遇节点再到环形入口节点数为z。 以下是所示:
# 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]:
slow=fast=head
# 1.判断是否有环
while fast and fast.next:
slow=slow.next
fast=fast.next.next
if slow==fast:#2.相遇之后,判断入口
slow=head
while slow!=fast:#一直判断,直到相遇
slow=slow.next
fast=fast.next
return slow
return None
总结
什么时候使用虚拟头结点,什么时候不用虚拟头结点。一般涉及到增删改操作,用虚拟头结点都会方便很多, 如果只能查的话,用不用虚拟头结点都差不多。也可以方便记忆,统一都用虚拟头结点。
- 链表的种类为:单链表,双链表,循环链表
- 链表的存储方式:链表的节点在内存中是分散存储的,通过指针放在一起。
- 链表流量进行增删改查。
- 虚拟头节点:链表的一大问题就是操作当前节点必须要找前一个节点才能操作。这个操作捕获了头结点的尴尬,因为头结点没有前一个节点。每次分组情况都要单独处理,所以使用虚拟分组的技巧,就可以统一这种技巧。