「这是我参与11月更文挑战的第 8 天,活动详情查看:2021最后一次更文挑战」
刷算法题,从来不是为了记题,而是练习把实际的问题抽象成具体的数据结构或算法模型,然后利用对应的数据结构或算法模型来进行解题。个人觉得,带着这种思维刷题,不仅能解决面试问题,也能更多的学会在日常工作中思考,如何将实际的场景抽象成相应的算法模型,从而提高代码的质量和性能
链表中环的入口点
题目描述
给定一个链表,返回链表开始入环的第一个节点。 从链表的头节点开始沿着 next 指针进入环的第一个节点为环的入口节点。如果链表无环,则返回 null
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中
说明:不允许修改给定的链表
示例
示例 1
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点
示例 3
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环
提示:
- 链表中节点的数目范围在范围
[0, 10^4]内 105 <= Node.val <= 105pos的值为1或者链表中的一个有效索引
**进阶:**是否可以使用 O(1) 空间解决此题?
解题
解法一:散列表法
思路
用散列表法很简单,我们遍历这个可能带环的链表,并记录每一个结点,如果在遍历的过程中,发现某一个结点已经在散列表中,说明它就是环的入口点
代码
//散列表法
func detectCycle(head *LinkList.Node) *LinkList.Node {
mapNode := map[*LinkList.Node]string{}
for head != nil {
if _, ok := mapNode[head]; ok {
return head
}
mapNode[head] = "reached"
head = head.Next
}
return nil
}
解法二:双指针法
思路
通常像环的判断、删除链表中间结点、寻找环的入口点,都适合用双指针来解题。定义快慢指针
fast和slow,fast一次遍历两个结点,slow一次遍历一个结点。如果链表中有环,它们一定会在环中的某一个点相遇。假设从链表的头部到入口点的距离为a,入口点到它们相遇的点的距离为b,相遇点到环的入口点为c。假设在他们相遇的时候fast已经走完了环的n圈,那此时fast走的距离就是
a + n(b+c) + b = a + (n+1)b + nc
因为fast是slow速度的2倍,所以任意时刻,fast走的距离都是slow的2倍,所以
a + (n+1)b + nc = 2(a+b) ⟹ a = c+ (n−1)(b+c)
得到了 a = c + (n-1)(b+c) ,可以发现,从相遇点到入环点的距离加上 n-1圈的环长,恰好等于从链表头部到入环点的距离
所以,我们就可以在fast和slow相遇的时候,让fast指针重新指向链表的头结点。然后fast和slow再每次都遍历一个结点,最终他们相遇的地方就是环的入口点
代码
if head == nil || head.Next == nil {
return nil
}
if head.Next == head {
return head
}
fast, slow := head, head
for fast != nil {
if fast.Next == nil {
return nil
}
slow = slow.Next
fast = fast.Next.Next
if fast == slow {
ptr := head
for ptr != slow {
ptr = ptr.Next
slow = slow.Next
}
return ptr
}
}
return nil