面试官:链表中环的问题你是如何处理的?

前言

设想一下,在面试的时候,面试官问你:来,请你说说如何判断一个链表里面是否有环?并且如果有环,找到环的起始节点

image.png

你心里想:呵呵,这还不简单?不就是一个简单的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;
}

这种方法很简单,接下来我们来看如何使用双指针思想啦解决这个问题。

双指针思想

首先,解决这个问题一共有两步对吧。先是要判断链表中是否有环,然后就是如果有环,找到环的入口位置。当然,上面那种方法也是这样做的,只不过在判断是否有环的过程中刚好能够找到起始节点。

我们先来判断链表中是否有环。

判断链表中是否有环

既然是双指针,那么就是快、慢两个指针。好,那么在遍历链表的时候,**如果链表没有环,那么快指针能够到达表尾,如果这个链表有环,那么慢指针和快指针一定会在某个位置相遇。**这就像操场长跑,一个人快一个人慢,只要时间足够,那么快的那个人一定会再次追上慢的那个人的。

那么这里可能会有人有疑问了,会不会快指针在快追上慢指针的时候,快指针刚好跳过去了导致两者不会相遇?

不会!为毛呢?且看我分析。

快指针一次走两个节点,慢指针一次走一个节点。当快指针快要追上慢指针的时候,快指针和慢指针的位置有两种情况,一种是距离一个节点,一种是距离两个节点,不会有其它的情况。(这里不要和我杠,我说的是快要追上慢指针的时候,其它距离都会变成这两种情况)

image.png

  • 假如距离一个节点,如情况1所示,fast和slow下一步都会到位置3,因此就相遇了
  • 假如距离两个节点,如情况2所示,fast下一步走到3,slow走到4,和情况1就一样了,两者距离为1个节点,下一步还会相遇

所以,如果有环,快慢指针一定会相遇。

我们已经分析好了如何使用快、慢指针判断链表中是否有环,如果有环,那么快、慢两个指针一定会相遇。接下来,我们来看如何找到环的起始节点。

找到环的起始节点

先放结论,先按快、慢指针方式寻找到相遇的位置,然后将两指针一个放在链表的头节点,一个放在相遇的位置,并改为相同速度推进,那么两指针就会在环的起始节点处相遇

嗯哼?是不是挺懵逼的?这是为什么呢?

这个我们从数学的角度来解决这个问题,假设快指针已经在环中走了N圈(不满一圈的按照一圈来算),之后才和慢指针相遇。同时在环中的相遇点是下图的Z点,环的长度为LEN。

image.png

那么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个节点?

那么这种方法:

  1. 第一次双指针,判断链表是否有环(两个快慢指针是否会相遇)
  2. 第二次双指针,一个固定在相遇位置不同,一个从相遇位置开始遍历,当两个再次相遇的时候就能确定环的长度K
  3. 第三次双指针,使用找倒数第K个节点的方式来找起始节点。首先,快、慢指针都在head节点,快指针先走到k+1位置的节点,慢指针不动,还在第一个节点,这样两个节点的距离就是k。然后两个指针一起走,同时判断快指针的next是不是和慢指针相等,如果相等,慢指针所在的地方就是环的起始位置。

理解到这里,你能否将代码写出来?

我是张小yu,创作不易,请多关照。stay hungry,stay foolish。

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张小yu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值