双指针问题

双指针问题基础知识

在这里插入图片描述

同向双指针

快慢指针

寻找环
寻找某个元素
寻找重复元素

尺取法

反向双指针

首尾指针

167. 两数之和 II - 输入有序数组
使用两个指针,初始分别位于第一个元素和最后一个元素位置,比较这两个元素之和与目标值的大小。如果和等于目标值,我们发现了这个唯一解。如果比目标值小,我们将较小元素指针增加一。如果比目标值大,我们将较大指针减小一。移动指针后重复上述比较知道找到答案。
模板:


		int first = 0;
		int last = numbers.length - 1;
		while(first<last){
			if(满足条件){
				return
			}else if(条件1){
				first++;
			}else if(条件2{
				last--;
			}
		}
class Solution{
	 public int[] twoSum(int[] numbers, int target) {
	 	if(numbers == null || numbers.length == 0){
			return null;
		}
		int first = 0;
		int last = numbers.length - 1;
		while(first<last){
			if(numbers[first] + numbers[last] == target){
				return new int[]{first+1,last+1};
			}else if(numbers[first] + numbers[last] < target){
				first++;
			}else{
				last--;
			}
		}
		return null;
	}
}

解题思路与原理(转载:作者:nettee,来源:力扣(LeetCode))

分离双指针

例题总结

有序矩阵中第K小的元素

给定一个 n x n 矩阵,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是排序后的第 k 小元素,而不是第 k 个不同的元素。

matrix = [
   [ 1,  5,  9],
   [10, 11, 13],
   [12, 13, 15]
],
k = 8,

返回 13。

可以利用类双指针的方法来对本题进行解答,传统的双指针方法如下:

  • 符合条件1,左边指针动一下,符合条件2,右边指针动一下,直到两个指针相遇,满足条件
  • 这种方法也叫尺取法,可以通过一次遍历将所需的答案算出来。
  • 使用这种方法的核心是操作必须可以通过一次遍历解决,即可以根据条件移动左右

在本题中,由于从左上到右下是递增分布的,因此,可以使用类双指针对于符合条件的值从左下到右上,进行计数,进行此种一维方式的遍历,再配合二分查找,每次进行计数即可。(题解)

    public int kthSmallest(int[][] matrix, int k) {
        int len=matrix.length-1;
        int left=matrix[0][0];
        int right= matrix[len][len];
        while(left<right){
            //夹逼停止条件left==right,
            int mid=(left+right)/2;
            //自带向下取整
            if(find(matrix,mid,len,k)){
                //代表需要向左进行取整了
                right=mid;
            }else{
                left=mid+1;
                //二分查找自带向左取整,因此使用右侧数+1破除这个影响
            }
        }
        return(left);
    }
    public boolean find(int[][] matrix,int mid,int len,int k){
        //想清楚边界条件,如果mid就是满足条件的情况则计数=k;此时返回的是true
        //返回true后代表right=mid,操作结果会不断循环right=mid,从而不断的向左靠
        //直到right=mid+1;left=mid,right+left/2=mid;
        int i=len;
        int j=0;
        int num=0;
        //从左下到右上进行单项操作
        while(i>=0&&j<=len){
            if(matrix[i][j]<=mid){
                //根据题意,同等数字也算作不大于情况,算在左边因此此处取等号
                j++;
                num=num+i+1;//这一整列的数都不大于目标数mid;
            }else{
                i--;
            }
        }
        return num>=k;
    }

此题有几个需要注意的点:

  1. 二分查找,注意等于情况算在左边,因为右边left=mid+1就相当于直接错过了
  2. 计数情况要想清楚,比如本题的num=i+1这代表了本列有多少存量
  3. 本题中相同数字是怎么解决的,比如这里有两个13,一个是第7小,一个是第8小
mid=8,left=1,right=15
 2
mid=12,left=9,right=15
 6
mid=14,left=13,right=15
 8
mid=13,left=13,right=14
 8
left=13,right=13

是通过夹逼法解决的例如,想要找到第7小的元素,自始至终,都没有计数到过7(因为13有多个)
而是通过收敛,前指针等于后指针等于13解决的。
这个可以理解为一直大于7一直向左移动,而左边的点又小于7,最终找到了6到8之间的那个点。

子数组和排序后的区间和

给你一个数组 nums ,它包含 n 个正整数。你需要计算所有非空连续子数组的和,并将它们按升序排序,得到一个新的包含 n * (n + 1) / 2 个数字的数组。

请你返回在新数组中下标为 left 到 right (下标从 1 开始)的所有数字和(包括左右端点)。由于答案可能很大,请你将它对 10^9 + 7 取模后返回。

输入:nums = [1,2,3,4], n = 4, left = 1, right = 5
输出:13 
解释:所有的子数组和为 1, 3, 6, 10, 2, 5, 9, 3, 7, 4 。将它们升序排序后,我们得到新的数组 [1, 2, 3, 3, 4, 5, 6, 7, 9, 10] 。下标从 le = 1 到 ri = 5 的和为 1 + 2 + 3 + 3 + 4 = 13 。

相当于上一题的变种题,也需要弄一个三角矩阵然后进行计数,关键是要找到这一条等高线(和上题思路一样)
在这里插入图片描述
这个滑动方式应该是怎么样的?

  • 毋庸置疑,应该找出两个滑动方向,一个方向确定会变小,一个方向确定会变大,按需滑动
  • 符合这一条件的只有右下角的点,如果当前元素大于目标,则向左上移动,如果小于目标,则向上移动
  • 直到找到符合条件的那条线(条件就是,在线以下有多少元素)
  • 如果有相同元素这种办法怎么进行处理,比如上图中有三个15,两个18,上一题中的方法不行,本次进行二次判断?确实,分为两部分,严格小于x的数目与等于x的数目
  • 双指针的本质在于一次循环,而不是左右移动,就比如在本题中,记录j的指针,可以在对横向一次循环的情况下,记录到底有多少点满足条件
  • 与上一题不同的是,本题不是那么严格的序列,但可以保证的是i与j的数值不会减少,这就满足了一次循环的条件
class Solution {
    static final int MODULO = 1000000007;

    public int rangeSum(int[] nums, int n, int left, int right) {
        int[] prefixSums = new int[n + 1];
        prefixSums[0] = 0;
        for (int i = 0; i < n; i++) {
            prefixSums[i + 1] = prefixSums[i] + nums[i];
        }
        int[] prefixPrefixSums = new int[n + 1];
        prefixPrefixSums[0] = 0;
        for (int i = 0; i < n; i++) {
            prefixPrefixSums[i + 1] = prefixPrefixSums[i] + prefixSums[i + 1];
        }
        return (getSum(prefixSums, prefixPrefixSums, n, right) - getSum(prefixSums, prefixPrefixSums, n, left - 1)) % MODULO;
    }

    public int getSum(int[] prefixSums, int[] prefixPrefixSums, int n, int k) {
        int num = getKth(prefixSums, n, k);
        //获知第K个元素到底是什么
        int sum = 0;
        int count = 0;
        for (int i = 0, j = 1; i < n; i++) {
            while (j <= n && prefixSums[j] - prefixSums[i] < num) {
                j++;
            }
            j--;
            sum = (sum + prefixPrefixSums[j] - prefixPrefixSums[i] - prefixSums[i] * (j - i)) % MODULO;
            count += j - i;
        }
        sum = (sum + num * (k - count)) % MODULO;
        return sum;
    }

    public int getKth(int[] prefixSums, int n, int k) {
    //用于获取第K个元素
        int low = 0, high = prefixSums[n];
        while (low < high) {
            int mid = (high - low) / 2 + low;
            int count = getCount(prefixSums, n, mid);
            //用于对小于等于的情况进行计数
            if (count < k) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        return low;
    }

    public int getCount(int[] prefixSums, int n, int x) {
    //这完全就是粗暴的乱取啊,有什么关系吗?
    //实际上这就是双指针,因为双指针的核心不是左右移动,而是一次遍历,而非循环遍历
        int count = 0;
        for (int i = 0, j = 1; i < n; i++) {
            while (j <= n && prefixSums[j] - prefixSums[i] <= x) {
                j++;
            }
            j--;
            count += j - i;
        }
        return count;
    }
}

非常艰难的做完

    public int[] nums;
    public int[] sum;
    public int len;
    public int mod=1000000007;
    public  int getmatrix(int x,int y){
        //本函数使用前缀和对于矩阵种的元素进行计算,并且本函数包含首尾x,y
        return sum[y]-sum[x]+nums[x];
    }

    public int[] find(int mid,int k){
        int sum=0;
        int mi=0;
        int eq=0;
        //此函数用于寻找出比mid小的值又多少个,并且返回计数
        for(int i=0,j=0;i<=len;i++){
            //这里的j可以理解为j始终指向那个比他大一个的数
            int ep=0;
            //使用while循环是对上下文记录的一种方式,记录上一次j循环到了哪里
            while(j<=len&&getmatrix(i,j)<=mid){
                int a=getmatrix(i,j);
                if(getmatrix(i,j)==mid) {
                    ep = ep + 1;
                }
                j++;
            }

            j--;//代表了本次有收入
            mi=mi+j-i-ep+1;
            eq=eq+ep;
            if(j!=-1){
            for(int kk=i;kk<=j-ep;kk++){
                sum=(sum+getmatrix(i,kk))%mod;
            }}else{
                j++;
            }
            //相等的情况,去除equal,对于本行进行的计算
            //放到最后再加还是有其和理性的
        }
        return(new int[]{sum,mi,eq});
    }

    public int getsum(int k){
        if(k==0){
            return(0);
        }

        //k可以认为是目标排序的位置
        int st=0;
        int en=getmatrix(0,len);

        while(st<=en){
            int mid=(st+en)/2;
            int[] re=find(mid,k);//sum,mi,eq;
            if(re[1]<=k&&re[1]+re[2]>=k){
                if(re[1]==k){
                    return(re[0]);
                }else{
                    return(re[0]+(k-re[1])*mid);
                }
            }else{
                if(re[1]+re[2]<k){
                    //数选的太小达不到k
                    st=mid+1;
                }else if(re[1]>k){
                    en=mid;
                }
            }
        }
        return 0;
    }
    public  int rangeSum(int[] nums, int n, int left, int right) {


        this.nums=nums;
        this.sum=new int[nums.length];
        this.sum[0]=nums[0];
        this.len=nums.length-1;
        for(int i=1;i< nums.length;i++){
            sum[i]=sum[i-1]+nums[i];
        }
        int a=(getsum(left-1))%mod;
        int b=(getsum(right))%mod;
        //System.out.println(b);
        //出现的第一个问题就是边界如何被考虑的问题,如何记录上边界数组?
        //简单,直接left-1不就行了,从根源上考虑问题
        return(b-a)%mod;
    }

启发:

  • while循环可以很好的保存一些中间状态,可以与for循环搭配使用,for循环用于顺序遍历,while用于条件便利,它的优势是可以对于一些状态进行记录。
  • 对于上一点,可以将while循环理解为:对于当前状态进行遍历,直到满足某一个状态
  • 想清楚指针代表的到底是什么,比如find中的j指针,代表的意思就是指向当前指针,并且包含当前指针,这就需要考虑不满足要求的情况,比如一个都不包含的情况。
  • 为什么在本函数中的二分查找部分需要考虑st=en的情况?:因为在本题中是在while循环中进行的判断,虽然最后左右一定会相等,但不进入循环进行计算,因此,相当于没有计算结果直接return0了

找出第 k 小的距离对

给定一个整数数组,返回所有数对之间的第 k 个最小距离。一对 (A, B) 的距离被定义为 A 和 B 之间的绝对差值。

输入:
nums = [1,3,1]
k = 1
输出:0 
解释:
所有数对如下:
(1,3) -> 2
(1,1) -> 0
(3,1) -> 2
因此第 1 个最小距离的数对是 (1,1),它们之间的距离为 0。

本题不要求顺序,和上一个题一样,构建一个三角矩阵,然后双指针

    public int len;
    public int[] nums;
    public int getmatrix(int i,int j){
        return nums[j]-nums[i];
    }
    public int find(int mid){
        int sum=0;
        //这个函数用于看看在二维矩阵中有多少内容是小于mid的
        for(int i=0,j=1;i<=len;i++){
            //考虑清楚有多少元素,考虑清楚上下界
            while(j<=len&&getmatrix(i,j)<=mid){
                //满足这个条件则进入循环,因此会导致最后的多加一数
                j++;
            }
            j--;
            if(j!=i){
                sum=sum+j-i;
            }else{
                j++;
            }
        }
        return sum;
    }
    public int getnum(int k){
        //这个函数用于进行二分查找
        int left=0;//最小
        int right=getmatrix(0,len);//最大
        while(left<right){
            int mid=(left+right)/2;
            int num=find(mid);
            if(num>=k){
                right=mid;
            }else{
                left=mid+1;
            }
        }
        return(left);
    }
    public int smallestDistancePair(int[] nums, int k) {
        //System.out.println(nums.length*(nums.length-1)/2);
        Arrays.sort(nums);
        this.len=nums.length-1;
        this.nums=nums;
        return getnum(k);
    }

本题需要注意的点:

  • 将二分查找左右情况思考好
  • 二分查找起点不好找的话就设置为0,因为样本是全正数

904. 水果成篮

在一排树中,第 i 棵树产生 tree[i] 型的水果。
你可以从你选择的任何树开始,然后重复执行以下步骤:

把这棵树上的水果放进你的篮子里。如果你做不到,就停下来。
移动到当前树右侧的下一棵树。如果右边没有树,就停下来。
请注意,在选择一颗树后,你没有任何选择:你必须执行步骤 1,然后执行步骤 2,然后返回步骤 1,然后执行步骤 2,依此类推,直至停止。

你有两个篮子,每个篮子可以携带任何数量的水果,但你希望每个篮子只携带一种类型的水果。
用这个程序你能收集的水果树的最大总量是多少?

        //原理上和尺取法看书的题是一样的,在不能选择的时候把之前的水果都弹出来
        //做这种题不能乱想,必须把所有情况想清楚再动手
        //想清楚初始情况
        int[] mem_st=new int[]{tree[0],1};
        int[] mem_en=new int[]{-1,0};
        int start=1;int start_num=0;
        while(start< tree.length&&tree[start]==tree[0]){
            mem_st[1]=mem_st[1]+1;
            start=start+1;
        }
        if(start==tree.length){
            return(mem_st[1]);
        }
        mem_en[0]=tree[start];
        mem_en[1]=1;
        int fin=0;start++;
        //向后进行循环
        for(int i=start;i< tree.length;i++){
            if(tree[i]==mem_en[0]){
                mem_en[1]=mem_en[1]+1;

            }else if(tree[i]==mem_st[0]){
                mem_st[1]=mem_st[1]+1;
            }else{
                int back=i-1;int count=0;
                while(back>=0&&tree[back]==tree[i-1]){
                    count++;
                    back=back-1;
                }
                fin=Math.max(fin,mem_st[1]+mem_en[1]);
                mem_st[0]=tree[i-1];
                mem_st[1]=count;
                mem_en[0]=tree[i];
                mem_en[1]=1;
            }

        }
        fin=Math.max(fin,mem_st[1]+mem_en[1]);
        return fin;
    }

本题需要注意的点:

  • 思考这类问题的时候不要胡思乱想设计一个自动机来进行思考,就比如本题中可以分为拥有两个状态的自动机,吸收状态,与停止转换状态。
  • 使用自动机的时候记得想清楚初始状态,比如本题的初始状态就是计算满足条件的两个篮子
  • 弄清题意,分清什么时候可以吸收,什么时候不行。

881. 救生艇

第 i 个人的体重为 people[i],每艘船可以承载的最大重量为 limit。

每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit。

返回载到每一个人所需的最小船数。(保证每个人都能被船载)。

输入:people = [1,2], limit = 3
输出:1
解释:1 艘船载 (1, 2)
class Solution {
    //从最重的开始装,因为如果最重的不开船则没有任何方式能将它运走
    //先装重的,因为轻的可以合并为重的
    //思考一种情况:装两个稍小的比装一个大的好,需要的船更少,这种情况是存在的
    //这就是之前那个两数之和的变种,什么时候动左指针,什么时候动右指针,来回动就可
    //注意看题,每船最多可以乘坐两个人
    public static int numRescueBoats(int[] people, int limit) {
        //对于while循环,思考一下数据进循环是什么样,出循环又是几种情况
        Arrays.sort(people);
        int fin=0;
        int start=0;int end=people.length-1;
        while(start<end){
            if(people[start]>limit- people[end]){
                fin=fin+1;
                end--;
                //只能放下最后一个的情况
            }else{
                fin=fin+1;
                end--;
                start++;
            }
        }
        if(start==end){
            fin=fin+1;
        }
        return fin;
    }
}

注意:

  • 注意审题,是要求每个船只能坐两名乘客
  • 注意while循环的进出情况,最好找个数列自己看一看

1711. 大餐计数

大餐 是指 恰好包含两道不同餐品 的一餐,其美味程度之和等于 2 的幂。

你可以搭配 任意 两道餐品做一顿大餐。

给你一个整数数组 deliciousness ,其中 deliciousness[i] 是第 i​​​​​​​​​​​​​​ 道餐品的美味程度,返回你可以用数组中的餐品做出的不同 大餐 的数量。结果需要对 109 + 7 取余。

注意,只要餐品下标不同,就可以认为是不同的餐品,即便它们的美味程度相同。

输入:deliciousness = [1,3,5,7,9]
输出:4
解释:大餐的美味程度组合为 (1,3) 、(1,7) 、(3,5) 和 (7,9) 。
它们各自的美味程度之和分别为 4 、8 、8 和 16 ,都是 2 的幂。

    //从头开始循环,从2开始一直到2^20结束,时间复杂度为20,再使用双指针看看有多少满足要求的,注意0不在其中
    //需要进行归并,然后分情况讨论,分为相同元素和不同元素

    public static int countPairs(int[] deliciousness) {
        Arrays.sort(deliciousness);
        int fin=0;
        int mo=(int)(1e9+7);
        TreeMap<Integer,Integer> map=new TreeMap<>();
        for(int i=0;i< deliciousness.length;i++){
            int tt=deliciousness[i];
            if(map.containsKey(tt)){
                int tem=map.get(tt);
                map.put(tt,tem+1);
            }else{
                map.put(tt,1);
            }
        }
        Integer[] del=(Integer[]) map.keySet().toArray(new Integer[map.keySet().size()]);
        for(int i=0;i<=22;i++){
            int tem_des=(int) Math.pow(2,i);
            int start=0;
            int end=del.length-1;
            while(start<end){
                if(del[start]+del[end]>tem_des){
                    //大了
                    end--;
                }else if(del[start]+del[end]<tem_des){
                    //小了
                    start++;
                }else{
                    int a=del[start];
                    int b=del[end];
                    start++;
                    end--;
                    fin=(fin+map.get(a)*map.get(b))%mo;
                }
                //由于是求两数之和,因此再最后不需要考虑前后指针的交叉情况;
            }
            for(int j=0;j<del.length;j++){
                if(map.get(del[j])>=2&&del[j]*2==tem_des){
                    long pp=map.get(del[j]);
                    long b=(pp*(pp-1)/2)%mo;
                    fin=(int)((fin+b)%mo);
                }
            }
        }
        return(fin);
    }
  • 这题不难,但是挖的坑太多了,首先是不同元素算是不同的菜类
  • 其次注意int的范围,都先转换为long然后最后再转换成int返回
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值