目录
2.思考:为什么慢指针走1步,快指针走两步,快指针一定会追上?
3.思考:如果快指针一次走3步,4步,5步...n步行不行?
4.思考:为什么一个指针从头指针的位置开始向后访问,一个指针从快慢指针相遇的地方开始向后访问,两个指针会在入环的第一个结点相遇?
定义:快慢指针,即慢指针一次走一步,快指针一次走两步,两个指针从链表其实位置开始运行,如果链表带环则一定会在环中相遇,否则快指针率先走到链表的末尾。
注:以下名词在接下来的书面中均以英文代替
头指针 head
快指针 fast
慢指针 slow
快慢指针相遇点 meet
2.思考:为什么慢指针走1步,快指针走两步,快指针一定会追上?
如图所示:
假设slow进环后与fast之间的结点距离为N,这个时候fast才真正开始追击slow,slow每次访问一个结点,fast每次访问两个结点,所以它们之间的距离每次都会缩减1个结点。
也就是 N N-1 N-2 N-3 ... 3 2 1 0 当两者之间的结点距离为0时,也就意味着快指针追上慢指针。
3.思考:如果快指针一次走3步,4步,5步...n步行不行?
如图所示:
设slow进环,环的大小为C,这个时候fast才真正开始追击slow,slow每次访问一个结点,fast每次访问三个结点,所以它们之间的距离每次都会缩减两个结点。
这时就会分情况讨论:
N是偶数:N N-2 N-4 N-6 ... 6 4 2 0 此时,fast追上了slow。
N是奇数: N N-2 N-4 N-6 ... 5 3 1 -1 当两者之间的结点距离为-1时,意味着快fast与slow之间的距离为 C - 1。
此时:偶数情况下,基本追的上,但是奇数情况下,也有一定程度上可能追不上。
综述,3,4...n步皆是视情况而定,不一定能追上,但是两步是一定能追上。
4.思考:为什么一个指针从头指针的位置开始向后访问,一个指针从快慢指针相遇的地方开始向后访问,两个指针会在入环的第一个结点相遇?
如图所示:
假设 head 到访问入环的第一个结点之间的距离为 L,环的大小为 C ,slow 进环所访问距离为 X,fast所访问距离为N。
那么 L = N*C - X
slow进环后,fast在一圈之内,必定追上slow。因为在追击过程中,它们之间相隔的距离每次都会缩减1,当slow访问一圈结点后,fast必定已经访问了两圈结点,所以它们之间的相对距离是一圈。
如图所示:
而 N*C - X 不就是快慢指针相遇的结点嘛。
结论:
一个指针从头指针的位置开始向后访问,一个指针从快慢指针相遇的地方开始向后访问,两个指针会在入环的第一个结点相遇。
可能会有些伙伴问这个有什么作用,这不是纯数学推导嘛,跟逻辑有什么关系,不妨我们来看看例题第四个问题。如果我们知道这个原理,可能代码复杂程度会大大降低。
1.求链表中的中间结点。oj习题
思路:定义一组快慢指针,fast为2倍的slow。两者同时从head的地址位置开始向后访问,当fast向后访问到NULL时,此时slow的位置即为中间结点。
struct ListNode* middleNode(struct ListNode* head){
struct ListNode* fase,*slow;
fase = slow = head;
while(fase && fase->next)//不清楚是奇数还是偶数
{
fase = fase->next->next;
slow = slow->next;
}
return slow;
}
2.求链表中倒数第K个结点。oj习题
思路:定义一组快慢指针,fast为2倍的slow。此时fast从head的地址位置开始向后先访问k个结点,然后再一起向后访问,当fast访问到NULL,slow就是倒数第k个结点。
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
// write code here
if(pListHead == NULL)
{
return NULL;
}
struct ListNode* fase,*slow;
fase = slow = pListHead;
while(k--)//先让fase先走k步
{
if(fase == NULL)//如果fase走到空,说明链表没有k步长
{
return NULL;
}
fase=fase->next;
}
while(fase)
{
fase = fase->next;
slow = slow->next;
}
return slow;
}
3.给定一个链表,判断链表中是否有环。oj习题
思路:定义一组快慢指针,fast为2倍的slow。两者同时从head的地址位置开始向后访问,当两者地址相等时,就证明链表中有环。反之当fast访问到NULL,两者的地址都不相等,则无环。
bool hasCycle(struct ListNode *head) {
if(head == NULL)
{
return false;
}
struct ListNode *fast, *slow;
fast = slow = head;
while(fast && fast->next)//不清楚是否带环,链表是否奇偶
{
fast = fast->next->next;
slow = slow->next;
if(slow == fast)
return true;
}
return false;
}
4.给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。oj习题
思路:定义一组快慢指针,fast为2倍的slow。
方法①. 运用4.思考 一个指针从头指针的位置开始向后访问,一个指针从快慢指针相遇的地方开始向后访问,两个指针会在入环的第一个结点相遇。
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode *fast, *slow;
fast = slow = head;
while(fast && fast->next)//不清楚是否带环,链表是否奇偶
{
fast = fast->next->next;
slow = slow->next;
if(slow == fast)
{
struct ListNode* meet = slow;//此时slow == fast == meet
while(meet != head)
{
meet = meet -> next;
head = head ->next;
}
return meet;//此时meet == head 就是入环的第一个结点
}
}
return NULL;
}
方法②.创建一个指针从meet位置的下一个结点开始向后访问,求两个链表相交的交点。(如下图所示)
这里需要考虑一个特殊情况,如果meet == head怎么办?
struct ListNode *detectCycle(struct ListNode *head)
{
struct ListNode* fast, * slow;
fast = slow = head;
while (fast && fast->next)
{
//快慢指针同时向后开始访问
fast = fast->next->next;
slow = slow->next;
//如果它们访问的地址相同,那么这个结点就是快慢指针相遇点
if (slow == fast)
{
struct ListNode* meet = slow;
if (head == meet)//如果head等于快慢指针相遇结点,就证明头指针就是入环的第一个结点
{
return head;
}
struct ListNode* curA = head, * curB = meet->next;
int lenA = 1, lenB = 1; //求长度,从1开始,不然尾结点会漏掉
while (curA->next != meet)
{
curA = curA->next;
lenA++;
}
while (curB->next != meet)
{
curB = curB->next;
lenB++;
}
//此时curA,curB都是尾结点
//求第一个交点
//假设A短B长
struct ListNode* shortList = head, * longList = meet->next;
//修正 如果A长B短,则交换彼此变量
if (lenA > lenB)
{
shortList = meet->next;
longList = head;
}
//差距
int gap = abs(lenA - lenB);//abs求绝对值
while (gap--) //长的先走差距步
{
longList = longList->next;
}
//同时走
while (shortList != longList)
{
shortList = shortList->next;
longList = longList->next;
}
//到这里肯定相等,返回 shortList / longList 都可以。
return shortList;
}
}
return NULL;//如果fast访问到NULL即证明无环
}