160. 相交链表
力扣题目链接
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
图示两个链表在节点 c1
开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构。
示例 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
输出:No intersection
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
一、双指针法【推荐】
此解法参考:灵茶山艾府
- 【思路】让两个指针分别遍历两个链表,当一个指针到达链表末尾时,让它从另一个链表的头部开始继续遍历。这样两个指针最终会在相交点相遇,或者同时到达末尾(都为None)。
- 【步骤】
- 初始化两个指针
p=headA, q=headB
。 - 不断循环,直到
p=q
。 - 每次循环,
p
和q
各向后走一步。具体来说,如果p
不是空节点,那么更新p
为p.next
,否则更新p
为headB
;如果q
不是空节点,那么更新q
为q.next
,否则更新q
为headA
。 - 循环结束时,如果两条链表相交,那么此时
p
和q
都在相交的起始节点处,返回p
;如果两条链表不相交,那么p
和q
都走到空节点,所以也可以返回p
,即空节点。
- 初始化两个指针
- 【为什么相交的话pq一定相遇?】
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
p, q = headA, headB
while p != q:
p = p.next if p else headB
q = q.next if q else headA
return p # 返回相交节点(如果不相交,两个指针都会是None)
- 时间复杂度 O(m + n):其中 m 是第一条链表的长度,n 是第二条链表的长度。除了交点,每个节点会被指针 p 访问至多一次,每个节点会被指针 q 访问至多一次。
- 空间复杂度 O(1)
二、哈希法
- 【思路】先遍历一个链表,将所有节点存入哈希表,然后遍历另一个链表,检查每个节点是否在哈希表中。(用额外空间换取直观的解题思路)
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
# 将链表A的所有节点存入集合
visited = set()
cur = headA
while cur:
visited.add(cur)
cur = cur.next
# 遍历链表B,查找第一个在集合中的节点
cur = headB
while cur:
if cur in visited:
return cur
cur = cur.next
return None
- 时间复杂度 O(m + n)
- 空间复杂度 O(m) 或 O(n)
206. 反转链表
力扣题目链接
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?
“鱼咬尾”(三指针迭代法)【推荐】
- 【思路】使用三个指针
prev
、cur
、next
来逐个反转链表中每个节点的指向。 - 代码如下,可以发现整个迭代的循环体(
while
)中是一段漂亮的“鱼咬尾”,非常好记!
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
cur = head
prev = None
while cur:
nxt = cur.next
cur.next = prev # 把next指回上一个节点
prev = cur # 再把prev向前移动一位
cur = nxt # 再把cur向前移动一位
return prev # 注意最后一轮过后cur = None, 所以新的头节点是prev!!
- 时间复杂度 O(n)
- 空间复杂度 O(1)
- 【记住一个性质(反复用于下一题和25题)】反转结束后,从原来的链表上看
pre
指向反转这一段的末尾cur
指向反转这一段后面的下一个节点(对本题来说是None)
*92. 反转链表 II
给你单链表的头指针 head
和两个整数 left
和 right
,其中 left <= right
。请你反转从位置 left
到位置 right
的链表节点 (1-based),返回 反转后的链表 。
示例 1:
输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]
- 【思路】同上题,使用三个指针
prev
、cur
、nxt
来逐个反转链表中每个节点的指向。 - 【注意】与上题不同的是:
-
要找到
p0
,也就是left
位置(需要反转的开头位置)的上一位。——从dummy
走left - 1
步。 -
注意
pre
初始值与上一题一样也是None
而不是p0
! -
cur
从p0.next
开始迭代(向前走),但结束条件不是走到结尾None(while
),而是走一定的步数(right-left+1
),不过还是满足上题说到的性质 【一段“鱼咬尾”迭代后】:pre
指向反转这一段的末尾cur
指向反转这一段后面的下一个节点
-
最后还需要:
① 把p0.next.next
(反转这一段的开头)连到cur
(反转这一段后面的下一个节点)上;
② 把p0.next
连到pre
(反转这一段的末尾)上。
-
【注意】这两步的顺序一定不能换!! 不然第二步就不能通过p0.next
来访问 反转这一段的开头 的节点了
图与此性质源自:灵茶山艾府
class Solution:
def reverseBetween(self, head: Optional[ListNode], left: int, right: int) -> Optional[ListNode]:
p0 = dummy = ListNode(next = head)
for _ in range(left - 1): # 先移到ROI的前一位
p0 = p0.next
cur = p0.next
pre = None # 别混淆pre和p0,pre的初始值始终为none
for _ in range(right - left + 1): # 鱼咬尾循环,指定步数
nxt = cur.next
cur.next = pre
pre = cur
cur = nxt
p0.next.next = cur # 【注意】这两行顺序一定一定不能换!!
p0.next = pre
return dummy.next
- 时间复杂度 O(right)
- 空间复杂度 O(1)
234. 回文链表
力扣题目链接
给你一个单链表的头节点head
,请你判断该链表是否为回文链表 (向前和向后读都相同的序列)。如果是,返回true
;否则,返回 false
。
示例 1:
输入:head = [1,2,2,1]
输出:true
示例 2:
输入:head = [1,2]
输出:false
进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
一、转换为数组
- 【思路】将链表转换为数组,然后使用双指针判断是否回文。
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 将链表转换为数组
values = []
current = head
while current:
values.append(current.val)
current = current.next
# 使用双指针判断回文
left, right = 0, len(values) - 1
while left < right:
if values[left] != values[right]:
return False
left += 1
right -= 1
return True
- 时间复杂度 O(n)
- 空间复杂度 O(n)
二、双指针+反转链表【推荐】
此方法参考:灵茶山艾府
- 【步骤】
- 用快慢指针找到链表中点 (876. 链表的中间结点)
- 反转后半部分链表
- 比较前半部分和反转后的后半部分
- 【注意】循环条件要判断
head2
是否为空而不是head
是否为空。若判断head
是否为空,会错误地多循环一次(下图中2—>3并没有断开,继续2—>3—>None
),导致访问head2.val
出现空指针异常。
class Solution:
# 206. 反转链表
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
curr = head
prev = None
while curr:
tmp = curr.next
curr.next = prev
prev = curr
curr = tmp
return prev
# 876. 链表的中间结点
def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow = fast = head
while fast and fast.next: # 奇数长度,fast停在最后一个节点,slow停在正中心
slow = slow.next # 偶数长度,fast停在None节点,slow停在正中心(偏右)
fast = fast.next.next
return slow
# 234.本题
def isPalindrome(self, head: Optional[ListNode]) -> bool:
mid = self.middleNode(head)
head2 = self.reverseList(mid)
while head2:
if head.val != head2.val:
return False
head = head.next
head2 = head2.next
return True
- 时间复杂度 O(n)
- 空间复杂度 O(1)
141. 环形链表
力扣题目链接
给你一个链表的头节点 head
,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0
开始)。注意:pos
不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true
。 否则,返回 false
。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
快慢指针
此方法参考:灵茶山艾府
- 【思路】想象兔子和乌龟在同一环形跑道上,一个速度快、另一个速度慢。那么兔子必然在一段时间后追上乌龟(套圈)。对于链表来说,如果在链表中引入两个以不同速度(一个比另一个快一倍)前进的指针,在链表存在环的情况下,这两个指针必定会相遇。
- 【问:兔子会不会「跳过」乌龟,不和乌龟相遇?】
答:这是不可能的。如果有环的话,那么兔子和乌龟都会进入环中。进入环后用「相对速度」思考,乌龟不动,兔子相对乌龟每次只走一步,这样就可以看出兔子一定会和乌龟相遇了。 - 【问:为什么代码的 while 循环没有判断 slow 是否为空?】
答:slow 在 fast 后面,如果 fast 不是空,那么 slow 也肯定不是空。好比快人先去探路,慢人走的都是快人走过的路。
class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
slow = fast = head # 同时从起点出发
while fast and fast.next:
slow = slow.next # 乌龟走一步
fast = fast.next.next # 兔子走两步
if fast == slow: # 若相遇(套圈)
return True
return False
- 时间复杂度 O(n),其中 n 为链表的长度。
- 空间复杂度 O(1),仅用到若干额外变量。
142. 环形链表 II
力扣题目链接
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
不允许修改 链表。
示例1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
Floyd 判圈算法(快慢指针)
此题解参考:灵茶山艾府
- 【思路】与上一题一样,也是快慢指针(两倍速):
- 【关键结论】慢指针从相遇点开始,移动 a 步后恰好走到入环口(虽然可能会多次经过入环口)
- ==>于是此时,同速度移动head(或另一个从head出发的指针)和慢指针,它们相遇时就是入环口!
- 【推导如下】
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
fast = slow = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if fast == slow: # 快慢指针相遇点
while slow != head: # 直到head和slow相遇(a步)
slow = slow.next
head = head.next
return slow
return None
- 时间复杂度 O(n)
- 空间复杂度 O(1)
21. 合并两个有序链表
力扣题目链接
将两个升序链表合并为一个新的 升序(非递减顺序排列) 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
一、迭代法【推荐】
此题解搬运自:灵茶山艾府
- 【前置知识】
- 【思路】
- 【步骤】
- 创建一个哨兵节点(dummy head),作为合并后的新链表头节点的前一个节点。(这样可以避免单独处理头节点,也无需特判链表为空的情况,从而简化代码。)
- 比较
list1
和list2
的节点值,如果list1
的节点值小,则把list1
加到新链表的末尾,然后把list1
前移。如果list2
的节点值小则同理 【哪个小放/移哪个】;如果两个节点值一样,那么把谁加到新链表的末尾都是一样的,不妨规定把list2
加到新链表末尾。 - 重复上述过程,直到其中一个链表为空。
- 循环结束后,其中一个链表可能还有剩余的节点,将剩余部分直接全部加到新链表的末尾。
- 返回新头节点。
class Solution:
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
cur = dummy = ListNode() # 用dummy head 简化逻辑
while list1 and list2:
if list1.val < list2.val: # 哪个小加哪个
cur.next = list1 # 加到新链表中
list1 = list1.next # 前移1
else: # 注:相等的情况加哪个节点都是可以的
cur.next = list2
list2 = list2.next
cur = cur.next # 前移,指到本轮新加的节点上
cur.next = list1 or list2 # 或者cur.next = list1 if list1 else list2
return dummy.next # 返回新节点的头
- 时间复杂度 O(m + n)
- 空间复杂度 O(1)
- 【补充
res = list1 or list2
的用法】等价于res = list1 if list1 else list2
。or
操作符:- 如果第一个操作数为"真值",直接返回第一个操作数
- 如果第一个操作数为"假值",返回第二个操作数
二、递归法
此题解搬运自:灵茶山艾府
- 【前置知识】如何理解递归?计算机是怎么执行递归的?【基础算法精讲 09】
- 【思路】直接用
mergeTwoLists
当作递归函数:- 递归边界:如果其中一个链表为空,直接返回另一个链表作为合并后的结果。
- 如果两个链表都不为空,则比较两个链表当前节点的值,并选择较小的节点作为新链表的当前节点。例如
list1
的节点值更小,那么递归调用mergeTwoLists(list1.next, list2)
,将递归返回的链表接在list1
的末尾。
class Solution:
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
if list1 is None: return list2 # 注:如果都为空则返回空
if list2 is None: return list1
if list1.val < list2.val:
list1.next = self.mergeTwoLists(list1.next, list2)
return list1
list2.next = self.mergeTwoLists(list1, list2.next)
return list2
- 时间复杂度 O(m + n),其中 n 为 list1的长度,m 为 list2 的长度。
- 空间复杂度 O(m + n),递归栈空间
2. 两数相加
力扣题目链接
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
示例 2:
输入:l1 = [0], l2 = [0]
输出:[0]
一、迭代法
- 【思路】模拟加法运算。由于链表是逆序存储的(低位在前,高位在后),这正好符合我们从个位开始相加的习惯(从头节点开始加相当于从个位开始加):
- 逐位相加:从两个链表的头部开始,逐个节点相加
- 处理进位:当相加结果
≥10
时,需要向下一位进位 - 处理长度不一致:两个链表可能长度不同,短的链表结束后按0处理
- 处理最后的进位:所有位都处理完后,如果还有进位,需要新增一个节点
- 【步骤】
- 初始化:创建虚拟头节点和当前指针,设置进位标志为0
- 主循环:当任一链表未结束或存在进位时继续循环
- 获取当前位的值(链表为空则不用加(取为0))
- 计算当前位相加结果(包括进位)
- 创建新节点存储结果的该位数(对10的余数)
- 更新进位值(对10的整除数)
- 移动指针
- 返回结果:返回虚拟头节点的下一个节点(头节点)
class Solution:
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
cur = dummy = ListNode()
carry = 0 # 进位
while l1 or l2 or carry: # 有一个不是空节点,或者还有进位,就继续
if l1:
carry += l1.val # 加入节点值(这里直接用carry来承载本轮总数)
l1 = l1.next # 前移1
if l2:
carry += l2.val
l2 = l2.next
cur.next = ListNode(carry % 10) # 要先添加节点
cur = cur.next # 再前移cur(不然前面没东西啊)
carry //= 10 # 新的进位
return dummy.next # 返回头节点
- 时间复杂度 O(n),其中 n 为
l1
长度和l2
长度的最大值。 - 空间复杂度 O(1),返回值不算的话。
二、递归法
- 【思路】此方法题解参见:灵茶山艾府
写法1. 创建新节点
class Solution:
# l1 和 l2 为当前遍历的节点,carry 为进位
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode], carry=0) -> Optional[ListNode]:
if l1 is None and l2 is None and carry == 0: # 递归边界
return None
s = carry
if l1:
s += l1.val # 累加进位与节点值
l1 = l1.next
if l2:
s += l2.val
l2 = l2.next
# s 除以 10 的余数为当前节点值,商为进位
return ListNode(s % 10, self.addTwoNumbers(l1, l2, s // 10))
写法2. 原地修改
class Solution:
# l1 和 l2 为当前遍历的节点,carry 为进位
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode], carry=0) -> Optional[ListNode]:
if l1 is None and l2 is None: # 递归边界
return ListNode(carry) if carry else None # 如果进位了,就额外创建一个节点
if l1 is None: # 如果 l1 是空的,那么此时 l2 一定不是空节点
l1, l2 = l2, l1 # 交换 l1 与 l2,保证 l1 非空,从而简化代码
s = carry + l1.val + (l2.val if l2 else 0) # 节点值和进位加在一起
l1.val = s % 10 # 每个节点保存一个数位(直接修改原链表)
l1.next = self.addTwoNumbers(l1.next, l2.next if l2 else None, s // 10) # 进位
return l1
- 时间复杂度 O(n)
- 空间复杂度 O(n)