leetcode双指针(C++)
目录
1.移动零

将数组中所有的0移动到数组末尾,并且不能开辟新数组。

算法原理:
数组划分,把一整个数组(直线表示)用一个分割线,划分成2部分,分别是左边的非零元素,还有右边的0
一般用双指针来解决数组划分问题,在数组问题中利用数组下标充当指针
cur:从左往右扫描,遍历数组
dest:已处理的区间内,非零元素的最后一个位置
三个区间:
[0,dest]:存放非零元素
[dest+1,cur-1]:存放0
[cur,n-1]:待处理区间
初始化时,dest放数组前面,cur放数组首元素。
cur遇到0直接往后一位,遇到非零元素让dest往后一位,同时交换dest和cur的元素,非边界情况下交换的都是0与非零元素(也不算特别边界其实)
边界情况下,例如[1,0,0],dest和cur同时指向1,1和1交换位置不动,依旧能满足操作
例如[0],cur会直接遍历完整个数组,依旧满足操作

class Solution {
public:
void moveZeroes(vector<int>& nums) {
for(int cur=0,dest=-1;cur<nums.size();cur++)
if(nums[cur]) swap(nums[cur],nums[++dest]);
}
};
2.复写零

请不要在超过该数组长度的位置写入元素。请对输入的数组就地进行上述修改,不要从函数返回任何东西。
把数组中出现的每一个0都复写一遍

思路A:
不管题目的要求,开辟一个新数组,把旧数组遍历一遍,如果是非零元素直接放到新数组中,如果是0就一次放2个0到新数组中。
思路B:
如果cur和dest都从同一个数组的开头往后移动,那么肯定有元素会被覆盖掉。
那么我们大可逆向思维,从后往前移动。
第一步:dest指向最后一个元素,cur指向最后一个需要复写的数
第二步:如果非零元素,就直接cur指向元素覆盖掉dest指向元素,然后统一向前1位;如果为0,就一次覆盖两个dest的元素(紧邻的2个),然后cur向前1位
第三步:当cur或dest出了数组,结束操作

现在的难点就在于,怎么让cur指向最后一个需要复写的数呢?
那我们就模仿第二步,搞出一个没有复写操作的逆操作,依旧可以完成这个目标
cur遇到非零元素,dest往后1步,cur也往后1步;cur遇到0,dest往后2步,cur往后1步
边界情况:
当0出现在了数组倒数第二个位置,然后dest +=2 后出现越界了怎么办?
直接让n-1下标的元素覆盖为0,然后dest -=2
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
int cur,dest;
//1.找数
for(cur=0,dest=-1;cur<arr.size();cur++){
if(arr[cur]) dest++;
else dest+=2;
if(dest >= arr.size()-1 ) break;
}
//2.处理边界情况
if(dest == arr.size())
{
arr[arr.size()-1] = 0;
cur--;
dest-=2;
}
//3.从后向前完成复写操作
while(cur >= 0)
{
if(arr[cur]) arr[dest--]=arr[cur--];
else
{
arr[dest--] = 0;
arr[dest--] = 0;
cur--;
}
}
}
};
3.快乐数


示例一:19中的 1^2 + 9^2 = 82,不断重复同一操作,最后就得到了 1^2 + 0 + 0 = 1(1不断循环成为1),这种就是快乐数
示例二:不断重复同一操作后,不难发现会回到4这个数字,然后又开始新一轮的循环,但是永远都不会到 1 的结果,因此就不是快乐数

