带你拿捏链表

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

博主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 的节点,并返回 新的头节点 。(题目来源)

带你拿捏链表_第1张图片
带你拿捏链表_第2张图片
带你拿捏链表_第3张图片

思路一:

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

1.考虑常见情况

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

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

带你拿捏链表_第4张图片
当cur指针指向的节点并非待移除的节点时,3个节点依次向后移动。
带你拿捏链表_第5张图片
当cur指针指向待移除的节点时,我们首先让Prev指针指向的节点指向next,然后把cur指针指向的节点释放掉,并将next指针赋值给cur指针,next指针向后移动。
带你拿捏链表_第6张图片
如此进行下去,直到链表遍历完毕,值val的节点也就删除完了。

2.考虑特殊情况

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

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

带你拿捏链表_第7张图片
这时我们需要先将头指针指向Next,然后释放掉cur所指向的节点,并将next指针赋值给cue指针,next再后移。

带你拿捏链表_第8张图片

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

当排查到最后一个节点时,cur指向最后一个节点,next指针指向该节点的位置,即NULL.
带你拿捏链表_第9张图片
我们上面常规情况的方法对其分析,发现常规情况的思路适用于这种特殊情况,并且发现遍历的终止条件,就是当cur为NULL时遍历停止。
带你拿捏链表_第10张图片

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;//返回新的头指针
}

思路二:

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

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

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

代码实现

/**
 * 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;//返回新的头指针
}

解题结果如下:

带你拿捏链表_第13张图片

反转链表

给你单链表的头节点 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.链表的中间节点)
带你拿捏链表_第14张图片

解题思路:

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

代码实现:

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指针最多可以走两步。
带你拿捏链表_第15张图片

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

需要注意的是:因为fast指针一次是走两步,所以当fast指针指向的内容为空或是fast指针指向的结点所指向的内容为空时,均停止遍历链表。
带你拿捏链表_第16张图片

这两种情况下均停止遍历,立刻返回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个结点。(题目来源)
带你拿捏链表_第17张图片

思路一(遍历):

由于这道题目并没有要求时间复杂度,我们完全可以先遍历一遍链表,得到链表的结点总数(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个结点。
带你拿捏链表_第18张图片

注意:在快指针(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;//返回慢指针的值
}

合并两个有序链表:

题目描述:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。(题目来源)
带你拿捏链表_第19张图片
带你拿捏链表_第20张图片

思路:

该题的思路比较简单,我们只需创建一个头结点,然后从两个链表的表头开始依次比较传入的两个链表的结点的大小,并将两个链表中较小的结点尾插到新链表的后面即可。
带你拿捏链表_第21张图片

完成一次尾插后,接着比较未尾插的结点,并将较小的结点继续尾插到新链表后面。
带你拿捏链表_第22张图片

直到最后两个链表的结点都被尾插到新链表的后面。
在这里插入图片描述
注意两点:
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的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。(题目来源)
带你拿捏链表_第23张图片

思路:

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

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

带你拿捏链表_第24张图片

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

带你拿捏链表_第25张图片

注意:
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。(题目来源)
带你拿捏链表_第26张图片

思路:

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

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

2.反转中间结点及其后面的结点。
带你拿捏链表_第27张图片

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

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

注意:就算传入的链表是结点数为奇数的回文结构,该思路也可以成功判断。
例如,以下链表反转其后半部分后,我们看似链表应该是这样的。
带你拿捏链表_第28张图片

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

因为我们反转的是中间结点及其后面的结点,并没有对前面的结点进行任何操作,所以结点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)
带你拿捏链表_第29张图片
带你拿捏链表_第30张图片
带你拿捏链表_第31张图片

思路:

1.判断这两个链表是否相交。
要寻找两个链表的起始结点,首先我们需要判断这两个链表是否相交。那么如何判断两个单向链表是否相交呢?如果两个单向链表是相交的那么这两个链表的最后一个结点必定是同一个。
带你拿捏链表_第32张图片

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

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

在两个指针同时向后移动的过程中,第一次指向的同一个结点便是这两个相交链表的起始结点。这时返回该结点地址即可。
带你拿捏链表_第34张图片

注意:在寻找链表的最后一个结点的同时,我们便可以计算两个链表的长度,只不过这时我们只遍历到了最后一个结点,并没有遍历到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 。
带你拿捏链表_第35张图片

思路:

带你拿捏链表_第36张图片
可以明确的是:若一个链表带环,那么用指针一直顺着链表遍历,最终会回到某个地方。
 我们可以定义两个指针(快慢指针),两个指针均从表头开始同时遍历链表,快指针一次走两步,慢指针一次走一步。如果链表带环,那么快慢指针一定会相遇,否则快指针会先遍历完链表(遇到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。

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

推论如下:
带你拿捏链表_第37张图片
根据最终推论可以得出结论:若一个指针从出发点开始走,另一个指针从相遇点开始走,则他们最终会在入口点处相遇。

代码实现

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;//链表不带环
}

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

不会永远追不上,证明如下:
带你拿捏链表_第38张图片

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

不行,这样可能会追不上,证明如下:
带你拿捏链表_第39张图片

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

你可能感兴趣的:(数据结构,leetcode,链表,数据结构,c++,链表)