LeetCode 剑指Offer 数据结构之 链表 总结 Part1
链表类的题目往往涉及到双指针(快慢),辅助栈,哈希表,递归等算法
剑指Offer中涉及到6道题,文本进行总结:
链表的类定义:
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
# 有时候还有不止一个链接 此时可以类比为二叉树,图等结构
第一题:
https://leetcode-cn.com/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/solution/
剑指 Offer 52. 两个链表的第一个公共节点
最经典的双指针相遇解法:
原理是设长度不同的两个链表长度分别为L1+C、L2+C, C为公共部分的长度,两个指针同步迁移,第一个指针走L1+C 再去走L2(必然走到C),第二个指针走L2+C,再去走L1(必然走到C).返回对面链表是为了确保都走过L1+L2+C。
链表题中常见的指针迁移:
node1 = head# 先定义指针(指向某个节点)
node = node.next # 指针迁移
题解1:
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
node1, node2 = headA, headB
while node1 != node2:
node1 = node1.next if node1 else headB
node2 = node2.next if node2 else headA
return node1
时间复杂度:O(M+N)
空间复杂度:O(1)
第二题:
https://leetcode-cn.com/problems/fu-za-lian-biao-de-fu-zhi-lcof/
剑指 Offer 35. 复杂链表的复制
请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
class Node:
def __init__(self, x: int, next: 'Node' = None, random: 'Node' = None):
self.val = int(x)
self.next = next
self.random = random# 随机会指向某个节点,类似图
要做到复制,必须新建数据结构,然后遍历原数据结构进行构建
本题类似于图的复制
1)图的遍历DFS,BFS等
2)图的构建or链表的构建(都要维护一个哈希表,记录节点数据(非指针),当遍历到新的节点时查表,如果没有遇到则放入,如果有则说明图中几点已经被遍历到过,或者链表有环)
算法:深度优先搜索
1)从头结点 head 开始拷贝;
2)由于一个结点可能被多个指针指到,因此如果该结点已被拷贝,则不需要重复拷贝;
3)如果还没拷贝该结点,则创建一个新的结点进行拷贝,并将拷贝过的结点保存在哈希表中;
4)使用递归拷贝所有的 next 结点,再递归拷贝所有的 random 结点。
class Solution:
def copyRandomList(self, head: 'Node') -> 'Node':
# 维护的哈希表key为原始链表,value为新copy的链表
def dfs(head):
if not head:
return None# 递归的终止出口
if head in visited:# 递归向一个全局哈希表中写数据
return visited[head]# 如果已经有了,则直接返回value
# 创建新结点
copy = Node(head.val, None, None)
visited[head] = copy# 做copy
# 类似二叉树的遍历
# 先next走到底,然后从倒数第一个节点的random开始走
copy.next = dfs(head.next)# copy.next就是head.next的所有
copy.random = dfs(head.random)# copy.random就是head.random的所有
return copy
visited = {}# 全局哈希表
return dfs(head)
算法:广度优先搜索
创建哈希表保存已拷贝结点,格式 {原结点:拷贝结点}
创建队列,并将头结点入队;
当队列不为空时,弹出一个结点,如果该结点的 next 结点未被拷贝过,则拷贝 next 结点并加入队列;
同理,如果该结点的 random 结点未被拷贝过,则拷贝 random 结点并加入队列;
def copyRandomList(self, head):
'''
:param head:
:return:
'''
visited = {}
def bfs(head):
if not head:
return head
clone = Node(head.val, None, None) # 创建新结点
queue = []
queue.append(head)
visited[head] = clone
while queue:
# 还没复制过的下方节点
tmp = queue.pop()
# 类似二叉树,左子树,右子树
if tmp.next and tmp.next not in visited:
visited[tmp.next] = Node(tmp.next.val, [], [])
queue.append(tmp.next)
if tmp.random and tmp.random not in visited:
visited[tmp.random] = Node(tmp.random.val, [], [])
queue.append(tmp.random)
visited[tmp].next = visited.get(tmp.next)
visited[tmp].random = visited.get(tmp.random)
return clone
return bfs(head)
算法:优化的迭代法
迭代解法:对于一个结点,分别拷贝此结点、next 指针指向的结点、random 指针指向的结点, 然后进行下一个结点…通过哈希表判断,如果遇到已经出现的结点,那么我们不用拷贝该结点,只需将 next 或 random 指针指向该结点即可
此时哈希表最终仍然需要O(N)的空间
考虑将哈希表优化为一个变量:
将链表进行拓展,在每个链表结点的旁边拷贝,比如 A->B->C 变成 A->A’->B->B’->C->C’,然后将拷贝的结点分离出来变成 A->B->C和A’->B’->C’,最后返回 A’->B’->C’
class Solution:
def copyRandomList(self, head: 'Node') -> 'Node':
if not head:
return head
cur = head
while cur:
new_node = Node(cur.val,None,None) # 克隆新结点
new_node.next = cur.next
cur.next = new_node # 克隆新结点在cur 后面
cur = new_node.next # 移动到下一个要克隆的点
cur = head
while cur: # 链接random
cur.next.random = cur.random.next if cur.random else None
cur = cur.next.next
cur_old_list = head # 将两个链表分开
cur_new_list = head.next
new_head = head.next
while cur_old_list:
cur_old_list.next = cur_old_list.next.next
cur_new_list.next = cur_new_list.next.next if cur_new_list.next else None
cur_old_list = cur_old_list.next
cur_new_list = cur_new_list.next
return new_head