每日算法题推送-->今日专题——双指针法
题目1:https://leetcode.cn/problems/move-zeroes
小编刚看到这道题的时候,想到的第一个方法就是建立一个与原数组等大的新的数组,然后遍历原数组,如果遇到元素值不为0的元素,就将这个元素放到新数组中,直到遍历完整个数组,那么当遍历完整个数组以后,新数组的剩余位置就应该都存放0,直接通过循环存放0即可,最后将新数组中的元素依次赋值给原数组即可。
但是,题目要求,必须在不复制数组的情况下对数组进行原地操作。
这要咋办?
还是请出我们的老朋友——双指针法,看利用它是否能解决这道问题。
我们可以这么想,这道题的本质无非就是实现数组的分块,以某一个位置为界限,这个位置左边的元素都是不为0的,右边的元素都等于0.
那我们就可以这样操作:设定一个界限dest,然后通过算法使得dest左边的元素都是不为0的。
现在我们来考虑实现这个算法:定义一个整形指针pcur来遍历数组,初始pcur指向0;而刚开始的时候dest所包含的区域中肯定是一个元素都没有的,所以我们可以将它初始化为-1。
进行一下操作:如果pcur指向的元素不等于0,那就让dest先往前走一步,然后再让dest位置的元素赋值为pcur所指向的元素,如果pcur指向的元素等于0,那直接让pcur往前走一步即可。
这样一来,pcur越界以后,dest之前的元素都是不为0的元素,我们只需要再将dest之后的元素通过循环都变成0即可。
void moveZeroes(int* nums, int numsSize)
{//双指针,定义prev和pcur指针//pcur指向的元素如果不等于0,就让prev指向的位置的值等于pcur指向位置的值,prev往前走,pcur也往前走,反之,prev不动,pcur往前走int pcur = 0, prev = -1;while (pcur < numsSize){if (nums[pcur] != 0){nums[++prev] = nums[pcur];pcur++;}else {pcur++;}}for (int i = prev+1; i < numsSize; i++){nums[i] = 0;}}
我们来画图演示一下这个过程:
上面的思路中我们分别用了两次循环才实现代码,有没有更简洁的方法呢?能不能一次就搞定啊?
包有的,现在我们就来看一下:
我们还是让dest初始指向-1,pcur初始指向0,如果pcur指向的元素不为0,那就先让dest往前走一步,然后交换dest和pcur所指向的元素,反之,直接让pcur往前走一步,到了最后pcur越界,此时就已经将数组变成预期的效果了
void moveZeroes(int* nums, int numsSize)
{int pcur=0,dest=-1;while(pcur<numsSize){if(nums[pcur]){dest++;int tmp=nums[dest];nums[dest]=nums[pcur];nums[pcur]=tmp;}pcur++;}
}
题目2:https://leetcode.cn/problems/duplicate-zeros
这道题就有点意思了,虽然这道题的难度设置是简单,但他的整体思路还是比较难想的。
如果我们异地修改整个数组,即创建一个新的数组来完成任务,那这道题就非常简单,但是这道题要求不能使用这个方法。
小编就不卖关子了,直接给出这道题的整体思路。
以上面的实例1为例,我们呢来讲解一下这道题的整体思路。
我们可以根据输出结果看到数组中要保留的最后一个元素是4,我们可以定义一个整形指针pcur指向要保留的最后一个数据,再定义一个整形指针dest指向数组中的最后一个位置,执行以下操作"
1.根据pcur指向的元素的值判断dest应该向前走多少步:
- 如果pcur指向的值是0,那么就进行两次这个操作:让dest指向的位置的值赋值为0,同时让dest往前走一步;之后还要再让pcur往前走一步
- 如果pcur指向非0值x,那么就进行以下操作:让dest指向的位置的值赋值为x,同时让dest和pcur往前走一步
如以下示意图:
可以看到,这种算法确实可以帮我们完成任务,但是现在的问题就转换为我们如何写一个算法能够得到最后一个要保留的元素的值。
这个方法比较难想到,我们直接给出算法:
定义两个指针pcur和dest,pcur初始指向0,dest指向-1,执行以下操作:
1.如果pcur指向的值为非0值,就让dest往前走一步;判断dest是否越界(dest>=numssize-1),如果dest越界,那么就直接结束遍历,如果没有越界,就让pcur往前走一步
2.如果pcur指向的值等于0,就让dest往前走两步,判断dest是否越界(dest>=numssize-1),如果dest越界,那么就直接结束遍历,如果没有越界,就让pcur往前走一步
当dest越界的时候,pcur指向的位置就是我们要找的位置。
我们可以画图来验证一下:
可以看到,通过以上算法步骤,pcur最终指向的位置确实就是我们要保留的最后一个数据。
但是,还有一个需要考虑的特殊情况,如以下案例:
我们可以发现,在上面的例子中,dest超过了数组可以表示的范围,其实原因就是最后一个要保留的数据是0,所以dest要走两步发生越界,这种情况下,我们需要进行特殊处理:
直接让数组下标为n-1的位置的值等于0,然后让pcur往前走一步,dest往前走两步即可:
我们结合上面的思路来写一下代码:
void duplicateZeros(int* arr, int arrSize)
{int n=arrSize;//先找到数组中最后一个要保留的数据的位置int pcur=0,dest=-1;while(pcur<n){if(arr[pcur])dest++;elsedest+=2;if(dest>=n-1)break;pcur++;} //进行特殊处理if(dest>n-1){arr[n-1]=0;dest-=2;pcur--;} //进行元素的复写while(pcur>=0){if(arr[pcur]){arr[dest--]=arr[pcur--];}else{arr[dest--]=0;arr[dest--]=0;pcur--;}}
}
小结:这一小节我们主要通过两个算法题对应用双指针的解题思路进行了讲解,小编认为第二道题还是比较有难度的。小伙伴们一定要自己在草稿纸或者电脑上多画一下图,理解算法的思想。
欢迎小伙伴们在评论区提出问题和讨论!!!