剑指Offer-57-链表中环的入口节点

项目地址:https://github.com/SpecialYy/Sword-Means-Offer

题目

给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。

解析

预备知识

此处指的是单链表存在的环的情况,以下图为似:
剑指Offer-57-链表中环的入口节点_第1张图片
我们目标就是要找环的入口点,也就是说从链表首部出发,首次切入环的点,比如上图中3就是就是环入口。
所以我们需要解决的问题有2个:

  1. 如何判断链表有环?
  2. 在有环的情况,如何找到环的入口点?

首要问题是判断链表有环,受这道题的启发:https://blog.csdn.net/dawn_after_dark/article/details/80744040。
我们用2个指针,一个快指针和一个慢指针。可以发现若存在环,必然在某个时刻快指针可以追上慢指针。
若果没有环,快指针必然走到链表末尾,为null。

思路一

这里其实我不太想用数学来描述这个解法,所以我先举个例子。比如2个人在操场上跑圈,a的速度为1m,b的速度为2m,同时从起点出发,快的必然能追上a。又因为同时从起点出发,所以最后相遇点还是起点。i % n = (2i) % n,显然i = kn(k = 0, 1, 2)
那如果b先走k步,a还是在起点呢,然后再同时出发,因为此时b要先与k步,本来要在起点相遇的,现在由于起始位置都占据了有力优势,所以b追上a比之前要少k步,也就说这时相遇点为n - k。第一种情况其实就是第二种情况特例,即b先走0步,可以规划为一种来讨论。
这个场景可以很好的对应到链表上,假设让b就走到环的入口点(起点离环入口k步),然后a还是在起点处,同时出发,相遇点就是上面讨论的n - k了。又因为起点离环入口也是k步,所以可以让a重新回到起点,b的速度变为1,再同时起步,这样相遇时即为环的入口点,因为两者都是走了k步。

    **
     * 经典解法
     * @param pHead
     * @return
     */
    public ListNode EntryNodeOfLoop(ListNode pHead) {
        if(pHead == null || pHead.next == null) {
            return null;
        }
        boolean isIntersect = true;
        ListNode p = pHead;
        ListNode q = pHead;
        while(q != null && q.next != null) {
            p = p.next;
            q = q.next.next;
            if(p == q) {
                break;
            }
        }
        isIntersect = p == q ? true : false;
        if(isIntersect) {
            p = pHead;
            while(p != q) {
                p = p.next;
                q = q.next;
            }
            return p;
        }else {
            return null;
        }
    }

可能操场那个例子不太准确,下面我写一下数学的推导过程:
假设链表首部到环入口点距离为x,环长为c, 两者在环内相交的点距离环的入口为a,slow表示慢指针走的距离,fast表示快指针走的距离,m,n分别表示快慢指针在相遇时已经走得多少环。
2slow = fast (因为快指针的速度是慢指针速度的2倍)

slow=x+mc+k s l o w = x + m c + k

fast=x+nc+a f a s t = x + n c + a

2(x+mc+a)=x+nc+a 2 ( x + m c + a ) = x + n c + a

x=(n2m)ca=(n2m1)c+ca x = ( n − 2 m ) c − a = ( n − 2 m − 1 ) c + c − a

通过以上推导可以发现链表首部到入口距离的x实则为 c - a,而相遇点也正好离终点(起点)的距离为 c - a。所以可以采用同时按1步长前进,相交求得结果。

思路二

对于上图,我们发现只要使两个指针相遇在3节点,即可找到环切点。这时我们发现第一个指针比第二指针多走一个环长,即可在环切点相遇。
我们还是利用2个指针,只要让一个指针先走一个环长的步数,然后2个指针再同时走,即可在环切点相遇。
问题是如何求环长呢?
很简单,我们可以根据预备知识找到碰撞点,然后从碰撞点出发,统计直到下一次到达碰撞点所走的步数即可。

    /**
     * 计算环长
     * @param node
     * @return
     */
    public int caculateCircleLength(ListNode node) {
        ListNode start = node;
        int length = 0;
        do {
            length++;
            node = node.next;
        }while(node != start);
        return length;
    }

利用环长求其切点。

    /**
     * 利用环长法
     * @param pHead
     * @return
     */
    public ListNode EntryNodeOfLoop2(ListNode pHead) {
        if(pHead == null || pHead.next == null) {
            return null;
        }
        boolean isIntersect = true;
        ListNode p = pHead;
        ListNode q = pHead;
        while(q != null && q.next != null) {
            p = p.next;
            q = q.next.next;
            if(p == q) {
                break;
            }
        }
        isIntersect = p == q ? true : false;
        if(isIntersect) {
            int circleLength = caculateCircleLength(q);
            System.out.println(circleLength);
            q = pHead;
            while(circleLength-- > 0) {
                q = q.next;
            }
            p = pHead;
            while(p != q) {
                p = p.next;
                q = q.next;
            }
            return p;
        }else {
            return null;
        }
    }

思路三

这种思路会破坏链表的连通性,改进的办法是通过为每个节点添加标志位表明是否被断开即可。但还是不太推荐,仅供学习思想。
我们还是采用2个指针的做法,一个指向当前节点,一个指向当前节点的前驱节点。我们对遍历到每一个当前节点,都进行断链操作,即断开前驱节点到当前节点的连接。这样当当前节点为null,说明我们重新访问到了之前的节点,因为该节点的联通性在之前已经被断开了。
Note: 当前节点为null,也有可能走到了链表末尾,所以我们还是需要先判断是否有环,再采用此方式找切点。

    /**
     * 断链法
     * @param pHead
     * @return
     */
    public ListNode EntryNodeOfLoop3(ListNode pHead) {
        if(pHead == null || pHead.next == null) {
            return null;
        }
        boolean isIntersect = true;
        ListNode p = pHead;
        ListNode q = pHead;
        while(q != null && q.next != null) {
            p = p.next;
            q = q.next.next;
            if(p == q) {
                break;
            }
        }
        isIntersect = p == q ? true : false;
        if(isIntersect) {
            p = pHead;
            q = pHead.next;
            while(q != null) {
                p.next = null;
                p = q;
                q = q.next;
            }
            return p;
        }else {
            return null;
        }
    }

总结

链表相交的题重在采用多个速度不一的指针来做,至于相交点,则要靠联想能力或数学推导能力。

你可能感兴趣的:(不刷题心里难受,剑指Offer)