Floyd/Brent判圈算法

Posted by Dongbo on April 17, 2021

简单记一下Floyd判圈算法和Brent判圈算法 leetcode-141 / leetcode-142,给出一条链的头节点,问链上是否存在环。

首先能想到一种简单的方法是用 set/unordered_set 记录已经访问过的节点,每遍历一个节点就塞入 set 中,如果遍历过程中碰到已经在集合中的节点,则表明有环;走到链的末尾则无环。

考虑 unordered_set 每次查询都是 $ O(1)$ 时间的话,这种方法的时间复杂度也是 $ O(n) $,但是空间复杂度也是 $ O(n) $,似乎有些题目会被卡。

Floyd 判圈算法1

简单概括来说就是,如果存在环,那么朝同一个方向、以不同速度前进的两个指针必定会相遇。一般设指针 $p_1$ 步长为1, $p_2$ 步长为2即可。当然也可以把 $p_2$ 的步长设得大一些,比如设置成3,4,5等,能更快“超圈”,这样就会变成brent算法。但是这里为了便于说明后续解释先不这么做。

如果只用判断是否存在环,则 $p_1$ 与 $p_2$ 相遇时函数就可以返回了。如果还要求环的起点,则需要进一步判断。

设从 $head$ 到环的起点要走 $d$ 步,设环的长度为 $k$,设相遇时 $p_1$ 走了 $x$步,则 $p_2$ 走了 $2x$ 步,又因为 $p_2$ 比 $p_1$ 多走了若干圈(不一定超1圈,可能$p_1$没进环内时$p_2$就在环上兜圈了),所以 $2x - x = x = nk$。此时 $p_1,p_2$ 的路程都是环长度的整数倍。

如果把 $p_1$ 放回起点 $p_2$ 位置不变,然后将 $p_1, p_2$ 步长都设置为1,同时前进,当 $p_1$ 走过 $d$ 步到达环的起点时,$p_1$共走过$d+x$步,$p_2$ 总共走过 $d + 2x$ 步,因为 $x$ 是环长度的整数倍所以此时 $p_2$ 也会恰好落在环的起点。因此找环的起点只要找到 $p_1, p_2$ 第二次相遇的位置就可以了,附上渣代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Definition for singly-linked list.
* struct ListNode {
*     int val;
*     ListNode *next;
*     ListNode(int x) : val(x), next(NULL) {}
* };
*/
ListNode *detectCycle(ListNode *head) {
    if(!head || !head->next) return nullptr;
    ListNode *p1 = head, *p2 = head;
    do{
        p1 = p1->next;
        p2 = p2->next->next;
    }while(p1 != p2 && p2 && p2->next);
    if(p1 != p2) 
        return nullptr;
    p1 = head;
    while(p1 != p2){
        p1 = p1->next;
        p2 = p2->next;
    }
    return p1;
}

Brent 判圈算法

虽然Floyd判圈的复杂度是$ O(n) $,但是还有更快的Brent算法。

Brent算法的流程大致为:

  1. $p_1, p_2$ 初始都指向 $head$,$p_2$先走 $step = 2$ 步,如果碰到 $p_1$,说明有环,终止;碰到链的末尾,无环,终止;
  2. 如果算法没有终止,则令 $step = 2 * step$,将 $p_1$ 指向 $p_2$ 当前位置,继续重复步骤1。

与Floyd求起点略微不同的是,Brent算法中$p_1, p_2$ 相遇时 $p_1$ 的步数不一定等于环的长度了。但是因为每次我们都会先将 $p_1$ 移动到 $p_2$ 当前的位置,然后 $p_2$ 才开始前进,所以 $p_2$ 是一次性走一整圈的长度追上 $p_1$ 的,此时我们可以直接得到环的长度 $k$。参考 这篇博客 ,在 Floyd 判圈算法中,我们利用的是 $p_2$ 走过的距离是环长度的整数倍来求出环的起点;而现在已知环的长度 $k$,只要把 $p_1$ 放回链表头,$p_2$ 放到距离链表头 $k$ 的地方,就能用 Floyd 的步骤找到起点。下面就直接附上代码了。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ListNode *detectCycle(ListNode *head) {
    if(!head || !head->next) return nullptr;
    ListNode * p1 = head, *p2 = head;
    int step = 2, cnt = 0;
    while(p2){
        cnt++;
        p2 = p2->next;
        if(p1 == p2)
            break;
        if(cnt == step){
            step *= 2;
            cnt = 0; 
            p1 = p2;
        }
    }
    if(!p2) return nullptr;
    p1 = p2 = head;
    while(cnt--){
        p2 = p2->next;
    }
    while(p1 != p2)
        p1 = p1->next, p2 = p2->next;        
    return p2;
}

感觉这题应该算是判断一个图是否存在环的特例,那么用来判别图上是否有环的 拓扑排序/DFS方法 也能够用在这里,只不过有些浪费而已(用 set 的方法应该就是一种DFS;至于拓扑排序那更麻烦了,在统计每个节点度数的时候就需要遍历所有节点,还不如直接用 set )

文字似乎有些啰嗦,以后有心情的话再改吧。

The End