单向链表相交的第一个公共结点, 判断链表是否有环以及环的入口节点

本文介绍了一种算法,用于查找两个单向链表的第一个公共节点。通过计算链表长度并调整遍历起点,确保同时到达交点,还探讨了如何检测链表中的环及其入口节点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

//////题目:两个单向链表,找出它们的第一个公共结点。
//////
//////链表的结点定义为:
//////
//////struct ListNode
//////
//////{
//////
//////      int       m_nKey;
//////
//////      ListNode*   m_pNext;
//////
//////};
//////
//////分析:这是一道微软的面试题。微软非常喜欢与链表相关的题目,因此在微软的面试题中,链表出现的概率相当高。
//////如果两个单向链表有公共的结点,也就是说两个链表从某一结点开始,它们的m_pNext都指向同一个结点。
//////但由于是单向链表的结点,每个结点只有一个m_pNext,因此从第一个公共结点开始,
//////之后它们所有结点都是重合的,不可能再出现分叉。所以,两个有公共结点而部分重合的链表,
//////拓扑形状看起来像一个Y,而不可能像X。

#include <iostream>
#include <algorithm>
using namespace std;

#define NSIZ 101

struct ListNode 
{ 

	int m_nKey; 
	ListNode*  m_pNext; 

}; 

//获得链表长度
int GetListLength(ListNode * head)
{
	if(!head)
	{
		return 0;
	}
	int len = 0;
	ListNode * tmp = head;

	while(tmp)
	{
		++len;
		tmp = tmp->m_pNext;
	}
	return len;
}

//尾插法构造链表
void CreateList(ListNode * & head, int arr[], int n)
{
	ListNode * p = head;
	if(head == 0)
	{
		head = new ListNode();
		head->m_nKey = arr[0];
		p = head;
	}
	int i = 1;
	for(;i < n; ++i)
	{
		ListNode * tmp = new ListNode();
		tmp->m_nKey = arr[i];
		p->m_pNext = tmp;
		p = tmp;
	}
	p->m_pNext = 0;
}

//合并2个链表
ListNode* UnionList(ListNode * head1, ListNode * head2)
{

	if(head1 == 0 && head2)
	{
		return head2;
	}
	else if(head1 && !head2)
	{
		return head1;
	}
	else if(head1 && head2)
	{
		ListNode * pre = head1;
		ListNode * p = pre->m_pNext;
		while(p)
		{
			pre = p;
			p = p->m_pNext;
		}

		pre->m_pNext = head2;
		return head1;
	}
	else
	{
		return 0;
	}
}

//打印链表元素值
void dump(ListNode * head)
{
	ListNode * p = head;
	while(p)
	{
		printf("%d ", p->m_nKey);
		p = p->m_pNext;
	}

	printf("\n");
}

//判断两个链表是否有交点,如果有则返回第一个交点
//如果head1 && head1 == head2那么显然相交,返回head1
//否则 分别获得head1链表和head2链表的长度为len1和len2 ,并且假设len1 > len2
//然后用指针pLong从head1开始向后移sub = len1 - len2步,指针pShort = head2
//然后p1,p2每次前进一步并且比较p1 == p2,若不为空且相等则返回p1,
//否则说明head1链表和head2链表没有交点
ListNode * FirstCommonNode(ListNode * head1, ListNode * head2)
{
	int len1 = GetListLength(head1);
	int len2 = GetListLength(head2);
	ListNode * Short = head1, *Long = head2;
	int sub = len2 - len1;
	if (len1  > len2)
	{
		Short = head2;
		Long = head1;
		sub  = len1 - len2;
	}

	while(sub > 0)
	{
		Long = Long->m_pNext;
		--sub;
	}

	while(Short && Long && Short != Long)
	{
		Short = Short->m_pNext;
		Long = Long->m_pNext;
	}

	if (Short && Long && Short == Long)
	{
		return Short;
	}

	return 0;
}

