什么是双指针法
双指针法,指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向或者相反方向的指针进行扫描,从而达到相应的目的。 这里的指针,并非专指c中指针的概念,而是指索引,游标或指针,可迭代对象。
左右双指针
分别指向两头,向中间逼近。通常应用在一个有序数组中。
常见用法
二分查找
二分查找算法其实就是左右双指针的应用之一,详见二分查找。
两数之和
在一个有序数组中,寻找两数的和等于目标值。通过左右指针调整和的大小。
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int left = 0;
int right = numbers.size() - 1;
while(left < right){
int sum = numbers[left] + numbers[right];
if(target > sum)
left++;
else if(target < sum)
right--;
else
return {left + 1, right + 1};
}
return {-1, -1};
}
};
反转数组
class Solution {
public:
void reverseString(vector<char>& s) {
int left = 0;
int right = s.size() - 1;
while(left < right){
char temp = s[left];
s[left] = s[right];
s[right] = temp;
left++;
right--;
}
}
};
因为在数组轮转的过程中,右边的数会转到左边来,轮转一遍时又变成了原来的数组。所以我们先将数组翻转,再将0~k的部分翻转,最后再将后半部分翻转,就得到的我们想要的结果!
class Solution {
public:
void rotate(vector<int>& nums, int k) {
k = k % nums.size();
reverse(nums.begin(), nums.end());
reverse(nums.begin(), nums.begin() + k);
reverse(nums.begin() + k, nums.end());
}
};
双指针在这道题的应用主要是反转数组的部分,在做题时我们可以直接调用reverse()函数,但是也要知道它的原理。
void reverse(vector<int>& nums,int begin,int end){
int temp;
while(begin<end){ //两个指针begin和end
temp = nums[begin];
nums[begin] = nums[end];
nums[end] = temp;
begin++;
end--;
}
}
滑动窗口
滑动窗口是更复杂的左右双指针问题,主要用来解决字符串匹配,详见滑动窗口。
例题
这题的普通解法是先对数组里的数进行平方然后再进行排序。因为原数组是有序的,所以进行平方后较大的数在两端而不是中间,双指针的解法是用两个指针分别指向数组的第一个数和最后一个数,再创建一个数组,用指针指向最后,比较上面两个指针所指数平方的大小,大的放到下面的数组,然后这个指针移动,直到两个指针相等。
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
vector<int> result(nums.size(), 0);
int k = result.size() - 1;
for(int i = 0, j = nums.size() - 1; i <= j;){ //两个指针i和j
if(nums[i] * nums[i] <= nums[j] * nums[j]){
result[k--] = nums[j] * nums[j];
j--;
}else{
result[k--] = nums[i] * nums[i];
i++;
}
}
return result;
}
};
快慢双指针
通常从头开始,只不过一个指针在前,另一个指针在后。通常应用在链表中。
常见用法
判断链表是否有环
在一个环状链表中,只用一个指针无法判断。我们可以用两个指针,快指针每次走两步,慢指针每次走一步,当他们相遇时,证明链表有环。就像是两个人在环形跑道上跑步,虽然同时同地出发,但是跑的快的可以追上跑的慢的,因为这时候已经超过一圈了。
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode *slow, *fast;
slow = fast = head;
while(fast != NULL && fast->next != NULL){
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
return true;
}
return false;
}
};
判断链表有环,找环的起点
首次相遇时慢指针走的距离为D+S1,快指针走的距离为D+S1+n(S1+S2),(由于两个指针并不是“同步”走的,所以首次相遇不一定只超过一圈)因为快指针每次走两步,慢指针每次走一步,所以走的距离也是它的2倍,2(D+S1)=D+S1+n(S1+S2),即D=(n-1)(S1+S2)+S2。可以看出让慢指针回到头节点,慢指针和快指针每次都走一步,当它们再次相遇时,正好是入环点。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode *slow, *fast;
slow = fast = head;
while(fast != NULL && fast->next != NULL){
fast = fast->next->next;
slow = slow->next;
if(slow == fast)
break;
}
if (fast == NULL || fast->next == NULL) {
return NULL;
}
slow = head;
while(slow != fast){
fast = fast->next;
slow = slow->next;
}
return fast;
}
};
寻找链表中点
由于快指针每次走两步,慢指针每次走一步,所以快指针到达终点时,慢指针在链表中点。寻找链表中点的一个重要作用是对链表进行归并排序,详见归并排序。
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode *slow, *fast;
slow = fast = head;
while(fast != NULL && fast->next != NULL){
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
};
寻找链表倒数第n个元素
由于倒数第n个元素与链表末尾相差n,所以快指针先走n步,然后两个指针一起走,当快指针到末尾时,慢指针就是我们要的。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *slow, *fast;
slow = fast = head;
while(n--)
fast = fast->next;
if(fast == NULL)
return head->next;
while(fast != NULL && fast->next != NULL){
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;
return head;
}
};
例题
这题虽然是在数组中,但是也用到了快慢双指针,所以要注意灵活使用。
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int slow = 0;
int fast = 0;
while(fast < nums.size()){ //将数组中的0去掉
if(nums[fast] != 0){
nums[slow] = nums[fast];
slow++;
}
fast++;
}
for(; slow < nums.size(); slow++) //在后面填充0
nums[slow] = 0;
}
};