1.题目
如果一个链表中包含环,如何找出环的入口节点?例如,在如图所示的链表中,环的入口节点是节点3。
2.解题思路
链表中有没有环?什么样的节点是环的入口节点呢?
2.1 我的思路
可不可以用一个list记录链表中已经遍历过的节点,第一个出现重复的节点,应该就是环的入口节点吧。是否正确?经过实践,这种思路是可行的。时间复杂度是O(n),空间复杂度S(n)。
2.2 书中思路
面试官若是要求空间复杂度小于S(n),那么应该怎么来实现呢?
第一步,如何确定链表中是否有环呢?
利用两个速度不同的指针,一个指针一次走一步,另一个指针一次走两步。如果走得快的指针追上了走的慢的指针,那么链表中就有环,如果走的慢的指针走到了链表的末尾都还没有被走的快的指针追上,那么链表中没有环。
第二步,如何找到环的入口节点?
使用两个指针,指针之间的距离为环的长度。也就是说环中有n个节点,则第一个节点先在链表中移动n步,然后两个指针以相同的速度向前移动,当第二个指针指向环的入口节点时,第一个指针也已经走完一圈这个环,并且重新指向入口节点了。那么我怎么知道链表当中的环中有几个节点呢?
获取链表里的环中的节点数量是在第一步实现的。当第一步中快、慢指针相遇之后,从这两个指针指向的节点出发,并开始计数,当再次回到这个节点时,就得到了环中的节点数了。
3. 代码实现
3.1 我的思路
# 节点类
class ListNode(object):
def __init__(self, value, next_node=None):
self.value = value
self.next = next_node
# 从链表中找出环的入口节点
def entry_node_of_loop(head):
finded_nodes = {} # 已经出现过的节点
cur = head
while cur:
if finded_nodes.get(cur.value) != 1:
finded_nodes[cur.value] = 1
else:
return cur
cur = cur.next
# 若遍历完链表之后仍没有发现环的入口节点,表明此链表没有环
return
3.2 书中思路
# 节点类
class ListNode(object):
def __init__(self, value, next_node=None):
self.value = value
self.next = next_node
# 判断链表中是否有环
def meeting_node(head):
if head == None: # 空链表
return
slow = head.next
if slow == None:
return
fast = slow.next
while fast != None and slow != None:
if fast == slow:
return fast # 这里返回的是环中的一个节点,这个节点不一定是入口节点(notice)
slow = slow.next
fast = fast.next
if fast != None:
fast = fast.next
return
# 判断链表中是否有环
def meeting_node(head):
if head == None: # 空链表
return
slow = head
fast = slow.next
#while fast != None and slow != None: # 为甚还要判断slow是否为None,前面fast都判断过了啊
while fast != None:
if fast == slow: # 当快指针与慢指针再次相遇这种情况存在,则表明此链表是有环的
return fast # 这里返回的是环中的一个节点,这个节点不一定是入口节点(notice)
slow = slow.next # 慢指针先往前移动一步
fast = fast.next # 快指针也往前移动一步
if fast != None:
fast = fast.next # 快指针再往前移动一步
return
# 从链表中找出环的入口节点
def entry_node_of_loop(head):
m_node = meeting_node(head)
if m_node: # 表明链表中有环
# 获取环的长度
length = 1
count = 0 # 走了几步
p_node = m_node
while p_node.next != m_node:
length += 1
p_node = p_node.next
front = head # 领先length步的指针
behind = head # 落后length步的指针
while count < length:
front = front.next
count += 1
while front == behind:
front = front.next
behind = behind.next
return front
return
# 从链表中找出环的入口节点
def entry_node_of_loop(head):
m_node = meeting_node(head)
if m_node: # 表明链表中有环
# 获取环的长度
length = 1
count = 1 # 走了几步
p_node = m_node
while p_node.next != m_node:
length += 1
m_node = m_node.next
front = head.next # 领先length步的指针
behind = head # 落后length步的指针
while front == behind:
front = front.next
count += 1
if count > length:
behind = behind.next
return front
return
4.总结
链表一般用到双指针有两种情况,一种是两个指针速度一样,但是始终间隔一定距离,另一种就是两个指针速度不一样,一快一慢。
如果条件允许可以使用额外的空间来记录链表,例如字典,因为字典中根据key来找value的时间复杂度是O(1),所以我一般使用字典来降低时间复杂度。其实在leetcode中的许多题,要降低时间复杂度都可以通过字典来实现,因为字典的相关操作时间复杂度低,当然字典所耗费的空间会更大一些。
5.参考文献
[1]剑指offer丛书