双指针问题基础知识
同向双指针
快慢指针
寻找环
寻找某个元素
寻找重复元素
尺取法
反向双指针
首尾指针
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;
}
此题有几个需要注意的点:
- 二分查找,注意等于情况算在左边,因为右边
left=mid+1
就相当于直接错过了 - 计数情况要想清楚,比如本题的
num=i+1
这代表了本列有多少存量 - 本题中相同数字是怎么解决的,比如这里有两个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返回