双指针
先说说双指针用来干嘛?
- 就是类似于快排的代码:i,j将一个数组分成三份已经处理好的,未处理好的,已经处理好的三份[{0,…,i},{i+1,…,j-1},{j,l-1}]
- 同样,我们可以分成两份,已经符合条件的,i(作为标签,说明i-1以前的都是处理好的),i到j的都是判定不符合条件的,需要被处理的,这个需要j遇到再处理[{0,…,i},{j,l-1}]
- 就是通过双指针来降低时间复杂度。
- 比如接雨水问题:通过贪心,然后用双指针来解决。
- 双指针查询问题:一般查询暴力的话都至少需要O(ne2),通过排序或者通过其他方式可以很好的处理查询的问题。
- 当然还有大名鼎鼎的滑动窗口,这个时候也需要i,j来控制滑动窗口
上面是做的简单的总结:下面看具体的代码
1. 双指针模仿栈
题目:27移除元素
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2,_,_]
我们可以知道,这题也是将数组分成两部分,一部分是不为3的,一部分是属于3而且又是操作原数组。
这个时候我们可以直接改写快排,非常方便,直接让flag=val即可,但是这个过程需要递归操作。
我们可以想一下,能不能不递归就在原来数组上修改,其实也就是一个sort的过程,分成{1,23,…,|val,val}
int removeElement(vector<int>& nums, int val) {
int i=0,j=0;
while(j<nums.size())
{
if(nums[j]!=val)
{
swap(nums[i],nums[j]);
i++;
}
j++;
}
return i;
}
int main()
{
vector<int>v={3,2,2,3};
int i=removeElement(v,3);
cout<<"sort end:"<<endl;
for(auto N:v)
{
cout<<N<<"_";
}
cout<<"end:"<<endl;
return 0;
}
2. 26删除有序数组中重复的数字
#include<iostream>
#include<vector>
using namespace std;
int removeDuplicates(vector<int>& nums) {
int i=0,j=0;
while(j<nums.size())
{
if(j>0&&nums[j]!=nums[j-1])
{
i++; //1. nums[0]是不处理的
nums[i]=nums[j];//2. 直接覆盖,然后可以保证0,0,1,1,1不会因为交换函数出现0,1,0,1,1的问题
}
j++;
}
return i;
}
int main()
{
vector<int>v={0,0,1,1,1,2,2,3,3,4};
int i=removeDuplicates(v);
cout<<"sort end:"<<endl;
for(auto N:v)
{
cout<<N<<"_";
}
cout<<"end:"<<endl;
return 0;
}
双指针快排,递归
int partition(vector<int>&nums,int l,int r){
int i=l,j=l;
int t=l+rand()%(r-l+1);
swap(nums[r],nums[t]);
int pivot=nums[r];
while(j<r){
if(nums[j]<=pivot)
{
swap(nums[i],nums[j]);
i++; //第一个元素也是需要进行排序的,所以i++放到后面
}
j++;
}
swap(nums[i],nums[r]);
return i;
}
void quickSort(vector<int>&nums,int l,int r)
{
if(l>=r)return;
int pivot=partition(nums,l,r);
quickSort(nums,l,pivot-1);
quickSort(nums,pivot+1,r);
}
int main()
{
srand(time(0));
vector<int>v={10,9,12,3,6,78,69,15,22,19};
int l=v.size();
quickSort(v,0,l-1);
cout<<"sort end:"<<endl;
for(auto N:v)
{
cout<<N<<"_";
}
cout<<"end:"<<endl;
return 0;
}
简单对比一下快排和这个模拟栈:
swap的区别
快排
while(j<r){
if(nums[j]<=pivot)
{
swap(nums[i],nums[j]);
i++; //第一个元素也是需要进行排序的,所以i++放到后面
}
j++;
}
模拟栈
while(j<nums.size())
{
if(nums[j]!=val)
{
swap(nums[i],nums[j]);
i++; //第一个元素也是需要进行考虑的,所以i++放到后面
}
j++;
}
删除重复元素
while(j<nums.size())
{
if(j>0&&nums[j]!=nums[j-1])
{
i++; //1. nums[0]是不处理的
nums[i]=nums[j];//2. 直接覆盖,然后可以保证0,0,1,1,1不会因为交换函数出现0,1,0,1,1的问题
}
j++;
}
思考总结1
我们是不是可以从上面发现规律,或者代码的框架了
当我们需要将数组分割成两个不同规律的区域时,我们可以进行如下处理:
int i=l,j=l;//左边界的开始,都是0,也可以将i赋值为-1,看个人习惯
while(j<nums.size())//下标不能超过数组最大值
{
if(**具体的处理逻辑**)//2.当nums[j]满足条件的就将下标为i值赋值或者交换为j的值
{
1.i=0是不是要处理,如果不处理,则先让i++;
i++;
nums[i]=nums[j];
2.快排时,j=0也要判断是不是把他放到{0,i}这个区间内
{
swap(nums[i],nums[j]);
i++; //第一个元素也是需要进行考虑的,所以i++放到后面
}
}
j++;
}
容积问题
11盛最多睡的容器
- 问题描述
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
- 思路
- 我们开始的思路可以这么想,要求盛最多的容器,就是容器内每一个相邻数组比如[i,i+1]的值最大,我们可以用贪心算法来解决。同时需要考虑到容器内的个数。
- 而容器内相邻数组最大的话,我们可以找出左右两边heigh[x],height[y]两个分别最大,以第三个{8,,6}能够小格子能够盛最多水,为左边为8,右边为8的时候,这个时候最大为8,但这个只是局部最优解,要求全局最优解,需要考虑容器内的个数所以,我们可以推导出全局最优解globl_max=min(小格子左边线的最长长度,小格子右边线的最长长度)*容器内的个数(x-y)。
- 我们所以这个时候我们直接暴力求解,左边的将0-1小格子考虑进去的话,从左边寻找最大的线长度i=0(本身),右边最大线长度从j=height.size()-1。
暴力解
#include<iostream>
#include<vector>
using namespace std;
//1.暴力解
int maxArea(vector<int>& height) {
int l=height.size();
int res=0;
vector<int>lv(l),rv(l);
int lm=0;
int rm=0;
for(int i=0;i<l;++i)
{
lm=max(lm,height[i]);
lv.push_back(lm);
}
for(int i=l-1;i>=0;--i)
{
rm=max(rm,height[i]);
rv.push_back(lm);
}
// for(int i=0;i<l;++i)
// {
// int tmp=0;
// lm=lv[i];
// rm=rv[i];
// tmp=min(lm,rm)*
// res=max(res,tmp);
// }
int left=0,,right=l-1;
while(left<right)
{
int tmp=min(lv[left],rv[right])*(right-left);
if(lv[left]<=rv[right])
{
left++;
}else{
right++;
}
res=max(res,tmp);
}
return res;
}
int main()
{
vector<int>v={1,8,6,2,5,4,8,3,7};
int res=maxArea(v);
cout<<"sort "<<endl;
cout<<res<<endl;
cout<<"end:"<<endl;
return 0;
}
时间复杂度为O(1),空间复杂度为O(1)
用空间换时间的思路
#include<iostream>
#include<vector>
using namespace std;
//备忘录方式
int maxArea(vector<int>& height) {
int l=height.size();
int res=0;
vector<int>lv(l),rv(l);//注意这里如果用l进行初始,下面的lv[i]=lm,rv[i]=rm;这个时候用的是双指针,一个增一个减
int lm=0;
int rm=0;
for(int i=0;i<l;++i)
{
lm=max(lm,height[i]);
lv[i]=lm;
}
for(int i=l-1;i>=0;--i)
{
rm=max(rm,height[i]);
rv[i]=rm;
}
int left=0,right=l-1;//因为是lm[1,..,left],rm[right,...,l-1]
while(left<right)
{
int tmp=min(lv[left],rv[right])*(right-left);
if(lv[left]<=rv[right])
{
left++;
}else{
right--;
}
res=max(res,tmp);
}
return res;
}
int main()
{
vector<int>v={1,8,6,2,5,4,8,3,7};
int res=maxArea(v);
cout<<"sort "<<endl;
cout<<res<<endl;
cout<<"end:"<<endl;
return 0;
}
代码稍微修改一下:
int maxArea(vector<int>& height) {
int l=height.size();
int res=0;
vector<int>lv,rv;
int lm=0;
int rm=0;
for(int i=0;i<l;++i)
{
lm=max(lm,height[i]);
lv.push_back(lm);
}
for(int i=l-1;i>=0;--i)
{
rm=max(rm,height[i]);
rv.push_back(rm);
}
for(int i=0;i<l;i++)
{
tmp=min(lv[i],rv[i])*
}
return res;
}
如果用lv[1]表示1左边最高,rv[i]为右边最高的话,这个时候无法获取i到两个最高边界的距离,所以如果想用的话,必须用一个vector<pair<int,int>>来表示,一个是边界长度,一个是边界距离
所以双指针这个时候的优势出来了。
双指针不用备忘录
#include<iostream>
#include<vector>
using namespace std;
//1.暴力解
int maxArea(vector<int>& height) {
int l=height.size();
int res=0;
int i=0,j=l-1;
int lm=0,rm=0;
while(i<j)
{
lm=max(height[i],lm);
rm=max(height[j],rm);
int tmp=min(lm,rm)*(j-i);
res=max(tmp,res);
if(lm<=rm)
{
i++;
}else{
j--;
}
}
return res;
}
int main()
{
vector<int>v={1,8,6,2,5,4,8,3,7};
int res=maxArea(v);
cout<<"sort "<<endl;
cout<<res<<endl;
cout<<"end:"<<endl;
return 0;
}
总结:双指针都出现了下面的代码
if(lm<=rm)
{
i++;
}else{
j--;
}
- 我们前面讲过用的是贪心算法,第一个小格子矩形,能够称多少水取决于左右的min(lm,rm),可以想象成木桶原理,这个时候我们肯定是去找补短板,然后去找一个更加长的。
- 数学推导怎么推导?s=min(x,y)*(yi-xi)
- 反推法:如果我们保持x边界不变化,这个时候min(x,m(未来的边界))<=m,同时(yi-xi)一定是变小的,双指针靠拢了,所以这个时候可以确定min(x,y)*(yi-xi)就是最大值了。
- 如果保持y边界不变,还有可能让min(y,m(未来的边界))>=y,s虽然这个时候(yi-xi)变小了
42 接雨水
#include<iostream>
#include<vector>
using namespace std;
int trap(vector<int>& height) {
int l=0;
int r=height.size()-1;
int lm=0,rm=0;
int res=0;
while(l<r)
{
lm=max(lm,height[l]);
rm=max(rm,height[r]);
//这里不再是容器问题,而是问矩形上面还能接多少雨水,所以他是求局部最优解,然后相加就能得到全局最优解,
所以该矩形只和左右矩形边界,以及自身高度有关,如果左边更高的话,漏桶原理,智能找右边,并得到能盛的雨水
if(lm<rm)
{
res+=lm-height[l];
l++;
}else{
res+=rm-height[r];
r--;
}
}
return res;
}
int main()
{
vector<int>v={0,1,0,2,1,0,1,3,2,1,2,1};
int res=trap(v);
cout<<"sort "<<endl;
cout<<res<<endl;
cout<<"end:"<<endl;
return 0;
}
所以该矩形只和左右矩形边界,以及自身高度有关,如果左边更高的话,漏桶原理,智能找右边,并得到能盛的雨水
if(lm<rm)
{
res+=lm-height[l];
l++;
}else{
res+=rm-height[r];
r--;
}
}
return res;
}
int main()
{
vector<int>v={0,1,0,2,1,0,1,3,2,1,2,1};
int res=trap(v);
cout<<"sort "<<endl;
cout<<res<<endl;
cout<<"end:"<<endl;
return 0;
}