这里的双指针并不是真的必须是指针 只是用到这种思想 可能是数组的下标或者是数值
一.移动零
题目解析
要求把数组中的0全部放到末尾 且非0元素的位置不能改变
不能用到其他数组 要在原地进行操作
算法原理分析
可以用两个指针cur和ptr 把这个数组分为三个部分
[0,ptr]为已经处理好的区间 (ptr,cur)都为0 [cur size-1]为要处理的区间 ptr就是非0元素的最后一个位置 cur指向要处理的数据
开始时候 ptr指向-1的位置 cur指向第一个位置 由cur开始遍历数组
cur遍历过程中遇到的分两种情况 1.是零 2.非零 如果是零的话不做处理 cur++继续对下一个元素进行判断 直到cur指向size(最后一个元素的后一个位置) 如果非零的话则prt++ 然后交换cur和ptr位置的值 cur++
最开始的时候 cur比ptr远一个距离 当cur位置的数为0时 cur++则两者之间会拉开距离且两者之间的数全为0 当cur位置的数非零时候 ptr++ 然后两者交换 这里有两种情况一种就是cur在这之前没有遇见过0 两者距离还是1 那么ptr++就是cur 这种情况的交换没什么意义但也没有影响 另一种就是两者拉开了距离且中间全为0的情况 此时ptr++指向的一定是第一个0 两者交换后ptr此时为非零 cur为0然后cur++对下一个数处理
代码实现
class Solution {
public:
void moveZeroes(vector<int>& nums)
{
int n=nums.size();
int cur=0,ptr=-1;
while(cur<n)
{
if(nums[cur]==0)
cur++;
else
{
swap(nums[++ptr],nums[cur++]);
}
}
}
};
二.复写零
题目解析
从左往右开始遍历 遇到不为0的值目标位置直接放该数 遇到0则目标位置和下一个位置都为0
数组的大小不能改变 多出来的数据不做处理
不能用到其他数组 原数组进行操作
算法原理分析
定义两个指针cur和ptr 都从第一个位置开始 cur向右遍历 当遇到非0 ptr位置就为该值 pre遇到0 prv和prv下一个位置变为0 但是这样会覆盖掉还没有遍历的值 且cur最后一个位置为0的时候ptr下一个位置会发生越界访问
从头开始的遍历不行 那我们就考虑从后开始 (不是从数组的最后一个位置开始 是从应该是cur最后一个位置的值开始 )
以下图为例 对应的最后一个位置就应该为4(前面有两个0 每一个0都多占一个位置最后就少两个)
cur指向的就应该是4 ptr指向最后一个位置 cur从后往前遍历 cur是0 ptr及前一个位置为0 cur为非零值 ptr为对应值 直到cur<0
所以我们需要找到cur的正确位置 这时候我们可以用从前往后的方式
cur初始指向第一个位置 ptr指向-1 当cur位置为非零则ptr++ cur位置为零 ptr+=2 然后判断ptr位置是不是最后一个位置 不是的话cur++继续进行 是的话结束 此时cur位置就是正确的
为什么此时cur的位置就是正确呢 我们可以假设一下
假设所有位置都是非0那么每一次ptr都会到cur的位置 直到最后一次ptr++后到了最后一个位置结束此时cur最后一个位置正确 假设数组里面靠前有一个0的情况 遇0之前ptr每次还是到cur的位置 cur遇到0之后 ptr+=2 之后每次都超过cur一个位置 直到到最后一个位置 此时cur在倒数第二个位置也是正确的
也就是说每当cur遇到一个0 在ptr移动之后两者就会多拉开一个距离刚好用来存复写的0
但是还有一种特殊的情况会出问题 当ptr在倒数第二个位置的时候 cur遇到的最后一个是0 那此时ptr+=2就指向最后一个元素的后一个位置 了 后面进行访问会发生越界的情况
其实解决很简单 赋值之前先判断是否ptr==n(即是否越界了) 如果越界了 让ptr上一个位置为0然后ptr-=2 cur也直接--
为什么可以这样呢 这种特殊情况 其实正着来看就是在最后只剩一个位置的时候cur遇到了0 但只有一个位置了最后就只能放一个0 刚刚的操作其实就是对这种情况进行了特殊处理 处理结束然后就是正常从后往前的遍历
代码实现
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
int n=arr.size();
int ptr=-1,cur=0;
//找pre位置
while(1)
{
if(arr[cur]==0)
ptr+=2;
else
ptr++;
if(ptr>=n-1) //==n-1是正常到了最后 >n-1最后一个0的特殊情况
break;
cur++;
}
if(ptr==n) //特殊的情况 相当于还把最后一个已经处理了
{
arr[--ptr]=0;
ptr--;
cur--;
}
while(cur>=0) //从后往前遍历
{
if(arr[cur]==0)
{
arr[ptr--]=0;
arr[ptr--]=0;
cur--;
}
else
{
arr[ptr--]=arr[cur--];
}
}
}
};
三.快乐数
题目解析
题目给了一个数n 将其每一位的值的平方加起来代替原理的 一直重复此过程 如果在过程中变为1 返回true 在这个过程中无限循环始终不为1则返回false
算法原理分析
要注意第二点 要么最后会到1 要么无限循环 也就是下图的两种情况
第二种无限循环的情况和当时循环链表那里非常相似 其实第一种到1的情况继续下去也就是不断1 1 1 1的循环
也就是说其实两种情况最终都一定会循环 对于第一种在循环中一直都是1 那么我们只要用类似循环链表那里的快慢指针(如果带坏 快指针一次两步慢指针一次一步 两着最终一定会相遇)找到相遇的节点判断它是否为1就可以了 当然这里不是真的指针 只是一个值
代码实现
class Solution {
public:
int Getpfh(int n) //单独写一个函数做处理 调用一次就相当于走了一步
{
int tmp=0;
while(n)
{
tmp+=pow(n%10,2); //每次+=最后一位的平方
n/=10; //去掉最后一位
}
return tmp;
}
bool isHappy(int n) {
int fast=n,slow=n;
fast=Getpfh(fast); //fast先走一步 要不然刚开始时相同的 循环直接结束了
while(fast!=slow)
{
slow=Getpfh(slow);
fast=Getpfh(fast); //相当于slow每次一步 fast每次两步
fast=Getpfh(fast);
} //这里题目是一定循环的 则一定会相遇
if(fast==1)
return true;
else
return false;
}
};
快乐数进阶
这个题目里是一定会循环 那么如果存在不循环的情况呢
首先先说一个原理
鸽巢原理:若将更多的物体放入较少的容器中,则至少有一个容器中包含多个物体
假如5只鸟放到4个巢穴里 一定有一个是>=2
在这里 第一个数之后的每一个数为前一个数的各项平方之和 如果是不循环的情况 这个值可能有多少种情况呢
int的最大值为2的31次方−1=2147483647 (有十位) 在这里范围给扩大一下 也好算一些 取(999999999) 则各项平方和最大为9*9*10=810 也就是说在不重复的情况下产生的结果有811种(算上0) 这里是都变为9来算了 其实要少很多还
如果是不循环的情况 后面的结果会一直不相同 所以循环的次数>811了 就说明一定是循环的
四.盛最多水的容器
题目解析
数的大小就代表高度 在一个数组中任意取两个位置的数 根据木桶原理 他们所能乘水的高度就是最小那一个的值 其实就是用两者中高度的最小值乘他们之间的距离 找到这个的最大值
算法原理分析
1.暴力求解
两层for循环 每次计算高度*距离 大的话就进行更新 但这样时间复杂度O(N^2) 会超时
2.双指针
假设先取最左和最右两个位置为要取的 算出他们的height*距离 下一次其中一个向中间移动
高度在移动之后有变小不变变大三种情况 但是他们之间的距离是一定会减小的 那么高度不管是变小还是不变最终的高度*距离一定是变小的的
基于此 我们设立两个指针 一个left指向第一个位置 一个right指向最后一个位置 sum为height*距离 先算出此时的sum
然后让两者中小的那一个往中间移动(如果移动大的话 最终的高度要么不变要么变小 而距离是一定变小的 也就是最终的sum一定是变小的) 其实在这个过程中就是把一定小于此时sum的情况给跳过了
再算出这时候的sum如果更大就进行更新 一直到left===right为止
代码实现
class Solution {
public:
int maxArea(vector<int>& height)
{
int left=0,right=height.size()-1;
int sum=0;
while(left<right)
{
int hei=min(height[left],height[right]); //高度为更小的那一个
int len=right-left; //之间的距离其实直接就是right-left
int sum2=hei*len;
if(sum2>sum) //如果更大就更新sum值
sum=sum2;
if(height[left]>height[right])
{
right--; //更小的向中间靠
}
else
left++;
}
return sum;
}
};
五.有效三角形个数
题目解析
找三个数满足三角形的条件 相同的数在不同位置也可以计入 那么就不需要考虑重复的情况 也就是在数组中任意选取三个数 满足三角形的条件就算一组数据
算法原理分析
判断是不是三角形很简单 任意两边之和大于第三边 其实只需要考虑最小的那两个数大于最大的那一个就可以
1.暴力求解
三层for循环 每一组都判断是不是三角形 是的话++ 这样同样会超时
2.双指针
既然取出的三个数里只需考虑更小的那两个大于最大的那一个就可以
可以考虑把数组给排序 然后在左边取两值判断就可以
①先排序
②设立一个指针end指向最后一个位置 在每一轮结束后-- 直到<2
③ 单趟中在设立一个指针指向最后一个位置后 设一个指针left指向第一个位置 一个指针right指向end前一个位置 判断此时left和right的和是否大于end位置的值
如果大于的话 那么此时计数的sum直接加right-left (left是最左边的数 如果left都符合了 left后面的数据加上rigjt位置的值会更大 一定也符合要求) 然后right--
如果小于的话 left++ (这时候的left加right都已经小于end位置的之后了 之后right--两着之和只会更小 更不满足要求 left位置改变持续到下一轮end位置改变 )
代码实现
class Solution {
public:
int triangleNumber(vector<int>& nums) {
//先排序
sort(nums.begin(),nums.end());
int n=nums.size();
int end=n-1;
int sum=0;
while(end>1) //控制最右边的end
{
int left=0;
int right=end-1;
while(left<right)
{
int add=nums[left]+nums[right];
if(add>nums[end])
{
sum+=right-left;
right--;
}
else
{
left++;
}
}
end--;
}
return sum;
}
};
六.和为s的两个数
题目解析
在数组中找到两个数 和为给定的值target 只需返回一个结果就可以
算法原理分析
1.暴力求解
两层for循环 每组的两个数加起来进行判断 是的话直接返回
2.双指针
一个left指向第一个位置 一个right指向最后一个位置
判断两者的和加起来是不是target 是的话返回 如果小于target left++ 大于target right--
class Solution {
public:
vector<int> twoSum(vector<int>& price, int target) {
int left=0,right=price.size()-1;
while(left<right)
{
int add=price[left]+price[right];
if(add<target)
left++;
else if(add>target)
right--;
else
return {price[left],price[right]};
}
return {};
}
};
七.三数之和
题目解析
数组中找三个数相加之和为0 但是要注意重复的值不能重复算 输出的三元组中三个数的位置可以改变 这里主要注意不能有重复的
算法原理解析
1.暴力求解
这里的暴力求解需要三层循环 这里还不能有重复的 需要找到的符合条件的里面进行去重 去重的比较很麻烦
2.双指针
这里主要注意取重 还有不能漏取 上一个题只需要返回一组结果 这里要都找到
三个数之和为零其实就可以转换为两个数之和为另一个数的相反数 就用到上一个题的思路 但这里是都要找到 target要改变 这里又用到第五题的思路
①先排序
②定义一个指针end指向最后一个位置 每一轮之后-- 直到<2
③定义一个指针left指向第一个位置 另一个指针right指向end前一个位置
如果left和right位置数之和add小于则left++ 大于则right-- 等于end位置的值 则返回然后left++right--继续找其他的情况 直到left>=right
在这个过程中不能有重复的 所以每次left和right及end满足条件之后的移动的时候要判断移动之后的值和之前的值是否相同 如果相同的话再走一步直到与上一次位置的值不相同 但是如果数组最左边连续相同值时候right和end在向左移动过程中停不下来最终会越界访问 或者是最右边连续相同的情况left会停不下来 会越界访问 所以每次移动还需要判断他们的结束条件 对于end还可以进行一个小优化end位置的值当到小于0的时候直接结束
代码实现
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
//先排序
sort(nums.begin(),nums.end());
vector<vector<int>> ss; //最终要返回的
int end=nums.size()-1;
while(end>=2)
{
if(nums[end]<0) break; //小优化当end位置的值小于零时相反数就为正数此时它左边两个数相加
int left=0,right=end-1; //不可能为正数
int target=-nums[end];
while(left<right)
{
int add=nums[left]+nums[right];
if(add>target)
right--;
else if(add<target)
left++;
else
{
ss.push_back({nums[left],nums[right],nums[end]});
left++;
right--; //对left和right的去重
while(left<right&&nums[left]==nums[left-1])
left++;
while(left<right&&nums[right]==nums[right+1])
right--;
}
}
end--; //对end的去重
while(end>=2&&nums[end]==nums[end+1])
{
end--;
}
}
return ss;
}
};
八.四数之和
题目解析
和上一个题类似 相当于上一题的强化版本 这里是找四个数之和为target 同样不能重复
算法原理分析
和上个题基本一样 这里除了需要一个end指向最后一个位置之外 还需要一个begin指向起始位置
先固定begin的位置 每轮结束之后begin++ 在里层基本就是和上一题一样了
要注意的也和上一题一样 要注意去重 去重时候做的调整不要越界
1.排序
2.一个指针begin指向第一个位置 end指向最后一个位置 先固定begin移动end 一轮结束后移动begin
3.在begin不动 end不动的里面设一个指针left指向begin后一个的位置 right指向end前一个位置
里面逻辑都和上一题一样
代码实现
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> ss; //最后返回的结果
//排序
sort(nums.begin(),nums.end());
int n=nums.size(); //元素个数
int begin=0;
while(begin<=n-4) //begin和end直接还要至少夹两个数 则begin最后一次为倒数第四个位置
{
long long target1=target-nums[begin]; //这里测试用例int还不够 会超过其最小值
int end=n-1;
while(end>=begin+3)
{
long long target2=target1-nums[end];
int left=begin+1,right=end-1;
while(left<right)
{
int add=nums[left]+nums[right];
if(add>target2)
right--;
else if(add<target2)
left++;
else
{
ss.push_back({nums[begin],nums[left],nums[right],nums[end]});
left++;
right--; //begin和right的去重操作
while(left<right&&nums[left]==nums[left-1]) left++;
while(left<right&&nums[right]==nums[right+1]) right--;
}
}
end--; //end去重操作
while(end>=begin+3&&nums[end]==nums[end+1]) end--;
}
begin++; //beign去重操作
while(begin<=n-4&&nums[begin]==nums[begin-1]) begin++;
}
return ss;
}
};