双指针法 思维方式
双指针法通常被用于 简化多层循环遍历的场景,降低时间复杂度(通常降低一个次幂)。
双指针并不是固定的公式,而是一种思维的方式~
链表 双指针法 快慢指针
真题 LeetCode141 环形链表
题目链接
解题思路:
快指针每次移动两步,慢指针每次一步。如果链表存在环,则快指针先一步进入环然后循环。一旦慢指针到了环的入口处,问题就变成了一个追击问题,此时快指针离入口处的距离就变成了的待追击的长度(假设长度为x)。每移动一次,快指针离慢指针的距离就缩小1步 (2步减1步),那么再经过x次移动后,两个指针必能相遇。
AC代码:
/**
* 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 = head;
ListNode* fast = head;
while(fast!=NULL && fast->next!=NULL){
fast = fast->next->next;
slow = slow->next;
if(fast==slow) return true;
}
return false;
}
};
真题 LeetCode142 环形链表II. 环有了,那入口节点呢?
题目链接
AC代码:
解题思路:
若快慢指针在上图的相遇点处相遇,假设此时快指针已经绕环nf圈,慢指针绕环ns圈,则有:
len_slow = a + (b+c)*ns + b
len_fast = a + (b+c)*nf + b
len_fast = 2*len_slow
结合可得:b+a = (b+c)*(nf-2*ns)
. (b+c)即为环的长度,故而b+a是环的长度的整数倍。也就是说,当slow指针从相遇处再移动a个长度时,正好处在环的起始位置。
因此,若有一个指针index
从链表头节点同时与slow指针出发,每次移动一个单位长度,则经过a次移动后,index第一次到达环的入口处,也即第一次与slow指针相遇。
其实,上面的等式还可以进一步简化:当slow节点第一次到达环的入口时,fast指针与slow指针变成了追击关系,此时两者之间的距离为[0, b+c)
. 包含0是因为二者可能在入口处就相遇了。接下来每移动一次,二者距离减一,最多不会超过环的长度(b+c)就已经追上来了,也就是说两指针第一次相遇时慢指针绕环0圈ns为0
,等式可简化为b+a = (b+c)*nf
.
当nf为1时,a与c相等,相较于环(b+c)来说a较短,如上图;当nf至少为2时,a的长度相较于环长来说很大,如下图:
AC代码:
/**
* 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* fast = head;
ListNode* slow = head;
while(fast && fast->next){
fast = fast->next->next;
slow = slow->next;
if(fast == slow){
slow = head;
while(slow!=fast){
slow = slow->next;
fast = fast->next;
}
return slow;
}
}
return NULL;
}
};
推荐阅读
数组
LeetCode 15 三数之和
题目链接
解题思路:该题目需要快速判断满足某个要求的整数或整数对存在不存在,例如已知c求满足a+b+c=0的整数对<a,c>是否存在. 首先想到哈希法,但题目要求了需要去掉重复的元组,纵使使用unordered_set,去重仍然是一个比较棘手的问题。
如何有效去重?
想一想怎么样才能使相等的元素紧紧相邻呢?
排序!
想到使用排序来简化降重之后,此题的解法就比较清晰了。无论是哈希法还是双指针遍历都可以,下面介绍双指针遍历的解法:排序+遍历*双指针/前后指针遍历,要注意两处需要去重的地方
AC代码:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());//排序
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for(int i=0; i< nums.size(); i++){//遍历
//a去重. 假设排序后的nums[0]为-1,且总共有m个-1,也即nums[0]=nums[1]=...=nums[m-1]= -1
//那么在i=0的遍历时,已经将最多m个-1存在的情况考虑完了
//当i=1时,最多只有m-1个-1, 已经在上一轮遍历中考虑过了,因此需要去重
//感觉不好理解的话,把三元组扩充成n元组,看看是不是能容易想明白
if(i>0 && nums[i] == nums[i-1]) continue;
// 错误去重方法,将会漏掉-1,-1,2 这种情况
/*
if (nums[i] == nums[i + 1]) {
continue;
}
*/
int left = i+1, right = nums.size()-1, tmp;
while(left<right){//前后指针遍历
tmp= nums[i] + nums[left] + nums[right];
if(tmp>0) right--;
else if(tmp<0) left++;
else{
res.push_back(vector<int>{nums[i], nums[left], nums[right]});
left++; right--;
//b和c降重 因为是三元组,在a相同的情况下若b或c相同,则最终满足要求的三元组一定已经重复了
while(left<right && nums[left] == nums[left-1]){left++;}
while(left<right && nums[right] == nums[right+1]){right--;}
}
}
}
return res;
}
};
LeetCode 16 最接近的三数之和
题目链接
解题思路:仍然是 排序+遍历*前后指针遍历 来避免三重循环枚举
AC代码:
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
//由提示知3 <= nums.length <= 1000
int res=nums[0]+nums[1]+nums[2];
for(int i=0;i<nums.size();i++){
if(i>0 && nums[i] == nums[i-1]) continue;
int left = i+1, right = nums.size() - 1, tmp;
while(left<right){
tmp = nums[i] + nums[left] + nums[right];
if(abs(tmp-target) < abs(res-target)) res=tmp;
if(tmp>target){
right--;
//避免重复枚举
while(left<right && nums[right] == nums[right+1]){right--;}
}
else if(tmp<target){
left++;
while(left<right && nums[left] == nums[left-1]){left++;}
}
else return target;
}
}
return res;
}
};
LeetCode 18 四数之和
四数之和
解题思路跟三数之和非常相似,多了一层嵌套循环,然后target不再固定为零.
AC代码:
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> res;
if(nums.size()<4) return res;
sort(nums.begin(), nums.end());
for(int i=0;i<nums.size();i++){
if(i>0 && nums[i] == nums[i-1]) continue; //a去重
for(int j=i+1;j<nums.size();j++){
if(j>i+1 && nums[j] == nums[j-1]) continue; //b去重
int left = j+1, right = nums.size() - 1;
while(left<right){
// nums[k] + nums[i] + nums[left] + nums[right] > target 会溢出
if(nums[i] + nums[j]>target-(nums[left] + nums[right])) right--;
else if(nums[i] + nums[j]<target-(nums[left] + nums[right])) left++;
else{
res.push_back(vector<int>{nums[i], nums[j], nums[left], nums[right]});
left++; right--;
//c和d去重
while(left<right && nums[left] == nums[left-1]){left++;}
while(left<right && nums[right] == nums[right+1]){right--;}
}
}
}
}
return res;
}
};
小总结
三数之和: 排序+遍历*双指针解法 是一层for循环num[i]将a固定,然后循环内有left和right下标作为前后指针,找到nums[i] + nums[left] + nums[right] == 0, 需要注意两处去重的地方。
四数之和:排序+遍历*遍历*双指针解法是两层for循环nums[k] + nums[i]将a+b固定,依然是循环内有left和right下标作为前后指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O( n^2 ),四数之和的时间复杂度是O( n^3 ), 需要注意三处去重的地方。
同理,五数之和、六数之和类推都采用这种解法。
对于三数之和,双指针法就是将原本暴力O( n^3 )的解法,降为O( n^2 )的解法;四数之和的双指针解法就是将原本暴力O( n^4 )的解法,降为O( n^3 )的解法。
练习题目 LeetCode 27 移除元素
题目链接
AC代码:
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
//避免多次遍历 考虑使用双指针
int slow=0, fast=0;
while(fast!=nums.size()){
if(nums[fast]==val){
fast += 1;
}
else{
nums[slow] = nums[fast];
fast += 1;
slow += 1;
}
}
return slow;
}
};