常⻅的双指针有两种形式,⼀种是对撞指针,⼀种是左右指针。
对撞指针:⼀般⽤于顺序结构中,也称左右指针。
• 对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼近。
• 对撞指针的终⽌条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环),也就是:
◦ left == right (两个指针指向同⼀个位置)
◦ left > right (两个指针错开)
快慢指针:⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列结构上移动。
这种⽅法对于处理环形链表或数组⾮常有⽤。
其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快慢指针的思想。
快慢指针的实现⽅式有很多种,最常⽤的⼀种就是:
• 在⼀次循环中,每次让慢的指针向后移动⼀位,⽽快的指针往后移动两位,实现⼀快⼀慢。
1、移动0题目
实际上是数组划分/数组分块的一类题型
即把数组划分为左右两个部分:非0和0
两个指针的作用:
cur:从左到右扫描数组,遍历数组
dest:已处理的区间内,非0元素的最后一个位置
那么我们可以把数组划分为3个区间:
[0,dest]->非0
[dest + 1, cur - 1]->0
[cur, n -1] -> 待处理
要想一直保持这三个区间,只需要:
cur从后往前遍历的过程中,遇到0元素,则cur++(保证cur后面的是0)
遇到非0元素,则dest++后,交换dest与cur(保证非0元素在dest后面),cur再++
题解:
class Solution {
public void moveZeroes(int[] nums) {
int cur = 0;
int dest = -1;//一开始并不知道dest的位置,设为-1
while(cur < nums.length){
if(nums[cur] == 0){
cur++;
}else{
dest++;
int tmp = nums[dest];
nums[dest] = nums[cur];
nums[cur] = tmp;
cur++;
}
}
}
}
2、复写0题目
题目描述的是在数组原地操作,为了便于理解,我们可以先模拟进行数组异地操作,再进行优化:
数组异地操作:
dest指向异地操作的数组,cur遍历原数组,当cur为非0元素,则dest复写一次cur,cur++,dest++
遇到0元素,则dest覆写两次cur,dest+=2,cur++
优化为本地操作:
从前往后:
遇到非0元素,则将cur复写一遍,但是当cur遇到0时,dest复写两遍,就会出现问题:
后面的元素会被0覆盖
因此从后往前是行不通的,我们试试从后往前
从后往前:
我们首先要找到最后一个复写的元素,记为cur,dest作为最后一次复写的位置(查找的过程后面再说)
那么从后往前复写,cur为非0,则dest复写一次cur的内容,cur为0,则dest复写两次cur的内容,这样我们发现,不会出现覆盖掉未遍历元素的情况
那么我们重中之重在于查找最后一个复写的元素位置
我们采用双指针法进行查找:
即从前往后找模拟一遍,但是不复写元素,当dest >= n -1 的时候,cur即为最后一个复写元素的位置
上面情况dest最后出现的位置刚好是最后一个元素,但是:假设数组为:
当我们按照这种情况查找cur的位置的时候,会出现:
前面说过dest代表最后一次复写的位置,但是dest这时候为n,数组就越界了
对于这种特殊情况,实际上是因为最后一次复写的元素刚好是0,导致dest越界,我们到时候特殊处理就好了
题解:
class Solution {
public void duplicateZeros(int[] arr) {
int cur = 0;
int dest = -1;
int n = arr.length;
while(cur < n){
if(arr[cur] == 0){
dest += 2;
}else{
dest++;
}if(dest >= n -1){
break;
}
cur++;
}
//处理特殊情况
if(dest == n){
arr[n-1] = 0;
dest -= 2;
cur--;
}
while(cur >= 0){
if(arr[cur] == 0){
arr[dest--] = 0;
arr[dest--] = 0;
cur--;
}else{
arr[dest--] = arr[cur--];
}
}
}
}