链表的双指针技巧
经典环绕问题
问题:给定一个链表,判断其中有无环
链表中使用两个速度不同的指针时会遇到的情况:
- 如果没有环,快指针将停在链表的末尾。
- 如果有环,快指针最终将与慢指针相遇
两个指针的速度应该如何设置?
3. 安全的选择是每次移动慢指针一步,而移动快指针两步。每一次迭代,快速指针将额外移动一步。如果环的长度为 M,经过 M 次迭代后,快指针肯定会多绕环一周,并赶上慢指针。
4. 其他选择?有用吗? 会更高效么?
链表双指针解题模板
在这里,我们为你提供了一个模板,用于解决链表中的双指针问题。
// Initialize slow & fast pointers
ListNode* slow = head;
ListNode* fast = head;
/**
* Change this condition to fit specific problem.
* Attention: remember to avoid null-pointer error
**/
while (slow && fast && fast->next) {
slow = slow->next; // move slow pointer one step each time
fast = fast->next->next; // move fast pointer two steps each time
if (slow == fast) {
// change this condition to fit specific problem
return true;
}
}
return false; // change return value to fit specific problem
Tips
1. 在调用 next 字段之前,始终检查节点是否为空。
获取空节点的下一个节点将导致空指针错误。例如,在我们运行 fast = fast.next.next 之前,需要检查 fast 和 fast.next 不为空。
2. 仔细定义循环的结束条件。
运行几个示例,确保结束条件不会导致无限循环。在定义结束条件时,你必须考虑我们的第一点提示。
3. 复杂度分析
空间复杂度分析容易。如果只使用指针,而不使用任何其他额外的空间,那么空间复杂度将是 O(1)。但是,时间复杂度的分析比较困难,我们需要分析运行循环的次数。
练习
1. 环形链表
题目链接: 141. 环形链表.
解题思路:
- 快慢指针,原则上设置快指针速度为M,慢指针速度为N,当M-N=1且存在环时,两者必然会在环内相遇,更概括的,当相遇的时候,快指针多跑了若干个环的长度(设环长为L),只要两者的速度差满足L%(M-N) = 0或者(M-N) = k*L(k为正整数)即可。
- 逐个删除,一个链表从头节点开始一个个删除,所谓删除就是让他的next指针指向他自己。如果没有环,从头结点一个个删除,最后肯定会删完;如果是环形的,那么有两种情况,一种是o型的,一种是6型的,删到最后,肯定会出现head=head.next,详情见逐个删除思路解析
- 反转比较,关于链表的反转可以看下432,剑指 Offer-反转链表的3种方式。如果有环,那么链表反转之后,原来的头结点和反转之后的头结点一定是同一个.
- 存放集合,把节点存放到集合set中,每次存放的时候判断当前节点是否存在,如果存在,说明有环
- 哈希表,每次遍历到一个节点时,判断该节点此前是否被访问过,用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。
代码:
//快慢指针版本1,时间O(N) 额外空间O(1)
class Solution {
public:
bool hasCycle(ListNode *head) {
if(head == NULL) return false;
ListNode* fast = head;
ListNode* slow = head;
while(slow != NULL && fast != NULL){
slow = slow->next;
if(fast->next != NULL){
fast = fast->next->next;
}
else break;
if(slow == fast) return true;
}
return false;
}
};
//快慢指针版本2,时间O(N) 额外空间O(1)
class Solution {
public:
bool hasCycle(ListNode* head) {
if (head == nullptr || head->