数据结构每日亿题(三)

一.环形链表

原题传送门:力扣

题目:
在这里插入图片描述

这一题的目的是让你判断一下一个链表是否有环。

现在有一个指针,假设这个链表带环,这个指针如果一直向后走,它的运动轨迹是不是先走到环的节点那里,然后一直在环里循环。如果这是没环的链表,那指针指针一直走,直到为空则停止。

现在我们在定义一个指针,这个指针走的比刚才那个指针慢,而且是慢一倍。也就是这里有个快指针一次走两步,有个满指针,一次走一步:
在这里插入图片描述
我们抽象一点,将这个链表画成这个样子。接下来我们慢慢移动这两个指针:
在这里插入图片描述
现在快指针即将进入环里面了,慢指针走的路程是快指针走的一半,然后我们继续走:
在这里插入图片描述当慢指针也走到这个节点时,我们假设fast走到图中指向的那个位置上,此时这个快指针是走了好几圈还是第一圈,我们也不清楚,反正就先假设走到这里。

好,到目前为止,我们定义的两个指针都已经进入了环里面了,我们现在假设他们之间距离为N:
在这里插入图片描述因为我们刚才已经设置好了,快指针一次走两步,慢指针一次走一步,这也就意味着,两个指针每走一次,他们之间的距离都会-1,而总距离是N。N,N-1,N-2,N-3…这样总会有一步导致它们俩相遇,只要相遇就知道这个链表带环。

如果链表不带环,因为快指针走的快,所以肯定会提前到达结尾,这时候我们要分两种情况:
在这里插入图片描述

如果链表节点个数为偶数个,fast总共要走两次,走到NULL,停下来。

在这里插入图片描述如果个数是奇数个,fast->next要为空才能停下来。

所以我们的循环条件可以这样写:

    struct ListNode* fast = head;
    struct ListNode* slow = head;

    while(fast && fast->next)
    {
    	...
    }

这样写的目的是,可以让两个指针在循环体内部运动,如果这个链表没有环,就一定能跳出循环,如果含有环,循环就会一直走,直到两个指针相遇,相遇后就直接返回,也不需要跳出循环了。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
bool hasCycle(struct ListNode *head) {
    struct ListNode* fast = head;
    struct ListNode* slow = head;

    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
        {
            return true;
        }
    }
    return false;
}

这里还要考虑链表为空的情况,如果为空就直接跳过循环返回false.

现在还有一个问题,快指针一定要比慢指针多走一步吗?多走2步,3步可不可以呢?
首先假设快慢两个指针进环之后的距离是N,如果快指针每次多走两步,它们之间的距离就是:
N
N-2
N-4

但我们也不清楚N是奇数还是偶数,如果是偶数那恭喜你,同样可以做出来,但如果是奇数呢?
N-6
N-8

1
-1
这样的话距离就变成-1了,也就是说,快指针超过了慢指针而且多跑了一步:
在这里插入图片描述
这也就意味着,它们之间的距离从N变成了圈大小-1,我们假设圈长度是C,所以距离从N变成了C-1。但是我们还有一次机会,如果C-1是偶数的时候,我们依然能赶上,但相同道理,我们不知道环的大小,所以依然判断不了其是奇是偶。如果运气不好C-1是奇数,每次距离-2,到最后它们的距离还是变成了C-1.

同理快指针多走3步,4步…都是一个道理。所以说最稳健,最合理的就是快指针一次走2步,慢指针一次走一步

二.环形链表 II

2.1

原题传送门:力扣

题目:
在这里插入图片描述
这题和第一题差不多,只是这题多了一个条件:让你返回入口节点。

刚才那一题我们只是判断了链表是否带环,但是没有判断链表环的入口在哪,也就是找这个地方:
在这里插入图片描述
我们假设第一题定义的快慢两个指针是在这里相遇的:
在这里插入图片描述
现在我们肯定很难想到比较好的解决方法,所以我们还是从数学的角度来寻找结果,我们通过刚才两个指针走的路程来找下面这三段的关系:
在这里插入图片描述
慢指针走的路程应该是L+(N)这一段,因为它不会像快指针那样在环里绕几圈,慢指针只可能走一圈的一部分。

那快指针走的路程呢?它们两个指针走的时间是一样的,而速度是两倍的关系,所以说快指针走的路程是慢指针的两倍。而快指针走的路程我们也可以直到L+(N*k)+(N)这里乘一个k是因为我们不知道快指针实际在里面转了多少圈,所以最后表达式是:

(L+N)2 = L+CK+N

两边约一下:

L = C*k - N

但是仔细想一下,这个k是不是对我们的算式没什么用处啊,C-N,2C-N,3C-N所在的地方是不是一样的?

也就是说L和C-N这两个距离相等:
在这里插入图片描述所以现在思路就有了,我们在定义两个指针cur指向头节点,meet指向之前两个指针相遇的节点:
在这里插入图片描述然后让这两个指针同时且步长相同的往后走,然后它们相遇的节点是不是就是我们需要求的节点?

把第一题的代码稍作改进即可:

struct ListNode* detectCycle(struct ListNode* head) {
    struct ListNode* fast = head;
    struct ListNode* slow = head;

    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
        {
            struct ListNode* meet = fast;
            struct ListNode* cur = head;
            while (meet != cur)
            {
                meet = meet->next;
                cur = cur->next;
            }
            return meet;
        }
    }
    return NULL;
}

2.2

这一题我再来将一种方法:
我们先将第一题找到的那个相遇的节点与下一个节点断开:
在这里插入图片描述
这样我们就把一个链表,变成了两个链表:
在这里插入图片描述
两个链表一个从头开始走,一个从meet指向的节点后面开始走,最终都走到meet指向的节点那里停下来。现在我们是不是把找路口的问题变成了找两个链表交点的问题?
关于相交链表的为题我在每日亿题(二)的最后一题里提到过,这里我就不再赘述,现在我们直接将那个函数拿来用。

//返回两个链表的相交节点
 struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB) {
    int lenA = 0;
    int lenB = 0;

    //随便定义两个指针,因为此时还不知道哪个长哪个短
    struct ListNode* curA = headA;
    struct ListNode* curB = headB;

    //计算链表的大小
    while (curA->next)
    {
        curA = curA->next;
        lenA++;
    }
    while (curB->next)
    {
        curB = curB->next;
        lenB++;
    }

    //此时curA,curB指向链表末尾,如果两者地址不相等说明没有交点
    if (curA != curB)
        return NULL;

    //计算两个数差值的绝对值,也就是快指针该走的步数
    int absoulte = abs(lenA - lenB);

    //随便定义两个指针,因为此时还不知道哪个长哪个短
    struct ListNode* fast = headA;
    struct ListNode* slow = headB;

    //假设headA比headB要长,如果猜错了就换一下位置
    if (lenA < lenB)
    {
        fast = headB;
        slow = headA;
    }

    //此时将快指针先走absoulte步
    while (absoulte--)
    {
        fast = fast->next;
    }

    //遍历
    while (fast != slow)
    {
        fast = fast->next;
        slow = slow->next;
    }
    return fast;
}

struct ListNode* detectCycle(struct ListNode* head) {
    struct ListNode* fast = head;
    struct ListNode* slow = head;

    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
        {
            struct ListNode* meet = fast;
            struct ListNode* next = meet->next;
            //保存相遇节点后一个位置将相遇处的节点断开
            meet->next = NULL;

            struct ListNode* cur = head;

            return getIntersectionNode(next,cur);
        }
    }
    return NULL;
}

虽然这种写法很容易想到,但是代码要写的太多了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值