23 链表中环的入口节点(第3章 高质量的代码-代码的鲁棒性)
题目描述:
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
测试用例:
功能测试(环入口在头节点、中间节点、尾节点)
特殊输入测试(链表为空、链表没有环)
解题思路:
1)··· 确定链表中是否存在环:定义两个指针,同时从链表头节点出发,一个指针一次走一步,一个指针一次走两步。如果走的快的指针追上了走的慢的指针,说明链表中包含环;如果走的快的指针走到了链表的末尾(next指向Null)都没有追上第一个指针,那么链表就不包含环。
··· 确定环中的节点个数:从相遇的节点出发,一边继续向前一边计数,再次回到这个节点时,则遍历完一遍环中的节点。
··· 巡展环中的入口节点:使用两个指针,一个指针从头节点先向前移动环中节点的个数(该指针到达环中的入口节点还差 环前面的非环节点数),设置另一个指针,初始化在头节点(该节点据环还差 环前面的非环节点数),因此让两个指针同时向前以相同速度向前移动。两者会在入口节点相遇。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};
*/
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
ListNode* pMeetingNode = MeetingNode( pHead);
if(pMeetingNode == nullptr) //没有相遇,说明没有环
return nullptr;
//计算环中的节点个数:
int numCircle = 1; //也可以定义为名字:nodesInLoop
ListNode* searchNode = pMeetingNode->next;
while(searchNode != pMeetingNode){
++numCircle;
searchNode = searchNode->next;
}
//先将一个节点挪动numCircle次
ListNode* moveToTail = pHead;
for(int i=0; i<numCircle; ++i){
moveToTail = moveToTail->next;
}
//两个节点同时移动,相遇处,即环节点的入口
searchNode = pHead;
while(searchNode != moveToTail){
searchNode = searchNode->next;
moveToTail = moveToTail->next;
}
return searchNode;
}
//该函数需要重点理解,极容易忘记判断。
ListNode* MeetingNode(ListNode* pHead){
if(pHead == nullptr)
return nullptr;
ListNode* OneStep = pHead->next; //每次走一步
if(OneStep==nullptr) //只有一个节点且没有环
return nullptr;
ListNode* TwoStep = OneStep->next; //每次走两步
//是否可以追上,是有环存在,否没有环存在
while(OneStep!= nullptr && TwoStep!= nullptr){
if(OneStep==TwoStep)
return TwoStep;
OneStep = OneStep->next; //走一步
TwoStep = TwoStep->next; //走一步,还应该在走一步,但一定要判断是否可以继续走
if(TwoStep!= nullptr) //等于:不会进入下一个循环
TwoStep = TwoStep->next;
}
return nullptr;
}
};
2)同方法2思路一直,但是不需要重新寻找据头节点的第n个节点
推导过程:
class Solution { public: ListNode* EntryNodeOfLoop(ListNode* pHead) { ListNode* pMeetingNode = MeetingNode( pHead); if(pMeetingNode == nullptr) //没有相遇,说明没有环 return nullptr; //计算环中的节点个数: int numCircle = 1; //也可以定义为名字:nodesInLoop ListNode* searchNode = pMeetingNode->next; while(searchNode != pMeetingNode){ ++numCircle; searchNode = searchNode->next; } //先将一个节点挪动numCircle次,不用挪动,因为pMeetingNode就在要挪动的位置出!!! /*ListNode* moveToTail = pHead; for(int i=0; i<numCircle; ++i){ moveToTail = moveToTail->next; }*/ //两个节点同时移动,相遇处,即环节点的入口 searchNode = pHead; while(searchNode != pMeetingNode){ searchNode = searchNode->next; pMeetingNode = pMeetingNode->next; } return searchNode; } //该函数需要重点理解,极容易忘记判断。 ListNode* MeetingNode(ListNode* pHead){ if(pHead == nullptr) return nullptr; ListNode* OneStep = pHead->next; //每次走一步 if(OneStep==nullptr) //只有一个节点且没有环 return nullptr; ListNode* TwoStep = OneStep->next; //每次走两步 //是否可以追上,是有环存在,否没有环存在 while(OneStep!= nullptr && TwoStep!= nullptr){ if(OneStep==TwoStep) return TwoStep; OneStep = OneStep->next; //走一步 TwoStep = TwoStep->next; //走一步,还应该在走一步,但一定要判断是否可以继续走 if(TwoStep!= nullptr) //等于:不会进入下一个循环 TwoStep = TwoStep->next; } return nullptr; } };
3)断链法:思考这种思想,但不建议使用,除非允许修改原来的链表
//实现1
//断链法 class Solution { public: ListNode* EntryNodeOfLoop(ListNode* pHead) { if(pHead==nullptr || pHead->next==nullptr) return nullptr; ListNode* slow = pHead; ListNode* fast = pHead->next; while(fast!=nullptr){ slow->next=nullptr; //断开 slow = fast; fast = fast->next; } if(slow == nullptr && fast == nullptr) //没有环的时候 return nullptr; return slow; } };
//实现2
//断链法 class Solution { public: ListNode* EntryNodeOfLoop(ListNode* pHead) { if (pHead == nullptr || pHead->next == nullptr) { return nullptr; } ListNode* fast = pHead->next; ListNode* slow = pHead; ListNode* seg = new ListNode(1); while (fast != nullptr) { slow->next = seg; slow = fast; if (fast->next == seg) { return fast; } fast = fast->next; } if (slow->next == seg) { // 作用是什么?不添加也可以通过编程 return slow; } return nullptr; } };
4)使用STL中的set,set有一个特性就是不能插入相同元素,这样只需遍历原List一次就可以判断出有没有环,还有环的入口地址。
s.insert(node).second这里在插入的同时也判断了插入是否成功,如果不成功表明set中已经有该元素了,该元素就是环的入口元素。
class Solution { public: ListNode* EntryNodeOfLoop(ListNode* pHead) { if(pHead==nullptr ||pHead==nullptr) return nullptr; set<ListNode*> nodesSet; ListNode* pNode = pHead; while(pNode!=nullptr){ if(nodesSet.insert(pNode).second) //插入节点:没有重复节点 pNode = pNode->next; else return pNode; } return nullptr; } };
s.insert(node).second用法说明:set的带有一个键参数的insert版本函数返回pair类型对象,该对象包含一个迭代器(第一个参数,first调用)和一个bool值(第二个参数,second调用),迭代器指向拥有该键的元素,而bool值表明是否添加了元素。
5)用map对访问过的节点做标记,这样如果访问一个节点两次,表明找到了环入口,否则没有环。 时间复杂度:O(n)
class Solution { public: ListNode* EntryNodeOfLoop(ListNode* pHead) { if(pHead==nullptr ||pHead==nullptr) return nullptr; map <ListNode*,int> mlist; //用map关联各个链表指针和对应的映射值 ListNode* pNode = pHead; while(pNode!=nullptr && mlist[pNode]!=1){ mlist[pNode]=1; pNode = pNode->next; } if(pNode==nullptr) return nullptr; return pNode; } };