带你拿捏链表

news2024/12/28 19:48:19

本专栏内容为:数据结构学习专栏,分为初阶和进阶两部分。 通过本专栏的深入学习,你可以了解并掌握数据结构。

💓博主csdn个人主页:小小unicorn
⏩专栏分类:数据结构
🚚代码仓库:小小unicorn的代码仓库🚚
🌹🌹🌹关注我带你学习编程知识

目录

  • 移除链表元素
  • 思路一:
    • 1.==考虑常见情况==:
    • 2.==考虑特殊情况==:
      • 2.1当链表的第一个节点为待移除的节点时:
      • 2.2当链表的最后一个节点为待移除的节点时:
      • 2.3当传入的链表为空时:
    • 代码实现
  • 思路二:
    • 代码实现
  • 反转链表
  • 思路一:
    • 1.==考虑一般情况==:
    • 2.==考虑特殊情况==:
      • 2.2当链表的最后一个节点为待移除的节点时:
      • 2.2当链表的最后一个节点为待移除的节点时:
      • 2.3当传入的链表为空时:
    • 代码实现
  • 思路二:
    • 代码实现
  • 链表的中间节点
    • 解题思路:
    • 代码实现:
    • 附加条件:
    • 思路(快慢指针)
    • 代码实现:
  • 链表中第K个节点
    • 思路一(遍历):
    • 代码实现:
    • 思路二(快慢指针):
    • 代码实现:
  • 合并两个有序链表:
    • 思路:
    • 代码实现:
  • 链表分割
    • 思路:
    • 代码实现
  • 链表的回文结构:
    • 思路:
    • 代码实现:
  • 相交链表
    • 思路:
    • 代码实现:
  • 环形链表
    • 思路:
  • 返回链表开始入环的第一个结点
    • 代码实现

移除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。(题目来源)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

思路一:

要移除链表中值为val的节点,我们肯定是要将链表遍历一遍的,关键是我们在遍历中如何操作是一个问题。所有,我们考虑问题的时候,可以先考虑比较常见的情况,再考虑特殊情况。

1.考虑常见情况

要移除某一节点,也就是让该节点的前一个节点指向待移除节点的后一个节点,然后将待移除节点释放即可。这里呢,我们可以定义三个指针变量:prev,cur,next.

prev(previous):记录待排查节点的前一个节点的位置。
cur(current):记录当前正在排查的节点位置。
next(next):记录待排查节点的后一个节点。

在这里插入图片描述
当cur指针指向的节点并非待移除的节点时,3个节点依次向后移动。
在这里插入图片描述
当cur指针指向待移除的节点时,我们首先让Prev指针指向的节点指向next,然后把cur指针指向的节点释放掉,并将next指针赋值给cur指针,next指针向后移动。
在这里插入图片描述
如此进行下去,直到链表遍历完毕,值val的节点也就删除完了。

2.考虑特殊情况

常见的请款的分析往往只能解决问题的一般情况,并不能解决问题的极端情况。要真正的解决问题,我们需要考虑到的问题的极端情况。如,当待移除的节点时第一个节点或者是最后一个节点的情况,当链表为空的情况。

2.1当链表的第一个节点为待移除的节点时:

在这里插入图片描述
这时我们需要先将头指针指向Next,然后释放掉cur所指向的节点,并将next指针赋值给cue指针,next再后移。

在这里插入图片描述

2.2当链表的最后一个节点为待移除的节点时:

当排查到最后一个节点时,cur指向最后一个节点,next指针指向该节点的位置,即NULL.
在这里插入图片描述
我们上面常规情况的方法对其分析,发现常规情况的思路适用于这种特殊情况,并且发现遍历的终止条件,就是当cur为NULL时遍历停止。
在这里插入图片描述

2.3当传入的链表为空时:

我们会发现,如果传来的是空链表,cur指针的值一开始就为空,而我们遍历的终止条件就是当cur为NULL时停止遍历,所以,如果传来的是空链表,直接执行到函数末尾,即返回头指针。(NULL)

代码实现

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


struct ListNode* removeElements(struct ListNode* head, int val)
{
   struct ListNode*prev=NULL;//记录待排查节点的前一个节点位置
   struct ListNode*cur=head;//记录当前正在排查的节点位置
   while(cur!=NULL)//当cur为空时,循环停止
   {
       if(cur->val==val)//当前排查的节点时待移除的节点
       {
            struct ListNode*next=cur->next;//记录待排查节点的后一个节点位置
            if(cur==head)//带移除节点是链表的第一个节点
            {
                head=next;//头指针指向next
                free(cur);//释放掉第一个节点
                cur=next;//将Next指针赋值给cur指针
            }
            else //带移除的节点不是链表的第一个节点
            {
                prev->next=next;//prev指针指向的节点指向next
                free(cur);//将cur指针指向的节点释放掉
                cur=next;//将next指针赋值给cur指针
            }
       }
       else//当前排查的节点不是待移除的节点
       {
           prev=cur;//指针后移
           cur=cur->next;//指针后移
       }
   }
     return head;//返回新的头指针
}

