高频算法面试题(十五)- 链表中环的入口点

330 阅读3分钟

「这是我参与11月更文挑战的第 8 天,活动详情查看:2021最后一次更文挑战

刷算法题,从来不是为了记题,而是练习把实际的问题抽象成具体的数据结构或算法模型,然后利用对应的数据结构或算法模型来进行解题。个人觉得,带着这种思维刷题,不仅能解决面试问题,也能更多的学会在日常工作中思考,如何将实际的场景抽象成相应的算法模型,从而提高代码的质量和性能

链表中环的入口点

题目来源LeetCode-142. 环形链表 II

题目描述

给定一个链表,返回链表开始入环的第一个节点。 从链表的头节点开始沿着 next 指针进入环的第一个节点为环的入口节点。如果链表无环,则返回 null

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中

说明:不允许修改给定的链表

示例

示例 1

1.png

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2

2.png

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点

示例 3

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环

提示:

  • 链表中节点的数目范围在范围 [0, 10^4] 内
  • 105 <= Node.val <= 105
  • pos 的值为 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再每次都遍历一个结点,最终他们相遇的地方就是环的入口点

3.png 代码

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