判断单链表是否带环?若带环,求环的长度?求环的入口点

本文详细介绍了链表中环的检测方法,包括如何判断链表是否包含环、求环的长度以及找到环的入口点。使用快慢指针技巧解决链表环问题,提供了具体的算法实现和测试代码。

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

问题一:如何判断链表是否带环?

思路:利用快慢指针,定义两个指针,同时从链表的头节点出发,一个指针一次走一步,另一个指针一次走两步。如果走的快的指针追上了走的慢的指针,那么链表就包含环;如果走的快的指针走到了链表的末尾都没有追上另一个指针,那么链表就不包含环。

 //判断单链表是否带环?
ListNode* ListIsCircle(ListNode* first)
{
	if (first == NULL) {
		return NULL;
	}
	ListNode* fast = first;
	ListNode* slow = first;
	while (fast != NULL && fast->next != NULL) {
		fast = fast->next->next;
		slow = slow->next;
		if (fast == slow) {
			//相遇,有环
			return fast;
		}
	}
	//fast走到结尾,证明没有环
	return NULL;
}

问题二:若带环,求环的长度

思路:如果有环,通过上面的方法可以得到快慢指针的相遇点,记作pos,  从pos出发,每走一步,计数器加一,再次回到该点,计数器的值就是环的长度

int ListLength(ListNode* first)
{
	if (first == NULL) {
		return 0;
	}
	ListNode* cur = ListIsCircle(first);
	ListNode* fast = cur;
	ListNode* slow = cur;
	int count = 0;
	while (1) {
		slow = slow->next;
		fast = fast->next->next;
		count++;
		if (slow == fast)
			break;
	}
	return count;
}

问题三:求环的入口点

思路:有两种方法 ,方法一:利用一个结论:从链表头结点到入口节点的长度 = 相遇节点到入口节点的长度

 结论证明 :设环的长度为:C 
从链表头结点到入口节点的长度为:L 
入口节点到相遇节点的长度为:X 
快指针在环中走的圈数:n 
快指针走的步数是:L+nC+X 
慢指针走的步数是:L+X 
根据快指针走的步数是慢指针的两倍可得关系:L+nC+X = 2(L+X)     化简后可得:nC = L+X 
 

ListNode* FindLoopPort(ListNode* first) {
	if (first == NULL) {
		return NULL;
	}
	ListNode* cur = ListIsCircle(first);
	assert(cur);
	ListNode* tmp = first;
	while (cur != tmp) {
		cur = cur->next;
		tmp = tmp->next;
	}
	return cur;
}

方法二:只写了思路,还是利用快慢指针,先定义两个指针P1和P2指向链表的头节点。如果链表中的环有n个节点,则指针P1先在链表上向前移动n步,然后两个指针以相同的速度向前移动,当第二个指针指向环的入口节点时,第一个指针已经围绕着环走了一圈,又回到了入口节点。

//测试
void TestCircle()
{

	ListNode* first = NULL;
	ListPushBack(&first, 1);
	ListPushBack(&first, 2);
	ListPushBack(&first, 3);
	ListPushBack(&first, 4);
	ListPushBack(&first, 5);
	ListPushBack(&first, 6);
	ListPushBack(&first, 7);
	ListNode* cur = first;
	while (cur->next != NULL) {
		cur = cur->next;
	}
	ListNode* tmp = FindTailK(first, 4);
	cur->next = tmp;
	ListNode* res = ListIsCircle(first);
	printf("%d ", res->data);
	int a = ListLength(first);
	printf("%d ", a);
	ListNode* node = FindLoopPort(first);
	printf("%d ", node->data);
}

