30+链表问题(一):如何判断链表是否成环?

引子

在上一篇文章《30+链表问题:概览》的引言中,我提到了当初在校招时遇到的一道链表面试题。就是这道题了。下面是问题描述以及解决方案。

问题描述

给定一个链表,如何判断该链表是否成环?

解题思路

首先,一听到这个问题,脑子一下子映出下面这幅链表成环的图:

如图,上面就是一个已经成环的链表。标红的是头结点。

针对这个问题,我的解决方案是:

使用两个指针 slow、fast 遍历该链表,slow每次走一步,fast走两步(实际上,只需要两个指针一快一慢即可)。如果 fast 指针 走到头,那说明链表中不存在环。否则,两个指针在无尽的旅程中一定会相遇。

使用一快一慢指针的这种算法,称为龟兔算法。

为了说明白这个情况:我还专门做了动态图(实际上,为了做这个动态图,我还花30大洋买了个软件。。。。):

代码实现

这里,LinkedList是我自己定义的链表类、Node是我自己定义的结点类。源码请参考附件,或《30+链表问题:概览》

/**
 * 判断一个链表是否成环
 *
 * @param list 链表
 * @return true Or false
 */
private boolean isContainCircle(LinkedList list) {
    // 两个指针,刚开始都指向 头结点
    Node slow = list.getHead();
    Node fast = list.getHead();
    // 如果链表成环,则直到量指针相遇之前,该循环将一直进行;
    while (fast != null && fast.getNext() != null) {
        /* fast 每次走两步,slow 走一步;
         * 在 fast 为null 之前,slow一定不为 null */
        slow = slow.getNext();
        fast = fast.getNext().getNext();
        // 这里直接比较其地址即可
        if (slow == fast) {
            return true;
        }
    }
    return false;
}

我们写代码测试一下:

public static void main(String[] args) {
    Main runner = new Main();
    LinkedList list = runner.buildLinkedListWithCircle();
    boolean containCircle = runner.isContainCircle(list);
    System.out.println("该链表是否包含环:" + containCircle);
}

/**
 * 构建出示例中的包含环的链表
 *
 * @return 包含环的链表
 */
private LinkedList buildLinkedListWithCircle() {
    LinkedList list = new LinkedList();
    Node nodeOf2 = null;
    for (int i = 1; i <= 7; i++) {
        Node node = new Node(String.valueOf(i) + i);
        list.add(node);
        if (i == 2) {
            nodeOf2 = node;
        }
    }
    list.add(nodeOf2);
    return list;
}

上面构建出的链表即上面我给的图片中的链表。我们执行这段代码,得到的结果如下:

该链表是否包含环:true

如果我们备注掉 buildLinkedListWithCircle 方法中的 list.add(nodeOf2); 这一行代码,再执行,得到结果:

该链表是否包含环:false

问题拓展

现在我们已经能判断一个链表是否包含环了。那么新的问题又来了:

  • 如何找到碰撞点(即两指针相遇的节点)?
  • 如何找到链表环的长度?
  • 如何找到链表环的入口?

针对以上问题,其实突破口就是所谓的碰撞点。我又做了个示意图:

如上图,我假设链表头结点与链表环入口节点的距离为 x,碰撞点与链表环入口的距离为 y,假设链表环长 L。

当 slow 每次走一步、fast 每次走两步时,有以下定律:

1、碰撞点的位置不变,每次相遇都在同一个点。

2、

  • 当两指针从 碰撞点出发时,slow每走完一个环长,两指针都相遇。
  • 当 链表头结点与链表环入口节点的距离 x 小于或等于链表环长L时,若两指针从 头结点或者 碰撞点出发,slow 每走完一个环长,两指针相遇。
  • 当链表头结点与链表环入口节点的距离 x 大于链表环长L时,若两指针从头结点出发,则在他们第一次相遇时,slow走过的步数 t 为:若 x % L == 0,则 t = x;否则 t = (x / L + 1) * L;

3、

  • 当 链表头结点与链表环入口节点的距离 x 等于链表环长L时,碰撞点是链表环入口节点;
  • 当 链表头结点与链表环入口节点的距离x 小于 链表环长L时,链表头结点与链表环入口节点的距离x 等于 碰撞点与链表环入口的距离y;
  • 当 链表头结点与链表环入口节点的距离x 大于 链表环长L时,x 与 碰撞点到链表环入口节点的距离 y 相差 链表环的整数倍。即 x - y = u·L。u为一正整数。

4、从第三点我们可以得出:若两个每次走一步的指针 p1、p2分别同时从头结点、碰撞点出发,则他们相遇的节点即为入口节点。

需要注意的是,上面四条定律成立的前提条件——slow每次走一步,fast每次走两步!

上面定律的证明其实也很简单,更多的是需要理解:

假设 slow走过 t 步后,两指针相遇,那么有:

2t - t = n·L    =>   t = n·L,其中n 是一个正整数。

这个结果也就是说,两指针相遇时,slow 走过的步数和链表环环长成正比。所以,碰撞点的位置永恒不变。

对于第二点定律,分三种情况考虑,第一二中情况很简单,稍微理一理就明了了。对于第三种,画个图,其实也能理解(我实在是不知道该如何表达出我的理解~)。

针对第三点定律,在第二点基础上思考,很容易就得出结果了。主要是第三点,当链表头结点与链表环入口节点的距离x 大于链表环长时,对于slow指针,假设经过 t 步后,两指针相遇,有公式:

