前言
设想一下,在面试的时候,面试官问你:来,请你说说如何判断一个链表里面是否有环?并且如果有环,找到环的起始节点。
你心里想:呵呵,这还不简单?不就是一个简单的Hash碰撞嘛。
这时候你一顿分析:使用Hash,遍历的时候将节点存到集合中,如果有环,那么一定会发生碰撞,并且,发生碰撞的那个节点就是环的入口位置。
面试官听了之后,哎呦,不错哦这小伙,然后接着问:那你还有其它的解决办法吗?又或者说,只用O(1)的空间来解决这个问题?
因为谁都知道面试的时候提出的问题都是循序渐进的,就好像你向女孩子表白成功了之后,你会忍不住进阶一下——“亲一下呗”(臭不要脸-),一样的道理。
你一听,瞬间懵逼了。我焯,其它的办法?用栈?好像不行啊,这种方法和用Hash解决是一个思路啊…,你绞尽脑汁想了好一会儿,怯怯的对面试官说:额… 我没有想到其它更好的办法来解决这个问题。
那么到这儿,面试官基本上已经对你的初始感觉大打折扣了,后果你也就可想而之了。
本文就是来开拓你的思维的,其实除了上面那种使用Hash集合来解决问题,在这里,我还有两种方法,一种是双指针思想,还有一种是用三次双指针来解决这个问题。其实,面试的时候你只需要掌握好双指针思想就游刃有余了,后面那种三次双指针的方法你可以看看(当然如果你时间紧张的话完全可以不用看)。
接下来我就依次来介绍这三种方法。
Hash集合
这种方法就不做过多讲解了,这是最基础的解题思路,当然也是必须要掌握的,如果你已经完全掌握了这种方法,那么完全可以跳过直接去看用双指针思想解决。
先遍历一遍链表,将节点存储到集合中,接着看后面的节点是否和这个集合有碰撞,如果有,那么这个链表就有环,并且碰撞的节点就是环的起始节点。
附上这种方法的Java代码:
public ListNode delectCycle(ListNode head) {
ListNode pos = head;
Set<ListNode> visited = new HashSet<ListNOde>();
while (pos != null) {
// 判断是否发生碰撞
if (visited.contains(pos)) {
return pos;
} else {
// 存到集合中
visited.add(pos);
}
pos = pos.next;
}
// 没有环
return null;
}
这种方法很简单,接下来我们来看如何使用双指针思想啦解决这个问题。
双指针思想
首先,解决这个问题一共有两步对吧。先是要判断链表中是否有环,然后就是如果有环,找到环的入口位置。当然,上面那种方法也是这样做的,只不过在判断是否有环的过程中刚好能够找到起始节点。
我们先来判断链表中是否有环。
判断链表中是否有环
既然是双指针,那么就是快、慢两个指针。好,那么在遍历链表的时候,**如果链表没有环,那么快指针能够到达表尾,如果这个链表有环,那么慢指针和快指针一定会在某个位置相遇。**这就像操场长跑,一个人快一个人慢,只要时间足够,那么快的那个人一定会再次追上慢的那个人的。
那么这里可能会有人有疑问了,会不会快指针在快追上慢指针的时候,快指针刚好跳过去了导致两者不会相遇?
不会!为毛呢?且看我分析。
快指针一次走两个节点,慢指针一次走一个节点。当快指针快要追上慢指针的时候,快指针和慢指针的位置有两种情况,一种是距离一个节点,一种是距离两个节点,不会有其它的情况。(这里不要和我杠,我说的是快要追上慢指针的时候,其它距离都会变成这两种情况)
- 假如距离一个节点,如情况1所示,fast和slow下一步都会到位置3,因此就相遇了
- 假如距离两个节点,如情况2所示,fast下一步走到3,slow走到4,和情况1就一样了,两者距离为1个节点,下一步还会相遇
所以,如果有环,快慢指针一定会相遇。
我们已经分析好了如何使用快、慢指针判断链表中是否有环,如果有环,那么快、慢两个指针一定会相遇。接下来,我们来看如何找到环的起始节点。
找到环的起始节点
先放结论,先按快、慢指针方式寻找到相遇的位置,然后将两指针一个放在链表的头节点,一个放在相遇的位置,并改为相同速度推进,那么两指针就会在环的起始节点处相遇。
嗯哼?是不是挺懵逼的?这是为什么呢?
这个我们从数学的角度来解决这个问题,假设快指针已经在环中走了N圈(不满一圈的按照一圈来算),之后才和慢指针相遇。同时在环中的相遇点是下图的Z点,环的长度为LEN。
那么fast指针走过的长度为:a + N * (b + c) + b
slow指针走过的长度就是a + b
同时,fast指针和slow指针还有一个关系,那就是fast指针走过的距离为slow指针的两倍。
也就是a + N * (b + c) + b = 2 * (a + b)
化简一下就是a = c + (N - 1) * (b + c)
即a = c + (N - 1) * LEN
通过这个关系,就能够说明,当p1指针从head节点开始走,p2指针从Z节点开始走,那么两者刚好在入口处相遇,只不过p2指针先在环里面自己转了N-1圈。
没关系,这个数学关系不重要,至少我们确定了,从两个指针的相遇位置和整个链表的head节点开始匀速走,最终一定会在环的入口位置处相遇。
这样,我们整个过程就已经分析好了,其实并不难理解,代码也很好写出来,接下来上代码:
public ListNode delectCycle(ListNode head) {
// 判断是不是空链表
if (head == null) {
return null;
}
// 定义快慢指针
ListNode fast = head, slow = head;
// 寻找相遇位置
while (fast != null) {
slow = slow.next;
if (fast.next != null) {
fast = fast.next.next;
} else {
// 走到尽头,没有环
return null;
}
// 如果fast和slow相遇
if (fast == slow) {
ListNode ptr = head;
// 匀速推进
while (ptr != slow) {
ptr = ptr.next;
slow = slow.next;
}
return ptr;
}
}
return null;
}
OK,到这里,你已经掌握了通过双指针思想来解决这个问题。
以上说的两种方法已经足够了,接下来的这种方法理解上简单,但是代码写起来很复杂,你可以直接跳过不看,不过如果你想开拓一下你的思维也是可以的。
三次双指针
考虑一下,如果我们确定了环的大小为K,同时也确定环的末尾节点(环的末尾节点.next就是环的起始节点),那么这个问题是不是就转换成了找倒数第k个节点?
那么这种方法:
- 第一次双指针,判断链表是否有环(两个快慢指针是否会相遇)
- 第二次双指针,一个固定在相遇位置不同,一个从相遇位置开始遍历,当两个再次相遇的时候就能确定环的长度K
- 第三次双指针,使用找倒数第K个节点的方式来找起始节点。首先,快、慢指针都在head节点,快指针先走到k+1位置的节点,慢指针不动,还在第一个节点,这样两个节点的距离就是k。然后两个指针一起走,同时判断快指针的next是不是和慢指针相等,如果相等,慢指针所在的地方就是环的起始位置。
理解到这里,你能否将代码写出来?
我是张小yu,创作不易,请多关照。stay hungry,stay foolish。