Pattern: two points, 双指针类型
介绍部分来自:https://www.zhihu.com/question/36738189/answer/908664455
双指针是这样的模式:两个指针朝着左右方向移动(双指针分为同向双指针和异向双指针),直到他们有一个或是两个都满足某种条件。双指针通常用在排好序的数组或是链表中寻找对子。比如,你需要去比较数组中每个元素和其他元素的关系时,你就需要用到双指针了。
我们需要双指针的原因是:如果你只用一个指针的话,你得来回跑才能在数组中找到你需要的答案。这一个指针来来回回的过程就很耗时和浪费空间了 — 这是考虑算法的复杂度分析的时候的重要概念。虽然brute force一个指针的解法可能会奏效,但时间复杂度一般会是O(n²)。在很多情况下,双指针能帮助我们找到空间或是时间复杂度更低的解。
上图是说,我们在排好序的数组里面找是否有一对数加起来刚好等于目标和
识别使用双指针的招数:
- 一般来说,数组或是链表是排好序的,你得在里头找一些组合满足某种限制条件
- 这种组合可能是一对数,三个数,或是一个子数组
以下题目经常因为 while(left <= right)
中用 < 号出错。忽略了重合时那个数的逻辑判断和进行操作。所以注意一下while中的判断条件。
public void twoPoint(数组或链表){
int left = 0; // 左指针 head low
int right = 0; // (同向) 右指针 tail high
// int right = 长度-1; // (异向)
// 结果
...
// 有时也会使用 for 循环 : 三数和问题
// while(right < 长度){ // (同向)
while(left <= right){ // 或者 (left < right) (异向)
// 逻辑判断左右指针的移动 和 结果的记录
...
}
}
经典题目:
1、Pair with Target Sum (easy)(异向双指针)
描述:
给定一个有序数组和一个目标和,在数组中找到一对和等于给定目标的数组,有就返回下标,没有就返回[-1,-1]。
例如:
s=[1,2,3,4,5,6,7,8],k=14,返回[5,7],也就是下标为5和下标为7的和为14:6+8=14。
public class TargetSum {
public static void main(String[] args) {
int[] t = {1,2,3,4,5,6,7,8};
System.out.println(Arrays.toString(targetSum(t, 14)));
}
public static int[] targetSum(int[] nums, int target){
int[] res = {-1, -1};
if (nums.length == 0)
return res;
int left = 0; // 左指针
int right = nums.length - 1; // 右指针
int sum = 0; // 和
while (left < right){
sum = nums[left] + nums[right];
if (sum < target){ // 小于目标和
left++; // 左指针右移
}else if (sum > target){ // 大于目标和
right--; // 右指针左移
}else{ // 等于目标和,返回下标
res[0] = left;
res[1] = right;
return res;
}
}
return res;
}
}
类似的:
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5
输出: 2
示例 2:
输入: [1,3,5,6], 2
输出: 1
示例 3:
输入: [1,3,5,6], 7
输出: 4
示例 4:
输入: [1,3,5,6], 0
输出: 0
使用二分法:
public static int searchInsert(int[] nums, int target) {
if (nums.length == 0)
return 0;
if (target > nums[nums.length - 1])
return nums.length;
if (target <= nums[0])
return 0;
int left = 0;
int right = nums.length - 1;
int mid = -1;
while(left <= right){
mid = (left + right) / 2;
if (target > nums[mid]){
left = mid + 1;
}else if (target < nums[mid]){
right = mid - 1;
}else{
return mid;
}
}
if(target>nums[mid]){
return mid+1;
}else{
return mid;
}
}
2、Remove Duplicates (easy)(同向双指针:快慢指针)
描述
给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
示例
示例 1:
给定数组 nums = [1,1,2],
函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,0,1,1,1,2,2,3,3,4],
函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。
你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
解答:
class Solution {
public int removeDuplicates(int[] nums) {
if (nums.length == 0)
return 0;
int low = 0; // 慢指针
int fast = 0; // 快指针
while (fast < nums.length){
// 移动快指针
if (nums[low] != nums[fast]){ // 不等于慢指针的值,继续移动
nums[low + 1] = nums[fast]; //
low++; // 移动慢指针
}
fast++;
}
return low + 1;
}
}
3、Squaring a Sorted Array (easy)
描述:
给定一个按非递减顺序排序的整数数组 A,返回每个数字的平方组成的新数组,要求也按非递减顺序排序。
示例:
示例 1:
输入:[-4,-1,0,3,10]
输出:[0,1,9,16,100]
示例 2:
输入:[-7,-3,2,3,11]
输出:[4,9,9,49,121]
提示:
1 <= A.length <= 10000
-10000 <= A[i] <= 10000
A 已按非递减顺序排序。
解法:
class Solution {
public int[] sortedSquares(int[] nums) {
if (nums.length == 0)
return null;
int start = 0; // 左指针
int end = nums.length - 1; // 右指针
int[] res = new int[nums.length]; // 结果数组
int index = nums.length - 1; // 结果坐标从后往前排
while (start <= end){
if (nums[start] * nums[start] > nums[end] * nums[end]){ // 左指针大
res[index] = nums[start] * nums[start];
start++;
}else{
res[index] = nums[end] * nums[end];
end--;
}
index--;
}
return res;
}
}
4、Triplet Sum to Zero (medium)
描述:
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
解答:
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
if (nums.length < 3)
return res;
Arrays.sort(nums); // 先进行排序
int len = nums.length;
int head; // 头指针,从数组后开始遍历
int tail; // 尾指针,从数组前开始遍历
for(int i = 0; i < len-2; i++){ // len -2 保证有三个数
int target = -nums[i]; // 第一个数到倒数第三个数作为保证遍历完毕(去负数,只要和另外两个数相加为0即可)
head = i + 1; // 头指针从下一个数开始
tail = len - 1; // 尾指针从最后一个数开始
while(head < tail){ // 进行试探
if(nums[head] + nums[tail] < target) head++; // 左边的数比较小
else if(nums[head] + nums[tail] > target) tail--; // 右边的数比较大
else{ // 相加为0
List<Integer> temp = new ArrayList<>(3);
temp.add(nums[i]);
temp.add(nums[head]);
temp.add(nums[tail]);
res.add(temp); // 存放结果
while(head + 1 < tail && nums[head+1] == nums[head]) head++; // 防止头指针值重复,如 10,10,10 但10已经添加过了,跳过去
while(tail - 1 > head && nums[tail-1] == nums[tail]) tail--; // 防止尾指针值重复
head++;
tail--;
}
while(i + 1 < len - 2 && nums[i+1] == nums[i]) i++; // 防止基数重复
}
}
return res;
}
}
5、Triplet Sum Close to Target (medium)
描述:
给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。
示例:
输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。
class Solution {
public int threeSumClosest(int[] nums, int target) {
if (nums.length < 3)
return 0;
Arrays.sort(nums); // 进行排序
int res = nums[0] + nums[1] + nums[2]; // 结果
int head; // 头指针
int tail; // 尾指针
for (int i = 0; i < nums.length - 2; i++) { // 0 - length-2 保证三个数。
int baseNum = nums[i]; // 基础值
head = i + 1; // 从当前的下一个开始往后
tail = nums.length - 1; // 从最后一个开始往回
while (head < tail){ // 循环条件
int threeSum = nums[head] + nums[tail] + baseNum; // 记录总和
if (threeSum > target){ // 比结果大
tail--;
}else if (threeSum < target) { // 比结果小
head++;
}else { // 相等
return threeSum;
}
// 结果处理
if (Math.abs(threeSum - target) < Math.abs(res - target)) {
res = threeSum;
}
}
}
return res;
}
}
6、Triplets with Smaller Sum (medium)
7、Subarrays with Product Less than a Target (medium)
描述:
给定一个正整数数组 nums。
找出该数组内乘积小于 k 的连续的子数组的个数。
示例 1:
输入: nums = [10,5,2,6], k = 100
输出: 8
解释: 8个乘积小于100的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于100的子数组。
思路:
双指针法,如果一个子串的乘积小于k,那么他的每个子集都小于k,而一个长度为n的数组,他的所有连续子串数量是1+2+...+n
,但是会和前面的重复。比如例子中[10, 5, 2, 6]
,第一个满足条件的子串是[10]
,第二个满足的是[10, 5]
,但是第二个数组的子集[10]
和前面的已经重复了,因此我们只需要计算包含最右边的数字的子串数量,就不会重复了,也就是在计算[10, 5]
这个数组的子串是,只加入[5]
和[10, 5]
,而不加入[10]
,这部分的子串数量刚好是right - left + 1
, 如果大于目标值,直接除以left
指针的值,即可继续比较。
class Solution {
public int numSubarrayProductLessThanK(int[] nums, int k) {
if (nums.length == 0 || k == 0 || k == 1)
return 0;
int res = 0;
int left = 0; // 头指针
int right = 0; // 尾指针
int pops = 1; // 保存乘积值
while (right < nums.length){
pops *= nums[right]; // 乘积
while (pops >= k){ // 乘积小于 k
pops /= nums[left]; // 除以左指针的值
left++; // 并将左指针向右移
}
res += right - left + 1; // 记录结果,一个长度为n的数组,他的所有连续子串数量是 1+2+...+n
right++; // 右指针移动
}
return res;
}
}
8、Dutch National Flag Problem (medium)
描述:
给定数组中只有“1”,“2”,“3”三种数字,且个数不等,进行排序。最终结果的顺序为:所有的1在前,所有的2在中间,所有的3在后面。
示例:
原数组:1232313231,排序后:1112223333
public class DutchNationalFlagProblem {
public static void main(String[] args) {
int[] a = {1, 2, 3, 2, 3, 1, 3, 2, 3, 1};
dutchNationalFlagProblem(a);
System.out.println(Arrays.toString(a));
}
public static void dutchNationalFlagProblem(int[] a){
if (a.length == 0)
return;
int red = 0; // 红色区域指针
int white = 0; // 白色区间指针,遍历的指针
int blue = a.length - 1; // 蓝色区间指针
while (white < blue){
int v = a[white]; // 获取当前值
if (v == 1){ // 红色区间的
a[white] = a[red]; // 将红色区间指针的值转移到白色区间
a[red] = v; // 移到红色区间
white++; // 白色指针右移,继续遍历
red++; // 红色区间右移
}
else if (v == 2){ // 白色区间的,不需要移动
white++; // 白色指针右移,继续遍历
}else{ // 蓝色区间的
a[white] = a[blue]; // 将蓝色区间指针的值转移到白色区间
a[blue] = v; // 移到蓝色区间
blue--; // 蓝色区间左移
}
}
}
}