通过两个示例,我们可以抽象组合成一个模型。一个最后会进行1的不断循环,一个最后会进入非1数字的循环圈。因此无论是否为快乐数,最后都能够进入一个循环圈
这就能和判断链表环结合起来,即判断环中的数是否全为1
在判断链表是否有环的题目中,我们使用到了快慢指针,因此该题我们依旧可以使用快慢双指针的办法
本题在题干中声明:某数可能变为 1,也可能是无限循环但始终变不到 1。所以是必然有环的,就不需要考虑不存在环的情况了
但如果没有这个声明,我们依旧可以通过抽屉原理判断出必然成环
整型最大值2^31 - 1 = 2,147,483,647 ≈ 2.1 * 10^9
999999999 = 9.9 * 10^8,最后通过快乐数的那个加法操作算出来是 9^3 =729
1999999999 最后通过快乐数的加法操作算出来是 730
因此无论n怎么变,最后的值算出来的范围都在[1,730]
根据抽屉原理(一个班级13个人,一定有2人生日在同一个月),至多进行731次快乐数的加法操作就一定能成环
算法原理:
1.定义快慢指针
2.慢指针每次向后移动1步,快指针每次向后移动2步
3.因为一定有环,所以快慢指针一定会相遇,判断相遇时候的值即可
我们可以把数字本身看作指针。
以示例1为例,slow = 2 -> slow = 4;fast = 2 -> fast = 16
class Solution {
public:
int my_pow(int n)//返回n这个数每一位上的平方和
{
int sum=0;
while(n)
{
int temp = n%10;
sum += temp*temp;
n/=10;
}
return sum;
}
bool isHappy(int n) {
int slow = n, fast = my_pow(n);
while(slow != fast)
{
slow = my_pow(slow);
fast = my_pow(my_pow(fast));
}
return slow == 1;
}
};
4.盛水最多的容器

每两条线相距为1,每条线的高度在数组中给出,找出两条线,要求所围绕的面积最大

思路A:
暴力解法就是把所有情况都算一遍(1和8结合、1和6结合、……8和6结合、8和2结合、……),把最大值找出来即可。过于暴力,leetcode不会通过的。
时间复杂度:n^2
思路B:
搞一个left,指向数组首元素;搞一个right,指向数组末元素。
当left与right不断向内夹逼的时候,宽度必然是不断减小的。
3种情况:
1.高度和宽度都降低 --> 体积降低
2.高度不变,宽度降低 --> 体积降低
3.高度变高,宽度降低 --> 体积可能升高也可能降低
例如[6,2,5,4]当中的4,与5、2、6相结合的时候高度恒为4,但宽度越来越大,所以4与6结合是最优的。可如果5与6结合,高度就变大了,体积就有更大的可能。因此我们可以提取出4与6结合的这种情况(4的最大情况),然后将right往左移动一位,试试5与6的结合(5的最大情况)。可如果是[6,2,3,4]的这种情况,right往左移动一位以后,高度和宽度都降低了,此时判断不判断没有影响了。
简而言之,就是遇到情况1、2都无关紧要,遇到情况3就判断大小(代码对3种情况都会进行判断,这样不会导致代码的冗余)
时间复杂度:n
class Solution {
public:
int maxArea(vector<int>& height) {
int n = height.size();
int left = 0,right = n-1;
int ret = 0;
while(left < right)
{
int v = min(height[left],height[right]) * (right-left);
ret = max(v,ret);
if(height[left] < height[right]) left++;
else right--;
}
return ret;
}
};
5.有效三角形的个数

返回数组中可以形成三角形的三元组个数(不同位置的2算两个2)
形成三角形的条件:较小的2个数相加,大于较大的那个数
由此,优化:对数组进行排序,[4,2,4]三元组和[2,4,4]三元组对结果没有影响

思路A:
暴力枚举,搞出所有情况进行判断(伪代码的写法如上图)
时间复杂度:n^3
思路B:
从数组的末尾开始,固定下来一个最大的数,利用排序完的数组具有单调性来优化
情况1:left和right指向的a+b > c(指定数),那么left和right之间的所有数都满足 a + b > c情况2:left和right指向的a+b <= c
遇到情况1,统计right - left 个三元组,都满足有效三角形。统计完之后,right所指向的元素已经所有情况都被考虑到了,因此可以去除,通过right -- 的方式来去除
遇到情况2,因为小的数过小,right减小之后,小的数和right指向的数也不会比c大。因此可以通过数组单调性直接把那个小的数给去除,通过left++的方式来去除,然后不断判断直到符合情况1
最后固定下数组中倒数第二大的数,重复上述操作……
时间复杂度:n^2

class Solution {
public:
int triangleNumber(vector<int>& nums) {
sort(nums.begin(),nums.end());
int n = nums.size(),ret = 0;
int left,right;
for(int num = n-1;num >= 2;num--)
{
int left = 0,right = num-1;
while(left < right)
{
if(nums[left]+nums[right]>nums[num])
{
ret += right-left;
right--;
}
else left++;
}
}
return ret;
}
};
6.和为s的两个数

