hello大家好~这篇文章接着之前的步伐,继续更新数据结构这一块的内容,对前面链表初步的文章进行补充,主要是对链表结构做出讲解。另外,本篇文章大多数为单链表,也就是说,只有一个next,大家注意分辨。
1、复制含有随机指针节点的链表
这也是一道比较经典的链表问题了,我给大家大致描述一下题目
有这样的一个链表结构:
class Node:
def __init__(self, val: int):
self.value = val # 节点的值
self.next = None # 指向下一个节点的指针
self.rand = None # 随机指针,可能指向链表中的任意节点或None
rand
指针是单链表节点结构中新增加的指针,rand
可能指向链表中的任意一个节点,也可能指向null
。给定一个由Node
节点类型组成的无环单链表的头节点head
,要求实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。
给大家来张图,便于理解
这题实际上有两种解法,考虑了不同的空间复杂度,我先来说说爆破法
也就是使用哈希表的map(有不懂哈希表的朋友可以看看我之前的文章),用额外空间来操作。首先第一步,设置map,key值为老链表节点的地址假设是1,value的值为克隆出来的节点,设置为1'。节点设置完成后,开始考虑指针。先设置next指针。1的next指针指向2,那么对应的,重复步骤一,找出2的克隆节点2',让1'指向2',这里就完成了第一个克隆节点next指针的设置。再来看rand指针,假设1的rand指针指向3,那么,重复步骤一,找到3',然后重复步骤二,将1'的rand指针指向3',完成rand指针的设置。接下来的节点就重复上述步骤,即可完成新链表的复制,只额外使用了一个map空间。
图示:
代码示例:
class Node:
def __init__(self, val: int):
self.value = val # 节点的值
self.next = None # 指向下一个节点的指针
self.rand = None # 随机指针,可能指向链表中的任意节点或None
def copy_random_list(head: Node) -> Node:
if not head:
return None # 如果链表为空,直接返回None
# 第一步:使用字典(哈希表)来保存原链表节点与新克隆节点的映射
# key 是原节点,value 是对应的克隆节点
node_map = {}
# 创建一个指针,用于遍历原链表
current = head
# 复制每个节点,并将新节点存入 map 中,完成第一步
while current:
# 创建新节点,值与原节点相同
node_map[current] = Node(current.value)
current = current.next
# 第二步:设置新节点的 next 和 rand 指针
current = head
while current:
# 先设置 next 指针
if current.next:
node_map[current].next = node_map[current.next]
# 设置 rand 指针
if current.rand:
node_map[current].rand = node_map[current.rand]
# 移动到下一个节点
current = current.next
# 第三步:返回复制的链表的头节点
return node_map[head]
#def copy_random_list(head: Node) -> Node:这里的node是代码注解的意思,告诉我们参数和返回值的类型,去掉也是对的
接着来看第二种,不用而外空间的做法。
这种方法的基本思想就是将复制的节点通过特定的位置关系,用老节点来确定新节点的位置,而每个节点的位置确定,就代表了每个指针也能确定下来。最后只要将新旧节点分离就OK。
首先,在原来每一个节点的后面添加其克隆节点,这样就可以通过老节点来找到新节点的位置,然后将rand指针,模仿老节点,把新的节点连起来,最后通过移动next指针,将新老节点分开。这样就可以做到不使用额外的空间,在原有的链表上面操作。
图示:
代码示例:
def copy_random_list(head: Node) -> Node:
if not head:
return None # 如果链表为空,直接返回None
# 第一步:在每个原节点后插入一个新节点
current = head
while current:
# 创建新节点,值与当前节点相同
new_node = Node(current.value)
# 将新节点插入到当前节点后面
new_node.next = current.next
current.next = new_node
# 移动到下一个原节点(跳过新插入的节点)
current = new_node.next
# 第二步:设置新节点的 rand 指针
current = head
while current:
if current.rand:
# 新节点的rand指向的是老节点rand指针所指节点的克隆节点
current.next.rand = current.rand.next
# 移动到下一个原节点(跳过新插入的节点)
current = current.next.next
# 第三步:将新旧链表分离
old_list = head # 原链表的指针
new_list = head.next # 新链表的指针
new_head = head.next # 保存新链表的头节点
while old_list:
# 将原链表的next指向原链表的下一个节点
old_list.next = old_list.next.next
# 将新链表的next指向新链表的下一个节点
if new_list.next:
new_list.next = new_list.next.next
# 移动到下一个节点
old_list = old_list.next
new_list = new_list.next
return new_head # 返回新链表的头节点
#这个代码其实不难,不懂的多看两遍就会了,主要难在想到这个方法
2、两个链表相交的一系列问题
这是一个链表里面难度较大的题目了,没见过或者不知道方法的人,确实是很难想到思路
题目要求大概是这样的:给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回null。
这题目的可能性确实是非常非常多,需要考虑到很多种情况,列如链表到底有无环,哪一个有环 ,是否相交。有很多种的结构,下面就给大家讲解一下。
首先先说一下,怎么判断链表是否有环。
这里同样也是两种方法,一种是哈希表,一种是快慢指针的方法。哈希表的做法就是建立一个set,遍历每一个节点,把节点记录在set里面, 每次新记录一个就查一次set,如果查到重复,就说明有环,如果遍历完都没查到,就说明没有环。
当然这个方法很慢,时间复杂度和空间复杂度都比较高。所以建议用快慢指针的方法。
这里先说明一点,如果一个链表有环,那他在遍历的时候一定停不下来,就是没办法走到null,如果停下来了,那他一定是在哪个节点多了一个next指针,就不是单链表结构了。
再来说说快慢指针。将两个指针从头节点开始移动,慢指针一次走一个,快指针一次走两个,如果最后他们在走完之前相遇,说明链表上有环。这里说明一点,因为快指针比慢指针快一倍,所以如果有环的话,那他们一定会在两圈之内相遇,这个是数学问题,这里就不证了,大家有兴趣的可以自己画图看看。
如果快慢指针相遇,那么在相遇处,快指针返回头节点,慢指针停留在原地,然后按照原本的步幅前进,他们下一次在环上相遇的节点一定是第一个入环节点(这个也是结论,记住就行)。
接着就是如何判断两个链表是否相交了
我们可以通过判断两个链表的结尾节点的地址是否一致,如果一致则一定相交了,反之则一定不相交。并且,通过在遍历链表的同时,用一个变量记录两个链表长度的差值,然后让长的链表先走差值步,再让两个指针一起同步幅出发(短的链表从头出发),最后一定会在第一个入环节点处相遇。
我们再来讨论一下其他情况。如果两个链表的入环节点都为空,那说明两个链表可相交,且没有环。如果一个链表入环节点为空,一个不为空,那说明两个链表不可能相交。如果两个入环节点都不为空,那么两个链表也可相交。我画图给大家看一下。
OK,思路差不多讲完了,接下来就来看看代码实现吧
class Node:
def __init__(self, val: int):
self.value = val
self.next = None
# 判断链表是否有环,如果有返回第一个入环节点,否则返回None
def detect_cycle(head: Node) -> Node:
if not head or not head.next:
return None
slow = head
fast = head
# 使用快慢指针判断链表是否有环
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 如果快慢指针相遇,说明链表有环
if slow == fast:
# 快指针回到头节点,慢指针不动,继续移动直到相遇
fast = head
while fast != slow:
fast = fast.next
slow = slow.next
# 相遇点即为第一个入环节点
return slow
return None
# 判断两个链表是否相交
def get_intersection_node(head1: Node, head2: Node) -> Node:
# 先判断两个链表是否有环,并得到入环节点
cycle_node1 = detect_cycle(head1)
cycle_node2 = detect_cycle(head2)
# 情况1:如果一个链表有环,另一个链表无环,必然不相交
if (cycle_node1 and not cycle_node2) or (not cycle_node1 and cycle_node2):
return None
# 情况2:如果两个链表都无环,判断是否相交
if not cycle_node1 and not cycle_node2:
return get_intersection_without_cycle(head1, head2)
# 情况3:如果两个链表都有环
return get_intersection_with_cycle(head1, head2, cycle_node1, cycle_node2)
# 情况2:无环的链表相交判断
def get_intersection_without_cycle(head1: Node, head2: Node) -> Node:
# 遍历链表,获取两个链表的长度
len1, len2 = 0, 0
cur1, cur2 = head1, head2
while cur1:
len1 += 1
cur1 = cur1.next
while cur2:
len2 += 1
cur2 = cur2.next
# 让较长的链表先走 |len1 - len2| 步
cur1, cur2 = head1, head2
if len1 > len2:
for _ in range(len1 - len2):
cur1 = cur1.next
else:
for _ in range(len2 - len1):
cur2 = cur2.next
# 同时移动两个指针,直到找到相交节点
while cur1 and cur2:
if cur1 == cur2:
return cur1
cur1 = cur1.next
cur2 = cur2.next
return None
# 情况3:有环的链表相交判断
def get_intersection_with_cycle(head1: Node, head2: Node, cycle_node1: Node, cycle_node2: Node) -> Node:
# 如果两个链表的入环节点相同
if cycle_node1 == cycle_node2:
# 链表在入环前相交,按无环链表的方法处理
return get_intersection_without_cycle_until_cycle(head1, head2, cycle_node1)
# 如果两个链表的入环节点不同,判断它们是否在同一个环中
cur = cycle_node1.next
while cur != cycle_node1:
if cur == cycle_node2:
return cycle_node1 # 相交于环内的某个节点
cur = cur.next
return None # 不相交
# 处理有环链表在入环前的相交情况
def get_intersection_without_cycle_until_cycle(head1: Node, head2: Node, cycle_node: Node) -> Node:
len1, len2 = 0, 0
cur1, cur2 = head1, head2
# 计算入环前的长度
while cur1 != cycle_node:
len1 += 1
cur1 = cur1.next
while cur2 != cycle_node:
len2 += 1
cur2 = cur2.next
# 让较长的链表先走 |len1 - len2| 步
cur1, cur2 = head1, head2
if len1 > len2:
for _ in range(len1 - len2):
cur1 = cur1.next
else:
for _ in range(len2 - len1):
cur2 = cur2.next
# 同时移动两个指针,直到找到相交节点
while cur1 != cur2:
cur1 = cur1.next
cur2 = cur2.next
return cur1
#前面讲的每一部分,代码都分成模块写出来了,根据情况调用不同模块就行。
3.一些小tip
本期文章内容也不少,但还挺好理解的我觉得。就是这些题目如果之前没看过的话,那可能其确实想不出来,所以还是建议大家多看几眼,万一以后面试或者笔试的时候遇到了呢
下一期可能更新机器学习的内容吧,论文终于是要写完了,但感觉开学之后事情是真的多,希望我们很快就能再见吧~
本期文章就到这里啦,祝大家学习生活顺利,拜拜~