Floyd判圈算法

Floyd判圈算法

wiki 的介绍

以下是引用 wiki 的介绍:
Floyd判圈算法(Floyd Cycle Detection Algorithm),又称龟兔赛跑算法(Tortoise and Hare Algorithm),是一个可以在有限状态机、迭代函数或者链表上判断是否存在环,求出该环的起点与长度的算法。

如果有限状态机、迭代函数或者链表存在环,那么一定存在一个起点可以到达某个环的某处(这个起点也可以在某个环上)。

初始状态下,假设已知某个起点节点为节点S。现设两个指针t和h,将它们均指向S。

接着,同时让t和h往前推进,但是二者的速度不同:t每前进1步,h前进2步。只要二者都可以前进而且没有相遇,就如此保持二者的推进。当h无法前进,即到达某个没有后继的节点时,就可以确定从S出发不会遇到环。反之当t与h再次相遇时,就可以确定从S出发一定会进入某个环,设其为环C。

如果确定了存在某个环,就可以求此环的起点与长度。

上述算法刚判断出存在环C时,显然t和h位于同一节点,设其为节点M。显然,仅需令h不动,而t不断推进,最终又会返回节点M,统计这一次t推进的步数,显然这就是环C的长度。

为了求出环C的起点,只要令h仍均位于节点M,而令t返回起点节点S,此时h与t之间距为环C长度的整数倍。随后,同时让t和h往前推进,且保持二者的速度相同:t每前进1步,h前进1步。持续该过程直至t与h再一次相遇,设此次相遇时位于同一节点P,则节点P即为从节点S出发所到达的环C的第一个节点,即环C的一个起点。

个人理解

判断是否有环

放一个快指针fast与慢指针slow在链表头部,快指针每次移动两步,慢指针移动一步。如此移动下去只会有两个结果:

  • 快指针走到链表尾部。即不存在环

    在这里插入图片描述

    【图1】

  • 快慢指针在环内相遇。即存在环

    在这里插入图片描述

    【图2】

求入环节点

那么如何上面已知环存在的基础上,求环的入口节点呢?

在这里插入图片描述

【图3】

一些数学推导(参考网上)

假设,环外长 a a a,环长为 L = b + c L=b+c L=b+c ,当slow刚进入环时,fast指针位于环内的 x x x点处,距离环入口长度 b b b 见【图3】。则fast追上slow的距离为 d = L − b d=L-b d=Lb

已知:

  • slow指针速度为 1/s;fast指针速度为 2/s 。因此fastslow的相对移动速度为1/s,即每周期距离缩短一个单位,所以两指针相交共需要 L − b 1 / s \frac{L-b}{1/s} 1/sLb个移动周期
  • 由于slow每周期移动一个单位,所以与fast相遇时slow需要在环内移动 L − b L-b Lb个单位,也就是说,在入环口处慢>指针再走 L − b L-b Lb个单位就可到达相交点
  • 观察 d d d取最大值的情况,即当 b = 0 b=0 b=0时,此时slow刚到环的入口就与fast相交,因此环内的追击距离为 0 0 0
  • 其他 b > 0 b>0 b>0的情况,有 d < L d<L d<L

​ 综上可得,在有环的情况下,在两指针相交时,slow指针在环内移动距离 L − b L-b Lb不会超过一圈的长度。

​ 接着上面的假设条件,假设到相遇时,fast指针在环内移动了 n n n圈(不同长度链表取值不一样,这里n=2), 两指针的运动轨迹【图4】,slow所走轨迹【图4.1】;fast指针所走轨迹【图4.2】

注1:在上面已证得,慢指针在环内移动轨迹不会超过一圈,因此下图中红色部分 L − b L-b Lb是不会超过一圈长度

注2:下面的圆周长(指针在环内的移动长度)都是相同的,为了方便表示才画的半径不一样

在这里插入图片描述

【图4】

在这里插入图片描述

【图4.1】

在这里插入图片描述

【图4.2】

  • 由图【4.1】得慢指针移动距离为: s = a + ( L − b ) s=a+(L-b) s=a+(Lb)

  • 由图【4.2】得快指针移动距离为: f = a + L ∗ n + ( L − b ) f=a+L*n+(L-b) f=a+Ln+(Lb)

  • 又因为快指针移动距离为慢指针的两倍: f = 2 s f=2s f=2s => f − s = ( a + L ∗ n + ( L − b ) ) − ( a + ( L − b ) ) = L ∗ n = s f-s=(a+L*n+(L-b))-(a+(L-b))=L*n=s fs=(a+Ln+(Lb))(a+(Lb))=Ln=s

因此可知慢指针所走距离为环长的 n n n倍: a + ( L − b ) = L ∗ n a+(L-b) = L*n a+(Lb)=Ln

而此时相交点距离入环点 L − b L-b Lb ,如果在相交点再接着走 a a a长度就可达长度从入环点开始移动 n n n倍的环长的距离,因此便到达了入环点。(解释:因为是一个环,所以可以纯天然的对移动的长度进行取模操作,模长为 L L L。当视入环点为0时在相交点再移动 L − b L-b Lb的长度有: ( L − b + a = L ∗ n ) (L-b+a=L*n) (Lb+a=Ln) % L = 0 L=0 L=0,因此可看出,最后还是会回到入环节点)

又因为头结点距离入环节点长度为 a a a,故可以放一个指针在头结点处,然后与环内交点指针同步每次移动一个单位,最后相交的结点既是入环节点。(置于头结点的指针起贡献 a a a长度的作用)

求环长

