双指针类型(two points)

Pattern: two points, 双指针类型

介绍部分来自:https://www.zhihu.com/question/36738189/answer/908664455

双指针是这样的模式:两个指针朝着左右方向移动(双指针分为同向双指针和异向双指针),直到他们有一个或是两个都满足某种条件。双指针通常用在排好序的数组或是链表中寻找对子。比如,你需要去比较数组中每个元素和其他元素的关系时,你就需要用到双指针了。

我们需要双指针的原因是:如果你只用一个指针的话,你得来回跑才能在数组中找到你需要的答案。这一个指针来来回回的过程就很耗时和浪费空间了 — 这是考虑算法的复杂度分析的时候的重要概念。虽然brute force一个指针的解法可能会奏效,但时间复杂度一般会是O(n²)。在很多情况下,双指针能帮助我们找到空间或是时间复杂度更低的解。

img

上图是说,我们在排好序的数组里面找是否有一对数加起来刚好等于目标和

识别使用双指针的招数:

  • 一般来说,数组或是链表是排好序的,你得在里头找一些组合满足某种限制条件
  • 这种组合可能是一对数,三个数,或是一个子数组

以下题目经常因为 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;
    }
}

类似的:

35. 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

你可以假设数组中无重复元素。

示例 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)(同向双指针:快慢指针)

26. 删除排序数组中的重复项

描述

给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 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)

977. 有序数组的平方

描述:

给定一个按非递减顺序排序的整数数组 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)

15. 三数之和

35.三数之和关注问题

描述:

给你一个包含 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)

59.最接近的三数之和

16. 最接近的三数之和

描述:

给定一个包括 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)

918.三数之和

7、Subarrays with Product Less than a Target (medium)

1075.乘积小于K的子数组

713. 乘积小于K的子数组

描述:

给定一个正整数数组 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--;                 // 蓝色区间左移
            }
        }
    }
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值