Linked List Cycle

所谓拳不离手曲不离口,算法和基础不能丢。

话说三藏师徒四人辞别女儿国,再上西行路,今天来到。。。啊呸,扯远了。。

今天来看这个,https://oj.leetcode.com/proble…,题目是这么说的

Given a linked list, determine if it has a cycle in it.

Follow up:
Can you solve it without using extra space?

这题原本是简单,他页面上提供了一个在线的编辑器,带语法高亮和自动补全,我三分钟直接在线写完,一次性提交就编译通过同时 Accepted,想想上次刷 OJ 还是很久之前了,如今依然没有手生,还是很开心的,土豪解法如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
 #include <set>
class Solution {
public:
    bool hasCycle(ListNode *head) {
        ListNode* curr = head;
        while (curr) {
            if (viewed.find(curr) != viewed.end()) {
                return true;
            }
            viewed.insert(curr);
            curr = curr->next;
        }
        return false;
    }
    std::set<ListNode*> viewed;
};

类和函数是他给好的,我们要做的就是在类里面填空而已。

可以看到,这个解法不论是在时间还是空间上,都是土豪,何止土豪,那是相当土豪。不过,实际项目中,其实这么写是可以的,稳定,不出错,可维护性高。

不过,原题里还有这么一句,Can you solve it without using extra space?,这个很明显就是见功夫的地方了,要抠时间和空间,时间上是没啥可压缩了,至少也要遍历一次,空间上嘛,他要求没有额外空间,怎么搞呢,想了想,想起来之前看书的时候看到这个题,当时说的是,用两个指针,一个每次走一步,一个每次走两步,如果这两个指针遇上了,那么就说明有环,具体写起来,可以这么写:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */

class Solution {
public:
    bool hasCycle(ListNode *head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while (1) {
            if (slow) {
                slow = slow->next;
            }
            if (fast) {
                fast = fast->next;
                if (fast) {
                    fast = fast->next;
                }
            }
            if (fast == NULL && slow == NULL) {
                return false;
            }
            if (slow == fast) {
                return true;
            }
        }
    }
};

又是一次 Accepted,我都怀疑是不是用例太弱了。。

需要注意的是,如果把最后的两个 if 判断对调位置,变成这样

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */

class Solution {
public:
    bool hasCycle(ListNode *head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while (1) {
            if (slow) {
                slow = slow->next;
            }
            if (fast) {
                fast = fast->next;
                if (fast) {
                    fast = fast->next;
                }
            }
            if (slow == fast) {
                return true;
            }
            if (fast == NULL && slow == NULL) {
                return false;
            }
        }
    }
};

那就会导致 WA,具体为什么可以想想,不难想出来。

不过,这个题目还有一个变式,要求找出环的入口节点,题目在这里,https://oj.leetcode.com/proble…,这个题的解法很是有技巧,分析思路可以看到这里,http://blog.reetsee.com/archiv…

我们设链表的无环的部分长度为L1,即有L1个节点,注意,这个L1是包括环的入口节点的。然后让环的长度是L2,这个L2也是包括环的入口节点。这个时候,p1和p2的交点如图所示,交点距离环的入口节点为a(从入口节点沿着行走方向走到交点),即在环的入口节点后面的第a个节点,就是交点,我用红色标记出a。

然后我们来考察一下L1,L2,a,以及n(n是走过的步数,不是走过的节点数,p1一步一个节点,p2一步两个节点)的关系。

忘记说一点了,我们可以明确的是,p1在进入环后,走了不到一圈就在交点处和p2重合,為什麼肯定没有走完一圈?因为p1在进入环的时候,p2和p1之间的距离(沿着行走方向)至多为 L2-1,不可能超过L2-1,因为环的大小也才只有L2 。p2追赶p1,最多只需要走L2-1步,因为每走一步,p1和p2的相对距离减小1,那么p1最多只走了L2-1步,就是最多只经过了L2-1个节点,不可能走完一圈。

现在可以列公式了:

L1+a=n #1 //n是p1走过的节点数
L1+k*L2+a=2*n #2 //2*n这个是p2走过的节点数,其中的k表示p2可能在环里面走了k圈,k>=1
由#2式减去#1式,有:

k*L2 = n #3
同时由#1和#3得到:

L1+a = k*L2 #4
接着由#4就得到了如下式:

L1 = k*L2 – a = (k-1)*L2 + (L-a)
得到这条式子就拨得云开见月明啊有木有,因为(L-a)表示的是交点与环入口的距离(从交点沿着行走方向到环入口),然后(k-1)是>=0的,因为p2在环中至少绕了一圈,这样我们就发现:L1的长度 = 环长度的整数倍 + 交点与环入口的距离

也就是说,p1再走L1步就可以达到环的入口。问题是L1不是已知的,没关系,在表头设置一个p3指针,p3每步前进一个节点。让p1和p3同时走,每次走1步,等p3和p1重合了,就是到了环口的位置了。Problem solved~

很自然的,写出下面的代码

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode *fast(head), *slow(head), *p1(head);
        while (1) {
            if (slow) {
                slow = slow->next;
            }
            if (fast) {
                fast = fast->next;
                if (fast) {
                    fast = fast->next;
                }
            }
            if (fast == NULL && slow == NULL) {
                return NULL;
            }
            if (slow == fast) {
                while (1) {
                    slow = slow->next;
                    p1 = p1->next;
                    if (p1 == slow) {
                        return p1;
                    }
                }
            }
        }
    }
};

但是这样是 WA 的,OJ 很仁慈的把错误说出来了

Input: {1,2}, tail connects to node index 0
Output: tail connects to node index 1
Expected: tail connects to node index 0

也就是说,考虑这种情况:整个链表只有两个节点,并且这两个节点构成一个环,那么上述代码就出错了,打个补丁:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode *fast(head), *slow(head), *p1(head);
        while (1) {
            if (slow) {
                slow = slow->next;
            }
            if (fast) {
                fast = fast->next;
                if (fast) {
                    fast = fast->next;
                }
            }
            if (fast == NULL && slow == NULL) {
                return NULL;
            }
            if (slow == fast) {
                if (slow == head) {
                    return head;
                }
                while (1) {
                    slow = slow->next;
                    p1 = p1->next;
                    if (p1 == slow) {
                        return p1;
                    }
                }
            }
        }
    }
};

得以 Accepted,而其实,上述的代码,是可以进一步改的,注意到我们外面的那个循环,为了保守起见,是等慢指针遍历到尾,才退出的,其实根本没必要,只要快指针遇到 NULL 了,就可以撤了,如下

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode *fast(head), *slow(head);
        while (fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
            if (slow == fast) {
                ListNode *p(head);
                while (p != slow) {
                    slow = slow->next;
                    p = p->next;
                }
                return p;
            }
        }
        return NULL;
    }
};

——————————–

2014-11-21 09:43:45 update

另外的一个思路是,每次前进一个节点,就把这个节点的 next 指针指向 head,这样,如果发现在某个节点被处理之前,就已经指向 head,那么就可以知道有环了,而且这个方法还可以天然的找出环的入口,不需要额外空间,缺点是破坏链表,一次处理之后,链表就被破坏的面目全非了。

这个思路尚未经过实际代码验证。

One thought on “Linked List Cycle

Leave a Reply

Your email address will not be published. Required fields are marked *