思路二:

思路一可能过于麻烦,当我们要移除某一个节点时,还需要判断该节点是否为第一个节点,那么有没有什么办法可以不进行这个操作呢?

回答是肯定的。新的解决办法就是在传入的链表前面强行加上一个头节点,并让链表原来的头指针指向该头节点,这样我们就不用判断待移除的节点是否为第一个节点了(因为现在第一个节点就是头节点)。

在这里插入图片描述
在加了头节点后,我们就只需要根据常见情况的逻辑进行代码编写。但是有一点需要注意的是,遍历完链表后要将头节点指向的位置(即第一个节点的位置)赋值给头指针,并将头节点释放掉,最后才能返回头指针。在这里插入图片描述

代码实现

/**
 * Definition for singly-linked list.
 * struct ListNode 
 * {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* removeElements(struct ListNode* head, int val)
{
   struct ListNode*guard=(struct ListNode*)malloc(sizeof(struct ListNode));//申请一个头节点,返回其地址
   guard->next=head;//让头节点指向链表的第一个节点
   struct ListNode*prev=guard;//prev指针指向头节点
   struct ListNode*cur=guard->next;//cur指针指向原链表的第一个节点

   while(cur!=NULL)//当cur为空时,循环停止
   {
       if(cur->val==val)//当前排查的节点时待移除的节点
       {
                struct ListNode*next=cur->next;//记录待排查节点的后一个节点位置
                prev->next=next;//prev指针指向的节点指向next
                free(cur);//将cur指针指向的节点释放掉
                cur=next;//将next指针赋值给cur指针
            
       }
       else//当前排查的节点不是待移除的节点
       {
           prev=cur;//指针后移
           cur=cur->next;//指针后移
       }
   }
    head=guard->next;//将头节点指向的位置赋值给头指针,使头指针指向链表第一个节点
    free(guard);//释放头节点
    guard=NULL;//及时置空
     return head;//返回新的头指针
}

解题结果如下:

在这里插入图片描述

反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

思路一:

反转一个单向链表,我们可以看成是将链表中的每个结点的指向反向(也就是说从后一个结点指向前一个结点)。
在这里插入图片描述
在这里插入图片描述

我们在考虑情况的时候,还是分两种情况:先考虑一般情况,再考虑极端情况。

1.考虑一般情况

思考一下:建立两个指针变量,我们如果直接让后一个结点指向前一个结点,那么后一个结点所指向的再后面一个结点的位置是不是不确定吧?
因此,我们需要定义3个指针变量:p1,p2,p3 。

p1:记录指针指向将要反转的结点反转后要指向的位置。
p2:记录指针指向将要反转的结点。
p3:记录指针指向将要反转的结点的下一个结点。
在这里插入图片描述
在反转时,我们首先让p2指向的结点指向p1指向的位置,其次让p1,p2,p3指针统一后移.
在这里插入图片描述
如此往复。

2.考虑特殊情况

特殊情况也就是对边界进行分类讨论,有三种1情况:反转第一个结点指针的指向,反转最后一个结点指针的指向,传入的链表为空时的情况。

2.2当链表的最后一个节点为待移除的节点时:

p2记录的是指针指向将要反转的结点,所以当反转第一个结点指针的指向时,p2指针便指向的就是第一个结点。
在这里插入图片描述

反转过程中是让p2指向的结点指向p1指向的位置,所以我们只需用将p1的初始值赋值为NULL即可。这样,反转后就让第一个结点指针指向NULL了(也就是说反转后的最后一个结点指向空)。

2.2当链表的最后一个节点为待移除的节点时:

当最后一个结点的指针指向被反转时,p2刚好指向了最后一个结点,在指针反转完成之后,p1,p2,p3指针统一向后移动.

在这里插入图片描述
我们可以基本上是没有问题的,而且此时也发现遍历链表时的终止条件 需要返回的新的头指针, 需要返回的新的头指针,也就是当p2指针为NULL时停止遍历,并且返回p1指针指向的位置。

我们这里需要注意,这时这3个指针统一后移动时,p3指针的后移将会失败,p3后移前指向的是NULL,因此我们后移p3指针前需判断其是否为空。

2.3当传入的链表为空时:

如果传入的链表为空,我们根本不需要对链表进行任何操作,直接返回传入的头指针就可解决。(只有一个节点时也满足)

代码实现

struct ListNode
 {
	int val;
	struct ListNode *next;
};

struct ListNode* reverseList(struct ListNode* head)
{
	if (head == NULL || head->next == NULL)              //当链表为空或只有一个结点时,无需操作
	{
	    return head;                                     //直接返回
	}                                 

	struct ListNode* p1 = NULL;                           //记录指针指向将要反转的结点反转后要指向的位置。
	struct ListNode* p2 = head;                           //记录指针指向将要反转的结点。
	struct ListNode* p3 = head->next;                     //记录指针指向将要反转的结点的下一个结点。
	while (p2)                                             //p2为NULL时,停止遍历
	{
		p2->next = p1;                                      //反转结点指向
		p1 = p2;                                            //指针后移
		p2 = p3;                                            //指针后移
		if (p3)                                             //判断p3是否为NULL
		{
	       p3 = p3->next;                              //指针后移
		}                                   
	}
	return p1;                                              //返回p1指针指向的位置
}

思路二:

将原链表的结点,从头到尾遍历完,然后一个一个拿下来头插到一个新链表中。(这个新链表起始时为一个空链表)
在这里插入图片描述
直到遍历完为止。

代码实现

struct ListNode 
{
	int val;
	struct ListNode *next;
};

struct ListNode* reverseList(struct ListNode* head)
{
	struct ListNode* cur = head;                                   //记录当前待头插的结点
	struct ListNode* newhead = NULL;                               //新链表初始时为空
	while (cur)                                                    //链表中结点头插完毕时停止循环
	{
		struct ListNode* next = cur->next;                         //记录下一个待头插的结点
		cur->next = newhead;                                       //将结点头插至新链表
		newhead = cur;                                             //新链表头指针后移
		cur = next;                                                //指向下一个待头插的结点
	}
	return newhead;                                                //返回反转后的头指针
}

链表的中间节点

题目描述:
给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。(题目来源:Leetcode876.链表的中间节点)
在这里插入图片描述

解题思路:

因为这道题目并没有时间复杂度的规定,所以若想要解决这道问题是非常简单的。我们只需要先遍历一遍链表,统计链表当中的结点个数,然后再遍历一遍链表,寻找中间位置的结点即可。

代码实现:

struct ListNode {
	int val;
	struct ListNode *next;
};

struct ListNode* middleNode(struct ListNode* head)
{
	struct ListNode* cur = head;//记录当前结点位置
	int count = 0;//记录链表中结点的总数
	while (cur)//遍历的停止条件
	{
		count++;//总数加一
		cur = cur->next;//指针后移
	}
	int mid = count / 2;//中间结点与第一个结点之间相差的结点数
	struct ListNode* midnode = head;//记录中间结点的位置
	while (mid--)//从第一个结点开始,指针后移mid个结点
	{
		midnode = midnode->next;//指针后移
	}
	return midnode;//返回中间结点
}

附加条件:

我们可以明显知道,上面这种思路的时间复杂度是O(2n),那么我们有没有办法在只遍历一遍链表的情况下找到中间结点的位置呢?也就是要求代码的时间复杂度为O(n)

思路(快慢指针)

我们不妨定义两个指针名叫:fastslow
fast:记录当前遍历到的最后一个结点。(快指针)
slow:记录已经遍历过的结点的中间结点。(慢指针)

通过观察,我们可以发现,当slow指针走一步时,fast指针走一步或是走两步都满足slow指针指向的是已经遍历过的结点的中间结点。也就是slow指针走一步,fast指针最多可以走两步。
在这里插入图片描述

所以,我们可以遍历链表,当fast指针遍历到链表末尾时,就立刻返回此时的slow指针即可。

需要注意的是:因为fast指针一次是走两步,所以当fast指针指向的内容为空或是fast指针指向的结点所指向的内容为空时,均停止遍历链表。
在这里插入图片描述

这两种情况下均停止遍历,立刻返回slow指针。

这样,我们就在只遍历了一遍链表的情况下找到了中间结点的位置,即时间复杂度为O(n)。

代码实现:

struct ListNode {
	int val;
	struct ListNode *next;
};

struct ListNode* middleNode(struct ListNode* head)
{
	struct ListNode* fast = head;//快指针
	struct ListNode* slow = head;//慢指针
	while (fast&&fast->next)//遍历继续的条件
	{
		slow = slow->next;//慢指针一次走一步
		fast = fast->next->next;//快指针一次走两步
	}
	return slow;//返回慢指针
}

链表中第K个节点

题目描述:输入一个链表,输出该链表中倒数第k个结点。(题目来源)
在这里插入图片描述

思路一(遍历):

由于这道题目并没有要求时间复杂度,我们完全可以先遍历一遍链表,得到链表的结点总数(count),然后再遍历一遍链表,从第一个结点开始,后面的第count - k个结点即为目标结点。

但是在求解过程中有两个情况需要中途便返回NULL:
1.当传入的链表为空时,直接返回空(NULL)。
2.当计算出的链表总结点数count小于k时,返回空(NULL)。

代码实现:

struct ListNode 
{
	int val;
	struct ListNode *next;
};

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k)
{
	if (pListHead == NULL)//判断链表是否为空
		return NULL;
	struct ListNode* cur = pListHead;//记录当前结点位置
	int count = 0;//记录链表的总结点数
	while (cur)
	{
		count++;//结点总数加一
		cur = cur->next;//指针后移
	}
	if (count < k)//count小于k,此时不存在倒数第k个结点
		return NULL;
	struct ListNode* ret = pListHead;
	int pos = count - k;//倒数第k个结点距离第一个结点的结点数
	while (pos--)
	{
		ret = ret->next;//指针后移
	}
	return ret;//返回目标结点
}

思路二(快慢指针):

其实我们还是可以运用“快慢指针”的思想来解决这道题,这样可以使得代码的时间复杂度直接从O(n2)变为O(n)
需要注意的是:这里所说的“快慢指针”并非一个指针走得快,另一个指针走得慢,而是快指针先走,慢指针在快指针走到某一位置后再开始走。(也可以称为双指针法)

因为从最后一个结点开始,再往后走一步便是NULL;从倒数第二个结点开始,再往后走两步便是NULL;从倒数第k个结点开始,再往后走k步便是NULL。所以我们可以先让快指针(fast)先走k步,然后慢指针(slow)再和快指针一起往后走,这样,当快指针走到NULL时,慢指针指向的结点就是倒数第k个结点。
在这里插入图片描述

注意:在快指针(fast)先向后走k步这个过程中,若遇到了NULL,那么说明链表为空,或是k的值大于链表中结点的总数,此时需返回NULL。

代码实现:

struct ListNode {
	int val;
	struct ListNode *next;
};

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k)
{
	struct ListNode* fast = pListHead;//快指针
	struct ListNode* slow = pListHead;//慢指针
	while (k--)//快指针先向后移动k步
	{
		if (fast == NULL)//快指针移动过程中链表遍历结束,不存在倒数第k个结点
			return NULL;
		fast = fast->next;//快指针后移
	}
	while (fast)//快指针遍历完链表时结束遍历
	{
		fast = fast->next;//快指针后移
		slow = slow->next;//慢指针后移
	}
	return slow;//返回慢指针的值
}

合并两个有序链表:

题目描述:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。(题目来源)
在这里插入图片描述
在这里插入图片描述

思路:

该题的思路比较简单,我们只需创建一个头结点,然后从两个链表的表头开始依次比较传入的两个链表的结点的大小,并将两个链表中较小的结点尾插到新链表的后面即可。
在这里插入图片描述

完成一次尾插后,接着比较未尾插的结点,并将较小的结点继续尾插到新链表后面。
在这里插入图片描述

直到最后两个链表的结点都被尾插到新链表的后面。
在这里插入图片描述
注意两点:
1.在尾插过程中,若某一链表已被遍历完毕,则直接将另一个未遍历完的链表剩下的结点尾插到新链表后面即可。
2.函数返回的时候,不是返回头结点的地址,而是第一个结点的地址,所以我们要返回头结点指向的位置并将头结点释放。

代码实现:

struct ListNode {
	int val;
	struct ListNode *next;
};

struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2)
{
	struct ListNode* guard = (struct ListNode*)malloc(sizeof(struct ListNode));//申请一个头结点
	struct ListNode* tail = guard;//尾指针
	struct ListNode* cur1 = l1;//记录当前遍历到的l1链表的结点位置
	struct ListNode* cur2 = l2;//记录当前遍历到的l2链表的结点位置
	while (cur1&&cur2)//当l1,l2中有一个链表遍历完毕时便停止
	{
		//取小的结点尾插到新链表后面
		if (cur1->val < cur2->val)
		{
			tail->next = cur1;
			cur1 = cur1->next;
		}
		else
		{
			tail->next = cur2;
			cur2 = cur2->next;
		}
		tail = tail->next;//结点增加,尾指针后移
	}
	//将未遍历完的链表的剩余结点接到新链表后面
	if (cur1)
		tail->next = cur1;
	else
		tail->next = cur2;

	struct ListNode* head = guard->next;//新链表的头指针
	free(guard);//释放头结点
	return head;//返回新链表
}

链表分割

题目描述:现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。(题目来源)
在这里插入图片描述

思路:

创建两个链表,遍历一遍传入的链表,将值大于x的结点和值小于x的结点依次尾插到两个链表中,最后再将这两个链表链接起来,并返回第一个结点的位置即可。

1.把小于x的结点尾插到less链表,把大于x的结点尾插到greater链表。

在这里插入图片描述

2.将less链表与greater链表链接起来。

在这里插入图片描述

注意:
1.链接后的链表的最后一个结点的指针域需要置空,否则可能造成链表成环。
2.返回的头指针应是lessHead->next,而不是lessHead。

代码实现

struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};

class Partition 
{
public:
	ListNode* partition(ListNode* pHead, int x) 
	{
		ListNode* greaterHead, *greaterTail, *lessHead, *lessTail;
		//申请一个头结点,后面链接大于x的结点
		greaterHead = greaterTail = (ListNode*)malloc(sizeof(struct ListNode));
		//申请一个头结点,后面链接小于x的结点
		lessHead = lessTail = (ListNode*)malloc(sizeof(struct ListNode));
		greaterTail->next = NULL;//尾指针的指针域置空
		lessTail->next = NULL;//尾指针的指针域置空
		ListNode* cur = pHead;//接收传入的链表,准备遍历
		while (cur)
		{
			if (cur->val < x)
			{
				//结点值小于x,链接到less链表后面
				lessTail->next = cur;
				lessTail = lessTail->next;
			}
			else
			{
				//结点值大于x,链接到greater链表后面
				greaterTail->next = cur;
				greaterTail = greaterTail->next;
			}
			cur = cur->next;//指针后移,遍历后面的结点
		}
		//将less链表和greater链表链接起来
		lessTail->next = greaterHead->next;//greater链表的第一个结点链接到less链表的尾上
		greaterTail->next = NULL;//将greater链表最后一个结点的指针域置空
		ListNode* head = lessHead->next;//接收链接后链表的第一个结点地址
		free(greaterHead);//释放greater链表的头结点
		free(lessHead);//释放less链表的头结点
		return head;//返回新链表
	}
};

链表的回文结构:

题目描述:
对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。

给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。(题目来源)
在这里插入图片描述

思路:

我们需要找到传入链表的中间结点,并将中间结点及其后面结点进行反转,然后再将原链表的前半部分与反转后的后半部分进行比较,若相同,则该链表是回文结构,否则,不是回文结构。

1.找到链表的中间结点。
在这里插入图片描述

2.反转中间结点及其后面的结点。
在这里插入图片描述

3.比较链表的前半部分与后半部分的结点值,若相同则是回文结构,否则,不是回文结构。
在这里插入图片描述

将A指针指向的结点与RHead指针指向的结点进行比较,若相同,则两个指针后移,继续比较后面的结点,直到RHead指针指向NULL时,比较结束。

注意:就算传入的链表是结点数为奇数的回文结构,该思路也可以成功判断。
例如,以下链表反转其后半部分后,我们看似链表应该是这样的。
在这里插入图片描述

但反转后的链表并不是这样的,而应该是下面这样:
在这里插入图片描述

因为我们反转的是中间结点及其后面的结点,并没有对前面的结点进行任何操作,所以结点5所指向的结点应该还是结点7。

于是该链表的比较过程应该是这样的:1等于1,3等于3,5等于5,7等于7,然后RHead指针指向NULL。所以判断该链表是回文结构。

代码实现:

struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) : val(x), next(NULL) {}
}; 

class PalindromeList 
{
public:
	//查找链表的中间结点
	struct ListNode* middleNode(struct ListNode* head)
	{
		struct ListNode* fast = head;//快指针
		struct ListNode* slow = head;//慢指针
		while (fast&&fast->next)//遍历继续的条件
		{
			slow = slow->next;//慢指针一次走一步
			fast = fast->next->next;//快指针一次走两步
		}
		return slow;//返回慢指针
	}
	//反转链表
	struct ListNode* reverseList(struct ListNode* head)
	{
		struct ListNode* cur = head;//记录当前待头插的结点
		struct ListNode* newhead = NULL;//新链表初始时为空
		while (cur)//链表中结点头插完毕时停止循环
		{
			struct ListNode* next = cur->next;//记录下一个待头插的结点
			cur->next = newhead;//将结点头插至新链表
			newhead = cur;//新链表头指针后移
			cur = next;//指向下一个待头插的结点
		}
		return newhead;//返回反转后的头指针
	}
	bool chkPalindrome(ListNode* A) {
		ListNode* mid = middleNode(A);//查找链表的中间结点
		ListNode* RHead = reverseList(mid);//反转后半段链表
		while (RHead)//比较结束的条件
		{
			if (A->val != RHead->val)//不是回文结构
				return false;
			A = A->next;//指针后移
			RHead = RHead->next;//指针后移
		}
		return true;//是回文结构
	}
};

相交链表

题目描述:
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。(题目来源1)(题目来源2)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

思路:

1.判断这两个链表是否相交。
要寻找两个链表的起始结点,首先我们需要判断这两个链表是否相交。那么如何判断两个单向链表是否相交呢?如果两个单向链表是相交的那么这两个链表的最后一个结点必定是同一个。
在这里插入图片描述

若两个链表的最后一个结点不是同一个结点,那么这两个链表必定不会相交。所以,我们只需判断两个链表的最后一个结点是否相同即可判断这两个链表是否相交了。

2.寻找这两个链表的起始相交结点。
我们假设这两个链表的结点个数之差为count,我们可以让指向较长链表的指针先向后移动count步,然后指向长链表的指针和指向短链表的指针再同时向后移动,这样这两个指针最后会同时走到各自的链表结尾(NULL)。
在这里插入图片描述

在两个指针同时向后移动的过程中,第一次指向的同一个结点便是这两个相交链表的起始结点。这时返回该结点地址即可。
在这里插入图片描述

注意:在寻找链表的最后一个结点的同时,我们便可以计算两个链表的长度,只不过这时我们只遍历到了最后一个结点,并没有遍历到NULL,所以统计的两个链表的结点个数都比链表实际长度少一,但这两个值相减后依然是这两个链表的结点个数差。

代码实现:

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{
    //用于定义两个链表的指针
    struct ListNode*curA=headA,*curB=headB;
    //记录链表长度
    int lenA=0,lenB=0;
    //寻找链表A的最后一个节点,同时计算链表A的长度
    while(curA->next)
    {
        lenA++;
        curA=curA->next;
    }
    //寻找链表B的最后一个节点,同时计算链表B的长度
    while(curB->next)
    {
        lenB++;
        curB=curB->next;
    }
    //判断两个链表是否相交
    if(curA!=curB)
      return NULL;
    //用于寻找两个链表的起始相交节点的指针
    struct ListNode*longlist=headA,*shortlist=headB;
    if(lenA<lenB)
    {
        longlist=headB;
        shortlist=headA;
    }
    //两个相交链表的节点数之差
    int count=abs(lenA-lenB);
    //让指向较长的链表的指针先走count步
    while(count--)
    {
        longlist=longlist->next;
    }
    //然后两个指针同时向后移动,在此过程中若两个指针指向的节点地址相同,则该节点为来拿表的起始位置
    while(longlist!=shortlist)
    {
        longlist=longlist->next;
        shortlist=shortlist->next;
    }
      //返回链表相交的起始节点   
       return longlist;
}

环形链表

题目描述:
给你一个链表的头节点 head ,判断链表中是否有环。

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

如果链表中存在环 ,则返回 true 。 否则,返回 false 。
在这里插入图片描述

思路:

在这里插入图片描述
可以明确的是:若一个链表带环,那么用指针一直顺着链表遍历,最终会回到某个地方。
 我们可以定义两个指针(快慢指针),两个指针均从表头开始同时遍历链表,快指针一次走两步,慢指针一次走一步。如果链表带环,那么快慢指针一定会相遇,否则快指针会先遍历完链表(遇到NULL)。
 若是你不明白为什么链表带环,快慢指针就一定会相遇,那么你可以想想龟兔赛跑。

代码:

struct ListNode 
{
	int val;
	struct ListNode *next;
};
bool hasCycle(struct ListNode *head) 
{
	struct ListNode* slow = head;
	struct ListNode* fast = head;
	while (fast && fast->next)
	{
		fast = fast->next->next;//兔子走两步
		slow = slow->next;//乌龟走一步
		if (fast == slow)//兔子与乌龟相遇
			return true;
	}
	return false;
}

返回链表开始入环的第一个结点

题目描述:给定一个链表,返回链表开始入环的第一个结点。如果链表无环,则返回NULL。

这不是一道简简单单的代码题,严格来说,这是一道数学推论题,若是不能得出最终推论,是不能很好的解决该问题的。

推论如下:
在这里插入图片描述
根据最终推论可以得出结论:若一个指针从出发点开始走,另一个指针从相遇点开始走,则他们最终会在入口点处相遇。

代码实现

struct ListNode {
	int val;
	struct ListNode *next;
};
struct ListNode *detectCycle(struct ListNode *head) {
	struct ListNode* fast = head;
	struct ListNode* slow = head;
	while (fast && fast->next)
	{
		slow = slow->next;//慢指针走一步
		fast = fast->next->next;//快指针走两步
		if (fast == slow)//相遇
		{
			struct ListNode* meet = fast;//相遇点
			while (head != meet)
			{
				head = head->next;//一个指针从出发点开始走
				meet = meet->next;//一个指针从相遇点开始走
			}
			return meet;//两个指针相遇,返回当前结点
		}
	}
	return NULL;//链表不带环
}

面试还可能出现的问题
问题一:为什么慢指针走一步,快指针走两步,他们一定会在环里面相遇?会不会永远追不上?请证明。

不会永远追不上,证明如下:
在这里插入图片描述

问题二:那么慢指针走一步,快指针走三步?走四步?或是走n步行不行?为什么?请证明。

不行,这样可能会追不上,证明如下:
在这里插入图片描述

总结一下:
 当慢指针走一步,快指针走三步时。若慢指针进环时与快指针之间的距离为奇数,并且环的周长恰好为偶数,那么他们会一直在环里面打转转,永远不会相遇。
(当慢指针走一步,快指针走四步或是走n步时,证明过程类似)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1174067.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mega Sales购物季营销,掌握流量密码不是事

“Mega Sales”购物季是东南亚地区“双十一、双十二”的两个重要节点&#xff0c;在年末即将来临之际&#xff0c;全球电商市场都正进入激动人心的备货大卖时期。面对这样的重要节目&#xff0c;商家应该如何做好营销铺垫&#xff0c;全面备战大促的不同阶段&#xff1f; 作为…

C生万物 | 从浅入深理解指针【第三部分】(qsort的使用和模拟实现)

C生万物 | 从浅入深理解指针【第四部分】&#xff08;qsort的使用和模拟实现&#xff09; 文章目录 C生万物 | 从浅入深理解指针【第四部分】&#xff08;qsort的使用和模拟实现&#xff09;回调函数是什么&#xff1f;qsort使用举例qsort函数的模拟实现 回调函数是什么&#x…

网络层:控制平面

路由选择算法 路由选择算法就是为了在端到端的数据传输中&#xff0c;选择路径上路由器的最好的路径。通常&#xff0c;一条好的路径指具有最低开销的路径。最低开销路径是指源和目的地之间具有最低开销的一条路。 根据集中式还是分散式来划分 集中式路由选择算法&#xff1a…

从UEFI如何启动到系统

从UEFI如何启动到系统 文章目录 从UEFI如何启动到系统UEFI须知1. 进入UEFI setup界面2. Setup界面3. BootManager界面4. Shell下操作4.1. 显示启动设备4.2. 进入设备及查看文件4.3. UEFI下的其他操作4.4. UEFI下的一些Shell命令 5. UEFI下更新固件方法GRUBGRUB界面1. 编辑GRUB选…

为什么搭建自己的帮助中心很重要?

在当今数字化和信息爆炸的时代&#xff0c;用户对产品和服务的期望越来越高。他们希望能够快速找到解决问题的方法和答案&#xff0c;而不是在不尽其穷的文档和论坛中浪费时间。在这样的背景下&#xff0c;建立一个完善的帮助中心成为了企业提供优质客户支持的关键。帮助中心不…

VSCode 连接不上 debian 的问题

之前一台笔记本上安装了 debian12&#xff0c;当时用 vscode 是可以连接上的&#xff0c;但今天连接突然就失败了&#xff0c;失败信息是这样的&#xff1a; 查看失败信息 因为 debian 是自动获取 ip 地址的&#xff0c;以前能连接上时&#xff0c;ip 地址是 104&#xff0c;然…

【免费生产力工具】CodeGeeX: AI代码自动补齐、对话、自动注释

CodeGeeX - A Multilingual Code Generation Tool - CodeGeeX 这个是官网&#xff0c;工程好像是开源的&#xff0c;生态不错。清华校友确实强&#xff0c;我是菜菜。 我是在vscode里面装的插件&#xff0c;直接搜索就行。 多的就不bb了&#xff0c;大家试试吧&#xff0c;确…

[强网杯 2019]随便注1

打开题目 输入1 输入1&#xff0c;页面报错&#xff0c;输入1 #页面正常 说明1为注入点且注入方式为字符型的单引号注入 判断列名 输入 1 order by 2 # 页面正常 1 order by 3 #页面报错 说明列名字段数为2 接下来我们尝试用联合注入的方式爆出数据显示位 输入1 union s…

Java生成word文档

一 前言 Java编程生成word文档这种操作一般是常规操作比较常见&#xff0c;主要采用Apache的POI Word 这个库操作的比较多&#xff0c;还有的用Spire.Doc&#xff0c;但是这个库有些稍微难点的功能要收费&#xff0c;看了下费用还不低&#xff0c;周末朋友问起是否有用java操作…

Linux - 守护进程的概念

Linux下的守护进程是在后台运行的特殊进程&#xff0c;它不与任何终端关联&#xff0c;通常在系统启动时自动启动&#xff0c;运行在后台并且不受用户登录或注销的影响。Linux 下的守护进程通常是以系统管理员的权限运行&#xff0c;用来执行一些系统任务&#xff0c;例如监控硬…

【RabbitMQ】RabbitMQ 集群的搭建 —— 基于 Docker 搭建 RabbitMQ 的普通集群,镜像集群以及仲裁队列

文章目录 一、集群分类1.1 普通模式1.2 镜像模式1.3 仲裁队列 二、普通集群2.1 目标集群2.2 获取 Erlang Cookie2.3 集群配置2.4 启动集群2.5 测试集群 三、镜像模式3.1 镜像模式的特征3.2 镜像模式的配置3.2.1 exactly 模式3.2.2 all 模式3.2.3 nodes 模式 3.3 测试镜像模式 四…

北大软微2024推免拟录取名单及分析

拟录取名单 直博生 硕士生 分析 北大软微在2023年的推免中共录取直博生17人&#xff0c;硕士生205人&#xff0c;其中硕士生全为专硕电子信息 当然&#xff0c;从录取结果咱们可以看出来&#xff0c;除了985和强势211&#xff0c;中国地质&#xff08;北京&#xf…

基于人工大猩猩部队算法的无人机航迹规划-附代码

基于人工大猩猩部队算法的无人机航迹规划 文章目录 基于人工大猩猩部队算法的无人机航迹规划1.人工大猩猩部队搜索算法2.无人机飞行环境建模3.无人机航迹规划建模4.实验结果4.1地图创建4.2 航迹规划 5.参考文献6.Matlab代码 摘要&#xff1a;本文主要介绍利用人工大猩猩部队算法…

解决:GnuTLS recv error (-110): The TLS connection was non-properly terminated.

fatal: 无法访问 https://github.com/facebookresearch/fvcore/&#xff1a;GnuTLS recv error (-110): The TLS connection was non-properly terminated. error: subprocess-exited-with-error 关闭梯子&#xff0c;再试一次就好了

web前端——HTML+CSS实现奥运五环

web前端——HTMLCSS实现奥运五环 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</titl…

【Vue基础-实践】数据可视化大屏设计(林月明螺蛳粉文化公司单量数据大屏)

目录 一、知识整理 1、页面自适应 2、下载插件px to rem & rpx 3、关于padding与margin 4、下载echarts 5、下载axios 6、experss官网接口创建 7、创建路由 8、api接口创建 9、设置基准路径 10、跨域设置 11、图表设置 12、地图数据引用 13、设置地图效果 二、…

【MySQL】数据库MySQL基础知识与操作

作者主页&#xff1a;paper jie_博客 本文作者&#xff1a;大家好&#xff0c;我是paper jie&#xff0c;感谢你阅读本文&#xff0c;欢迎一建三连哦。 本文录入于《MySQL》专栏&#xff0c;本专栏是针对于大学生&#xff0c;编程小白精心打造的。笔者用重金(时间和精力)打造&a…

聊一聊关于手机Charge IC的电流流向

关于手机Charge&#xff0c;小白在以前的文章很少讲&#xff0c;一是这部分东西太多&#xff0c;过于复杂。二是总感觉写起来欠缺点什么。但后来想一想&#xff0c;本是抱着互相学习来写文章的心理态度&#xff0c;还是决定尝试写一些。 关于今天要讲的关于手机Charge的内容&a…

软件测试具体人员分工

最近看了点敏捷测试的东西&#xff0c;看得比较模糊。一方面是因为没有见真实的环境与流程&#xff0c;也许它跟本就没有固定的模式与流程&#xff0c;它就像告诉人们要“勇敢”“努力”。有的人在勇敢的面对生活&#xff0c;有些人在勇敢的挑战自我&#xff0c;有些人在勇敢的…

SLAM从入门到精通(被忽视的基础图像处理)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 工业上用激光slam的多&#xff0c;用视觉slam的少&#xff0c;这是大家都知道的常识。毕竟对于工业来说&#xff0c;健壮和稳定是我们必须要考虑的…