双指针
一、双指针的概念
在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。
双指针法可以充分使用了数组有序的特征,从而在某些情况下能够简化一些运算,减少时间消耗。
二、两类双指针方法
- 快慢指针
- 对撞指针(左右指针)
【注】前者主要解决链表中的问题,比如典型的判定链表中是否包含环、链表中间元素等;后者主要解决数组(或者字符串) 中的问题,比如二分查找。
三、对撞指针
3.1 思想
对撞指针是指在有序数组中,将指向最左侧的索引定义为左指针(left),最右侧的定义为右指针(right),然后从两头向中间进行数组遍历。当题目不是有序数组时,可以通过sort或haep方式得到有序数组,方便进一步处理。
3.2 对撞指针的C++实现模板
//这里 以int型数组为例)(已排序)
void crash_Ptr(vector<int>& nums, param1,param2,...){
int left=0,right=nums.size()-1;
while(left<right){
//data proc: 这里一般是遍历数组,对每个元素进行处理
if( condition 1){ //条件1:左指针右移
...
left++;
}
else if( condition 2){ //条件2:右指针左移
...
right--;
}
}//通过while循环,用left和right记录所需的两个元素。
... //后处理相关问题
}
3.3 LeetCode相关例题
3.3.1 两数之和
题目描述:https://leetcode-cn.com/problems/two-sum
解题思路
1、
Q:如何使用对撞指针?
A:可以对原数组排序,定义left和right索引记录nums两个元素,这样就可以使用sum=nums[left]+nums[right]记录两数之和,所以遍历数组时就会有三种情况:
(1)sum == target:可以直接返回left和right就是和为target的数组索引,
(2)sum < target:表明两数取值较小,由于数组有序,将left索引右移,可以提高sum值向target靠近
(3)sum > target:表明两数取值较大,由于数组有序,将right索引左移,可以减小sum值向target靠近
Q:若直接对原数组进行排序,会破坏原索引值,如何解决?
A:用临时数组Temp拷贝nums,对temp进行排序和对撞处理,当得到left和right索引时,break掉,在原数组找到和temp[left]、temp[right]相等的元素就得到了原数组对应值索引。
Q:是否需要边界处理?
A:可以的,若nums.size()为空,直接返回空数组!
C++代码实现:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target){
vector<int> res;
vector<int> temp=nums; //拷贝一份原数组
int n=nums.size();
int left=0,right=nums.size()-1;
sort(temp.begin(),temp.end());//排序
while(left<right){ //移动left和right指针找到目标值
if(nums[left]+nums[right] == target){
break;
}
else if(nums[left]+nums[right]<target){
left++;
}
else{
right--;
}
}
for(int k=0;k<n;k++){
if(left<n && nums[k]==temp[left]){ //找到left在原数组对应位置
res.push_back(k);
left=n;
}
else if(right<n && nums[k]==temp[right]){ //找到right在原数组对应位置
res.push_back(k);
right=n;
}
if(left==n && right==n) //若两则相等则返回
return res;
}
return res;
}
};
这题也可以通过创建map<int,int>使用Hash的方式解决,而且更为方便,可以参考官方解题!
3.3.2 三数之和
LeetCode15
3.3.3 四数之和
LeetCode18
四、快慢指针
4.1 思想
快慢指针一般都初始化指向链表的头结点 head,前进时快指针 fast 在前,慢指针 slow 在后,巧妙解决一些链表中的问题。如判断链表是否有环、寻找链表中点等特殊问题。
4.2 快慢指针的C++实现模板
struct ListNode{
int val; //数据域
ListNode* next; //指针域
ListNode(int _x):val(_x),next(NULL){}; //初始化
};
ListNode* fast_slow_Ptr(ListNode* head){
ListNode *fast, *slow;
fast = slow = head; //初始化为头指针
while(fast != NULL && fast->next != NULL){
slow = slow->next; //slow走一步
fast = fast->next;
fast = fast->next; //fast走两步
if(fast == slow){
//链表有环状结构
.... //break,或者对该结点处理
}
}
....
}
4.3 LeetCode相关例题
4.3.1 环形链表
题目描述:https://leetcode-cn.com/problems/linked-list-cycle/
解题思路
分析一下,由于链表中含有环,那么通过 head=head->next 遍历环形数组就会陷入死循环,因为没有 NULL指针作为尾结点。
这时就可以使用双指针,一个slow指针每次走一步,一个fast指针每次走两步,那么无论环多大,都会在
C++代码实现
class Solution {
public:
bool hasCycle(ListNode *head) {
if(!head || !head->next) //边界值:链表为空
return nullptr;
ListNode *fast, *slow;
fast = slow = head; //初始化为头指针
while(fast != NULL && fast->next != NULL){
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
return true;
}
return false;
}
};
4.3.1 环形链表 II
题目描述:https://leetcode-cn.com/problems/linked-list-cycle-ii/
解题思路:和上题一样,但是这题需要返回环的第一个节点,所以可以利用一些数学知识,作图表示一下
假设:(1)绿色段为a,(2)蓝色段为b,(3)橙色段为c
由于fast路程是slow的二倍,所以得出
2
∗
(
a
+
b
)
=
a
+
b
+
c
+
b
2*(a+b)= a+b+c+b
2∗(a+b)=a+b+c+b
即:
a
=
c
a=c
a=c
所以可知,从相遇点meet和head节点同时出发,相遇点即为环的起点!
C++代码实现
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
if(!head || !head->next) //边界值:链表为空
return nullptr;
ListNode *fast, *slow;
ListNode* meet=NULL;
fast = slow = head; //初始化为头指针
while(fast != NULL && fast->next != NULL){
fast = fast->next->next;
slow = slow->next;
if(fast == slow){
meet=fast;
break;
}
}
if(meet == NULL)
return nullptr; //无环,没有相遇
while(head && meet){
if(head == meet) //如果相遇,说明到了环的起点
return head;
head=head->next;
meet=meet->next;
}
return nullptr;
}
};
这题也可以使用集合实现, h e a d = h e a d − > n e x t head=head->next head=head−>next遍历链表数组,同时使用set<ListNode*>记录所经过的元素,找到第一个 s e t . f i n d ( h e a d ) ! = s e t . e n d ( ) set.find(head) != set.end() set.find(head)!=set.end()元素,即为环的初始结点,这个方便代码更方便一点!
参考Blog:
1、算法一招鲜——双指针问题
2、双指针技巧总结(这是算法大神labuladong的个人总结,感觉受益良多,读者可以进他主页查看!)