Welcome to 9ilk's Code World
(๑•́ ₃ •̀๑) 个人主页: 9ilk
(๑•́ ₃ •̀๑) 文章专栏: 算法Journey
本篇博客我们讲解一下双指针算法的应用,双指针算法在做数组相关的题比较常用,下面开始我们今天的学习~
🏠 移动零
📒 题目解析
题目内容:
这道题目是给我们一个数组,我们需要将数组划分两个部分,一个部分是非0元素,另一部分都是0,同属还要注意不能复制数组要原地操作,同时非0元素在移动后顺序要与移动前相同,比如示例1中1和3在移动完成后1还是要在3的前面
📒 算法原理
思路1:
1.首先定义两个指针target用来找非0的数,dest用来找0
2.在target不越界的情况下如果target的位置比dest大我们就对两者位置的数进行交换,交换完后target继续遍历扫描,而dest要注意如果交换完后位置还是0就让其不动,反之++
3.如果target的位置比dest小就不需要交换,我们只需target++让他继续扫描
动图演示:
参考代码:
class Solution {
public:
void moveZeroes(vector<int>& nums)
{
int target = 0; //找非0的指针
int dest = 0;//找0的指针
if(nums.size() == 1)
return;
while (target < nums.size() )
{
//先找非0的数
while (target < nums.size() && nums[target] == 0)
{
target++;
}
//找0
while (dest < nums.size() && nums[dest] != 0)
{
dest++;
}
//交换并更新指针
if (target < nums.size() && dest < nums.size()&& target > dest)
{
int temp = nums[dest];
nums[dest] = nums[target];
nums[target] = temp;
//更新指针位置
if (nums[dest] != 0)//这样才能保证相对顺序
dest++;
target++;
}
else
{
target++;//target继续遍历
}
}
}
}s;
这种思路主要是一个指针遍历整个数组找到非0元素,另一个指针紧随其后找0,满足题目条件就进行交换,直到遍历完为止,但是细节较多且有点混乱我们从另一个角度看看。
思路2:
我们最终的目的是将数组划分为两块,以dest为分界点,dest及其左半部分都是非0元素,dest右边的都是0.
因此我们仍然采用两个指针:
1.cur指针用来遍历整个数组,dest作为一个分界点
2.当cur遇到0元素时,cur++继续移动因为此时(dest,cur-1]都是0
3.当cur遇到非0元素时,要将非0元素并入[0,dest]这个区间,也就是dest++给他一个位置再交换,cur继续移动++
动图演示:
参考代码:
class Solution {
public:
void moveZeroes(vector<int>& nums)
{
int cur = 0;//从零下标的开始待处理 所以dst下标是-1
int dest = -1;
while(cur < nums.size())
{
if(nums[cur] == 0)
{
cur++;
}
else
{
dest++;//dest++是为了让非0的数并入[0,dest]的区间
swap(nums[dest],nums[cur]);
cur++;
}
}
}
}s;
这样是不是就很清晰了
总结:对于类似这种数组划分区间/数组分块的问题可以考虑用双指针
🏠 复写零
📒 题目解析
题目内容:
这道题目要求我们对一个数组中的0复写一遍,但要注意只能原地修改,返回数组是没用的;同时并不是复写前数组中每个0都要复写,如果复写到超出原数组长度就停止!
📒 算法原理
思路1:
1.找0:首先我们需要定义一个cur指针遍历数组,找到0就停下来复写
2.复写:到0的位置,我们利用迭代器在该位置的下一个位置进行复写(插入0)并更新指针
3.删除:复写完成后可能有超出size的部分,这时需要我们进行删除
注意点:
1.在找0时,如果遇到类似【1,2,3】这样的例子,可能会越界,所以我们需要加上一个限制条件
2.在复写时,如果插入0的位置合法,我们正常插入;如果不合法(比如 【1 2 3 4 0】),我们仍然需
要更新cur指针使它遍历完数组
3.在复写找0后的插入位置时,用find我们要注意从cur位置处往后找,因为前面可能有相同的数字
4.删除过程判断:将迭代器更新到begin() + size - 1位置,再判断复写后size是否变化,有变化则
删除数据
动画演示:
参考代码:
class Solution {
public:
void duplicateZeros(vector<int>& arr)
{
int cur = 0;
int oldsize = arr.size();
vector<int>::iterator it = arr.begin();
while (cur < oldsize)
{ //找0
while (cur < oldsize && arr[cur] != 0)
{
cur++;
}
//复写过程
if (cur + 1 < oldsize)//可能造成死循环所以要加上else 比如1 1 2 4 0最后一个是0
{
it = arr.begin() + cur;
it = std::find(it, arr.end(), arr[cur + 1]);//注意要从cur的后面开始找 前面可能有一样的数字
it = arr.insert(it, 0);
cur += 2;
}
else
{
cur++;
}
}
it = arr.begin() + (oldsize - 1);//判断是否增加了数字 多的要删除
//删除过程
if (arr.size() - oldsize > 0)
arr.erase(it + 1, arr.end());
}
}s1;
这种思路比较容易想到,但是要注意细节(越界,边界情况,迭代器更新等),一不小心就容易出错,下面提供第二种思路
思路2:
如果我们跟移动零一样从左往右进行复写呢?
很明显我们发现从左往右会导致部分数据被覆盖,因此我们选择从右往左复写
从后往前就很好地解决了覆盖的问题,问题是怎么确定最后一个复写的数,这里又要用到我们的双指针算法
这里的原理主要是利用一个指针cur确定这个数要复写几次,另一个指针dest移动次数代表了这个数复写的次数,复写到size() - 1为止,这样就很好地模拟了这个过程,实在巧妙!但是我们还要注意边界情况!
参考代码:
class Solution {
public:
void duplicateZeros(vector<int>& arr)
{
int cur = 0;
int dest = -1;
//找最后一个复写的数 dest到达size() - 1就停止
while(dest != arr.size() - 1)
{
if(arr[cur])
{
dest++;
}
else
{
if(dest == arr.size() - 2) //边界情况
{
arr[arr.size() - 1] = 0;
cur--;
break;
}
else
dest+=2;
}
if(dest < arr.size() - 1)
cur++;
}
//复写过程 从后往前
while(cur >= 0)
{
if(arr[cur])
{
arr[dest] = arr[cur];
cur--;
dest--;
}
else
{
int count = 2;
while(count--)
{
arr[dest--] = 0;
}
cur--;
}
}
}
}s1;
启示:这两道题都可以用双指针来解决问题,他们都用了一个指针cur来探路,一个指针dest来处理我们需要做的事,如果指针从左往右不行,我们试试从右往左,先试着画图模拟出移动和指针的更新规律。