算法通关村第一关 —— 链表中环的问题(黄金挑战)

目录

算法通关村第一关 —— 链表中环的问题(黄金挑战)

1. 判断是否有环

方法一 使用Hash

方法二 双指针

2. 确定环的入口

方法一 双指针

方法二 三次双指针


算法通关村第一关 —— 链表中环的问题(黄金挑战)

1. 判断是否有环

方法一 使用Hash

判断链表中是否有环是链表的经典问题,相对容易一些,最容易的方法是使用Hash,遍历链表的时候将元素放入到map或者set中,如果有环一定会发生碰撞。发送碰撞的位置也就是入口的位置,所以这个题so easy!让我们一起用代码来实现吧!

/**
 * 方法1:通过HashMap判断
 *
 * @param head
 * @return
 */
public static boolean hasCycleByMap(ListNode head) {
    Set set = new HashSet();
    while (head != null) {
        if (!set.add(head)) {
            return true;
        }
        head = head.next;
    }
    return false;
}

方法二 双指针

确定是否有环,最有效的方法其实是双指针。快指针一次走两步,慢指针一次走一步,如果快的能到达表尾就不会有环,如果存在环,那两指针必定会在某个位置相遇。

这里有一个疑问需要解决,因为两者每次走的距离不一样,会不会快的人在快追上的时候跳过去了导致两者不会相遇?

答案是不会!如下图所示,当fast快追上slow时,fast一定距离slow还有一个或者两个空格。

算法通关村第一关 —— 链表中环的问题(黄金挑战)_第1张图片如果只有一个空格,那么如图一所示,下一步fast和slow将在3相遇。

如果有两个空格,那么如图二所示,下一步fast到达3,slow到达4,回到情况1,因此也会相遇。

所以只要有环,两指针必定相遇。下面是具体的实现代码:

/**
* 通过双指针思想
* @param head
* @return
*/
public static boolean hasCycleByTwoPoint(ListNode head) {
    if (head == null || head.next == null) {
        return false;
    }
    ListNode fast = head;
    ListNode slow = head;
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        if (fast == slow)
            return true;
    }
    return false;
}

2. 确定环的入口

方法一 双指针

第一种题型我们已经学会判断是否有环了,那当环存在时,就一定会有入口,那么如何确定入口的位置呢,我们用图的方法来理解比较好。下图中两指针遍历到相遇位置(Z),然后将两只真分别放在表头(X)和相遇位置(Z),并以相同速度前进,则最后将在(Y) 相遇,下面我们来解释原理。

快指针多绕了一圈就相遇的情况

算法通关村第一关 —— 链表中环的问题(黄金挑战)_第2张图片

寻找入口过程为:

1)两指针一起遍历到第一次相遇,其中fast一次走两步,slow一次走一步

2)此时fast指针走了a+b+c+b步,slow指针走了a+b步,由行进速度可得以下关系式:

a+b+c+b = 2*(a+b) , 故a = c

因此两指针分别从X和Z以相同速度行进,他们最终一定会在Y点相遇,即为环的起始点。

  快指针多绕了多圈之后才相遇

如果fast指针在相遇前已经绕了n圈,那么他走过的距离为 a + (n+1)b + nc

此时slow指针只走了a + b, 由行进速度有如下关系式:

a + (n+1)b + nc = 2 *(a+b),此时b+c为环的长度,设为LEN

则 a = c + (n-1)LEN,说明在第二次相遇的时候,快指针从Z出发已经转了(n-1)圈回到Z,然后两指针再一起向前走c步,此时一起到达Y,则又相遇了,所以也是一样的道理,代码并不会改变,所以第二次相遇的位置就是环的入口。具体实现代码如下所示:

/**
 * 通过双指针实现
 *
 * @param head
 * @return
 */

public static ListNode detectCycleByTwoPoint(ListNode head) {
    if (head == null) {
        return null;
    }
    ListNode slow = head, fast = head;
    // 第一次遍历到相遇
    while (fast != null) {
        slow = slow.next;
        if (fast.next != null) {
            fast = fast.next.next;
        } else {
            return null;
        }
        if (fast == slow) {
            // 第二次遍历到相遇的位置即为环的入口
            ListNode ptr = head;         
            while (ptr != slow) {
                ptr = ptr.next;
                slow = slow.next;
            }
            return ptr;
        }
    }
    return null;
}

方法二 三次双指针

三次双指针的思想其实很好理解,就是如果我们通过两次双指针遍历确定了环的大小K和末尾结点,那么问题就可以退化成找倒数第k个结点了。因为我们知道环的入口就是整个链表刚好遍历一次的倒数第K个结点,K即为环的大小。下面我们用代码来实现以下:

public class Solution {
    // 找到入口
    public ListNode detectCycle(ListNode head) {
        if(head == null){
            return null;
        }
        ListNode slow = head, fast = head;
        // 判断是否相遇
        while(fast != null){
            slow = slow.next;
            if(fast.next !=null){
                fast = fast.next.next;
            }else{
                return null;
            }
            if(fast == slow){
                // 固定一指针,另一个遍历,得到环的长度
                fast = fast.next;
                int len =1;
                while(slow != fast){
                    fast = fast.next;
                    len++;
                }
            // 取到倒数第k个结点,即为环的入口
            return getKthFromEnd(head,len);
            }
        }
    return null;
}

    // 得到倒数第k个结点,注意最后fast != null 要改成fast != slow
    public static ListNode getKthFromEnd(ListNode head, int k) {
        ListNode fast = head;
        ListNode slow = head;

        while (fast != null && k > 0) {
            fast = fast.next;
            k--;
        }
        while (fast != slow) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }
}

你可能感兴趣的:(算法,链表,数据结构)