双指针算法介绍:
所谓的双指针算法看似十分的神秘,但是实质上就是两个标志查找元素的变量。双指针既可以是我们平常最常说的指针(类似int *类型的数据),也可以是数组的下标。因为对于一个数组数据的查找,通过下标我们照样可以找到相应的数据。
而我们最经常说到的算法当中的指针并不是平常所说的指针,而是类似于数组下标之类的,方便于查找对应的数据的值。
而双指针算法就是通过两个“指针”方便我们查找对应的数据以达到对应要求的算法。
双指针的类型:
最常见的双指针算法包括两种:一种是碰撞指针的类型,另一种是快慢指针类型。
碰撞指针:
碰撞指针指的是两个指针,一个从最左边开始查找元素,另一个从最右边查找元素,最后两个指针相遇的时候就跳出循环。我们将这种从两边开始最终相遇的指针叫做碰撞指针。
快慢指针:
快慢指针指的是两个从同一位置开始的指针。但是我们这两个指针移动的速度不同。我们可以根据题目要求进行判断。例如设置一个慢指针一次移动一步,设置一个快指针一次移动两步。
通常情况下快慢指针用于带有循环节点的情况,例如:对于我们循环链表数据的查找就使用了快慢指针。但是不仅仅是循环链表,只要我们根据题目当中的要求进行分析,存在循环的情况就可以尝试使用快慢指针进行问题的解决。
题目示例:
题目一: 查找总价格为目标值的两个商品
解题思路:
思路一:暴力查找:
这道题想要解决很简单。最简单的解决的方法就是暴力查找。我们只需要将数据两两匹配,直到查找到符合要求的目标返回即可。但是我们暴力匹配的时间复杂度很高。该算法是一个O(N^2)的算法。 很多时候都无法通过,所以我们就需要想到对代码进行优化。
运行代码:
#include<iostream>
#include<vector>
using namespace std;
vector<int> twoSum(vector<int>& price, int target)
{
//暴力查找算法解决问题
vector<int> ret;
for (int i = 0; i < price.size()-1; i++)
{
for (int j = i+1; j < price.size(); j++)
{
if (price[i] + price[j] == target)
{
ret.push_back(price[i]);
ret.push_back(price[j]);
return ret;
}
}
}
return ret;
}
int main()
{
vector<int> ret={ 3, 9, 12, 15 };
int target = 18;
vector<int> tmp=twoSum(ret, target);
for (auto e : tmp)
{
cout << e << " ";
}
return 0;
}
运行结果:
我们会发现VS当中运行一切正常,但是放到力扣上面就无法通过:
显示超出时间限制,这个时候我们就应该想到优化代码。
思路二:双指针算法
我们仔细阅读题目要求会发现有一个很神奇的条件:数据当中的数据为升序,所以我们这个时候就可以使用双指针算法进行代码的优化。
我们可以使用碰撞指针,设置两个从不同端开始的指针。从最左端找到一个数据,从最右端找到一个数据。
将这两个数据相加,如果得到的结果大于想要查找的目标的值,就将右边的指针向左进行移动。如果得到的结果小于想要查找的目标的值,就将左边的指针向右进行移动。如果符合条件的值存在的话就一定可以查到。
算法原理解析:
为什么我们使用碰撞指针如果目标数据存在就一定可以查找到我们想要的数据呢?
假如存在符合条件的数据因为它是固定的,那么它对应的另一个答案也是固定的。无非就三种情况:我们找到的符合条件的数据是两个数据当中较小的那一个,我们找到的数据是两个当中较大的那一个,我们找到的数据相等。
无论是这两种的哪一种,只要我们都可以通过目标值进行推算出来另外一个值,另外一个值也无非是大于等于小于这三种情况。又因为我们的数组是有序的,假设我们先固定的是较大的值,另外一个值还没有找到,相加之和一定比目标值小,left一定需要向右进行移动。如果我们先固定的是较小的值,较大的值没有找到,相加之和就一定比目标值大。所以我们需要将right向左进行移动。
代码编写:
下面我们就来使用碰撞指针算法进行解决该问题:
#include<iostream>
#include<vector>
using namespace std;
vector<int> twoSum(vector<int>& price, int target)
{
//暴力查找算法解决问题
vector<int> ret;
int left = 0, right = price.size()-1;
while (left < right)
{
if (price[left] + price[right] < target)
{
left++;
}
else if (price[left] + price[right] > target)
{
right--;
}
else
{
ret.push_back(price[left]);
ret.push_back(price[right]);
return ret;
}
}
return ret;
}
int main()
{
vector<int> ret = { 3, 9, 12, 15 };
int target = 18;
vector<int> tmp = twoSum(ret, target);
for (auto e : tmp)
{
cout << e << " ";
}
return 0;
}
运行结果:
题目示例二:快乐数
解题思路:
思路一:暴力计算:
这道题的解题方法暴力计算其实和双指针算法的差别不大,但是使用双指针算法可能会更加省心一些。我们依次来实现这两种算法。
对于一个数据判断是不是快乐数,我们只需要重复一个操作:取个位数求平方相加即可。最后的循环结果等于1,就代表是快乐数。不为1那么就不是快乐数。其中需要我们特别注意的是不是快乐数的判断:我们需要创建一个vector对象,统计每一次产生的结果,如果vector对象当中原本存在的数据再次出现,且该值不等于1就代表我们的循环结束,该数不为快乐数。
代码示例:
#include<iostream>
#include<vector>
using namespace std;
int operatChange(int n)
{
int tmp = 0;
int sum = 0;
while (n)
{
tmp = n %10;
sum = sum + tmp * tmp;
n /= 10;
}
return sum;
}
bool in_ret(vector<int> ret, int data)
{
for (auto e : ret)
{
if (data == e)
{
return true;
}
}
return false;
}
bool isHappy(int n)
{
vector<int> ret;
n = operatChange(n);
while (!in_ret(ret,n))
{
ret.push_back(n);
n = operatChange(n);
}
if (n == 1)
{
return true;
}
else
{
return false;
}
}
int main()
{
int data = 19;
if (isHappy(data))
{
cout << data << "是快乐数" << endl;
}
else
{
cout << data << "不是快乐数" << endl;
}
return 0;
}
运行结果:
思路二:快慢指针
这道题使用双指针进行求解其实和我们正常的暴力求解差不多,并没有起到太大的优化作用。在这里使用快慢指针仅仅是为了加深我们对双指针算法的学习。下面我们使用快慢指针算法进行求解:
我们可以设置两个指针,一个快指针,一个慢指针。快指针每一次向前走两步,慢指针每一次向前走一步。如果存在循环节点那么这两个指针就一定可以相遇。所以我们就可以将相遇的数据的值作为判断条件。
代码示例:
#include<iostream>
#include<vector>
using namespace std;
int operatChange(int n)
{
int tmp = 0;
int sum = 0;
while (n)
{
tmp = n % 10;
sum = sum + tmp * tmp;
n /= 10;
}
return sum;
}
bool isHappy(int n)
{
//使用双指针算法判断是否是快乐数
int fast = -1;
int slow = 0;
fast = operatChange(n);
slow =n;
while (fast!=slow)
{
fast=operatChange(fast);
fast = operatChange(fast);
slow = operatChange(slow);
}
if (fast == 1)
{
return true;
}
else
{
return false;
}
}
int main()
{
int data = 19;
if (isHappy(data))
{
cout << data << "是快乐数" << endl;
}
else
{
cout << data << "不是快乐数" << endl;
}
return 0;
}
运行结果:
原理证明:
之前我们说到:快慢指针最适合的应用场景是存在循环节点的时候。我们只需要证明可以该题目当中无论什么情况都可以落入循环当中即可。
首先我们有两种情况,一种是是快乐数的情况,一种是不是快乐数的情况。如果是快乐数的时候我们最后的数据会变成1,之后无论进行多少次操作都是1,构成循环。另一种不是快乐数的情况:我们可以列举一个不是快乐数的示例:2->4->16->37->58->89->145->42->20->4
当不是快乐数的数据在经过操作之后我们的数据会再次得到列表当中出现过的数据,这个时候就进入了一个循环。所以我们本道题目当中无论是什么情况最终都会进入循环当中,这个时候就可以通过判断相遇的值是否为1,进而判断是否为快乐数。
题目示例三:移动零
解题思路:
这道题就是一个很简单的双指针的问题。我们可以从前向后进行查找,找到不为0的数据就将它移动到前面。我们可以设置两个指针进行控制这个操作:一个指针用于查找非零的元素,一个指针用于保存查找到非零元素需要移动到的位置。
代码示例:
#include<iostream>
#include<vector>
using namespace std;
void moveZeroes(vector<int>& nums)
{
//直接从前向后进行查找,找到非0的元素就将其向前移动最终得到想要的结果
int pos=0;
for(int i=0;i<nums.size();i++)
{
if(nums[i]!=0)
{
//不等于0就将数据向前移动
nums[pos]=nums[i];
pos++;
}
}
//当循环结束之后,将pos之后的数据全部置为0
while(pos<nums.size())
{
nums[pos]=0;
pos++;
}
return;
}
int main()
{
vector<int> ret;
ret.push_back(0);
ret.push_back(1);
ret.push_back(0);
ret.push_back(3);
ret.push_back(12);
moveZeroes(ret);
for(int i=0;i<ret.size();i++)
{
cout<<ret[i]<<" ";
}
return 0;
}
运行结果:
题目示例四:两数之和
解题思路:
这道题和我们之前写的查找价格为目标值的两件商品可以说是一模一样。但是我们题目当中没有具体说明数组数据有序,所以我们就需要先将数组进行排序之后再查找为目标值的两个数据即可。
代码示例:
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<int> twoSum(vector<int>& nums, int target)
{
//首先对数组当中的数据进行排序
vector<int> tmp=nums;
vector<int> ret;
sort(tmp.begin(),tmp.end());
int left=0;
int right=nums.size()-1;
int data1=0;
int data2=0;
while(left<right)
{
if(tmp[left]+tmp[right]<target)
{
left++;
}
else if(tmp[left]+tmp[right]>target)
{
right--;
}
else
{
data1=tmp[left];
data2=tmp[right];
break;
}
}
//在原数组当中重新查找数据元素的位置
for(int i=0;i<nums.size();i++)
{
if(nums[i]==data1||nums[i]==data2)
{
ret.push_back(i);
}
}
return ret;
}
int main()
{
vector<int> ret;
ret.push_back(2);
ret.push_back(7);
ret.push_back(11);
ret.push_back(15);
int target=9;
vector<int> out=twoSum(ret,target);
for(int i=0;i<out.size();i++)
{
cout<<out[i]<<" ";
}
return 0;
}
运行结果:
题目示例五:有效三角形的个数
解题思路:
思路一:暴力计算
当我们的问题较为复杂的时候我们就重新一步一步进行对代码进行优化,所以我们先编写一个可以运行通过的代码,方便我们对题目的理解。
当我们看到题目的时候,最开始的思路肯定是暴力匹配的算法,使用三个循环嵌套,每一次选出三个数据,判断这三个数据是否可以组成一个三角形。判断是否可以组成三角形的条件为:两边之和大于第三边,两边之差小于第三边。我们可以将这个条件转化为最短的两条边相加大于最长的边。我们可以通过思考得到该三边可以组成三角形。(证明省略)所以我们要想快速找到最短的两条边,就需要保证数组有序,位于前面的两个数据一定是较小的两个数据。所以我们可以写出如下的代码。
代码示例:
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int triangleNumber(vector<int>& nums)
{
int count=0;
sort(nums.begin(),nums.end());
for(int i=0;i<nums.size()-2;i++)
{
for(int j=i+1;j<nums.size()-1;j++)
{
for(int k=j+1;k<nums.size();k++)
{
//判断是否可以组成三角形,如果可以组成三角形就进行统计
if(nums[i]+nums[j]>nums[k])
{
count++;
}
}
}
}
return count;
}
int main()
{
vector<int> ret;
ret.push_back(4);
ret.push_back(2);
ret.push_back(3);
ret.push_back(4);
cout<<triangleNumber(ret);
return 0;
}
运行结果:
但是毫无疑问的是,我们的程序肯定会超出时间限制。毕竟我们的算法是一个O(N^3)的算法。所以我们需要对上述代码进行优化。
思路二:多指针优化
这个时候就需要使用到我们今天所学到的内容了:我们可以使用指针对我们的代码进行优化。
我们可以设计三个指针,由于之前我们将数组已经排好序了。所以我们只要固定一个数据,那么使用另外两个数据就可以将问题转化为求两个数据之和的问题。
我们将固定好的数据以及之前的数据排除,之后的数据进行判断,设置一个left指针,设置一个right指针。我们可以将原本O(N^2)的算法转换成为O(N)的算法,使用一趟排序判断出需要移动的次数。
由于我们需要求得是较小的两个数相加大于较大的两个数。所以我们只需要求满足条件的最小的第二个数据即可。因为我们的数组是有序的,所以我们前面的数据满足条件,后面指针之间的数据一定也满足条件。