<think>好的,用户让我用Golang写一个方法来判断单链表是否的起不一定是第一个节点,还要知道有多少种实现方式。首先,我得先理解这个问题。单链表意味着链表的某个节点的next指针指向了之前的一个节点,形成一个判断是否存在,常见的解法有哪些呢? 首先想到的是快慢指针法。这是比较经典的方法,用两个指针,一个每次走两步,另一个走一步。如果存在,快指针最终会追上慢指针。这种方法的时间复杂度是O(n),空间复杂度O(1),应该可行。那具体怎么实现呢?在Golang中,需要定义链表的结构,然后让两个指针移动,直到相遇或者快指针走到nil。 然后,可能还有其他方法。比如使用哈希表,遍历每个节点,记录访问过的节点,每次检查当前节点是否已经存在哈希表中。如果存在,说明有。这种方法的时间复杂度也是O(n),但空间复杂度是O(n),因为需要存储所有节点的地址。这在Golang中可以用map来实现,节点的指针作为key。 有没有第三种方法呢?比如破坏链表的方法,比如反转链表,但这种方法可能会修改原链表的结构,可能不适用,特别是如果用户不希望修改原链表的话。所以可能不算一种好的实现方式,或者需要额外处理,可能不算主流的解法。所以可能主要就是这两种方法? 或者,还可以用标记法,比如遍历时给每个节点打标记,比如用一个布尔字段,但需要修改节点结构,可能不符合要。或者,用并发的方式,比如在遍历时记录时间,如果某个时间超过阈值,可能存在,但这种方法不可靠,可能不算正式的方法。 所以主要的实现方式应该是两种:快慢指针法和哈希表法。用户问有多少种实现方式,可能答案就是这两种。不过我得确认是否还有其他方法。比如,如果允许使用递归,但递归可能会导致栈溢出,特别是链表很长的情况下,可能不实用。或者利用一些数学方法,比如计算步数差异,但可能和快慢指针法类似。 所以,总结下来,主要的实现方式有两种:哈希表记录访问节点,快慢指针追赶。那用户的问题需要我写出这两种方法的Golang代码,并说明有多少种实现方式。 接下来,我需要确保代码的正确性。对于哈希表法,在Golang中,可以使用map[*ListNode]bool来存储节点指针,每次遍历时检查是否存在。如果遇到nil,说明链表结束,无。如果某个节点已经在map中,则有。 对于快慢指针法,初始化slow和fast都指向head。然后循,slow每次移动一步,fast移动两步。如果fast或fast.Next为nil,说明无。如果slow == fast,说明有。同时,如果要找到的起,可以在相遇后,将其中一个指针移到head,然后同速移动,再次相遇就是入口。但题目只要判断是否,所以可能不需要找起,但用户的问题可能需要判断是否存在即可。 那代码部分需要注意边界情况,比如空链表,或者只有一个节点的情况。比如在哈希表法中,如果头节点是nil,直接返回false。在快慢指针中,如果head是nil,同样返回false。测试用例需要考虑这些情况。 现在,编写这两种方法的Golang代码。结构体定义应该是: type ListNode struct { Val int Next *ListNode } 然后方法作为函数,接收*ListNode参数,返回bool。 哈希表法的代码大致是: func hasCycleHash(head *ListNode) bool { visited := make(map[*ListNode]bool) current := head for current != nil { if visited[current] { return true } visited[current] = true current = current.Next } return false } 快慢指针法的代码: func hasCycleTwoPointers(head *ListNode) bool { if head == nil || head.Next == nil { return false } slow, fast := head, head.Next for fast != nil && fast.Next != nil { if slow == fast { return true } slow = slow.Next fast = fast.Next.Next } return false } 或者另一种写法,初始slow和fast都指向head: func hasCycleTwoPointers(head *ListNode) bool { slow, fast := head, head for fast != nil && fast.Next != nil { slow = slow.Next fast = fast.Next.Next if slow == fast { return true } } return false } 这两种写法是否都正确?比如,初始时slow和fast都从head开始,那么在循中先移动指针再比较,还是先比较再移动?比如,如果是初始都指向head,那么在第一次循时,slow移动一次,fast移动两次。如果在第一个节点,比如头节点指向自己,那么此时slow和fast会在第一次循后相遇吗? 比如,链表是1 -> 1(自)。初始slow和fast都是1。进入循,因为fast != nil且fast.Next != nil。然后slow移动到1.Next(还是1),fast移动到1.Next.Next(也是1)。然后比较slow和fast,相等,返回true。正确。 但是如果链表是头节点为nil的情况,函数一开始就会返回false。是的。那这个写法是正确的。 所以两种写法都可以。可能后一种写法更简洁,初始都指向head,然后在循中先移动再比较。这样代码更简洁。 那用户的问题需要展示这两种方法。因此,在回答中,应该说明有两种实现方式,分别是哈希表法和快慢指针法,并给出代码示例。 另外,可能还有其他方法吗?比如,使用递归和哈希表结合,但本质上还是哈希表的思路。或者使用系统栈的地址记录,但可能不太实际。所以用户的问题的答案应该是两种主要的实现方式。 因此,最终的结论是有两种常见实现方式,分别是哈希表记录法和快慢指针法。需要写出这两种的Golang代码,并解释其原理。</think>判断单链表是否的常见实现方式主要有两种,以下是详细说明和代码实现: --- ### **方法一:哈希表记录法** **核心思想**:遍历链表时记录访问过的节点,若遇到重复节点则存在 **时间复杂度**:$O(n)$ **空间复杂度**:$O(n)$ ```go type ListNode struct { Val int Next *ListNode } func HasCycleHash(head *ListNode) bool { visited := make(map[*ListNode]bool) current := head for current != nil { if visited[current] { return true } visited[current] = true current = current.Next } return false } ``` --- ### **方法二:快慢指针法(Floyd判圈算法)** **核心思想**:快指针每次走两步,慢指针每次走一步。若存在,两者最终会相遇 **时间复杂度**:$O(n)$ **空间复杂度**:$O(1)$ ```go func HasCycleTwoPointers(head *ListNode) bool { if head == nil { return false } slow, fast := head, head for fast != nil && fast.Next != nil { slow = slow.Next fast = fast.Next.Next if slow == fast { return true } } return false } ``` --- ### **其他方法** 1. **反转链表法**:通过破坏性反转链表判断是否,但会修改原链表结构,不推荐 2. **标记法**:修改链表节点结构添加访问标记,需侵入式设计 3. **长度统计法**:统计链表长度超过理论最大长度时判定有,不适用于动态场景 --- ### **总结** **常用实现方式有2种**:哈希表法和快慢指针法。推荐使用**快慢指针法**,因空间复杂度更低且效率更高。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值