双指针的基本思想: 使用两个指针, 以不同的方向或不同的速度移动, 尤其在数组类题目中常见, 它也是优化的算法的一种方案。
可为了: 对撞双指针、快慢双指针。
对撞双指针:一个指针指向头, 一个指针指向尾, 两个指针都向中间移动,相遇就停止。
快慢双指针: 一个指针走的快,在前面找符合条件的元素, 一个指针走的慢, 用于将符合条件的元素换到前面来, 通常 [0 ~ 慢指针]的所有元素都是满足条件的元素值。
双指针
对撞双指针
写leetcode的,大概都听过大名鼎鼎的 ”两数之和问题“ 吧, 这是个典型的双指针题; ”三数之和“ 、”四数之和“、这跟两数之和问题大同小异。可以参考这个博文,写的清晰易懂 https://labuladong.gitbook.io/algo/di-ling-zhang-bi-du-xi-lie/nsum。
LeetCode - 344. Reverse String(反转字符串)
用对撞指针的思想, 两个指针的值进行交换。
class Solution {
public:
void reverseString(vector<char>& s) {
for(int i = 0, j = s.size() - 1; i < j; i++,j--){
swap(s[i],s[j]);
}
}
};
LeetCode - 345. Reverse Vowels of a String(反转字符串中的元音字母)
这题不一定只有字母, 也会有别的字符在其中, 需要特判。
此外用一个散列表记录 aeiou 这五个字母, 那么判断是否为元音字母就可以在很方便的完成。
class Solution {
public:
string reverseVowels(string s) {
bool f[26];
memset(f,0,sizeof(f));
//利用散列表记录aeiou
f['a' - 'a'] = f['e' - 'a'] = f['o' - 'a'] = f['u' - 'a'] = f['i' - 'a'] = 1;
for(int i = 0, j = s.size() - 1; i < j;){
//不是元音字母就移动双指针
if(isalpha(s[i]) && isalpha(s[j]) && f[tolower(s[i]) - 'a'] && f[tolower(s[j]) - 'a'])
{swap(s[i],s[j]); i++; j--; continue;}
i++; j--;
if(isalpha(s[i - 1]) && f[tolower(s[i - 1]) - 'a']) i--;
if(isalpha(s[j + 1]) && f[tolower(s[j + 1]) - 'a']) j++;
}
return s;
}
};
LeetCode - 125. Valid Palindrome(验证回文串)
class Solution {
public:
bool isPalindrome(string s) {
if(s.size() == 0) return true;
for(int i = 0, j = s.size() - 1; i < j;){
while(!isalnum(s[i]) && i < j) i++;
while(!isalnum(s[j]) && i < j) j--;
if(i >= j) return true;
if(tolower(s[i]) == tolower(s[j])){
i++; j--;
}
else
return false;
}
return true;
}
};
LeetCode - 11. Container With Most Water(盛最多水的容器)
这题也可以用对撞指针解决, 对于数组中的任意一个元素值,以它做为容器的边,那么用盛最多的水,另外一条边应该离它尽量远并且大于等于它,那么得到的就是这条边的最大盛水。
class Solution {
public:
int maxArea(vector<int>& height) {
int maxwater = -1;
for(int i = 0, j = height.size() - 1;i < j; ){
//左边的边小于右边的边, 那么最大盛水:左右的距离 * 左边的高度
if(height[i] <= height[j]) {
maxwater = max(maxwater,(j - i) * height[i]);
i++;
}
//左边的边大于右边的边, 那么最大盛水:左右的距离 * 右边的高度
if(height[i] > height[j]){
maxwater = max(maxwater,(j - i) * height[j]);
j--;
}
}
return maxwater;
}
};
参考博客:https://zhuanlan.zhihu.com/p/107457778
快慢指针
Leetcode 141-环形链表
所谓快慢指针,就是在同一个循环里,快慢指针同时移动,但快指针永远比慢指针多走几步(通常是2倍关系)。
/**
* 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 *low = head,*quick = head;
if(quick == NULL) return false;
while(true){
quick = quick -> next;
if(quick != NULL) quick = quick -> next;
low = low->next;
if(quick == NULL) return false;
if(quick == low) return true;
}
return true;
}
};
Leetcode 879-链表的中间结点
这道题就是针对我在上一题所总结的规律的典型应用。利用快指针永远比慢指针多走2n(n通常等于1)的特点,只要快指针走到了链表尾部,那慢指针就一定到了链表的中间.
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode *low,*quick;
int qnums,lnums;
lnums = 1;
qnums = 1;
low = quick = head;
while(quick != NULL){
if(qnums / 2 + 1 > lnums) {low = low->next; lnums++;}
quick = quick -> next;
qnums++;
}
return low;
}
};
Leetcode 142- 环形链表 II
这题有个重要的结论:当快慢指针相遇时,它们距离入环节点的距离等于从头节点到入环节点的距离。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode *low = head,*quick = head;
if(quick == NULL) return NULL;
while(true){
quick = quick -> next;
if(quick != NULL) quick = quick -> next;
low = low->next;
if(quick == NULL) return NULL;
if(quick == low) break;
}
low = head;
while(low != quick){
low = low -> next;
quick = quick -> next;
}
return low;
}
};
总结:
快慢指针的题目做的不是很多,但它的规律比双指针更加明显:
同样总是以链表/数组为背景;
一定能构成一个循环(或成环),促使快指针能“追上”慢指针;
快指针通常比慢指针多走一步,形成两倍关系(视情况而定)。