以下的标题是oj链接
常见的双指针有两种,⼀种是对撞指针,⼀种是快慢指针。
对撞指针:⼀般用于顺序结构中,也称左右指针。
1.对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼近。
2.对撞指针的终止条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环)
- left == right (两个指针指向同⼀个位置)
- left > right (两个指针错开)
快慢指针:又称为龟兔赛跑算法,其基本思想就是使用两个移动速度不同的指针在数组或链表等序列结构上移动。
这种方法对于处理环形链表或数组非常有⽤,如果我们要研究的问题出现循环往复的情况,均可考虑使用快慢指针的思想。
快慢指针的实现方式有很多种,最常用的⼀种就是:在⼀次循环中,每次让慢的指针向后移动⼀位,而快的指针往后移动两位,实现⼀快⼀慢。
移动零
思路:
我们可以用⼀个 cur 指针来遍历整个数组,另⼀个 dest 指针用来记录非零数序列
的最后⼀个位置。使 [0, dest] 的元素全部都是非零元素, [dest + 1, cur - 1] 的
元素全是零,剩下的区间是未处理的数据.
过程:
1.初始化 cur = 0 (⽤来遍历数组), dest = -1 (指向非零元素序列的最后⼀个位置。
因为刚开始我们不知道最后⼀个非零元素在什么位置,因此初始化为 -1 )
2.cur 依次往后遍历每个元素,遍历到的元素会有下面两种情况:
i. 遇到的元素是 0 , cur 直接 ++ 。
ii. 遇到的元素不是 0 , dest++ ,如果dest != cur时交换 cur 位置和 dest 位置的元素,之后让cur++
class Solution {
public void moveZeroes(int[] nums) {
for(int cur = 0 ,dest = -1; cur <nums.length; cur++) {
if(nums[cur] != 0) {
dest++;
if(dest != cur) {
int tmp =nums[cur];
nums[cur] = nums[dest];
nums[dest] = tmp;
}
}
}
}
}
更好理解的另一种做法:
用cur遍历数组
- cur下标的元素不为0,那就把元素放入dest下标中,cur++,dest++
- cur下标的元素为0,那就cur++
之后将剩下的元素置为0
class Solution {
public void moveZeroes(int[] nums) {
int dest = 0;
for(int cur = 0 ; cur <nums.length; cur++) {
if(nums[cur] != 0) {
nums[dest++] = nums[cur];
}
}
for(; dest < nums.length; dest++ ) {
nums[dest] = 0;
}
}
}
复写零
思路:
如果 从前向后 进行原地复写操作的话,由于 0 复写两次,导致后面的一个数「被覆盖掉」。因此我们选择「从后往前」的复写策略。但是「从后向前」复写的时候,我们需要找到复写后的数组的最后一个数,因此我们的流程分两步:
i. 先找到复写后的数组的最后一个数;
ii. 然后从后向前进⾏复写操作。
过程:
- 初始化两个指针 cur = 0 , dest = -1;
- 找到最后⼀个复写的数:
当 cur < n 的时候,⼀直执行下面循环:
判断 cur 位置的元素:
- 如果是 0 的话, dest 往后移动两位;
- 否则, dest 往后移动⼀位。
判断 dest 是否已经到结束位置,如果结束就终止循环;如果没有结束, cur++ ,继续判断。
3.判断 dest 是否越界到 n 的位置:
如果越界,执行下⾯操作:
n - 1 位置的值修改成 0 ;
cur 向移动⼀步;
dest 向前移动两步。
4.从 cur 位置开始往前遍历原数组,依次还原出复写后的结果数组:
判断 cur 位置的值:
- 如果是 0 : dest 以及 dest - 1 位置修改成 0 , dest -= 2 ;
- 如果非零: dest 位置修改成 0 , dest -= 1 ;
cur-- ,复写下⼀个位置。
class Solution {
public void duplicateZeros(int[] arr) {
int cur = 0, dest = -1, n = arr.length;
//1.先找到最后一个需要复写的数
while(cur < n) {
if(arr[cur] == 0) dest += 2;
else dest += 1;
if(dest >= n-1) break;
cur++;
}
//2.处理一下边界情况
if(dest == n) {
arr[n-1] = 0;
cur--;
dest -= 2;
}
//3.从后往前完成复写操作
while(cur >= 0) {
if(arr[cur] != 0) arr[dest--] = arr[cur--];
else {
arr[dest--] = 0;
arr[dest--] = 0;
cur--;
}
}
}
}
快乐数
思路:
为了方便描述,将「将一个正整数替换为它每个位置上的数字的平方和」这⼀个操作记为 squareSum 操作
题目告诉我们,当我们不断重复 squareSum 操作,⼀定会「死循环」,最终有两种结果:
- 情况⼀:⼀直在 1 中死循环,1squareSum之后还是1
- 情况⼆:在历史的数据中死循环,但始终squareSum不到 1
由于上述两种情况只会出现⼀种,因此,只要我们能确定死循环是「情况⼀」,还是「情况⼆」,就能得到结果。
解法(快慢指针)
当重复执行squareSum 的时候,数据会陷入到⼀个「循环」之中。
而「快慢指针」有⼀个特性,就是在⼀个圆圈(循环)中,快指针总是会追上慢指针,也就是说速度相差一个元素时他们总会相遇在同⼀个位置。如果相遇位置的值是 1 ,那么这个数是快乐数;如果相遇位置不是 1的话,就不是快乐数。
PS:如何求⼀个数 n 每个位置上的数字的平⽅和。
a. 把 n 每⼀位的数提取出来:循环迭代下⾯步骤(直到 n 的值变为 0 ):
- t = n % 10 提取个位;
- n /= 10 ⼲掉个位;
b. 提取每⼀位的时候,⽤⼀个变量 sum 记录这⼀位的平方加上之前提取位数的平方和
sum = sum + t * t
class Solution {
public boolean isHappy(int n) {
int slow = n, fast = squareSum(n);
while(slow != fast) {
slow = squareSum(slow);
fast = squareSum(squareSum(fast));
}
return slow == 1;
}
public int squareSum(int n) {
int sum = 0;
while(n != 0) {
int t = n%10;
sum += t*t;
n /=10;
}
return sum;
}
}
盛水最多的容器
算法思路(对撞指针):
设两个指针 left , right 分别指向容器的左右端点,容器的左边界为 height[left] ,右边界为 height[right] 。此时容器的容积 :
v = (right - left) * min( height[right], height[left] )
我们假设「左边界」小于「右边界」。
此时我们固定⼀个边界,改变另⼀个边界,水的容积会有如下变化:
- 容器的宽度⼀定变小。
- 由于左边界较小,决定了水的高度。如果改变左边界,容器的容积可能会增大。
- 如果改变右边界,无论右边界移动到哪里,新的水面高度⼀定不会超过左边界,由于容器的宽度减小,因此容器的容积一定减少。
由此可见,左边界比右边界小的情况下算出容量后,左边界和其余边界的组合都可以舍去, left++ 跳过这个左边界,继续判断下⼀个左右边界。
当我们不断重复上述过程,每次都可以舍去大量不必要的枚举过程,直到 left 与 right 相
遇。期间产生的所有的容积里面的最大值,就是答案。
class Solution {
public int maxArea(int[] height) {
int left = 0, right = height.length-1, ret = 0;
while(left < right) {
ret = Math.max(ret, Math.min(height[left], height[right]) * (right-left));
if(height[left] < height[right]) left++;
else right--;
}
return ret;
}
}
不建议这么写:
class Solution {
public int maxArea(int[] height) {
int left = 0, right = height.length-1, ret = 0;
while(left < right) {
ret = height[left] < height[right] ?
Math.max(ret, (right-left) * height[left++] ):
Math.max(ret, (right-left) * height[right--] );
}
return ret;
}
}
(right-left)和height[left++]调换顺序会先执行自增就错了,而且换种语言的执行结果五花八门了
有效三角形的个数
算法思路(排序 + 双指针):
- 先将数组排序。
- 固定⼀个「最长边」,然后在比这条边小的有序数组中找出两个数之和大于这个最长边。由于数组是有序的,我们可以利用「对撞指针」来优化。
class Solution {
public int triangleNumber(int[] nums) {
//1.排序
Arrays.sort(nums);
//2.利用对撞指针
int ret = 0, n = nums.length;
//i固定最大数
for(int i = n-1; i >= 2; i--) {
int left = 0, right = i-1;
while(left < right) {
if(nums[left] + nums[right] > nums[i]) {
ret += right - left;
right--;
} else {
left++;
}
}
}
return ret;
}
}
查找总价格为目标值的两个商品
算法流程:
因为是升序数组,可以用对撞指针
- left , right 分别指向数组的左右两端
- 当 left < right 的时候,⼀直循环
-
当 nums[left] + nums[right] == target 时,记录结果,并且返回
-
当 nums[left] + nums[right] < target 时: left++
-
当 nums[left] + nums[right] > target 时, right - -
class Solution {
public int[] twoSum(int[] price, int target) {
int left = 0, right = price.length-1;
while(left < right) {
int sum = price[left] + price[right];
if(sum > target) right--;
else if(sum < target) left++;
else return new int[] {price[left], price[right]};
}
//照顾编译器
return new int[]{};
}
}
三数之和
算法思路(排序+双指针):
- 先排序;
- 然后固定⼀个数 i :
- 在这个数后面的区间内,使用「双指针算法」快速找到两个数之和等于 -i 。
但是要注意的是,这道题需要「去重」操作~
i. 找到⼀个结果之后, left 和 right 指针要「跳过重复」的元素;
ii. 当⼀次循环之后,固定的 i 也要「跳过重复」的元素。
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ret = new ArrayList<>();
//1.排序
Arrays.sort(nums);
//2.利用双指针解决问题
int n = nums.length;
for(int i = 0; i < n; ) {
if(nums[i] > 0) break;//优化
int left = i + 1, right = n - 1, target = -nums[i];
while(left < right) {
int sum = nums[left] + nums[right];
if(sum > target) right--;
else if(sum < target) left++;
else {
ret.add(new ArrayList<Integer>(Arrays.asList(nums[i], nums[left], nums[right])));
left++;
right--;
//去重
while(left < right && nums[left] == nums[left-1]) left++;
while(left < right && nums[right] == nums[right+1]) right--;
}
}
//去重i
i++;
while(i < n && nums[i] == nums[i-1]) i++;
}
return ret;
}
}
四数之和
算法思路(排序 + 双指针):
- 依次固定⼀个数 i ;
- 在 i 的后面区间上,利用「三数之和」找到三个数,使这三个数的和等于 target - i 即可。
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> ret = new ArrayList<>();
//1.排序
Arrays.sort(nums);
//2.利用双指针解决问题
int n = nums.length;
for(int i = 0; i < n; ) { //固定i
//三数之和
for(int j = i + 1; j < n; ){ //固定j
int left = j + 1, right = n - 1;
long aim = (long)target-nums[i]-nums[j];
while(left < right) {
int sum = nums[left] + nums[right];
if(sum > aim) right--;
else if(sum < aim) left++;
else {
ret.add(Arrays.asList(nums[i],nums[j], nums[left++], nums[right--]));
//去重
while(left < right && nums[left] == nums[left-1]) left++;
while(left < right && nums[right] == nums[right+1]) right--;
}
}
//去重j
j++;
while(j < n && nums[j] == nums[j-1]) j++;
}
//去重i
i++;
while(i < n && nums[i] == nums[i-1]) i++;
}
return ret;
}
}