快慢指针技巧汇总
双指针技巧可以分为两类:一类是【快慢指针】、一类是【左右指针】
一、快慢指针
快慢指针一般初始化指向链表头结点head,前进时快指针fast在前,慢指针slow在后,巧妙解决链表中的一些问题。
1、判断链表中是否有环
经典解法就是用两个指针,一个每次前进两步,一个每次前进一步。如果不含有环,跑得快的那个指针最终会遇到 null,说明链表不含环;如果含有环,快指针最终会超慢指针一圈,和慢指针相遇,说明链表含有环。
141. 环形链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode *slow, *fast;
slow = head; fast = head;
while (fast != NULL && fast->next != NULL) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow) return true;
}
return false;
}
};
2、返回环的起始位置
第一次相遇时,假设慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步,也就是说比 slow 多走了 k 步(也就是环的长度)。设相遇点距环的起点的距离为 m,那么环的起点距头结点 head 的距离为 k - m,也就是说如果从 head 前进 k - m 步就能到达环起点。
巧的是,如果从相遇点继续前进 k - m 步,也恰好到达环起点。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode *slow, *fast;
bool hascycle = false; //标记链表中是否有环
slow = head; fast = head;
while (fast != NULL && fast->next != NULL) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow) {
hascycle = true;
break;
}
}
if (hascycle) {
slow = head;
while (slow != fast) {
slow = slow->next;
fast = fast->next;
}
return slow;
} else
return NULL;
}
};
3、寻找链表的中点
快指针一次前进两步,慢指针一次前进一步,当快指针到达链表尽头时,慢指针就处于链表的中间位置。
876. 链表的中间结点
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode *slow, *fast;
slow = head; fast = head;
while (fast != NULL && fast->next != NULL) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
};
4、寻找链表的倒数第 k 个元素
我们的思路还是使用快慢指针,让快指针先走 k 步,然后快慢指针开始同速前进。这样当快指针走到链表末尾 null 时,慢指针所在的位置就是倒数第 k 个链表节点.
剑指 Offer 22. 链表中倒数第k个节点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode *slow, *fast;
slow = head; fast = head;
while (k--) {
fast = fast->next;
}
while(fast != NULL) {
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
二、左右指针
左右指针在数组中实际是指两个索引值,一般初始化为 left = 0, right = nums.length - 1 。
1、二分查找
int binarySerarch(vector<int> &nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int mid = (left + right) >> 1;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
2、两数之和
如果数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节 left 和 right 可以调整 sum 的大小:
int twoSum(vector<int> &nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left < right) {
int num = nums[left] + nums[right];
if (num == target) {
return {left, right};
} else if (num < target) {
left++;
} else {
right--;
}
}
return {};
}
3、反转数组
void reverse(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
// swap(nums[left], nums[right])
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++; right--;
}
}
4、滑动窗口
也许是双指针技巧的最高境界了,如果掌握了此算法,可以解决一大类子字符串匹配的问题,不过「滑动窗口」算法比上述的这些算法稍微复杂些,将再详细记录。