//检测链表中是否有环,快慢指针方法
//若有环则返回从头结点进入环的第一个节点
//假设p1 = head1, p2 = head->next->next
//首先检查是否有环,有环的话p1,p2必然在环内相遇
//此时从相遇处端口,方法为
//common = p2;
//head2 = p2->next, p2 ->next=  0;
//此时原链表被分成了2个链表,头结点分别为head1, head2
//利用FirstCommonNode(ListNode * head1, ListNode * head2)函数得到相交节点FirtNode
//然后修复链表:common->next= head2
//最后返回FirstNode
ListNode * HasLoop(ListNode * head)
{
	if (!head)
	{
		return 0;
	}

	ListNode * slow = head;
	ListNode * quick = head;
	while(quick && quick->m_pNext)
	{
		slow = slow->m_pNext;
		quick = quick->m_pNext->m_pNext;
		if (slow == quick)
		{
			break;
		}
	}

	// 无环的话
	if (slow != quick )
	{
		return 0;
	}

	// p1表示相遇的点, p2表示p1->next
	//然后把p1->next 置为0
	//那么原链表形成了2个相交链表,一个以head为表头,一个以p2为表头
	ListNode * p1 = quick;
	ListNode * p2 = p1->m_pNext;
	p1->m_pNext = 0;
	ListNode * firtNode = FirstCommonNode(head,p2);

	p1->m_pNext = p2;
	return firtNode;
}


//给定单链表中某一个节点P,删除该节点(P节点不是最后节点)
//方法:首先存放好p中数据,然后把P->next中数据copy到p中,然后把p->next删除。
int DeleteNode(ListNode * p)
{
	if (!p)
	{
		return INT_MIN;
	}

	int tmp = p->m_nKey;
	ListNode * tmpNode = p->m_pNext;
	p->m_nKey = p->m_pNext->m_nKey;
	p->m_pNext = p->m_pNext->m_pNext;

	if (tmpNode != 0)
	{
		delete tmpNode;
		tmpNode = 0;
	}
}

//给定单链表中某一个非空节点P,在节点P前面插入一个节点
//方法:首先分配一个节点q,将q插在p点之后
//然后将p点数据copy到q节点中,然后再把要插入的数据放到p中
int InsertNode(ListNode * p, int data)
{
	if (!p)
	{
		return INT_MIN;
	}

	ListNode * tmpNode = new ListNode();
	tmpNode->m_nKey = p->m_nKey;
	tmpNode->m_pNext = p->m_pNext;
	p->m_pNext = tmpNode;

	p->m_nKey = data;

}

int main()
{

	int arr[] = {1, 2, 3};
	int brr[] = {4, 5, 6};
	int crr[] = {7, 8, 9};
	int alen = sizeof(arr)/sizeof(arr[0]);
	int blen = sizeof(brr)/sizeof(brr[0]);
	int clen = sizeof(crr)/sizeof(crr[0]);

	ListNode * head1 = 0;
	ListNode * head2 = 0;
	ListNode * head3 = 0;

	CreateList(head1, arr, alen);
	CreateList(head2, brr, blen);
	CreateList(head3, crr, clen);

	//合并链表1和链表3
	head1 = UnionList(head1, head3);
	//合并链表2和链表3
	head2 = UnionList(head2, head3);

	//打印链表1
	dump(head1);

	//打印链表2
	dump(head2);

	//寻找两个链表的第一个公共节点
	ListNode *findNode =  FirstCommonNode(head1, head2);
	printf("第一个公共节,地址为:Ox%x 值为:%d\n", findNode, findNode->m_nKey);

	findNode = HasLoop(head1);
	findNode == 0?printf("没有环\n"):printf("有环,换的入口节点值为:%d\n", findNode->m_nKey);
	return 0;
}


