目录
一.判断单链表是否存在环
首先我们来看一下,什么叫做单链表中存在环,即下图中所示的这种情况。
其中,我们将节点5称为连接点,即这个单链表中环开始的那个节点。
1.判断一个单链表中是否存在环
定义两个指针,即快指针fast和慢指针slow。两个指针均从链表的头结点开始向后遍历,快指针fast每次都向后移动两个节点,慢指针slow每次都向后移动一个节点。
如果,这个链表中存在环,那么在某一时刻,这两个指针一定会相遇。
如果,这个链表中不存环,那么快指针一定会率先遍历到链表尾部,变为NULL。
为什么,快指针和慢指针相遇即可证明链表中存在环?
最简单的一个例子,在一个环形跑道上,两个运动员在同一地点起跑,一个运动员速度快,一个运动员速度慢。当两人跑了一段时间,速度快的运动员必然会从速度慢的运动员身后再次追上并超过,原因很简单,因为跑道是环形的。
bool IsCycle(Node*head)
{
if (head == nullptr || head->next == nullptr) return false;
Node* fast = head;//快指针
Node* slow = head;//慢指针
while (fast!=nullptr && fast->next!=nullptr)
{
fast = fast->next->next;//快指针每次向后移动两个节点
slow = slow->next;//慢指针每次向后移的一个节点
if (fast == slow) return true;//相遇及说明有环
}
return false;
}
补充:利用哈希判断
class Solution {
public:
bool hasCycle(ListNode *head) {
unordered_set<ListNode*> seen;
while (head != nullptr) {
if (seen.count(head)) {
return true;
}
seen.insert(head);
head = head->next;
}
return false;
}
};
2.寻找环的连接点
现在我们已经可以判断单链表中是否存在环,那么我们如何找到这个环开始的位置,即连接点。
如上图所示,环开始的地方,即连接点为节点5。假如,此时快指针和慢指针在节点7相遇。
在这个问题上,有一个数学定理,即相遇点到来连接点的距离等于头结点到连接点的距离。因此,在快慢指针相遇后,我们让慢指针重新指向头结点,快指针依旧指向相遇点,二者同时开始,每次向后移动一个节点,当快慢指针再次相遇时,相遇的位置即连接点。
Node* FindLoopNode(Node* head)
{
if (head == nullptr || head->next == nullptr) return nullptr;
Node* slow = head;//慢指针
Node* fast = head;//快指针
while (fast!=nullptr && fast->next!=nullptr)//判断链表中是否有环
{
fast = fast->next->next;
slow = slow->next;
if (fast == slow) break;
}
if (fast == nullptr || fast->next == nullptr) return nullptr;//如果快指针为null,或指向的下一个节点为null,即说链表中不存在环
//至此快慢指针相遇
slow = head;//让慢指针重新指向头结点
while (slow != fast)
{
slow = slow->next;
fast = fast->next;
}
return slow;//相遇的位置即连接点
}
3.如何获取环的长度
当快指针和慢指针相遇后,让其中一个指针保持不变,另一个指针依旧向后遍历(每次向后遍历一个节点),每向后遍历一个节点,length++,当两个指针再一次相遇时,length的值就是这个环的长度。和在椭圆形的操场上跑一圈的原理一样。
int GetLoopLength_(Node* head)
{
if (head == nullptr || !IsCycle(head)) return 0;
Node* fast = head;//快指针
Node* slow = head;//慢指针
while (fast!=nullptr && fast->next!=nullptr)
{
fast = fast->next->next;
slow = slow->next;
if (fast == slow) break;//找到碰撞点
}
int length = 1;//length代表环的长度
fast = fast->next;
while (slow != fast)
{
fast = fast->next;
length++;
}
return length;
}
or
int GetLoopLength(Node* head)
{
if (head == nullptr || !IsCycle(head)) return 0;
Node* slow = FindLoopNode(head);//找到了连接点
Node* fast = slow->next;
int length = 1;
while (fast != slow)
{
fast = fast->next;
++length;
}
return length;
}
4.如何将带环的单链表变为普通的单链表
现在我们要将上面图示的带环链表变为下图所示的普通单链表
我们可以发现,在带环单链表中节点10指向连接点5,所以节点10为这个带环单链表变为普通单链表后的尾节点。
所以,我们可以从连接点开始向后遍历,当遍历到某个节点的后继节点为连接点时,即可判断这个节点为链表的尾节点,只需将它指向null即可。
void ClearLoopLinkList(Node* head)
{
if (head == nullptr || !IsCycle(head)) return;
Node* p = FindLoopNode(head);//找到连接点
Node* q = p;//q用来遍历
while (q->next != p)
{
q = q->next;
}
q->next = nullptr;
}
二.判断两个无环单链表是否相交
1.方法一 利用两个相交链表尾节点相等的属性
仔细研究上面两个链表,可以发现,如果他们相交的话,那么他们最后的一个节点一定是相同的,否则是不相交的。因此判断两个链表是否相交就很简单了,分别遍历到两个链表的尾部,然后判断他们是否相同,如果相同,则相交;否则不相交。
bool IsIntersect(Node* head1, Node* head2)
{
if (head1 == nullptr || head2 == nullptr) return false;;
Node* p1 = head1;
Node* p2 = head2;
int len_1 = 0;//链表一的长度
int len_2 = 0;//链表二的长度
int gap = 0;//len_1和len_2的差
while (p1->next != nullptr)
{
p1 = p1->next;
len_1++;
}
while (p2->next != nullptr)
{
p2 = p2->next;
len_2++;
}
if (p1 != p2) return false; //两个链表的尾指针不相同,则说明两个链表不相交
return true;
}
判断出两个链表相交后就是判断他们的交点了。假设第一个链表长度为len1,第二个问len2,然后找出长度较长的,让长度较长的链表指针向后移动|len1 - len2| (len1-len2)的绝对值),然后在开始遍历两个链表,判断节点是否相同即可。
Node* FindPoint(Node* head1, Node* head2)
{
if (head1 == nullptr || head2 == nullptr) return nullptr;
Node* p1 = head1;
Node* p2 = head2;
int len_1 = 0;//链表一的长度
int len_2 = 0;//链表二的长度
int gap = 0;//len_1和len_2的差
while (p1->next != nullptr)
{
p1 = p1->next;
len_1++;
}
while (p2->next != nullptr)
{
p2 = p2 ->next;
len_2++;
}
if (p1 != p2) return nullptr; //两个链表的尾指针不相同,则说明两个链表不相交
//至此可以判定两个链表相交
gap = abs(len_1 - len_2);
if (len_1 > len_2)
{
p1 = head1;
p2 = head2;
}
else
{
p1 = head2;
p2 = head1;
}
while (gap--)
{
p1 = p1->next;
}
while (p1 != p2)
{
p1 = p1->next;
p2 = p2->next;
}//p1和p2相遇的节点即链表一和链表二相交的地方
return p1;
}
2.方法二 利用栈
我们可以从头遍历两个链表。创建两个栈,第一个栈存储第一个链表的节点,第二个栈存储第二个链表的节点,直至链表的所有节点入栈,通过取两个栈的栈顶元素节点判断是否相等即可判断两个链表是否相交。(注意这里是判断节点,即判断两个节点的内存地址是否相同,而不是节点的值)。
如果两个栈的栈顶元素相等,则说明这两个链表相交,然后循环pop(),依次判断栈顶元素是否相等,如果某一时刻,两个栈的栈顶元素不同了,说明此时的这个元素为两个链表相交点的前继节点。
Node* temp = nullptr; //存第一个相交节点
//此时已经确定两个链表相交
while(!stack1.empty()&&!stack1.empty()) //两栈不为空
{
temp=stack1.top();
stack1.pop();
stack2.pop();
if(stack1.top()!=stack2.top())
{
break;
}
}
3.方法三 利用环
第三种思路先遍历第一个链表到他的尾部,然后将尾部的next指针指向第二个链表(尾部指针的next本来指向的是null)。这样两个链表就合成了一个链表,判断原来的两个链表是否相交也就转变成了判断新的链表是否有环的问题了:即判断单链表是否有环?
而此时寻找两个链表的交点,也就变成了寻找这个环的连接点。