算法题学习---判断链表中是否有环

123 阅读3分钟

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

题目

描述

判断给定的链表中是否有环。如果有环则返回true,否则返回false。

数据范围:链表长度 0≤n≤10000,链表中任意节点的值满足∣val∣<=100000

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

输入分为两部分,第一部分为链表,第二部分代表是否有环,然后将组成的head头结点传入到函数里面。-1代表无环,其它的数字代表有环,这些参数解释仅仅是为了方便读者自测调试。实际在编程时读入的是链表的头节点。

例如输入{3,2,0,-4},1时,对应的链表结构如下图所示:

可以看出环的入口结点为从头结点开始的第1个结点(注:头结点为第0个结点),所以输出true。

示例1

输入:
{3,2,0,-4},1
返回值:
true
说明:
第一部分{3,2,0,-4}代表一个链表,第二部分的1表示,-4到位置1(注:头结点为位置0),即-4->2存在一个链接,组成传入的head为一个带环的链表,返回true           

示例2

输入:
{1},-1
返回值:
false
说明:
第一部分{1}代表一个链表,-1代表无环,组成传入head为一个无环的单链表,返回false           

示例3

输入:
{-1,-7,7,-4,19,6,-9,-5,-2,-5},6
返回值:
true

分析

考虑特殊情况

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

不难发现,如下情况:

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

考虑一般情况

很容易想到第一种方法:

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

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

第二种方法,我们来观察一下链表中有环的情况,因为链表中只有一个指针域,如果出现环,那么一定是末尾指向NULL的指针指向了前面的节点,换言之,有环链表中没有指针域为NULL的节点。

那么我们要如何才能判断我们的遍历是一直在环中循环还是因为链表太长还未遍历到末尾呢?

这时,我们就需要用到双指针了,双指针指的是在遍历对象的过程中使用两个指针,同方向不同速度(快慢指针),同方向同速度,不同方向同速度(对撞指针)来进行访问。

考虑这样一种用法,我们初始化两个指针都指向head,然后快指针每次走两步,慢指针每次走一步,如果存在环的话,由于速度差异,那么它们一定会在某一时刻碰到对方,此时也就说明是存在环的。

代码示例

版本一

利用了std::set,空间占用达到O(n)

/**
 * 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) {
        if (nullptr == head || nullptr == head->next) {
            return false;
        }
        std::set<ListNode*> walkedListNode;
        ListNode* pCur = head;
        while (nullptr != pCur) {
            if (walkedListNode.count(pCur) == 0) {
                walkedListNode.insert(pCur);
                pCur = pCur->next;
            } else {
                return true;
            }
        }
        return false;
    }
};

版本二

如下:

  • step 1:设置快慢两个指针,初始都指向链表头。
  • step 2:遍历链表,快指针每次走两步,慢指针每次走一步。
  • step 3:如果快指针到了链表末尾,说明没有环,因为它每次走两步,所以要验证连续两步是否为NULL。
  • step 4:如果链表有环,那快慢双指针会在环内循环,因为快指针每次走两步,因此快指针会在环内追到慢指针,二者相遇就代表有环。
/**
 * 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) {
        if (nullptr == head || nullptr == head->next) {
            return false;
        }

        ListNode* pQuick = head;
        ListNode* pSlow = head;
        while(nullptr != pQuick && nullptr != pQuick->next){
            pQuick = pQuick->next->next;
            pSlow = pSlow->next;
            if(pQuick == pSlow){
                return true;
            }
        }
        return false;
    }
};