<think>嗯,我现在要解决的问题是找到两个无链表第一个公共节点,如果没有的话返回空。而且题目要求空间复杂度是O(1),时间复杂度是O(n)。那我得仔细想想怎么做。 首先,单链表公共节点意味着从某个节点开始,后面的所有节点都是相同的。也就是说,两个链表在某个点交汇之后,后面的节点都共享。比如,链表A在节点C处开始和链表B的节点C重合,那么C就是它们的第一个公共节点。 那我应该怎么找到这个节点呢?常见的思路可能有几种。比如,用哈希表存储其中一个链表的节点,然后遍历另一个链表,看是否有节点在哈希表中存在。第一个存在的节点就是公共节点。不过这样的话,空间复杂度是O(n),不符合题目要求的O(1)。所以这种方法不行,得换其他方法。 那有没有空间O(1)的方法呢?这时候可能需要双指针的方法。我之前听说过一种方法,让两个指针分别从两个链表的头开始走,当走到末尾时,跳到另一个链表的头继续走。这样两个指针会在公共节点相遇。比如,链表A的长度是a,链表B的长度是b,公共部分的长度是c。那么,当两个指针走完各自的链表后,再走对方的链表,总路程是a + b - c,这样他们会在交汇点相遇。这个方法可能可行。 那具体怎么操作呢?假设链表A的长度是m,链表B的长度是n。如果两个链表有公共部分,那么从公共节点开始,后面的长度是一样的。所以,如果两个链表的长度不同,那么较长的链表的前面部分可能多出一些节点,这些节点不可能出现在公共部分。所以,我们可以先计算两个链表的长度差,让长的链表的指针先走这个差值的步数,然后两个指针再同时走,直到相遇。 比如,链表A比链表B长k步,那么让A的指针先走k步,然后两个指针同时走,这样当它们相遇的时候就是第一个公共节点。这种方法的时间复杂度是O(m+n),空间复杂度是O(1),符合要求。 那具体步骤应该是怎样的呢?首先,遍历两个链表,得到它们的长度。然后计算长度差,让较长的链表的指针先移动长度差的步数。然后同时移动两个指针,直到找到相同的节点或者走到末尾。 那这样的话,代码的结构大致是: 1. 遍历链表A,得到长度lenA; 2. 遍历链表B,得到长度lenB; 3. 计算差值diff = abs(lenA - lenB); 4. 让较长的链表的指针先移动diff步; 5. 然后两个指针同时移动,直到它们相等; 6. 返回相遇的节点,或者null如果没有的话。 那这样是否正确呢?举个例子,假设链表A是1->2->3->4,链表B是5->3->4。公共节点是3。那么A的长度是4,B的长度是3。差值是1。让A的指针先移动1步到2,然后A的指针和B的指针同时移动:A的指针到3,B的指针到3,这时候相遇,返回正确节点。 另一个情况,如果两个链表没有公共节点,那么它们的指针最后都会走到null,这时候返回null即可。 这个方法应该可行。那现在需要考虑如何实现。代码的大致步骤: - 获取两个链表的长度。 - 移动长链表的头指针到和短链表相同长度的位置。 - 同时移动两个指针,直到找到相同节点。 那具体在Java中,怎么遍历链表?可以用循,直到指针走到null。同时记录长度。 然后,调整长链表的起始点。比如,如果链表A较长,那么让指针curA先走diff步。然后curA和curB同时走,比较是否相等。 需要注意的是,当两个链表相交的时候,两个指针会同时到达null,此时应该返回null。 那现在写代码的大体结构: public class Solution { public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { // 计算两个链表的长度 int len1 = getLength(pHead1); int len2 = getLength(pHead2); // 调整指针到相同起始点 ListNode cur1 = pHead1; ListNode cur2 = pHead2; // 让长的链表的指针先移动差值步 if(len1 > len2){ int diff = len1 - len2; while(diff-- >0){ cur1 = cur1.next; } } else { int diff = len2 - len1; while(diff-- >0){ cur2 = cur2.next; } } // 同时移动指针,直到相遇或null while(cur1 != null && cur2 != null){ if(cur1 == cur2){ return cur1; } cur1 = cur1.next; cur2 = cur2.next; } return null; } private int getLength(ListNode head){ int len =0; while(head != null){ len++; head = head.next; } return len; } } 这样的代码应该可以解决问题。那时间复杂度是O(n + m),空间复杂度是O(1),因为只用了几个指针变量。 不过还有一种更巧妙的方法,不需要计算长度。也就是我之前提到的双指针交替遍历两个链表的方法。具体来说,指针A从链表A开始遍历,到末尾后跳到链表B继续遍历;指针B从链表B开始遍历,到末尾后跳到链表A继续遍历。这样,当两个指针相遇的时候,就是第一个公共节点。或者如果没公共节点,最终两个指针都走到null。 比如,链表A的长度是a,链表B的长度是b,公共部分长度是c。那么,指针A走的路径是a + (b -c),指针B走的路径是 b + (a -c)。当相遇的时候,正好是公共节点。或者如果没有公共节点,那么他们走完a + b步后同时到达null。 这个方法的空间复杂度也是O(1),时间复杂度是O(n + m)。这种方法是否可行? 举个例子,链表A是1->2->3->4,链表B是5->3->4。指针A走的顺序是1,2,3,4,5,3,4。指针B走的顺序是5,3,4,1,2,3,4。当他们在第二个3的时候相遇。那这样是正确的。 那如何实现这种方法呢?代码可能更简洁。例如: public class Solution { public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { ListNode a = pHead1; ListNode b = pHead2; while(a != b){ a = (a == null) ? pHead2 : a.next; b = (b == null) ? pHead1 : b.next; } return a; } } 这样的话,代码更简短,但可能效率一样。因为每个指针都走了两个链表的长度之和减去公共部分的长度。所以时间复杂度是O(n + m),空间是O(1)。 那这两种方法哪种更好呢?两种方法都是满足条件的。但第二种方法更简洁,不需要计算长度。那为什么第一次想到的是第一种方法呢?可能因为第一种方法更直观,容易想到。 但是第二种方法是否存在什么问题?比如,当链表没有公共节点的时候,是否会陷入死循?不会。因为当两个链表没有公共节点时,两个指针都会走到对方的尾部null,这时候a和b都为null,循结束,返回null。 例如,链表A是1->2->3,链表B是4->5。a的遍历路径是1,2,3,null,4,5,null;b的遍历路径是4,5,null,1,2,3,null。当a和b都走到null的时候,循条件a != b不成立,退出循,返回a(此时是null),正确。 那这种方法是不是更优?是的。代码更简洁,不需要计算长度,也不需要考虑哪个链表更长。所以应该采用第二种方法。 所以最终的解决方案应该是第二种方法。那用户的问题是要求用Java实现,所以这样的代码是可行的。 那现在检查一下是否满足题目要求。题目中说明输入的是两个无的单链表,所以不用担心循的情况。而空间复杂度为O(1),时间O(n)都满足。 综上,应该推荐使用双指针交替遍历的方法,即第二种方法。</think>### 问题分析 我们需要找到两个无链表第一个公共节点,若不存在则返回空。要求空间复杂度为 $O(1)$,时间复杂度为 $O(n)$。以下是解决该问题的两种方法,其中第二种方法更为高效简洁。 --- ### 方法一:双指针对齐法 **核心思路** 1. **计算链表长度**:分别遍历两个链表,得到它们的长度差。 2. **对齐起点**:让较长链表的指针先移动长度差的步数,使得两个指针的起始位置对齐。 3. **同步遍历**:同时移动两个指针,找到第一个公共节点。 **代码实现** ```java public class Solution { public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { int len1 = getLength(pHead1); int len2 = getLength(pHead2); ListNode cur1 = pHead1; ListNode cur2 = pHead2; // 对齐起点 if (len1 > len2) { int diff = len1 - len2; while (diff-- > 0) cur1 = cur1.next; } else { int diff = len2 - len1; while (diff-- > 0) cur2 = cur2.next; } // 同步遍历找公共节点 while (cur1 != null && cur2 != null) { if (cur1 == cur2) return cur1; cur1 = cur1.next; cur2 = cur2.next; } return null; } private int getLength(ListNode head) { int len = 0; while (head != null) { len++; head = head.next; } return len; } } ``` --- ### 方法二:双指针交替遍历法(更优) **核心思路** 1. **双指针交替遍历**:两个指针分别从链表头出发,遍历完当前链表后跳转到另一个链表的头部继续遍历。 2. **相遇条件**:若存在公共节点,两个指针会在遍历过程中相遇;若不存在,最终会同时到达 `null`。 **数学推导** 设链表 A 的非公共部分长度为 $a$,链表 B 的非公共部分长度为 $b$,公共部分长度为 $c$。 - 指针 A 的路径为 $a + b - c + c = a + b$ - 指针 B 的路径为 $b + a - c + c = a + b$ 两指针路径长度相同,最终会在公共节点相遇。 **代码实现** ```java public class Solution { public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { ListNode a = pHead1, b = pHead2; while (a != b) { a = (a == null) ? pHead2 : a.next; // A遍历完跳转到B b = (b == null) ? pHead1 : b.next; // B遍历完跳转到A } return a; // 相遇点即为公共节点(若为null,说明无公共节点) } } ``` --- ### 方法对比 | 方法 | 时间复杂度 | 空间复杂度 | 实现复杂度 | |--------------------|------------|------------|------------| | 双指针对齐法 | $O(n)$ | $O(1)$ | 中等 | | 双指针交替遍历法 | $O(n)$ | $O(1)$ | 简单 | 推荐使用**方法二**,代码简洁且无需显式计算长度,直接通过交替遍历解决。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值