随便一个指针指向一个环内的点(如上面求得的环内焦点)固定不动,然后另置一个指向该节点的指针不停移动,直至再次相遇为止,该指针移动的距离为环长。

代码汇总
#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>
#include<stdbool.h>

typedef struct Node
{
    int data;                       //数据域 每个节点存放一个数据
    struct Node *pNext;    //指针域 指针指向下一个节点
} LNode, *LinkList;

LinkList Create(int[][2],int);                  //利用二维数组创建链表
LNode *HGetElem(LinkList,int);          //按位查找
LNode *detectCycle(LinkList);             //检查是否有环,若有则求其长度
int LoopLength(LinkList);                   //找环长

int main(void){
    int enm[4][2] = {{3, 2}, {2, 3}, {0, 4}, {-4, 2}};
    LinkList p = Create(enm, 4);
    LNode *intersListNode = detectCycle(p);
    if(intersListNode){
        printf("有环,入口节点数值为:%d\t环长为:%d\n"\
        , intersListNode->data,LoopLength(intersListNode));
    }
    else{
        printf("无环\n");
    }
    return 0;
}

/** 利用二维数组创建链表
 * @param a 数组指针
 * @param n 数组行数
 */
LinkList Create(int enm[][2],int n){
    LNode *head = (LNode *)malloc(sizeof(LNode));
    head->pNext = NULL;
    LNode *p = head;
    for (int i = 0; i < n;i++){
        LNode *node = (LNode *)malloc(sizeof(LNode));
        node->data = enm[i][0];                              //赋值
        LNode *next = HGetElem(head, enm[i][1]); // 获取对应的后继指针
        node->pNext = next;                                 //若next非空,则构成环
        p->pNext = node;                                    //尾插法创建
        if(next)    break;                                       //只允许有一个环
        p = node;
    }
    return head;
}

/**  按位查找,返回第 i 个节点元素(包含头节点)
 * @param L 需要操作的链表
 *  @param i 需要返回的节点位序
 * */
LNode *HGetElem(LinkList L, int i){
    if(i<1)                             //判断位序的合法性
        return NULL;
    LNode *p = L;               //p指针指向当前扫描到的节点元素
    int j = 0;                      //p初始化为指向头节点,头节点属与第0号节点
    //循环找到第 i 个节点
    while(p!=NULL&&j<i){
        p = (p->pNext);
        j++;
    }
    return p;
}

/** 检查是否有环
 *  @param p 头结点
 * @return 如果有环则返回环入口节点;否则NULL
 */
LNode* detectCycle(LinkList p){
    LNode *slow = p, *fast = p;             //慢指针;快指针。初始时均指向头结点
    LNode *intersListNode = NULL;     //环内相交结点,初始为空
    while(fast&&fast->pNext){
        fast = fast->pNext->pNext;        //快指针移动两步
        slow = slow->pNext;                 //慢指针移动一步
        if(fast==slow)  break;               //相遇
    }
    if(!fast||!(fast->pNext))
        return intersListNode;              //如果没有环,即快指针走到表尾了,返回NULL
    else{                                            //有环,找入口节点
        intersListNode = p;                //置交点指针指向头结点
        while(intersListNode!=slow){//直至交点指针与慢指针相交
            intersListNode = intersListNode->pNext;
            slow = slow->pNext;
        }                                           //相交节点便是环的入口
    }
    return intersListNode;
}


/** 找环长
 * @param intersListNode 循环圈内 快慢指针的交点
 * @return 一个环的长度
 */
int LoopLength(LinkList intersListNode){
    int cnt = 0;
    if (!intersListNode)                 //无环,返回0
        return cnt;
    else{
        //工作指针p指向交点的下一个节点
        LNode *p = intersListNode->pNext;
        //由于下面while循环退出时,是指针指向交点时,且cnt不会+1,故需要提前+1
        cnt++;
        while (p != intersListNode && ++cnt > 0)
            p = p->pNext;
    }
    return cnt;
}
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Floyd判圈算法,又称龟兔赛跑算法,是用来判断链表是否有环、计算环的长度以及寻找环的起点的一种算法。该算法通过使用两个指针,一个慢指针和一个快指针,在链表中进行遍历。慢指针每次移动一步,而快指针每次移动两步。如果链表中存在环,那么快指针最终会追上慢指针,两个指针会相遇。这可以通过两个条件来判断:当快指针和慢指针相遇时,且快指针不为null且快指针的下一个节点也不为null。如果两个指针相遇,那么说明链表中存在环。接下来,我们可以从相遇点开始,用一个指针再次遍历链表,直到回到相遇点,记录遍历过程中的节点数。这个节点数就是环的长度。 如果慢指针和快指针相遇,那么我们可以进一步判断是否有环存在。如果相遇点是链表中的某个节点,那么从相遇点开始遍历链表,再次回到相遇点的位置一定是环的起点。这是因为从相遇点继续遍历,最终会再次回到相遇点,而在这个过程中,慢指针每次移动一步,快指针每次移动两步,所以在某一时刻,慢指针和快指针会在环的起点处相遇。 然而,如果慢指针和快指针从环的不同起点出发,那么它们再次相遇的位置会发生偏移,不会是环的起点。同样地,如果慢指针和快指针从环外的同一起点出发,它们在进入环之前还有一段距离,因此再次相遇的位置也不一定是环的起点。 综上所述,Floyd判圈算法通过两个指针的移动,可以判断链表是否有环,并且计算出环的长度和寻找环的起点。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Floyd判圈算法](https://blog.csdn.net/qq_26012495/article/details/117407011)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [cjing-interview:C ++面试; 基本算法数据结构提示](https://download.csdn.net/download/weixin_42144707/16474761)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值