t = n·L     (定律一)

t + y - x = m·L

联合推出:

y = (m - n)·L + x       =>        x - y = (m - n)·L       =>        x - y = u·L

也就是说,x 与 y 的差值是 链表环长L的整数倍。

实际上,我看有的博主给出了证明。但是那个证明是不规范的,结果也是错误的。只有在 头结点到入口节点的距离x 小于 链表长度L 时,碰撞点到入口的距离才等于 头结点到入口的距离。这里我也想尝试着给出证明,但是最终并不能构建出合适的关系。

好在,这里的逻辑并不复杂,实际上,我们只要仔细思考,很容易就理解了。

对于定律四,其实就在上面定律三的基础上,稍微理解下,就可以得出。

我们分为三种情况考虑:x > y ,x = y,x < y。下面我用 x > y来举例。

如果 x - y = u·L,u > 0。有两个每次走一步的指针 p1、p2分别从 头结点、碰撞点出发。

由于碰撞点在链表环上,所以,两指针每走完一个环长L,p2指针会回到出发时的碰撞点。那么经过 u·L步后,p2 回到出发时的碰撞点,此时 p2 距离入口点的距离仍然为 y;而此时,p1 也走过了 u·L 步,p1 距离 走完 x 还剩下的距离为: x - u·L = y。也就是说,此时 p1 距离入口点的距离也为 y。

而对于另外两种情况,就简单很多了,这里我就不多加赘述了。

所以,当两个指针p1、p2分别同时从 头结点、碰撞点出发的话,他们相遇的点,就是链表环入口节点。现在,我们回到上面拓展的三个问题:

1、如何找到碰撞点?

其实这个就很简单了,和判断链表是否存在环算法一样,只是这里在 slow == fast 时,将slow 或者 fast 指向的节点返回即可。

/**
 * 获取碰撞节点
 *
 * @param list 一个包含环的链表
 * @return 碰撞节点
 */
private Node getMeetNode(LinkedList list) {
    Node slow = list.getHead();
    Node fast = list.getHead();
    while (fast != null && fast.getNext() != null) {
        slow = slow.getNext();
        fast = fast.getNext().getNext();
        if (slow == fast) {
            // 这里随便返回 fast 或者 slow 都可以
            return slow;
        }
    }
    throw new IllegalArgumentException("链表不含环!");
}

2、如何找到链表环的长度?

这个就很简单了,我们已知了碰撞点,那我们就直接记录在此遇到碰撞点时走过的步数,即可求得链表环长。

/**
 * 获取链表环长
 *
 * @param list 链表
 * @return 环长
 */
private int getLinkedListCircleLength(LinkedList list) {
    Node meetNode = this.getMeetNode(list);
    Node p = meetNode;
    int len = 0;
    while (p.getNext() != null) {
        p = p.getNext();
        ++len;
        if (p == meetNode) {
            return len;
        }
    }
    throw new IllegalArgumentException("链表不含环!");
}

3、如何找到链表环的入口?

这里我们使用两个每次都走一步的指针 p1、p2,分别同时从头结点、碰撞点出发,他们相遇的那个点,就是链表环入口。

/**
 * 获取 链表环的入口节点
 *
 * @param list 链表
 * @return 入口节点
 */
private Node getEntranceNode(LinkedList list) {
    // p1 从头结点出发
    Node p1 = list.getHead();
    // p2 从碰撞点出发
    Node p2 = this.getMeetNode(list);
    // 链表就一个环,相遇点就是头结点时,直接返回
    if (p1 == p2) {
        return meetNode;
    }
    while (p1.getNext() != null) {
        p1 = p1.getNext();
        p2 = p2.getNext();
        if (p1 == p2) {
            return p1;
        }
    }
    throw new IllegalArgumentException("链表不含环!");
}

最后,我们修改一下main方法:

public static void main(String[] args) {
    Main runner = new Main();
    LinkedList list = runner.buildLinkedListWithCircle();
    boolean containCircle = runner.isContainCircle(list);
    System.out.println("该链表是否包含环:" + containCircle);
    Node meetNode = runner.getMeetNode(list);
    System.out.println("碰撞点的值为:" + meetNode.getValue());
    Node entranceNode = runner.getEntranceNode(list);
    System.out.println("入口节点的值为:" + entranceNode.getValue());
    int circleLen = runner.getLinkedListCircleLength(list);
    System.out.println("链表环长为:" + circleLen);
}

执行,输出结果为:

该链表是否包含环:true
碰撞点的值为:77
入口节点的值为:22
链表环长为:6 

done。问题解决部分就讲到这里了。 

总结

在处理链表问题的时候,双指针、多指针是常见的结题方式。这种问题见得多了,感觉都是套路。

我感觉上面的定律证明的部分,写得并不严谨。我实在是没找到一种合理的数学证明方式。但是我相信,只要仔细去理解,其实还是没问题的。

文中代码作者只是为了做个演示,有的细节并没有做严格校验。目前没有发现有严重的错误,如果有发现问题的朋友,欢迎指出哦~希望和大家一起进步~

源码附件

源代码:https://download.csdn.net/download/zereao/11705715

GitHub:https://github.com/Zereao/LinkListInterviewQuestion/tree/master/LinkedListContainCircle

参考文章

1、https://javarevisited.blogspot.com/2013/05/find-if-linked-list-contains-loops-cycle-cyclic-circular-check.html

2、http://blog.sina.com.cn/s/blog_725dd1010100tqwp.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值