快慢指针中的快慢指的是移动的步长,即每次向前移动速度的快慢。例如可以让快指针每次沿链表向前移动2,慢指针每次向前移动1次。
Leetcode 141 Linked List Cycle
Given a linked list, determine if it has a cycle in it.
Follow up: Can you solve it without using extra space?
分析与解法
大家可以想一下上体育课长跑的情景。当同学们绕着操场跑步的时候,速度快的同学会遥遥领先,最后甚至会超越其他同学一圏乃至n圈——这是绕圈跑。那么如果不是绕圈跑呢?速度快的同学则会一直领先直到终点,不会再次碰到后面速度较慢的同学。
这种思想可以用来判断单链表是否有环。如果链表存在环,就好像操场的跑道一样是一个环形一样。此时让快、慢指针都从链表头开始遍历,快指针每次向前移动两个位置,慢指针每次向前移动一个位置;如果快指针到达NULL,说明链表以NULL为结尾,没有环。如果快指针追上慢指针,则表示有环。
参考代码如下所示:
1 /** 2 * Definition for singly-linked list. 3 * struct ListNode 4 * { 5 * int val; 6 * ListNode *next; 7 * ListNode(int x) : val(x), next(NULL) {} 8 * }; 9 */ 10 class Solution 11 { 12 public: 13 bool hasCycle(ListNode *head) 14 { 15 if (!head || !head->next) return false; 16 ListNode *fast = head, *slow = head; 17 while (fast->next && fast->next->next) 18 { 19 fast = fast->next->next; 20 slow = slow->next; 21 if (fast == slow) return true; 22 } 23 return false; 24 } 25 };
Leetcode 142 Linked List Cycle II
Given a linked list, return the node where the cycle begins. If there is no cycle, return null.
Note: Do not modify the linked list.
Follow up:
Can you solve it without using extra space?
分析与解法
可以用如上方法判断链表是否存在环,如果不存在环,返回null;如果链表存在环,那么怎么寻找环的入口呢?
假设链表的长为L,起始点到环入口长度为a,环长度为r,则 L = a + r。
在快指针进入环到慢指针进入环前的这段时间,若环的长度较短,也许快指针已经走了好几圈了。然后慢指针进入环,设慢指针和快指针在环内相遇时,慢指针在环内走了X步,走的总步数为(包括环内与环外)为S步,显然 S = X + a,那么快指针走了多少步呢?
快指针在环内已经走了n圈加X步,即 nr + X 步,其中n最少为1,而走的总步数为 nr + X + a 步。
由于快指针走的总步数是慢指针的2倍,故 nr + X + a = (X + a) * 2。
由上式得 a + X = nr,即 a = nr - X = (n - 1)r + r - X。
上式的含义为环入口距离起点的距离(等于a)和相遇点距离环入口的距离(等于r - X)相差整数倍的r。
故让慢指针回到起点,快指针从相遇点开始继续走,步长都为1,则当相遇时,即为环入口。此时慢指针走了a步,而快指针也走了a步(a = (n - 1)r + r - X)。
参考代码如下所示:
1 class Solution 2 { 3 public: 4 ListNode *detectCycle(ListNode *head) 5 { 6 if (!head || !head->next) return NULL; 7 ListNode *fast = head, *slow = head, *entry = head; 8 while (fast->next && fast->next->next) 9 { 10 fast = fast->next->next; 11 slow = slow->next; 12 if (fast == slow) 13 { 14 while (slow != entry) 15 { 16 slow = slow->next; 17 entry = entry->next; 18 } 19 return entry; 20 } 21 } 22 return NULL; 23 } 24 };
Leetcode 19 Remove Nth Node from End of List
Given a linked list, remove the Nth node from the end of list and return its head.
For example, given linked list: 1 -> 2 -> 3 -> 4 -> 5, and n = 2. After removing the second node from the end, the linked list becomes 1 -> 2 -> 3 -> 5.
Note: given n will always be valid. Try to do this in one pass.
分析与解法
经典的快慢指针问题。我们可以定义两个指针,第一个指针从链表的头指针开始遍历向前走n步,第二个指针保持不动;从第n+1步开始,第二个指针也开始从链表的头指针开始遍历。由于两个指针的距离保持在n,当第一个指针到达链表的结尾点时,第二个指针正好是倒数第n个结点的前驱结点。然后可以方便地删除倒数第k个结点,参考代码如下所示:
1 class Solution { 2 public: 3 ListNode* removeNthFromEnd(ListNode* head, int n) 4 { 5 ListNode new_head_node(0); new_head_node.next = head; 6 ListNode *p = head, *q = &new_head_node, *s = NULL; 7 for (int i = 0; i < n; i++) p = p->next; 8 while (p) 9 { 10 p = p->next; 11 q = q->next; 12 } 13 s = q->next; 14 q->next = s->next; 15 delete s; 16 return new_head_node.next; 17 } 18 };
Leetcode 160 Intersection of Two Linked Lists
Write a program to find the node at which the intersection of two singly linked lists begins.
For example, the following two linked lists begin to intersect at node c1.
A: a1 → a2 c1 → c2 → c3 B: b1 → b2 → b3
Notes:
If the two linked lists have no intersection at all, return null;
The linked lists must retain their original structure after the function returns;
You may assume there are no cycles anywhere in the entire linked structure;
You code should preferably run in O(n) time and use only O(1) memory.
分析与解法
(1) 直观的想法
判断第一个链表的每个节点是否在第二个链表中。这种方法的时间复杂度为O(Length(L1) * Length(L2))。
(2) 利用计数的方法
很容易想到,如果两链表相交,那么这两个链表就会有共同的节点。而节点地址又是节点的唯一标识。所以,如果我们能够判断两个链表中是否存在地址一致的节点,就可以知道两个链表是否相交。一个简单的做法就是对第一个链表节点地址进行hash排序,建立hash表,然后针对第二个链表的每个结点的地址查询hash表,如果它在hash表中出现,那么说明第二个链表和第一个链表有共同的节点。这个方法的时间复杂度为O(Length(L1) + Length(L2))。但是它同时需要附加O(Length(L1))的存储空间,以存储哈希表。
(3) 转化为环问题
由于两个链表都没有环,我们可以把第二个链表接在第一个链表后面,如果得到的链表有环,则说明两个链表相交。否则,不相交。这样我们就把问题转化为判断一个链表是否有环。但这种方法不容易求出相交节点的位置。
(4) 快慢指针
如果两个没有环的链表相交于某一节点的话,那么在这个节点之后的所有节点都是两个链表所共有的。那么我们能否利用这个特点简化我们的解法呢?
我们知道,如果它们相交,则最后一个结点一定是共有的。而我们容易得到链表的最后一个结点,所以这成了我们简化解法的一个主要突破口。
首先两个链表各遍历一次,求出两个链表的长度,然后可以得出两个链表的长度差L。然后先在长链表上遍历L个结点,之后再同步遍历,于是在遍历中,第一个相同的结点就是第一个公共的结点。时间复杂度为O(Length(L1) + Length(L2)),空间复杂度为O(1)。
参考代码如下所示:
1 class Solution 2 { 3 ListNode *getIntersectionNode(ListNode *headA, ListNode *headB, int lenA, int lenB) 4 { 5 if (lenA < lenB) return getIntersectionNode(headB, headA, lenB, lenA); 6 int diff = lenA - lenB; 7 ListNode *a_cur = headA, *b_cur = headB; 8 for (int i = 0; i < diff; i++) a_cur = a_cur->next; 9 while (a_cur && b_cur) 10 { 11 if (a_cur == b_cur) return a_cur; 12 a_cur = a_cur->next; 13 b_cur = b_cur->next; 14 } 15 return NULL; 16 } 17 public: 18 ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) 19 { 20 ListNode *a_cur = headA, *b_cur = headB; 21 int a_len = 0, b_len = 0; 22 while (a_cur) 23 { 24 ++a_len; 25 a_cur = a_cur->next; 26 } 27 while (b_cur) 28 { 29 ++b_len; 30 b_cur = b_cur->next; 31 } 32 return getIntersectionNode(headA, headB, a_len, b_len); 33 } 34 };
思考:如果链表可能有环呢?如何求出第一个相交的结点?
如果都没有环,则和上述方法相同;
如果一个链表有环,另一个链表无环,则不相交;
如果两个链表都有环,则判断任意一个链表上快慢指针相遇的那个结点,在不在另一条链表上。如果在,则相交,如果不在,则不相交。若相交,两个链表的入口结点可能并不是环上的同一个结点。如果是同一个结点,则交点在入环之前,找相交的第一个结点又转换成了无环的情况;如果不是同一个结点,再找出两个链表环的入口点,可以定义任一一个入口点即为相交的第一个结点。