数组中找出2个数,相加等于指定值。
利用排序完的数组具有单调性+左右指针向内夹逼来解决
class Solution {
public:
vector<int> FindNumbersWithSum(vector<int> array,int sum) {
int left=0,right=array.size()-1;
while(left<right)
{
if(array[left]+array[right]>sum) right--;
else if(array[left]+array[right]<sum) left++;
else return {array[left],array[right]};
}
return {};//一定要有个数组类型返回值
}
};
7.三数之和

数组中三个不同位置的数相加为0,构成无序三元组,不同顺序但数字相同的三元组挑一个返回即可,不需要全部返回。怎么都不可能形成所需的三元组时,返回空数组

通过排序可以简化整道题目。
证明:以示例1为例,排序之后就会有[-4,-1,-1,0,1,2],即使出现数字相同的两个三元组,顺序也是必然相同的,这种情况下对我们“舍去其中一个”的操作非常有利。
思路A:
排序 + 暴力枚举 + 哈希表(unordered_set)
时间复杂度:n^3
思路B:
固定数(有效三角形个数)+ 左右指针向内夹逼 + 排序 + 排序完的数组具有单调性
时间复杂度:n^2
例如下图所给的例子,固定了-4以后,右边那个区间的数组就可以转换到和为4的两个数字问题,但要转换成不停下来的和为s的两个数字问题
并且,找到一种结果后,left和right指针要跳过重复元素;当使用完1次某个固定数以后,不同位置的同一固定数不能再用了,也要跳过。这样做可以有效去重
当也要考虑到下图出现的特殊情况,这时left或者right可能会越界访问
最后,当固定数比0大时,那么后面的情况都可以直接排除了,因为三个正数相加不可能为0;同时,如果数组中全是正数或全是正数的情况下,可以直接返回空数组了

class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ret;
int n = nums.size();
sort(nums.begin(),nums.end());
if(nums[0] > 0 || nums[n-1] < 0) return {};
for(int i = 0;i < n-2;)
{
if(nums[i] > 0) break;
int left = i+1,right = n-1;
while(left < right)
{
if(nums[left] + nums[right] > -nums[i]) right--;
else if(nums[left] + nums[right] < -nums[i]) left++;
else
{
ret.push_back({nums[i],nums[left],nums[right]});
left++,right--;
while(nums[left] == nums[left-1] && left < n - 1) left++;
while(nums[left] == nums[left-1] && right > i + 1) right--;
}
}
i++;
while(i < n - 2 && nums[i] == nums[i-1]) i++;
}
return ret;
}
};
8.四数之和

该题就是基于“三数之和”又添加了一个数

class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> ret;
int n=nums.size();
long int sum=0;
sort(nums.begin(),nums.end());
for(int a=0;a<n;)
{
for(int b=a+1;b<n;)
{
int left=b+1,right=n-1;
long long flag=(long long)target-nums[a]-nums[b];
while(left<right)
{
sum=nums[left]+nums[right];
if(sum>flag)right--;
else if(sum<flag)left++;
else
{
ret.push_back({nums[left],nums[right],nums[a],nums[b]});
right--;
left++;
while(left<right&&nums[right]==nums[right+1])right--;
while(left<right&&nums[left]==nums[left-1])left++;
}
}
b++;
while(b<n&&nums[b]==nums[b-1])b++;
}
a++;
while(a<n&&nums[a]==nums[a-1])a++;
}
return ret;
}
};
总结:
画图、数组划分(分割线)、固定数思维、逆向思维、(链表)环判断思想、快慢指针、抽屉原理、函数封装、左右指针向内夹逼、 单调性、排序
双指针只是一种思想,可以把数组下标视为指针(1、2题),甚至可以把某一个数字作为指针(3题)
C++特殊用法:在函数中,return {X,Y} 就相当于返回了一个有2个元素的数组,return { } 就相当于返回了一个空数组,return{X,Y,Z}就相当于返回了一个有3个元素的数组
更多推荐
所有评论(0)