常⻅的双指针有两种形式,⼀种是对撞指针,⼀种是左右指针。
对撞指针:⼀般⽤于顺序结构中,也称左右指针。
对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼近。
对撞指针的终⽌条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环),也就是:
快慢指针:⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列 结构上移动。这种⽅法对于处理环形链表或数组⾮常有⽤。 其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快慢指针的思想。快慢指针的实现⽅式有很多种,最常⽤的⼀种就是:
在本题中,我们可以⽤⼀个 cur 指针来扫描整个数组,另⼀个 dest 指针⽤来记录⾮零数序列的最后⼀个位置。根据 cur 在扫描的过程中,遇到的不同情况,分类处理,实现数组的划分。 在 cur 遍历期间,使 [0, dest] 的元素全部都是⾮零元素, [dest + 1, cur - 1] 的元素全是零。
class Solution {
public void moveZeroes(int[] nums) {
int cur = 0;
int dest = -1; // 指向非0的最后一位数
while(cur < nums.length) {
if(nums[cur] != 0) {
swap(nums, ++dest, cur);
}
cur++;
}
}
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
如果「从前向后」进⾏原地复写操作的话,由于 0 的出现会复写两次,导致没有复写的数「被覆盖掉」。因此我们选择「从后往前」的复写策略。
但是「从后向前」复写的时候,我们需要找到「最后⼀个复写的数」,因此我们的⼤体流程分两步:
i. 先找到最后⼀个复写的数;
ii. 然后从后向前进⾏复写操作。
class Solution {
public void duplicateZeros(int[] arr) {
// 1. 找到最后一个数
int cur = 0, dest = -1, 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;
cur--;
dest -= 2;
}
// 从后往前开始覆盖
while(cur >= 0) {
if(arr[cur] == 0) {
arr[dest--] = 0;
arr[dest --] = 0;
}else {
arr[dest--] = arr[cur];
}
cur--;
}
}
}
根据上述的题⽬分析,我们可以知道,当重复执⾏ x 的时候,数据会陷⼊到⼀个「循环」之中。⽽「快慢指针」有⼀个特性,就是在⼀个圆圈中,快指针总是会追上慢指针的,也就是说他们总会相遇在⼀个位置上。如果相遇位置的值是 1 ,那么这个数⼀定是快乐数;如果相遇位置不是 1的话,那么就不是快乐数。
class Solution {
public boolean isHappy(int n) {
// 快慢指针
int slow = n;
int fast = num(n);
while(slow != fast) {
slow = num(slow);
fast = num(fast);
fast = num(fast);
}
return slow == 1;
}
// 返回x这个数每一位上的平方
private int num(int x) {
int result = 0;
while(x > 0) {
result += (x % 10) * (x % 10);
x /= 10;
}
return result;
}
}
解法1:枚举遍历O(n^2),超时
解法2:对撞指针
设两个指针 left
,right
分别指向容器的左右两个端点,此时容器的容积 : v = (right - left) * min( height[right], height[left])
容器的左边界为 height[left]
,右边界为 height[right]
。
为了⽅便叙述,我们假设「左边边界」⼩于「右边边界」。
如果此时我们固定⼀个边界,改变另⼀个边界,⽔的容积会有如下变化形式:
由此可⻅,左边界和其余边界的组合情况都可以舍去。所以我们可以 left++ 跳过这个边界,继续去判断下⼀个左右边界。
当我们不断重复上述过程,每次都可以舍去⼤量不必要的枚举过程,直到 left 与 right 相遇。期间产⽣的所有的容积⾥⾯的最⼤值,就是最终答案。
class Solution {
public int maxArea(int[] height) {
// 利用单调性,使用对撞指针来解决问题
int left = 0;
int right = height.length - 1;
int max = 0;
while(left < right) {
if(height[left] < height[right]) {
max = Math.max(max, (right - left) * height[left]);
left++;
}else {
max = Math.max(max, (right - left) * height[right]);
right--;
}
}
return max;
}
}
class Solution {
public int triangleNumber(int[] nums) {
// 1. 排序
Arrays.sort(nums);
// 2. 利用单调性,使用双指针
// 先固定最大的数,在他的左区间使用双指针
int count = 0;
for(int i = nums.length - 1; i > 1; i--) {
int left = 0;
int right = i-1;
while(left < right) {
if(nums[left] + nums[right] > nums[i]) {
// left 到 right区间都是符合要求的可以直接跳过
count += right - left;
right--;
}else {
left++;
}
}
}
return count;
}
}
class Solution {
public int[] twoSum(int[] price, int target) {
int left = 0;
int right = price.length - 1;
int[] result = new int[2];
while(left < right) {
if(price[left] + price[right] > target) {
right--;
}else if(price[left] + price[right] < target) {
left++;
}else {
result[0] = price[left];
result[1] = price[right];
break;
}
}
return result;
}
}
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> result = new ArrayList<>();
for(int i = nums.length - 1; i >= 2;) {
int target = -nums[i];
// 变成求两数之和为target
int left = 0;
int right = i - 1;
while(left < right) {
int sum = nums[left] + nums[right];
if(sum < target) {
left++;
}else if(sum > target) {
right--;
}else {
result.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--;
// 去重
while(i >= 2 && nums[i] == nums[i+1]) i--;
}
return result;
}
}
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums);
int n = nums.length;
List<List<Integer>> result = new ArrayList<>();
for(int i = 0; i < n - 3;) {
// 三数之和
for(int j = i+1; j < n - 2;) {
// 大数处理
long tmp = (long)target - nums[i] - nums[j];
int left = j+1;
int right = n - 1;
while(left < right) {
int sum = nums[left] + nums[right];
if(sum > tmp) {
right--;
}else if(sum < tmp) {
left++;
}else {
result.add(new ArrayList<Integer>(Arrays.asList(nums[i], nums[j], nums[left], nums[right])));
left++;
right--;
while(left < right && nums[left] == nums[left - 1]) left++;
while(left < right && nums[right] == nums[right + 1]) right--;
}
}
j++;
while(j < n - 2 && nums[j] == nums[j - 1]) j++;
}
i++;
while(i < n - 3 && nums[i] == nums[i-1]) i++;
}
return result;
}
}