链表是空节点,或者有一个值和一个指向下一个链表的指针,因此很多链表问题可以用递归来处理。
1. 找出两个链表的交点
例如以下示例中 A 和 B 两个链表相交于 c1:
A: a1 → a2
↘
c1 → c2 → c3
↗
B: b1 → b2 → b3
但是不会出现以下相交的情况,因为每个节点只有一个 next 指针,也就只能有一个后继节点,而以下示例中节点 c 有两个后继节点。
A: a1 → a2 d1 → d2
↘ ↗
c
↗ ↘
B: b1 → b2 → b3 e1 → e2
要求时间复杂度为 O(N),空间复杂度为 O(1)。如果不存在交点则返回 null。
设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。
当访问 A 链表的指针访问到链表尾部时,令它从链表 B 的头部开始访问链表 B;同样地,当访问 B 链表的指针访问到链表尾部时,令它从链表 A 的头部开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。
如果不存在交点,那么 a + b = b + a,以下实现代码中 l1 和 l2 会同时为 null,从而退出循环。
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
l1, l2 = headA, headB
while l1 != l2:
l1 = headB if not l1 else l1.next
l2 = headA if not l2 else l2.next
return l1
如果只是判断是否存在交点,那么就是另一个问题,即 编程之美 3.6 的问题。有两种解法:
- 把第一个链表的结尾连接到第二个链表的开头,看第二个链表是否存在环;
- 或者直接比较两个链表的最后一个节点是否相同。
2.1 链表反转
反转一个单链表。
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
递归法:
处理的技巧是 “不要跳进递归,而是利用明确的定义来实现算法逻辑”。
base case
为head.next == None
,如果链表只有一个节点的时候反转也是它自己,直接返回即可。- 当链表递归反转之后,新的头结点是
last
,而之前的head
变成了最后一个节点,同时链表的末尾要指向 None。
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
if head == None or head.next == None:
return head
cur = self.reverseList(head.next)
head.next.next = head # 将本节点的下一个结点指向自己
head.next = None # 本节点指向空,当回溯时本节点便会指向前一个节点,而原本的头节点便会指向空
return cur # 每次递归结束返回的都是同一个尾结点
迭代法:
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
pre = None
cur = head
while cur:
nex = cur.next
cur.next = pre
pre = cur
cur = nex
return pre
2.2 反转链表前 N 个节点
base case
变为n == 1
,反转一个元素,就是它本身,同时要记录后驱节点。- 链表反转直接把
head.next
设置为None
,因为整个链表反转后原来的head
变成了整个链表的最后一个节点。但现在head
节点在递归反转之后不一定是最后一个节点了,所以要记录后驱successor
(第 n + 1 个节点),反转之后将head
连接上。
def reverseN(head, n): # 反转以 head 为起点的 n 个节点,返回新的头节点
if n == 1:
successor = head.next # 记录第 n + 1 个节点
return head
last = reverseN(head.next, n - 1) # 以 head.next 为起点,需要反转前 n - 1 个节点
head.next.next = head
head.next = successor # 让反转之后的 head 节点和后面的节点连起来
return last
2.3 反转链表的一部分
反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
说明: 1 ≤ m ≤ n ≤ 链表长度。注意这里的索引是从 1 开始的。
输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL
- 如果
m == 1
,就相当于反转链表开头的n
个元素,也就是上一题 - 如果
m != 1
,前进到反转的起点触发base case
,将问题转化为反转前 n 个节点
class Solution:
def __init__(self):
self.successor = None
def reverseBetween(self, head: ListNode, m: int, n: int) -> ListNode:
# base case
if m == 1:
# 相当于反转前 n 个元素
return self.reverseN(head, n)
# 前进到反转的起点触发 base case,将问题转化为反转前 n 个节点
head.next = self.reverseBetween(head.next, m - 1, n - 1)
return head # 若 m != 1,反转部分节点之后的头节点仍然是 head
def reverseN(self, head, n): # 反转以 head 为起点的 n 个节点,返回新的头节点
if n == 1:
self.successor = head.next # 记录第 n + 1 个节点
return head
last = self.reverseN(head.next, n - 1) # 以 head.next 为起点,需要反转前 n - 1 个节点
head.next.next = head
head.next = self.successor # 让反转之后的 head 节点和后面的节点连起来
return last
2.4 k 个一组反转链表
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
给你这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
递归性质:
- 链表是一种兼具递归和迭代性质的数据结构。当我们以 2 个节点为一组反转链表之后,后面的节点仍然是一条链表,并且规模 (长度) 比原来的链表要小,这就是子问题。
算法流程:
- 先反转以
head
开头的k
个元素。 - 将第
k + 1
个元素作为head
递归调用reverseKGroup
函数。 - 将上述两个过程的结果连接起来
1. 迭代反转链表:
def reverse(a):
pre, cur, nxt = None, a, a
while cur != None:
nxt = cur.next # 存储下一个节点
cur.next = pre # 逐个节点反转,将当前节点的 next 指向上一个节点
pre = cur # 更新 pre 指针位置
cur = nxt # 更新 cur 节点位置
return pre
2. 反转部分链表,即反转区间 [a, b) 的元素,左闭右开:
def reverse(a, b):
pre, cur, nxt = None, a, a
while cur != b:
nxt = cur.next
cur.next = pre
pre = cur
cur = nxt
# 返回反转后的头节点
return pre
3. 实现 k 个一组反转链表:
class Solution:
def reverseKGroup(self, head: ListNode, k: int) -> ListNode:
if head == None: return None
a, b = head, head
for i in range(k):
if b == None: # 不足 k 个,不需要反转,base case
return head
b = b.next
new_head = self.reverse(a, b) # 反转前 k 个元素
a.next = self.reverseKGroup(b, k) # 递归反转后续链表并连接起来
return new_head
# 反转区间 [a, b) 的元素,左闭右开
def reverse(self, a, b):
pre, cur, nxt = None, a, a
while cur != b:
nxt = cur.next
cur.next = pre
pre = cur
cur = nxt
return pre # 返回反转后的头节点
最终递归完成之后的样子:
3. 归并两个有序的链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
- 递归法:两个链表头部值较小的一个节点与剩下元素的
merge
操作结果合并。
class Solution:
def mergeTwoLists(self, l1, l2):
if l1 is None:
return l2
elif l2 is None:
return l1
elif l1.val < l2.val:
l1.next = self.mergeTwoLists(l1.next, l2)
return l1
else:
l2.next = self.mergeTwoLists(l1, l2.next)
return l2
- 迭代法:
class Solution:
def mergeTwoLists(self, l1, l2):
prehead = ListNode(-1)
prev = prehead
while l1 and l2:
if l1.val <= l2.val:
prev.next = l1
l1 = l1.next
else:
prev.next = l2
l2 = l2.next
prev = prev.next
# 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
prev.next = l1 if l1 is not None else l2
return prehead.next
4. 从有序链表中删除重复节点
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
输入: 1->1->2
输出: 1->2
class Solution:
def deleteDuplicates(self, head: ListNode) -> ListNode:
if not head or not head.next:
return head
head.next = self.deleteDuplicates(head.next)
return head.next if head.val == head.next.val else head
class Solution:
def deleteDuplicates(self, head: ListNode) -> ListNode:
if not head:
return None
slow, fast = head, head.next
while fast != None:
if fast.val != slow.val:
slow.next = fast # nums[slow] = nums[fast]
slow = slow.next # slow += 1
fast = fast.next
slow.next = None # 断开与后面重复元素的连接
return head
5. 删除链表的倒数第 n 个节点
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
fast = head
while n > 0:
fast = fast.next
n -= 1
if not fast: return head.next
slow = head
while fast.next:
fast = fast.next
slow = slow.next
slow.next = slow.next.next
return head
6. 交换链表中的相邻节点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
给定 1->2->3->4, 你应该返回 2->1->4->3.
class Solution:
def swapPairs(self, head: ListNode) -> ListNode:
if not head or not head.next: # 如果链表没有节点或只有一个节点
return head
# 要交换的节点
first_node = head
second_node = head.next
# 交换节点
first_node.next = self.swapPairs(second_node.next)
second_node.next = first_node
# 头节点为 second_node
return second_node
class Solution:
def swapPairs(self, head: ListNode) -> ListNode:
dummy = ListNode(-1)
dummy.next = head
prev_node = dummy
while head and head.next:
# 要交换的节点
first_node = head;
second_node = head.next;
# 交换
prev_node.next = second_node
first_node.next = second_node.next
second_node.next = first_node
# 为下一次交换重新初始化 head 和 prev_node
prev_node = first_node
head = first_node.next
# 返回新的头节点
return dummy.next
7. 链表求和
给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。
你可以假设除了数字 0 之外,这两个数字都不会以零开头。
输入:(7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 8 -> 0 -> 7
class Solution:
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
s1, s2 = [], []
while l1:
s1.append(l1.val)
l1 = l1.next
while l2:
s2.append(l2.val)
l2 = l2.next
ans = None
carry = 0
while s1 or s2 or carry != 0:
a = 0 if not s1 else s1.pop()
b = 0 if not s2 else s2.pop()
cur = a + b + carry
carry = cur // 10
cur = cur % 10
cur_node = ListNode(cur)
cur_node.next = ans
ans = cur_node
return ans
8. 回文链表
def isPalindrome(self, head: ListNode) -> bool:
vals = []
current_node = head
while current_node is not None:
vals.append(current_node.val)
current_node = current_node.next
return vals == vals[::-1]
9. 分隔链表
给定一个头节点为 root 的链表, 编写一个函数以将链表分隔为 k 个连续的部分。
每部分的长度应该尽可能的相等: 任意两部分的长度差距不能超过 1,也就是说可能有些部分为 null。
这k个部分应该按照在链表中出现的顺序进行输出,并且排在前面的部分的长度应该大于或等于后面的长度。
返回一个符合上述规则的链表的列表。
输入:
root = [1, 2, 3], k = 5
输出: [[1],[2],[3],[],[]]
解释:
输入输出各部分都应该是链表,而不是数组。
class Solution:
def splitListToParts(self, root: ListNode, k: int) -> List[ListNode]:
length = 0
cur = root
while cur:
length += 1
cur = cur.next
mod = length % k
size = length // k
ans = []
cur = root
for i in range(k):
head = write = ListNode(None)
for j in range(size + (i < mod)):
write.next = ListNode(cur.val)
write = write.next
if cur:
cur = cur.next
ans.append(head.next)
return ans
直接拆分原链表
class Solution:
def splitListToParts(self, root: ListNode, k: int) -> List[ListNode]:
length = 0
cur = root
while cur:
length += 1
cur = cur.next
mod = length % k
size = length // k
ans = []
cur = root
for i in range(k):
head = cur
for j in range(size + (i < mod) - 1):
if cur: cur = cur.next
if cur:
cur.next, cur = None, cur.next
ans.append(head)
return ans
10. 链表元素奇偶聚集
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
输入: 2->1->3->5->6->4->7->NULL
输出: 2->3->6->7->1->5->4->NULL
将奇节点放在一个链表里,偶链表放在另一个链表里。然后把偶链表接在奇链表的尾部。
class Solution:
def oddEvenList(self, head: ListNode) -> ListNode:
if not head: return None
odd = head
even = head.next
even_head = even
while even and even.next:
odd.next = even.next
odd = odd.next
even.next = odd.next
even = even.next
odd.next = even_head
return head