算法题学习---链表中环的入口结点

114 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情

题目

描述

给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。

数据范围: n≤10000,1<=结点值<=10000

要求:空间复杂度 O(1),时间复杂度 O(n)

例如,输入{1,2},{3,4,5}时,对应的环形链表如下图所示:

可以看到环的入口结点的结点值为3,所以返回结点值为3的结点。

输入描述:

输入分为2段,第一段是入环前的链表部分,第二段是链表环的部分,后台会根据第二段是否为空将这两段组装成一个无环或者有环单链表

返回值描述:

返回链表的环的入口结点即可,我们后台程序会打印这个结点对应的结点值;若没有,则返回对应编程语言的空结点即可。

示例1

输入:
{1,2},{3,4,5}
返回值:
3
说明:
返回环形链表入口结点,我们后台程序会打印该环形链表入口结点对应的结点值,即3   

示例2

输入:
{1},{}
返回值:
"null"
说明:
没有环,返回对应编程语言的空结点,后台程序会打印"null"   

示例3

输入:
{},{2}
返回值:
2
说明:
环的部分只有一个结点,所以返回该环形链表入口结点,后台程序打印该结点对应的结点值,即2  

分析

考虑特殊情况

在进行问题分析过程前,我们先考虑一下特殊情况,那么在这道问题中,有哪些情况是特殊的呢?

不难发现,如下情况:

  • 如果输入的头结点为空,那么返回nullptr即可
  • 如果输入的头结点的next为空,那么也同样返回nullptr即可。

考虑一般情况

很容易想到第一种方法

我们从头节点开始遍历链表,并使用set记录每个节点的指针,如果没有发现相同的就insert到set中,如果发现有相同的,那就说明出现了重复的指针,说明此时就是环的起点,返回该指针即可

很明显,这种方法在最坏的情况下面,会占用O(n)的空间,并不满足题目的要求,基于此,我们实现的是版本一的代码。

第二种方法

第二种方法,我们就要用到前面使用过的快慢双指针的方法,当快慢双指针相等时,即说明慢指针追上了快指针,说明有环,此时我们来分析一下此时的位置情况,如下

我们假设从头节点到环入口的距离为x,环入口到相遇点的距离为y,相遇点到环入口的距离为z,则环的长度为y+z,同时我们假设慢指针已经在环内走了m圈,快指针走了n圈,则有

快指针走的步数=x+y+n(y+z);

慢指针走的步数=x+y+m(y+z);

同时我们又知道,快指针走的步数是慢指针的两倍,即x+y+n(y+z) = 2(x+y+m(y+z)),化简一下可得:

x+y = (n-2m)(y+z),x+y正好是y+z的整数倍,那么当我们使用两个指针,一个从头结点出发,一个从当前相遇的节点出发,那么他们一定会相遇而且有一段距离y一点是会相互重合的,它们相遇的第一个节点就应该是环的入口了。

所以步骤如下:

  • step 1:使用之前的方法判断链表是否有环,并找到相遇的节点。
  • step 2:慢指针继续在相遇节点,快指针回到链表头,两个指针同步逐个元素逐个元素开始遍历链表。
  • step 3:再次相遇的地方就是环的入口。

代码示例

版本一

思路比较清晰,简洁,但是空间复杂度比较大

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) :
        val(x), next(NULL) {
    }
};
*/
class Solution {
public:
    ListNode* EntryNodeOfLoop(ListNode* pHead) {
        if(nullptr == pHead || nullptr == pHead->next){
            return nullptr;
        }

        std::set<ListNode*> walkedListNode;
        ListNode* pReturn = nullptr;
        ListNode* pCur = pHead;
        while(nullptr != pCur){
            if(walkedListNode.count(pCur) == 1){
                pReturn = pCur;
                break;
            }else{
                walkedListNode.insert(pCur);
                pCur = pCur->next;
            }
        }
        return pReturn;
    }
};

版本二

较难理解,特别是找到相遇点后,快指针又退回头结点,再次遍历,这里需要计算,可以结合图自己分析一下。

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) :
        val(x), next(NULL) {
    }
};
*/
class Solution {
public:
    ListNode* EntryNodeOfLoop(ListNode* pHead) {
        if(nullptr == pHead || nullptr == pHead->next){
            return nullptr;
        }

        ListNode* pQuick = pHead;
        ListNode* pSlow = pHead;
        ListNode* pReturn = nullptr;
        bool hasLoop = false;
        while(nullptr != pQuick && nullptr != pQuick->next){
            pQuick = pQuick->next->next;
            pSlow = pSlow->next;
            if(pQuick == pSlow){
                hasLoop = true;
                break;
            }
        }
        if(hasLoop){
            pQuick = pHead;
            while(pQuick != pSlow){
                pQuick = pQuick->next;
                pSlow = pSlow->next;
            }
            pReturn = pQuick;
        }
        return pReturn;
    }
};