判断链表中是否有环并找出环入口位置

本文章只对单向链表做出示例

如何理解链表存在环

存在环就说明链表的尾节点不是指向 null,而是指向了链表中的另一个节点,只有这样才会构成环,如下图所示就是一个存在环的单向链表:

如何判断链表存在环呢

1. Set对象

思路:直接遍历链表,并且判断当前节点是否存在于Set集合中,如果存在,那就说明当前链表存在环,如果不存在,那么我们就将当前链表节点存入Set集合中,并继续往后遍历,如果存在环,那么Set集合中就一定会发生碰撞,如果不存在环,那么就一定有一个节点的 next 指针指向null,所以循环也会终止。

const isCircleBySet (head) {
    if (null == head || null == head.next) {
        return false;
    }
    // 判断是否存在闭环
    const setBox = new Set();
    while (null != head) {
        // 判断set集合中是否包含当前节点
        if (setBox.has(head.next)) {
            return true; // 中断循环并抛出存在闭环
        }
        setBox.add(head.next);
        head = head.next;
    };
    // 循环结束未发现闭环
    return false;
};

2. 快慢双指针

思路:设想一下这个场景,如果说有两个人在圆形跑道上跑步,一个人一秒钟跑一米,另一个人一秒钟跑两米,然后他们两同时开始并且一直跑,只要体力允许,他们两人一定会在某一个节点相遇;相反,如果是直线跑道,那么他们就不可能相遇。

快慢双指针法主要步骤为:定义两个指针,一个 slow 指针,一次走一步;另一个 fast 指针,一次走两步(必须是两步后面有注释)。如果可以在某一个点满足 slow = fast,那么就说明存在环,否则 fast指针必然先到到终点。

const isByTwoPorint (head) {
    if (null == head || null == head.next) {
        return false;
    }
    // 两个指针
    const slow = head;
    const fast = head;
    // 试着想象一下链表不存在闭环且快指针落在最后两个节点的情况,就可以理解为什么这样判断了
    // 只判断快指针是因为如果不存在闭环可以早些退出循环,如果存在闭环则必定会在闭环中与慢指针相遇
    while (null != fast && null != fast.next) {
        slow = slow.next; // 慢指针一次一步
        fast = fast.next.next; // 快指针一次两步
        // 如果快慢指针值一致了,则代表存在闭环
        if (slow == fast) {
            return true;
        }
    }
    return false;
};
那么为什么快指针必须是两步呢?

我们先来看看假如快指针走 3 步会有什么问题?我们还是以文中开头的环形链表为例子,当第一次 fast 走了 3 步,slow 走 1 步之后,快慢指针位置如下图所示:

这时候 fast 继续走 3 步,而 slow 继续走 1 步,快慢指针位置如下图所示:

我们发现,快指针超过了慢指针,又跑到前面去了,虽然最终他们还是会相遇,但是这会导致一个问题,那就是当快慢指针相遇时,我们无法知道快指针在环形里面走了多少圈,也无法知道慢指针在环形里面走了多少圈,这会导致很难推断环的入口位置。

而如果快指针走 2 步,当慢指针也入环之后,每走一次,慢指针都会和快指针拉开 1 步距离;而反过来想,相当于是快指针每次都在以缩短 1 步的距离来追赶 slow 指针,因为每次只缩短 1 步,所以快慢指针一定不会出现错过相遇点的情况

快指针任何时候走的距离一定为慢指针的 2 倍

我们都知道 fast 指针在任何时候走的距离一定是 slow 指针的 2 倍,那么我们再来看一下下面这幅图:

上图中以节点为划分,有三段链表,abc 分别表示三段的距离,我们假设当快慢指针相遇的时候,快指针已经在环中走了 n 圈,那么就可以得到快指针走过的距离为:a+n*(b+c)+b,而慢指针走过的距离为 a+b根据快指针走的路程一定是慢指针的两倍,可以得到如下等式:

a+n*(b+c)+b = 2*(a+b) ,最终经过转换,得到 a=(n-1)*(b+c) + c

到这里可能有人会有疑问,快慢指针相遇的时候,为什么慢指针一定没有走完一圈呢?如果慢指针也走了 m 圈,那么慢指针走过的距离就是 a+m*(b+c)+b 了,但是这其实是不可能的。

假设一个链表15个节点,以第节点为环的入口,慢指针一次一步,快指针一次

则会出现以下情况:

1  1;   2  3;  3  5;   4  7;  5  9;  6  11;  7  13;  8  15; // 快指针未入环时

9  11; 10  13;  11  15;  12  11;13  13;   //  快指针入环后

得出:快指针是有可能走 n 圈的,但不会跨过慢指针

假设一个链表20个节点,以第个节点为环的入口,慢指针一次一步,快指针一次

则会出现以下情况:

1  1;  2  4;  3  7;   4  10;  5  13;  6  16;  7 19;  //  快指针未入环时

8  7;  9  10;  10  13;11 16;  12  19;  13  7;  14  10;  15  13;  16  16; //  快指针入环后

得出:快指针是有可能走 n 圈的并且会跨过慢指针

总结:存在环的情况下

        1.只要慢指针没有入环,快指针永远不可能与慢指针相遇。

        2.快指针在一次两步时,一旦慢指针入环则快指针以每步减少一节点的差距向慢指针追

        赶,绝对不会在环内有跨过慢指针的情况。

        3.快指针在一次  n  (n > 2)步时,一旦慢指针入环则快指针以每步减少 n - 1 节点的差距

        向慢指针追赶,可能会在环内有跨过慢指针的情况。

为什么快慢指针相遇时慢指针没有走完一圈?(fast 指针一次两步情况下)

我们假设这个环的长度为 x,那么当 slow 指针走完一圈时,需要走 x 步,而当 slow 指针走完 x 步,fast 指针已经走了 2x 步了,也就是走了两圈了,那么他们一定在某一个点相遇过了(因为他们不可能错过相遇点),所以当快慢指针第一次相遇时,慢指针是不可能走完一圈的。

3. 找环的入口位置

利用第三个指针找到环的位置

继续回到上面的等式:a=(n-1)*(b+c) + c,然后我们可以发现,其实 b+c 正好等于环的长度,也就是说:从链表头部到入环的距离(a)恰好等于从相遇点到入环点的距离(c)再加上 n-1 圈个环的长度。

这时候就有个有趣的现象了,如果 slow 和 fast 相遇了,那么这时候我们再定义一个指针指向链表起点,一次走一步,slow 指针也同步继续往后走,那么这两个指针就一定会在链表的入口位置相遇

const findCycleHead (head) {
    if (null == head || null == head.next) {
        return false;
    }
    // 两个指针
    const slow = head;
    const fast = head;
    // 试着想象一下链表不存在闭环且快指针落在最后两个节点的情况,就可以理解为什么这样判断了
    // 只判断快指针是因为如果不存在闭环可以早些退出循环,如果存在闭环则必定会在闭环中与慢指针相遇
    while (null != fast && null != fast.next) {
        slow = slow.next; // 慢指针一次一步
        fast = fast.next.next; // 快指针一次两步
        // 如果快慢指针值一致了,则代表存在闭环
        if (slow == fast) {
            const access = head; //定义一个新指针
            while (access != slow){
                access = access.next;
                slow = slow.next;
            }
            return access;//返回环的入口位置
        }
    }
    return null;
};

  • 24
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值