LeetCode 142. 环形链表 II 【知识点:快慢指针、散列表】

题目

来源:力扣(LeetCode)
链接:142. 环形链表 II
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例 1:
在这里插入图片描述

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:
在这里插入图片描述

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:
在这里插入图片描述

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

提示:

链表中节点的数目范围在范围 [0, 104] 内
-105 <= Node.val <= 105
pos 的值为 -1 或者链表中的一个有效索引

进阶: 你是否可以使用 O(1) 空间解决此题?

解题

方法一:散列查找(哈希表)

遍历链表,将每个结点映射到哈希表上,如果某个结点已经存在哈希表上,那么有环且是环入口

  • 自己写Hash表:(采用的冲突解决方案是平方探测)
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

#define MAXTABLESIZE 10000
typedef struct ListNode* ElementType;
typedef int Index;
typedef Index Position;
typedef enum{ Legitimate, Empty, Deleted } EntryType;

typedef struct HashEntry{
    ElementType Data;
    EntryType Info;
}Cell;

typedef struct _HashTable{
    Cell* Cells;
    int TableSize;
}HashTable;

int NextPrime( int N )
{
    int i,P = (N%2)?N+2:N+1;

    while(1){
        for(i=(int)sqrt(P);i>2;i--){
            if(!(P%i)) break;
        }

        if(i == 2) break;
        else P +=2;
    }

    return P;
}

HashTable* CreateTable( int Size )
{
    HashTable* H = (HashTable*)malloc(sizeof(HashTable));
    H->TableSize = NextPrime( Size );

    H->Cells = (Cell*)malloc(H->TableSize*sizeof(Cell));

    for(int i=0;i<H->TableSize;i++){
        H->Cells[i].Info = Empty;
    }

    return H;
}

Position Hash( ElementType Key,int TableSize )
{
    return (int)Key % TableSize;
}

Position Find( HashTable* H,ElementType Key )
{
    Position CurrentPos,NewPos;
    CurrentPos = Hash( Key,H->TableSize );

    int Di;
    for(Di=0;Di<=H->TableSize/2;Di++){

        NewPos = (CurrentPos + Di*Di)%H->TableSize;
        if(H->Cells[NewPos].Info == Empty || H->Cells[NewPos].Data == Key) break;

        NewPos = (CurrentPos - Di*Di + H->TableSize)%H->TableSize;
        if(H->Cells[NewPos].Info == Empty || H->Cells[NewPos].Data == Key) break;
    }

    return NewPos;
}

bool Insert( HashTable* H,ElementType Key )
{
    Position Pos = Find( H,Key );

    if(H->Cells[Pos].Info != Legitimate){
        H->Cells[Pos].Info = Legitimate;
        H->Cells[Pos].Data = Key;

        return true;
    }else{
        return false;
    }
}

struct ListNode *detectCycle(struct ListNode *head)
{
    HashTable* H = CreateTable( MAXTABLESIZE );

    struct ListNode* p;
    for(p=head;p;p=p->next){
        
        if(!Insert( H,p )){
            break;
        }
    }

    free(H->Cells);
    free(H);

    return p;
}
  • 调用第三方头文件 uthash.h(仅是头文件)
/*结构定义*/
struct _HashTable{
	struct ListNode* Key;
	UT_hash_handle hh;
};

/*哈希表:初始化为空表*/
struct _HashTable* HashTable= NULL;

/*实现自己的查找接口函数:*/
struct _HashTable* find( struct ListNode* iKey )
{
	struct _HashTable* temp;
	HASH_FIND_PTR( HashTable,&iKey,temp );
	return temp;
}

/*实现自己的插入接口函数:*/
void Insert( struct ListNode* iKey )
{
	struct _HashTable* temp = malloc(sizeof(struct _HashTable));
	temp->Key = iKey;
	HASH_ADD_PTR( HashTable,Key,temp );
}

struct ListNode* detectCycle(struct ListNode* head)
{
	struct ListNode* p = head;
	for(P=head;p;p=p->next){
		if(find(p)) break;
		Insert(p);
	}
	
	return p;
}

方法二:快慢双指针(本题最优解)

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

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

    while(fast && fast->next){
        slow = slow->next;
        fast = fast->next->next;

        if(fast == slow){
            struct ListNode* Third = head;
            while( Third != slow){
                Third = Third->next;
                slow = slow->next;
            }

            return Third;
        }
    }
	
	return NULL;
}

算法详解:142.环形链表II:快慢指针算法详解

  • 思路

这道题目,不仅考察对链表的操作,而且还需要一些数学运算。

主要考察两知识点:

  • 判断链表是否环
  • 如果有环,如何找到这个环的入口

判断链表是否有环
__________________________________________________________________________________
可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢

首先第一点: fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。

那么来看一下,为什么fast指针和slow指针一定会相遇呢?

可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。

会发现最终都是这种情况, 如下图:
在这里插入图片描述
fast和slow各自再走一步, fast和slow就相遇了

这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。

动画如下:
在这里插入图片描述
如果有环,如何找到这个环的入口
__________________________________________________________________________________
假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:

在这里插入图片描述
那么相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数: x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:

(x + y) * 2 = x + y + n (y + z)

两边消掉一个(x+y): x + y = n (y + z)

因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。

所以要求x ,将x单独放在左面:x = n (y + z) - y ,

再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。

这个公式说明什么呢?

  • 从相遇点到入环点的距离加上 n-1 圈的环长,恰好等于从链表头部到入环点的距离。

这就意味着:

  • 从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。

为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?
__________________________________________________________________________________
首先slow进环的时候,fast一定是先进环来了。

如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子:
在这里插入图片描述
可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。

重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图:

在这里插入图片描述
那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。

因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。

也就是说slow一定没有走到环入口3,而fast已经到环入口3了。

这说明什么呢?

在slow开始走的那一环已经和fast相遇了。

那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,fast相对于slow是一次移动一个节点,所以不可能跳过去。

PS

这个算法难吗? 不难!
数学分析难吗?不难!
想得到吗?想不到!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值